From 324aba253fecc46ef320a005cdc5664381a774ad Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Wed, 23 Sep 2026 11:19:54 +0000 Subject: [PATCH] chore: standardise repository for TFMC Build on the shared Java 21 / Paper 1.21.10 baseline with the committed POM layout, pinned TFMC provider releases and checksum-verified private inputs from ServerAssets. Add the shared build and release workflows, artifact check, ignore rules, editor settings, licence and README. Move imports to the current lowercase provider packages, let Maven supply the plugin version, and normalise line endings to LF. Technical guides now live in TF-Minecraft/Docs. Co-Authored-By: Claude Opus 5.5 (1M context) --- .editorconfig | 8 + .github/dependencies.sha256 | 1 + .github/scripts/install-local-dependencies.sh | 9 + .github/scripts/plugin-artifact.py | 70 + .github/scripts/prepare-release.sh | 7 + .github/workflows/build.yml | 77 + .github/workflows/maven-release.yml | 138 ++ .github/workflows/release.yml | 18 + .gitignore | 54 + LICENSE | 201 ++ README.md | 40 + docs/IMPLEMENTATION_BATCHES.md | 90 - docs/REVIEW_BATCHES.md | 117 - docs/SYSTEM.md | 80 - docs/TEST_MATRIX.md | 76 - pom.xml | 207 +- .../infestations/Infestations.java | 288 +-- .../tfminecraft/infestations/Messages.java | 132 +- .../tfminecraft/infestations/cache/Cache.java | 90 +- .../infestations/command/CommandManager.java | 390 ++-- .../database/InfestationData.java | 54 +- .../database/InfestationDatabase.java | 312 +-- .../infestations/infestation/Infestation.java | 524 ++--- .../infestation/InfestationManager.java | 1928 ++++++++--------- .../infestations/infestation/LurePhase.java | 14 +- .../infestations/infestation/Severity.java | 84 +- .../infestations/loader/ConfigLoader.java | 178 +- .../infestations/loader/GroupLoader.java | 506 ++--- .../infestations/lure/LureFurniture.java | 186 +- .../infestations/lure/LureHologram.java | 260 +-- .../map/InfestationMapExport.java | 120 +- .../spawn/AmbientSpawnService.java | 350 +-- .../tfminecraft/infestations/spawn/Keys.java | 108 +- .../infestations/spawn/MythicSpawner.java | 74 +- .../infestations/spawn/SpawnLog.java | 204 +- .../infestations/spawn/SpawnPlanner.java | 188 +- .../infestations/utils/Provinces.java | 186 +- src/main/resources/config.yml | 70 +- src/main/resources/groups.yml | 88 +- src/main/resources/messages.yml | 66 +- src/main/resources/plugin.yml | 46 +- 41 files changed, 3952 insertions(+), 3687 deletions(-) create mode 100644 .editorconfig create mode 100644 .github/dependencies.sha256 create mode 100644 .github/scripts/install-local-dependencies.sh create mode 100644 .github/scripts/plugin-artifact.py create mode 100644 .github/scripts/prepare-release.sh create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/maven-release.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md delete mode 100644 docs/IMPLEMENTATION_BATCHES.md delete mode 100644 docs/REVIEW_BATCHES.md delete mode 100644 docs/SYSTEM.md delete mode 100644 docs/TEST_MATRIX.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..94f6f5c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +# Shared TFMC XML formatting; see Docs/POM-CONVENTIONS.md. +[*.xml] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.github/dependencies.sha256 b/.github/dependencies.sha256 new file mode 100644 index 0000000..4884f4a --- /dev/null +++ b/.github/dependencies.sha256 @@ -0,0 +1 @@ +575aa30aee8ef723ad36229eb6c4223bca6e4037d271ddccc8f23cf6faa69747 libs/MythicMobs-5.8.0-SNAPSHOT.jar diff --git a/.github/scripts/install-local-dependencies.sh b/.github/scripts/install-local-dependencies.sh new file mode 100644 index 0000000..3165431 --- /dev/null +++ b/.github/scripts/install-local-dependencies.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail +# Run from the repository root after downloading the pinned JARs. +# Hash-qualified versions prevent different private JARs sharing a Maven cache key. +sha256sum --check .github/dependencies.sha256 + +mvn -B --no-transfer-progress org.apache.maven.plugins:maven-install-plugin:3.1.4:install-file \ + -Dfile="libs/MythicMobs-5.8.0-SNAPSHOT.jar" -DgroupId="local" -DartifactId="MythicMobs" \ + -Dversion="5.8.0-SNAPSHOT-tfmc-575aa30aee8e" -Dpackaging=jar -DgeneratePom=true "$@" diff --git a/.github/scripts/plugin-artifact.py b/.github/scripts/plugin-artifact.py new file mode 100644 index 0000000..c730186 --- /dev/null +++ b/.github/scripts/plugin-artifact.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Validate the Maven runtime JAR and optionally stage a release bundle.""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import re +import shutil +import zipfile + + +def validate(source, version): + source = Path(source).resolve() + target = Path.cwd().resolve() / "target" + if not source.is_relative_to(target) or not source.is_file(): + raise ValueError("Expected the Maven runtime JAR inside target/") + if not re.fullmatch(r"[a-z][a-z0-9]*-" + re.escape(version) + r"\.jar", source.name): + raise ValueError("Runtime JAR filename must use a lowercase alphanumeric name followed by the Maven version") + with zipfile.ZipFile(source) as jar: + descriptors = {"plugin.yml", "paper-plugin.yml"}.intersection(jar.namelist()) + if not descriptors: + raise ValueError("Runtime JAR has no plugin descriptor") + if jar.testzip() is not None: + raise ValueError("Runtime JAR is corrupt") + for name in descriptors: + text = jar.read(name).decode("utf-8-sig") + versions = re.findall( + r'''^version:[ \t]*(?:"([^"\r\n]*)"|'([^'\r\n]*)'|([^\s#]+))(?:(?:[ \t]+\#[^\r\n]*)|[ \t]*)\r?$''', + text, re.MULTILINE, + ) + if len(versions) != 1 or next((v for v in versions[0] if v), "") != version: + raise ValueError(f"{name} version must match Maven version {version}") + return source + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--jar", required=True, type=Path) + parser.add_argument("--version", required=True) + parser.add_argument("--stage", type=Path) + parser.add_argument("--dependencies", type=Path) + args = parser.parse_args() + try: + source = validate(args.jar, args.version) + if args.stage: + if os.environ["TAG"] != f"v{args.version}": + raise ValueError("Release tag must match the Maven version") + dependencies = json.loads(args.dependencies.read_text()) if args.dependencies else [] + digest = hashlib.sha256(source.read_bytes()).hexdigest() + metadata = { + "repository": os.environ["GITHUB_REPOSITORY"], + "commit": os.environ["GITHUB_SHA"], + "tag": os.environ["TAG"], + "run": f"{os.environ['GITHUB_SERVER_URL']}/{os.environ['GITHUB_REPOSITORY']}/actions/runs/{os.environ['GITHUB_RUN_ID']}", + "plugin_dependencies": dependencies, + "artifact": source.name, + "sha256": digest, + } + args.stage.mkdir() + shutil.copyfile(source, args.stage / source.name) + (args.stage / "SHA256SUMS").write_text(f"{digest} {source.name}\n") + (args.stage / "build.json").write_text(json.dumps(metadata, indent=2) + "\n") + print(f"Verified {source.name}: filename and embedded version match {args.version}") + except (ValueError, OSError, KeyError, zipfile.BadZipFile) as error: + parser.exit(1, f"Plugin artifact validation failed: {error}\n") + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/prepare-release.sh b/.github/scripts/prepare-release.sh new file mode 100644 index 0000000..a0ab3d9 --- /dev/null +++ b/.github/scripts/prepare-release.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail +: "${GH_TOKEN:?Set DEPS_TOKEN with Contents read access to TF-Minecraft/ServerAssets}" +ref=183a187cf6128371a31ad20f3b524f6f30d537b9 +mkdir -p libs +curl --fail --location --silent --show-error --retry 3 -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github.raw+json" "https://api.github.com/repos/TF-Minecraft/ServerAssets/contents/jars/575aa30aee8e/MythicMobs-5.8.0-SNAPSHOT.jar?ref=$ref" > "libs/MythicMobs-5.8.0-SNAPSHOT.jar" +bash .github/scripts/install-local-dependencies.sh "$@" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..40b7935 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,77 @@ +name: Build + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: actions/setup-java@v6 + with: + distribution: temurin + java-version: '21' + - name: Install shared plugin dependencies + uses: TF-Minecraft/TLibs/.github/actions/setup-plugins@4f7a3545063cb2e8d06a034687c817ab863f0835 + with: + mode: pinned + private-token: ${{ secrets.DEPS_TOKEN }} + + - name: Prepare pinned dependencies + env: + GH_TOKEN: ${{ secrets.DEPS_TOKEN }} + run: bash .github/scripts/prepare-release.sh + - name: Set build version + id: dev + shell: bash + run: | + dev_version="main-SNAPSHOT" + if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then + dev_version="DEV-$(date -u +%Y%m%d-%H%M)" + fi + mvn -B --no-transfer-progress -P'!deploy-live' org.codehaus.mojo:versions-maven-plugin:2.22.0:set \ + -DnewVersion="$dev_version" -DgenerateBackupPoms=false + mvn -B --no-transfer-progress -P'!deploy-live' help:evaluate \ + -Dexpression=project.build.finalName -Doutput="$RUNNER_TEMP/final-name" + final_name=$(cat "$RUNNER_TEMP/final-name") + [[ "$final_name" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || exit 1 + echo "version=$dev_version" >> "$GITHUB_OUTPUT" + echo "name=$final_name" >> "$GITHUB_OUTPUT" + echo "jar=target/$final_name.jar" >> "$GITHUB_OUTPUT" + + - name: Run unit tests and build + run: mvn -B --no-transfer-progress -P'!deploy-live' clean verify -DskipTests=false -Dmaven.test.skip=false + + - name: Verify runtime JAR + env: + ARTIFACT_PATH: ${{ steps.dev.outputs.jar }} + BUILD_VERSION: ${{ steps.dev.outputs.version }} + run: python3 .github/scripts/plugin-artifact.py --jar "$ARTIFACT_PATH" --version "$BUILD_VERSION" + + - if: github.event_name == 'pull_request' + uses: actions/upload-artifact@v7 + with: + name: ${{ steps.dev.outputs.name }}-${{ github.run_id }}-${{ github.run_attempt }} + path: | + ${{ steps.dev.outputs.jar }} + .build/plugin-dependencies.json + include-hidden-files: true + if-no-files-found: error + + - name: Upload unit test reports + if: ${{ !cancelled() && hashFiles('target/surefire-reports/*.xml') != '' }} + uses: actions/upload-artifact@v7 + with: + name: unit-test-reports-${{ github.run_id }}-${{ github.run_attempt }} + path: target/surefire-reports/ + if-no-files-found: error diff --git a/.github/workflows/maven-release.yml b/.github/workflows/maven-release.yml new file mode 100644 index 0000000..9d015e5 --- /dev/null +++ b/.github/workflows/maven-release.yml @@ -0,0 +1,138 @@ +name: Maven plugin release + +on: + workflow_call: + inputs: + java-version: + required: true + type: string + secrets: + DEPS_TOKEN: + required: false + +permissions: + contents: read + +concurrency: + group: plugin-release-${{ github.repository }}-${{ github.ref }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + build: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + + - uses: actions/setup-java@v6 + with: + distribution: temurin + java-version: ${{ inputs.java-version }} + + - name: Validate release version + env: + TAG: ${{ github.ref_name }} + run: | + [[ "$TAG" =~ ^v[0-9]+\.[0-9]+(\.[0-9]+)?(-[0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$ ]] || { + echo '::error::Expected vMAJOR.MINOR or vMAJOR.MINOR.PATCH, with an optional prerelease suffix.' + exit 1 + } + [[ "${TAG^^}" != *SNAPSHOT* ]] || exit 1 + + - name: Install shared plugin dependencies + uses: TF-Minecraft/TLibs/.github/actions/setup-plugins@4f7a3545063cb2e8d06a034687c817ab863f0835 + with: + mode: pinned + private-token: ${{ secrets.DEPS_TOKEN }} + + - name: Prepare pinned dependencies + env: + GH_TOKEN: ${{ secrets.DEPS_TOKEN }} + run: | + if [[ -f .github/scripts/prepare-release.sh ]]; then + bash .github/scripts/prepare-release.sh + fi + + - name: Set version from tag and build + id: maven + env: + TAG: ${{ github.ref_name }} + run: | + mvn -B --no-transfer-progress -P'!deploy-live' org.codehaus.mojo:versions-maven-plugin:2.22.0:set \ + -DnewVersion="${TAG#v}" -DgenerateBackupPoms=false + mvn -B --no-transfer-progress -P'!deploy-live' help:evaluate \ + -Dexpression=project.build.finalName -Doutput="$RUNNER_TEMP/final-name" + final_name=$(cat "$RUNNER_TEMP/final-name") + [[ "$final_name" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || exit 1 + echo "jar=target/$final_name.jar" >> "$GITHUB_OUTPUT" + mvn -B --no-transfer-progress -P'!deploy-live' clean verify -DskipTests=false -Dmaven.test.skip=false + + - name: Stage only the release JAR + env: + ARTIFACT_PATH: ${{ steps.maven.outputs.jar }} + TAG: ${{ github.ref_name }} + run: | + python3 .github/scripts/plugin-artifact.py \ + --jar "$ARTIFACT_PATH" --version "${TAG#v}" \ + --stage "$RUNNER_TEMP/plugin-release" \ + --dependencies .build/plugin-dependencies.json + + - uses: actions/upload-artifact@v7 + with: + name: plugin-release-${{ github.sha }} + path: ${{ runner.temp }}/plugin-release/ + if-no-files-found: error + retention-days: 30 + + publish: + needs: build + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + contents: write + steps: + - uses: actions/download-artifact@v8 + with: + name: plugin-release-${{ github.sha }} + path: release + + - name: Create draft release + working-directory: release + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + TAG: ${{ github.ref_name }} + run: | + sha256sum --check SHA256SUMS + flags=() + if [[ "$TAG" == *-* ]]; then flags+=(--prerelease); fi + gh api --method POST "repos/$GH_REPO/releases/generate-notes" \ + -f tag_name="$TAG" --jq .body > "$RUNNER_TEMP/release-changes.md" + python3 - <<'PY' + import os, re + from pathlib import Path + from urllib.parse import quote + temp = Path(os.environ['RUNNER_TEMP']) + changes = (temp / 'release-changes.md').read_text().strip() + changes = re.sub(r"^## What's Changed\s*", '', changes) + changes = re.sub(r'^(#{1,5}) ', r'\1# ', changes, flags=re.MULTILINE) + lines = ['## Changes', '', changes or 'Initial release.', '', '## Downloads', ''] + assets = sorted([*Path('.').glob('*.jar'), *Path('.').glob('*.tar.gz')]) + assets += [Path('SHA256SUMS'), Path('build.json')] + base = f"https://github.com/{os.environ['GH_REPO']}/releases/download/{quote(os.environ['TAG'], safe='')}/" + for asset in assets: + if not asset.is_file(): + raise SystemExit(f'Missing release asset: {asset}') + description = {'SHA256SUMS': ' — SHA-256 checksums.', 'build.json': ' — Build metadata.'}.get(asset.name, '') + lines.append(f'- [`{asset.name}`]({base}{quote(asset.name)}){description}') + (temp / 'release-notes.md').write_text('\n'.join(lines) + '\n') + PY + gh release create "$TAG" ./*.jar SHA256SUMS build.json \ + --verify-tag --draft --title "$TAG" --notes-file "$RUNNER_TEMP/release-notes.md" "${flags[@]}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..241e773 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,18 @@ +name: Release + +on: + push: + tags: ['v*'] + +permissions: + contents: read + +jobs: + release: + permissions: + contents: write + uses: ./.github/workflows/maven-release.yml + with: + java-version: '21' + secrets: + DEPS_TOKEN: ${{ secrets.DEPS_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fee0cd8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,54 @@ +# Build output +target/ +build/ +out/ +dist/ +nbbuild/ +nbdist/ +*.class +dependency-reduced-pom.xml +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties + +# Build dependencies +/.build/ +*.jar +!.mvn/wrapper/maven-wrapper.jar + +# Python caches and environments +__pycache__/ +*.py[cod] +.venv/ +venv/ + +# IDE files +.idea/ +*.iml +*.ipr +*.iws +.vscode/ +.project +.classpath +.settings/ +nbproject/private/ +.nb-gradle/ + +# Logs and temporary files +*.log +hs_err_pid* +replay_pid* +*.bak +*.swp +*.tmp +*~ + +# Operating system files +.DS_Store +Thumbs.db +Desktop.ini + +# Local investigation notes and test artifacts +/.scratch/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9d00de5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + The Artistic License 2.0 + + Copyright (c) 2000-2006, The Perl Foundation. + + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +Preamble + +This license establishes the terms under which a given free software +Package may be copied, modified, distributed, and/or redistributed. +The intent is that the Copyright Holder maintains some artistic +control over the development of that Package while still keeping the +Package available as open source and free software. + +You are always permitted to make arrangements wholly outside of this +license directly with the Copyright Holder of a given Package. If the +terms of this license do not permit the full use that you propose to +make of the Package, you should contact the Copyright Holder and seek +a different licensing arrangement. + +Definitions + + "Copyright Holder" means the individual(s) or organization(s) + named in the copyright notice for the entire Package. + + "Contributor" means any party that has contributed code or other + material to the Package, in accordance with the Copyright Holder's + procedures. + + "You" and "your" means any person who would like to copy, + distribute, or modify the Package. + + "Package" means the collection of files distributed by the + Copyright Holder, and derivatives of that collection and/or of + those files. A given Package may consist of either the Standard + Version, or a Modified Version. + + "Distribute" means providing a copy of the Package or making it + accessible to anyone else, or in the case of a company or + organization, to others outside of your company or organization. + + "Distributor Fee" means any fee that you charge for Distributing + this Package or providing support for this Package to another + party. It does not mean licensing fees. + + "Standard Version" refers to the Package if it has not been + modified, or has been modified only in ways explicitly requested + by the Copyright Holder. + + "Modified Version" means the Package, if it has been changed, and + such changes were not explicitly requested by the Copyright + Holder. + + "Original License" means this Artistic License as Distributed with + the Standard Version of the Package, in its current version or as + it may be modified by The Perl Foundation in the future. + + "Source" form means the source code, documentation source, and + configuration files for the Package. + + "Compiled" form means the compiled bytecode, object code, binary, + or any other form resulting from mechanical transformation or + translation of the Source form. + + +Permission for Use and Modification Without Distribution + +(1) You are permitted to use the Standard Version and create and use +Modified Versions for any purpose without restriction, provided that +you do not Distribute the Modified Version. + + +Permissions for Redistribution of the Standard Version + +(2) You may Distribute verbatim copies of the Source form of the +Standard Version of this Package in any medium without restriction, +either gratis or for a Distributor Fee, provided that you duplicate +all of the original copyright notices and associated disclaimers. At +your discretion, such verbatim copies may or may not include a +Compiled form of the Package. + +(3) You may apply any bug fixes, portability changes, and other +modifications made available from the Copyright Holder. The resulting +Package will still be considered the Standard Version, and as such +will be subject to the Original License. + + +Distribution of Modified Versions of the Package as Source + +(4) You may Distribute your Modified Version as Source (either gratis +or for a Distributor Fee, and with or without a Compiled form of the +Modified Version) provided that you clearly document how it differs +from the Standard Version, including, but not limited to, documenting +any non-standard features, executables, or modules, and provided that +you do at least ONE of the following: + + (a) make the Modified Version available to the Copyright Holder + of the Standard Version, under the Original License, so that the + Copyright Holder may include your modifications in the Standard + Version. + + (b) ensure that installation of your Modified Version does not + prevent the user installing or running the Standard Version. In + addition, the Modified Version must bear a name that is different + from the name of the Standard Version. + + (c) allow anyone who receives a copy of the Modified Version to + make the Source form of the Modified Version available to others + under + + (i) the Original License or + + (ii) a license that permits the licensee to freely copy, + modify and redistribute the Modified Version using the same + licensing terms that apply to the copy that the licensee + received, and requires that the Source form of the Modified + Version, and of any works derived from it, be made freely + available in that license fees are prohibited but Distributor + Fees are allowed. + + +Distribution of Compiled Forms of the Standard Version +or Modified Versions without the Source + +(5) You may Distribute Compiled forms of the Standard Version without +the Source, provided that you include complete instructions on how to +get the Source of the Standard Version. Such instructions must be +valid at the time of your distribution. If these instructions, at any +time while you are carrying out such distribution, become invalid, you +must provide new instructions on demand or cease further distribution. +If you provide valid instructions or cease distribution within thirty +days after you become aware that the instructions are invalid, then +you do not forfeit any of your rights under this license. + +(6) You may Distribute a Modified Version in Compiled form without +the Source, provided that you comply with Section 4 with respect to +the Source of the Modified Version. + + +Aggregating or Linking the Package + +(7) You may aggregate the Package (either the Standard Version or +Modified Version) with other packages and Distribute the resulting +aggregation provided that you do not charge a licensing fee for the +Package. Distributor Fees are permitted, and licensing fees for other +components in the aggregation are permitted. The terms of this license +apply to the use and Distribution of the Standard or Modified Versions +as included in the aggregation. + +(8) You are permitted to link Modified and Standard Versions with +other works, to embed the Package in a larger work of your own, or to +build stand-alone binary or bytecode versions of applications that +include the Package, and Distribute the result without restriction, +provided the result does not expose a direct interface to the Package. + + +Items That are Not Considered Part of a Modified Version + +(9) Works (including, but not limited to, modules and scripts) that +merely extend or make use of the Package, do not, by themselves, cause +the Package to be a Modified Version. In addition, such works are not +considered parts of the Package itself, and are not subject to the +terms of this license. + + +General Provisions + +(10) Any use, modification, and distribution of the Standard or +Modified Versions is governed by this Artistic License. By using, +modifying or distributing the Package, you accept this license. Do not +use, modify, or distribute the Package, if you do not accept this +license. + +(11) If your Modified Version has been derived from a Modified +Version made by someone other than you, you are nevertheless required +to ensure that your Modified Version complies with the requirements of +this license. + +(12) This license does not grant you the right to use any trademark, +service mark, tradename, or logo of the Copyright Holder. + +(13) This license includes the non-exclusive, worldwide, +free-of-charge patent license to make, have made, use, offer to sell, +sell, import and otherwise transfer the Package with respect to any +patent claims licensable by the Copyright Holder that are necessarily +infringed by the Package. If you institute patent litigation +(including a cross-claim or counterclaim) against any party alleging +that the Package constitutes direct or contributory patent +infringement, then this Artistic License to you shall terminate on the +date that such litigation is filed. + +(14) Disclaimer of Warranty: +THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS +IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR +NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL +LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..80bb509 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# Infestations + +> Seed monster infestations across TF-Minecraft provinces and let players clear them with lure raids. + +Infestations ties MythicMobs encounters to SimpleFactions provinces. An infested +province harasses players with ambient monsters until a party places a lure, +commits to the fight, and defeats every monster the lure draws in. + +## Features + +- **Province infestations** — administrators assign a configured mob group and + severity to a land province, with at most one infestation per province. +- **Ambient harassment** — MythicMobs spawn around players inside an infested + province, with per-severity caps, intervals, and spawn rings, plus optional + night-only, minimum-height, and terrain rules. +- **Lure raids** — an InteractibleFurniture lure opens a join window, then + releases a paced wave; defeating the full wave clears the infestation. +- **Committed parties** — leaving the province or losing the whole party fails + the lure, bystanders are warned and damaged, and logged-out players get a + grace period. +- **Optional spread** — idle infestations can worsen and spread to neighbouring + land, including across a single water or sea province. +- **Map and persistence** — infestation severity is exported to the + SimpleFactions map service, and infestation and lure state survive restarts. + +Originally created by [Drefvelin](https://github.com/Drefvelin). + +## Documentation + +[Project documentation](https://github.com/TF-Minecraft/Docs/blob/main/projects/Infestations/README.md) + +Technical documentation is maintained in [TF-Minecraft/Docs](https://github.com/TF-Minecraft/Docs). + +## License + +Copyright (c) 2026 TF-Minecraft contributors. + +TF-Minecraft-authored material in this repository is licensed under the +[Artistic License 2.0](LICENSE). Third-party dependencies and pre-existing +material retain their own licenses. diff --git a/docs/IMPLEMENTATION_BATCHES.md b/docs/IMPLEMENTATION_BATCHES.md deleted file mode 100644 index 38dac71..0000000 --- a/docs/IMPLEMENTATION_BATCHES.md +++ /dev/null @@ -1,90 +0,0 @@ -# Infestations - Implementation batches - -Work in order. Each batch should compile and be testable before the next. - -See [SYSTEM.md](SYSTEM.md) for locked rules. See [TEST_MATRIX.md](TEST_MATRIX.md) for the manual checklist. - -Sign-off pass: [REVIEW_BATCHES.md](REVIEW_BATCHES.md) (all seven closed). - ---- - -## Batch 1 - Scaffold - -- [x] Maven project, plugin.yml, config/messages/groups yaml -- [x] Bootstrap class, ConfigLoader, GroupLoader, Cache, Messages -- [x] `/infestation reload` -- [x] Documentation (`SYSTEM.md`, this file, `TEST_MATRIX.md`) - -**Test:** `mvn package`, plugin enables, `/infestation reload` succeeds. - ---- - -## Batch 2 - Infestation data + admin - -- [x] Gson under `Data/infestations.json` -- [x] One record per province: id, group, severity -- [x] `/infestation set here| `, `clear`, `list` -- [x] Reject second infestation in the same province -- [x] Province from SimpleFactions grid - -**Test:** set/list/clear, restart persistence, duplicate set fails. - ---- - -## Batch 3 - Ambient spawn - -- [x] SpawnPlanner (player origin, ring min/max, clearance, same province) -- [x] Tick players in infested provinces only -- [x] PDC `infestationId` + `kind=ambient` -- [x] Caps; silent remove when no players / chunk unload -- [x] Deaths do not change infestation - -**Test:** walk infested province, `SwampGhoul` at ring; leave province, ambients stay; sea/uninfested quiet. - ---- - -## Batch 4 - Lure place, protect, join UI - -- [x] `FurniturePlaceEvent`: IF lure only if infested, else cancel; vanilla `BlockPlace` of the lure item is cancelled -- [x] Protect until plugin break -- [x] TextDisplay remaining/countdown; chunk load/unload + restart, no orphans -- [x] 20s join: broadcast + action bar -- [x] Right-click block to commit (window and after activate) -- [x] Non-joiners: leave warning - -**Test:** no infestation = no place; chunk unload/reload display; restart; second lure denied. - ---- - -## Batch 5 - Lure wave, any-kill, victory/fail, deserter - -- [x] On activate: stop ambient, spawn budget near lure (radius 48) -- [x] Any killer decrements remaining; hologram updates -- [x] Remaining 0: victory particles, plugin-break lure, delete infestation -- [x] Wipe: plugin-break lure, keep infestation, resume ambient -- [x] Leave province while committed: lure fails (destroyed, infestation stays) -- [x] Late click still commits - -**Test:** kill remaining to clear; walk out fails the lure; wipe fail keeps infestation. - ---- - -## Batch 6 - Logout grace + login kill - -- [x] Quit while committed: 5 min grace (persisted) -- [x] Rejoin in time: still committed, no kill -- [x] After grace: flag; on login fatal vanilla damage so death events fire -- [x] That death can fail the lure - -**Test:** logout 10s and return; logout past grace and login-die. - ---- - -## Batch 7 - Website infestation map mode - -- [x] `infestation_data.json` upload -- [x] Paint `infestation_map.png` (yellow to dark red, no green) -- [x] Frontend mode, overlay, hover -- [x] Map docs - -**Test:** set severe swamp, upload/regen, overlay dark red, hover `Infestation: Severe (Swamp Monsters)`; clear then transparent. diff --git a/docs/REVIEW_BATCHES.md b/docs/REVIEW_BATCHES.md deleted file mode 100644 index 6f29e79..0000000 --- a/docs/REVIEW_BATCHES.md +++ /dev/null @@ -1,117 +0,0 @@ -# Infestations - Review batches - -**All seven reviews closed** (30 Aug 2026). Code sign-off is done. Manual in-game checks: [TEST_MATRIX.md](TEST_MATRIX.md). - -Locked rules: [SYSTEM.md](SYSTEM.md). - -Known shape vs the original plan: - -- There is no `commit/` package. Logout, deserter, and wipe live in `InfestationManager`. -- Lure is InteractibleFurniture (`ia.tfmc:lure`), not a solid block. `plugin.yml` also `depend`s InteractibleFurniture. - ---- - -## Review 1 - Docs + scaffold - -**Status:** closed - -**Files:** `docs/SYSTEM.md`, `pom.xml`, `plugin.yml`, `Infestations.java`, `config.yml`, `messages.yml`, `groups.yml`, loaders, `Cache`, `Messages`, `CommandManager` reload path. - -**Confirm:** -- Rules in SYSTEM.md still match what we want -- `depend: [TLibs, SimpleFactions, MythicMobs, ItemsAdder, InteractibleFurniture]` -- Bootstrap only in main class; `/infestation reload` -- `swamp_mobs` / `SwampGhoul`; lure path `ia.tfmc:lure` -- No em dash in `messages.yml` - ---- - -## Review 2 - Data + admin - -**Status:** closed - -**Files:** `infestation/Infestation.java`, `Severity.java`, `database/*`, `CommandManager` set/clear/list, `utils/Provinces.java`. - -**Confirm:** -- One infestation per province; second `set` rejected -- Persist `Data/infestations.json` -- `here` and numeric id; skip water/sea -- Clear does not leave a lure behind -- Chunk load: lure furniture not in saved state is removed - ---- - -## Review 3 - Ambient spawn - -**Status:** closed - -**Files:** `spawn/SpawnPlanner.java`, `MythicSpawner.java`, `Keys.java`, `AmbientSpawnService.java`. - -**Confirm:** -- Ring min/max, 3x3 air + floor, same province -- PDC `infestationId` + `kind=ambient` -- Cap and interval from severity -- Deaths do not clear infestation -- Leave province / chunk unload silent `remove()` (nearby scan, not a full-world sweep) - ---- - -## Review 4 - Lure place, protect, hologram, join window - -**Status:** closed - -**Files:** `InfestationManager` place/break/interact, `lure/LureHologram.java`, `lure/LureFurniture.java`. - -**Confirm:** -- No infestation: place cancelled -- One lure per province -- Protected unless plugin remove; orphan IF lure on interact is deleted, not picked up -- TextDisplay PDC, proximity, chunk load/unload, orphan cleanup -- 20s window; warn province; right-click joins during window **and** after activate - ---- - -## Review 5 - Wave, any-kill, victory, wipe, deserter - -**Status:** closed - -**Files:** `InfestationManager` activate/wave/death/wipe/deserter. - -**Confirm:** -- Activate: ambient stopped; spawn within `lure-spawn-radius` (48) -- Remaining is budget ints; any death of lure-tagged mob counts -- Despawn / chunk unload re-queues, not a kill -- Remaining 0: particles, plugin-break lure, infestation deleted -- All committed dead (grace / pending login-kill still count as in): lure destroyed, infestation stays, ambient can resume -- Committed leave province: lure fails (destroyed, infestation stays) -- Non-joiners still in province after activate: damage + leave message -- Late click commits - ---- - -## Review 6 - Logout grace - -**Status:** closed - -**Files:** `InfestationManager` quit/join + persisted `logoutGraceUntil` / `deathOnLogin`. - -**Confirm:** -- 5 min grace persisted -- Return in time: still committed, not killed (login at logout location) -- After grace: flag; login uses vanilla `damage` so death events fire -- That death can fail the lure if they were last - ---- - -## Review 7 - Website map mode - -**Status:** closed - -**Files:** `map/InfestationMapExport.java`; ProvinceSystem `infestationgen.py`, `data_routes.py`, `regeneration.py`; frontend `types.ts`, `MapToolbar.tsx`, `useMapModeData.ts`, `MapCanvas.tsx`, `useProvinceHover.ts`. - -**Confirm:** -- Upload `infestation_data` (not a SimpleFactions `Province` field) -- Overlay yellow → dark red, no green; empty transparent; skip water/sea -- Regen on fullregen and on upload; overlay `no-store` -- Hover `Infestation: Severe (swamp_mobs)` -- Toolbar option; no nation region fetch for this mode diff --git a/docs/SYSTEM.md b/docs/SYSTEM.md deleted file mode 100644 index ecac371..0000000 --- a/docs/SYSTEM.md +++ /dev/null @@ -1,80 +0,0 @@ -# Infestations - System Overview - -Province-scale MythicMob infestations. Idle harassment around players, then a lure raid that commits you until the budget is dead or the party wipes. - -See [IMPLEMENTATION_BATCHES.md](IMPLEMENTATION_BATCHES.md), [REVIEW_BATCHES.md](REVIEW_BATCHES.md), and [TEST_MATRIX.md](TEST_MATRIX.md). - -## Rules (locked) - -- One infestation per province. Admin add fails if that province already has one. -- **Spread:** `config.yml` `spread` is a global kill switch (default false for the test server). Worsening and neighbor infection are the same clock (`spread-interval-seconds`). When true, each idle infestation rolls `worsen-chance` to step severity up one (mild → worrying → severe → extreme, then stops) and tries to infect **one** valid land province: a direct land neighbor, or land on the far side of a **single** adjacent water or sea tile. Water/sea cannot be infested. Two-tile hops (water-water, sea-sea, water-sea) are not allowed. If both a land path and a hop exist, land wins. Hop chance is per source severity × via (`spread-chance-land` / `water` / `sea`). A group's `terrains` list in `groups.yml` (if non-empty) further limits both spread and `/infestation set` to those province terrains. Lure provinces do not spread or worsen. -- Two spawn phases, both TrialRooms-style: Mythic spawn, PDC tags, integer counters, SpawnPlanner disk, silent `remove()` (not UUID lists). Port those patterns; do not depend on TrialRooms. -- **Idle:** tagged ambient around players in the province. Caps, ring, and interval come from the **group** per severity (`groups.yml`). Spawns must land in the same province. Optional group `min-y` rejects spots below that block Y. Ambient deaths do not clear the infestation. `night-only` groups skip day (world time 13000-23000). This plugin never `remove()`s mobs (leave, lure, clear, chunk unload). Vanilla / Mythic / chunk unload may despawn them. -- **Lure:** InteractibleFurniture piece using item `ia.tfmc:lure` (not a solid block). Place only if that province is infested. One lure per province. Ambient spawning stops (existing ambient stay). Mobs spawn within config radius of the lure (default 48), paced over `lure-duration-seconds` up to `lure-count`, and also honor group `min-y`. Any `EntityDeathEvent` on a lure-tagged mob counts. Display = remaining budget (pending + alive), persisted as ints. Missing wave members (vanilla / Mythic despawn) are not counted as kills. On chunk load, lure furniture that is not in saved state is removed. -- **Join:** 20s window. Everyone in the province is warned (chat + action bar) to leave or right-click the **base block**. Right-click joins even after activate (commits). Non-joiners still in the province after activate take deserter damage and are told to leave. -- **Commit:** `PlayerProvinceLeaveEvent` is leave. A committed player leaving the province fails the lure (plugin destroys it, infestation stays). New arrivals can click to join. -- **Wipe:** if no living committed players remain (logged-out-in-grace still count as in the lure), or a committed player walks out: fail: plugin destroys the lure, infestation stays. -- **Logout:** 5 minute grace. Return in time: still committed. After grace: mark for death; on next login kill via vanilla damage so `PlayerDeathEvent` fires. -- **Lure block:** cancel furniture break/pickup unless this plugin removes it (victory, fail, admin clear, or orphan sweep). Victory: particles, destroy lure, clear infestation. -- **TextDisplay:** PDC-tagged, proximity and chunk gated like TrialRooms holograms. Lure state is Gson. On chunk load and enable: recreate if missing; delete orphan tagged displays. -- First group: `swamp_mobs` (`Swamp Monsters`), Mythic id `SwampGhoul`, night-only, `terrains: [bog]`, `min-y: 56`. -- Severity numbers live on each group. Overlay is yellow to dark red, no green. Chat and hover use group `display`. -- Spawn debug: `logging` / `wipe-log` write `logs/spawn.log`. -- Player-facing strings: no em dash. Hex via TLibs `StringFormatter`. - -```mermaid -stateDiagram-v2 - idle: IdleAmbient - joining: LureJoining - active: LureActive - cleared: ClearedGone - failed: FailedStillInfested - idle --> joining: lurePlaced - joining --> active: timerEnds - active --> cleared: remainingZero - active --> failed: allCommittedDeadOrWalkout - failed --> idle: ambientResumes -``` - -## Config folder layout - -All paths under `plugins/Infestations/` on the server. - -| Path | Purpose | -|------|---------| -| `config.yml` | Debug, lure item, join seconds, radii, grace, per-severity numbers | -| `messages.yml` | Player-facing chat, action bar, hologram lines | -| `groups.yml` | Mob groups, display names, night-only, per-severity caps / lure | -| `Data/infestations.json` | Gson: infestations and lure runtime | - -## Target source tree - -Packages by domain. Deserter, logout, and wipe live on `InfestationManager` (no `commit/` package). - -``` -infestations/ - docs/ - src/main/java/net/tfminecraft/infestations/ - Infestations.java - command/ loader/ cache/ database/ - infestation/ # record, manager (lure, commit, wipe) - spawn/ # SpawnPlanner port, Mythic + PDC - lure/ # TextDisplay hologram - map/ # infestation_data.json upload -``` - -## Spawn (TrialRooms) - -| Concern | Approach | -|---------|----------| -| Spawn API | `MythicMob.spawn(BukkitAdapter.adapt(loc), 1)` | -| Ownership | PDC int province id + kind `ambient` or `lure` | -| Alive tracking | `int` pending + alive, not UUID sets | -| Death | `EntityDeathEvent` + PDC lookup | -| Forced despawn | none (plugin does not `remove()` mobs) | -| Positions | Shuffled disk, 3x3x3 air, 3x3 solid floor | -| UI | Real TextDisplay, proximity + chunk gated | - -## Map - -This plugin owns infestation state. Do not add fields to SimpleFactions `Province`. Export `infestation_data.json` and upload mode `infestation_data` via SimpleFactions `RestServer`. Overlay is yellow (mild) through dark red (extreme); uninfested provinces stay transparent. diff --git a/docs/TEST_MATRIX.md b/docs/TEST_MATRIX.md deleted file mode 100644 index 453c35b..0000000 --- a/docs/TEST_MATRIX.md +++ /dev/null @@ -1,76 +0,0 @@ -# Infestations - Manual test matrix - -Code reviews 1-7 are closed. Tick these on the live server. Player-facing strings must not use an em dash (U+2014). - ---- - -## Batch 1 - Scaffold - -- [ ] `mvn package` succeeds -- [ ] Plugin enables with TLibs, SimpleFactions, MythicMobs, ItemsAdder -- [ ] Default yaml copied to `plugins/Infestations/` -- [ ] `/infestation` with no args shows usage -- [ ] `/infestation reload` as op succeeds -- [ ] No permission: denial from `messages.yml` - ---- - -## Batch 2 - Data + admin - -- [ ] `/infestation set here swamp_mobs mild` in a **bog** province creates one -- [ ] Same command in plains (or any non-bog) is rejected (`wrong-terrain`) -- [ ] Second set on the same province is rejected -- [ ] `/infestation list` shows id, group, severity -- [ ] `/infestation clear here` removes it -- [ ] Restart: set infestation is still there -- [ ] Sea/water / unknown province rejected - ---- - -## Batch 3 - Ambient - -- [ ] Infested land: `SwampGhoul` spawns at configured ring, same province -- [ ] Cap respected -- [ ] Leave province: ambients stay (infestation remains); plugin does not `remove()` them -- [ ] Uninfested and sea: no plugin spawns -- [ ] Killing ambients does not clear the infestation - ---- - -## Batch 4 - Lure UI - -- [ ] Place `ia.tfmc:lure` with no infestation: cancelled + message -- [ ] Place with infestation: lure sits, hologram countdown, province warned -- [ ] Right-click base during window joins -- [ ] Second lure in same province denied -- [ ] Players cannot break the lure; plugin still can -- [ ] Chunk unload/reload and server restart: hologram returns, no extra displays - ---- - -## Batch 5 - Wave + commit - -- [ ] After 20s, ambients gone, lure mobs within 48 of lure -- [ ] Any player's kill decrements hologram remaining -- [ ] Remaining 0: particles, lure gone, infestation gone -- [ ] Committed player leaves province: lure destroyed, infestation still listed -- [ ] Late right-click still commits -- [ ] Non-joiner staying after activate: damage + leave message -- [ ] All committed die: lure destroyed, infestation still listed - ---- - -## Batch 6 - Logout - -- [ ] Logout while committed, return within 5 minutes: still committed, not killed -- [ ] Logout past 5 minutes, login: die (death event fires) -- [ ] That death can fail the lure if they were last - ---- - -## Batch 7 - Map - -- [ ] Set severe infestation, overlay is dark red -- [ ] Hover: `Infestation: Severe (Swamp Monsters)` -- [ ] Mild is yellow; no green anywhere on the overlay -- [ ] Clear infestation: province transparent on infestation mode diff --git a/pom.xml b/pom.xml index 75ff349..ea4f125 100644 --- a/pom.xml +++ b/pom.xml @@ -1,101 +1,106 @@ - - - 4.0.0 - - net.tfminecraft - infestations - 0.1.0 - - - 17 - 17 - UTF-8 - D:/Dokumenter/My Plugins/Reference Libs - D:/Dokumenter/My Plugins/TFMC/Infestations - D:/Dokumenter/My Plugins/TFMC/TLibs - D:/Dokumenter/My Plugins/TFMC/SimpleFactions - - - - - local - spigot-api - 1.21.8 - system - ${tfmc.refs}/spigot-api.jar - - - local - TLibs - 1.0 - system - ${tfmc.tlibs}/TLibs.jar - - - local - SimpleFactions - 2.8.7 - system - ${tfmc.simplefactions}/simplefactions-2.8.7.jar - - - local - MythicMobs - 5.8.0 - system - ${tfmc.refs}/MythicMobs-5.8.0-SNAPSHOT.jar - - - local - ItemsAdder - 3.5.0 - system - ${tfmc.refs}/ItemsAdder_3.5.0-r2.jar - - - local - InteractibleFurniture - 0.1.3 - system - ${project.basedir}/../interactiblefurniture/target/interactiblefurniture-0.1.3-BETA.jar - - - local - BungeeCord - 1.0 - system - ${tfmc.refs}/BungeeCord.jar - - - com.google.code.gson - gson - 2.10.1 - provided - - - - - - - org.apache.maven.plugins - maven-antrun-plugin - 3.1.0 - - - package - run - - - - - - - - - - - - + + + 4.0.0 + + net.tfminecraft + infestations + main-SNAPSHOT + + Infestations + https://github.com/TF-Minecraft/Infestations + + + 21 + UTF-8 + full + 0.2.1 + 3.0.2 + 2.0.1 + + + + + io.papermc.paper + paper-api + 1.21.10-R0.1-SNAPSHOT + provided + + + local + MythicMobs + 5.8.0-SNAPSHOT-tfmc-575aa30aee8e + provided + + + me.plugins + tlibs + ${tlibs.version} + provided + + + * + * + + + + + net.tfminecraft + simplefactions + ${simplefactions.version} + provided + + + * + * + + + + + net.tfminecraft + interactiblefurniture + ${interactiblefurniture.version} + provided + + + * + * + + + + + + + + papermc + https://repo.papermc.io/repository/maven-public/ + + + + + + + src/main/resources + true + + plugin.yml + + + + src/main/resources + false + + plugin.yml + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.1 + + + + diff --git a/src/main/java/net/tfminecraft/infestations/Infestations.java b/src/main/java/net/tfminecraft/infestations/Infestations.java index d460ac7..9acde4d 100644 --- a/src/main/java/net/tfminecraft/infestations/Infestations.java +++ b/src/main/java/net/tfminecraft/infestations/Infestations.java @@ -1,144 +1,144 @@ -package net.tfminecraft.infestations; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.util.Arrays; - -import org.bukkit.plugin.java.JavaPlugin; - -import net.tfminecraft.infestations.cache.Cache; -import net.tfminecraft.infestations.command.CommandManager; -import net.tfminecraft.infestations.infestation.InfestationManager; -import net.tfminecraft.infestations.loader.ConfigLoader; -import net.tfminecraft.infestations.loader.GroupLoader; - -/** - * Province infestations. See docs/ for locked rules and batches. - */ -public class Infestations extends JavaPlugin { - - public static Infestations plugin; - - private final ConfigLoader configLoader = new ConfigLoader(); - private final GroupLoader groupLoader = new GroupLoader(); - private final CommandManager commandManager = new CommandManager(); - private InfestationManager infestationManager; - - @Override - public void onEnable() { - plugin = this; - createFolders(); - createConfigs(); - if (!loadConfigs()) { - getLogger().warning("Infestations loaded with config errors."); - } - - infestationManager = new InfestationManager(this); - getServer().getPluginManager().registerEvents(infestationManager, this); - infestationManager.start(); - - var cmd = getCommand("infestation"); - if (cmd != null) { - cmd.setExecutor(commandManager); - cmd.setTabCompleter(commandManager); - } else { - getLogger().severe("Command 'infestation' missing from plugin.yml"); - } - - getLogger().info("Infestations enabled."); - } - - @Override - public void onDisable() { - if (infestationManager != null) { - infestationManager.shutdown(); - } - getLogger().info("Infestations disabled."); - } - - public InfestationManager getInfestationManager() { - return infestationManager; - } - - public boolean reloadAll() { - boolean ok = loadConfigs(); - if (infestationManager != null) { - infestationManager.reloadFromDisk(); - } - return ok; - } - - private boolean loadConfigs() { - boolean ok = true; - ok &= configLoader.loadSafe(new File(getDataFolder(), "config.yml")); - Messages.load(new File(getDataFolder(), "messages.yml")); - ok &= groupLoader.loadSafe(new File(getDataFolder(), "groups.yml")); - net.tfminecraft.infestations.spawn.SpawnLog.configure( - Cache.loggingEnabled, Cache.wipeLog, getDataFolder()); - if (ok) { - getLogger().info("[Infestations] Configs loaded."); - } - if (Cache.debug) { - getLogger().info("[Infestations] Debug: lure-item=" + Cache.lureItem - + " join-seconds=" + Cache.joinSeconds - + " lure-spawn-radius=" + Cache.lureSpawnRadius - + " spread=" + Cache.spread - + " spread-interval-seconds=" + Cache.spreadIntervalSeconds - + " worsen-chance=" + Cache.worsenChance - + " spread-chance-land=" + Arrays.toString(Cache.spreadChanceLand) - + " spread-chance-water=" + Arrays.toString(Cache.spreadChanceWater) - + " spread-chance-sea=" + Arrays.toString(Cache.spreadChanceSea) - + " groups=" + GroupLoader.groupIds()); - } - return ok; - } - - private void createFolders() { - if (!getDataFolder().exists()) { - getDataFolder().mkdir(); - } - mkdir("Data"); - mkdir("MapAPI"); - mkdir("logs"); - } - - private void mkdir(String relativePath) { - File folder = new File(getDataFolder(), relativePath); - if (!folder.exists()) { - folder.mkdirs(); - } - } - - private void createConfigs() { - String[] defaultFiles = { - "config.yml", - "messages.yml", - "groups.yml" - }; - for (String path : defaultFiles) { - copyResourceIfMissing(path); - } - } - - private void copyResourceIfMissing(String relativePath) { - File target = new File(getDataFolder(), relativePath); - if (target.exists()) { - return; - } - File parent = target.getParentFile(); - if (parent != null) { - parent.mkdirs(); - } - try (InputStream in = getResource(relativePath)) { - if (in == null) { - getLogger().warning("Missing bundled resource: " + relativePath); - return; - } - Files.copy(in, target.toPath()); - } catch (IOException ex) { - getLogger().severe("Failed to copy default resource " + relativePath + ": " + ex.getMessage()); - } - } -} +package net.tfminecraft.infestations; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.Arrays; + +import org.bukkit.plugin.java.JavaPlugin; + +import net.tfminecraft.infestations.cache.Cache; +import net.tfminecraft.infestations.command.CommandManager; +import net.tfminecraft.infestations.infestation.InfestationManager; +import net.tfminecraft.infestations.loader.ConfigLoader; +import net.tfminecraft.infestations.loader.GroupLoader; + +/** + * Province infestations. See the Docs project guide for behaviour and configuration. + */ +public class Infestations extends JavaPlugin { + + public static Infestations plugin; + + private final ConfigLoader configLoader = new ConfigLoader(); + private final GroupLoader groupLoader = new GroupLoader(); + private final CommandManager commandManager = new CommandManager(); + private InfestationManager infestationManager; + + @Override + public void onEnable() { + plugin = this; + createFolders(); + createConfigs(); + if (!loadConfigs()) { + getLogger().warning("Infestations loaded with config errors."); + } + + infestationManager = new InfestationManager(this); + getServer().getPluginManager().registerEvents(infestationManager, this); + infestationManager.start(); + + var cmd = getCommand("infestation"); + if (cmd != null) { + cmd.setExecutor(commandManager); + cmd.setTabCompleter(commandManager); + } else { + getLogger().severe("Command 'infestation' missing from plugin.yml"); + } + + getLogger().info("Infestations enabled."); + } + + @Override + public void onDisable() { + if (infestationManager != null) { + infestationManager.shutdown(); + } + getLogger().info("Infestations disabled."); + } + + public InfestationManager getInfestationManager() { + return infestationManager; + } + + public boolean reloadAll() { + boolean ok = loadConfigs(); + if (infestationManager != null) { + infestationManager.reloadFromDisk(); + } + return ok; + } + + private boolean loadConfigs() { + boolean ok = true; + ok &= configLoader.loadSafe(new File(getDataFolder(), "config.yml")); + Messages.load(new File(getDataFolder(), "messages.yml")); + ok &= groupLoader.loadSafe(new File(getDataFolder(), "groups.yml")); + net.tfminecraft.infestations.spawn.SpawnLog.configure( + Cache.loggingEnabled, Cache.wipeLog, getDataFolder()); + if (ok) { + getLogger().info("[Infestations] Configs loaded."); + } + if (Cache.debug) { + getLogger().info("[Infestations] Debug: lure-item=" + Cache.lureItem + + " join-seconds=" + Cache.joinSeconds + + " lure-spawn-radius=" + Cache.lureSpawnRadius + + " spread=" + Cache.spread + + " spread-interval-seconds=" + Cache.spreadIntervalSeconds + + " worsen-chance=" + Cache.worsenChance + + " spread-chance-land=" + Arrays.toString(Cache.spreadChanceLand) + + " spread-chance-water=" + Arrays.toString(Cache.spreadChanceWater) + + " spread-chance-sea=" + Arrays.toString(Cache.spreadChanceSea) + + " groups=" + GroupLoader.groupIds()); + } + return ok; + } + + private void createFolders() { + if (!getDataFolder().exists()) { + getDataFolder().mkdir(); + } + mkdir("Data"); + mkdir("MapAPI"); + mkdir("logs"); + } + + private void mkdir(String relativePath) { + File folder = new File(getDataFolder(), relativePath); + if (!folder.exists()) { + folder.mkdirs(); + } + } + + private void createConfigs() { + String[] defaultFiles = { + "config.yml", + "messages.yml", + "groups.yml" + }; + for (String path : defaultFiles) { + copyResourceIfMissing(path); + } + } + + private void copyResourceIfMissing(String relativePath) { + File target = new File(getDataFolder(), relativePath); + if (target.exists()) { + return; + } + File parent = target.getParentFile(); + if (parent != null) { + parent.mkdirs(); + } + try (InputStream in = getResource(relativePath)) { + if (in == null) { + getLogger().warning("Missing bundled resource: " + relativePath); + return; + } + Files.copy(in, target.toPath()); + } catch (IOException ex) { + getLogger().severe("Failed to copy default resource " + relativePath + ": " + ex.getMessage()); + } + } +} diff --git a/src/main/java/net/tfminecraft/infestations/Messages.java b/src/main/java/net/tfminecraft/infestations/Messages.java index f7c2ffb..8e65948 100644 --- a/src/main/java/net/tfminecraft/infestations/Messages.java +++ b/src/main/java/net/tfminecraft/infestations/Messages.java @@ -1,66 +1,66 @@ -package net.tfminecraft.infestations; - -import java.io.File; -import java.io.IOException; - -import org.bukkit.configuration.InvalidConfigurationException; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.configuration.file.YamlConfiguration; - -import me.Plugins.TLibs.Objects.API.SubAPI.StringFormatter; - -/** - * Player-facing strings from messages.yml. - */ -public final class Messages { - - private static FileConfiguration config; - - private Messages() {} - - public static void load(File file) { - FileConfiguration loaded = new YamlConfiguration(); - try { - loaded.load(file); - config = loaded; - } catch (IOException | InvalidConfigurationException ex) { - Infestations.plugin.getLogger().severe("[Infestations] Failed to load messages.yml: " + ex.getMessage()); - config = new YamlConfiguration(); - } - } - - public static String get(String path) { - return format(applyPrefix(getRaw(path))); - } - - public static String get(String path, String... keyValues) { - String raw = applyPrefix(getRaw(path)); - for (int i = 0; i + 1 < keyValues.length; i += 2) { - String value = keyValues[i + 1] != null ? keyValues[i + 1] : ""; - raw = raw.replace("{" + keyValues[i] + "}", value); - } - return format(raw); - } - - public static String getRaw(String path) { - if (config == null) { - return path; - } - return config.getString(path, path); - } - - private static String applyPrefix(String raw) { - if (raw == null) { - return ""; - } - String prefix = config != null ? config.getString("prefix", "") : ""; - return raw.replace("{prefix}", prefix != null ? prefix : ""); - } - - private static String format(String raw) { - if (raw == null) { - return ""; - } - return StringFormatter.formatHex(raw.replace('&', '\u00A7')); - } -} +package net.tfminecraft.infestations; + +import java.io.File; +import java.io.IOException; + +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; + +import net.tfminecraft.tlibs.objects.api.subapi.StringFormatter; + +/** + * Player-facing strings from messages.yml. + */ +public final class Messages { + + private static FileConfiguration config; + + private Messages() {} + + public static void load(File file) { + FileConfiguration loaded = new YamlConfiguration(); + try { + loaded.load(file); + config = loaded; + } catch (IOException | InvalidConfigurationException ex) { + Infestations.plugin.getLogger().severe("[Infestations] Failed to load messages.yml: " + ex.getMessage()); + config = new YamlConfiguration(); + } + } + + public static String get(String path) { + return format(applyPrefix(getRaw(path))); + } + + public static String get(String path, String... keyValues) { + String raw = applyPrefix(getRaw(path)); + for (int i = 0; i + 1 < keyValues.length; i += 2) { + String value = keyValues[i + 1] != null ? keyValues[i + 1] : ""; + raw = raw.replace("{" + keyValues[i] + "}", value); + } + return format(raw); + } + + public static String getRaw(String path) { + if (config == null) { + return path; + } + return config.getString(path, path); + } + + private static String applyPrefix(String raw) { + if (raw == null) { + return ""; + } + String prefix = config != null ? config.getString("prefix", "") : ""; + return raw.replace("{prefix}", prefix != null ? prefix : ""); + } + + private static String format(String raw) { + if (raw == null) { + return ""; + } + return StringFormatter.formatHex(raw.replace('&', '\u00A7')); + } +} diff --git a/src/main/java/net/tfminecraft/infestations/cache/Cache.java b/src/main/java/net/tfminecraft/infestations/cache/Cache.java index 845b030..542f994 100644 --- a/src/main/java/net/tfminecraft/infestations/cache/Cache.java +++ b/src/main/java/net/tfminecraft/infestations/cache/Cache.java @@ -1,45 +1,45 @@ -package net.tfminecraft.infestations.cache; - -import java.util.HashSet; -import java.util.Set; - -import net.tfminecraft.infestations.infestation.Severity; - -public final class Cache { - - public static final double[] DEFAULT_SPREAD_LAND = {0.00099, 0.0030, 0.0050, 0.00694}; - public static final double[] DEFAULT_SPREAD_WATER = {0.00040, 0.0012, 0.0020, 0.00278}; - public static final double[] DEFAULT_SPREAD_SEA = {0.00015, 0.00045, 0.00075, 0.00104}; - - public static boolean debug = false; - public static boolean loggingEnabled = true; - public static boolean wipeLog = true; - public static boolean spread = false; - public static int spreadIntervalSeconds = 600; - public static double worsenChance = 0.0015; - public static double[] spreadChanceLand = DEFAULT_SPREAD_LAND.clone(); - public static double[] spreadChanceWater = DEFAULT_SPREAD_WATER.clone(); - public static double[] spreadChanceSea = DEFAULT_SPREAD_SEA.clone(); - public static String lureItem = "ia.tfmc:lure"; - public static int joinSeconds = 20; - public static int lureSpawnRadius = 48; - public static int logoutGraceSeconds = 300; - public static double deserterDamage = 2.0; - public static double hologramViewRange = 96; - public static Set skipTerrains = new HashSet<>(); - - private Cache() {} - - public static double hopChance(Severity severity, int viaOrdinal) { - double[] table = switch (viaOrdinal) { - case 0 -> spreadChanceLand; - case 1 -> spreadChanceWater; - default -> spreadChanceSea; - }; - int index = severity.ordinal(); - if (index < 0 || index >= table.length) { - return table[table.length - 1]; - } - return table[index]; - } -} +package net.tfminecraft.infestations.cache; + +import java.util.HashSet; +import java.util.Set; + +import net.tfminecraft.infestations.infestation.Severity; + +public final class Cache { + + public static final double[] DEFAULT_SPREAD_LAND = {0.00099, 0.0030, 0.0050, 0.00694}; + public static final double[] DEFAULT_SPREAD_WATER = {0.00040, 0.0012, 0.0020, 0.00278}; + public static final double[] DEFAULT_SPREAD_SEA = {0.00015, 0.00045, 0.00075, 0.00104}; + + public static boolean debug = false; + public static boolean loggingEnabled = true; + public static boolean wipeLog = true; + public static boolean spread = false; + public static int spreadIntervalSeconds = 600; + public static double worsenChance = 0.0015; + public static double[] spreadChanceLand = DEFAULT_SPREAD_LAND.clone(); + public static double[] spreadChanceWater = DEFAULT_SPREAD_WATER.clone(); + public static double[] spreadChanceSea = DEFAULT_SPREAD_SEA.clone(); + public static String lureItem = "ia.tfmc:lure"; + public static int joinSeconds = 20; + public static int lureSpawnRadius = 48; + public static int logoutGraceSeconds = 300; + public static double deserterDamage = 2.0; + public static double hologramViewRange = 96; + public static Set skipTerrains = new HashSet<>(); + + private Cache() {} + + public static double hopChance(Severity severity, int viaOrdinal) { + double[] table = switch (viaOrdinal) { + case 0 -> spreadChanceLand; + case 1 -> spreadChanceWater; + default -> spreadChanceSea; + }; + int index = severity.ordinal(); + if (index < 0 || index >= table.length) { + return table[table.length - 1]; + } + return table[index]; + } +} diff --git a/src/main/java/net/tfminecraft/infestations/command/CommandManager.java b/src/main/java/net/tfminecraft/infestations/command/CommandManager.java index 843377a..0ee5d42 100644 --- a/src/main/java/net/tfminecraft/infestations/command/CommandManager.java +++ b/src/main/java/net/tfminecraft/infestations/command/CommandManager.java @@ -1,195 +1,195 @@ -package net.tfminecraft.infestations.command; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; -import java.util.stream.Collectors; - -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.command.TabCompleter; -import org.bukkit.entity.Player; - -import net.tfminecraft.infestations.Infestations; -import net.tfminecraft.infestations.Messages; -import net.tfminecraft.infestations.infestation.Infestation; -import net.tfminecraft.infestations.infestation.LurePhase; -import net.tfminecraft.infestations.infestation.Severity; -import net.tfminecraft.infestations.loader.GroupLoader; -import net.tfminecraft.infestations.utils.Provinces; - -public final class CommandManager implements CommandExecutor, TabCompleter { - - public static final String CMD = "infestation"; - - @Override - public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - if (args.length == 0) { - if (!sender.hasPermission("infestations.admin") - && !sender.hasPermission("infestations.admin.reload")) { - sender.sendMessage(Messages.get("no-permission")); - return true; - } - sender.sendMessage(Messages.get("usage")); - return true; - } - if (args[0].equalsIgnoreCase("reload")) { - if (!sender.hasPermission("infestations.admin.reload")) { - sender.sendMessage(Messages.get("no-permission")); - return true; - } - boolean ok = Infestations.plugin.reloadAll(); - sender.sendMessage(Messages.get(ok ? "reload-ok" : "reload-fail")); - return true; - } - if (!sender.hasPermission("infestations.admin")) { - sender.sendMessage(Messages.get("no-permission")); - return true; - } - switch (args[0].toLowerCase(Locale.ROOT)) { - case "set" -> handleSet(sender, args); - case "clear" -> handleClear(sender, args); - case "list" -> handleList(sender); - default -> sender.sendMessage(Messages.get("unknown-subcommand")); - } - return true; - } - - private void handleSet(CommandSender sender, String[] args) { - if (args.length < 4) { - sender.sendMessage(Messages.get("usage")); - return; - } - Integer provinceId = resolveProvince(sender, args[1]); - if (provinceId == null) { - return; - } - if (!Provinces.validLand(provinceId)) { - sender.sendMessage(Messages.get( - provinceId > 0 && Provinces.skipTerrain(provinceId) ? "skip-terrain" : "unknown-province")); - return; - } - if (GroupLoader.get(args[2]) == null) { - sender.sendMessage(Messages.get("unknown-group", "group", args[2])); - return; - } - if (!GroupLoader.allowsTerrain(args[2], provinceId)) { - sender.sendMessage(Messages.get("wrong-terrain", - "group", GroupLoader.displayName(args[2]), - "terrains", GroupLoader.terrainList(args[2]))); - return; - } - Severity severity = Severity.fromString(args[3]); - if (severity == null) { - sender.sendMessage(Messages.get("unknown-severity", "severity", args[3])); - return; - } - var manager = Infestations.plugin.getInfestationManager(); - if (manager.get(provinceId) != null) { - sender.sendMessage(Messages.get("already-infested", "id", String.valueOf(provinceId))); - return; - } - manager.set(provinceId, args[2].toLowerCase(Locale.ROOT), severity); - sender.sendMessage(Messages.get("set-ok", "id", String.valueOf(provinceId), - "group", GroupLoader.displayName(args[2]), - "severity", severity.display())); - } - - private void handleClear(CommandSender sender, String[] args) { - if (args.length < 2) { - sender.sendMessage(Messages.get("usage")); - return; - } - Integer provinceId = resolveProvince(sender, args[1]); - if (provinceId == null) { - return; - } - var manager = Infestations.plugin.getInfestationManager(); - if (manager.get(provinceId) == null) { - sender.sendMessage(Messages.get("not-infested", "id", String.valueOf(provinceId))); - return; - } - manager.clear(provinceId, false); - sender.sendMessage(Messages.get("clear-ok", "id", String.valueOf(provinceId))); - } - - private void handleList(CommandSender sender) { - var all = Infestations.plugin.getInfestationManager().all(); - if (!all.iterator().hasNext()) { - sender.sendMessage(Messages.get("list-empty")); - return; - } - for (Infestation infestation : all) { - String lure = infestation.getPhase() != LurePhase.NONE ? Messages.get("list-lure-suffix") : ""; - sender.sendMessage(Messages.get("list-line", - "id", String.valueOf(infestation.getProvinceId()), - "group", GroupLoader.displayName(infestation.getGroupId()), - "severity", infestation.getSeverity().display(), - "lure", lure)); - } - } - - private Integer resolveProvince(CommandSender sender, String token) { - if ("here".equalsIgnoreCase(token)) { - if (!(sender instanceof Player player)) { - sender.sendMessage(Messages.get("player-only")); - return null; - } - int id = Provinces.at(player); - if (!Provinces.validLand(id)) { - sender.sendMessage(Messages.get( - id > 0 && Provinces.skipTerrain(id) ? "skip-terrain" : "unknown-province")); - return null; - } - return id; - } - try { - return Integer.parseInt(token); - } catch (NumberFormatException ex) { - sender.sendMessage(Messages.get("unknown-province")); - return null; - } - } - - @Override - public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { - if (args.length == 1) { - List options = new ArrayList<>(); - if (sender.hasPermission("infestations.admin.reload")) { - options.add("reload"); - } - if (sender.hasPermission("infestations.admin")) { - options.add("set"); - options.add("clear"); - options.add("list"); - } - return prefix(options, args[0]); - } - if (!sender.hasPermission("infestations.admin")) { - return List.of(); - } - if (args.length == 2 && (args[0].equalsIgnoreCase("set") || args[0].equalsIgnoreCase("clear"))) { - return prefix(List.of("here"), args[1]); - } - if (args.length == 3 && args[0].equalsIgnoreCase("set")) { - return prefix(new ArrayList<>(GroupLoader.groupIds()), args[2]); - } - if (args.length == 4 && args[0].equalsIgnoreCase("set")) { - return prefix(Arrays.stream(Severity.values()).map(Severity::id).collect(Collectors.toList()), args[3]); - } - return List.of(); - } - - private static List prefix(List options, String typed) { - String t = typed.toLowerCase(Locale.ROOT); - List out = new ArrayList<>(); - for (String o : options) { - if (o.toLowerCase(Locale.ROOT).startsWith(t)) { - out.add(o); - } - } - return out; - } -} +package net.tfminecraft.infestations.command; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; + +import net.tfminecraft.infestations.Infestations; +import net.tfminecraft.infestations.Messages; +import net.tfminecraft.infestations.infestation.Infestation; +import net.tfminecraft.infestations.infestation.LurePhase; +import net.tfminecraft.infestations.infestation.Severity; +import net.tfminecraft.infestations.loader.GroupLoader; +import net.tfminecraft.infestations.utils.Provinces; + +public final class CommandManager implements CommandExecutor, TabCompleter { + + public static final String CMD = "infestation"; + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + if (args.length == 0) { + if (!sender.hasPermission("infestations.admin") + && !sender.hasPermission("infestations.admin.reload")) { + sender.sendMessage(Messages.get("no-permission")); + return true; + } + sender.sendMessage(Messages.get("usage")); + return true; + } + if (args[0].equalsIgnoreCase("reload")) { + if (!sender.hasPermission("infestations.admin.reload")) { + sender.sendMessage(Messages.get("no-permission")); + return true; + } + boolean ok = Infestations.plugin.reloadAll(); + sender.sendMessage(Messages.get(ok ? "reload-ok" : "reload-fail")); + return true; + } + if (!sender.hasPermission("infestations.admin")) { + sender.sendMessage(Messages.get("no-permission")); + return true; + } + switch (args[0].toLowerCase(Locale.ROOT)) { + case "set" -> handleSet(sender, args); + case "clear" -> handleClear(sender, args); + case "list" -> handleList(sender); + default -> sender.sendMessage(Messages.get("unknown-subcommand")); + } + return true; + } + + private void handleSet(CommandSender sender, String[] args) { + if (args.length < 4) { + sender.sendMessage(Messages.get("usage")); + return; + } + Integer provinceId = resolveProvince(sender, args[1]); + if (provinceId == null) { + return; + } + if (!Provinces.validLand(provinceId)) { + sender.sendMessage(Messages.get( + provinceId > 0 && Provinces.skipTerrain(provinceId) ? "skip-terrain" : "unknown-province")); + return; + } + if (GroupLoader.get(args[2]) == null) { + sender.sendMessage(Messages.get("unknown-group", "group", args[2])); + return; + } + if (!GroupLoader.allowsTerrain(args[2], provinceId)) { + sender.sendMessage(Messages.get("wrong-terrain", + "group", GroupLoader.displayName(args[2]), + "terrains", GroupLoader.terrainList(args[2]))); + return; + } + Severity severity = Severity.fromString(args[3]); + if (severity == null) { + sender.sendMessage(Messages.get("unknown-severity", "severity", args[3])); + return; + } + var manager = Infestations.plugin.getInfestationManager(); + if (manager.get(provinceId) != null) { + sender.sendMessage(Messages.get("already-infested", "id", String.valueOf(provinceId))); + return; + } + manager.set(provinceId, args[2].toLowerCase(Locale.ROOT), severity); + sender.sendMessage(Messages.get("set-ok", "id", String.valueOf(provinceId), + "group", GroupLoader.displayName(args[2]), + "severity", severity.display())); + } + + private void handleClear(CommandSender sender, String[] args) { + if (args.length < 2) { + sender.sendMessage(Messages.get("usage")); + return; + } + Integer provinceId = resolveProvince(sender, args[1]); + if (provinceId == null) { + return; + } + var manager = Infestations.plugin.getInfestationManager(); + if (manager.get(provinceId) == null) { + sender.sendMessage(Messages.get("not-infested", "id", String.valueOf(provinceId))); + return; + } + manager.clear(provinceId, false); + sender.sendMessage(Messages.get("clear-ok", "id", String.valueOf(provinceId))); + } + + private void handleList(CommandSender sender) { + var all = Infestations.plugin.getInfestationManager().all(); + if (!all.iterator().hasNext()) { + sender.sendMessage(Messages.get("list-empty")); + return; + } + for (Infestation infestation : all) { + String lure = infestation.getPhase() != LurePhase.NONE ? Messages.get("list-lure-suffix") : ""; + sender.sendMessage(Messages.get("list-line", + "id", String.valueOf(infestation.getProvinceId()), + "group", GroupLoader.displayName(infestation.getGroupId()), + "severity", infestation.getSeverity().display(), + "lure", lure)); + } + } + + private Integer resolveProvince(CommandSender sender, String token) { + if ("here".equalsIgnoreCase(token)) { + if (!(sender instanceof Player player)) { + sender.sendMessage(Messages.get("player-only")); + return null; + } + int id = Provinces.at(player); + if (!Provinces.validLand(id)) { + sender.sendMessage(Messages.get( + id > 0 && Provinces.skipTerrain(id) ? "skip-terrain" : "unknown-province")); + return null; + } + return id; + } + try { + return Integer.parseInt(token); + } catch (NumberFormatException ex) { + sender.sendMessage(Messages.get("unknown-province")); + return null; + } + } + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + if (args.length == 1) { + List options = new ArrayList<>(); + if (sender.hasPermission("infestations.admin.reload")) { + options.add("reload"); + } + if (sender.hasPermission("infestations.admin")) { + options.add("set"); + options.add("clear"); + options.add("list"); + } + return prefix(options, args[0]); + } + if (!sender.hasPermission("infestations.admin")) { + return List.of(); + } + if (args.length == 2 && (args[0].equalsIgnoreCase("set") || args[0].equalsIgnoreCase("clear"))) { + return prefix(List.of("here"), args[1]); + } + if (args.length == 3 && args[0].equalsIgnoreCase("set")) { + return prefix(new ArrayList<>(GroupLoader.groupIds()), args[2]); + } + if (args.length == 4 && args[0].equalsIgnoreCase("set")) { + return prefix(Arrays.stream(Severity.values()).map(Severity::id).collect(Collectors.toList()), args[3]); + } + return List.of(); + } + + private static List prefix(List options, String typed) { + String t = typed.toLowerCase(Locale.ROOT); + List out = new ArrayList<>(); + for (String o : options) { + if (o.toLowerCase(Locale.ROOT).startsWith(t)) { + out.add(o); + } + } + return out; + } +} diff --git a/src/main/java/net/tfminecraft/infestations/database/InfestationData.java b/src/main/java/net/tfminecraft/infestations/database/InfestationData.java index 13a3a7e..d040ad9 100644 --- a/src/main/java/net/tfminecraft/infestations/database/InfestationData.java +++ b/src/main/java/net/tfminecraft/infestations/database/InfestationData.java @@ -1,27 +1,27 @@ -package net.tfminecraft.infestations.database; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class InfestationData { - - public int provinceId; - public String groupId; - public String severity; - public String phase; - public String worldName; - public Integer lureX; - public Integer lureY; - public Integer lureZ; - public long joinEndsAt; - public int lureRemaining; - public int pendingSpawns; - public int enemiesAlive; - public int ambientAlive; - public long lureActivatedAt; - public List committed = new ArrayList<>(); - public Map logoutGraceUntil = new HashMap<>(); - public List deathOnLogin = new ArrayList<>(); -} +package net.tfminecraft.infestations.database; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class InfestationData { + + public int provinceId; + public String groupId; + public String severity; + public String phase; + public String worldName; + public Integer lureX; + public Integer lureY; + public Integer lureZ; + public long joinEndsAt; + public int lureRemaining; + public int pendingSpawns; + public int enemiesAlive; + public int ambientAlive; + public long lureActivatedAt; + public List committed = new ArrayList<>(); + public Map logoutGraceUntil = new HashMap<>(); + public List deathOnLogin = new ArrayList<>(); +} diff --git a/src/main/java/net/tfminecraft/infestations/database/InfestationDatabase.java b/src/main/java/net/tfminecraft/infestations/database/InfestationDatabase.java index 1b9d212..a72cbed 100644 --- a/src/main/java/net/tfminecraft/infestations/database/InfestationDatabase.java +++ b/src/main/java/net/tfminecraft/infestations/database/InfestationDatabase.java @@ -1,156 +1,156 @@ -package net.tfminecraft.infestations.database; - -import java.io.File; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.reflect.TypeToken; - -import net.tfminecraft.infestations.Infestations; -import net.tfminecraft.infestations.infestation.Infestation; -import net.tfminecraft.infestations.infestation.LurePhase; -import net.tfminecraft.infestations.infestation.Severity; - -public final class InfestationDatabase { - - private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); - - private InfestationDatabase() {} - - public static File file() { - return new File(Infestations.plugin.getDataFolder(), "Data/infestations.json"); - } - - public static List load() { - File file = file(); - if (!file.exists()) { - return new ArrayList<>(); - } - try (FileReader reader = new FileReader(file, StandardCharsets.UTF_8)) { - List rows = GSON.fromJson(reader, new TypeToken>() {}.getType()); - List out = new ArrayList<>(); - if (rows == null) { - return out; - } - for (InfestationData row : rows) { - Infestation infestation = fromData(row); - if (infestation != null) { - out.add(infestation); - } - } - return out; - } catch (Exception ex) { - Infestations.plugin.getLogger().severe("[Infestations] Failed to load infestations: " + ex.getMessage()); - return new ArrayList<>(); - } - } - - public static void save(Collection infestations) { - File file = file(); - File parent = file.getParentFile(); - if (parent != null) { - parent.mkdirs(); - } - List rows = new ArrayList<>(); - for (Infestation infestation : infestations) { - rows.add(toData(infestation)); - } - try (FileWriter writer = new FileWriter(file, StandardCharsets.UTF_8)) { - GSON.toJson(rows, writer); - } catch (IOException ex) { - Infestations.plugin.getLogger().severe("[Infestations] Failed to save infestations: " + ex.getMessage()); - } - } - - private static Infestation fromData(InfestationData row) { - if (row == null) { - return null; - } - Severity severity = Severity.fromString(row.severity); - if (severity == null || row.groupId == null) { - return null; - } - Infestation infestation = new Infestation(row.provinceId, row.groupId, severity); - LurePhase phase = LurePhase.NONE; - if (row.phase != null) { - try { - phase = LurePhase.valueOf(row.phase); - } catch (IllegalArgumentException ignored) { - phase = LurePhase.NONE; - } - } - infestation.setPhase(phase); - infestation.setWorldName(row.worldName); - infestation.setLureX(row.lureX); - infestation.setLureY(row.lureY); - infestation.setLureZ(row.lureZ); - infestation.setJoinEndsAt(row.joinEndsAt); - infestation.setLureRemaining(row.lureRemaining); - infestation.setPendingSpawns(row.pendingSpawns); - infestation.setEnemiesAlive(row.enemiesAlive); - infestation.setAmbientAlive(row.ambientAlive); - infestation.setLureActivatedAt(row.lureActivatedAt); - if (row.committed != null) { - for (String id : row.committed) { - try { - infestation.getCommitted().add(UUID.fromString(id)); - } catch (IllegalArgumentException ignored) { - } - } - } - if (row.logoutGraceUntil != null) { - for (Map.Entry e : row.logoutGraceUntil.entrySet()) { - try { - infestation.getLogoutGraceUntil().put(UUID.fromString(e.getKey()), e.getValue()); - } catch (IllegalArgumentException ignored) { - } - } - } - if (row.deathOnLogin != null) { - for (String id : row.deathOnLogin) { - try { - infestation.getDeathOnLogin().add(UUID.fromString(id)); - } catch (IllegalArgumentException ignored) { - } - } - } - return infestation; - } - - private static InfestationData toData(Infestation infestation) { - InfestationData row = new InfestationData(); - row.provinceId = infestation.getProvinceId(); - row.groupId = infestation.getGroupId(); - row.severity = infestation.getSeverity().id(); - row.phase = infestation.getPhase().name(); - row.worldName = infestation.getWorldName(); - row.lureX = infestation.getLureX(); - row.lureY = infestation.getLureY(); - row.lureZ = infestation.getLureZ(); - row.joinEndsAt = infestation.getJoinEndsAt(); - row.lureRemaining = infestation.getLureRemaining(); - row.pendingSpawns = infestation.getPendingSpawns(); - row.enemiesAlive = infestation.getEnemiesAlive(); - row.ambientAlive = infestation.getAmbientAlive(); - row.lureActivatedAt = infestation.getLureActivatedAt(); - for (UUID id : infestation.getCommitted()) { - row.committed.add(id.toString()); - } - for (Map.Entry e : infestation.getLogoutGraceUntil().entrySet()) { - row.logoutGraceUntil.put(e.getKey().toString(), e.getValue()); - } - for (UUID id : infestation.getDeathOnLogin()) { - row.deathOnLogin.add(id.toString()); - } - return row; - } -} +package net.tfminecraft.infestations.database; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; + +import net.tfminecraft.infestations.Infestations; +import net.tfminecraft.infestations.infestation.Infestation; +import net.tfminecraft.infestations.infestation.LurePhase; +import net.tfminecraft.infestations.infestation.Severity; + +public final class InfestationDatabase { + + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + private InfestationDatabase() {} + + public static File file() { + return new File(Infestations.plugin.getDataFolder(), "Data/infestations.json"); + } + + public static List load() { + File file = file(); + if (!file.exists()) { + return new ArrayList<>(); + } + try (FileReader reader = new FileReader(file, StandardCharsets.UTF_8)) { + List rows = GSON.fromJson(reader, new TypeToken>() {}.getType()); + List out = new ArrayList<>(); + if (rows == null) { + return out; + } + for (InfestationData row : rows) { + Infestation infestation = fromData(row); + if (infestation != null) { + out.add(infestation); + } + } + return out; + } catch (Exception ex) { + Infestations.plugin.getLogger().severe("[Infestations] Failed to load infestations: " + ex.getMessage()); + return new ArrayList<>(); + } + } + + public static void save(Collection infestations) { + File file = file(); + File parent = file.getParentFile(); + if (parent != null) { + parent.mkdirs(); + } + List rows = new ArrayList<>(); + for (Infestation infestation : infestations) { + rows.add(toData(infestation)); + } + try (FileWriter writer = new FileWriter(file, StandardCharsets.UTF_8)) { + GSON.toJson(rows, writer); + } catch (IOException ex) { + Infestations.plugin.getLogger().severe("[Infestations] Failed to save infestations: " + ex.getMessage()); + } + } + + private static Infestation fromData(InfestationData row) { + if (row == null) { + return null; + } + Severity severity = Severity.fromString(row.severity); + if (severity == null || row.groupId == null) { + return null; + } + Infestation infestation = new Infestation(row.provinceId, row.groupId, severity); + LurePhase phase = LurePhase.NONE; + if (row.phase != null) { + try { + phase = LurePhase.valueOf(row.phase); + } catch (IllegalArgumentException ignored) { + phase = LurePhase.NONE; + } + } + infestation.setPhase(phase); + infestation.setWorldName(row.worldName); + infestation.setLureX(row.lureX); + infestation.setLureY(row.lureY); + infestation.setLureZ(row.lureZ); + infestation.setJoinEndsAt(row.joinEndsAt); + infestation.setLureRemaining(row.lureRemaining); + infestation.setPendingSpawns(row.pendingSpawns); + infestation.setEnemiesAlive(row.enemiesAlive); + infestation.setAmbientAlive(row.ambientAlive); + infestation.setLureActivatedAt(row.lureActivatedAt); + if (row.committed != null) { + for (String id : row.committed) { + try { + infestation.getCommitted().add(UUID.fromString(id)); + } catch (IllegalArgumentException ignored) { + } + } + } + if (row.logoutGraceUntil != null) { + for (Map.Entry e : row.logoutGraceUntil.entrySet()) { + try { + infestation.getLogoutGraceUntil().put(UUID.fromString(e.getKey()), e.getValue()); + } catch (IllegalArgumentException ignored) { + } + } + } + if (row.deathOnLogin != null) { + for (String id : row.deathOnLogin) { + try { + infestation.getDeathOnLogin().add(UUID.fromString(id)); + } catch (IllegalArgumentException ignored) { + } + } + } + return infestation; + } + + private static InfestationData toData(Infestation infestation) { + InfestationData row = new InfestationData(); + row.provinceId = infestation.getProvinceId(); + row.groupId = infestation.getGroupId(); + row.severity = infestation.getSeverity().id(); + row.phase = infestation.getPhase().name(); + row.worldName = infestation.getWorldName(); + row.lureX = infestation.getLureX(); + row.lureY = infestation.getLureY(); + row.lureZ = infestation.getLureZ(); + row.joinEndsAt = infestation.getJoinEndsAt(); + row.lureRemaining = infestation.getLureRemaining(); + row.pendingSpawns = infestation.getPendingSpawns(); + row.enemiesAlive = infestation.getEnemiesAlive(); + row.ambientAlive = infestation.getAmbientAlive(); + row.lureActivatedAt = infestation.getLureActivatedAt(); + for (UUID id : infestation.getCommitted()) { + row.committed.add(id.toString()); + } + for (Map.Entry e : infestation.getLogoutGraceUntil().entrySet()) { + row.logoutGraceUntil.put(e.getKey().toString(), e.getValue()); + } + for (UUID id : infestation.getDeathOnLogin()) { + row.deathOnLogin.add(id.toString()); + } + return row; + } +} diff --git a/src/main/java/net/tfminecraft/infestations/infestation/Infestation.java b/src/main/java/net/tfminecraft/infestations/infestation/Infestation.java index 7ea4b1e..c570b82 100644 --- a/src/main/java/net/tfminecraft/infestations/infestation/Infestation.java +++ b/src/main/java/net/tfminecraft/infestations/infestation/Infestation.java @@ -1,262 +1,262 @@ -package net.tfminecraft.infestations.infestation; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.UUID; - -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.block.Block; - -public final class Infestation { - - private final int provinceId; - private String groupId; - private Severity severity; - private LurePhase phase = LurePhase.NONE; - private String worldName; - private Integer lureX; - private Integer lureY; - private Integer lureZ; - private long joinEndsAt; - private int lureRemaining; - private int pendingSpawns; - private int enemiesAlive; - private int ambientAlive; - private int waveRetryAtTick; - private long lureActivatedAt; - private final Set committed = new HashSet<>(); - private final Map logoutGraceUntil = new HashMap<>(); - private final Set deathOnLogin = new HashSet<>(); - - public Infestation(int provinceId, String groupId, Severity severity) { - this.provinceId = provinceId; - this.groupId = groupId; - this.severity = severity; - } - - public int getProvinceId() { - return provinceId; - } - - public String getGroupId() { - return groupId; - } - - public void setGroupId(String groupId) { - this.groupId = groupId; - } - - public Severity getSeverity() { - return severity; - } - - public void setSeverity(Severity severity) { - this.severity = severity; - } - - public LurePhase getPhase() { - return phase; - } - - public void setPhase(LurePhase phase) { - this.phase = phase; - } - - public boolean hasLure() { - return phase != LurePhase.NONE && worldName != null && lureX != null; - } - - public Location lureLocation() { - if (worldName == null || lureX == null || lureY == null || lureZ == null) { - return null; - } - World world = Bukkit.getWorld(worldName); - if (world == null) { - return null; - } - return new Location(world, lureX + 0.5, lureY, lureZ + 0.5); - } - - public Block lureBlock() { - if (worldName == null || lureX == null || lureY == null || lureZ == null) { - return null; - } - World world = Bukkit.getWorld(worldName); - if (world == null) { - return null; - } - return world.getBlockAt(lureX, lureY, lureZ); - } - - public boolean isLureBlock(Block block) { - if (block == null || worldName == null || lureX == null) { - return false; - } - return worldName.equals(block.getWorld().getName()) - && lureX == block.getX() - && lureY == block.getY() - && lureZ == block.getZ(); - } - - public void placeLure(Block block, long joinEndsAt, int budget) { - this.worldName = block.getWorld().getName(); - this.lureX = block.getX(); - this.lureY = block.getY(); - this.lureZ = block.getZ(); - this.phase = LurePhase.JOINING; - this.joinEndsAt = joinEndsAt; - this.lureRemaining = budget; - this.pendingSpawns = 0; - this.enemiesAlive = 0; - this.committed.clear(); - this.logoutGraceUntil.clear(); - this.deathOnLogin.clear(); - this.waveRetryAtTick = 0; - this.lureActivatedAt = 0; - } - - public void clearLure() { - phase = LurePhase.NONE; - worldName = null; - lureX = null; - lureY = null; - lureZ = null; - joinEndsAt = 0; - lureRemaining = 0; - pendingSpawns = 0; - enemiesAlive = 0; - committed.clear(); - logoutGraceUntil.clear(); - deathOnLogin.clear(); - waveRetryAtTick = 0; - lureActivatedAt = 0; - } - - public String getWorldName() { - return worldName; - } - - public void setWorldName(String worldName) { - this.worldName = worldName; - } - - public Integer getLureX() { - return lureX; - } - - public void setLureX(Integer lureX) { - this.lureX = lureX; - } - - public Integer getLureY() { - return lureY; - } - - public void setLureY(Integer lureY) { - this.lureY = lureY; - } - - public Integer getLureZ() { - return lureZ; - } - - public void setLureZ(Integer lureZ) { - this.lureZ = lureZ; - } - - public long getJoinEndsAt() { - return joinEndsAt; - } - - public void setJoinEndsAt(long joinEndsAt) { - this.joinEndsAt = joinEndsAt; - } - - public int joinSecondsLeft() { - long left = joinEndsAt - System.currentTimeMillis(); - return (int) Math.max(0, Math.ceil(left / 1000.0)); - } - - public int getLureRemaining() { - return lureRemaining; - } - - public void setLureRemaining(int lureRemaining) { - this.lureRemaining = Math.max(0, lureRemaining); - } - - public int displayRemaining() { - if (phase == LurePhase.JOINING) { - return lureRemaining; - } - return Math.max(lureRemaining, pendingSpawns + enemiesAlive); - } - - public int getPendingSpawns() { - return pendingSpawns; - } - - public void setPendingSpawns(int pendingSpawns) { - this.pendingSpawns = Math.max(0, pendingSpawns); - } - - public int getEnemiesAlive() { - return enemiesAlive; - } - - public void setEnemiesAlive(int enemiesAlive) { - this.enemiesAlive = Math.max(0, enemiesAlive); - } - - public int getAmbientAlive() { - return ambientAlive; - } - - public void setAmbientAlive(int ambientAlive) { - this.ambientAlive = Math.max(0, ambientAlive); - } - - public int getWaveRetryAtTick() { - return waveRetryAtTick; - } - - public void setWaveRetryAtTick(int waveRetryAtTick) { - this.waveRetryAtTick = Math.max(0, waveRetryAtTick); - } - - public long getLureActivatedAt() { - return lureActivatedAt; - } - - public void setLureActivatedAt(long lureActivatedAt) { - this.lureActivatedAt = Math.max(0, lureActivatedAt); - } - - public int ambientCap() { - return net.tfminecraft.infestations.loader.GroupLoader.tune(groupId, severity).ambientCap(); - } - - public int lureBudget() { - return net.tfminecraft.infestations.loader.GroupLoader.tune(groupId, severity).lureCount(); - } - - public Set getCommitted() { - return committed; - } - - public Map getLogoutGraceUntil() { - return logoutGraceUntil; - } - - public Set getDeathOnLogin() { - return deathOnLogin; - } - - public boolean isCommitted(UUID id) { - return committed.contains(id); - } -} +package net.tfminecraft.infestations.infestation; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; + +public final class Infestation { + + private final int provinceId; + private String groupId; + private Severity severity; + private LurePhase phase = LurePhase.NONE; + private String worldName; + private Integer lureX; + private Integer lureY; + private Integer lureZ; + private long joinEndsAt; + private int lureRemaining; + private int pendingSpawns; + private int enemiesAlive; + private int ambientAlive; + private int waveRetryAtTick; + private long lureActivatedAt; + private final Set committed = new HashSet<>(); + private final Map logoutGraceUntil = new HashMap<>(); + private final Set deathOnLogin = new HashSet<>(); + + public Infestation(int provinceId, String groupId, Severity severity) { + this.provinceId = provinceId; + this.groupId = groupId; + this.severity = severity; + } + + public int getProvinceId() { + return provinceId; + } + + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + public Severity getSeverity() { + return severity; + } + + public void setSeverity(Severity severity) { + this.severity = severity; + } + + public LurePhase getPhase() { + return phase; + } + + public void setPhase(LurePhase phase) { + this.phase = phase; + } + + public boolean hasLure() { + return phase != LurePhase.NONE && worldName != null && lureX != null; + } + + public Location lureLocation() { + if (worldName == null || lureX == null || lureY == null || lureZ == null) { + return null; + } + World world = Bukkit.getWorld(worldName); + if (world == null) { + return null; + } + return new Location(world, lureX + 0.5, lureY, lureZ + 0.5); + } + + public Block lureBlock() { + if (worldName == null || lureX == null || lureY == null || lureZ == null) { + return null; + } + World world = Bukkit.getWorld(worldName); + if (world == null) { + return null; + } + return world.getBlockAt(lureX, lureY, lureZ); + } + + public boolean isLureBlock(Block block) { + if (block == null || worldName == null || lureX == null) { + return false; + } + return worldName.equals(block.getWorld().getName()) + && lureX == block.getX() + && lureY == block.getY() + && lureZ == block.getZ(); + } + + public void placeLure(Block block, long joinEndsAt, int budget) { + this.worldName = block.getWorld().getName(); + this.lureX = block.getX(); + this.lureY = block.getY(); + this.lureZ = block.getZ(); + this.phase = LurePhase.JOINING; + this.joinEndsAt = joinEndsAt; + this.lureRemaining = budget; + this.pendingSpawns = 0; + this.enemiesAlive = 0; + this.committed.clear(); + this.logoutGraceUntil.clear(); + this.deathOnLogin.clear(); + this.waveRetryAtTick = 0; + this.lureActivatedAt = 0; + } + + public void clearLure() { + phase = LurePhase.NONE; + worldName = null; + lureX = null; + lureY = null; + lureZ = null; + joinEndsAt = 0; + lureRemaining = 0; + pendingSpawns = 0; + enemiesAlive = 0; + committed.clear(); + logoutGraceUntil.clear(); + deathOnLogin.clear(); + waveRetryAtTick = 0; + lureActivatedAt = 0; + } + + public String getWorldName() { + return worldName; + } + + public void setWorldName(String worldName) { + this.worldName = worldName; + } + + public Integer getLureX() { + return lureX; + } + + public void setLureX(Integer lureX) { + this.lureX = lureX; + } + + public Integer getLureY() { + return lureY; + } + + public void setLureY(Integer lureY) { + this.lureY = lureY; + } + + public Integer getLureZ() { + return lureZ; + } + + public void setLureZ(Integer lureZ) { + this.lureZ = lureZ; + } + + public long getJoinEndsAt() { + return joinEndsAt; + } + + public void setJoinEndsAt(long joinEndsAt) { + this.joinEndsAt = joinEndsAt; + } + + public int joinSecondsLeft() { + long left = joinEndsAt - System.currentTimeMillis(); + return (int) Math.max(0, Math.ceil(left / 1000.0)); + } + + public int getLureRemaining() { + return lureRemaining; + } + + public void setLureRemaining(int lureRemaining) { + this.lureRemaining = Math.max(0, lureRemaining); + } + + public int displayRemaining() { + if (phase == LurePhase.JOINING) { + return lureRemaining; + } + return Math.max(lureRemaining, pendingSpawns + enemiesAlive); + } + + public int getPendingSpawns() { + return pendingSpawns; + } + + public void setPendingSpawns(int pendingSpawns) { + this.pendingSpawns = Math.max(0, pendingSpawns); + } + + public int getEnemiesAlive() { + return enemiesAlive; + } + + public void setEnemiesAlive(int enemiesAlive) { + this.enemiesAlive = Math.max(0, enemiesAlive); + } + + public int getAmbientAlive() { + return ambientAlive; + } + + public void setAmbientAlive(int ambientAlive) { + this.ambientAlive = Math.max(0, ambientAlive); + } + + public int getWaveRetryAtTick() { + return waveRetryAtTick; + } + + public void setWaveRetryAtTick(int waveRetryAtTick) { + this.waveRetryAtTick = Math.max(0, waveRetryAtTick); + } + + public long getLureActivatedAt() { + return lureActivatedAt; + } + + public void setLureActivatedAt(long lureActivatedAt) { + this.lureActivatedAt = Math.max(0, lureActivatedAt); + } + + public int ambientCap() { + return net.tfminecraft.infestations.loader.GroupLoader.tune(groupId, severity).ambientCap(); + } + + public int lureBudget() { + return net.tfminecraft.infestations.loader.GroupLoader.tune(groupId, severity).lureCount(); + } + + public Set getCommitted() { + return committed; + } + + public Map getLogoutGraceUntil() { + return logoutGraceUntil; + } + + public Set getDeathOnLogin() { + return deathOnLogin; + } + + public boolean isCommitted(UUID id) { + return committed.contains(id); + } +} diff --git a/src/main/java/net/tfminecraft/infestations/infestation/InfestationManager.java b/src/main/java/net/tfminecraft/infestations/infestation/InfestationManager.java index a075039..d69a36d 100644 --- a/src/main/java/net/tfminecraft/infestations/infestation/InfestationManager.java +++ b/src/main/java/net/tfminecraft/infestations/infestation/InfestationManager.java @@ -1,964 +1,964 @@ -package net.tfminecraft.infestations.infestation; - -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.ThreadLocalRandom; - -import org.bukkit.Bukkit; -import org.bukkit.GameMode; -import org.bukkit.Location; -import org.bukkit.Particle; -import org.bukkit.Sound; -import org.bukkit.block.Block; -import org.bukkit.block.BlockFace; -import org.bukkit.World; -import org.bukkit.entity.LivingEntity; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.EventPriority; -import org.bukkit.event.Listener; -import org.bukkit.event.block.Action; -import org.bukkit.event.block.BlockBreakEvent; -import org.bukkit.event.block.BlockExplodeEvent; -import org.bukkit.event.block.BlockPistonExtendEvent; -import org.bukkit.event.block.BlockPistonRetractEvent; -import org.bukkit.event.block.BlockPlaceEvent; -import org.bukkit.event.entity.EntityDeathEvent; -import org.bukkit.event.entity.EntityExplodeEvent; -import org.bukkit.event.player.PlayerInteractEvent; -import org.bukkit.event.player.PlayerJoinEvent; -import org.bukkit.event.player.PlayerQuitEvent; -import org.bukkit.event.world.ChunkLoadEvent; -import org.bukkit.event.world.ChunkUnloadEvent; -import org.bukkit.inventory.EquipmentSlot; -import org.bukkit.inventory.ItemStack; -import org.bukkit.potion.PotionEffect; -import org.bukkit.potion.PotionEffectType; -import org.bukkit.scheduler.BukkitTask; - -import net.md_5.bungee.api.ChatMessageType; -import net.md_5.bungee.api.chat.TextComponent; - -import me.Plugins.SimpleFactions.Events.PlayerProvinceEnterEvent; -import me.Plugins.SimpleFactions.Events.PlayerProvinceLeaveEvent; -import me.Plugins.TLibs.TLibs; -import net.tfminecraft.infestations.Infestations; -import net.tfminecraft.infestations.Messages; -import net.tfminecraft.infestations.cache.Cache; -import net.tfminecraft.infestations.database.InfestationDatabase; -import net.tfminecraft.events.FurnitureBreakEvent; -import net.tfminecraft.events.FurnitureInteractEvent; -import net.tfminecraft.events.FurniturePlaceEvent; -import net.tfminecraft.furniture.Furniture; -import net.tfminecraft.infestations.loader.GroupLoader; -import net.tfminecraft.infestations.loader.GroupLoader.GroupTune; -import net.tfminecraft.infestations.lure.LureFurniture; -import net.tfminecraft.infestations.lure.LureHologram; -import net.tfminecraft.infestations.map.InfestationMapExport; -import net.tfminecraft.infestations.spawn.AmbientSpawnService; -import net.tfminecraft.infestations.spawn.Keys; -import net.tfminecraft.infestations.spawn.MythicSpawner; -import net.tfminecraft.infestations.spawn.SpawnLog; -import net.tfminecraft.infestations.spawn.SpawnPlanner; -import net.tfminecraft.infestations.utils.Provinces; - -public final class InfestationManager implements Listener, AmbientSpawnService.CollectionInfestations { - - private final Infestations plugin; - private final Map byProvince = new HashMap<>(); - private final AmbientSpawnService ambient = new AmbientSpawnService(); - private boolean pluginFurnitureRemove; - private BukkitTask task; - private int tick; - private int lastLureHighlightTick = -1; - private int lastLureHighlightProvince = -1; - - public InfestationManager(Infestations plugin) { - this.plugin = plugin; - } - - public void start() { - for (Infestation infestation : InfestationDatabase.load()) { - byProvince.put(infestation.getProvinceId(), infestation); - } - InfestationMapExport.exportAsync(byProvince.values()); - task = Bukkit.getScheduler().runTaskTimer(plugin, this::tick, 20L, 1L); - Bukkit.getScheduler().runTaskLater(plugin, () -> { - sweepLoadedChunks(); - ambient.reconcileLoaded(this); - }, 20L); - } - - public void shutdown() { - if (task != null) { - task.cancel(); - task = null; - } - for (Infestation infestation : byProvince.values()) { - LureHologram.remove(infestation); - } - save(); - } - - public Infestation get(int provinceId) { - return byProvince.get(provinceId); - } - - public Collection all() { - return byProvince.values(); - } - - public void set(int provinceId, String groupId, Severity severity) { - byProvince.put(provinceId, new Infestation(provinceId, groupId, severity)); - save(); - InfestationMapExport.exportAsync(byProvince.values()); - } - - public void clear(int provinceId, boolean victory) { - Infestation infestation = byProvince.get(provinceId); - if (infestation == null) { - return; - } - if (infestation.hasLure()) { - destroyLure(infestation, victory); - } - byProvince.remove(provinceId); - save(); - InfestationMapExport.exportAsync(byProvince.values()); - } - - public void reloadFromDisk() { - Map previous = new HashMap<>(byProvince); - byProvince.clear(); - for (Infestation infestation : InfestationDatabase.load()) { - byProvince.put(infestation.getProvinceId(), infestation); - } - pluginFurnitureRemove = true; - try { - for (Infestation old : previous.values()) { - if (!old.hasLure()) { - continue; - } - Infestation current = byProvince.get(old.getProvinceId()); - if (current != null && current.hasLure() && sameLureBlock(old, current)) { - continue; - } - LureHologram.remove(old); - LureFurniture.removeFor(old); - } - } finally { - pluginFurnitureRemove = false; - } - sweepLoadedChunks(); - ambient.reconcileLoaded(this); - save(); - InfestationMapExport.exportAsync(byProvince.values()); - } - - private static boolean sameLureBlock(Infestation a, Infestation b) { - return a.getWorldName() != null - && a.getWorldName().equals(b.getWorldName()) - && java.util.Objects.equals(a.getLureX(), b.getLureX()) - && java.util.Objects.equals(a.getLureY(), b.getLureY()) - && java.util.Objects.equals(a.getLureZ(), b.getLureZ()); - } - - public void save() { - InfestationDatabase.save(byProvince.values()); - } - - private void tick() { - tick++; - long now = System.currentTimeMillis(); - ambient.tick(this, tick); - - for (Infestation infestation : List.copyOf(byProvince.values())) { - expireGrace(infestation, now); - if (infestation.getPhase() == LurePhase.JOINING) { - warnJoining(infestation); - if (now >= infestation.getJoinEndsAt()) { - activate(infestation); - } - } - if (infestation.getPhase() == LurePhase.ACTIVE) { - warnNonJoiners(infestation); - spawnLureWave(infestation); - checkWipe(infestation); - } - if (infestation.hasLure()) { - LureHologram.tick(infestation); - } - tickDeserters(infestation); - } - - tickSpread(); - - if (tick % 100 == 0) { - save(); - } - } - - private void tickSpread() { - if (!Cache.spread) { - return; - } - int intervalTicks = Cache.spreadIntervalSeconds * 20; - if (intervalTicks <= 0 || tick % intervalTicks != 0) { - return; - } - boolean changed = false; - for (Infestation source : List.copyOf(byProvince.values())) { - if (source.getPhase() != LurePhase.NONE) { - continue; - } - if (source.getSeverity().canWorsen() - && ThreadLocalRandom.current().nextDouble() < Cache.worsenChance) { - source.setSeverity(source.getSeverity().worse()); - changed = true; - if (Cache.debug) { - plugin.getLogger().info("[Infestations] Spread worsened " - + source.getProvinceId() + " to " + source.getSeverity().id()); - } - } - SpreadPick pick = pickSpreadTarget(source.getProvinceId(), source.getSeverity(), source.getGroupId()); - if (pick == null) { - continue; - } - byProvince.put(pick.provinceId, new Infestation(pick.provinceId, source.getGroupId(), Severity.MILD)); - changed = true; - if (Cache.debug) { - plugin.getLogger().info("[Infestations] Spread " + source.getGroupId() - + " from " + source.getProvinceId() + " to " + pick.provinceId - + " via " + pick.via.id()); - } - } - if (changed) { - save(); - InfestationMapExport.exportAsync(byProvince.values()); - } - } - - private SpreadPick pickSpreadTarget(int fromId, Severity severity, String groupId) { - Map best = new HashMap<>(); - for (int neighborId : Provinces.neighbours(fromId)) { - if (canInfect(neighborId, groupId)) { - offerSpread(best, neighborId, SpreadVia.LAND); - continue; - } - if (Provinces.isWater(neighborId)) { - collectWaterOrSeaHop(fromId, neighborId, groupId, SpreadVia.WATER, best); - } else if (Provinces.isSea(neighborId)) { - collectWaterOrSeaHop(fromId, neighborId, groupId, SpreadVia.SEA, best); - } - } - if (best.isEmpty()) { - return null; - } - int provinceId = weightedSpreadPick(best, severity); - SpreadVia via = best.get(provinceId); - if (ThreadLocalRandom.current().nextDouble() >= via.chance(severity)) { - if (Cache.debug) { - plugin.getLogger().info("[Infestations] Spread hop failed via " + via.id() - + " from " + fromId + " toward " + provinceId); - } - return null; - } - return new SpreadPick(provinceId, via); - } - - private void collectWaterOrSeaHop( - int fromId, int mediumId, String groupId, SpreadVia via, Map best) { - for (int farId : Provinces.neighbours(mediumId)) { - if (farId == fromId || !canInfect(farId, groupId)) { - continue; - } - offerSpread(best, farId, via); - } - } - - private boolean canInfect(int provinceId, String groupId) { - return Provinces.validLand(provinceId) - && !byProvince.containsKey(provinceId) - && GroupLoader.allowsTerrain(groupId, provinceId); - } - - private static void offerSpread(Map best, int provinceId, SpreadVia via) { - SpreadVia current = best.get(provinceId); - if (current == null || via.easierThan(current)) { - best.put(provinceId, via); - } - } - - private static int weightedSpreadPick(Map best, Severity severity) { - double total = 0; - for (SpreadVia via : best.values()) { - total += Math.max(0.0001, via.chance(severity)); - } - double roll = ThreadLocalRandom.current().nextDouble() * total; - int last = -1; - for (Map.Entry entry : best.entrySet()) { - last = entry.getKey(); - roll -= Math.max(0.0001, entry.getValue().chance(severity)); - if (roll <= 0) { - return entry.getKey(); - } - } - return last; - } - - private enum SpreadVia { - LAND, - WATER, - SEA; - - boolean easierThan(SpreadVia other) { - return ordinal() < other.ordinal(); - } - - double chance(Severity severity) { - return Cache.hopChance(severity, ordinal()); - } - - String id() { - return name().toLowerCase(); - } - } - - private record SpreadPick(int provinceId, SpreadVia via) {} - - private void sweepLoadedChunks() { - pluginFurnitureRemove = true; - try { - for (org.bukkit.World world : Bukkit.getWorlds()) { - for (org.bukkit.Chunk chunk : world.getLoadedChunks()) { - LureFurniture.sweepChunk(chunk, this::get); - } - } - } finally { - pluginFurnitureRemove = false; - } - } - - private void activate(Infestation infestation) { - infestation.setPhase(LurePhase.ACTIVE); - infestation.setLureActivatedAt(System.currentTimeMillis()); - spawnLureWave(infestation); - save(); - } - - private void spawnLureWave(Infestation infestation) { - if (tick < infestation.getWaveRetryAtTick()) { - return; - } - Location origin = infestation.lureLocation(); - if (origin == null) { - return; - } - if (GroupLoader.blockedByNight(infestation.getGroupId(), origin.getWorld())) { - if (tick % 40 == 0) { - SpawnLog.line(infestation, "-", "night"); - } - return; - } - GroupTune tune = GroupLoader.tune(infestation.getGroupId(), infestation.getSeverity()); - long started = infestation.getLureActivatedAt(); - if (started <= 0) { - started = System.currentTimeMillis(); - infestation.setLureActivatedAt(started); - } - long elapsed = Math.max(0, System.currentTimeMillis() - started); - long durationMs = tune.lureDurationSeconds() * 1000L; - int lureCount = tune.lureCount(); - int allowed = durationMs <= 0 - ? lureCount - : (int) Math.min(lureCount, elapsed * lureCount / durationMs); - int killsDone = Math.max(0, lureCount - infestation.getLureRemaining()); - int need = allowed - infestation.getPendingSpawns() - infestation.getEnemiesAlive() - killsDone; - if (need <= 0) { - return; - } - List spots = SpawnPlanner.find( - origin, - 4, - Cache.lureSpawnRadius, - need, - loc -> Provinces.at(loc) == infestation.getProvinceId() - && GroupLoader.allowsY(infestation.getGroupId(), loc.getBlockY())); - if (spots.isEmpty()) { - infestation.setWaveRetryAtTick(tick + 40); - SpawnLog.line(infestation, "-", "no-spot"); - return; - } - infestation.setWaveRetryAtTick(0); - for (Location spot : spots) { - infestation.setPendingSpawns(infestation.getPendingSpawns() + 1); - int delay = ThreadLocalRandom.current().nextInt(20, 61); - Bukkit.getScheduler().runTaskLater(plugin, () -> { - if (infestation.getPhase() != LurePhase.ACTIVE) { - infestation.setPendingSpawns(Math.max(0, infestation.getPendingSpawns() - 1)); - return; - } - if (GroupLoader.blockedByNight(infestation.getGroupId(), spot.getWorld())) { - infestation.setPendingSpawns(Math.max(0, infestation.getPendingSpawns() - 1)); - SpawnLog.line(infestation, "-", "night"); - return; - } - LivingEntity spawned = MythicSpawner.spawn( - spot, infestation.getGroupId(), infestation.getProvinceId(), Keys.KIND_LURE); - infestation.setPendingSpawns(Math.max(0, infestation.getPendingSpawns() - 1)); - if (spawned != null) { - infestation.setEnemiesAlive(infestation.getEnemiesAlive() + 1); - SpawnLog.spawned( - infestation, - "-", - spot, - infestation.getEnemiesAlive(), - lureCount); - } else { - SpawnLog.line(infestation, "-", "unknown-mythic"); - } - }, delay); - } - } - - private void warnJoining(Infestation infestation) { - if (tick % 20 != 0) { - return; - } - String bar = Messages.get("lure-activating-bar", "seconds", String.valueOf(infestation.joinSecondsLeft())); - for (Player player : Bukkit.getOnlinePlayers()) { - if (Provinces.at(player) != infestation.getProvinceId()) { - continue; - } - actionBar(player, bar); - if (tick % 100 == 0) { - player.sendMessage(Messages.get("lure-leave-or-join")); - } - } - } - - private void warnNonJoiners(Infestation infestation) { - if (tick % 20 != 0) { - return; - } - String bar = Messages.get("lure-nonjoiner-bar"); - for (Player player : Bukkit.getOnlinePlayers()) { - if (player.getGameMode() == GameMode.CREATIVE || player.getGameMode() == GameMode.SPECTATOR) { - continue; - } - if (Provinces.at(player) != infestation.getProvinceId()) { - continue; - } - if (infestation.isCommitted(player.getUniqueId())) { - continue; - } - actionBar(player, bar); - player.damage(Cache.deserterDamage); - if (tick % 100 == 0) { - player.sendMessage(Messages.get("lure-nonjoiner")); - } - } - } - - private void tickDeserters(Infestation infestation) { - if (tick % 20 != 0 || infestation.getPhase() == LurePhase.NONE) { - return; - } - for (UUID id : List.copyOf(infestation.getCommitted())) { - Player player = Bukkit.getPlayer(id); - if (player == null || !player.isOnline() || player.isDead()) { - continue; - } - if (Provinces.at(player) == infestation.getProvinceId()) { - continue; - } - failLure(infestation); - return; - } - } - - private void expireGrace(Infestation infestation, long now) { - Iterator> it = infestation.getLogoutGraceUntil().entrySet().iterator(); - while (it.hasNext()) { - Map.Entry e = it.next(); - if (now >= e.getValue()) { - infestation.getDeathOnLogin().add(e.getKey()); - it.remove(); - } - } - } - - private void checkWipe(Infestation infestation) { - if (infestation.getPhase() != LurePhase.ACTIVE) { - return; - } - if (hasLivingOrGrace(infestation)) { - return; - } - failLure(infestation); - } - - private boolean hasLivingOrGrace(Infestation infestation) { - if (!infestation.getLogoutGraceUntil().isEmpty()) { - return true; - } - if (!infestation.getDeathOnLogin().isEmpty()) { - return true; - } - for (UUID id : infestation.getCommitted()) { - Player player = Bukkit.getPlayer(id); - if (player != null && player.isOnline() && !player.isDead()) { - return true; - } - } - return false; - } - - private void failLure(Infestation infestation) { - if (infestation.getPhase() == LurePhase.NONE) { - return; - } - Set committed = new HashSet<>(infestation.getCommitted()); - int provinceId = infestation.getProvinceId(); - Location loc = infestation.lureLocation(); - destroyLure(infestation, false); - infestation.clearLure(); - ambient.reconcileLoaded(this); - save(); - if (loc != null && loc.getWorld() != null) { - loc.getWorld().playSound(loc, Sound.ENTITY_WITHER_SPAWN, 0.4f, 1.4f); - } - for (Player player : Bukkit.getOnlinePlayers()) { - if (Provinces.at(player) == provinceId || committed.contains(player.getUniqueId())) { - player.sendMessage(Messages.get("lure-fail")); - } - } - } - - private void victory(Infestation infestation) { - Location loc = infestation.lureLocation(); - int provinceId = infestation.getProvinceId(); - destroyLure(infestation, true); - if (loc != null && loc.getWorld() != null) { - loc.getWorld().spawnParticle(Particle.TOTEM_OF_UNDYING, loc.clone().add(0, 1, 0), 40, 0.4, 0.8, 0.4, 0.2); - loc.getWorld().spawnParticle(Particle.FIREWORK, loc.clone().add(0, 1, 0), 20, 0.3, 0.5, 0.3, 0.05); - loc.getWorld().playSound(loc, Sound.UI_TOAST_CHALLENGE_COMPLETE, 1f, 1.1f); - } - for (Player player : Bukkit.getOnlinePlayers()) { - if (Provinces.at(player) == provinceId) { - player.sendMessage(Messages.get("lure-victory")); - } - } - byProvince.remove(provinceId); - save(); - InfestationMapExport.exportAsync(byProvince.values()); - } - - private void destroyLure(Infestation infestation, boolean victory) { - LureHologram.remove(infestation); - pluginFurnitureRemove = true; - try { - LureFurniture.removeFor(infestation); - } finally { - pluginFurnitureRemove = false; - } - } - - private void handleLureClick(Player player, Infestation infestation) { - if (player == null) { - return; - } - if (infestation.getPhase() == LurePhase.ACTIVE) { - int count = highlightLureMobs(infestation); - if (tick != lastLureHighlightTick || lastLureHighlightProvince != infestation.getProvinceId()) { - lastLureHighlightTick = tick; - lastLureHighlightProvince = infestation.getProvinceId(); - player.sendMessage(Messages.get("lure-highlight", "count", String.valueOf(count))); - } - if (!infestation.isCommitted(player.getUniqueId())) { - tryJoin(player, infestation); - } - return; - } - tryJoin(player, infestation); - } - - private int highlightLureMobs(Infestation infestation) { - Location origin = infestation.lureLocation(); - if (origin == null) { - return 0; - } - World world = origin.getWorld(); - if (world == null) { - return 0; - } - int count = 0; - PotionEffect glow = new PotionEffect(PotionEffectType.GLOWING, 200, 0, false, false); - int provinceId = infestation.getProvinceId(); - for (LivingEntity entity : world.getLivingEntities()) { - if (entity instanceof Player) { - continue; - } - Integer tagged = Keys.infestationId(entity.getPersistentDataContainer()); - if (tagged == null || tagged != provinceId) { - continue; - } - if (!Keys.KIND_LURE.equals(Keys.kind(entity.getPersistentDataContainer()))) { - continue; - } - entity.addPotionEffect(glow); - count++; - } - return count; - } - - private void tryJoin(Player player, Infestation infestation) { - if (player.getGameMode() == GameMode.SPECTATOR) { - return; - } - if (infestation.isCommitted(player.getUniqueId())) { - player.sendMessage(Messages.get("lure-already-joined")); - return; - } - infestation.getCommitted().add(player.getUniqueId()); - infestation.getLogoutGraceUntil().remove(player.getUniqueId()); - infestation.getDeathOnLogin().remove(player.getUniqueId()); - player.sendMessage(Messages.get("lure-join")); - save(); - } - - private boolean isLureItem(ItemStack stack) { - if (stack == null || stack.getType().isAir()) { - return false; - } - return TLibs.getItemAPI().getChecker().checkItemWithPath(stack, Cache.lureItem); - } - - private boolean isProtectedLure(Block block) { - if (block == null) { - return false; - } - for (Infestation infestation : byProvince.values()) { - if (infestation.isLureBlock(block)) { - return true; - } - } - return false; - } - - private boolean isActiveLure(Furniture furniture) { - if (furniture == null || furniture.getLoc() == null) { - return false; - } - Infestation infestation = get(Provinces.at(furniture.getLoc())); - return infestation != null && LureFurniture.matches(infestation, furniture); - } - - private void removeLureFurniture(Furniture furniture) { - pluginFurnitureRemove = true; - try { - LureFurniture.remove(furniture); - } finally { - pluginFurnitureRemove = false; - } - } - - @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) - public void onFurniturePlace(FurniturePlaceEvent event) { - Furniture furniture = event.getFurniture(); - if (!LureFurniture.isLure(furniture)) { - return; - } - Player player = event.getPlayer(); - Location loc = furniture.getLoc(); - int provinceId = Provinces.at(loc); - Infestation infestation = get(provinceId); - if (infestation == null || !Provinces.validLand(provinceId)) { - event.setCancelled(true); - if (player != null) { - player.sendMessage(Messages.get("no-infestation-here")); - } - return; - } - if (infestation.hasLure()) { - event.setCancelled(true); - if (player != null) { - player.sendMessage(Messages.get("lure-already")); - } - } - } - - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onFurniturePlaceDone(FurniturePlaceEvent event) { - Furniture furniture = event.getFurniture(); - if (!LureFurniture.isLure(furniture) || furniture.getLoc() == null) { - return; - } - Player player = event.getPlayer(); - int provinceId = Provinces.at(furniture.getLoc()); - Infestation infestation = get(provinceId); - if (infestation == null || infestation.hasLure()) { - return; - } - infestation.placeLure( - furniture.getLoc().getBlock(), - System.currentTimeMillis() + Cache.joinSeconds * 1000L, - infestation.lureBudget()); - if (player != null) { - tryJoin(player, infestation); - } - for (Player other : Bukkit.getOnlinePlayers()) { - if (other.equals(player)) { - continue; - } - if (Provinces.at(other) == provinceId) { - other.sendMessage(Messages.get("lure-placed")); - } - } - save(); - } - - @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) - public void onFurnitureBreak(FurnitureBreakEvent event) { - if (pluginFurnitureRemove) { - return; - } - Furniture furniture = event.getFurniture(); - if (!LureFurniture.isLure(furniture)) { - return; - } - event.setCancelled(true); - if (!isActiveLure(furniture)) { - removeLureFurniture(furniture); - } - } - - @EventHandler - public void onFurnitureInteract(FurnitureInteractEvent event) { - Furniture furniture = event.getFurniture(); - if (!LureFurniture.isLure(furniture)) { - return; - } - event.setCancelled(true); - if (!isActiveLure(furniture)) { - removeLureFurniture(furniture); - return; - } - Infestation infestation = get(Provinces.at(furniture.getLoc())); - if (infestation == null || infestation.getPhase() == LurePhase.NONE) { - return; - } - handleLureClick(event.getPlayer(), infestation); - } - - @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) - public void onPlace(BlockPlaceEvent event) { - if (!isLureItem(event.getItemInHand())) { - return; - } - event.setCancelled(true); - } - - @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) - public void onBreak(BlockBreakEvent event) { - if (isProtectedLure(event.getBlock())) { - event.setCancelled(true); - } - } - - @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) - public void onExplode(BlockExplodeEvent event) { - event.blockList().removeIf(this::isProtectedLure); - } - - @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) - public void onExplode(EntityExplodeEvent event) { - event.blockList().removeIf(this::isProtectedLure); - } - - @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) - public void onPiston(BlockPistonExtendEvent event) { - if (event.getBlocks().stream().anyMatch(this::isProtectedLure)) { - event.setCancelled(true); - } - } - - @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) - public void onPiston(BlockPistonRetractEvent event) { - if (event.getBlocks().stream().anyMatch(this::isProtectedLure)) { - event.setCancelled(true); - } - } - - @EventHandler - public void onInteract(PlayerInteractEvent event) { - if (event.getAction() != Action.RIGHT_CLICK_BLOCK || event.getClickedBlock() == null) { - return; - } - if (event.getHand() != EquipmentSlot.HAND) { - return; - } - Block block = event.getClickedBlock(); - Infestation infestation = lureAt(block); - if (infestation == null) { - infestation = lureAt(block.getRelative(BlockFace.UP)); - } - if (infestation == null || infestation.getPhase() == LurePhase.NONE) { - return; - } - event.setCancelled(true); - handleLureClick(event.getPlayer(), infestation); - } - - private Infestation lureAt(Block block) { - for (Infestation infestation : byProvince.values()) { - if (infestation.isLureBlock(block)) { - return infestation; - } - } - return null; - } - - @EventHandler - public void onDeath(EntityDeathEvent event) { - LivingEntity entity = event.getEntity(); - Integer provinceId = Keys.infestationId(entity.getPersistentDataContainer()); - String kind = Keys.kind(entity.getPersistentDataContainer()); - if (provinceId == null || kind == null) { - if (entity instanceof Player player) { - onPlayerDeath(player); - } - return; - } - Infestation infestation = get(provinceId); - if (infestation == null) { - return; - } - if (Keys.KIND_AMBIENT.equals(kind)) { - if (infestation.getAmbientAlive() > 0) { - infestation.setAmbientAlive(infestation.getAmbientAlive() - 1); - } - return; - } - if (Keys.KIND_LURE.equals(kind) && infestation.getPhase() == LurePhase.ACTIVE) { - if (infestation.getEnemiesAlive() > 0) { - infestation.setEnemiesAlive(infestation.getEnemiesAlive() - 1); - } - infestation.setLureRemaining(infestation.getLureRemaining() - 1); - if (infestation.getLureRemaining() <= 0) { - victory(infestation); - } - } - } - - private void onPlayerDeath(Player player) { - for (Infestation infestation : List.copyOf(byProvince.values())) { - if (!infestation.isCommitted(player.getUniqueId())) { - continue; - } - infestation.getCommitted().remove(player.getUniqueId()); - infestation.getLogoutGraceUntil().remove(player.getUniqueId()); - infestation.getDeathOnLogin().remove(player.getUniqueId()); - if (infestation.getPhase() != LurePhase.NONE) { - checkWipe(infestation); - } - } - save(); - } - - @EventHandler - public void onQuit(PlayerQuitEvent event) { - Player player = event.getPlayer(); - long until = System.currentTimeMillis() + Cache.logoutGraceSeconds * 1000L; - for (Infestation infestation : byProvince.values()) { - if (!infestation.isCommitted(player.getUniqueId())) { - continue; - } - infestation.getLogoutGraceUntil().put(player.getUniqueId(), until); - } - save(); - } - - @EventHandler - public void onJoin(PlayerJoinEvent event) { - Player player = event.getPlayer(); - Bukkit.getScheduler().runTaskLater(plugin, () -> { - if (!player.isOnline()) { - return; - } - for (Infestation infestation : byProvince.values()) { - infestation.getLogoutGraceUntil().remove(player.getUniqueId()); - if (infestation.getDeathOnLogin().remove(player.getUniqueId()) - && infestation.isCommitted(player.getUniqueId()) - && !player.isDead()) { - player.damage(Math.max(40.0, player.getHealth() + 10.0)); - } - } - }, 5L); - } - - @EventHandler - public void onLeaveProvince(PlayerProvinceLeaveEvent event) { - Infestation infestation = get(event.getProvinceId()); - if (infestation == null) { - return; - } - if (infestation.getPhase() == LurePhase.NONE) { - return; - } - if (infestation.isCommitted(event.getPlayer().getUniqueId())) { - failLure(infestation); - } - } - - @EventHandler - public void onEnterProvince(PlayerProvinceEnterEvent event) { - Infestation infestation = get(event.getProvinceId()); - if (infestation != null && infestation.getPhase() == LurePhase.JOINING) { - event.getPlayer().sendMessage(Messages.get("lure-leave-or-join")); - } - if (infestation != null && infestation.getPhase() == LurePhase.ACTIVE - && !infestation.isCommitted(event.getPlayer().getUniqueId())) { - event.getPlayer().sendMessage(Messages.get("lure-nonjoiner")); - } - } - - @EventHandler - public void onChunkUnload(ChunkUnloadEvent event) { - LureHologram.removeOrphans(event.getChunk()); - } - - @EventHandler - public void onChunkLoad(ChunkLoadEvent event) { - org.bukkit.Chunk chunk = event.getChunk(); - Bukkit.getScheduler().runTask(plugin, () -> { - pluginFurnitureRemove = true; - try { - LureFurniture.sweepChunk(chunk, this::get); - } finally { - pluginFurnitureRemove = false; - } - LureHologram.removeOrphans(chunk); - ambient.onChunkLoaded(chunk, this); - for (Infestation infestation : byProvince.values()) { - if (!infestation.hasLure()) { - continue; - } - Block block = infestation.lureBlock(); - if (block != null && block.getChunk().equals(chunk)) { - LureHologram.tick(infestation); - } - } - }); - } - - private static void actionBar(Player player, String message) { - player.spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacyText(message)); - } -} +package net.tfminecraft.infestations.infestation; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; + +import org.bukkit.Bukkit; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.Particle; +import org.bukkit.Sound; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.World; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.event.block.BlockExplodeEvent; +import org.bukkit.event.block.BlockPistonExtendEvent; +import org.bukkit.event.block.BlockPistonRetractEvent; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.event.entity.EntityExplodeEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.world.ChunkLoadEvent; +import org.bukkit.event.world.ChunkUnloadEvent; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.ItemStack; +import org.bukkit.potion.PotionEffect; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.scheduler.BukkitTask; + +import net.md_5.bungee.api.ChatMessageType; +import net.md_5.bungee.api.chat.TextComponent; + +import net.tfminecraft.simplefactions.events.PlayerProvinceEnterEvent; +import net.tfminecraft.simplefactions.events.PlayerProvinceLeaveEvent; +import net.tfminecraft.tlibs.TLibs; +import net.tfminecraft.infestations.Infestations; +import net.tfminecraft.infestations.Messages; +import net.tfminecraft.infestations.cache.Cache; +import net.tfminecraft.infestations.database.InfestationDatabase; +import net.tfminecraft.interactiblefurniture.events.FurnitureBreakEvent; +import net.tfminecraft.interactiblefurniture.events.FurnitureInteractEvent; +import net.tfminecraft.interactiblefurniture.events.FurniturePlaceEvent; +import net.tfminecraft.interactiblefurniture.furniture.Furniture; +import net.tfminecraft.infestations.loader.GroupLoader; +import net.tfminecraft.infestations.loader.GroupLoader.GroupTune; +import net.tfminecraft.infestations.lure.LureFurniture; +import net.tfminecraft.infestations.lure.LureHologram; +import net.tfminecraft.infestations.map.InfestationMapExport; +import net.tfminecraft.infestations.spawn.AmbientSpawnService; +import net.tfminecraft.infestations.spawn.Keys; +import net.tfminecraft.infestations.spawn.MythicSpawner; +import net.tfminecraft.infestations.spawn.SpawnLog; +import net.tfminecraft.infestations.spawn.SpawnPlanner; +import net.tfminecraft.infestations.utils.Provinces; + +public final class InfestationManager implements Listener, AmbientSpawnService.CollectionInfestations { + + private final Infestations plugin; + private final Map byProvince = new HashMap<>(); + private final AmbientSpawnService ambient = new AmbientSpawnService(); + private boolean pluginFurnitureRemove; + private BukkitTask task; + private int tick; + private int lastLureHighlightTick = -1; + private int lastLureHighlightProvince = -1; + + public InfestationManager(Infestations plugin) { + this.plugin = plugin; + } + + public void start() { + for (Infestation infestation : InfestationDatabase.load()) { + byProvince.put(infestation.getProvinceId(), infestation); + } + InfestationMapExport.exportAsync(byProvince.values()); + task = Bukkit.getScheduler().runTaskTimer(plugin, this::tick, 20L, 1L); + Bukkit.getScheduler().runTaskLater(plugin, () -> { + sweepLoadedChunks(); + ambient.reconcileLoaded(this); + }, 20L); + } + + public void shutdown() { + if (task != null) { + task.cancel(); + task = null; + } + for (Infestation infestation : byProvince.values()) { + LureHologram.remove(infestation); + } + save(); + } + + public Infestation get(int provinceId) { + return byProvince.get(provinceId); + } + + public Collection all() { + return byProvince.values(); + } + + public void set(int provinceId, String groupId, Severity severity) { + byProvince.put(provinceId, new Infestation(provinceId, groupId, severity)); + save(); + InfestationMapExport.exportAsync(byProvince.values()); + } + + public void clear(int provinceId, boolean victory) { + Infestation infestation = byProvince.get(provinceId); + if (infestation == null) { + return; + } + if (infestation.hasLure()) { + destroyLure(infestation, victory); + } + byProvince.remove(provinceId); + save(); + InfestationMapExport.exportAsync(byProvince.values()); + } + + public void reloadFromDisk() { + Map previous = new HashMap<>(byProvince); + byProvince.clear(); + for (Infestation infestation : InfestationDatabase.load()) { + byProvince.put(infestation.getProvinceId(), infestation); + } + pluginFurnitureRemove = true; + try { + for (Infestation old : previous.values()) { + if (!old.hasLure()) { + continue; + } + Infestation current = byProvince.get(old.getProvinceId()); + if (current != null && current.hasLure() && sameLureBlock(old, current)) { + continue; + } + LureHologram.remove(old); + LureFurniture.removeFor(old); + } + } finally { + pluginFurnitureRemove = false; + } + sweepLoadedChunks(); + ambient.reconcileLoaded(this); + save(); + InfestationMapExport.exportAsync(byProvince.values()); + } + + private static boolean sameLureBlock(Infestation a, Infestation b) { + return a.getWorldName() != null + && a.getWorldName().equals(b.getWorldName()) + && java.util.Objects.equals(a.getLureX(), b.getLureX()) + && java.util.Objects.equals(a.getLureY(), b.getLureY()) + && java.util.Objects.equals(a.getLureZ(), b.getLureZ()); + } + + public void save() { + InfestationDatabase.save(byProvince.values()); + } + + private void tick() { + tick++; + long now = System.currentTimeMillis(); + ambient.tick(this, tick); + + for (Infestation infestation : List.copyOf(byProvince.values())) { + expireGrace(infestation, now); + if (infestation.getPhase() == LurePhase.JOINING) { + warnJoining(infestation); + if (now >= infestation.getJoinEndsAt()) { + activate(infestation); + } + } + if (infestation.getPhase() == LurePhase.ACTIVE) { + warnNonJoiners(infestation); + spawnLureWave(infestation); + checkWipe(infestation); + } + if (infestation.hasLure()) { + LureHologram.tick(infestation); + } + tickDeserters(infestation); + } + + tickSpread(); + + if (tick % 100 == 0) { + save(); + } + } + + private void tickSpread() { + if (!Cache.spread) { + return; + } + int intervalTicks = Cache.spreadIntervalSeconds * 20; + if (intervalTicks <= 0 || tick % intervalTicks != 0) { + return; + } + boolean changed = false; + for (Infestation source : List.copyOf(byProvince.values())) { + if (source.getPhase() != LurePhase.NONE) { + continue; + } + if (source.getSeverity().canWorsen() + && ThreadLocalRandom.current().nextDouble() < Cache.worsenChance) { + source.setSeverity(source.getSeverity().worse()); + changed = true; + if (Cache.debug) { + plugin.getLogger().info("[Infestations] Spread worsened " + + source.getProvinceId() + " to " + source.getSeverity().id()); + } + } + SpreadPick pick = pickSpreadTarget(source.getProvinceId(), source.getSeverity(), source.getGroupId()); + if (pick == null) { + continue; + } + byProvince.put(pick.provinceId, new Infestation(pick.provinceId, source.getGroupId(), Severity.MILD)); + changed = true; + if (Cache.debug) { + plugin.getLogger().info("[Infestations] Spread " + source.getGroupId() + + " from " + source.getProvinceId() + " to " + pick.provinceId + + " via " + pick.via.id()); + } + } + if (changed) { + save(); + InfestationMapExport.exportAsync(byProvince.values()); + } + } + + private SpreadPick pickSpreadTarget(int fromId, Severity severity, String groupId) { + Map best = new HashMap<>(); + for (int neighborId : Provinces.neighbours(fromId)) { + if (canInfect(neighborId, groupId)) { + offerSpread(best, neighborId, SpreadVia.LAND); + continue; + } + if (Provinces.isWater(neighborId)) { + collectWaterOrSeaHop(fromId, neighborId, groupId, SpreadVia.WATER, best); + } else if (Provinces.isSea(neighborId)) { + collectWaterOrSeaHop(fromId, neighborId, groupId, SpreadVia.SEA, best); + } + } + if (best.isEmpty()) { + return null; + } + int provinceId = weightedSpreadPick(best, severity); + SpreadVia via = best.get(provinceId); + if (ThreadLocalRandom.current().nextDouble() >= via.chance(severity)) { + if (Cache.debug) { + plugin.getLogger().info("[Infestations] Spread hop failed via " + via.id() + + " from " + fromId + " toward " + provinceId); + } + return null; + } + return new SpreadPick(provinceId, via); + } + + private void collectWaterOrSeaHop( + int fromId, int mediumId, String groupId, SpreadVia via, Map best) { + for (int farId : Provinces.neighbours(mediumId)) { + if (farId == fromId || !canInfect(farId, groupId)) { + continue; + } + offerSpread(best, farId, via); + } + } + + private boolean canInfect(int provinceId, String groupId) { + return Provinces.validLand(provinceId) + && !byProvince.containsKey(provinceId) + && GroupLoader.allowsTerrain(groupId, provinceId); + } + + private static void offerSpread(Map best, int provinceId, SpreadVia via) { + SpreadVia current = best.get(provinceId); + if (current == null || via.easierThan(current)) { + best.put(provinceId, via); + } + } + + private static int weightedSpreadPick(Map best, Severity severity) { + double total = 0; + for (SpreadVia via : best.values()) { + total += Math.max(0.0001, via.chance(severity)); + } + double roll = ThreadLocalRandom.current().nextDouble() * total; + int last = -1; + for (Map.Entry entry : best.entrySet()) { + last = entry.getKey(); + roll -= Math.max(0.0001, entry.getValue().chance(severity)); + if (roll <= 0) { + return entry.getKey(); + } + } + return last; + } + + private enum SpreadVia { + LAND, + WATER, + SEA; + + boolean easierThan(SpreadVia other) { + return ordinal() < other.ordinal(); + } + + double chance(Severity severity) { + return Cache.hopChance(severity, ordinal()); + } + + String id() { + return name().toLowerCase(); + } + } + + private record SpreadPick(int provinceId, SpreadVia via) {} + + private void sweepLoadedChunks() { + pluginFurnitureRemove = true; + try { + for (org.bukkit.World world : Bukkit.getWorlds()) { + for (org.bukkit.Chunk chunk : world.getLoadedChunks()) { + LureFurniture.sweepChunk(chunk, this::get); + } + } + } finally { + pluginFurnitureRemove = false; + } + } + + private void activate(Infestation infestation) { + infestation.setPhase(LurePhase.ACTIVE); + infestation.setLureActivatedAt(System.currentTimeMillis()); + spawnLureWave(infestation); + save(); + } + + private void spawnLureWave(Infestation infestation) { + if (tick < infestation.getWaveRetryAtTick()) { + return; + } + Location origin = infestation.lureLocation(); + if (origin == null) { + return; + } + if (GroupLoader.blockedByNight(infestation.getGroupId(), origin.getWorld())) { + if (tick % 40 == 0) { + SpawnLog.line(infestation, "-", "night"); + } + return; + } + GroupTune tune = GroupLoader.tune(infestation.getGroupId(), infestation.getSeverity()); + long started = infestation.getLureActivatedAt(); + if (started <= 0) { + started = System.currentTimeMillis(); + infestation.setLureActivatedAt(started); + } + long elapsed = Math.max(0, System.currentTimeMillis() - started); + long durationMs = tune.lureDurationSeconds() * 1000L; + int lureCount = tune.lureCount(); + int allowed = durationMs <= 0 + ? lureCount + : (int) Math.min(lureCount, elapsed * lureCount / durationMs); + int killsDone = Math.max(0, lureCount - infestation.getLureRemaining()); + int need = allowed - infestation.getPendingSpawns() - infestation.getEnemiesAlive() - killsDone; + if (need <= 0) { + return; + } + List spots = SpawnPlanner.find( + origin, + 4, + Cache.lureSpawnRadius, + need, + loc -> Provinces.at(loc) == infestation.getProvinceId() + && GroupLoader.allowsY(infestation.getGroupId(), loc.getBlockY())); + if (spots.isEmpty()) { + infestation.setWaveRetryAtTick(tick + 40); + SpawnLog.line(infestation, "-", "no-spot"); + return; + } + infestation.setWaveRetryAtTick(0); + for (Location spot : spots) { + infestation.setPendingSpawns(infestation.getPendingSpawns() + 1); + int delay = ThreadLocalRandom.current().nextInt(20, 61); + Bukkit.getScheduler().runTaskLater(plugin, () -> { + if (infestation.getPhase() != LurePhase.ACTIVE) { + infestation.setPendingSpawns(Math.max(0, infestation.getPendingSpawns() - 1)); + return; + } + if (GroupLoader.blockedByNight(infestation.getGroupId(), spot.getWorld())) { + infestation.setPendingSpawns(Math.max(0, infestation.getPendingSpawns() - 1)); + SpawnLog.line(infestation, "-", "night"); + return; + } + LivingEntity spawned = MythicSpawner.spawn( + spot, infestation.getGroupId(), infestation.getProvinceId(), Keys.KIND_LURE); + infestation.setPendingSpawns(Math.max(0, infestation.getPendingSpawns() - 1)); + if (spawned != null) { + infestation.setEnemiesAlive(infestation.getEnemiesAlive() + 1); + SpawnLog.spawned( + infestation, + "-", + spot, + infestation.getEnemiesAlive(), + lureCount); + } else { + SpawnLog.line(infestation, "-", "unknown-mythic"); + } + }, delay); + } + } + + private void warnJoining(Infestation infestation) { + if (tick % 20 != 0) { + return; + } + String bar = Messages.get("lure-activating-bar", "seconds", String.valueOf(infestation.joinSecondsLeft())); + for (Player player : Bukkit.getOnlinePlayers()) { + if (Provinces.at(player) != infestation.getProvinceId()) { + continue; + } + actionBar(player, bar); + if (tick % 100 == 0) { + player.sendMessage(Messages.get("lure-leave-or-join")); + } + } + } + + private void warnNonJoiners(Infestation infestation) { + if (tick % 20 != 0) { + return; + } + String bar = Messages.get("lure-nonjoiner-bar"); + for (Player player : Bukkit.getOnlinePlayers()) { + if (player.getGameMode() == GameMode.CREATIVE || player.getGameMode() == GameMode.SPECTATOR) { + continue; + } + if (Provinces.at(player) != infestation.getProvinceId()) { + continue; + } + if (infestation.isCommitted(player.getUniqueId())) { + continue; + } + actionBar(player, bar); + player.damage(Cache.deserterDamage); + if (tick % 100 == 0) { + player.sendMessage(Messages.get("lure-nonjoiner")); + } + } + } + + private void tickDeserters(Infestation infestation) { + if (tick % 20 != 0 || infestation.getPhase() == LurePhase.NONE) { + return; + } + for (UUID id : List.copyOf(infestation.getCommitted())) { + Player player = Bukkit.getPlayer(id); + if (player == null || !player.isOnline() || player.isDead()) { + continue; + } + if (Provinces.at(player) == infestation.getProvinceId()) { + continue; + } + failLure(infestation); + return; + } + } + + private void expireGrace(Infestation infestation, long now) { + Iterator> it = infestation.getLogoutGraceUntil().entrySet().iterator(); + while (it.hasNext()) { + Map.Entry e = it.next(); + if (now >= e.getValue()) { + infestation.getDeathOnLogin().add(e.getKey()); + it.remove(); + } + } + } + + private void checkWipe(Infestation infestation) { + if (infestation.getPhase() != LurePhase.ACTIVE) { + return; + } + if (hasLivingOrGrace(infestation)) { + return; + } + failLure(infestation); + } + + private boolean hasLivingOrGrace(Infestation infestation) { + if (!infestation.getLogoutGraceUntil().isEmpty()) { + return true; + } + if (!infestation.getDeathOnLogin().isEmpty()) { + return true; + } + for (UUID id : infestation.getCommitted()) { + Player player = Bukkit.getPlayer(id); + if (player != null && player.isOnline() && !player.isDead()) { + return true; + } + } + return false; + } + + private void failLure(Infestation infestation) { + if (infestation.getPhase() == LurePhase.NONE) { + return; + } + Set committed = new HashSet<>(infestation.getCommitted()); + int provinceId = infestation.getProvinceId(); + Location loc = infestation.lureLocation(); + destroyLure(infestation, false); + infestation.clearLure(); + ambient.reconcileLoaded(this); + save(); + if (loc != null && loc.getWorld() != null) { + loc.getWorld().playSound(loc, Sound.ENTITY_WITHER_SPAWN, 0.4f, 1.4f); + } + for (Player player : Bukkit.getOnlinePlayers()) { + if (Provinces.at(player) == provinceId || committed.contains(player.getUniqueId())) { + player.sendMessage(Messages.get("lure-fail")); + } + } + } + + private void victory(Infestation infestation) { + Location loc = infestation.lureLocation(); + int provinceId = infestation.getProvinceId(); + destroyLure(infestation, true); + if (loc != null && loc.getWorld() != null) { + loc.getWorld().spawnParticle(Particle.TOTEM_OF_UNDYING, loc.clone().add(0, 1, 0), 40, 0.4, 0.8, 0.4, 0.2); + loc.getWorld().spawnParticle(Particle.FIREWORK, loc.clone().add(0, 1, 0), 20, 0.3, 0.5, 0.3, 0.05); + loc.getWorld().playSound(loc, Sound.UI_TOAST_CHALLENGE_COMPLETE, 1f, 1.1f); + } + for (Player player : Bukkit.getOnlinePlayers()) { + if (Provinces.at(player) == provinceId) { + player.sendMessage(Messages.get("lure-victory")); + } + } + byProvince.remove(provinceId); + save(); + InfestationMapExport.exportAsync(byProvince.values()); + } + + private void destroyLure(Infestation infestation, boolean victory) { + LureHologram.remove(infestation); + pluginFurnitureRemove = true; + try { + LureFurniture.removeFor(infestation); + } finally { + pluginFurnitureRemove = false; + } + } + + private void handleLureClick(Player player, Infestation infestation) { + if (player == null) { + return; + } + if (infestation.getPhase() == LurePhase.ACTIVE) { + int count = highlightLureMobs(infestation); + if (tick != lastLureHighlightTick || lastLureHighlightProvince != infestation.getProvinceId()) { + lastLureHighlightTick = tick; + lastLureHighlightProvince = infestation.getProvinceId(); + player.sendMessage(Messages.get("lure-highlight", "count", String.valueOf(count))); + } + if (!infestation.isCommitted(player.getUniqueId())) { + tryJoin(player, infestation); + } + return; + } + tryJoin(player, infestation); + } + + private int highlightLureMobs(Infestation infestation) { + Location origin = infestation.lureLocation(); + if (origin == null) { + return 0; + } + World world = origin.getWorld(); + if (world == null) { + return 0; + } + int count = 0; + PotionEffect glow = new PotionEffect(PotionEffectType.GLOWING, 200, 0, false, false); + int provinceId = infestation.getProvinceId(); + for (LivingEntity entity : world.getLivingEntities()) { + if (entity instanceof Player) { + continue; + } + Integer tagged = Keys.infestationId(entity.getPersistentDataContainer()); + if (tagged == null || tagged != provinceId) { + continue; + } + if (!Keys.KIND_LURE.equals(Keys.kind(entity.getPersistentDataContainer()))) { + continue; + } + entity.addPotionEffect(glow); + count++; + } + return count; + } + + private void tryJoin(Player player, Infestation infestation) { + if (player.getGameMode() == GameMode.SPECTATOR) { + return; + } + if (infestation.isCommitted(player.getUniqueId())) { + player.sendMessage(Messages.get("lure-already-joined")); + return; + } + infestation.getCommitted().add(player.getUniqueId()); + infestation.getLogoutGraceUntil().remove(player.getUniqueId()); + infestation.getDeathOnLogin().remove(player.getUniqueId()); + player.sendMessage(Messages.get("lure-join")); + save(); + } + + private boolean isLureItem(ItemStack stack) { + if (stack == null || stack.getType().isAir()) { + return false; + } + return TLibs.getItemAPI().getChecker().checkItemWithPath(stack, Cache.lureItem); + } + + private boolean isProtectedLure(Block block) { + if (block == null) { + return false; + } + for (Infestation infestation : byProvince.values()) { + if (infestation.isLureBlock(block)) { + return true; + } + } + return false; + } + + private boolean isActiveLure(Furniture furniture) { + if (furniture == null || furniture.getLoc() == null) { + return false; + } + Infestation infestation = get(Provinces.at(furniture.getLoc())); + return infestation != null && LureFurniture.matches(infestation, furniture); + } + + private void removeLureFurniture(Furniture furniture) { + pluginFurnitureRemove = true; + try { + LureFurniture.remove(furniture); + } finally { + pluginFurnitureRemove = false; + } + } + + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onFurniturePlace(FurniturePlaceEvent event) { + Furniture furniture = event.getFurniture(); + if (!LureFurniture.isLure(furniture)) { + return; + } + Player player = event.getPlayer(); + Location loc = furniture.getLoc(); + int provinceId = Provinces.at(loc); + Infestation infestation = get(provinceId); + if (infestation == null || !Provinces.validLand(provinceId)) { + event.setCancelled(true); + if (player != null) { + player.sendMessage(Messages.get("no-infestation-here")); + } + return; + } + if (infestation.hasLure()) { + event.setCancelled(true); + if (player != null) { + player.sendMessage(Messages.get("lure-already")); + } + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onFurniturePlaceDone(FurniturePlaceEvent event) { + Furniture furniture = event.getFurniture(); + if (!LureFurniture.isLure(furniture) || furniture.getLoc() == null) { + return; + } + Player player = event.getPlayer(); + int provinceId = Provinces.at(furniture.getLoc()); + Infestation infestation = get(provinceId); + if (infestation == null || infestation.hasLure()) { + return; + } + infestation.placeLure( + furniture.getLoc().getBlock(), + System.currentTimeMillis() + Cache.joinSeconds * 1000L, + infestation.lureBudget()); + if (player != null) { + tryJoin(player, infestation); + } + for (Player other : Bukkit.getOnlinePlayers()) { + if (other.equals(player)) { + continue; + } + if (Provinces.at(other) == provinceId) { + other.sendMessage(Messages.get("lure-placed")); + } + } + save(); + } + + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onFurnitureBreak(FurnitureBreakEvent event) { + if (pluginFurnitureRemove) { + return; + } + Furniture furniture = event.getFurniture(); + if (!LureFurniture.isLure(furniture)) { + return; + } + event.setCancelled(true); + if (!isActiveLure(furniture)) { + removeLureFurniture(furniture); + } + } + + @EventHandler + public void onFurnitureInteract(FurnitureInteractEvent event) { + Furniture furniture = event.getFurniture(); + if (!LureFurniture.isLure(furniture)) { + return; + } + event.setCancelled(true); + if (!isActiveLure(furniture)) { + removeLureFurniture(furniture); + return; + } + Infestation infestation = get(Provinces.at(furniture.getLoc())); + if (infestation == null || infestation.getPhase() == LurePhase.NONE) { + return; + } + handleLureClick(event.getPlayer(), infestation); + } + + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onPlace(BlockPlaceEvent event) { + if (!isLureItem(event.getItemInHand())) { + return; + } + event.setCancelled(true); + } + + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onBreak(BlockBreakEvent event) { + if (isProtectedLure(event.getBlock())) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onExplode(BlockExplodeEvent event) { + event.blockList().removeIf(this::isProtectedLure); + } + + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onExplode(EntityExplodeEvent event) { + event.blockList().removeIf(this::isProtectedLure); + } + + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onPiston(BlockPistonExtendEvent event) { + if (event.getBlocks().stream().anyMatch(this::isProtectedLure)) { + event.setCancelled(true); + } + } + + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onPiston(BlockPistonRetractEvent event) { + if (event.getBlocks().stream().anyMatch(this::isProtectedLure)) { + event.setCancelled(true); + } + } + + @EventHandler + public void onInteract(PlayerInteractEvent event) { + if (event.getAction() != Action.RIGHT_CLICK_BLOCK || event.getClickedBlock() == null) { + return; + } + if (event.getHand() != EquipmentSlot.HAND) { + return; + } + Block block = event.getClickedBlock(); + Infestation infestation = lureAt(block); + if (infestation == null) { + infestation = lureAt(block.getRelative(BlockFace.UP)); + } + if (infestation == null || infestation.getPhase() == LurePhase.NONE) { + return; + } + event.setCancelled(true); + handleLureClick(event.getPlayer(), infestation); + } + + private Infestation lureAt(Block block) { + for (Infestation infestation : byProvince.values()) { + if (infestation.isLureBlock(block)) { + return infestation; + } + } + return null; + } + + @EventHandler + public void onDeath(EntityDeathEvent event) { + LivingEntity entity = event.getEntity(); + Integer provinceId = Keys.infestationId(entity.getPersistentDataContainer()); + String kind = Keys.kind(entity.getPersistentDataContainer()); + if (provinceId == null || kind == null) { + if (entity instanceof Player player) { + onPlayerDeath(player); + } + return; + } + Infestation infestation = get(provinceId); + if (infestation == null) { + return; + } + if (Keys.KIND_AMBIENT.equals(kind)) { + if (infestation.getAmbientAlive() > 0) { + infestation.setAmbientAlive(infestation.getAmbientAlive() - 1); + } + return; + } + if (Keys.KIND_LURE.equals(kind) && infestation.getPhase() == LurePhase.ACTIVE) { + if (infestation.getEnemiesAlive() > 0) { + infestation.setEnemiesAlive(infestation.getEnemiesAlive() - 1); + } + infestation.setLureRemaining(infestation.getLureRemaining() - 1); + if (infestation.getLureRemaining() <= 0) { + victory(infestation); + } + } + } + + private void onPlayerDeath(Player player) { + for (Infestation infestation : List.copyOf(byProvince.values())) { + if (!infestation.isCommitted(player.getUniqueId())) { + continue; + } + infestation.getCommitted().remove(player.getUniqueId()); + infestation.getLogoutGraceUntil().remove(player.getUniqueId()); + infestation.getDeathOnLogin().remove(player.getUniqueId()); + if (infestation.getPhase() != LurePhase.NONE) { + checkWipe(infestation); + } + } + save(); + } + + @EventHandler + public void onQuit(PlayerQuitEvent event) { + Player player = event.getPlayer(); + long until = System.currentTimeMillis() + Cache.logoutGraceSeconds * 1000L; + for (Infestation infestation : byProvince.values()) { + if (!infestation.isCommitted(player.getUniqueId())) { + continue; + } + infestation.getLogoutGraceUntil().put(player.getUniqueId(), until); + } + save(); + } + + @EventHandler + public void onJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + Bukkit.getScheduler().runTaskLater(plugin, () -> { + if (!player.isOnline()) { + return; + } + for (Infestation infestation : byProvince.values()) { + infestation.getLogoutGraceUntil().remove(player.getUniqueId()); + if (infestation.getDeathOnLogin().remove(player.getUniqueId()) + && infestation.isCommitted(player.getUniqueId()) + && !player.isDead()) { + player.damage(Math.max(40.0, player.getHealth() + 10.0)); + } + } + }, 5L); + } + + @EventHandler + public void onLeaveProvince(PlayerProvinceLeaveEvent event) { + Infestation infestation = get(event.getProvinceId()); + if (infestation == null) { + return; + } + if (infestation.getPhase() == LurePhase.NONE) { + return; + } + if (infestation.isCommitted(event.getPlayer().getUniqueId())) { + failLure(infestation); + } + } + + @EventHandler + public void onEnterProvince(PlayerProvinceEnterEvent event) { + Infestation infestation = get(event.getProvinceId()); + if (infestation != null && infestation.getPhase() == LurePhase.JOINING) { + event.getPlayer().sendMessage(Messages.get("lure-leave-or-join")); + } + if (infestation != null && infestation.getPhase() == LurePhase.ACTIVE + && !infestation.isCommitted(event.getPlayer().getUniqueId())) { + event.getPlayer().sendMessage(Messages.get("lure-nonjoiner")); + } + } + + @EventHandler + public void onChunkUnload(ChunkUnloadEvent event) { + LureHologram.removeOrphans(event.getChunk()); + } + + @EventHandler + public void onChunkLoad(ChunkLoadEvent event) { + org.bukkit.Chunk chunk = event.getChunk(); + Bukkit.getScheduler().runTask(plugin, () -> { + pluginFurnitureRemove = true; + try { + LureFurniture.sweepChunk(chunk, this::get); + } finally { + pluginFurnitureRemove = false; + } + LureHologram.removeOrphans(chunk); + ambient.onChunkLoaded(chunk, this); + for (Infestation infestation : byProvince.values()) { + if (!infestation.hasLure()) { + continue; + } + Block block = infestation.lureBlock(); + if (block != null && block.getChunk().equals(chunk)) { + LureHologram.tick(infestation); + } + } + }); + } + + private static void actionBar(Player player, String message) { + player.spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacyText(message)); + } +} diff --git a/src/main/java/net/tfminecraft/infestations/infestation/LurePhase.java b/src/main/java/net/tfminecraft/infestations/infestation/LurePhase.java index b7e75bd..d1a47e4 100644 --- a/src/main/java/net/tfminecraft/infestations/infestation/LurePhase.java +++ b/src/main/java/net/tfminecraft/infestations/infestation/LurePhase.java @@ -1,7 +1,7 @@ -package net.tfminecraft.infestations.infestation; - -public enum LurePhase { - NONE, - JOINING, - ACTIVE -} +package net.tfminecraft.infestations.infestation; + +public enum LurePhase { + NONE, + JOINING, + ACTIVE +} diff --git a/src/main/java/net/tfminecraft/infestations/infestation/Severity.java b/src/main/java/net/tfminecraft/infestations/infestation/Severity.java index 1456e2a..9f0cd44 100644 --- a/src/main/java/net/tfminecraft/infestations/infestation/Severity.java +++ b/src/main/java/net/tfminecraft/infestations/infestation/Severity.java @@ -1,42 +1,42 @@ -package net.tfminecraft.infestations.infestation; - -import java.util.Locale; - -public enum Severity { - MILD, - WORRYING, - SEVERE, - EXTREME; - - public String id() { - return name().toLowerCase(Locale.ROOT); - } - - public String display() { - String id = id(); - return Character.toUpperCase(id.charAt(0)) + id.substring(1); - } - - public boolean canWorsen() { - return this != EXTREME; - } - - public Severity worse() { - return switch (this) { - case MILD -> WORRYING; - case WORRYING -> SEVERE; - case SEVERE, EXTREME -> EXTREME; - }; - } - - public static Severity fromString(String raw) { - if (raw == null) { - return null; - } - try { - return Severity.valueOf(raw.trim().toUpperCase(Locale.ROOT)); - } catch (IllegalArgumentException ex) { - return null; - } - } -} +package net.tfminecraft.infestations.infestation; + +import java.util.Locale; + +public enum Severity { + MILD, + WORRYING, + SEVERE, + EXTREME; + + public String id() { + return name().toLowerCase(Locale.ROOT); + } + + public String display() { + String id = id(); + return Character.toUpperCase(id.charAt(0)) + id.substring(1); + } + + public boolean canWorsen() { + return this != EXTREME; + } + + public Severity worse() { + return switch (this) { + case MILD -> WORRYING; + case WORRYING -> SEVERE; + case SEVERE, EXTREME -> EXTREME; + }; + } + + public static Severity fromString(String raw) { + if (raw == null) { + return null; + } + try { + return Severity.valueOf(raw.trim().toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException ex) { + return null; + } + } +} diff --git a/src/main/java/net/tfminecraft/infestations/loader/ConfigLoader.java b/src/main/java/net/tfminecraft/infestations/loader/ConfigLoader.java index 4ed7b6f..c810ccd 100644 --- a/src/main/java/net/tfminecraft/infestations/loader/ConfigLoader.java +++ b/src/main/java/net/tfminecraft/infestations/loader/ConfigLoader.java @@ -1,89 +1,89 @@ -package net.tfminecraft.infestations.loader; - -import java.io.File; -import java.io.IOException; -import java.util.HashSet; -import java.util.Locale; -import java.util.Set; - -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.InvalidConfigurationException; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.configuration.file.YamlConfiguration; - -import me.Plugins.TLibs.Interface.LoaderInterface; -import net.tfminecraft.infestations.Infestations; -import net.tfminecraft.infestations.cache.Cache; - -public final class ConfigLoader implements LoaderInterface { - - @Override - public void load(File configFile) { - loadSafe(configFile); - } - - public boolean loadSafe(File configFile) { - FileConfiguration config = new YamlConfiguration(); - try { - config.load(configFile); - } catch (IOException | InvalidConfigurationException ex) { - Infestations.plugin.getLogger().severe("[Infestations] Failed to load config.yml: " + ex.getMessage()); - return false; - } - - Cache.debug = config.getBoolean("debug", false); - Cache.loggingEnabled = config.getBoolean("logging", true); - Cache.wipeLog = config.getBoolean("wipe-log", true); - Cache.spread = config.getBoolean("spread", false); - Cache.spreadIntervalSeconds = Math.max(1, config.getInt("spread-interval-seconds", 600)); - Cache.worsenChance = clamp01(config.getDouble("worsen-chance", 0.0015)); - Cache.spreadChanceLand = loadSeverityChances(config, "spread-chance-land", Cache.DEFAULT_SPREAD_LAND); - Cache.spreadChanceWater = loadSeverityChances(config, "spread-chance-water", Cache.DEFAULT_SPREAD_WATER); - Cache.spreadChanceSea = loadSeverityChances(config, "spread-chance-sea", Cache.DEFAULT_SPREAD_SEA); - Cache.lureItem = config.getString("lure-item", "ia.tfmc:lure"); - Cache.joinSeconds = Math.max(1, config.getInt("join-seconds", 20)); - Cache.lureSpawnRadius = Math.max(4, config.getInt("lure-spawn-radius", 48)); - Cache.logoutGraceSeconds = Math.max(1, config.getInt("logout-grace-seconds", 300)); - Cache.deserterDamage = Math.max(0.5, config.getDouble("deserter-damage", 2.0)); - Cache.hologramViewRange = Math.max(16, config.getDouble("hologram-view-range", 96)); - - Set skip = new HashSet<>(); - for (String t : config.getStringList("skip-terrains")) { - if (t != null && !t.isBlank()) { - skip.add(t.toLowerCase(Locale.ROOT)); - } - } - if (skip.isEmpty()) { - skip.add("water"); - skip.add("sea"); - } - Cache.skipTerrains = skip; - return true; - } - - private static double[] loadSeverityChances(FileConfiguration config, String path, double[] defaults) { - double[] out = defaults.clone(); - if (!config.isConfigurationSection(path)) { - return out; - } - ConfigurationSection section = config.getConfigurationSection(path); - if (section == null) { - return out; - } - out[0] = clamp01(section.getDouble("mild", defaults[0])); - out[1] = clamp01(section.getDouble("worrying", defaults[1])); - out[2] = clamp01(section.getDouble("severe", defaults[2])); - out[3] = clamp01(section.getDouble("extreme", defaults[3])); - return out; - } - - private static double clamp01(double value) { - if (value < 0) { - return 0; - } - if (value > 1) { - return 1; - } - return value; - } -} +package net.tfminecraft.infestations.loader; + +import java.io.File; +import java.io.IOException; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; + +import net.tfminecraft.tlibs.interfaces.LoaderInterface; +import net.tfminecraft.infestations.Infestations; +import net.tfminecraft.infestations.cache.Cache; + +public final class ConfigLoader implements LoaderInterface { + + @Override + public void load(File configFile) { + loadSafe(configFile); + } + + public boolean loadSafe(File configFile) { + FileConfiguration config = new YamlConfiguration(); + try { + config.load(configFile); + } catch (IOException | InvalidConfigurationException ex) { + Infestations.plugin.getLogger().severe("[Infestations] Failed to load config.yml: " + ex.getMessage()); + return false; + } + + Cache.debug = config.getBoolean("debug", false); + Cache.loggingEnabled = config.getBoolean("logging", true); + Cache.wipeLog = config.getBoolean("wipe-log", true); + Cache.spread = config.getBoolean("spread", false); + Cache.spreadIntervalSeconds = Math.max(1, config.getInt("spread-interval-seconds", 600)); + Cache.worsenChance = clamp01(config.getDouble("worsen-chance", 0.0015)); + Cache.spreadChanceLand = loadSeverityChances(config, "spread-chance-land", Cache.DEFAULT_SPREAD_LAND); + Cache.spreadChanceWater = loadSeverityChances(config, "spread-chance-water", Cache.DEFAULT_SPREAD_WATER); + Cache.spreadChanceSea = loadSeverityChances(config, "spread-chance-sea", Cache.DEFAULT_SPREAD_SEA); + Cache.lureItem = config.getString("lure-item", "ia.tfmc:lure"); + Cache.joinSeconds = Math.max(1, config.getInt("join-seconds", 20)); + Cache.lureSpawnRadius = Math.max(4, config.getInt("lure-spawn-radius", 48)); + Cache.logoutGraceSeconds = Math.max(1, config.getInt("logout-grace-seconds", 300)); + Cache.deserterDamage = Math.max(0.5, config.getDouble("deserter-damage", 2.0)); + Cache.hologramViewRange = Math.max(16, config.getDouble("hologram-view-range", 96)); + + Set skip = new HashSet<>(); + for (String t : config.getStringList("skip-terrains")) { + if (t != null && !t.isBlank()) { + skip.add(t.toLowerCase(Locale.ROOT)); + } + } + if (skip.isEmpty()) { + skip.add("water"); + skip.add("sea"); + } + Cache.skipTerrains = skip; + return true; + } + + private static double[] loadSeverityChances(FileConfiguration config, String path, double[] defaults) { + double[] out = defaults.clone(); + if (!config.isConfigurationSection(path)) { + return out; + } + ConfigurationSection section = config.getConfigurationSection(path); + if (section == null) { + return out; + } + out[0] = clamp01(section.getDouble("mild", defaults[0])); + out[1] = clamp01(section.getDouble("worrying", defaults[1])); + out[2] = clamp01(section.getDouble("severe", defaults[2])); + out[3] = clamp01(section.getDouble("extreme", defaults[3])); + return out; + } + + private static double clamp01(double value) { + if (value < 0) { + return 0; + } + if (value > 1) { + return 1; + } + return value; + } +} diff --git a/src/main/java/net/tfminecraft/infestations/loader/GroupLoader.java b/src/main/java/net/tfminecraft/infestations/loader/GroupLoader.java index ab4b1d1..5e0ae42 100644 --- a/src/main/java/net/tfminecraft/infestations/loader/GroupLoader.java +++ b/src/main/java/net/tfminecraft/infestations/loader/GroupLoader.java @@ -1,253 +1,253 @@ -package net.tfminecraft.infestations.loader; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.EnumMap; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ThreadLocalRandom; - -import org.bukkit.World; -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.InvalidConfigurationException; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.configuration.file.YamlConfiguration; - -import me.Plugins.SimpleFactions.enums.Terrain; -import me.Plugins.TLibs.Interface.LoaderInterface; -import net.tfminecraft.infestations.Infestations; -import net.tfminecraft.infestations.infestation.Severity; -import net.tfminecraft.infestations.utils.Provinces; - -public final class GroupLoader implements LoaderInterface { - - private static final Map GROUPS = new LinkedHashMap<>(); - - public record WeightedMob(String id, int weight) {} - - public record GroupTune( - int ambientCap, - int ambientIntervalTicks, - int ambientRingMin, - int ambientRingMax, - int lureCount, - int lureDurationSeconds - ) { - public static GroupTune defaults(Severity s) { - return switch (s) { - case MILD -> new GroupTune(8, 40, 10, 22, 20, 120); - case WORRYING -> new GroupTune(16, 40, 10, 20, 40, 120); - case SEVERE -> new GroupTune(28, 30, 9, 19, 60, 120); - case EXTREME -> new GroupTune(40, 20, 8, 18, 80, 120); - }; - } - } - - public record MobGroup( - String id, - String display, - boolean nightOnly, - Integer minY, - Set terrains, - List mobs, - Map severity - ) {} - - @Override - public void load(File configFile) { - loadSafe(configFile); - } - - public boolean loadSafe(File configFile) { - FileConfiguration config = new YamlConfiguration(); - try { - config.load(configFile); - } catch (IOException | InvalidConfigurationException ex) { - Infestations.plugin.getLogger().severe("[Infestations] Failed to load groups.yml: " + ex.getMessage()); - return false; - } - - GROUPS.clear(); - ConfigurationSection root = config.getConfigurationSection("groups"); - if (root == null) { - Infestations.plugin.getLogger().warning("[Infestations] groups.yml has no groups."); - return true; - } - for (String id : root.getKeys(false)) { - ConfigurationSection sec = root.getConfigurationSection(id); - if (sec == null) { - continue; - } - String display = sec.getString("display", id); - boolean nightOnly = sec.getBoolean("night-only", false); - Integer minY = sec.contains("min-y") ? sec.getInt("min-y") : null; - Set terrains = parseTerrains(id, sec.getStringList("terrains")); - List mobs = new ArrayList<>(); - List> list = sec.getMapList("mobs"); - for (Map row : list) { - Object mobId = row.get("id"); - if (mobId == null) { - continue; - } - int weight = 1; - Object w = row.get("weight"); - if (w instanceof Number n) { - weight = Math.max(1, n.intValue()); - } - mobs.add(new WeightedMob(mobId.toString(), weight)); - } - if (mobs.isEmpty()) { - Infestations.plugin.getLogger().warning("[Infestations] Group " + id + " has no mobs."); - continue; - } - EnumMap tunes = new EnumMap<>(Severity.class); - ConfigurationSection sevRoot = sec.getConfigurationSection("severity"); - for (Severity s : Severity.values()) { - ConfigurationSection row = sevRoot != null ? sevRoot.getConfigurationSection(s.id()) : null; - GroupTune d = GroupTune.defaults(s); - if (row == null) { - tunes.put(s, d); - continue; - } - tunes.put(s, new GroupTune( - Math.max(1, row.getInt("ambient-cap", d.ambientCap())), - Math.max(10, row.getInt("ambient-interval-ticks", d.ambientIntervalTicks())), - Math.max(1, row.getInt("ambient-ring-min", d.ambientRingMin())), - Math.max(1, row.getInt("ambient-ring-max", d.ambientRingMax())), - Math.max(1, row.getInt("lure-count", d.lureCount())), - Math.max(10, row.getInt("lure-duration-seconds", d.lureDurationSeconds())) - )); - } - GROUPS.put(id.toLowerCase(Locale.ROOT), new MobGroup( - id, display, nightOnly, minY, Set.copyOf(terrains), List.copyOf(mobs), Map.copyOf(tunes))); - } - return true; - } - - private static Set parseTerrains(String groupId, List raw) { - Set terrains = new LinkedHashSet<>(); - if (raw == null) { - return terrains; - } - for (String row : raw) { - if (row == null || row.isBlank()) { - continue; - } - String name = row.trim().toLowerCase(Locale.ROOT); - if (!knownTerrain(name)) { - Infestations.plugin.getLogger().warning( - "[Infestations] Group " + groupId + " has unknown terrain: " + name); - } - terrains.add(name); - } - return terrains; - } - - private static boolean knownTerrain(String name) { - for (Terrain terrain : Terrain.values()) { - if (terrain.name().equalsIgnoreCase(name)) { - return true; - } - } - return false; - } - - public static MobGroup get(String id) { - if (id == null) { - return null; - } - return GROUPS.get(id.toLowerCase(Locale.ROOT)); - } - - public static String displayName(String groupId) { - MobGroup group = get(groupId); - return group != null ? group.display() : groupId; - } - - public static GroupTune tune(String groupId, Severity severity) { - MobGroup group = get(groupId); - if (group == null || severity == null) { - return GroupTune.defaults(severity != null ? severity : Severity.MILD); - } - GroupTune t = group.severity().get(severity); - return t != null ? t : GroupTune.defaults(severity); - } - - public static boolean nightOnly(String groupId) { - MobGroup group = get(groupId); - return group != null && group.nightOnly(); - } - - public static boolean allowsY(String groupId, int y) { - MobGroup group = get(groupId); - if (group == null || group.minY() == null) { - return true; - } - return y >= group.minY(); - } - - public static boolean allowsTerrain(String groupId, int provinceId) { - MobGroup group = get(groupId); - if (group == null) { - return false; - } - if (group.terrains().isEmpty()) { - return true; - } - Terrain terrain = Provinces.terrain(provinceId); - if (terrain == null) { - return false; - } - return group.terrains().contains(terrain.name().toLowerCase(Locale.ROOT)); - } - - public static String terrainList(String groupId) { - MobGroup group = get(groupId); - if (group == null || group.terrains().isEmpty()) { - return "any land"; - } - return String.join(", ", group.terrains()); - } - - public static boolean isNight(World world) { - if (world == null) { - return false; - } - long t = world.getTime(); - return t >= 13000L && t < 23000L; - } - - public static boolean blockedByNight(String groupId, World world) { - return nightOnly(groupId) && !isNight(world); - } - - public static Set groupIds() { - return Collections.unmodifiableSet(GROUPS.keySet()); - } - - public static String pickMobId(String groupId) { - MobGroup group = get(groupId); - if (group == null || group.mobs().isEmpty()) { - return null; - } - int total = 0; - for (WeightedMob m : group.mobs()) { - total += m.weight(); - } - int roll = ThreadLocalRandom.current().nextInt(Math.max(1, total)); - int acc = 0; - for (WeightedMob m : group.mobs()) { - acc += m.weight(); - if (roll < acc) { - return m.id(); - } - } - return group.mobs().get(0).id(); - } -} +package net.tfminecraft.infestations.loader; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; + +import org.bukkit.World; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; + +import net.tfminecraft.simplefactions.enums.Terrain; +import net.tfminecraft.tlibs.interfaces.LoaderInterface; +import net.tfminecraft.infestations.Infestations; +import net.tfminecraft.infestations.infestation.Severity; +import net.tfminecraft.infestations.utils.Provinces; + +public final class GroupLoader implements LoaderInterface { + + private static final Map GROUPS = new LinkedHashMap<>(); + + public record WeightedMob(String id, int weight) {} + + public record GroupTune( + int ambientCap, + int ambientIntervalTicks, + int ambientRingMin, + int ambientRingMax, + int lureCount, + int lureDurationSeconds + ) { + public static GroupTune defaults(Severity s) { + return switch (s) { + case MILD -> new GroupTune(8, 40, 10, 22, 20, 120); + case WORRYING -> new GroupTune(16, 40, 10, 20, 40, 120); + case SEVERE -> new GroupTune(28, 30, 9, 19, 60, 120); + case EXTREME -> new GroupTune(40, 20, 8, 18, 80, 120); + }; + } + } + + public record MobGroup( + String id, + String display, + boolean nightOnly, + Integer minY, + Set terrains, + List mobs, + Map severity + ) {} + + @Override + public void load(File configFile) { + loadSafe(configFile); + } + + public boolean loadSafe(File configFile) { + FileConfiguration config = new YamlConfiguration(); + try { + config.load(configFile); + } catch (IOException | InvalidConfigurationException ex) { + Infestations.plugin.getLogger().severe("[Infestations] Failed to load groups.yml: " + ex.getMessage()); + return false; + } + + GROUPS.clear(); + ConfigurationSection root = config.getConfigurationSection("groups"); + if (root == null) { + Infestations.plugin.getLogger().warning("[Infestations] groups.yml has no groups."); + return true; + } + for (String id : root.getKeys(false)) { + ConfigurationSection sec = root.getConfigurationSection(id); + if (sec == null) { + continue; + } + String display = sec.getString("display", id); + boolean nightOnly = sec.getBoolean("night-only", false); + Integer minY = sec.contains("min-y") ? sec.getInt("min-y") : null; + Set terrains = parseTerrains(id, sec.getStringList("terrains")); + List mobs = new ArrayList<>(); + List> list = sec.getMapList("mobs"); + for (Map row : list) { + Object mobId = row.get("id"); + if (mobId == null) { + continue; + } + int weight = 1; + Object w = row.get("weight"); + if (w instanceof Number n) { + weight = Math.max(1, n.intValue()); + } + mobs.add(new WeightedMob(mobId.toString(), weight)); + } + if (mobs.isEmpty()) { + Infestations.plugin.getLogger().warning("[Infestations] Group " + id + " has no mobs."); + continue; + } + EnumMap tunes = new EnumMap<>(Severity.class); + ConfigurationSection sevRoot = sec.getConfigurationSection("severity"); + for (Severity s : Severity.values()) { + ConfigurationSection row = sevRoot != null ? sevRoot.getConfigurationSection(s.id()) : null; + GroupTune d = GroupTune.defaults(s); + if (row == null) { + tunes.put(s, d); + continue; + } + tunes.put(s, new GroupTune( + Math.max(1, row.getInt("ambient-cap", d.ambientCap())), + Math.max(10, row.getInt("ambient-interval-ticks", d.ambientIntervalTicks())), + Math.max(1, row.getInt("ambient-ring-min", d.ambientRingMin())), + Math.max(1, row.getInt("ambient-ring-max", d.ambientRingMax())), + Math.max(1, row.getInt("lure-count", d.lureCount())), + Math.max(10, row.getInt("lure-duration-seconds", d.lureDurationSeconds())) + )); + } + GROUPS.put(id.toLowerCase(Locale.ROOT), new MobGroup( + id, display, nightOnly, minY, Set.copyOf(terrains), List.copyOf(mobs), Map.copyOf(tunes))); + } + return true; + } + + private static Set parseTerrains(String groupId, List raw) { + Set terrains = new LinkedHashSet<>(); + if (raw == null) { + return terrains; + } + for (String row : raw) { + if (row == null || row.isBlank()) { + continue; + } + String name = row.trim().toLowerCase(Locale.ROOT); + if (!knownTerrain(name)) { + Infestations.plugin.getLogger().warning( + "[Infestations] Group " + groupId + " has unknown terrain: " + name); + } + terrains.add(name); + } + return terrains; + } + + private static boolean knownTerrain(String name) { + for (Terrain terrain : Terrain.values()) { + if (terrain.name().equalsIgnoreCase(name)) { + return true; + } + } + return false; + } + + public static MobGroup get(String id) { + if (id == null) { + return null; + } + return GROUPS.get(id.toLowerCase(Locale.ROOT)); + } + + public static String displayName(String groupId) { + MobGroup group = get(groupId); + return group != null ? group.display() : groupId; + } + + public static GroupTune tune(String groupId, Severity severity) { + MobGroup group = get(groupId); + if (group == null || severity == null) { + return GroupTune.defaults(severity != null ? severity : Severity.MILD); + } + GroupTune t = group.severity().get(severity); + return t != null ? t : GroupTune.defaults(severity); + } + + public static boolean nightOnly(String groupId) { + MobGroup group = get(groupId); + return group != null && group.nightOnly(); + } + + public static boolean allowsY(String groupId, int y) { + MobGroup group = get(groupId); + if (group == null || group.minY() == null) { + return true; + } + return y >= group.minY(); + } + + public static boolean allowsTerrain(String groupId, int provinceId) { + MobGroup group = get(groupId); + if (group == null) { + return false; + } + if (group.terrains().isEmpty()) { + return true; + } + Terrain terrain = Provinces.terrain(provinceId); + if (terrain == null) { + return false; + } + return group.terrains().contains(terrain.name().toLowerCase(Locale.ROOT)); + } + + public static String terrainList(String groupId) { + MobGroup group = get(groupId); + if (group == null || group.terrains().isEmpty()) { + return "any land"; + } + return String.join(", ", group.terrains()); + } + + public static boolean isNight(World world) { + if (world == null) { + return false; + } + long t = world.getTime(); + return t >= 13000L && t < 23000L; + } + + public static boolean blockedByNight(String groupId, World world) { + return nightOnly(groupId) && !isNight(world); + } + + public static Set groupIds() { + return Collections.unmodifiableSet(GROUPS.keySet()); + } + + public static String pickMobId(String groupId) { + MobGroup group = get(groupId); + if (group == null || group.mobs().isEmpty()) { + return null; + } + int total = 0; + for (WeightedMob m : group.mobs()) { + total += m.weight(); + } + int roll = ThreadLocalRandom.current().nextInt(Math.max(1, total)); + int acc = 0; + for (WeightedMob m : group.mobs()) { + acc += m.weight(); + if (roll < acc) { + return m.id(); + } + } + return group.mobs().get(0).id(); + } +} diff --git a/src/main/java/net/tfminecraft/infestations/lure/LureFurniture.java b/src/main/java/net/tfminecraft/infestations/lure/LureFurniture.java index 5ef3dcb..e9207c9 100644 --- a/src/main/java/net/tfminecraft/infestations/lure/LureFurniture.java +++ b/src/main/java/net/tfminecraft/infestations/lure/LureFurniture.java @@ -1,93 +1,93 @@ -package net.tfminecraft.infestations.lure; - -import org.bukkit.Bukkit; -import org.bukkit.Chunk; -import org.bukkit.Location; -import org.bukkit.plugin.Plugin; - -import net.tfminecraft.InteractibleFurniture; -import net.tfminecraft.furniture.Furniture; -import net.tfminecraft.furniture.FurnitureType; -import net.tfminecraft.infestations.cache.Cache; -import net.tfminecraft.infestations.infestation.Infestation; - -public final class LureFurniture { - - private LureFurniture() {} - - public static boolean available() { - Plugin plugin = Bukkit.getPluginManager().getPlugin("InteractibleFurniture"); - return plugin != null && plugin.isEnabled(); - } - - public static boolean isLure(Furniture furniture) { - if (furniture == null) { - return false; - } - FurnitureType type = furniture.getType(); - if (type == null || type.getItemPath() == null || type.getItemPath().isBlank()) { - return false; - } - return type.getItemPath().equalsIgnoreCase(Cache.lureItem); - } - - public static boolean matches(Infestation infestation, Furniture furniture) { - if (infestation == null || !infestation.hasLure() || furniture == null || furniture.getLoc() == null) { - return false; - } - Location loc = furniture.getLoc(); - if (infestation.getWorldName() == null || loc.getWorld() == null) { - return false; - } - if (!infestation.getWorldName().equals(loc.getWorld().getName())) { - return false; - } - return infestation.getLureX() != null - && infestation.getLureX() == loc.getBlockX() - && infestation.getLureY() != null - && infestation.getLureY() == loc.getBlockY() - && infestation.getLureZ() != null - && infestation.getLureZ() == loc.getBlockZ(); - } - - public static void remove(Furniture furniture) { - if (!available() || furniture == null) { - return; - } - InteractibleFurniture.getInstance().getFurnitureManager().removePlugin(furniture); - } - - public static void removeFor(Infestation infestation) { - if (!available() || infestation == null) { - return; - } - Location loc = infestation.lureLocation(); - if (loc == null || loc.getWorld() == null || !loc.getWorld().isChunkLoaded(loc.getBlockX() >> 4, loc.getBlockZ() >> 4)) { - return; - } - Chunk chunk = loc.getChunk(); - for (Furniture furniture : InteractibleFurniture.getInstance().getFurnitureManager().getFurnitureInChunk(chunk)) { - if (isLure(furniture) && matches(infestation, furniture)) { - remove(furniture); - } - } - } - - public static void sweepChunk(Chunk chunk, java.util.function.Function byProvince) { - if (!available() || chunk == null) { - return; - } - for (Furniture furniture : java.util.List.copyOf( - InteractibleFurniture.getInstance().getFurnitureManager().getFurnitureInChunk(chunk))) { - if (!isLure(furniture)) { - continue; - } - Infestation infestation = byProvince.apply( - net.tfminecraft.infestations.utils.Provinces.at(furniture.getLoc())); - if (infestation != null && matches(infestation, furniture)) { - continue; - } - remove(furniture); - } - } -} +package net.tfminecraft.infestations.lure; + +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.plugin.Plugin; + +import net.tfminecraft.interactiblefurniture.InteractibleFurniture; +import net.tfminecraft.interactiblefurniture.furniture.Furniture; +import net.tfminecraft.interactiblefurniture.furniture.FurnitureType; +import net.tfminecraft.infestations.cache.Cache; +import net.tfminecraft.infestations.infestation.Infestation; + +public final class LureFurniture { + + private LureFurniture() {} + + public static boolean available() { + Plugin plugin = Bukkit.getPluginManager().getPlugin("InteractibleFurniture"); + return plugin != null && plugin.isEnabled(); + } + + public static boolean isLure(Furniture furniture) { + if (furniture == null) { + return false; + } + FurnitureType type = furniture.getType(); + if (type == null || type.getItemPath() == null || type.getItemPath().isBlank()) { + return false; + } + return type.getItemPath().equalsIgnoreCase(Cache.lureItem); + } + + public static boolean matches(Infestation infestation, Furniture furniture) { + if (infestation == null || !infestation.hasLure() || furniture == null || furniture.getLoc() == null) { + return false; + } + Location loc = furniture.getLoc(); + if (infestation.getWorldName() == null || loc.getWorld() == null) { + return false; + } + if (!infestation.getWorldName().equals(loc.getWorld().getName())) { + return false; + } + return infestation.getLureX() != null + && infestation.getLureX() == loc.getBlockX() + && infestation.getLureY() != null + && infestation.getLureY() == loc.getBlockY() + && infestation.getLureZ() != null + && infestation.getLureZ() == loc.getBlockZ(); + } + + public static void remove(Furniture furniture) { + if (!available() || furniture == null) { + return; + } + InteractibleFurniture.getInstance().getFurnitureManager().removePlugin(furniture); + } + + public static void removeFor(Infestation infestation) { + if (!available() || infestation == null) { + return; + } + Location loc = infestation.lureLocation(); + if (loc == null || loc.getWorld() == null || !loc.getWorld().isChunkLoaded(loc.getBlockX() >> 4, loc.getBlockZ() >> 4)) { + return; + } + Chunk chunk = loc.getChunk(); + for (Furniture furniture : InteractibleFurniture.getInstance().getFurnitureManager().getFurnitureInChunk(chunk)) { + if (isLure(furniture) && matches(infestation, furniture)) { + remove(furniture); + } + } + } + + public static void sweepChunk(Chunk chunk, java.util.function.Function byProvince) { + if (!available() || chunk == null) { + return; + } + for (Furniture furniture : java.util.List.copyOf( + InteractibleFurniture.getInstance().getFurnitureManager().getFurnitureInChunk(chunk))) { + if (!isLure(furniture)) { + continue; + } + Infestation infestation = byProvince.apply( + net.tfminecraft.infestations.utils.Provinces.at(furniture.getLoc())); + if (infestation != null && matches(infestation, furniture)) { + continue; + } + remove(furniture); + } + } +} diff --git a/src/main/java/net/tfminecraft/infestations/lure/LureHologram.java b/src/main/java/net/tfminecraft/infestations/lure/LureHologram.java index a22ca65..a3a04dd 100644 --- a/src/main/java/net/tfminecraft/infestations/lure/LureHologram.java +++ b/src/main/java/net/tfminecraft/infestations/lure/LureHologram.java @@ -1,130 +1,130 @@ -package net.tfminecraft.infestations.lure; - -import org.bukkit.Bukkit; -import org.bukkit.Chunk; -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.block.Block; -import org.bukkit.entity.Entity; -import org.bukkit.entity.TextDisplay; - -import net.tfminecraft.infestations.Infestations; -import net.tfminecraft.infestations.Messages; -import net.tfminecraft.infestations.cache.Cache; -import net.tfminecraft.infestations.infestation.Infestation; -import net.tfminecraft.infestations.infestation.LurePhase; -import net.tfminecraft.infestations.spawn.Keys; - -public final class LureHologram { - - private LureHologram() {} - - public static void tick(Infestation infestation) { - Block block = infestation.lureBlock(); - if (block == null || !infestation.hasLure()) { - return; - } - Location pos = position(block); - if (pos.getWorld() == null) { - return; - } - double rangeSq = Cache.hologramViewRange * Cache.hologramViewRange; - boolean nearby = false; - for (var player : Bukkit.getOnlinePlayers()) { - if (player.getWorld() == pos.getWorld() && player.getLocation().distanceSquared(pos) <= rangeSq) { - nearby = true; - break; - } - } - TextDisplay display = find(block, infestation.getProvinceId()); - if (!nearby) { - if (display != null) { - display.remove(); - } - return; - } - if (display == null || display.isDead()) { - display = spawn(block, infestation.getProvinceId()); - } - if (display != null) { - display.setText(text(infestation)); - } - } - - public static void remove(Infestation infestation) { - Block block = infestation.lureBlock(); - if (block == null) { - return; - } - TextDisplay display = find(block, infestation.getProvinceId()); - if (display != null) { - display.remove(); - } - } - - public static void removeOrphans(Chunk chunk) { - for (Entity entity : chunk.getEntities()) { - if (!(entity instanceof TextDisplay display)) { - continue; - } - Integer provinceId = Keys.displayProvince(display.getPersistentDataContainer()); - if (provinceId == null) { - continue; - } - Infestation infestation = Infestations.plugin.getInfestationManager().get(provinceId); - if (infestation == null || !infestation.hasLure()) { - display.remove(); - } - } - } - - private static TextDisplay spawn(Block block, int provinceId) { - Location pos = position(block); - World world = pos.getWorld(); - if (world == null) { - return null; - } - return world.spawn(pos, TextDisplay.class, td -> { - td.setInvulnerable(true); - td.setPersistent(true); - td.setGravity(false); - try { - td.setBillboard(org.bukkit.entity.Display.Billboard.CENTER); - } catch (Throwable ignored) { - } - try { - td.setShadowed(true); - } catch (Throwable ignored) { - } - Keys.tagDisplay(td.getPersistentDataContainer(), provinceId); - }); - } - - private static TextDisplay find(Block block, int provinceId) { - Location pos = position(block); - World world = pos.getWorld(); - if (world == null) { - return null; - } - for (Entity entity : world.getNearbyEntities(pos, 2, 2, 2)) { - if (entity instanceof TextDisplay display) { - Integer id = Keys.displayProvince(display.getPersistentDataContainer()); - if (id != null && id == provinceId) { - return display; - } - } - } - return null; - } - - private static Location position(Block block) { - return block.getLocation().clone().add(0.5, 1.6, 0.5); - } - - private static String text(Infestation infestation) { - if (infestation.getPhase() == LurePhase.JOINING) { - return Messages.get("hologram-joining", "seconds", String.valueOf(infestation.joinSecondsLeft())); - } - return Messages.get("hologram-remaining", "remaining", String.valueOf(infestation.getLureRemaining())); - } -} +package net.tfminecraft.infestations.lure; + +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.entity.Entity; +import org.bukkit.entity.TextDisplay; + +import net.tfminecraft.infestations.Infestations; +import net.tfminecraft.infestations.Messages; +import net.tfminecraft.infestations.cache.Cache; +import net.tfminecraft.infestations.infestation.Infestation; +import net.tfminecraft.infestations.infestation.LurePhase; +import net.tfminecraft.infestations.spawn.Keys; + +public final class LureHologram { + + private LureHologram() {} + + public static void tick(Infestation infestation) { + Block block = infestation.lureBlock(); + if (block == null || !infestation.hasLure()) { + return; + } + Location pos = position(block); + if (pos.getWorld() == null) { + return; + } + double rangeSq = Cache.hologramViewRange * Cache.hologramViewRange; + boolean nearby = false; + for (var player : Bukkit.getOnlinePlayers()) { + if (player.getWorld() == pos.getWorld() && player.getLocation().distanceSquared(pos) <= rangeSq) { + nearby = true; + break; + } + } + TextDisplay display = find(block, infestation.getProvinceId()); + if (!nearby) { + if (display != null) { + display.remove(); + } + return; + } + if (display == null || display.isDead()) { + display = spawn(block, infestation.getProvinceId()); + } + if (display != null) { + display.setText(text(infestation)); + } + } + + public static void remove(Infestation infestation) { + Block block = infestation.lureBlock(); + if (block == null) { + return; + } + TextDisplay display = find(block, infestation.getProvinceId()); + if (display != null) { + display.remove(); + } + } + + public static void removeOrphans(Chunk chunk) { + for (Entity entity : chunk.getEntities()) { + if (!(entity instanceof TextDisplay display)) { + continue; + } + Integer provinceId = Keys.displayProvince(display.getPersistentDataContainer()); + if (provinceId == null) { + continue; + } + Infestation infestation = Infestations.plugin.getInfestationManager().get(provinceId); + if (infestation == null || !infestation.hasLure()) { + display.remove(); + } + } + } + + private static TextDisplay spawn(Block block, int provinceId) { + Location pos = position(block); + World world = pos.getWorld(); + if (world == null) { + return null; + } + return world.spawn(pos, TextDisplay.class, td -> { + td.setInvulnerable(true); + td.setPersistent(true); + td.setGravity(false); + try { + td.setBillboard(org.bukkit.entity.Display.Billboard.CENTER); + } catch (Throwable ignored) { + } + try { + td.setShadowed(true); + } catch (Throwable ignored) { + } + Keys.tagDisplay(td.getPersistentDataContainer(), provinceId); + }); + } + + private static TextDisplay find(Block block, int provinceId) { + Location pos = position(block); + World world = pos.getWorld(); + if (world == null) { + return null; + } + for (Entity entity : world.getNearbyEntities(pos, 2, 2, 2)) { + if (entity instanceof TextDisplay display) { + Integer id = Keys.displayProvince(display.getPersistentDataContainer()); + if (id != null && id == provinceId) { + return display; + } + } + } + return null; + } + + private static Location position(Block block) { + return block.getLocation().clone().add(0.5, 1.6, 0.5); + } + + private static String text(Infestation infestation) { + if (infestation.getPhase() == LurePhase.JOINING) { + return Messages.get("hologram-joining", "seconds", String.valueOf(infestation.joinSecondsLeft())); + } + return Messages.get("hologram-remaining", "remaining", String.valueOf(infestation.getLureRemaining())); + } +} diff --git a/src/main/java/net/tfminecraft/infestations/map/InfestationMapExport.java b/src/main/java/net/tfminecraft/infestations/map/InfestationMapExport.java index 84ef0a9..ac365de 100644 --- a/src/main/java/net/tfminecraft/infestations/map/InfestationMapExport.java +++ b/src/main/java/net/tfminecraft/infestations/map/InfestationMapExport.java @@ -1,60 +1,60 @@ -package net.tfminecraft.infestations.map; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Collection; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; - -import org.bukkit.Bukkit; - -import me.Plugins.SimpleFactions.REST.RestServer; -import net.tfminecraft.infestations.Infestations; -import net.tfminecraft.infestations.infestation.Infestation; -import net.tfminecraft.infestations.loader.GroupLoader; - -public final class InfestationMapExport { - - private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); - - private InfestationMapExport() {} - - public static void exportAsync(Collection infestations) { - JsonObject root = new JsonObject(); - JsonArray provinces = new JsonArray(); - for (Infestation infestation : infestations) { - JsonObject o = new JsonObject(); - o.addProperty("id", infestation.getProvinceId()); - o.addProperty("severity", infestation.getSeverity().id()); - o.addProperty("group", infestation.getGroupId()); - o.addProperty("display", GroupLoader.displayName(infestation.getGroupId())); - provinces.add(o); - } - root.add("provinces", provinces); - String json = GSON.toJson(root); - - Bukkit.getScheduler().runTaskAsynchronously(Infestations.plugin, () -> { - File file = new File(Infestations.plugin.getDataFolder(), "MapAPI/infestation_data.json"); - File parent = file.getParentFile(); - if (parent != null) { - parent.mkdirs(); - } - try (FileWriter writer = new FileWriter(file, StandardCharsets.UTF_8)) { - writer.write(json); - } catch (IOException ex) { - Infestations.plugin.getLogger().warning("[Infestations] Map export write failed: " + ex.getMessage()); - return; - } - try { - RestServer.upload("infestation_data", file); - } catch (Throwable t) { - Infestations.plugin.getLogger().warning("[Infestations] Map upload failed: " + t.getMessage()); - } - }); - } -} +package net.tfminecraft.infestations.map; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collection; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +import org.bukkit.Bukkit; + +import net.tfminecraft.simplefactions.rest.RestServer; +import net.tfminecraft.infestations.Infestations; +import net.tfminecraft.infestations.infestation.Infestation; +import net.tfminecraft.infestations.loader.GroupLoader; + +public final class InfestationMapExport { + + private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); + + private InfestationMapExport() {} + + public static void exportAsync(Collection infestations) { + JsonObject root = new JsonObject(); + JsonArray provinces = new JsonArray(); + for (Infestation infestation : infestations) { + JsonObject o = new JsonObject(); + o.addProperty("id", infestation.getProvinceId()); + o.addProperty("severity", infestation.getSeverity().id()); + o.addProperty("group", infestation.getGroupId()); + o.addProperty("display", GroupLoader.displayName(infestation.getGroupId())); + provinces.add(o); + } + root.add("provinces", provinces); + String json = GSON.toJson(root); + + Bukkit.getScheduler().runTaskAsynchronously(Infestations.plugin, () -> { + File file = new File(Infestations.plugin.getDataFolder(), "MapAPI/infestation_data.json"); + File parent = file.getParentFile(); + if (parent != null) { + parent.mkdirs(); + } + try (FileWriter writer = new FileWriter(file, StandardCharsets.UTF_8)) { + writer.write(json); + } catch (IOException ex) { + Infestations.plugin.getLogger().warning("[Infestations] Map export write failed: " + ex.getMessage()); + return; + } + try { + RestServer.upload("infestation_data", file); + } catch (Throwable t) { + Infestations.plugin.getLogger().warning("[Infestations] Map upload failed: " + t.getMessage()); + } + }); + } +} diff --git a/src/main/java/net/tfminecraft/infestations/spawn/AmbientSpawnService.java b/src/main/java/net/tfminecraft/infestations/spawn/AmbientSpawnService.java index be0659d..e482e5c 100644 --- a/src/main/java/net/tfminecraft/infestations/spawn/AmbientSpawnService.java +++ b/src/main/java/net/tfminecraft/infestations/spawn/AmbientSpawnService.java @@ -1,175 +1,175 @@ -package net.tfminecraft.infestations.spawn; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ThreadLocalRandom; - -import org.bukkit.Bukkit; -import org.bukkit.Chunk; -import org.bukkit.GameMode; -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.entity.Entity; -import org.bukkit.entity.LivingEntity; -import org.bukkit.entity.Player; - -import net.tfminecraft.infestations.Infestations; -import net.tfminecraft.infestations.infestation.Infestation; -import net.tfminecraft.infestations.infestation.LurePhase; -import net.tfminecraft.infestations.loader.GroupLoader; -import net.tfminecraft.infestations.loader.GroupLoader.GroupTune; -import net.tfminecraft.infestations.utils.Provinces; - -public final class AmbientSpawnService { - - private final Map lastSpawnTick = new HashMap<>(); - - public void tick(CollectionInfestations infestations, int tick) { - for (Player player : Bukkit.getOnlinePlayers()) { - if (player.getGameMode() == GameMode.CREATIVE || player.getGameMode() == GameMode.SPECTATOR) { - continue; - } - int provinceId = Provinces.at(player); - Infestation infestation = infestations.get(provinceId); - if (infestation == null || infestation.getPhase() != LurePhase.NONE) { - continue; - } - GroupTune tune = GroupLoader.tune(infestation.getGroupId(), infestation.getSeverity()); - int last = lastSpawnTick.getOrDefault(player.getUniqueId(), 0); - if (tick - last < tune.ambientIntervalTicks()) { - continue; - } - lastSpawnTick.put(player.getUniqueId(), tick); - if (GroupLoader.blockedByNight(infestation.getGroupId(), player.getWorld())) { - SpawnLog.line(infestation, player.getName(), "night"); - continue; - } - if (infestation.getAmbientAlive() >= tune.ambientCap()) { - SpawnLog.line(infestation, player.getName(), "cap alive=" - + infestation.getAmbientAlive() + "/" + tune.ambientCap()); - continue; - } - spawnAround(player, infestation, 1); - } - } - - public void spawnAround(Player player, Infestation infestation, int amount) { - GroupTune tune = GroupLoader.tune(infestation.getGroupId(), infestation.getSeverity()); - int room = tune.ambientCap() - infestation.getAmbientAlive(); - int want = Math.min(amount, Math.max(0, room)); - if (want <= 0) { - return; - } - UUID playerId = player.getUniqueId(); - String playerName = player.getName(); - int provinceId = infestation.getProvinceId(); - List spots = SpawnPlanner.find( - player.getLocation(), - tune.ambientRingMin(), - tune.ambientRingMax(), - want, - loc -> Provinces.at(loc) == provinceId - && GroupLoader.allowsY(infestation.getGroupId(), loc.getBlockY())); - if (spots.isEmpty()) { - SpawnLog.line(infestation, playerName, "no-spot"); - return; - } - for (Location spot : spots) { - int delay = ThreadLocalRandom.current().nextInt(20, 61); - Bukkit.getScheduler().runTaskLater(Infestations.plugin, () -> { - if (Infestations.plugin.getInfestationManager() == null) { - return; - } - Infestation current = Infestations.plugin.getInfestationManager().get(provinceId); - if (current != infestation || infestation.getPhase() != LurePhase.NONE) { - return; - } - Player origin = Bukkit.getPlayer(playerId); - if (origin == null || !origin.isOnline() || Provinces.at(origin) != provinceId) { - SpawnLog.line(infestation, playerName, "left-province"); - return; - } - if (GroupLoader.blockedByNight(infestation.getGroupId(), origin.getWorld())) { - SpawnLog.line(infestation, playerName, "night"); - return; - } - if (infestation.getAmbientAlive() >= tune.ambientCap()) { - SpawnLog.line(infestation, playerName, "cap alive=" - + infestation.getAmbientAlive() + "/" + tune.ambientCap()); - return; - } - if (Provinces.at(spot) != provinceId) { - SpawnLog.line(infestation, playerName, "no-spot"); - return; - } - LivingEntity spawned = MythicSpawner.spawn( - spot, infestation.getGroupId(), infestation.getProvinceId(), Keys.KIND_AMBIENT); - if (spawned != null) { - infestation.setAmbientAlive(infestation.getAmbientAlive() + 1); - SpawnLog.spawned(infestation, playerName, spot, infestation.getAmbientAlive(), tune.ambientCap()); - } else { - SpawnLog.line(infestation, playerName, "unknown-mythic"); - } - }, delay); - } - } - - /** - * Set ambientAlive from loaded chunks. Does not despawn anything. - */ - public void reconcileLoaded(CollectionInfestations infestations) { - recountAmbient(infestations); - } - - public void onChunkLoaded(Chunk chunk, CollectionInfestations infestations) { - if (hasTagged(chunk)) { - recountAmbient(infestations); - } - } - - private boolean hasTagged(Chunk chunk) { - for (Entity entity : chunk.getEntities()) { - if (!(entity instanceof LivingEntity living) || living instanceof Player) { - continue; - } - if (Keys.infestationId(living.getPersistentDataContainer()) != null) { - return true; - } - } - return false; - } - - private void recountAmbient(CollectionInfestations infestations) { - Map counts = new HashMap<>(); - for (World world : Bukkit.getWorlds()) { - for (Chunk chunk : world.getLoadedChunks()) { - for (Entity entity : chunk.getEntities()) { - if (!(entity instanceof LivingEntity living) || living instanceof Player) { - continue; - } - Integer id = Keys.infestationId(living.getPersistentDataContainer()); - String kind = Keys.kind(living.getPersistentDataContainer()); - if (id == null || !Keys.KIND_AMBIENT.equals(kind)) { - continue; - } - Infestation infestation = infestations.get(id); - if (infestation == null || infestation.getPhase() != LurePhase.NONE) { - continue; - } - counts.merge(id, 1, Integer::sum); - } - } - } - for (Infestation infestation : infestations.all()) { - infestation.setAmbientAlive(counts.getOrDefault(infestation.getProvinceId(), 0)); - } - } - - public interface CollectionInfestations { - Infestation get(int provinceId); - - Iterable all(); - } -} +package net.tfminecraft.infestations.spawn; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ThreadLocalRandom; + +import org.bukkit.Bukkit; +import org.bukkit.Chunk; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Entity; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; + +import net.tfminecraft.infestations.Infestations; +import net.tfminecraft.infestations.infestation.Infestation; +import net.tfminecraft.infestations.infestation.LurePhase; +import net.tfminecraft.infestations.loader.GroupLoader; +import net.tfminecraft.infestations.loader.GroupLoader.GroupTune; +import net.tfminecraft.infestations.utils.Provinces; + +public final class AmbientSpawnService { + + private final Map lastSpawnTick = new HashMap<>(); + + public void tick(CollectionInfestations infestations, int tick) { + for (Player player : Bukkit.getOnlinePlayers()) { + if (player.getGameMode() == GameMode.CREATIVE || player.getGameMode() == GameMode.SPECTATOR) { + continue; + } + int provinceId = Provinces.at(player); + Infestation infestation = infestations.get(provinceId); + if (infestation == null || infestation.getPhase() != LurePhase.NONE) { + continue; + } + GroupTune tune = GroupLoader.tune(infestation.getGroupId(), infestation.getSeverity()); + int last = lastSpawnTick.getOrDefault(player.getUniqueId(), 0); + if (tick - last < tune.ambientIntervalTicks()) { + continue; + } + lastSpawnTick.put(player.getUniqueId(), tick); + if (GroupLoader.blockedByNight(infestation.getGroupId(), player.getWorld())) { + SpawnLog.line(infestation, player.getName(), "night"); + continue; + } + if (infestation.getAmbientAlive() >= tune.ambientCap()) { + SpawnLog.line(infestation, player.getName(), "cap alive=" + + infestation.getAmbientAlive() + "/" + tune.ambientCap()); + continue; + } + spawnAround(player, infestation, 1); + } + } + + public void spawnAround(Player player, Infestation infestation, int amount) { + GroupTune tune = GroupLoader.tune(infestation.getGroupId(), infestation.getSeverity()); + int room = tune.ambientCap() - infestation.getAmbientAlive(); + int want = Math.min(amount, Math.max(0, room)); + if (want <= 0) { + return; + } + UUID playerId = player.getUniqueId(); + String playerName = player.getName(); + int provinceId = infestation.getProvinceId(); + List spots = SpawnPlanner.find( + player.getLocation(), + tune.ambientRingMin(), + tune.ambientRingMax(), + want, + loc -> Provinces.at(loc) == provinceId + && GroupLoader.allowsY(infestation.getGroupId(), loc.getBlockY())); + if (spots.isEmpty()) { + SpawnLog.line(infestation, playerName, "no-spot"); + return; + } + for (Location spot : spots) { + int delay = ThreadLocalRandom.current().nextInt(20, 61); + Bukkit.getScheduler().runTaskLater(Infestations.plugin, () -> { + if (Infestations.plugin.getInfestationManager() == null) { + return; + } + Infestation current = Infestations.plugin.getInfestationManager().get(provinceId); + if (current != infestation || infestation.getPhase() != LurePhase.NONE) { + return; + } + Player origin = Bukkit.getPlayer(playerId); + if (origin == null || !origin.isOnline() || Provinces.at(origin) != provinceId) { + SpawnLog.line(infestation, playerName, "left-province"); + return; + } + if (GroupLoader.blockedByNight(infestation.getGroupId(), origin.getWorld())) { + SpawnLog.line(infestation, playerName, "night"); + return; + } + if (infestation.getAmbientAlive() >= tune.ambientCap()) { + SpawnLog.line(infestation, playerName, "cap alive=" + + infestation.getAmbientAlive() + "/" + tune.ambientCap()); + return; + } + if (Provinces.at(spot) != provinceId) { + SpawnLog.line(infestation, playerName, "no-spot"); + return; + } + LivingEntity spawned = MythicSpawner.spawn( + spot, infestation.getGroupId(), infestation.getProvinceId(), Keys.KIND_AMBIENT); + if (spawned != null) { + infestation.setAmbientAlive(infestation.getAmbientAlive() + 1); + SpawnLog.spawned(infestation, playerName, spot, infestation.getAmbientAlive(), tune.ambientCap()); + } else { + SpawnLog.line(infestation, playerName, "unknown-mythic"); + } + }, delay); + } + } + + /** + * Set ambientAlive from loaded chunks. Does not despawn anything. + */ + public void reconcileLoaded(CollectionInfestations infestations) { + recountAmbient(infestations); + } + + public void onChunkLoaded(Chunk chunk, CollectionInfestations infestations) { + if (hasTagged(chunk)) { + recountAmbient(infestations); + } + } + + private boolean hasTagged(Chunk chunk) { + for (Entity entity : chunk.getEntities()) { + if (!(entity instanceof LivingEntity living) || living instanceof Player) { + continue; + } + if (Keys.infestationId(living.getPersistentDataContainer()) != null) { + return true; + } + } + return false; + } + + private void recountAmbient(CollectionInfestations infestations) { + Map counts = new HashMap<>(); + for (World world : Bukkit.getWorlds()) { + for (Chunk chunk : world.getLoadedChunks()) { + for (Entity entity : chunk.getEntities()) { + if (!(entity instanceof LivingEntity living) || living instanceof Player) { + continue; + } + Integer id = Keys.infestationId(living.getPersistentDataContainer()); + String kind = Keys.kind(living.getPersistentDataContainer()); + if (id == null || !Keys.KIND_AMBIENT.equals(kind)) { + continue; + } + Infestation infestation = infestations.get(id); + if (infestation == null || infestation.getPhase() != LurePhase.NONE) { + continue; + } + counts.merge(id, 1, Integer::sum); + } + } + } + for (Infestation infestation : infestations.all()) { + infestation.setAmbientAlive(counts.getOrDefault(infestation.getProvinceId(), 0)); + } + } + + public interface CollectionInfestations { + Infestation get(int provinceId); + + Iterable all(); + } +} diff --git a/src/main/java/net/tfminecraft/infestations/spawn/Keys.java b/src/main/java/net/tfminecraft/infestations/spawn/Keys.java index ad45f9d..5e8dbde 100644 --- a/src/main/java/net/tfminecraft/infestations/spawn/Keys.java +++ b/src/main/java/net/tfminecraft/infestations/spawn/Keys.java @@ -1,54 +1,54 @@ -package net.tfminecraft.infestations.spawn; - -import org.bukkit.NamespacedKey; -import org.bukkit.persistence.PersistentDataContainer; -import org.bukkit.persistence.PersistentDataType; - -import net.tfminecraft.infestations.Infestations; - -public final class Keys { - - public static final String KIND_AMBIENT = "ambient"; - public static final String KIND_LURE = "lure"; - - private Keys() {} - - public static NamespacedKey infestationId() { - return new NamespacedKey(Infestations.plugin, "infestationId"); - } - - public static NamespacedKey kind() { - return new NamespacedKey(Infestations.plugin, "kind"); - } - - public static NamespacedKey displayProvince() { - return new NamespacedKey(Infestations.plugin, "lureDisplay"); - } - - public static void tagMob(PersistentDataContainer pdc, int provinceId, String kind) { - pdc.set(infestationId(), PersistentDataType.INTEGER, provinceId); - pdc.set(kind(), PersistentDataType.STRING, kind); - } - - public static Integer infestationId(PersistentDataContainer pdc) { - if (!pdc.has(infestationId(), PersistentDataType.INTEGER)) { - return null; - } - return pdc.get(infestationId(), PersistentDataType.INTEGER); - } - - public static String kind(PersistentDataContainer pdc) { - return pdc.get(kind(), PersistentDataType.STRING); - } - - public static void tagDisplay(PersistentDataContainer pdc, int provinceId) { - pdc.set(displayProvince(), PersistentDataType.INTEGER, provinceId); - } - - public static Integer displayProvince(PersistentDataContainer pdc) { - if (!pdc.has(displayProvince(), PersistentDataType.INTEGER)) { - return null; - } - return pdc.get(displayProvince(), PersistentDataType.INTEGER); - } -} +package net.tfminecraft.infestations.spawn; + +import org.bukkit.NamespacedKey; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; + +import net.tfminecraft.infestations.Infestations; + +public final class Keys { + + public static final String KIND_AMBIENT = "ambient"; + public static final String KIND_LURE = "lure"; + + private Keys() {} + + public static NamespacedKey infestationId() { + return new NamespacedKey(Infestations.plugin, "infestationId"); + } + + public static NamespacedKey kind() { + return new NamespacedKey(Infestations.plugin, "kind"); + } + + public static NamespacedKey displayProvince() { + return new NamespacedKey(Infestations.plugin, "lureDisplay"); + } + + public static void tagMob(PersistentDataContainer pdc, int provinceId, String kind) { + pdc.set(infestationId(), PersistentDataType.INTEGER, provinceId); + pdc.set(kind(), PersistentDataType.STRING, kind); + } + + public static Integer infestationId(PersistentDataContainer pdc) { + if (!pdc.has(infestationId(), PersistentDataType.INTEGER)) { + return null; + } + return pdc.get(infestationId(), PersistentDataType.INTEGER); + } + + public static String kind(PersistentDataContainer pdc) { + return pdc.get(kind(), PersistentDataType.STRING); + } + + public static void tagDisplay(PersistentDataContainer pdc, int provinceId) { + pdc.set(displayProvince(), PersistentDataType.INTEGER, provinceId); + } + + public static Integer displayProvince(PersistentDataContainer pdc) { + if (!pdc.has(displayProvince(), PersistentDataType.INTEGER)) { + return null; + } + return pdc.get(displayProvince(), PersistentDataType.INTEGER); + } +} diff --git a/src/main/java/net/tfminecraft/infestations/spawn/MythicSpawner.java b/src/main/java/net/tfminecraft/infestations/spawn/MythicSpawner.java index 1c783d0..7c0cb79 100644 --- a/src/main/java/net/tfminecraft/infestations/spawn/MythicSpawner.java +++ b/src/main/java/net/tfminecraft/infestations/spawn/MythicSpawner.java @@ -1,37 +1,37 @@ -package net.tfminecraft.infestations.spawn; - -import org.bukkit.Location; -import org.bukkit.entity.LivingEntity; - -import io.lumine.mythic.api.mobs.MythicMob; -import io.lumine.mythic.bukkit.BukkitAdapter; -import io.lumine.mythic.bukkit.MythicBukkit; -import io.lumine.mythic.core.mobs.ActiveMob; -import net.tfminecraft.infestations.Infestations; -import net.tfminecraft.infestations.loader.GroupLoader; - -public final class MythicSpawner { - - private MythicSpawner() {} - - public static LivingEntity spawn(Location location, String groupId, int provinceId, String kind) { - String mobId = GroupLoader.pickMobId(groupId); - if (mobId == null || location == null) { - return null; - } - MythicMob mm = MythicBukkit.inst().getMobManager().getMythicMob(mobId).orElse(null); - if (mm == null) { - Infestations.plugin.getLogger().warning("[Infestations] Unknown Mythic mob: " + mobId); - return null; - } - ActiveMob am = mm.spawn(BukkitAdapter.adapt(location), 1); - if (am == null || am.getEntity() == null) { - return null; - } - if (!(am.getEntity().getBukkitEntity() instanceof LivingEntity living)) { - return null; - } - Keys.tagMob(living.getPersistentDataContainer(), provinceId, kind); - return living; - } -} +package net.tfminecraft.infestations.spawn; + +import org.bukkit.Location; +import org.bukkit.entity.LivingEntity; + +import io.lumine.mythic.api.mobs.MythicMob; +import io.lumine.mythic.bukkit.BukkitAdapter; +import io.lumine.mythic.bukkit.MythicBukkit; +import io.lumine.mythic.core.mobs.ActiveMob; +import net.tfminecraft.infestations.Infestations; +import net.tfminecraft.infestations.loader.GroupLoader; + +public final class MythicSpawner { + + private MythicSpawner() {} + + public static LivingEntity spawn(Location location, String groupId, int provinceId, String kind) { + String mobId = GroupLoader.pickMobId(groupId); + if (mobId == null || location == null) { + return null; + } + MythicMob mm = MythicBukkit.inst().getMobManager().getMythicMob(mobId).orElse(null); + if (mm == null) { + Infestations.plugin.getLogger().warning("[Infestations] Unknown Mythic mob: " + mobId); + return null; + } + ActiveMob am = mm.spawn(BukkitAdapter.adapt(location), 1); + if (am == null || am.getEntity() == null) { + return null; + } + if (!(am.getEntity().getBukkitEntity() instanceof LivingEntity living)) { + return null; + } + Keys.tagMob(living.getPersistentDataContainer(), provinceId, kind); + return living; + } +} diff --git a/src/main/java/net/tfminecraft/infestations/spawn/SpawnLog.java b/src/main/java/net/tfminecraft/infestations/spawn/SpawnLog.java index fd5b42a..ac12e40 100644 --- a/src/main/java/net/tfminecraft/infestations/spawn/SpawnLog.java +++ b/src/main/java/net/tfminecraft/infestations/spawn/SpawnLog.java @@ -1,102 +1,102 @@ -package net.tfminecraft.infestations.spawn; - -import java.io.BufferedWriter; -import java.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.time.Instant; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; - -import org.bukkit.Location; - -import net.tfminecraft.infestations.infestation.Infestation; - -/** - * Immediate lines in {@code logs/spawn.log}. Same logging / wipe-log as SimpleFactions. - */ -public final class SpawnLog { - - private static final DateTimeFormatter TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") - .withZone(ZoneId.systemDefault()); - - private static volatile boolean enabled; - private static volatile Path logFile; - private static final Object LOCK = new Object(); - - private SpawnLog() {} - - public static void configure(boolean loggingEnabled, boolean wipeLog, File dataFolder) { - enabled = loggingEnabled; - if (dataFolder != null) { - Path dir = dataFolder.toPath().resolve("logs"); - logFile = dir.resolve("spawn.log"); - } else { - logFile = null; - } - if (wipeLog) { - wipe(); - } - } - - private static void wipe() { - if (logFile == null) { - return; - } - synchronized (LOCK) { - try { - Files.deleteIfExists(logFile); - } catch (IOException ignored) { - } - } - } - - public static void line(Infestation infestation, String player, String reason) { - if (!enabled || infestation == null) { - return; - } - String who = player != null ? player : "-"; - append("p=" + infestation.getProvinceId() - + " " + infestation.getSeverity().id() - + " " + infestation.getGroupId() - + " " + who - + " " + reason); - } - - public static void spawned(Infestation infestation, String player, Location loc, int alive, int cap) { - if (loc == null) { - line(infestation, player, "spawned alive=" + alive + "/" + cap); - return; - } - line(infestation, player, "spawned " - + loc.getBlockX() + "," + loc.getBlockY() + "," + loc.getBlockZ() - + " alive=" + alive + "/" + cap); - } - - private static void append(String message) { - if (!enabled || message == null || logFile == null) { - return; - } - Path file = logFile; - synchronized (LOCK) { - try { - Path parent = file.getParent(); - if (parent != null) { - Files.createDirectories(parent); - } - try (BufferedWriter writer = Files.newBufferedWriter( - file, - StandardCharsets.UTF_8, - StandardOpenOption.CREATE, - StandardOpenOption.APPEND)) { - writer.write(TIME.format(Instant.now()) + " " + message); - writer.newLine(); - } - } catch (IOException ignored) { - } - } - } -} +package net.tfminecraft.infestations.spawn; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +import org.bukkit.Location; + +import net.tfminecraft.infestations.infestation.Infestation; + +/** + * Immediate lines in {@code logs/spawn.log}. Same logging / wipe-log as SimpleFactions. + */ +public final class SpawnLog { + + private static final DateTimeFormatter TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + .withZone(ZoneId.systemDefault()); + + private static volatile boolean enabled; + private static volatile Path logFile; + private static final Object LOCK = new Object(); + + private SpawnLog() {} + + public static void configure(boolean loggingEnabled, boolean wipeLog, File dataFolder) { + enabled = loggingEnabled; + if (dataFolder != null) { + Path dir = dataFolder.toPath().resolve("logs"); + logFile = dir.resolve("spawn.log"); + } else { + logFile = null; + } + if (wipeLog) { + wipe(); + } + } + + private static void wipe() { + if (logFile == null) { + return; + } + synchronized (LOCK) { + try { + Files.deleteIfExists(logFile); + } catch (IOException ignored) { + } + } + } + + public static void line(Infestation infestation, String player, String reason) { + if (!enabled || infestation == null) { + return; + } + String who = player != null ? player : "-"; + append("p=" + infestation.getProvinceId() + + " " + infestation.getSeverity().id() + + " " + infestation.getGroupId() + + " " + who + + " " + reason); + } + + public static void spawned(Infestation infestation, String player, Location loc, int alive, int cap) { + if (loc == null) { + line(infestation, player, "spawned alive=" + alive + "/" + cap); + return; + } + line(infestation, player, "spawned " + + loc.getBlockX() + "," + loc.getBlockY() + "," + loc.getBlockZ() + + " alive=" + alive + "/" + cap); + } + + private static void append(String message) { + if (!enabled || message == null || logFile == null) { + return; + } + Path file = logFile; + synchronized (LOCK) { + try { + Path parent = file.getParent(); + if (parent != null) { + Files.createDirectories(parent); + } + try (BufferedWriter writer = Files.newBufferedWriter( + file, + StandardCharsets.UTF_8, + StandardOpenOption.CREATE, + StandardOpenOption.APPEND)) { + writer.write(TIME.format(Instant.now()) + " " + message); + writer.newLine(); + } + } catch (IOException ignored) { + } + } + } +} diff --git a/src/main/java/net/tfminecraft/infestations/spawn/SpawnPlanner.java b/src/main/java/net/tfminecraft/infestations/spawn/SpawnPlanner.java index 5f26944..4e9218f 100644 --- a/src/main/java/net/tfminecraft/infestations/spawn/SpawnPlanner.java +++ b/src/main/java/net/tfminecraft/infestations/spawn/SpawnPlanner.java @@ -1,94 +1,94 @@ -package net.tfminecraft.infestations.spawn; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.ThreadLocalRandom; -import java.util.function.Predicate; - -import org.bukkit.Location; -import org.bukkit.World; -import org.bukkit.block.Block; - -/** - * TrialRooms SpawnPlanner: shuffled disk, 3x3x3 air, 3x3 solid floor. - */ -public final class SpawnPlanner { - - private SpawnPlanner() {} - - public static List find(Location origin, int ringMin, int ringMax, int max, - Predicate extra) { - List results = new ArrayList<>(max); - if (origin == null) { - return results; - } - World world = origin.getWorld(); - if (world == null) { - return results; - } - - int min = Math.max(0, Math.min(ringMin, ringMax)); - int maxR = Math.max(min, Math.max(ringMin, ringMax)); - int minSq = min * min; - int maxSq = maxR * maxR; - int baseY = origin.getBlockY(); - - List candidates = new ArrayList<>(); - for (int dx = -maxR; dx <= maxR; dx++) { - for (int dz = -maxR; dz <= maxR; dz++) { - int d2 = dx * dx + dz * dz; - if (d2 >= minSq && d2 <= maxSq) { - candidates.add(new int[] { dx, dz }); - } - } - } - Collections.shuffle(candidates, ThreadLocalRandom.current()); - - for (int[] off : candidates) { - int x = origin.getBlockX() + off[0]; - int z = origin.getBlockZ() + off[1]; - for (int y = baseY - 2; y <= baseY + 2; y++) { - if (!is3x3x3ClearAir(world, x, y, z) || !is3x3FloorSolid(world, x, y - 1, z)) { - continue; - } - Location loc = new Location(world, x + 0.5, y, z + 0.5); - if (extra != null && !extra.test(loc)) { - continue; - } - results.add(loc); - break; - } - if (results.size() >= max) { - break; - } - } - return results; - } - - private static boolean is3x3x3ClearAir(World world, int x, int y, int z) { - for (int oy = 0; oy < 3; oy++) { - for (int ox = -1; ox <= 1; ox++) { - for (int oz = -1; oz <= 1; oz++) { - Block b = world.getBlockAt(x + ox, y + oy, z + oz); - if (!b.getType().isAir()) { - return false; - } - } - } - } - return true; - } - - private static boolean is3x3FloorSolid(World world, int x, int yFloor, int z) { - for (int ox = -1; ox <= 1; ox++) { - for (int oz = -1; oz <= 1; oz++) { - Block b = world.getBlockAt(x + ox, yFloor, z + oz); - if (!b.getType().isSolid()) { - return false; - } - } - } - return true; - } -} +package net.tfminecraft.infestations.spawn; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Predicate; + +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; + +/** + * TrialRooms SpawnPlanner: shuffled disk, 3x3x3 air, 3x3 solid floor. + */ +public final class SpawnPlanner { + + private SpawnPlanner() {} + + public static List find(Location origin, int ringMin, int ringMax, int max, + Predicate extra) { + List results = new ArrayList<>(max); + if (origin == null) { + return results; + } + World world = origin.getWorld(); + if (world == null) { + return results; + } + + int min = Math.max(0, Math.min(ringMin, ringMax)); + int maxR = Math.max(min, Math.max(ringMin, ringMax)); + int minSq = min * min; + int maxSq = maxR * maxR; + int baseY = origin.getBlockY(); + + List candidates = new ArrayList<>(); + for (int dx = -maxR; dx <= maxR; dx++) { + for (int dz = -maxR; dz <= maxR; dz++) { + int d2 = dx * dx + dz * dz; + if (d2 >= minSq && d2 <= maxSq) { + candidates.add(new int[] { dx, dz }); + } + } + } + Collections.shuffle(candidates, ThreadLocalRandom.current()); + + for (int[] off : candidates) { + int x = origin.getBlockX() + off[0]; + int z = origin.getBlockZ() + off[1]; + for (int y = baseY - 2; y <= baseY + 2; y++) { + if (!is3x3x3ClearAir(world, x, y, z) || !is3x3FloorSolid(world, x, y - 1, z)) { + continue; + } + Location loc = new Location(world, x + 0.5, y, z + 0.5); + if (extra != null && !extra.test(loc)) { + continue; + } + results.add(loc); + break; + } + if (results.size() >= max) { + break; + } + } + return results; + } + + private static boolean is3x3x3ClearAir(World world, int x, int y, int z) { + for (int oy = 0; oy < 3; oy++) { + for (int ox = -1; ox <= 1; ox++) { + for (int oz = -1; oz <= 1; oz++) { + Block b = world.getBlockAt(x + ox, y + oy, z + oz); + if (!b.getType().isAir()) { + return false; + } + } + } + } + return true; + } + + private static boolean is3x3FloorSolid(World world, int x, int yFloor, int z) { + for (int ox = -1; ox <= 1; ox++) { + for (int oz = -1; oz <= 1; oz++) { + Block b = world.getBlockAt(x + ox, yFloor, z + oz); + if (!b.getType().isSolid()) { + return false; + } + } + } + return true; + } +} diff --git a/src/main/java/net/tfminecraft/infestations/utils/Provinces.java b/src/main/java/net/tfminecraft/infestations/utils/Provinces.java index b129b18..8ec74f6 100644 --- a/src/main/java/net/tfminecraft/infestations/utils/Provinces.java +++ b/src/main/java/net/tfminecraft/infestations/utils/Provinces.java @@ -1,93 +1,93 @@ -package net.tfminecraft.infestations.utils; - -import java.util.Collections; -import java.util.Set; - -import org.bukkit.Location; -import org.bukkit.entity.Player; - -import me.Plugins.SimpleFactions.SimpleFactions; -import me.Plugins.SimpleFactions.Map.ProvinceGrid; -import me.Plugins.SimpleFactions.Map.Provinces.Province; -import me.Plugins.SimpleFactions.REST.RestServer; -import me.Plugins.SimpleFactions.enums.Terrain; -import net.tfminecraft.infestations.cache.Cache; - -public final class Provinces { - - public static final int UNKNOWN = -2; - - private Provinces() {} - - public static int at(Player player) { - if (player == null) { - return UNKNOWN; - } - return RestServer.getProvince(player); - } - - public static int at(Location location) { - if (location == null) { - return UNKNOWN; - } - SimpleFactions sf = SimpleFactions.getInstance(); - if (sf == null) { - return UNKNOWN; - } - ProvinceGrid grid = sf.getProvinceGrid(); - if (grid == null) { - return UNKNOWN; - } - return grid.getAt(location.getBlockX(), location.getBlockZ()); - } - - public static Terrain terrain(int provinceId) { - if (provinceId <= 0) { - return null; - } - SimpleFactions sf = SimpleFactions.getInstance(); - if (sf == null || sf.getProvinceManager() == null) { - return null; - } - Province province = sf.getProvinceManager().get(provinceId); - if (province == null) { - return null; - } - return province.getTerrain(); - } - - public static boolean skipTerrain(int provinceId) { - Terrain terrain = terrain(provinceId); - if (terrain == null) { - return true; - } - return Cache.skipTerrains.contains(terrain.name().toLowerCase()); - } - - public static boolean isWater(int provinceId) { - return terrain(provinceId) == Terrain.WATER; - } - - public static boolean isSea(int provinceId) { - return terrain(provinceId) == Terrain.SEA; - } - - public static boolean validLand(int provinceId) { - return provinceId > 0 && !skipTerrain(provinceId); - } - - public static Set neighbours(int provinceId) { - if (provinceId <= 0) { - return Collections.emptySet(); - } - SimpleFactions sf = SimpleFactions.getInstance(); - if (sf == null || sf.getProvinceManager() == null) { - return Collections.emptySet(); - } - Province province = sf.getProvinceManager().get(provinceId); - if (province == null) { - return Collections.emptySet(); - } - return province.getNeighbours(); - } -} +package net.tfminecraft.infestations.utils; + +import java.util.Collections; +import java.util.Set; + +import org.bukkit.Location; +import org.bukkit.entity.Player; + +import net.tfminecraft.simplefactions.SimpleFactions; +import net.tfminecraft.simplefactions.map.ProvinceGrid; +import net.tfminecraft.simplefactions.map.provinces.Province; +import net.tfminecraft.simplefactions.rest.RestServer; +import net.tfminecraft.simplefactions.enums.Terrain; +import net.tfminecraft.infestations.cache.Cache; + +public final class Provinces { + + public static final int UNKNOWN = -2; + + private Provinces() {} + + public static int at(Player player) { + if (player == null) { + return UNKNOWN; + } + return RestServer.getProvince(player); + } + + public static int at(Location location) { + if (location == null) { + return UNKNOWN; + } + SimpleFactions sf = SimpleFactions.getInstance(); + if (sf == null) { + return UNKNOWN; + } + ProvinceGrid grid = sf.getProvinceGrid(); + if (grid == null) { + return UNKNOWN; + } + return grid.getAt(location.getBlockX(), location.getBlockZ()); + } + + public static Terrain terrain(int provinceId) { + if (provinceId <= 0) { + return null; + } + SimpleFactions sf = SimpleFactions.getInstance(); + if (sf == null || sf.getProvinceManager() == null) { + return null; + } + Province province = sf.getProvinceManager().get(provinceId); + if (province == null) { + return null; + } + return province.getTerrain(); + } + + public static boolean skipTerrain(int provinceId) { + Terrain terrain = terrain(provinceId); + if (terrain == null) { + return true; + } + return Cache.skipTerrains.contains(terrain.name().toLowerCase()); + } + + public static boolean isWater(int provinceId) { + return terrain(provinceId) == Terrain.WATER; + } + + public static boolean isSea(int provinceId) { + return terrain(provinceId) == Terrain.SEA; + } + + public static boolean validLand(int provinceId) { + return provinceId > 0 && !skipTerrain(provinceId); + } + + public static Set neighbours(int provinceId) { + if (provinceId <= 0) { + return Collections.emptySet(); + } + SimpleFactions sf = SimpleFactions.getInstance(); + if (sf == null || sf.getProvinceManager() == null) { + return Collections.emptySet(); + } + Province province = sf.getProvinceManager().get(provinceId); + if (province == null) { + return Collections.emptySet(); + } + return province.getNeighbours(); + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 47aea39..392ea9e 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,35 +1,35 @@ -debug: false -logging: false -wipe-log: true - -# Neighbor spread. Keep false on the test server so set infestations stay put. -# Can hop across a single water or sea tile, never onto water/sea, never two tiles. -spread: false -spread-interval-seconds: 600 -worsen-chance: 0.0015 -spread-chance-land: - mild: 0.00099 - worrying: 0.0030 - severe: 0.0050 - extreme: 0.00694 -spread-chance-water: - mild: 0.00040 - worrying: 0.0012 - severe: 0.0020 - extreme: 0.00278 -spread-chance-sea: - mild: 0.00015 - worrying: 0.00045 - severe: 0.00075 - extreme: 0.00104 - -lure-item: ia.tfmc:lure -join-seconds: 20 -lure-spawn-radius: 48 -logout-grace-seconds: 300 -deserter-damage: 2 -hologram-view-range: 96 - -skip-terrains: - - water - - sea +debug: false +logging: false +wipe-log: true + +# Neighbor spread. Keep false on the test server so set infestations stay put. +# Can hop across a single water or sea tile, never onto water/sea, never two tiles. +spread: false +spread-interval-seconds: 600 +worsen-chance: 0.0015 +spread-chance-land: + mild: 0.00099 + worrying: 0.0030 + severe: 0.0050 + extreme: 0.00694 +spread-chance-water: + mild: 0.00040 + worrying: 0.0012 + severe: 0.0020 + extreme: 0.00278 +spread-chance-sea: + mild: 0.00015 + worrying: 0.00045 + severe: 0.00075 + extreme: 0.00104 + +lure-item: ia.tfmc:lure +join-seconds: 20 +lure-spawn-radius: 48 +logout-grace-seconds: 300 +deserter-damage: 2 +hologram-view-range: 96 + +skip-terrains: + - water + - sea diff --git a/src/main/resources/groups.yml b/src/main/resources/groups.yml index 25df0bb..74a620e 100644 --- a/src/main/resources/groups.yml +++ b/src/main/resources/groups.yml @@ -1,44 +1,44 @@ -# Per-group Mythic list and severity numbers. display is player-facing (chat, map hover). -# night-only: skip ambient and lure spawns unless world time is 13000-23000. -# min-y: optional. Skip spawn spots with feet block Y below this (omitted = no floor). -# terrains: optional list of SimpleFactions terrain names. Empty / omitted = any valid land. -# If set, admin set and neighbor spread only land those types (still cannot infest skip-terrains). -groups: - swamp_mobs: - display: Swamp Monsters - night-only: true - min-y: 56 - terrains: - - bog - mobs: - - id: SwampGhoul - weight: 1 - severity: - mild: - ambient-cap: 8 - ambient-interval-ticks: 40 - ambient-ring-min: 10 - ambient-ring-max: 22 - lure-count: 20 - lure-duration-seconds: 120 - worrying: - ambient-cap: 16 - ambient-interval-ticks: 40 - ambient-ring-min: 10 - ambient-ring-max: 20 - lure-count: 40 - lure-duration-seconds: 120 - severe: - ambient-cap: 28 - ambient-interval-ticks: 30 - ambient-ring-min: 9 - ambient-ring-max: 19 - lure-count: 60 - lure-duration-seconds: 120 - extreme: - ambient-cap: 40 - ambient-interval-ticks: 20 - ambient-ring-min: 8 - ambient-ring-max: 18 - lure-count: 80 - lure-duration-seconds: 120 +# Per-group Mythic list and severity numbers. display is player-facing (chat, map hover). +# night-only: skip ambient and lure spawns unless world time is 13000-23000. +# min-y: optional. Skip spawn spots with feet block Y below this (omitted = no floor). +# terrains: optional list of SimpleFactions terrain names. Empty / omitted = any valid land. +# If set, admin set and neighbor spread only land those types (still cannot infest skip-terrains). +groups: + swamp_mobs: + display: Swamp Monsters + night-only: true + min-y: 56 + terrains: + - bog + mobs: + - id: SwampGhoul + weight: 1 + severity: + mild: + ambient-cap: 8 + ambient-interval-ticks: 40 + ambient-ring-min: 10 + ambient-ring-max: 22 + lure-count: 20 + lure-duration-seconds: 120 + worrying: + ambient-cap: 16 + ambient-interval-ticks: 40 + ambient-ring-min: 10 + ambient-ring-max: 20 + lure-count: 40 + lure-duration-seconds: 120 + severe: + ambient-cap: 28 + ambient-interval-ticks: 30 + ambient-ring-min: 9 + ambient-ring-max: 19 + lure-count: 60 + lure-duration-seconds: 120 + extreme: + ambient-cap: 40 + ambient-interval-ticks: 20 + ambient-ring-min: 8 + ambient-ring-max: 18 + lure-count: 80 + lure-duration-seconds: 120 diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml index 3130120..6f5cf37 100644 --- a/src/main/resources/messages.yml +++ b/src/main/resources/messages.yml @@ -1,33 +1,33 @@ -prefix: "#8a6a3c[Infestations] " -no-permission: "{prefix}#c45c5cYou cannot do that." -reload-ok: "{prefix}#7dba7aConfigs reloaded." -reload-fail: "{prefix}#c45c5cReload failed. Check console." -usage: "{prefix}#d4c9ae/infestation " -unknown-subcommand: "{prefix}#c45c5cUnknown subcommand." -player-only: "{prefix}#c45c5cPlayers only for that argument." -unknown-group: "{prefix}#c45c5cUnknown group: #d4c9ae{group}" -unknown-severity: "{prefix}#c45c5cUnknown severity: #d4c9ae{severity}" -unknown-province: "{prefix}#c45c5cNot a valid land province." -skip-terrain: "{prefix}#c45c5cThat province cannot be infested." -wrong-terrain: "{prefix}#c45c5c{group} can only infest: #d4c9ae{terrains}" -already-infested: "{prefix}#c45c5cProvince {id} already has an infestation." -not-infested: "{prefix}#c45c5cProvince {id} has no infestation." -set-ok: "{prefix}#7dba7aInfestation set on province {id}: #d4c9ae{group} {severity}" -clear-ok: "{prefix}#7dba7aCleared infestation on province {id}." -list-empty: "{prefix}#d4c9aeNo infestations." -list-line: "#d4c9ae{id} - {group} {severity}{lure}" -list-lure-suffix: " #c45c5c[lure]" -no-infestation-here: "{prefix}#c45c5cThere is no infestation here. The lure does nothing." -lure-already: "{prefix}#c45c5cA lure is already placed in this province." -lure-placed: "{prefix}#c9a44aA lure is activating. Leave the province or right-click the lure to join." -lure-join: "{prefix}#7dba7aYou joined the lure. Leave the province and the lure fails." -lure-already-joined: "{prefix}#d4c9aeYou are already in the lure." -lure-highlight: "{prefix}#d4c9aeHighlighted {count} remaining lure mobs for 10s." -lure-activating-bar: "#c9a44aLure activating: {seconds}s - leave or right-click to join" -lure-leave-or-join: "{prefix}#c9a44aA lure is activating. Leave or right-click the lure to commit." -lure-nonjoiner: "{prefix}#c45c5cLeave this province. The lure has started without you." -lure-nonjoiner-bar: "#c45c5cLeave this province. The lure has started without you." -lure-victory: "{prefix}#7dba7aThe infestation is cleared." -lure-fail: "{prefix}#c45c5cThe lure failed. The infestation remains." -hologram-joining: "#c9a44aLure - join {seconds}s" -hologram-remaining: "#c45c5cRemaining: #f0e6d0{remaining}" +prefix: "#8a6a3c[Infestations] " +no-permission: "{prefix}#c45c5cYou cannot do that." +reload-ok: "{prefix}#7dba7aConfigs reloaded." +reload-fail: "{prefix}#c45c5cReload failed. Check console." +usage: "{prefix}#d4c9ae/infestation " +unknown-subcommand: "{prefix}#c45c5cUnknown subcommand." +player-only: "{prefix}#c45c5cPlayers only for that argument." +unknown-group: "{prefix}#c45c5cUnknown group: #d4c9ae{group}" +unknown-severity: "{prefix}#c45c5cUnknown severity: #d4c9ae{severity}" +unknown-province: "{prefix}#c45c5cNot a valid land province." +skip-terrain: "{prefix}#c45c5cThat province cannot be infested." +wrong-terrain: "{prefix}#c45c5c{group} can only infest: #d4c9ae{terrains}" +already-infested: "{prefix}#c45c5cProvince {id} already has an infestation." +not-infested: "{prefix}#c45c5cProvince {id} has no infestation." +set-ok: "{prefix}#7dba7aInfestation set on province {id}: #d4c9ae{group} {severity}" +clear-ok: "{prefix}#7dba7aCleared infestation on province {id}." +list-empty: "{prefix}#d4c9aeNo infestations." +list-line: "#d4c9ae{id} - {group} {severity}{lure}" +list-lure-suffix: " #c45c5c[lure]" +no-infestation-here: "{prefix}#c45c5cThere is no infestation here. The lure does nothing." +lure-already: "{prefix}#c45c5cA lure is already placed in this province." +lure-placed: "{prefix}#c9a44aA lure is activating. Leave the province or right-click the lure to join." +lure-join: "{prefix}#7dba7aYou joined the lure. Leave the province and the lure fails." +lure-already-joined: "{prefix}#d4c9aeYou are already in the lure." +lure-highlight: "{prefix}#d4c9aeHighlighted {count} remaining lure mobs for 10s." +lure-activating-bar: "#c9a44aLure activating: {seconds}s - leave or right-click to join" +lure-leave-or-join: "{prefix}#c9a44aA lure is activating. Leave or right-click the lure to commit." +lure-nonjoiner: "{prefix}#c45c5cLeave this province. The lure has started without you." +lure-nonjoiner-bar: "#c45c5cLeave this province. The lure has started without you." +lure-victory: "{prefix}#7dba7aThe infestation is cleared." +lure-fail: "{prefix}#c45c5cThe lure failed. The infestation remains." +hologram-joining: "#c9a44aLure - join {seconds}s" +hologram-remaining: "#c45c5cRemaining: #f0e6d0{remaining}" diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index a650cc0..cf3c295 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,23 +1,23 @@ -main: net.tfminecraft.infestations.Infestations -api-version: '1.21' -name: Infestations -version: 0.1.0 -author: Drefvelin -depend: [TLibs, SimpleFactions, MythicMobs, ItemsAdder, InteractibleFurniture] - -commands: - infestation: - description: Infestation admin commands - usage: /infestation - aliases: [infestations] - permission: infestations.admin - -permissions: - infestations.admin: - description: Allows /infestation admin commands - default: op - children: - infestations.admin.reload: true - infestations.admin.reload: - description: Reload Infestations configs - default: op +main: net.tfminecraft.infestations.Infestations +api-version: '1.21.10' +name: Infestations +version: '${project.version}' +author: Drefvelin +depend: [TLibs, SimpleFactions, MythicMobs, ItemsAdder, InteractibleFurniture] + +commands: + infestation: + description: Infestation admin commands + usage: /infestation + aliases: [infestations] + permission: infestations.admin + +permissions: + infestations.admin: + description: Allows /infestation admin commands + default: op + children: + infestations.admin.reload: true + infestations.admin.reload: + description: Reload Infestations configs + default: op