From 8c85b8bd50406fa1d5ecfff4b3c5c8746df3632c Mon Sep 17 00:00:00 2001 From: Ryan Barlow <7389646+ryanbarlow97@users.noreply.github.com> Date: Wed, 23 Sep 2026 11:19:56 +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 | 2 + .github/scripts/install-local-dependencies.sh | 12 + .github/scripts/plugin-artifact.py | 70 +++ .github/scripts/prepare-release.sh | 8 + .github/workflows/build.yml | 77 +++ .github/workflows/maven-release.yml | 138 +++++ .github/workflows/release.yml | 18 + .gitignore | 65 ++- LICENSE | 201 +++++++ README.md | 41 ++ docs/ARCHITECTURE.md | 88 --- docs/IMPLEMENTATION_BATCHES.md | 512 ------------------ docs/MIGRATION_FROM_ADVANCEDRESEARCH.md | 82 --- docs/RESEARCH_REDESIGN.md | 360 ------------ docs/SYSTEM.md | 269 --------- pom.xml | 122 +++-- .../net/tfminecraft/research/GuiCache.java | 114 ++-- .../net/tfminecraft/research/Messages.java | 2 +- .../research/database/StationSaveData.java | 4 +- .../research/event/ResearchCompleteEvent.java | 2 +- .../research/loader/AspectLoader.java | 2 +- .../research/loader/ConfigLoader.java | 2 +- .../research/loader/GuiLoader.java | 188 +++---- .../research/loader/InputLoader.java | 328 +++++------ .../research/loader/MessagesLoader.java | 2 +- .../research/loader/OutputLoader.java | 314 +++++------ .../research/loader/TemplateLoader.java | 2 +- .../research/model/ActiveProject.java | 2 +- .../research/model/AspectRequirement.java | 32 +- .../research/model/AspectSounds.java | 82 +-- .../research/model/ExperimentMatch.java | 94 ++-- .../tfminecraft/research/model/InputDef.java | 166 +++--- .../tfminecraft/research/model/OutputDef.java | 212 ++++---- .../research/registry/AspectItemRegistry.java | 206 +++---- .../research/util/AspectSlotOrder.java | 202 +++---- .../tfminecraft/research/util/GuiText.java | 160 +++--- .../research/util/InputMatcher.java | 52 +- .../tfminecraft/research/util/ItemRef.java | 2 +- .../research/util/OutputPicker.java | 82 +-- .../research/util/PlayerInventoryUtil.java | 108 ++-- .../research/util/ResultResolver.java | 140 ++--- .../tfminecraft/research/util/SoundKeys.java | 104 ++-- .../research/util/StationCompleteEffects.java | 140 ++--- .../research/util/StationStartEffects.java | 92 ++-- src/main/resources/gui.yml | 82 +-- src/main/resources/plugin.yml | 4 +- 47 files changed, 2142 insertions(+), 2853 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 LICENSE create mode 100644 README.md delete mode 100644 docs/ARCHITECTURE.md delete mode 100644 docs/IMPLEMENTATION_BATCHES.md delete mode 100644 docs/MIGRATION_FROM_ADVANCEDRESEARCH.md delete mode 100644 docs/RESEARCH_REDESIGN.md delete mode 100644 docs/SYSTEM.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..b988d80 --- /dev/null +++ b/.github/dependencies.sha256 @@ -0,0 +1,2 @@ +14850d7454374d7312305d3e84a391be720301e5964963f869b27ae97f6264eb libs/MMOCore-1.13.1.jar +660ff2a6ec86bc8d7779948cf65c1637e271a16e1ef812d6a273f4a5c5eec73c libs/MythicLib-1.7.jar diff --git a/.github/scripts/install-local-dependencies.sh b/.github/scripts/install-local-dependencies.sh new file mode 100644 index 0000000..06358db --- /dev/null +++ b/.github/scripts/install-local-dependencies.sh @@ -0,0 +1,12 @@ +#!/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/MMOCore-1.13.1.jar" -DgroupId="local" -DartifactId="MMOCore" \ + -Dversion="1.13.1-tfmc-14850d745437" -Dpackaging=jar -DgeneratePom=true "$@" +mvn -B --no-transfer-progress org.apache.maven.plugins:maven-install-plugin:3.1.4:install-file \ + -Dfile="libs/MythicLib-1.7.jar" -DgroupId="local" -DartifactId="MythicLib" \ + -Dversion="1.7-tfmc-660ff2a6ec86" -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..c06d651 --- /dev/null +++ b/.github/scripts/prepare-release.sh @@ -0,0 +1,8 @@ +#!/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/14850d745437/MMOCore-1.13.1.jar?ref=$ref" > "libs/MMOCore-1.13.1.jar" +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/660ff2a6ec86/MythicLib-1.7.jar?ref=$ref" > "libs/MythicLib-1.7.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 index 7a9655e..fee0cd8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,33 +1,54 @@ -# Ignore Maven target directory -target - -# Ignore Eclipse/IntelliJ IDEA/NetBeans project files -*.iml -.idea/ -*.ipr -*.iws -*.bak -*.swp -*.tmp -*.log -*.class - -# NetBeans specific -nbproject/private/ +# Build output +target/ build/ -nbbuild/ +out/ dist/ +nbbuild/ nbdist/ -.nb-gradle/ +*.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 -# Eclipse specific +# Python caches and environments +__pycache__/ +*.py[cod] +.venv/ +venv/ + +# IDE files +.idea/ +*.iml +*.ipr +*.iws +.vscode/ .project .classpath .settings/ +nbproject/private/ +.nb-gradle/ -# OS-specific files +# Logs and temporary files +*.log +hs_err_pid* +replay_pid* +*.bak +*.swp +*.tmp +*~ + +# Operating system files .DS_Store Thumbs.db +Desktop.ini -# Logs -*.log +# 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..be1dce5 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# Research + +> Run experiments at research stations to uncover hidden recipes and discoveries for TF-Minecraft characters. + +Research turns lecterns into research stations. A player starts a project with a +configured research item, then tests real items as experiments to work out which hidden +aspects the project needs. Each experiment costs mental points, and a finished +project produces its result item at the station. + +## Features + +- **Configurable projects** — each configured start item rolls one of its weighted + outputs, and the output's result is fixed when the project begins. +- **Experiment-driven discovery** — items carry primary and secondary aspects that + add points to the project, while unneeded aspects are eventually rejected. +- **Hidden recipes** — required aspects sit among decoy rows in a shuffled station + menu, and both their identity and the product are revealed as research progresses. +- **Character focus** — experiments spend TFMCCore mental points, and a configurable + MMOCore attribute lowers the reveal and confirmation thresholds. +- **Weighted result pools** — outputs grant a fixed item or roll from nested, + weighted result templates using TLibs item paths. +- **Plugin integration** — completed projects fire a `ResearchCompleteEvent` so + other plugins can react to discoveries. +- **Persistent stations** — ownership, aspect progress, tested items, and the rolled + result survive server restarts. + +Originally created by [Drefvelin](https://github.com/Drefvelin). + +## Documentation + +[Project documentation](https://github.com/TF-Minecraft/Docs/blob/main/projects/Research/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/ARCHITECTURE.md b/docs/ARCHITECTURE.md deleted file mode 100644 index fa76958..0000000 --- a/docs/ARCHITECTURE.md +++ /dev/null @@ -1,88 +0,0 @@ -# Research Plugin - Architecture - -Compact layout aligned with Gathering, Dowsing, and AdvancedCrafting patterns. Gameplay logic lives in managers, not micro-services. - -> **Redesign (batches 14-18):** See [`RESEARCH_REDESIGN.md`](RESEARCH_REDESIGN.md). Planner removed; `gui.yml` added; `outputs/` + `inputs/` replace `projects/`; aspect item lists replace `experiment-items/`. - -## Package map - -```text -net.tfminecraft.research/ - Research.java # enable, loadConfigs, reloadAll, folders - Cache.java # scalars from config.yml - Messages.java # player strings from messages.yml - ResourceBootstrap.java # first-run resource copy - - loader/ # LoaderInterface + static registries - registry/ # AspectItemRegistry (batch 17+) - model/ # yaml definition objects - manager/ # PlayerManager (Focus facade); ResearchManager, InventoryManager - database/ # station persistence (batch 4+) - command/ # CommandManager - util/ # ItemRef, YamlFolder, GridLayout, AspectSlotOrder, OutputPicker, ResultResolver, InputMatcher, ResultSpawnEffects, ExternalModifiers - event/ # ResearchCompleteEvent (Codex hook) -``` - -## Data flow - -1. **Config load** (`Research.loadConfigs`) runs loaders in order: - - `ConfigLoader` -> `Cache` - - `MessagesLoader` -> `Messages` - - `GuiLoader` -> GUI item refs + colour tokens (batch 14+) - - `AspectLoader` -> aspect registry (+ item index batch 17+) - - `TemplateLoader` -> result template registry (validates nested `t.*` refs) - - `OutputLoader` -> output registry (batch 18+; replaces `ProjectLoader`) - - `InputLoader` -> input registry (batch 18+) - - **Removed batch 14:** `PlannerLoader` - **Removed batch 17:** `ExperimentItemLoader` (replaced by `AspectItemRegistry`) - **Removed batch 18:** `ProjectLoader`, `ProjectPicker`, `ResultPicker` - -2. **Runtime**: - - `PlayerManager` - reads/spends character Focus via TFMCCore (no local MP store) - - `MmoAttributes` - read configured MMOCore stat ids from `Cache` - - `ResearchManager` - lectern, experiments, confirm/reject, reveal, complete (batch 4+) - - `InventoryManager` - GUI render only (batch 4+) - - `StationStore` - per-station JSON in `data/stations/` (owner, rolled output, aspect state, tested items) - -## Registries vs Cache - -| Source | Storage | Access | -|--------|---------|--------| -| `config.yml` scalars | `Cache` static fields | `Cache.mentalPointsMax`, etc. | -| `gui.yml` | `GuiLoader` / `GuiCache` | GUI item refs, hex colour tokens | -| `aspects/`, `outputs/`, `inputs/` | Loader static maps | `AspectLoader.getById(id)` | -| Player-facing text | `Messages` | `Messages.get("reload.success")` | - -## Item references - -Yaml: `vanilla.MATERIAL`, `m.TYPE.ID`, `ia.namespace:id` - -[`ItemRef`](src/main/java/net/tfminecraft/research/util/ItemRef.java) normalizes `vanilla.` to TLibs `v.` for builds. Matching uses Material, MMOItems NBT, or ItemsAdder `CustomStack`. - -## YAML folders - -Loaded via [`YamlFolder.listYamlFiles`](src/main/java/net/tfminecraft/research/util/YamlFolder.java) - same pattern as VehicleFramework `vehicles/` and AdvancedCrafting `recipes/`. - -## What not to add - -- Separate service class per verb (`ExperimentService`, etc.) -- One Java file per listener event -- Hardcoded attribute names or item ids (use `Cache` and yaml) -- Player-biased output selection (planner removed) - -## Batch status - -- Batch 1: scaffold -- Batch 2: loaders + `/research reload` -- Batch 3: Mental Points + player persistence (`PlayerManager`, `PlayerStore`, `MmoAttributes`) -- Batch 4: Station shell + placeholder GUI (`ResearchManager`, `InventoryManager`, `StationStore`) -- Batch 5: Experiment logic (item slot, MP spend, aspect point tracking) -- Batch 6: Confirm/reject thresholds, aspect states, completion detection -- Batch 7: Product and hidden aspect reveals (`RevealHelper`, `DiscoveryScaling`) -- Batch 8: Research Planner (**removed batch 14**) -- Batch 9: Ring-grid station GUI (superseded by batch 11 vertical layout; path lines removed) -- Batch 10: Results, Codex event stub, migration docs (`ResearchCompleteEvent`) -- Batch 12: Aspect row progress UI with discovery reveal and points-% confirm gate -- Batch 13: Result templates (`t.*`), world result spawn (`ResultSpawnEffects`, `TemplateLoader`) -- **Batch 14-18:** Redesign - see [`IMPLEMENTATION_BATCHES.md`](IMPLEMENTATION_BATCHES.md) and [`RESEARCH_REDESIGN.md`](RESEARCH_REDESIGN.md) diff --git a/docs/IMPLEMENTATION_BATCHES.md b/docs/IMPLEMENTATION_BATCHES.md deleted file mode 100644 index 9aa55e0..0000000 --- a/docs/IMPLEMENTATION_BATCHES.md +++ /dev/null @@ -1,512 +0,0 @@ -# Research Plugin - Implementation Batches - -Roadmap from batch 2 onward. Batch 1 (setup) is complete. - -**Redesign (batches 14-18):** Locked specification in [`RESEARCH_REDESIGN.md`](RESEARCH_REDESIGN.md). Includes `gui.yml`, planner removal, progress/wave fixes, aspect item lists, and output-centric `inputs/` + `outputs/`. - -## Batch 1 - Setup (complete) - -**Deliverables:** - -- Maven project `research/` with pom, deploy to `TFMC/Research/` -- Minimal `Research.java` (folders + default config copy) -- `ResourceBootstrap` helper -- Default yaml tree in `src/main/resources/` -- `docs/SYSTEM.md`, `docs/IMPLEMENTATION_BATCHES.md` - -**Staff can edit after:** yaml examples in resources (copied to server on first run). - -**Test checklist:** - -- [ ] `mvn package` succeeds -- [ ] JAR appears in `TFMC/Research/research-0.1.0.jar` -- [ ] Plugin loads on server with setup-only log message -- [ ] `plugins/Research/` contains all default folders and yaml files - ---- - -## Batch 2 - Config loaders and reload (complete) - -**Deliverables:** - -- `LoaderInterface` loaders: `AspectLoader`, `ExperimentItemLoader`, `ProjectLoader`, `PlannerLoader`, `ConfigLoader`, `MessagesLoader` -- `Cache`, `Messages`, `ItemRef`, `YamlFolder` -- Folder iteration for `aspects/`, `experiment-items/`, `projects/`, `planner/` (VehicleFramework pattern) -- Validation on load (unknown aspect refs, bad item refs, duplicate ids) -- `/research reload` command with `research.admin` permission -- `Research.reloadAll()` orchestrating all loaders -- `docs/ARCHITECTURE.md` - -**Staff can edit after:** all yaml; reload without restart. - -**Test checklist:** - -- [ ] `mvn package` succeeds (`research-0.2.0.jar`) -- [ ] `/research reload` reports success on valid yaml -- [ ] Invalid yaml logs clear errors (e.g. project references missing aspect) -- [ ] Duplicate project id across two files fails validation -- [ ] `ItemRef` matches vanilla/MMOItems/ItemsAdder stacks in a dev test - ---- - -## Batch 3 - Mental Points and player persistence (complete) - -**Deliverables:** - -- `ResearchPlayer` model + `PlayerSaveData` Gson DTO -- `PlayerStore` - `data/players/.json` -- `PlayerManager` - join/quit, online regen task, offline accrual on join -- `MmoAttributes` - configured `mmocore_id` reads from MMOCore -- Attribute-scaled regen from `attributes.regen_bonuses` in `config.yml` - -**Staff can edit after:** `mental_points` and `attributes.regen_bonuses` in `config.yml`. - -**Test checklist:** - -- [ ] `mvn package` -> `research-0.3.0.jar` -- [ ] MP regens while online at configured rate -- [ ] Offline player gains MP on join when `offline_regen: true` -- [ ] Higher wisdom/intelligence increases regen (MMOCore stat edits) -- [ ] Player data persists across relog -- [ ] `/research reload` restarts regen task without wiping loaded MP - ---- - -## Batch 4 - Station shell and project lifecycle (complete) - -**Deliverables:** - -- Lectern interaction via `ResearchManager` (`Cache.stationBlock`) -- One active project per lectern (owner stored on the station) -- Random project start stub from `ProjectLoader` pool -- Scrap with confirmation GUI -- `StationStore` persistence in `data/stations/` -- Placeholder chest GUI via `InventoryManager` - -**Staff can edit after:** `station` section in `config.yml`, `projects/` definitions. - -**Test checklist:** - -- [ ] Right-click lectern opens GUI with mystery paper + visible aspects -- [ ] Second player cannot use occupied station -- [ ] Scrap clears project and frees station -- [ ] Station state survives restart -- [ ] Player with active project at lectern A can start a new project at lectern B - ---- - -## Batch 5 - Experiment logic (complete) - -**Deliverables:** - -- Experiment item slot in placeholder GUI (slot 22) -- Primary/secondary aspect preview (slots 12, 14) -- Confirm experiment: spend MP (`experiment_cost`), item not consumed -- Per-aspect experiment points on `ActiveProject` -- Tested experiment items tracked and shown gray in preview - -**Staff can edit after:** `experiment-items/`, `mental_points.experiment_cost`. - -**Test checklist:** - -- [ ] Dynamite (if configured) adds destruction + fire test points -- [ ] MP deducted on confirm; item stays in inventory -- [ ] Unknown item rejected with message -- [ ] Same item cannot be re-tested (or shows as already tested) -- [ ] `aspect_points` and `tested_items` persist across restart - ---- - -## Batch 6 - Confirm, reject, and completion (complete) - -**Deliverables:** - -- Confirm/reject thresholds from `config.yml` + discovery attribute modifiers (`DiscoveryScaling`) -- Aspect state: UNKNOWN, TESTING, CONFIRMED, REJECTED -- Completion when all non-hidden required aspects reach CONFIRMED with enough points -- UI updates: gray rejected, green confirmed with points progress - -**Staff can edit after:** `experiment.base_confirm_tests`, `experiment.base_reject_tests`, discovery attribute scaling. - -**Test checklist:** - -- [ ] Wrong aspect eventually rejects after enough tests -- [ ] Correct aspect confirms and tracks point progress toward `required_points` -- [ ] Project completes when recipe satisfied -- [ ] High intelligence confirms/rejects faster -- [ ] State, test counts, and `completed` persist across restart - ---- - -## Batch 7 - Reveal system (complete) - -**Deliverables:** - -- Product reveal when `base_after_confirmed_aspects` threshold met (discovery attribute bonus) -- Hidden aspect slots appear per `reveal_when_confirmed_count` (discovery attribute bonus) -- Mystery paper -> product display item swap in GUI -- Messages from `messages.yml` (`product.revealed`) - -**Staff can edit after:** `product_reveal`, per-aspect `hidden` and `reveal_when_confirmed_count` in projects. - -**Test checklist:** - -- [ ] Product name/item revealed at correct progress point -- [ ] Hidden arcanum slot appears only after enough confirms -- [ ] High intelligence reveals earlier than low intelligence -- [ ] `product_revealed` survives restart; hidden visibility recomputed on load - ---- - -## Batch 8 - Research Planner (complete) - -**Deliverables:** - -- Pre-start planner GUI from `planner/categories.yml` -- MP cost for preference vs free random -- Bias odds from `base_bias_percent` to `max_bias_percent` scaled by discovery attribute -- Category -> aspect grouping in UI - -**Staff can edit after:** `planner/categories.yml`, `preference_cost`, bias percents. - -**Test checklist:** - -- [ ] `enabled: false` -> lectern starts random project immediately -- [ ] `enabled: true` -> planner GUI opens; free start picks unbiased project -- [ ] Paid aspect/category deducts MP and biases selection -- [ ] High intelligence yields higher bias % than low intelligence - ---- - -## Batch 9 - Full grid station UI (9x6) (superseded by batch 11) - -**Historical note:** Batch 9 shipped a ring-grid layout with path lines from center to confirmed aspects. Batch 11 replaced this with the vertical column layout; `GridPaths` and `gui.path_lines` were removed. - -**Note:** Bukkit chest inventories max out at 54 slots (9x6), not 63. The design uses columns 0-1 for controls and columns 2-8 as a 7-wide aspect grid with center paper at slot 23. - -**Deliverables:** - -- 54-slot layout: 2 control columns + 7x6 aspect/path grid -- Central mystery/product paper at slot 23 -- Aspect nodes on a ring around the center (`GridLayout.ASPECT_RING_SLOTS`) -- Path lines (ItemsAdder line textures) from center to confirmed aspects via `gui.path_lines` -- GUI item refs from `config.yml` `gui` section - -**Staff can edit after:** aspect `display` items, `grid_color`, `gui` button and path line refs. - -**Test checklist:** - -- [x] Grid shows control column + 7-wide area + center paper -- [x] Paths appear when aspects confirm (grid_color maps to path line item) -- [x] Rejected aspects gray out on grid ring -- [x] Experiment / confirm / scrap clicks use new slots (0, 1, 45) - ---- - -## Batch 10 - Results, hooks, migration (complete) - -**Deliverables:** - -- Weighted result selection from project `results` (`ResultPicker`) -- Item grant via `ItemRef` (`ResultGrantor`); inventory full blocks completion (superseded by Batch 13 world spawn) -- `ResearchCompleteEvent` Codex unlock stub + console log -- `external_modifiers` hook (`ExternalModifiers`, empty default) -- Project `enabled` flag for production pool control -- [`MIGRATION_FROM_ADVANCEDRESEARCH.md`](MIGRATION_FROM_ADVANCEDRESEARCH.md) - -**Staff can edit after:** project `results`, `external_modifiers`, project `enabled`. - -**Test checklist:** - -- [x] Completion grants weighted result item -- [x] Inventory full shows message, project stays active until space available -- [x] `ResearchCompleteEvent` fires; Codex stub logged - ---- - -## Batch 11 - Vertical station GUI - -### Batch 11a - Layout foundation (complete) - -**Deliverables:** - -- Vertical 54-slot layout in `GridLayout` (scrap 0, MP 2, product 10, aspects col 3, experiment 28, confirm 36, previews 37-38) -- `InventoryManager.populateMain()` uses new slots; aspects still in yaml order (shuffle in 11b) -- Confirm wave radiates from product slot 10; skips aspect column -- `gui.locked_aspect` + `aspect.locked_name` / `aspect.locked_lore` prepared for 11c - -**Test checklist:** - -- [ ] Fixed slots match design on open -- [ ] Scrap, experiment, confirm, previews work -- [ ] Gray filler on unused slots -- [ ] `/research reload` safe with GUI open - -### Batch 11b - Shuffle + persistence (complete) - -**Deliverables:** - -- `aspectSlotOrder` on `ActiveProject`, persisted in station JSON as `aspect_slot_order` -- `AspectSlotOrder` utility: shuffle on start, reconcile on load for legacy saves -- `InventoryManager` renders aspects at shuffled column positions; hidden slots show lock icon until reveal (11c) -- Projects with more than 6 aspects fail validation at load - -**Test checklist:** - -- [ ] Two lecterns same project can show different aspect row order -- [ ] Order survives server restart -- [ ] Legacy station files without `aspect_slot_order` get shuffled order on load - -### Batch 11c - Locked hidden slots (complete) - -**Deliverables:** - -- `buildLockedAspectItem()` using `gui.locked_aspect` and `aspect.locked_name` / `aspect.locked_lore` -- Hidden aspect slots show lock until `RevealHelper` visibility threshold; then real aspect icon at same shuffled row -- Wave animation skips lock items via aspect column skip + `ItemRef.matches` guard - -**Test checklist:** - -- [ ] Example codex: arcanum shows lock until 2 aspects confirmed -- [ ] Visible aspects show real icons at shuffled positions from project start -- [ ] Lock slots not overwritten by filler or wave pulse - -### Batch 11d - Docs polish (complete) - -**Deliverables:** - -- [`SYSTEM.md`](SYSTEM.md): vertical slot map, shuffle, locks; removed path lines and stale batch references -- [`ARCHITECTURE.md`](ARCHITECTURE.md): removed `GridPaths`; added `AspectSlotOrder`, batch 11 status, `aspect_slot_order` persistence -- [`IMPLEMENTATION_BATCHES.md`](IMPLEMENTATION_BATCHES.md): batch 9 superseded note, dependency graph, handoff section -- [`MIGRATION_FROM_ADVANCEDRESEARCH.md`](MIGRATION_FROM_ADVANCEDRESEARCH.md): vertical GUI description - -**Test checklist:** - -- [x] No stale `GridPaths`, `path_lines`, `ASPECT_RING`, or `7x7` references in staff-facing docs -- [x] Slot numbers in SYSTEM.md match `GridLayout` constants - ---- - -## Batch 12 - Aspect row progress UI - -### Batch 12a - Aspect rows + progress panes (complete) - -**Deliverables:** - -- Always 6 aspect rows (lock col 3 + 5 progress panes cols 4-8 per row) -- Random decoy rows for projects with fewer than 6 aspects -- `AspectProgressLayout` pane threshold math -- `AspectSlotOrder.buildRowAssignments` / 6-entry `aspect_slot_order` with empty decoy slots -- Yellow `???` progress panes driven by aspect points (`gui.aspect_progress_unknown`) -- Wave/filler skip all aspect row slots - -**Test checklist:** - -- [ ] 4-aspect project: 6 lock rows; 2 decoy rows never show yellow progress -- [ ] Experiment points fill correct row's yellow panes at threshold steps -- [ ] 3-needed aspect: only 3 active panes (thresholds 1/2/3) -- [ ] Legacy `aspect_slot_order` migrates to 6-row format on load -- [ ] Confirm wave does not overwrite locks or progress panes - -### Batch 12b - Discovery + confirm gate (complete) - -**Deliverables:** - -- `aspect_discovery` / `aspect_confirm` config with intelligence scaling (`DiscoveryScaling`) -- Identity reveal: aspect display item + orange panes with shared name/lore -- Points-% confirm gate; removed `base_confirm_tests` / `base_reject_tests` -- Off-recipe experiment aspects ignored (no points, no reject) -- Removed `hidden` / `reveal_when_confirmed_count` from project yaml and `ProjectDef` -- Version **0.12.0** - -**Test checklist:** - -- [ ] 10-required aspect: yellow until reveal %; orange row at threshold; confirm at confirm % -- [ ] High intelligence reveals/confirms earlier than low intelligence -- [ ] Off-recipe aspect from experiment: no row change -- [ ] `required_points: 0` discovers/confirms on first point -- [ ] Product reveal and full completion still work - -### Batch 12c - Reveal tuning + wave colors (complete) - -**Deliverables:** - -- Pane-based identity reveal and confirm thresholds (aligned with progress panes) -- Rotating `gui.wave_pulse_colors` on each experiment confirm -- `required_points` must be > 0; metal removed from example codex -- Version **0.12.1** - ---- - -## Batch 13 - Result templates + world spawn (complete) - -**Deliverables:** - -- `templates/` folder with `TemplateLoader` and `ResultTemplateDef` -- Project results accept `t.template_id`; nested templates with cycle detection at load -- `ResultRef` + `ResultPicker.pickResolved` resolves templates to concrete item refs -- `ResultSpawnEffects` pops result above lectern (kick velocity, crit trail, burst particles) -- `station.result_spawn` config; removed inventory grant / inventory-full completion gate -- Example `templates/runestones.yml` + `projects/example_staff_runestone.yml` -- Version **0.13.0** - -**Staff can edit after:** template `outputs`, project `t.*` results, `station.result_spawn` tuning. - -**Test checklist:** - -- [ ] `/research reload` loads templates before projects -- [ ] Cycle in nested `t.*` logs severe and skips bad entry -- [ ] `example_staff_runestone` completes with random rune from `t.runestones` -- [ ] `ResearchCompleteEvent` carries resolved `m.*` ref -- [ ] Result entity spawns above lectern with crit trail; player picks up from ground -- [ ] `example_decarian_codex` also world-spawns result - ---- - -## Batch 14 - `gui.yml` + remove Research Planner (complete) - -**Spec:** [`RESEARCH_REDESIGN.md`](RESEARCH_REDESIGN.md) sections `gui.yml`, Research Planner removed. - -**Deliverables:** - -- New `gui.yml` with `items.*` refs and `colors.*` hex tokens (default copied on first run) -- `GuiLoader` + `GuiCache`; remove `gui` section from `config.yml` -- Update `validateGuiItemRefs()` to read from `gui.yml` -- **Delete Research Planner:** remove `planner/` resources, `PlannerLoader`, `PlannerGuiState`, `PlannerDef`, planner GUI/handlers -- Lectern start opens main GUI directly (no planner) -- Remove planner keys from `messages.yml` -- Version **0.14.0** - -**Staff can edit after:** `gui.yml` icons and colour tokens. - -**Test checklist:** - -- [x] `/research reload` loads `gui.yml`; missing file logs severe -- [x] Invalid item ref in `gui.yml` logs on reload -- [x] Right-click lectern with valid start item opens main research GUI (never planner) -- [x] No `planner/` folder created on fresh install -- [x] Existing in-progress stations still load (still on old project model until batch 18) - ---- - -## Batch 15 - Progress UI: 5 panes, point cap, pulse wave (complete) - -**Spec:** [`RESEARCH_REDESIGN.md`](RESEARCH_REDESIGN.md) Progress panes, Confirm wave, Point cap. - -**Deliverables:** - -- `AspectProgressLayout`: always 5 active panes when `required_points > 0` -- Retune default `aspect_discovery` / `aspect_confirm` in `config.yml` if low point totals reveal too early (document chosen defaults in SYSTEM.md) -- Cap `addAspectPoints` at `required_points`; no 4/3 display -- Per-aspect `pulse_color` on aspects (replace unused `grid_color` or alias it); fallback `gui.yml` `items.wave_pulse_default` -- Pulse colour selection: primary aspect if it gained recipe points, else secondary, else default -- Wave animation: trailing ring reverts to gray filler (pulse, not sticky); remove `wave_pulse_colors` roster -- Version **0.15.0** - -**Staff can edit after:** `pulse_color` per aspect, `wave_pulse_default`, discovery thresholds. - -**Test checklist:** - -- [x] Aspect with `required_points: 3` fills all 5 panes at 3/3 -- [x] Points stop at required max; further confirms do not increase stored points -- [x] Confirm dynamite (destruction primary): wave uses red (or configured `pulse_color`) -- [x] Off-recipe confirm: wave uses default colour; slots revert to gray behind pulse front -- [x] Reveal/confirm thresholds still feel correct on 3-, 6-, and 10-point aspects - ---- - -## Batch 16 - GUI hex styling (complete) - -**Spec:** [`RESEARCH_REDESIGN.md`](RESEARCH_REDESIGN.md) GUI hex styling. - -**Deliverables:** - -- `GuiText` helper applies `StringFormatter.formatHex` using `gui.yml` `colors.*` -- `InventoryManager` (and scrap confirm sub-GUI) use palette for display names/lore -- Aspect row labels, MP item, confirm button, progress `???` text, wave pulse marker, filler panes -- **Chat messages unchanged** (`messages.yml` still uses § codes only) -- Version **0.16.0** - -**Test checklist:** - -- [x] GUI labels show hex colours in client; chat confirm lines unchanged -- [x] Missing colour token falls back safely (white `#ffffff`) -- [x] No em dash (U+2014) in new player-facing GUI strings - ---- - -## Batch 17 - Aspect-centric experiment items (complete) - -**Spec:** [`RESEARCH_REDESIGN.md`](RESEARCH_REDESIGN.md) Aspect catalog. - -**Deliverables:** - -- `AspectDef` + `AspectLoader`: `sounds`, `primary_items`, `secondary_items` -- `AspectItemRegistry` reverse index; duplicate same-role registration logs conflict, last wins -- `ExperimentItemLoader` removed; `findByItemStack` via `AspectItemRegistry` -- `config.yml` `experiment.primary_points` / `experiment.secondary_points` -- `experiment-items/` removed; example content merged into `aspects/elements.yml` -- Preview, confirm sounds, and point application use registry + global defaults -- Version **0.17.0** - -**Staff can edit after:** aspect item lists and sounds only (no per-item yaml files). - -**Deploy:** delete server `plugins/Research/experiment-items/` after updating jar. - -**Test checklist:** - -- [x] Items in `primary_items` grant primary aspect + default points on confirm -- [x] Same item can be secondary on another aspect -- [x] Duplicate primary on two aspects: reload logs conflict; last aspect wins -- [x] Unknown item in experiment slot still blocked with existing message -- [x] `/research reload` rebuilds index without restart - ---- - -## Batch 18 - Output-centric research model (complete) - -**Spec:** [`RESEARCH_REDESIGN.md`](RESEARCH_REDESIGN.md) Inputs, Outputs, persistence, Common vs rare. - -**Deliverables:** - -- `OutputLoader` + `OutputDef`; `InputLoader` + `InputDef` -- `OutputPicker.roll` + `ResultResolver.resolve` at station start; no player bias -- `ActiveProject` stores `inputId`, `resolvedOutputId`, `resolvedResultRef` -- `StationSaveData` / Gson persistence for new fields; no legacy migration -- Runtime reads aspects, displays, completion from rolled output -- Completion spawns pre-resolved result; `ResultPicker` removed -- `projects/` removed; `inputs/` + `outputs/` examples (codex, staff runestone, rare fragment) -- `InputMatcher` replaces `ProjectPicker`; `ResearchCompleteEvent` adds `inputId` / `outputId` -- Version **1.0.0** - -**Staff action on deploy:** Delete server folders `projects/`, `experiment-items/`, `planner/`; clear stale station JSON; author `inputs/` and `outputs/`. - -**Test checklist:** - -- [x] Named paper input with one output: result known from start; aspects match output yaml -- [x] Rare input with multiple outputs: output rolled at start, persisted across relog/reload -- [x] Mid-research `/research reload` does not re-roll output -- [x] Template result (`t.*`) resolved at start; completion spawns same item -- [x] Product reveal uses rolled output's `product_display` / thresholds -- [x] `ResearchCompleteEvent` fires with resolved item ref -- [x] Invalid output id in input yaml fails reload - ---- - -## Dependency graph - -```text -Batch 2 -> Batch 3 -> Batch 4 -> Batch 5 -> Batch 6 -Batch 6 -> Batch 7 -> Batch 10 (results) -Batch 7 -> Batch 9 (ring grid) -> Batch 11 (vertical GUI) -> Batch 12 (aspect rows, discovery) -Batch 13 (templates + world spawn) -Batch 14 (gui.yml, planner removal) -> Batch 15 (progress + wave) -> Batch 16 (hex GUI) -Batch 15 -> Batch 17 (aspect item lists; pulse_color already on aspects from 15) -Batch 17 -> Batch 18 (outputs/inputs; requires aspect index from 17) -``` - -**Note:** Batch 8 (Research Planner) is **superseded and removed** in batch 14. - -## Handoff to content staff - -After **batch 18**, staff author `aspects/`, `inputs/`, `outputs/`, and `templates/` only. See [`RESEARCH_REDESIGN.md`](RESEARCH_REDESIGN.md) and [`SYSTEM.md`](SYSTEM.md). diff --git a/docs/MIGRATION_FROM_ADVANCEDRESEARCH.md b/docs/MIGRATION_FROM_ADVANCEDRESEARCH.md deleted file mode 100644 index 8d534bf..0000000 --- a/docs/MIGRATION_FROM_ADVANCEDRESEARCH.md +++ /dev/null @@ -1,82 +0,0 @@ -# Migration from AdvancedResearch - -This document covers moving from **AdvancedResearch** (`me.Plugins.AdvancedResearch`) to **Research** (`net.tfminecraft.research`). Do not run both plugins on the same server lecterns at the same time. - -## Summary - -| AdvancedResearch | Research | -|------------------|----------| -| `Input` projects in single config | `inputs/*.yml` + `outputs/*.yml` | -| Element strings on station | Aspect ids from `aspects/` registry | -| `ResearchNote` slots | Experiment item + aspect point tracking | -| Result string on `RStation` | Output `result` + `t.*` templates resolved at station start | -| Station data in AdvancedResearch DB | `plugins/Research/data/stations/*.json` | - -## Before cutover - -1. **Disable AdvancedResearch** on the test server (remove from `plugins/` or disable in plugin manager). -2. **Install Research** `research-0.12.0.jar` (or current version). -3. **Author yaml** for aspects, inputs, outputs, and templates (see `docs/SYSTEM.md`). -4. Delete legacy folders on deploy: `projects/`, `experiment-items/`, `planner/`. Clear stale `data/stations/*.json` when upgrading to 1.0.0. -5. Set `enabled: false` on example content until production definitions are ready. - -## Station state - -AdvancedResearch stores per-station: - -- Owner, location, current input project, element progress, completion flag, result string - -Research stores per-station in `data/stations/___.json`: - -- Owner uuid, `input_id`, `resolved_output_id`, `resolved_result_ref`, aspect states/points, tested items, product revealed, completed, `aspect_slot_order` - -There is **no automatic import** from AdvancedResearch station files or legacy Research `project_id` saves. Players with active projects will need to restart on Research stations after cutover. - -## Content mapping - -### Inputs and outputs - -- AdvancedResearch `inputs.` maps to an `inputs/` entry (start item) plus one or more `outputs/` recipes. -- `neededElements` / element lists -> `aspects` with `required_points` on the rolled output. -- Hidden elements -> aspect identity reveal is runtime via `aspect_discovery`. -- Result list -> split: input `outputs` pool picks output id at start; chosen output's `result.item` or `result.template` is resolved at start. - -### Experiment items - -AdvancedResearch used note items tied to elements. Research uses aspect `primary_items` / `secondary_items` lists that grant points when confirmed in the experiment slot. - -### GUI - -Research uses a vertical 54-slot (9x6) station GUI: six aspect rows (lock + progress panes), shuffled assignments with decoy rows, intelligence-scaled identity reveal, and orange progress panes. Off-recipe experiment aspects are ignored. - -## Codex integration - -On project completion, Research: - -1. Spawns the pre-resolved result item via `ItemRef`. -2. Fires `ResearchCompleteEvent` with `inputId`, `outputId`, and `resultItemRef`. -3. Logs a Codex stub line to console for integration testing. - -Codex plugins should listen to `net.tfminecraft.research.event.ResearchCompleteEvent` and unlock entries from `resultItemRef`. Use `getOutputId()` (or deprecated `getProjectId()`) for output-based unlocks. - -## Mental Points - -AdvancedResearch did not use Mental Points. Research uses MP for experiments and regen from `config.yml`. Tune `mental_points` and MMOCore attribute scaling before go-live. - -## Rollback - -If you must roll back: - -1. Stop server. -2. Remove Research jar. -3. Restore AdvancedResearch jar and its data folder. -4. Research `plugins/Research/data/` can remain; it is not read by AdvancedResearch. - -## Production checklist - -- [ ] All live inputs and outputs authored with valid refs -- [ ] Example content `enabled: false` if not used -- [ ] Aspect item lists match server item ids (ItemsAdder / MMOItems) -- [ ] `/research reload` passes with no severe errors -- [ ] One full playthrough: start -> experiment -> confirm -> complete -> item grant -- [ ] Codex listener (if any) receives `ResearchCompleteEvent` diff --git a/docs/RESEARCH_REDESIGN.md b/docs/RESEARCH_REDESIGN.md deleted file mode 100644 index 250b03e..0000000 --- a/docs/RESEARCH_REDESIGN.md +++ /dev/null @@ -1,360 +0,0 @@ -# Research Plugin - Redesign Spec (locked) - -This document locks design decisions for batches 14 onward. It supersedes conflicting sections in older docs (`projects/`, `experiment-items/`, Research Planner, input-centric flow, `config.yml` `gui` section). - -**No legacy migration.** Staff delete old folders on the server and author fresh yaml under the new layout. - ---- - -## Goals - -1. Richer GUI presentation (SimpleFactions-style hex colours on GUI text only; chat messages unchanged). -2. Confirm wave as a **pulse** (colour expands, trailing slots revert to gray filler). -3. **100% progress always fills all 5 panes** per aspect row (even when `required_points` is 3). -4. **Aspect-centric experiment items** - each aspect owns sounds, `primary_items`, and `secondary_items`. -5. **Cap aspect points** at `required_points` (no 4/3 overstack). -6. **Output-centric research** - roll the result at **project start**, not at completion; aspects and GUI displays come from the rolled output. -7. **Remove Research Planner** - player choices must not bias the rolled output. -8. **`gui.yml`** - all GUI item refs and GUI hex colour tokens (layout stays fixed in code). - ---- - -## Folder layout (target) - -| Path | Purpose | -|------|---------| -| `config.yml` | MP economy, attributes, discovery thresholds, station block/effects, experiment point defaults | -| `gui.yml` | GUI item refs + hex colour tokens (no layout) | -| `messages.yml` | Player-facing chat strings (unchanged role) | -| `aspects/` | Aspect catalog: display, pulse colour, sounds, item lists | -| `inputs/` | Start items and weighted output pools | -| `outputs/` | Result definitions: displays, aspect recipe, final result | -| `templates/` | Weighted item pools (`t.*` refs) | -| `data/players/` | Player MP persistence | -| `data/stations/` | Active station state including rolled output | - -**Removed (delete on server, remove from plugin):** - -- `experiment-items/` -- `projects/` -- `planner/` - ---- - -## Player flow (target) - -1. Player holds a **start item** (research paper / fragment) and right-clicks the lectern. -2. Plugin matches `inputs/` by start item, **rolls one output** from that input's weighted pool, resolves template refs if needed, and **persists** `resolvedOutputId` + `resolvedResultRef` on the station. -3. Main GUI opens using **that output's** `mystery_display`, aspect recipe, and reveal rules. -4. Player experiments with **global** items (any item in aspect lists). Only aspects on the rolled output's recipe gain points. -5. Aspect identity reveal, confirm, and product reveal behave as today (pane-based thresholds). -6. On completion, spawn the **already-resolved** result above the lectern (no roll at finish). - -There is **no** pre-start planner screen. Start goes directly to the main research GUI. - ---- - -## Common vs rare research (staff model) - -| Type | Input | Output roll | GUI visibility | -|------|-------|-------------|----------------| -| **Named / common** | Crafted paper tied to one item (e.g. Abyssalite ingot + parchment) | Single output, weight 1.0 | `mystery_display` may equal `product_display` and final result from the start | -| **Rare / mystery** | Lost Knowledge Fragment (generic paper) | Random from multi-entry pool at start | Mystery placeholder until product reveal threshold; then `product_display`; final item on complete | - -The same three display stages apply to every output: - -1. **Start** - `mystery_display` -2. **After product reveal threshold** - `product_display` -3. **Completion** - `result` (concrete `m.*` / `ia.*` / `vanilla.*`, or pre-resolved pick from `t.*` at start) - ---- - -## `gui.yml` - -All GUI **icons and pane materials** move out of `config.yml`. Layout (slot numbers) remains in `GridLayout` Java. - -```yaml -# Item refs: m.TYPE.ID, ia.namespace:id, vanilla.MATERIAL -items: - confirm_button: ia.mcicons:icon_confirm - cancel_button: ia.mcicons:icon_cancel - scrap_button: ia.mcicons:icon_cancel # if distinct from cancel scrap flow - filler: vanilla.GRAY_STAINED_GLASS_PANE - mental_points_icon: vanilla.LIGHT - locked_aspect: ia.mcicons:icon_lock - aspect_progress_unknown: vanilla.YELLOW_STAINED_GLASS_PANE - aspect_progress_revealed: vanilla.ORANGE_STAINED_GLASS_PANE - wave_pulse_default: vanilla.LIGHT_BLUE_STAINED_GLASS_PANE - -# Hex colours for GUI display names/lore only (TLibs StringFormatter.formatHex). -# Chat messages stay in messages.yml with standard § codes. -colors: - label_muted: "#7a706a" - label_body: "#d4c9ae" - label_accent: "#b8ae61" - aspect_confirmed: "#82d461" - aspect_hidden: "#575150" - progress_unknown: "#9c9775" - mental_points: "#d1b43f" - wave_pulse_name: "#5bc4d4" -``` - -Staff may retune icons and palette without touching `config.yml`. Java reads tokens from `GuiLoader` / `GuiCache` (name TBD in implementation). - ---- - -## Aspect catalog (`aspects/`) - -Each aspect id is a top-level key. **Experiment contribution is defined here**, not in a separate experiment-items folder. - -```yaml -destruction: - name: "§cDestruction" - lore: - - "§7The force of ruin and explosive change." - display: - item: ia.mcicons:icon_cancel - pulse_color: vanilla.RED_STAINED_GLASS_PANE - sounds: - input: entity.generic.explode - confirm: entity.generic.explode - volume: 0.6 - pitch: 1.2 - primary_items: - - m.MATERIAL.DYNAMITE - - m.MATERIAL.GUNPOWDER - secondary_items: - - m.MATERIAL.TNT -``` - -### Item lookup rules - -- Global defaults in `config.yml`: - -```yaml -experiment: - primary_points: 2 - secondary_points: 1 -``` - -- For a placed item ref, scan all aspects: - - Listed under `primary_items` of aspect A → primary aspect A, `primary_points`. - - Listed under `secondary_items` of aspect B → secondary aspect B, `secondary_points`. -- One item may be primary for one aspect and secondary for another (normal case). -- **Confirm sound** and **input sound** use the item's **primary** aspect's `sounds` block. If the item has no primary registration (secondary-only), use the secondary aspect's sounds. - -### Duplicate registration - -If the same item ref appears in the **same role** (`primary_items` or `secondary_items`) on more than one aspect: - -- Log `[Research] SEVERE` (or warning) naming the item, aspects, and which aspect won. -- **Last loaded wins** (file iteration order documented in SYSTEM.md). - -Conflicts to detect at load: - -- Same item in `primary_items` on two aspects. -- Same item in `secondary_items` on two aspects. -- Same item in both `primary_items` and `secondary_items` **on the same aspect** (log; prefer primary). - -### Pulse colour on confirm wave - -When an experiment is confirmed: - -| Case | Wave pane colour | -|------|------------------| -| Primary aspect gained points on the rolled output recipe | That aspect's `pulse_color` | -| Only secondary hit the recipe | Secondary aspect's `pulse_color` | -| Neither hit the recipe | `gui.yml` `items.wave_pulse_default` | - -Remove rotating `wave_pulse_colors` list from config. - -### Point cap - -When adding aspect points: - -```text -newPoints = min(required_points, current + delta) -``` - -Do not exceed `required_points`. Optional: skip further point application when already at cap (silent; chat unchanged). - ---- - -## Progress panes - -- **Always 5 active panes** per real aspect row (`AspectProgressLayout.activePaneCount` returns 5 when `required_points > 0`). -- Pane k fills when `currentPoints >= ceil(k * required / 5)` for k = 1..5. -- Example: `required_points: 3` at 3/3 → all **5** panes lit. -- Retune `aspect_discovery` / `aspect_confirm` after this change (low `required_points` reaches reveal thresholds faster with 5 panes). - -Decoy rows unchanged (6 rows, empty decoy slots). - ---- - -## Confirm wave animation - -After confirm (when project not complete): - -1. Pulse colour from aspect rules above. -2. Rings expand outward from product slot (existing Manhattan ring logic). -3. **Trailing revert:** as ring N is painted, ring N-1 reverts to gray `filler` (true pulse, not sticky colour until full redraw). -4. Final ring revert + `populateMain()` refresh. - -Wave still skips reserved slots (controls, aspect rows, experiment slot, etc.). - ---- - -## Inputs (`inputs/`) - -Maps **start items** to weighted **output** ids. - -```yaml -lost_knowledge_fragment: - enabled: true - start_item: - item: m.research.lost_fragment - amount: 1 - outputs: - - id: seithr_essence - weight: 1.0 - - id: decarian_codex - weight: 0.5 - -abyssalite_paper: - enabled: true - start_item: - item: m.research.paper_abyssalite - amount: 1 - outputs: - - id: abyssalite_ingot - weight: 1.0 -``` - -- Match held item to `start_item` ref (same as today's project start match). -- If multiple inputs match the same start item, load validation fails (duplicate start item refs). -- Roll output once in `startResearch()`; store on `ActiveProject` / station JSON. - ---- - -## Outputs (`outputs/`) - -Defines recipe, GUI displays, and result. **No `start_item` here.** - -```yaml -seithr_essence: - enabled: true - mystery_display: - item: m.research.mystery_paper - product_display: - item: m.research.seithr_preview - product_reveal: - base_after_confirmed_aspects: 2 - result: - item: m.alchemy.seithr_essence - aspects: - ice: - required_points: 3 - food: - required_points: 2 - magic: - required_points: 5 - -abyssalite_ingot: - enabled: true - mystery_display: - item: m.research.paper_abyssalite - product_display: - item: m.research.paper_abyssalite - product_reveal: - base_after_confirmed_aspects: 0 - result: - item: m.materials.abyssalite_ingot - aspects: - metal: - required_points: 4 - earth: - required_points: 2 -``` - -### Result field - -- `result.item` - concrete ref. -- `result.template` - `t.*` id; **resolve at start** to concrete ref, store `resolvedResultRef` on station. -- Nested templates follow existing `TemplateLoader` rules. - -### Completion - -Spawn `resolvedResultRef` above lectern. Fire `ResearchCompleteEvent` with resolved ref. **No** `ResultPicker` roll at complete time. - ---- - -## Persistence (`data/stations/`) - -Extend station save data: - -| Field | Purpose | -|-------|---------| -| `inputId` | Which input definition started the session | -| `resolvedOutputId` | Rolled output id | -| `resolvedResultRef` | Concrete item ref after template resolution | -| Existing fields | `aspect_slot_order`, aspect points, tested items, product revealed, etc. | - -Mid-research reload must not re-roll output. - ---- - -## Research Planner - removed - -The Research Planner (batch 8) is **deleted**, not disabled: - -- Remove `planner/` folder, `PlannerLoader`, `PlannerGuiState`, planner GUI in `InventoryManager`, planner branch in `ResearchManager`. -- Remove bias logic from pickers (`preferredAspectIds`, `effectiveBiasPercent` for project selection). -- Remove planner messages from `messages.yml` (or leave unused keys harmlessly until cleanup). -- Lectern start: match input → roll output → open main GUI directly. - -**Rationale:** Research discovers what the paper is; players cannot bias the rolled output. - ---- - -## GUI hex styling (SimpleFactions-style) - -- Use `me.Plugins.TLibs.Objects.API.SubAPI.StringFormatter.formatHex` for GUI item display names and lore built in `InventoryManager`. -- Apply `gui.yml` `colors.*` tokens to aspect row labels, MP display, confirm button lore, progress pane `???` text, etc. -- **Do not** change chat feedback strings in `messages.yml` for this pass. - -Optional later: map aspect `pulse_color` to revealed pane material; v1 may keep `aspect_progress_revealed` from `gui.yml` for all aspects. - ---- - -## Load order - -```text -config.yml -messages.yml -gui.yml -aspects/ (build item->aspect index here) -templates/ -outputs/ (validate aspect refs) -inputs/ (validate output ids exist) -``` - -`/research reload` reloads all of the above. Delete stale registries for removed folders. - ---- - -## Validation checklist (load time) - -- Every aspect ref in `outputs/*.yml` exists in `aspects/`. -- Every `inputs/*.yml` output id exists in `outputs/`. -- Every item ref in aspect lists passes `ItemRef` validation. -- Duplicate start items across inputs fail load. -- Duplicate same-role item registration logs + last wins (warn staff in reload summary). -- Template cycles unchanged from batch 13 rules. - ---- - -## Related docs - -- Implementation order: [`IMPLEMENTATION_BATCHES.md`](IMPLEMENTATION_BATCHES.md) batches 14-18 -- Staff authoring (updated after implementation): [`SYSTEM.md`](SYSTEM.md) -- Code layout: [`ARCHITECTURE.md`](ARCHITECTURE.md) diff --git a/docs/SYSTEM.md b/docs/SYSTEM.md deleted file mode 100644 index a893301..0000000 --- a/docs/SYSTEM.md +++ /dev/null @@ -1,269 +0,0 @@ -# Research Plugin - System Overview - -> **Version 1.0.0 (batch 18):** Research uses `inputs/` + `outputs/` instead of `projects/`. Output and result are rolled at station start and persisted in station JSON. See [`RESEARCH_REDESIGN.md`](RESEARCH_REDESIGN.md). - -This document explains the Thaumcraft-style research system for staff who author config and for developers implementing later batches. - -## What players do - -Research is a **discovery minigame**, not a spreadsheet grind. - -1. **Start a project** at a research station (default: lectern). **Hold** that project's **start_item** research paper (`m.research.r_*` MMOItems) in your hand and right-click the lectern. The paper in your hand is consumed (not items elsewhere in your inventory). -2. **Click an item** in your inventory to place it in the experiment slot (one per click). **Click the experiment slot** to take it back out. -3. **Confirm the experiment** (costs Mental Points). The item's **primary aspect** grants `experiment.primary_points` (default 2) and **secondary aspect** grants `experiment.secondary_points` (default 1) toward testing those aspects on the project. -4. After enough experiments on an aspect, it is either **confirmed** (you need X points in it to complete the recipe) or **rejected** (gray out - do not use it anymore). -5. Over time the **product reveals** and aspect rows **unlock identity** (tested-unknown icon, then aspect display + orange progress panes). -6. When all required aspects are confirmed with enough points, the **pre-resolved result** pops above the lectern. The result was chosen at station start (output roll + template resolve). Pick it up from the world. Codex plugins listen to `ResearchCompleteEvent`. - -High **Intelligence** (configurable MMOCore attribute) lowers aspect identity reveal and confirm thresholds, speeds product reveal. **Wisdom** and **Intelligence** can increase Focus regen (configured in TFMCCore `focus.yml`). Mental points are **character-keyed** on TFMCCore, shared with Magic meditation. - -## Comparison with AdvancedResearch - -| Area | AdvancedResearch (old) | Research (new) | -|------|------------------------|----------------| -| Core action | Click abstract research notes | Insert real items as experiments | -| Product | Known from the start | Hidden until mid-research | -| Required elements | Names visible immediately | Hidden until discovered | -| Item cost | Starter item consumed; notes use MP only | Per-project `start_item` paper on start; experiment items returned after confirm | -| Feedback | Random +/- deltas on known bars | Confirm / reject / gray aspects | -| UI | 54-slot vanilla chest GUI | Vertical 54-slot station GUI (shuffled aspect column, lock placeholders) | -| Intelligence | Better note RNG | Earlier reveals and confirm/reject | - -Both plugins should **not** share the same lectern stations until migration is planned. - -## Config folder layout - -All paths are under `plugins/Research/` on the server. - -| Path | Purpose | -|------|---------| -| `config.yml` | MP economy, attribute IDs, experiment thresholds, global experiment points, station block | -| `messages.yml` | Player-facing chat strings | -| `aspects/` | Aspect catalog (display, sounds, `primary_items`, `secondary_items`) | -| `inputs/` | Start items mapped to weighted output id pools | -| `outputs/` | Rolled recipes: displays, aspects, single `result` | -| `templates/` | Weighted result template pools (`t.*` refs in output `result.template`) | -| `planner/` | Research Planner (**removed in batch 14**) | -| `gui.yml` | GUI item refs and hex colour tokens (**added in batch 14**) | -| `data/players/` | Runtime player JSON (batch 3) | -| `data/stations/` | Runtime active station state (batch 4) | - -Load order: `config.yml`, `messages.yml`, `gui.yml`, `aspects/`, `templates/`, `outputs/`, `inputs/`. Aspect and loader yaml files iterate in **alphabetical filename order**. Duplicate start items across inputs fail reload. Duplicate aspect item refs in the same role log a conflict and **last loaded wins**. Use `/research reload` to apply yaml changes without a restart. - -## Item reference format - -Never hardcode item ids in Java. Use these strings in yaml: - -| Prefix | Example | Meaning | -|--------|---------|---------| -| `m.TYPE.ID` | `m.MATERIAL.DYNAMITE` | MMOItems type + item id | -| `ia.namespace:id` | `ia.tfmc:arcane_crystal` | ItemsAdder namespaced id | -| `vanilla.MATERIAL` | `vanilla.IRON_INGOT` | Vanilla material | -| `t.ID` | `t.runestones` | Result template id in `templates/` | - -Result templates can nest (`t.child` inside a template). Cycles are detected at load; invalid entries are skipped with a severe log. - -## Authoring workflow for staff - -### 1. Define aspects (`aspects/`) - -Each aspect needs a unique id key used everywhere else: - -```yaml -destruction: - name: "§cDestruction" - lore: - - "§7The force of ruin." - display: - item: ia.mcicons:icon_cancel - grid_color: red - pulse_color: vanilla.RED_STAINED_GLASS_PANE - sounds: - input: entity.item.pickup - confirm: entity.generic.explode - volume: 0.6 - pitch: 1.2 - primary_items: - - m.materials.dynamite - - v.GUNPOWDER - secondary_items: - - v.TNT -``` - -You can split aspects across multiple files (e.g. `elements.yml`, `arcane.yml`). Aspect yaml files load in alphabetical filename order. - -Global point defaults live in `config.yml`: - -```yaml -experiment: - primary_points: 2 - secondary_points: 1 -``` - -**Item lookup:** scan all aspects. A ref in `primary_items` grants primary aspect + `primary_points`. A ref in `secondary_items` grants secondary aspect + `secondary_points`. One item may be primary on one aspect and secondary on another. Confirm and input sounds use the **primary** aspect's `sounds` block; if the item has no primary registration, use the secondary aspect's sounds. - -**Deploy note:** delete the legacy `experiment-items/` folder on the server after updating to batch 17+; item lists in `aspects/` are authoritative. - -### 2. Define inputs (`inputs/`) - -Maps a **start item** to weighted **output ids**: - -```yaml -decarian_codex_paper: - enabled: true - start_item: - item: m.research.r_ - amount: 1 - outputs: - - id: decarian_codex - weight: 1.0 -``` - -Each start item ref must be unique across all inputs. Output is rolled once when the player starts research. - -### 3. Define outputs (`outputs/`) - -Recipe, GUI displays, and result. **No `start_item` here.** - -```yaml -decarian_codex: - enabled: true - mystery_display: - item: m.research.r_ - product_display: - item: m.research.c_arcane_crystal - product_reveal: - base_after_confirmed_aspects: 2 - result: - item: m.research.c_arcane_crystal - aspects: - destruction: - required_points: 6 - arcanum: - required_points: 4 -``` - -- `result.item` - concrete ref spawned on completion. -- `result.template` - `t.*` id; resolved **at station start** to a concrete ref and stored on the station. - -`required_points` must be **greater than 0** for every aspect in an output. - -**Deploy note:** delete legacy `projects/` and `experiment-items/` folders on the server when upgrading to 1.0.0. Clear stale `data/stations/*.json` (old `project_id` format is not migrated). - -### 4. Result templates (`templates/`) - -Reusable weighted pools referenced as `t.template_id` in output `result.template`: - -```yaml -runestones: - outputs: - - item: m.seithr_runes.rune_of_ice_shard - weight: 1.0 - - item: m.oseni_runes.rune_of_fire_breath - weight: 1.0 -``` - -Output example: - -```yaml -result: - template: t.runestones -``` - -`ResearchCompleteEvent` carries `inputId`, `outputId`, and the resolved concrete item ref (e.g. `m.*`), not `t.runestones`. - -### 5. Tune economy (`config.yml`) - -- `mental_points` - cap, hourly regen, offline regen, experiment cost. -- `attributes.discovery.mmocore_id` - which MMOCore stat lowers reveal/confirm thresholds (default `intelligence`). -- `aspect_discovery` - `base_reveal_percent`, `reveal_percent_reduction_per_point`, `min_reveal_percent`. -- `aspect_confirm` - `base_confirm_percent`, `confirm_percent_reduction_per_point`, `min_confirm_percent`. -- `attributes.regen_bonuses` - list of stats that add MP per hour. -- `station.result_spawn` - kick velocity, crit trail duration, burst particles when the result item pops above the lectern. - -Attribute ids are **not** hardcoded in plugin code. - -## Aspect state machine (runtime) - -Each aspect on an active project moves through: - -```text -UNKNOWN -> TESTING (accumulating experiment points) -> CONFIRMED or REJECTED -``` - -- **CONFIRMED**: aspect is part of the recipe; confirmed when points reach `aspect_confirm` percent of `required_points` (intelligence lowers threshold). -- **REJECTED**: legacy state only; off-recipe experiment aspects are ignored (no points, no UI). - -Confirm thresholds use `aspect_confirm` in `config.yml`, not experiment test counts. Off-recipe aspects from experiment items do nothing. - -## Station GUI - -The research station uses a **vertical 54-slot** (9x6) chest inventory. Fixed control slots plus a shuffled aspect column; `grid_filler` on unused slots. - -### Slot map - -| Slot | Role | -|------|------| -| 0 | Scrap | -| 2 | Mental Points display | -| 10 | Mystery / product paper | -| 3, 12, 21, 30, 39, 48 | Aspect lock column (always 6 rows) | -| 4-8, 13-17, ... (cols 4-8 per row) | Progress panes (5 per aspect row) | -| 28 | Experiment item | -| 36 | Confirm experiment | -| 37, 38 | Primary / secondary aspect preview | - -### Aspect rows - -- **Always 6 rows:** Projects with fewer than 6 aspects use random **decoy rows** (black glass divider + empty progress panes). -- **Shuffle:** Real aspect ids shuffled into random rows at project start (`aspect_slot_order`, 6 entries, empty = decoy). -- **Progress panes (batch 15):** Always **5 active panes** per real aspect row. Pane k fills when points reach `ceil(k * required / 5)`. At full `required_points`, all 5 panes are lit (e.g. 3/3 fills 5 panes). Points are capped at `required_points` (no overstack). -- **Divider (col 3):** `aspect_row_divider` (default black stained glass, blank name, no hover tooltip) for decoy rows and real aspects with **0 points**. -- **Tested-unknown (col 3):** When points > 0 but identity is not revealed, `aspect_tested_unknown` icon. Label from `messages.yml` `aspect.tested_unknown_name`. Yellow progress panes fill as points accumulate (`aspect.progress_unknown_name`). -- **Identity reveal:** When **filled progress panes** reach `aspect_discovery` percent of 5 panes (default 0.8; intelligence lowers threshold), col 3 becomes the aspect display item and filled panes become orange glass with shared name/lore. -- **Confirm:** When filled panes reach `aspect_confirm` percent of 5 panes (default 0.8), aspect state becomes CONFIRMED. Completion still requires full `required_points` on every recipe aspect. -- **Off-recipe aspects:** Experiment items that grant aspects not in the rolled output yaml add no points and show no UI feedback (wave and sound still play). Example: iron ingot (metal) on decarian codex. - -### Confirm wave - -After confirming an experiment, a brief **pulse** radiates outward from the product slot (10). Colour comes from the **primary aspect's** `pulse_color` in `aspects/` if that aspect gained recipe points; else the secondary aspect's colour; else `gui.yml` `items.wave_pulse_default`. Each ring reverts to gray filler behind the wave front (not sticky). Only empty filler slots are touched, not content slots. - -Aspect `pulse_color` is an item ref (e.g. `vanilla.RED_STAINED_GLASS_PANE`). If omitted, `grid_color` maps to a default pane colour. - -### GUI item refs (`gui.yml` `items` section) - -- `confirm_button`, `cancel_button`, `filler` (legacy fallback), `grid_filler` (background padding), `aspect_progress_inactive` (cols 4-8 empty panes), `mental_points_icon`, `locked_aspect`, `aspect_row_divider`, `aspect_tested_unknown`, `aspect_progress_unknown`, `aspect_progress_revealed`, `wave_pulse_default` -- Padding items (`aspect_row_divider`, `aspect_progress_inactive`, `grid_filler`) use a blank display name and hide tooltips so they do not pop a name on hover. -- `labels.inventory_title` / `labels.scrap_confirm_title` plus `colors.inventory_title` for the chest title (applied on each open, so `/research reload` is picked up next time the GUI opens). -- Hex colour tokens under `colors`; applied to GUI item names/lore via `GuiText` + TLibs `StringFormatter.formatHex`. -- GUI labels that come from `messages.yml` (`aspect.progress_unknown_name`, `aspect.tested_unknown_name`, lore lines via `GuiText.guiMessage`) are formatted the same way. Chat (`Messages.get` / `Messages.format`) also runs `formatHex` when the string contains `#`. - -## Research Planner (batch 8) - removed in batch 14 - -The Research Planner allowed players to spend MP to bias project selection. It is **removed** in batch 14: output is rolled at research start with no player bias. See [`RESEARCH_REDESIGN.md`](RESEARCH_REDESIGN.md). - -~~Before starting a project, players may:~~ - -~~- Pay Mental Points to bias toward an aspect or planner category (higher chance that aspect appears in the project).~~ -~~- Skip preference for free.~~ - -~~Odds scale from `base_bias_percent` to `max_bias_percent` based on discovery attribute. Config lives in `planner/categories.yml`.~~ - -## Post-implementation notes - -- **Researcher profession** perks - configure via `external_modifiers` in `config.yml` when profession rework is ready. -- **AdvancedResearch migration** - see [`MIGRATION_FROM_ADVANCEDRESEARCH.md`](MIGRATION_FROM_ADVANCEDRESEARCH.md); do not run both plugins on the same stations. - -## Example playthrough (Decarian Codex) - -Balanced for **6 unique experiment items** and default `aspect_confirm.base_confirm_percent: 0.8`. - -1. Player opens station - six lock rows; decoy rows never gain progress. -2. Confirm **dynamite**, **gunpowder**, and **TNT**. Yellow panes fill on unknown rows; at 60% of active panes filled, rows reveal aspect name + orange panes. -3. Destruction and fire **confirm** when enough panes fill; product reveals after 2 confirmed aspects. -4. Confirm **arcane crystal**, **enchanted book**, and **lapis** for the revealed arcanum row until 4 points and confirm. -5. Pre-resolved result item `m.research.c_arcane_crystal` pops above the lectern (chosen at station start). - -## Reload - -`/research reload` (permission `research.admin`) reloads all yaml and restarts the MP regen task. Safe to run with a station GUI open. diff --git a/pom.xml b/pom.xml index f3fab99..49669ed 100644 --- a/pom.xml +++ b/pom.xml @@ -1,94 +1,98 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 net.tfminecraft research - 1.0.0 + main-SNAPSHOT + + Research + https://github.com/TF-Minecraft/Research - 17 - 17 + 21 + UTF-8 + full + 2.0.1 + 2.0.1 - local - gson - 2.10.1 - system - D:/Dokumenter/My Plugins/Reference Libs/gson-2.10.1.jar + io.papermc.paper + paper-api + 1.21.10-R0.1-SNAPSHOT + provided local MMOCore - 1.11.3 - system - D:/Dokumenter/My Plugins/Reference Libs/MMOCore-1.11.3.jar - - - local - MMOItems - 6.10 - system - D:/Dokumenter/My Plugins/Reference Libs/MMOItems-6.10.jar + 1.13.1-tfmc-14850d745437 + provided local MythicLib - 1.6.1 - system - D:/Dokumenter/My Plugins/Reference Libs/MythicLib-1.7.jar - - - local - spigot-api - 1.21.8 - system - D:/Dokumenter/My Plugins/Reference Libs/spigot-api.jar + 1.7-tfmc-660ff2a6ec86 + provided - local - TLibs - 1.0 - system - D:/Dokumenter/My Plugins/TFMC/TLibs/TLibs.jar + me.plugins + tlibs + ${tlibs.version} + provided + + + * + * + + - local - TFMCCore - 1.0 - system - D:/Dokumenter/My Plugins/TFMC/TFMCCore/TFMCCore.jar - - - local - LoneLibs - 3.5.0 - system - D:/Dokumenter/My Plugins/Reference Libs/ItemsAdder_3.5.0-r2.jar + net.tfminecraft + tfmccore + ${tfmccore.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-antrun-plugin - - - package - run - - - - - - - + maven-compiler-plugin + 3.14.1 diff --git a/src/main/java/net/tfminecraft/research/GuiCache.java b/src/main/java/net/tfminecraft/research/GuiCache.java index 03f4e13..c975df8 100644 --- a/src/main/java/net/tfminecraft/research/GuiCache.java +++ b/src/main/java/net/tfminecraft/research/GuiCache.java @@ -1,57 +1,57 @@ -package net.tfminecraft.research; - -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * GUI item refs and colour tokens from gui.yml. Layout slots stay in {@code GridLayout}. - */ -public final class GuiCache { - - private GuiCache() {} - - public static String confirmButton = "ia.mcicons:icon_confirm"; - public static String cancelButton = "ia.mcicons:icon_cancel"; - public static String filler = "vanilla.GRAY_STAINED_GLASS_PANE"; - public static String gridFiller = "vanilla.GRAY_STAINED_GLASS_PANE"; - public static String aspectProgressInactive = "vanilla.GRAY_STAINED_GLASS_PANE"; - public static String mentalPointsIcon = "vanilla.LIGHT"; - public static String lockedAspect = "ia.mcicons:icon_lock"; - public static String aspectRowDivider = "vanilla.BLACK_STAINED_GLASS_PANE"; - public static String aspectTestedUnknown = "ia.mcicons:icon_lock"; - public static String aspectProgressUnknown = "vanilla.YELLOW_STAINED_GLASS_PANE"; - public static String aspectProgressRevealed = "vanilla.ORANGE_STAINED_GLASS_PANE"; - public static String wavePulseDefault = "vanilla.LIGHT_BLUE_STAINED_GLASS_PANE"; - public static String leftFiller = "vanilla.BLACK_STAINED_GLASS_PANE"; - - public static String inventoryTitleLabel = "Research Station"; - public static String scrapConfirmTitleLabel = "Confirm Scrap"; - public static String undiscoveredAspectName = "§8Undiscovered Aspect"; - public static List undiscoveredAspectLore = List.of(); - public static String scrapButtonLabel = "#c45749Scrap"; - public static String confirmExperimentLabel = "#82d461Confirm Experiment"; - public static String experimentCompletedLabel = "#575150Completed"; - public static String scrapConfirmYesLabel = "#82d461Confirm Scrap"; - public static String scrapConfirmNoLabel = "#c45749Cancel"; - - /** Hex colour tokens for GUI display names/lore (applied in batch 16). */ - public static Map colors = Collections.emptyMap(); - - public static void resetColors(Map loaded) { - if (loaded == null || loaded.isEmpty()) { - colors = Collections.emptyMap(); - return; - } - colors = Collections.unmodifiableMap(new LinkedHashMap<>(loaded)); - } - - public static String color(String key, String fallback) { - String value = colors.get(key); - if (value == null || value.isBlank()) { - return fallback; - } - return value; - } -} +package net.tfminecraft.research; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * GUI item refs and colour tokens from gui.yml. Layout slots stay in {@code GridLayout}. + */ +public final class GuiCache { + + private GuiCache() {} + + public static String confirmButton = "ia.mcicons:icon_confirm"; + public static String cancelButton = "ia.mcicons:icon_cancel"; + public static String filler = "vanilla.GRAY_STAINED_GLASS_PANE"; + public static String gridFiller = "vanilla.GRAY_STAINED_GLASS_PANE"; + public static String aspectProgressInactive = "vanilla.GRAY_STAINED_GLASS_PANE"; + public static String mentalPointsIcon = "vanilla.LIGHT"; + public static String lockedAspect = "ia.mcicons:icon_lock"; + public static String aspectRowDivider = "vanilla.BLACK_STAINED_GLASS_PANE"; + public static String aspectTestedUnknown = "ia.mcicons:icon_lock"; + public static String aspectProgressUnknown = "vanilla.YELLOW_STAINED_GLASS_PANE"; + public static String aspectProgressRevealed = "vanilla.ORANGE_STAINED_GLASS_PANE"; + public static String wavePulseDefault = "vanilla.LIGHT_BLUE_STAINED_GLASS_PANE"; + public static String leftFiller = "vanilla.BLACK_STAINED_GLASS_PANE"; + + public static String inventoryTitleLabel = "Research Station"; + public static String scrapConfirmTitleLabel = "Confirm Scrap"; + public static String undiscoveredAspectName = "§8Undiscovered Aspect"; + public static List undiscoveredAspectLore = List.of(); + public static String scrapButtonLabel = "#c45749Scrap"; + public static String confirmExperimentLabel = "#82d461Confirm Experiment"; + public static String experimentCompletedLabel = "#575150Completed"; + public static String scrapConfirmYesLabel = "#82d461Confirm Scrap"; + public static String scrapConfirmNoLabel = "#c45749Cancel"; + + /** Hex colour tokens for GUI display names/lore. */ + public static Map colors = Collections.emptyMap(); + + public static void resetColors(Map loaded) { + if (loaded == null || loaded.isEmpty()) { + colors = Collections.emptyMap(); + return; + } + colors = Collections.unmodifiableMap(new LinkedHashMap<>(loaded)); + } + + public static String color(String key, String fallback) { + String value = colors.get(key); + if (value == null || value.isBlank()) { + return fallback; + } + return value; + } +} diff --git a/src/main/java/net/tfminecraft/research/Messages.java b/src/main/java/net/tfminecraft/research/Messages.java index 24acaf9..9ba2d5a 100644 --- a/src/main/java/net/tfminecraft/research/Messages.java +++ b/src/main/java/net/tfminecraft/research/Messages.java @@ -3,7 +3,7 @@ import java.util.HashMap; import java.util.Map; -import me.Plugins.TLibs.Objects.API.SubAPI.StringFormatter; +import net.tfminecraft.tlibs.objects.api.subapi.StringFormatter; public final class Messages { diff --git a/src/main/java/net/tfminecraft/research/database/StationSaveData.java b/src/main/java/net/tfminecraft/research/database/StationSaveData.java index db4323f..c33588b 100644 --- a/src/main/java/net/tfminecraft/research/database/StationSaveData.java +++ b/src/main/java/net/tfminecraft/research/database/StationSaveData.java @@ -88,7 +88,7 @@ public void setResolvedResultRef(String resolvedResultRef) { } /** - * @deprecated Legacy field from project-centric saves; not read on load in batch 18+. + * @deprecated Legacy field from project-centric saves; not read on load. */ @Deprecated public String getProjectId() { @@ -96,7 +96,7 @@ public String getProjectId() { } /** - * @deprecated Legacy field from project-centric saves; not written in batch 18+. + * @deprecated Legacy field from project-centric saves; not written. */ @Deprecated public void setProjectId(String projectId) { diff --git a/src/main/java/net/tfminecraft/research/event/ResearchCompleteEvent.java b/src/main/java/net/tfminecraft/research/event/ResearchCompleteEvent.java index ad2acfe..774a683 100644 --- a/src/main/java/net/tfminecraft/research/event/ResearchCompleteEvent.java +++ b/src/main/java/net/tfminecraft/research/event/ResearchCompleteEvent.java @@ -37,7 +37,7 @@ public String getOutputId() { } /** - * @deprecated Use {@link #getOutputId()} for batch 18+ output-centric model. + * @deprecated Use {@link #getOutputId()} for the output-centric model. */ @Deprecated public String getProjectId() { diff --git a/src/main/java/net/tfminecraft/research/loader/AspectLoader.java b/src/main/java/net/tfminecraft/research/loader/AspectLoader.java index 61480dc..bc90fbd 100644 --- a/src/main/java/net/tfminecraft/research/loader/AspectLoader.java +++ b/src/main/java/net/tfminecraft/research/loader/AspectLoader.java @@ -12,7 +12,7 @@ import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.YamlConfiguration; -import me.Plugins.TLibs.Interface.LoaderInterface; +import net.tfminecraft.tlibs.interfaces.LoaderInterface; import net.tfminecraft.research.Research; import net.tfminecraft.research.model.AspectDef; import net.tfminecraft.research.registry.AspectItemRegistry; diff --git a/src/main/java/net/tfminecraft/research/loader/ConfigLoader.java b/src/main/java/net/tfminecraft/research/loader/ConfigLoader.java index 9c3c94b..59f2d57 100644 --- a/src/main/java/net/tfminecraft/research/loader/ConfigLoader.java +++ b/src/main/java/net/tfminecraft/research/loader/ConfigLoader.java @@ -10,7 +10,7 @@ import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; -import me.Plugins.TLibs.Interface.LoaderInterface; +import net.tfminecraft.tlibs.interfaces.LoaderInterface; import net.tfminecraft.research.Cache; import net.tfminecraft.research.Research; diff --git a/src/main/java/net/tfminecraft/research/loader/GuiLoader.java b/src/main/java/net/tfminecraft/research/loader/GuiLoader.java index 376b162..6ba4299 100644 --- a/src/main/java/net/tfminecraft/research/loader/GuiLoader.java +++ b/src/main/java/net/tfminecraft/research/loader/GuiLoader.java @@ -1,94 +1,94 @@ -package net.tfminecraft.research.loader; - -import java.io.File; -import java.io.IOException; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -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.research.GuiCache; -import net.tfminecraft.research.Research; - -public final class GuiLoader implements LoaderInterface { - - @Override - public void load(File configFile) { - if (!loadInternal(configFile)) { - Research.plugin.getLogger().severe("[Research] gui.yml load failed."); - } - } - - public boolean loadSafe(File configFile) { - return loadInternal(configFile); - } - - private boolean loadInternal(File configFile) { - FileConfiguration config = new YamlConfiguration(); - try { - config.load(configFile); - } catch (IOException | InvalidConfigurationException ex) { - Research.plugin.getLogger().severe("[Research] Failed to load gui.yml: " + ex.getMessage()); - return false; - } - - ConfigurationSection items = config.getConfigurationSection("items"); - if (items != null) { - GuiCache.confirmButton = items.getString("confirm_button", GuiCache.confirmButton); - GuiCache.cancelButton = items.getString("cancel_button", GuiCache.cancelButton); - GuiCache.filler = items.getString("filler", GuiCache.filler); - GuiCache.gridFiller = items.getString("grid_filler", GuiCache.filler); - GuiCache.aspectProgressInactive = items.getString( - "aspect_progress_inactive", GuiCache.filler); - GuiCache.mentalPointsIcon = items.getString("mental_points_icon", GuiCache.mentalPointsIcon); - GuiCache.lockedAspect = items.getString("locked_aspect", GuiCache.lockedAspect); - GuiCache.aspectRowDivider = items.getString("aspect_row_divider", GuiCache.aspectRowDivider); - GuiCache.aspectTestedUnknown = items.getString("aspect_tested_unknown", GuiCache.aspectTestedUnknown); - GuiCache.aspectProgressUnknown = items.getString("aspect_progress_unknown", GuiCache.aspectProgressUnknown); - GuiCache.aspectProgressRevealed = items.getString("aspect_progress_revealed", GuiCache.aspectProgressRevealed); - GuiCache.wavePulseDefault = items.getString("wave_pulse_default", GuiCache.wavePulseDefault); - GuiCache.leftFiller = items.getString("left_filler", GuiCache.leftFiller); - } - - ConfigurationSection labels = config.getConfigurationSection("labels"); - if (labels != null) { - GuiCache.inventoryTitleLabel = labels.getString("inventory_title", GuiCache.inventoryTitleLabel); - GuiCache.scrapConfirmTitleLabel = labels.getString( - "scrap_confirm_title", GuiCache.scrapConfirmTitleLabel); - GuiCache.undiscoveredAspectName = labels.getString( - "undiscovered_aspect_name", GuiCache.undiscoveredAspectName); - if (labels.isList("undiscovered_aspect_lore")) { - GuiCache.undiscoveredAspectLore = List.copyOf(labels.getStringList("undiscovered_aspect_lore")); - } else { - GuiCache.undiscoveredAspectLore = List.of(); - } - GuiCache.scrapButtonLabel = labels.getString("scrap_button", GuiCache.scrapButtonLabel); - GuiCache.confirmExperimentLabel = labels.getString( - "confirm_experiment", GuiCache.confirmExperimentLabel); - GuiCache.experimentCompletedLabel = labels.getString( - "experiment_completed", GuiCache.experimentCompletedLabel); - GuiCache.scrapConfirmYesLabel = labels.getString( - "scrap_confirm_yes", GuiCache.scrapConfirmYesLabel); - GuiCache.scrapConfirmNoLabel = labels.getString( - "scrap_confirm_no", GuiCache.scrapConfirmNoLabel); - } - - Map colorTokens = new LinkedHashMap<>(); - ConfigurationSection colors = config.getConfigurationSection("colors"); - if (colors != null) { - for (String key : colors.getKeys(false)) { - String value = colors.getString(key, ""); - if (value != null && !value.isBlank()) { - colorTokens.put(key, value); - } - } - } - GuiCache.resetColors(colorTokens); - return true; - } -} +package net.tfminecraft.research.loader; + +import java.io.File; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +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.research.GuiCache; +import net.tfminecraft.research.Research; + +public final class GuiLoader implements LoaderInterface { + + @Override + public void load(File configFile) { + if (!loadInternal(configFile)) { + Research.plugin.getLogger().severe("[Research] gui.yml load failed."); + } + } + + public boolean loadSafe(File configFile) { + return loadInternal(configFile); + } + + private boolean loadInternal(File configFile) { + FileConfiguration config = new YamlConfiguration(); + try { + config.load(configFile); + } catch (IOException | InvalidConfigurationException ex) { + Research.plugin.getLogger().severe("[Research] Failed to load gui.yml: " + ex.getMessage()); + return false; + } + + ConfigurationSection items = config.getConfigurationSection("items"); + if (items != null) { + GuiCache.confirmButton = items.getString("confirm_button", GuiCache.confirmButton); + GuiCache.cancelButton = items.getString("cancel_button", GuiCache.cancelButton); + GuiCache.filler = items.getString("filler", GuiCache.filler); + GuiCache.gridFiller = items.getString("grid_filler", GuiCache.filler); + GuiCache.aspectProgressInactive = items.getString( + "aspect_progress_inactive", GuiCache.filler); + GuiCache.mentalPointsIcon = items.getString("mental_points_icon", GuiCache.mentalPointsIcon); + GuiCache.lockedAspect = items.getString("locked_aspect", GuiCache.lockedAspect); + GuiCache.aspectRowDivider = items.getString("aspect_row_divider", GuiCache.aspectRowDivider); + GuiCache.aspectTestedUnknown = items.getString("aspect_tested_unknown", GuiCache.aspectTestedUnknown); + GuiCache.aspectProgressUnknown = items.getString("aspect_progress_unknown", GuiCache.aspectProgressUnknown); + GuiCache.aspectProgressRevealed = items.getString("aspect_progress_revealed", GuiCache.aspectProgressRevealed); + GuiCache.wavePulseDefault = items.getString("wave_pulse_default", GuiCache.wavePulseDefault); + GuiCache.leftFiller = items.getString("left_filler", GuiCache.leftFiller); + } + + ConfigurationSection labels = config.getConfigurationSection("labels"); + if (labels != null) { + GuiCache.inventoryTitleLabel = labels.getString("inventory_title", GuiCache.inventoryTitleLabel); + GuiCache.scrapConfirmTitleLabel = labels.getString( + "scrap_confirm_title", GuiCache.scrapConfirmTitleLabel); + GuiCache.undiscoveredAspectName = labels.getString( + "undiscovered_aspect_name", GuiCache.undiscoveredAspectName); + if (labels.isList("undiscovered_aspect_lore")) { + GuiCache.undiscoveredAspectLore = List.copyOf(labels.getStringList("undiscovered_aspect_lore")); + } else { + GuiCache.undiscoveredAspectLore = List.of(); + } + GuiCache.scrapButtonLabel = labels.getString("scrap_button", GuiCache.scrapButtonLabel); + GuiCache.confirmExperimentLabel = labels.getString( + "confirm_experiment", GuiCache.confirmExperimentLabel); + GuiCache.experimentCompletedLabel = labels.getString( + "experiment_completed", GuiCache.experimentCompletedLabel); + GuiCache.scrapConfirmYesLabel = labels.getString( + "scrap_confirm_yes", GuiCache.scrapConfirmYesLabel); + GuiCache.scrapConfirmNoLabel = labels.getString( + "scrap_confirm_no", GuiCache.scrapConfirmNoLabel); + } + + Map colorTokens = new LinkedHashMap<>(); + ConfigurationSection colors = config.getConfigurationSection("colors"); + if (colors != null) { + for (String key : colors.getKeys(false)) { + String value = colors.getString(key, ""); + if (value != null && !value.isBlank()) { + colorTokens.put(key, value); + } + } + } + GuiCache.resetColors(colorTokens); + return true; + } +} diff --git a/src/main/java/net/tfminecraft/research/loader/InputLoader.java b/src/main/java/net/tfminecraft/research/loader/InputLoader.java index c9f69b8..42a60df 100644 --- a/src/main/java/net/tfminecraft/research/loader/InputLoader.java +++ b/src/main/java/net/tfminecraft/research/loader/InputLoader.java @@ -1,164 +1,164 @@ -package net.tfminecraft.research.loader; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.InvalidConfigurationException; -import org.bukkit.configuration.file.YamlConfiguration; - -import me.Plugins.TLibs.Interface.LoaderInterface; -import net.tfminecraft.research.Research; -import net.tfminecraft.research.model.InputDef; -import net.tfminecraft.research.util.ItemRef; -import net.tfminecraft.research.util.YamlFolder; - -public final class InputLoader implements LoaderInterface { - - private static final Map map = new LinkedHashMap<>(); - - public static Map get() { - return map; - } - - public static InputDef getById(String id) { - return map.get(id); - } - - @Override - public void load(File configFile) { - loadFile(configFile); - } - - public boolean loadFolder(File folder) { - map.clear(); - boolean ok = true; - List files = new ArrayList<>(YamlFolder.listYamlFiles(folder)); - files.sort(Comparator.comparing(file -> file.getName().toLowerCase())); - for (File file : files) { - ok &= loadFile(file); - } - ok &= validateUniqueStartItems(); - return ok; - } - - private boolean loadFile(File configFile) { - YamlConfiguration config = new YamlConfiguration(); - try { - config.load(configFile); - } catch (IOException | InvalidConfigurationException ex) { - Research.plugin.getLogger().severe("[Research] Failed to load input file " + configFile.getName() - + ": " + ex.getMessage()); - return false; - } - - boolean ok = true; - for (String key : config.getKeys(false)) { - if (map.containsKey(key)) { - Research.plugin.getLogger().severe("[Research] Duplicate input id '" + key + "' in " - + configFile.getName()); - ok = false; - continue; - } - - ConfigurationSection section = config.getConfigurationSection(key); - if (section == null) { - Research.plugin.getLogger().severe("[Research] Input '" + key + "' has no data in " - + configFile.getName()); - ok = false; - continue; - } - - InputDef def = new InputDef(key, section); - ok &= validate(def, configFile); - map.put(key, def); - } - return ok; - } - - private boolean validateUniqueStartItems() { - Map seen = new LinkedHashMap<>(); - boolean ok = true; - for (InputDef def : map.values()) { - if (!def.requiresStartItem()) { - continue; - } - String normalized = ItemRef.normalize(def.getStartItemRef()); - if (normalized.isBlank()) { - continue; - } - String previous = seen.put(normalized, def.getId()); - if (previous != null && !previous.equals(def.getId())) { - Research.plugin.getLogger().severe("[Research] Start item '" + normalized - + "' registered on inputs '" + previous + "' and '" + def.getId() + "'."); - ok = false; - } - } - return ok; - } - - private boolean validate(InputDef def, File configFile) { - boolean ok = true; - if (!def.requiresStartItem()) { - Research.plugin.getLogger().severe("[Research] Input '" + def.getId() + "' missing start_item in " - + configFile.getName()); - ok = false; - } else { - ok &= validateItemRef(def.getId(), "start_item", def.getStartItemRef(), configFile); - if (def.getStartItemAmount() <= 0) { - Research.plugin.getLogger().severe("[Research] Input '" + def.getId() + "' start_item amount must be > 0 in " - + configFile.getName()); - ok = false; - } - } - - if (def.getOutputs().isEmpty()) { - Research.plugin.getLogger().severe("[Research] Input '" + def.getId() + "' has no outputs in " - + configFile.getName()); - ok = false; - } - - for (InputDef.WeightedOutput output : def.getOutputs()) { - if (output.getOutputId() == null || output.getOutputId().isBlank()) { - Research.plugin.getLogger().severe("[Research] Input '" + def.getId() + "' has output with blank id in " - + configFile.getName()); - ok = false; - continue; - } - if (output.getWeight() <= 0) { - Research.plugin.getLogger().severe("[Research] Input '" + def.getId() + "' has output '" - + output.getOutputId() + "' with weight <= 0 in " + configFile.getName()); - ok = false; - } - if (OutputLoader.getById(output.getOutputId()) == null) { - Research.plugin.getLogger().severe("[Research] Input '" + def.getId() + "' references unknown output '" - + output.getOutputId() + "' in " + configFile.getName()); - ok = false; - } - } - return ok; - } - - private boolean validateItemRef(String inputId, String field, String ref, File configFile) { - if (ref == null || ref.isBlank()) { - Research.plugin.getLogger().severe("[Research] Input '" + inputId + "' missing " + field - + " item in " + configFile.getName()); - return false; - } - if (!ItemRef.hasKnownPrefix(ref)) { - Research.plugin.getLogger().severe("[Research] Input '" + inputId + "' has invalid " + field - + " item '" + ref + "' in " + configFile.getName()); - return false; - } - if (!ItemRef.isValid(ref)) { - Research.plugin.getLogger().warning("[Research] Input '" + inputId + "' " + field - + " item could not be built: " + ref); - } - return true; - } -} +package net.tfminecraft.research.loader; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.YamlConfiguration; + +import net.tfminecraft.tlibs.interfaces.LoaderInterface; +import net.tfminecraft.research.Research; +import net.tfminecraft.research.model.InputDef; +import net.tfminecraft.research.util.ItemRef; +import net.tfminecraft.research.util.YamlFolder; + +public final class InputLoader implements LoaderInterface { + + private static final Map map = new LinkedHashMap<>(); + + public static Map get() { + return map; + } + + public static InputDef getById(String id) { + return map.get(id); + } + + @Override + public void load(File configFile) { + loadFile(configFile); + } + + public boolean loadFolder(File folder) { + map.clear(); + boolean ok = true; + List files = new ArrayList<>(YamlFolder.listYamlFiles(folder)); + files.sort(Comparator.comparing(file -> file.getName().toLowerCase())); + for (File file : files) { + ok &= loadFile(file); + } + ok &= validateUniqueStartItems(); + return ok; + } + + private boolean loadFile(File configFile) { + YamlConfiguration config = new YamlConfiguration(); + try { + config.load(configFile); + } catch (IOException | InvalidConfigurationException ex) { + Research.plugin.getLogger().severe("[Research] Failed to load input file " + configFile.getName() + + ": " + ex.getMessage()); + return false; + } + + boolean ok = true; + for (String key : config.getKeys(false)) { + if (map.containsKey(key)) { + Research.plugin.getLogger().severe("[Research] Duplicate input id '" + key + "' in " + + configFile.getName()); + ok = false; + continue; + } + + ConfigurationSection section = config.getConfigurationSection(key); + if (section == null) { + Research.plugin.getLogger().severe("[Research] Input '" + key + "' has no data in " + + configFile.getName()); + ok = false; + continue; + } + + InputDef def = new InputDef(key, section); + ok &= validate(def, configFile); + map.put(key, def); + } + return ok; + } + + private boolean validateUniqueStartItems() { + Map seen = new LinkedHashMap<>(); + boolean ok = true; + for (InputDef def : map.values()) { + if (!def.requiresStartItem()) { + continue; + } + String normalized = ItemRef.normalize(def.getStartItemRef()); + if (normalized.isBlank()) { + continue; + } + String previous = seen.put(normalized, def.getId()); + if (previous != null && !previous.equals(def.getId())) { + Research.plugin.getLogger().severe("[Research] Start item '" + normalized + + "' registered on inputs '" + previous + "' and '" + def.getId() + "'."); + ok = false; + } + } + return ok; + } + + private boolean validate(InputDef def, File configFile) { + boolean ok = true; + if (!def.requiresStartItem()) { + Research.plugin.getLogger().severe("[Research] Input '" + def.getId() + "' missing start_item in " + + configFile.getName()); + ok = false; + } else { + ok &= validateItemRef(def.getId(), "start_item", def.getStartItemRef(), configFile); + if (def.getStartItemAmount() <= 0) { + Research.plugin.getLogger().severe("[Research] Input '" + def.getId() + "' start_item amount must be > 0 in " + + configFile.getName()); + ok = false; + } + } + + if (def.getOutputs().isEmpty()) { + Research.plugin.getLogger().severe("[Research] Input '" + def.getId() + "' has no outputs in " + + configFile.getName()); + ok = false; + } + + for (InputDef.WeightedOutput output : def.getOutputs()) { + if (output.getOutputId() == null || output.getOutputId().isBlank()) { + Research.plugin.getLogger().severe("[Research] Input '" + def.getId() + "' has output with blank id in " + + configFile.getName()); + ok = false; + continue; + } + if (output.getWeight() <= 0) { + Research.plugin.getLogger().severe("[Research] Input '" + def.getId() + "' has output '" + + output.getOutputId() + "' with weight <= 0 in " + configFile.getName()); + ok = false; + } + if (OutputLoader.getById(output.getOutputId()) == null) { + Research.plugin.getLogger().severe("[Research] Input '" + def.getId() + "' references unknown output '" + + output.getOutputId() + "' in " + configFile.getName()); + ok = false; + } + } + return ok; + } + + private boolean validateItemRef(String inputId, String field, String ref, File configFile) { + if (ref == null || ref.isBlank()) { + Research.plugin.getLogger().severe("[Research] Input '" + inputId + "' missing " + field + + " item in " + configFile.getName()); + return false; + } + if (!ItemRef.hasKnownPrefix(ref)) { + Research.plugin.getLogger().severe("[Research] Input '" + inputId + "' has invalid " + field + + " item '" + ref + "' in " + configFile.getName()); + return false; + } + if (!ItemRef.isValid(ref)) { + Research.plugin.getLogger().warning("[Research] Input '" + inputId + "' " + field + + " item could not be built: " + ref); + } + return true; + } +} diff --git a/src/main/java/net/tfminecraft/research/loader/MessagesLoader.java b/src/main/java/net/tfminecraft/research/loader/MessagesLoader.java index 76abe7e..ce4f405 100644 --- a/src/main/java/net/tfminecraft/research/loader/MessagesLoader.java +++ b/src/main/java/net/tfminecraft/research/loader/MessagesLoader.java @@ -8,7 +8,7 @@ import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; -import me.Plugins.TLibs.Interface.LoaderInterface; +import net.tfminecraft.tlibs.interfaces.LoaderInterface; import net.tfminecraft.research.Messages; import net.tfminecraft.research.Research; diff --git a/src/main/java/net/tfminecraft/research/loader/OutputLoader.java b/src/main/java/net/tfminecraft/research/loader/OutputLoader.java index 396b4ea..a4ee99b 100644 --- a/src/main/java/net/tfminecraft/research/loader/OutputLoader.java +++ b/src/main/java/net/tfminecraft/research/loader/OutputLoader.java @@ -1,157 +1,157 @@ -package net.tfminecraft.research.loader; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import org.bukkit.configuration.ConfigurationSection; -import org.bukkit.configuration.InvalidConfigurationException; -import org.bukkit.configuration.file.YamlConfiguration; - -import me.Plugins.TLibs.Interface.LoaderInterface; -import net.tfminecraft.research.Research; -import net.tfminecraft.research.model.AspectRequirement; -import net.tfminecraft.research.model.OutputDef; -import net.tfminecraft.research.util.GridLayout; -import net.tfminecraft.research.util.ItemRef; -import net.tfminecraft.research.util.ResultRef; -import net.tfminecraft.research.util.YamlFolder; - -public final class OutputLoader implements LoaderInterface { - - private static final Map map = new LinkedHashMap<>(); - - public static Map get() { - return map; - } - - public static OutputDef getById(String id) { - return map.get(id); - } - - @Override - public void load(File configFile) { - loadFile(configFile); - } - - public boolean loadFolder(File folder) { - map.clear(); - boolean ok = true; - List files = new ArrayList<>(YamlFolder.listYamlFiles(folder)); - files.sort(Comparator.comparing(file -> file.getName().toLowerCase())); - for (File file : files) { - ok &= loadFile(file); - } - return ok; - } - - private boolean loadFile(File configFile) { - YamlConfiguration config = new YamlConfiguration(); - try { - config.load(configFile); - } catch (IOException | InvalidConfigurationException ex) { - Research.plugin.getLogger().severe("[Research] Failed to load output file " + configFile.getName() - + ": " + ex.getMessage()); - return false; - } - - boolean ok = true; - for (String key : config.getKeys(false)) { - if (map.containsKey(key)) { - Research.plugin.getLogger().severe("[Research] Duplicate output id '" + key + "' in " - + configFile.getName()); - ok = false; - continue; - } - - ConfigurationSection section = config.getConfigurationSection(key); - if (section == null) { - Research.plugin.getLogger().severe("[Research] Output '" + key + "' has no data in " - + configFile.getName()); - ok = false; - continue; - } - - OutputDef def = new OutputDef(key, section); - ok &= validate(def, configFile); - map.put(key, def); - } - return ok; - } - - private boolean validate(OutputDef def, File configFile) { - boolean ok = true; - ok &= validateItemRef(def.getId(), "mystery_display", def.getMysteryDisplayItemRef(), configFile); - ok &= validateItemRef(def.getId(), "product_display", def.getProductDisplayItemRef(), configFile); - - if (def.getAspects().size() > GridLayout.ASPECT_ROW_COUNT) { - Research.plugin.getLogger().severe("[Research] Output '" + def.getId() + "' has " - + def.getAspects().size() + " aspects but the station GUI supports " - + GridLayout.ASPECT_ROW_COUNT + " in " + configFile.getName()); - ok = false; - } - - for (Map.Entry entry : def.getAspects().entrySet()) { - String aspectId = entry.getKey(); - if (AspectLoader.getById(aspectId) == null) { - Research.plugin.getLogger().severe("[Research] Output '" + def.getId() + "' references unknown aspect '" - + aspectId + "' in " + configFile.getName()); - ok = false; - } - if (entry.getValue().getRequiredPoints() <= 0) { - Research.plugin.getLogger().severe("[Research] Output '" + def.getId() + "' aspect '" + aspectId - + "' must have required_points > 0 in " + configFile.getName()); - ok = false; - } - } - - ok &= validateResult(def, configFile); - return ok; - } - - private boolean validateResult(OutputDef def, File configFile) { - OutputDef.ResultSpec result = def.getResult(); - if (result.hasItem() && result.hasTemplate()) { - Research.plugin.getLogger().severe("[Research] Output '" + def.getId() - + "' result must specify item or template, not both in " + configFile.getName()); - return false; - } - if (!result.hasItem() && !result.hasTemplate()) { - Research.plugin.getLogger().severe("[Research] Output '" + def.getId() - + "' missing result.item or result.template in " + configFile.getName()); - return false; - } - if (result.hasTemplate()) { - String templateId = ResultRef.templateId(result.getTemplateRef()); - if (TemplateLoader.getById(templateId) == null) { - Research.plugin.getLogger().severe("[Research] Output '" + def.getId() + "' references unknown template '" - + templateId + "' in " + configFile.getName()); - return false; - } - return true; - } - return validateItemRef(def.getId(), "result", result.getItemRef(), configFile); - } - - private boolean validateItemRef(String outputId, String field, String ref, File configFile) { - if (ref == null || ref.isBlank()) { - Research.plugin.getLogger().severe("[Research] Output '" + outputId + "' missing " + field - + " item in " + configFile.getName()); - return false; - } - if (!ItemRef.hasKnownPrefix(ref)) { - Research.plugin.getLogger().severe("[Research] Output '" + outputId + "' has invalid " + field - + " item '" + ref + "' in " + configFile.getName()); - return false; - } - if (!ItemRef.isValid(ref)) { - Research.plugin.getLogger().warning("[Research] Output '" + outputId + "' " + field - + " item could not be built: " + ref); - } - return true; - } -} +package net.tfminecraft.research.loader; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.YamlConfiguration; + +import net.tfminecraft.tlibs.interfaces.LoaderInterface; +import net.tfminecraft.research.Research; +import net.tfminecraft.research.model.AspectRequirement; +import net.tfminecraft.research.model.OutputDef; +import net.tfminecraft.research.util.GridLayout; +import net.tfminecraft.research.util.ItemRef; +import net.tfminecraft.research.util.ResultRef; +import net.tfminecraft.research.util.YamlFolder; + +public final class OutputLoader implements LoaderInterface { + + private static final Map map = new LinkedHashMap<>(); + + public static Map get() { + return map; + } + + public static OutputDef getById(String id) { + return map.get(id); + } + + @Override + public void load(File configFile) { + loadFile(configFile); + } + + public boolean loadFolder(File folder) { + map.clear(); + boolean ok = true; + List files = new ArrayList<>(YamlFolder.listYamlFiles(folder)); + files.sort(Comparator.comparing(file -> file.getName().toLowerCase())); + for (File file : files) { + ok &= loadFile(file); + } + return ok; + } + + private boolean loadFile(File configFile) { + YamlConfiguration config = new YamlConfiguration(); + try { + config.load(configFile); + } catch (IOException | InvalidConfigurationException ex) { + Research.plugin.getLogger().severe("[Research] Failed to load output file " + configFile.getName() + + ": " + ex.getMessage()); + return false; + } + + boolean ok = true; + for (String key : config.getKeys(false)) { + if (map.containsKey(key)) { + Research.plugin.getLogger().severe("[Research] Duplicate output id '" + key + "' in " + + configFile.getName()); + ok = false; + continue; + } + + ConfigurationSection section = config.getConfigurationSection(key); + if (section == null) { + Research.plugin.getLogger().severe("[Research] Output '" + key + "' has no data in " + + configFile.getName()); + ok = false; + continue; + } + + OutputDef def = new OutputDef(key, section); + ok &= validate(def, configFile); + map.put(key, def); + } + return ok; + } + + private boolean validate(OutputDef def, File configFile) { + boolean ok = true; + ok &= validateItemRef(def.getId(), "mystery_display", def.getMysteryDisplayItemRef(), configFile); + ok &= validateItemRef(def.getId(), "product_display", def.getProductDisplayItemRef(), configFile); + + if (def.getAspects().size() > GridLayout.ASPECT_ROW_COUNT) { + Research.plugin.getLogger().severe("[Research] Output '" + def.getId() + "' has " + + def.getAspects().size() + " aspects but the station GUI supports " + + GridLayout.ASPECT_ROW_COUNT + " in " + configFile.getName()); + ok = false; + } + + for (Map.Entry entry : def.getAspects().entrySet()) { + String aspectId = entry.getKey(); + if (AspectLoader.getById(aspectId) == null) { + Research.plugin.getLogger().severe("[Research] Output '" + def.getId() + "' references unknown aspect '" + + aspectId + "' in " + configFile.getName()); + ok = false; + } + if (entry.getValue().getRequiredPoints() <= 0) { + Research.plugin.getLogger().severe("[Research] Output '" + def.getId() + "' aspect '" + aspectId + + "' must have required_points > 0 in " + configFile.getName()); + ok = false; + } + } + + ok &= validateResult(def, configFile); + return ok; + } + + private boolean validateResult(OutputDef def, File configFile) { + OutputDef.ResultSpec result = def.getResult(); + if (result.hasItem() && result.hasTemplate()) { + Research.plugin.getLogger().severe("[Research] Output '" + def.getId() + + "' result must specify item or template, not both in " + configFile.getName()); + return false; + } + if (!result.hasItem() && !result.hasTemplate()) { + Research.plugin.getLogger().severe("[Research] Output '" + def.getId() + + "' missing result.item or result.template in " + configFile.getName()); + return false; + } + if (result.hasTemplate()) { + String templateId = ResultRef.templateId(result.getTemplateRef()); + if (TemplateLoader.getById(templateId) == null) { + Research.plugin.getLogger().severe("[Research] Output '" + def.getId() + "' references unknown template '" + + templateId + "' in " + configFile.getName()); + return false; + } + return true; + } + return validateItemRef(def.getId(), "result", result.getItemRef(), configFile); + } + + private boolean validateItemRef(String outputId, String field, String ref, File configFile) { + if (ref == null || ref.isBlank()) { + Research.plugin.getLogger().severe("[Research] Output '" + outputId + "' missing " + field + + " item in " + configFile.getName()); + return false; + } + if (!ItemRef.hasKnownPrefix(ref)) { + Research.plugin.getLogger().severe("[Research] Output '" + outputId + "' has invalid " + field + + " item '" + ref + "' in " + configFile.getName()); + return false; + } + if (!ItemRef.isValid(ref)) { + Research.plugin.getLogger().warning("[Research] Output '" + outputId + "' " + field + + " item could not be built: " + ref); + } + return true; + } +} diff --git a/src/main/java/net/tfminecraft/research/loader/TemplateLoader.java b/src/main/java/net/tfminecraft/research/loader/TemplateLoader.java index 67852f9..b8e90d6 100644 --- a/src/main/java/net/tfminecraft/research/loader/TemplateLoader.java +++ b/src/main/java/net/tfminecraft/research/loader/TemplateLoader.java @@ -13,7 +13,7 @@ import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.YamlConfiguration; -import me.Plugins.TLibs.Interface.LoaderInterface; +import net.tfminecraft.tlibs.interfaces.LoaderInterface; import net.tfminecraft.research.Research; import net.tfminecraft.research.model.ResultTemplateDef; import net.tfminecraft.research.util.ItemRef; diff --git a/src/main/java/net/tfminecraft/research/model/ActiveProject.java b/src/main/java/net/tfminecraft/research/model/ActiveProject.java index a6e8282..5b43243 100644 --- a/src/main/java/net/tfminecraft/research/model/ActiveProject.java +++ b/src/main/java/net/tfminecraft/research/model/ActiveProject.java @@ -54,7 +54,7 @@ public String getResolvedResultRef() { } /** - * @deprecated Use {@link #getResolvedOutputId()} for batch 18+ output-centric model. + * @deprecated Use {@link #getResolvedOutputId()} for the output-centric model. */ @Deprecated public String getProjectId() { diff --git a/src/main/java/net/tfminecraft/research/model/AspectRequirement.java b/src/main/java/net/tfminecraft/research/model/AspectRequirement.java index da5fa5c..1c0ab77 100644 --- a/src/main/java/net/tfminecraft/research/model/AspectRequirement.java +++ b/src/main/java/net/tfminecraft/research/model/AspectRequirement.java @@ -1,16 +1,16 @@ -package net.tfminecraft.research.model; - -import org.bukkit.configuration.ConfigurationSection; - -public final class AspectRequirement { - - private final int requiredPoints; - - public AspectRequirement(ConfigurationSection config) { - this.requiredPoints = config != null ? config.getInt("required_points", 0) : 0; - } - - public int getRequiredPoints() { - return requiredPoints; - } -} +package net.tfminecraft.research.model; + +import org.bukkit.configuration.ConfigurationSection; + +public final class AspectRequirement { + + private final int requiredPoints; + + public AspectRequirement(ConfigurationSection config) { + this.requiredPoints = config != null ? config.getInt("required_points", 0) : 0; + } + + public int getRequiredPoints() { + return requiredPoints; + } +} diff --git a/src/main/java/net/tfminecraft/research/model/AspectSounds.java b/src/main/java/net/tfminecraft/research/model/AspectSounds.java index 09d8876..594bd78 100644 --- a/src/main/java/net/tfminecraft/research/model/AspectSounds.java +++ b/src/main/java/net/tfminecraft/research/model/AspectSounds.java @@ -1,41 +1,41 @@ -package net.tfminecraft.research.model; - -import org.bukkit.configuration.ConfigurationSection; - -public final class AspectSounds { - - private final String input; - private final String confirm; - private final float volume; - private final float pitch; - - public AspectSounds(ConfigurationSection config) { - if (config != null) { - this.input = config.getString("input", "entity.item.pickup"); - this.confirm = config.getString("confirm", "ENTITY_EXPERIENCE_ORB_PICKUP"); - this.volume = (float) config.getDouble("volume", 1.0); - this.pitch = (float) config.getDouble("pitch", 1.0); - } else { - this.input = "entity.item.pickup"; - this.confirm = "ENTITY_EXPERIENCE_ORB_PICKUP"; - this.volume = 1.0f; - this.pitch = 1.0f; - } - } - - public String getInput() { - return input; - } - - public String getConfirm() { - return confirm; - } - - public float getVolume() { - return volume; - } - - public float getPitch() { - return pitch; - } -} +package net.tfminecraft.research.model; + +import org.bukkit.configuration.ConfigurationSection; + +public final class AspectSounds { + + private final String input; + private final String confirm; + private final float volume; + private final float pitch; + + public AspectSounds(ConfigurationSection config) { + if (config != null) { + this.input = config.getString("input", "entity.item.pickup"); + this.confirm = config.getString("confirm", "ENTITY_EXPERIENCE_ORB_PICKUP"); + this.volume = (float) config.getDouble("volume", 1.0); + this.pitch = (float) config.getDouble("pitch", 1.0); + } else { + this.input = "entity.item.pickup"; + this.confirm = "ENTITY_EXPERIENCE_ORB_PICKUP"; + this.volume = 1.0f; + this.pitch = 1.0f; + } + } + + public String getInput() { + return input; + } + + public String getConfirm() { + return confirm; + } + + public float getVolume() { + return volume; + } + + public float getPitch() { + return pitch; + } +} diff --git a/src/main/java/net/tfminecraft/research/model/ExperimentMatch.java b/src/main/java/net/tfminecraft/research/model/ExperimentMatch.java index c04e950..dc9d320 100644 --- a/src/main/java/net/tfminecraft/research/model/ExperimentMatch.java +++ b/src/main/java/net/tfminecraft/research/model/ExperimentMatch.java @@ -1,47 +1,47 @@ -package net.tfminecraft.research.model; - -import net.tfminecraft.research.Cache; -import net.tfminecraft.research.loader.AspectLoader; - -public final class ExperimentMatch { - - private final String itemRef; - private final String primaryAspectId; - private final String secondaryAspectId; - - public ExperimentMatch(String itemRef, String primaryAspectId, String secondaryAspectId) { - this.itemRef = itemRef; - this.primaryAspectId = primaryAspectId; - this.secondaryAspectId = secondaryAspectId; - } - - public String getItemRef() { - return itemRef; - } - - public String getPrimaryAspect() { - return primaryAspectId; - } - - public int getPrimaryPoints() { - return primaryAspectId != null && !primaryAspectId.isBlank() ? Cache.experimentPrimaryPoints : 0; - } - - public String getSecondaryAspect() { - return secondaryAspectId; - } - - public int getSecondaryPoints() { - return secondaryAspectId != null && !secondaryAspectId.isBlank() ? Cache.experimentSecondaryPoints : 0; - } - - public AspectDef getSoundAspect() { - if (primaryAspectId != null && !primaryAspectId.isBlank()) { - return AspectLoader.getById(primaryAspectId); - } - if (secondaryAspectId != null && !secondaryAspectId.isBlank()) { - return AspectLoader.getById(secondaryAspectId); - } - return null; - } -} +package net.tfminecraft.research.model; + +import net.tfminecraft.research.Cache; +import net.tfminecraft.research.loader.AspectLoader; + +public final class ExperimentMatch { + + private final String itemRef; + private final String primaryAspectId; + private final String secondaryAspectId; + + public ExperimentMatch(String itemRef, String primaryAspectId, String secondaryAspectId) { + this.itemRef = itemRef; + this.primaryAspectId = primaryAspectId; + this.secondaryAspectId = secondaryAspectId; + } + + public String getItemRef() { + return itemRef; + } + + public String getPrimaryAspect() { + return primaryAspectId; + } + + public int getPrimaryPoints() { + return primaryAspectId != null && !primaryAspectId.isBlank() ? Cache.experimentPrimaryPoints : 0; + } + + public String getSecondaryAspect() { + return secondaryAspectId; + } + + public int getSecondaryPoints() { + return secondaryAspectId != null && !secondaryAspectId.isBlank() ? Cache.experimentSecondaryPoints : 0; + } + + public AspectDef getSoundAspect() { + if (primaryAspectId != null && !primaryAspectId.isBlank()) { + return AspectLoader.getById(primaryAspectId); + } + if (secondaryAspectId != null && !secondaryAspectId.isBlank()) { + return AspectLoader.getById(secondaryAspectId); + } + return null; + } +} diff --git a/src/main/java/net/tfminecraft/research/model/InputDef.java b/src/main/java/net/tfminecraft/research/model/InputDef.java index b3b8576..88c06b0 100644 --- a/src/main/java/net/tfminecraft/research/model/InputDef.java +++ b/src/main/java/net/tfminecraft/research/model/InputDef.java @@ -1,83 +1,83 @@ -package net.tfminecraft.research.model; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import org.bukkit.configuration.ConfigurationSection; - -public final class InputDef { - - private final String id; - private final boolean enabled; - private final String startItemRef; - private final int startItemAmount; - private final List outputs; - - public InputDef(String id, ConfigurationSection config) { - this.id = id; - this.enabled = config.getBoolean("enabled", true); - - ConfigurationSection startItem = config.getConfigurationSection("start_item"); - if (startItem != null) { - this.startItemRef = startItem.getString("item", ""); - this.startItemAmount = startItem.getInt("amount", 1); - } else { - this.startItemRef = ""; - this.startItemAmount = 1; - } - - this.outputs = new ArrayList<>(); - for (Map entry : config.getMapList("outputs")) { - String outputId = entry.get("id") != null ? String.valueOf(entry.get("id")) : ""; - double weight = 1.0; - if (entry.get("weight") instanceof Number number) { - weight = number.doubleValue(); - } - outputs.add(new WeightedOutput(outputId, weight)); - } - } - - public String getId() { - return id; - } - - public boolean isEnabled() { - return enabled; - } - - public String getStartItemRef() { - return startItemRef; - } - - public int getStartItemAmount() { - return startItemAmount; - } - - public boolean requiresStartItem() { - return startItemRef != null && !startItemRef.isBlank(); - } - - public List getOutputs() { - return Collections.unmodifiableList(outputs); - } - - public static final class WeightedOutput { - private final String outputId; - private final double weight; - - public WeightedOutput(String outputId, double weight) { - this.outputId = outputId; - this.weight = weight; - } - - public String getOutputId() { - return outputId; - } - - public double getWeight() { - return weight; - } - } -} +package net.tfminecraft.research.model; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.bukkit.configuration.ConfigurationSection; + +public final class InputDef { + + private final String id; + private final boolean enabled; + private final String startItemRef; + private final int startItemAmount; + private final List outputs; + + public InputDef(String id, ConfigurationSection config) { + this.id = id; + this.enabled = config.getBoolean("enabled", true); + + ConfigurationSection startItem = config.getConfigurationSection("start_item"); + if (startItem != null) { + this.startItemRef = startItem.getString("item", ""); + this.startItemAmount = startItem.getInt("amount", 1); + } else { + this.startItemRef = ""; + this.startItemAmount = 1; + } + + this.outputs = new ArrayList<>(); + for (Map entry : config.getMapList("outputs")) { + String outputId = entry.get("id") != null ? String.valueOf(entry.get("id")) : ""; + double weight = 1.0; + if (entry.get("weight") instanceof Number number) { + weight = number.doubleValue(); + } + outputs.add(new WeightedOutput(outputId, weight)); + } + } + + public String getId() { + return id; + } + + public boolean isEnabled() { + return enabled; + } + + public String getStartItemRef() { + return startItemRef; + } + + public int getStartItemAmount() { + return startItemAmount; + } + + public boolean requiresStartItem() { + return startItemRef != null && !startItemRef.isBlank(); + } + + public List getOutputs() { + return Collections.unmodifiableList(outputs); + } + + public static final class WeightedOutput { + private final String outputId; + private final double weight; + + public WeightedOutput(String outputId, double weight) { + this.outputId = outputId; + this.weight = weight; + } + + public String getOutputId() { + return outputId; + } + + public double getWeight() { + return weight; + } + } +} diff --git a/src/main/java/net/tfminecraft/research/model/OutputDef.java b/src/main/java/net/tfminecraft/research/model/OutputDef.java index 2d7a24a..69c734a 100644 --- a/src/main/java/net/tfminecraft/research/model/OutputDef.java +++ b/src/main/java/net/tfminecraft/research/model/OutputDef.java @@ -1,106 +1,106 @@ -package net.tfminecraft.research.model; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import org.bukkit.configuration.ConfigurationSection; - -public final class OutputDef { - - private final String id; - private final boolean enabled; - private final String mysteryDisplayItemRef; - private final String productDisplayItemRef; - private final int productRevealAfterConfirmedAspects; - private final Map aspects; - private final ResultSpec result; - - public OutputDef(String id, ConfigurationSection config) { - this.id = id; - this.enabled = config.getBoolean("enabled", true); - - ConfigurationSection mystery = config.getConfigurationSection("mystery_display"); - this.mysteryDisplayItemRef = mystery != null ? mystery.getString("item", "") : ""; - - ConfigurationSection product = config.getConfigurationSection("product_display"); - this.productDisplayItemRef = product != null ? product.getString("item", "") : ""; - - ConfigurationSection reveal = config.getConfigurationSection("product_reveal"); - this.productRevealAfterConfirmedAspects = reveal != null - ? reveal.getInt("base_after_confirmed_aspects", 0) - : 0; - - this.aspects = new LinkedHashMap<>(); - ConfigurationSection aspectsSection = config.getConfigurationSection("aspects"); - if (aspectsSection != null) { - for (String aspectId : aspectsSection.getKeys(false)) { - aspects.put(aspectId, new AspectRequirement(aspectsSection.getConfigurationSection(aspectId))); - } - } - - ConfigurationSection resultSection = config.getConfigurationSection("result"); - this.result = resultSection != null ? new ResultSpec(resultSection) : new ResultSpec(null); - } - - public String getId() { - return id; - } - - public boolean isEnabled() { - return enabled; - } - - public String getMysteryDisplayItemRef() { - return mysteryDisplayItemRef; - } - - public String getProductDisplayItemRef() { - return productDisplayItemRef; - } - - public int getProductRevealAfterConfirmedAspects() { - return productRevealAfterConfirmedAspects; - } - - public Map getAspects() { - return Collections.unmodifiableMap(aspects); - } - - public ResultSpec getResult() { - return result; - } - - public static final class ResultSpec { - private final String itemRef; - private final String templateRef; - - public ResultSpec(ConfigurationSection config) { - if (config != null) { - this.itemRef = config.getString("item", ""); - this.templateRef = config.getString("template", ""); - } else { - this.itemRef = ""; - this.templateRef = ""; - } - } - - public String getItemRef() { - return itemRef; - } - - public String getTemplateRef() { - return templateRef; - } - - public boolean hasItem() { - return itemRef != null && !itemRef.isBlank(); - } - - public boolean hasTemplate() { - return templateRef != null && !templateRef.isBlank(); - } - } -} +package net.tfminecraft.research.model; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.bukkit.configuration.ConfigurationSection; + +public final class OutputDef { + + private final String id; + private final boolean enabled; + private final String mysteryDisplayItemRef; + private final String productDisplayItemRef; + private final int productRevealAfterConfirmedAspects; + private final Map aspects; + private final ResultSpec result; + + public OutputDef(String id, ConfigurationSection config) { + this.id = id; + this.enabled = config.getBoolean("enabled", true); + + ConfigurationSection mystery = config.getConfigurationSection("mystery_display"); + this.mysteryDisplayItemRef = mystery != null ? mystery.getString("item", "") : ""; + + ConfigurationSection product = config.getConfigurationSection("product_display"); + this.productDisplayItemRef = product != null ? product.getString("item", "") : ""; + + ConfigurationSection reveal = config.getConfigurationSection("product_reveal"); + this.productRevealAfterConfirmedAspects = reveal != null + ? reveal.getInt("base_after_confirmed_aspects", 0) + : 0; + + this.aspects = new LinkedHashMap<>(); + ConfigurationSection aspectsSection = config.getConfigurationSection("aspects"); + if (aspectsSection != null) { + for (String aspectId : aspectsSection.getKeys(false)) { + aspects.put(aspectId, new AspectRequirement(aspectsSection.getConfigurationSection(aspectId))); + } + } + + ConfigurationSection resultSection = config.getConfigurationSection("result"); + this.result = resultSection != null ? new ResultSpec(resultSection) : new ResultSpec(null); + } + + public String getId() { + return id; + } + + public boolean isEnabled() { + return enabled; + } + + public String getMysteryDisplayItemRef() { + return mysteryDisplayItemRef; + } + + public String getProductDisplayItemRef() { + return productDisplayItemRef; + } + + public int getProductRevealAfterConfirmedAspects() { + return productRevealAfterConfirmedAspects; + } + + public Map getAspects() { + return Collections.unmodifiableMap(aspects); + } + + public ResultSpec getResult() { + return result; + } + + public static final class ResultSpec { + private final String itemRef; + private final String templateRef; + + public ResultSpec(ConfigurationSection config) { + if (config != null) { + this.itemRef = config.getString("item", ""); + this.templateRef = config.getString("template", ""); + } else { + this.itemRef = ""; + this.templateRef = ""; + } + } + + public String getItemRef() { + return itemRef; + } + + public String getTemplateRef() { + return templateRef; + } + + public boolean hasItem() { + return itemRef != null && !itemRef.isBlank(); + } + + public boolean hasTemplate() { + return templateRef != null && !templateRef.isBlank(); + } + } +} diff --git a/src/main/java/net/tfminecraft/research/registry/AspectItemRegistry.java b/src/main/java/net/tfminecraft/research/registry/AspectItemRegistry.java index b56c53b..7118235 100644 --- a/src/main/java/net/tfminecraft/research/registry/AspectItemRegistry.java +++ b/src/main/java/net/tfminecraft/research/registry/AspectItemRegistry.java @@ -1,103 +1,103 @@ -package net.tfminecraft.research.registry; - -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import org.bukkit.inventory.ItemStack; - -import net.tfminecraft.research.Research; -import net.tfminecraft.research.model.AspectDef; -import net.tfminecraft.research.model.ExperimentMatch; -import net.tfminecraft.research.util.ItemRef; - -public final class AspectItemRegistry { - - private static final Map primaryByRef = new LinkedHashMap<>(); - private static final Map secondaryByRef = new LinkedHashMap<>(); - private static List matches = Collections.emptyList(); - - private AspectItemRegistry() {} - - public static void rebuild(Map aspects) { - primaryByRef.clear(); - secondaryByRef.clear(); - - if (aspects == null || aspects.isEmpty()) { - matches = Collections.emptyList(); - return; - } - - for (AspectDef aspect : aspects.values()) { - registerPrimaryItems(aspect); - registerSecondaryItems(aspect); - } - - Map merged = new LinkedHashMap<>(); - for (Map.Entry entry : primaryByRef.entrySet()) { - merged.put(entry.getKey(), new ExperimentMatch(entry.getKey(), entry.getValue(), null)); - } - for (Map.Entry entry : secondaryByRef.entrySet()) { - String ref = entry.getKey(); - ExperimentMatch existing = merged.get(ref); - if (existing != null) { - merged.put(ref, new ExperimentMatch(ref, existing.getPrimaryAspect(), entry.getValue())); - } else { - merged.put(ref, new ExperimentMatch(ref, null, entry.getValue())); - } - } - matches = List.copyOf(merged.values()); - } - - public static ExperimentMatch findByItemStack(ItemStack stack) { - if (stack == null || stack.getType().isAir()) { - return null; - } - for (ExperimentMatch match : matches) { - if (ItemRef.matches(stack, match.getItemRef())) { - return match; - } - } - return null; - } - - private static void registerPrimaryItems(AspectDef aspect) { - for (String ref : aspect.getPrimaryItems()) { - String normalized = ItemRef.normalize(ref); - if (normalized.isBlank()) { - continue; - } - String previous = primaryByRef.get(normalized); - if (previous != null && !previous.equals(aspect.getId())) { - Research.plugin.getLogger().severe("[Research] Item '" + normalized - + "' registered as primary on aspects '" + previous + "' and '" + aspect.getId() - + "'; '" + aspect.getId() + "' wins."); - } - primaryByRef.put(normalized, aspect.getId()); - } - } - - private static void registerSecondaryItems(AspectDef aspect) { - for (String ref : aspect.getSecondaryItems()) { - String normalized = ItemRef.normalize(ref); - if (normalized.isBlank()) { - continue; - } - if (primaryByRef.containsKey(normalized) - && aspect.getId().equals(primaryByRef.get(normalized))) { - Research.plugin.getLogger().warning("[Research] Item '" + normalized - + "' listed as primary and secondary on aspect '" + aspect.getId() - + "'; primary wins."); - continue; - } - String previous = secondaryByRef.get(normalized); - if (previous != null && !previous.equals(aspect.getId())) { - Research.plugin.getLogger().severe("[Research] Item '" + normalized - + "' registered as secondary on aspects '" + previous + "' and '" + aspect.getId() - + "'; '" + aspect.getId() + "' wins."); - } - secondaryByRef.put(normalized, aspect.getId()); - } - } -} +package net.tfminecraft.research.registry; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.bukkit.inventory.ItemStack; + +import net.tfminecraft.research.Research; +import net.tfminecraft.research.model.AspectDef; +import net.tfminecraft.research.model.ExperimentMatch; +import net.tfminecraft.research.util.ItemRef; + +public final class AspectItemRegistry { + + private static final Map primaryByRef = new LinkedHashMap<>(); + private static final Map secondaryByRef = new LinkedHashMap<>(); + private static List matches = Collections.emptyList(); + + private AspectItemRegistry() {} + + public static void rebuild(Map aspects) { + primaryByRef.clear(); + secondaryByRef.clear(); + + if (aspects == null || aspects.isEmpty()) { + matches = Collections.emptyList(); + return; + } + + for (AspectDef aspect : aspects.values()) { + registerPrimaryItems(aspect); + registerSecondaryItems(aspect); + } + + Map merged = new LinkedHashMap<>(); + for (Map.Entry entry : primaryByRef.entrySet()) { + merged.put(entry.getKey(), new ExperimentMatch(entry.getKey(), entry.getValue(), null)); + } + for (Map.Entry entry : secondaryByRef.entrySet()) { + String ref = entry.getKey(); + ExperimentMatch existing = merged.get(ref); + if (existing != null) { + merged.put(ref, new ExperimentMatch(ref, existing.getPrimaryAspect(), entry.getValue())); + } else { + merged.put(ref, new ExperimentMatch(ref, null, entry.getValue())); + } + } + matches = List.copyOf(merged.values()); + } + + public static ExperimentMatch findByItemStack(ItemStack stack) { + if (stack == null || stack.getType().isAir()) { + return null; + } + for (ExperimentMatch match : matches) { + if (ItemRef.matches(stack, match.getItemRef())) { + return match; + } + } + return null; + } + + private static void registerPrimaryItems(AspectDef aspect) { + for (String ref : aspect.getPrimaryItems()) { + String normalized = ItemRef.normalize(ref); + if (normalized.isBlank()) { + continue; + } + String previous = primaryByRef.get(normalized); + if (previous != null && !previous.equals(aspect.getId())) { + Research.plugin.getLogger().severe("[Research] Item '" + normalized + + "' registered as primary on aspects '" + previous + "' and '" + aspect.getId() + + "'; '" + aspect.getId() + "' wins."); + } + primaryByRef.put(normalized, aspect.getId()); + } + } + + private static void registerSecondaryItems(AspectDef aspect) { + for (String ref : aspect.getSecondaryItems()) { + String normalized = ItemRef.normalize(ref); + if (normalized.isBlank()) { + continue; + } + if (primaryByRef.containsKey(normalized) + && aspect.getId().equals(primaryByRef.get(normalized))) { + Research.plugin.getLogger().warning("[Research] Item '" + normalized + + "' listed as primary and secondary on aspect '" + aspect.getId() + + "'; primary wins."); + continue; + } + String previous = secondaryByRef.get(normalized); + if (previous != null && !previous.equals(aspect.getId())) { + Research.plugin.getLogger().severe("[Research] Item '" + normalized + + "' registered as secondary on aspects '" + previous + "' and '" + aspect.getId() + + "'; '" + aspect.getId() + "' wins."); + } + secondaryByRef.put(normalized, aspect.getId()); + } + } +} diff --git a/src/main/java/net/tfminecraft/research/util/AspectSlotOrder.java b/src/main/java/net/tfminecraft/research/util/AspectSlotOrder.java index 9de1a49..158d323 100644 --- a/src/main/java/net/tfminecraft/research/util/AspectSlotOrder.java +++ b/src/main/java/net/tfminecraft/research/util/AspectSlotOrder.java @@ -1,101 +1,101 @@ -package net.tfminecraft.research.util; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.ThreadLocalRandom; - -import net.tfminecraft.research.Research; -import net.tfminecraft.research.model.OutputDef; - -public final class AspectSlotOrder { - - private AspectSlotOrder() {} - - public static List buildRowAssignments(OutputDef def) { - List rows = new ArrayList<>(); - for (int i = 0; i < GridLayout.ASPECT_ROW_COUNT; i++) { - rows.add(""); - } - if (def == null || def.getAspects().isEmpty()) { - return rows; - } - - List aspectIds = new ArrayList<>(def.getAspects().keySet()); - Collections.shuffle(aspectIds, ThreadLocalRandom.current()); - - List rowIndices = new ArrayList<>(); - for (int i = 0; i < GridLayout.ASPECT_ROW_COUNT; i++) { - rowIndices.add(i); - } - Collections.shuffle(rowIndices, ThreadLocalRandom.current()); - - for (int i = 0; i < aspectIds.size() && i < rowIndices.size(); i++) { - rows.set(rowIndices.get(i), aspectIds.get(i)); - } - return rows; - } - - /** - * Returns six row assignments (index 0 = top row). Empty string = decoy row. - */ - public static List reconcileRowAssignments(OutputDef def, List saved, String context) { - if (def == null) { - return emptyRows(); - } - if (saved == null || saved.isEmpty()) { - Research.plugin.getLogger().info("[Research] Assigned shuffled aspect rows for " + context - + " (no saved order)."); - return buildRowAssignments(def); - } - if (saved.size() != GridLayout.ASPECT_ROW_COUNT || !isValidRowAssignments(def, saved)) { - Research.plugin.getLogger().warning("[Research] Aspect row assignments for " + context - + " were missing or stale; reshuffling."); - return buildRowAssignments(def); - } - return new ArrayList<>(saved); - } - - public static String aspectIdForRow(List rowAssignments, int rowIndex) { - if (rowAssignments == null || rowIndex < 0 || rowIndex >= rowAssignments.size()) { - return ""; - } - String aspectId = rowAssignments.get(rowIndex); - return aspectId != null ? aspectId : ""; - } - - public static boolean isDecoyRow(List rowAssignments, int rowIndex) { - return aspectIdForRow(rowAssignments, rowIndex).isBlank(); - } - - private static boolean isValidRowAssignments(OutputDef def, List saved) { - List expected = new ArrayList<>(def.getAspects().keySet()); - List found = new ArrayList<>(); - for (String entry : saved) { - if (entry == null || entry.isBlank()) { - continue; - } - if (!def.getAspects().containsKey(entry) || found.contains(entry)) { - return false; - } - found.add(entry); - } - if (found.size() != expected.size()) { - return false; - } - for (String aspectId : expected) { - if (!found.contains(aspectId)) { - return false; - } - } - return true; - } - - private static List emptyRows() { - List rows = new ArrayList<>(); - for (int i = 0; i < GridLayout.ASPECT_ROW_COUNT; i++) { - rows.add(""); - } - return rows; - } -} +package net.tfminecraft.research.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +import net.tfminecraft.research.Research; +import net.tfminecraft.research.model.OutputDef; + +public final class AspectSlotOrder { + + private AspectSlotOrder() {} + + public static List buildRowAssignments(OutputDef def) { + List rows = new ArrayList<>(); + for (int i = 0; i < GridLayout.ASPECT_ROW_COUNT; i++) { + rows.add(""); + } + if (def == null || def.getAspects().isEmpty()) { + return rows; + } + + List aspectIds = new ArrayList<>(def.getAspects().keySet()); + Collections.shuffle(aspectIds, ThreadLocalRandom.current()); + + List rowIndices = new ArrayList<>(); + for (int i = 0; i < GridLayout.ASPECT_ROW_COUNT; i++) { + rowIndices.add(i); + } + Collections.shuffle(rowIndices, ThreadLocalRandom.current()); + + for (int i = 0; i < aspectIds.size() && i < rowIndices.size(); i++) { + rows.set(rowIndices.get(i), aspectIds.get(i)); + } + return rows; + } + + /** + * Returns six row assignments (index 0 = top row). Empty string = decoy row. + */ + public static List reconcileRowAssignments(OutputDef def, List saved, String context) { + if (def == null) { + return emptyRows(); + } + if (saved == null || saved.isEmpty()) { + Research.plugin.getLogger().info("[Research] Assigned shuffled aspect rows for " + context + + " (no saved order)."); + return buildRowAssignments(def); + } + if (saved.size() != GridLayout.ASPECT_ROW_COUNT || !isValidRowAssignments(def, saved)) { + Research.plugin.getLogger().warning("[Research] Aspect row assignments for " + context + + " were missing or stale; reshuffling."); + return buildRowAssignments(def); + } + return new ArrayList<>(saved); + } + + public static String aspectIdForRow(List rowAssignments, int rowIndex) { + if (rowAssignments == null || rowIndex < 0 || rowIndex >= rowAssignments.size()) { + return ""; + } + String aspectId = rowAssignments.get(rowIndex); + return aspectId != null ? aspectId : ""; + } + + public static boolean isDecoyRow(List rowAssignments, int rowIndex) { + return aspectIdForRow(rowAssignments, rowIndex).isBlank(); + } + + private static boolean isValidRowAssignments(OutputDef def, List saved) { + List expected = new ArrayList<>(def.getAspects().keySet()); + List found = new ArrayList<>(); + for (String entry : saved) { + if (entry == null || entry.isBlank()) { + continue; + } + if (!def.getAspects().containsKey(entry) || found.contains(entry)) { + return false; + } + found.add(entry); + } + if (found.size() != expected.size()) { + return false; + } + for (String aspectId : expected) { + if (!found.contains(aspectId)) { + return false; + } + } + return true; + } + + private static List emptyRows() { + List rows = new ArrayList<>(); + for (int i = 0; i < GridLayout.ASPECT_ROW_COUNT; i++) { + rows.add(""); + } + return rows; + } +} diff --git a/src/main/java/net/tfminecraft/research/util/GuiText.java b/src/main/java/net/tfminecraft/research/util/GuiText.java index cb0aeec..04996aa 100644 --- a/src/main/java/net/tfminecraft/research/util/GuiText.java +++ b/src/main/java/net/tfminecraft/research/util/GuiText.java @@ -1,80 +1,80 @@ -package net.tfminecraft.research.util; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import org.bukkit.ChatColor; - -import me.Plugins.TLibs.Objects.API.SubAPI.StringFormatter; -import net.tfminecraft.research.GuiCache; -import net.tfminecraft.research.Messages; - -/** - * GUI text styling via {@code gui.yml} hex tokens and TLibs {@link StringFormatter#formatHex}. - * Chat messages use {@code messages.yml}; those strings are formatted in {@link Messages}. - */ -public final class GuiText { - - private static final String FALLBACK_COLOR = "#ffffff"; - - private GuiText() {} - - public static String format(String template) { - return StringFormatter.formatHex(template != null ? template : ""); - } - - public static String color(String key) { - return GuiCache.color(key, FALLBACK_COLOR); - } - - public static String text(String colorKey, String plain) { - return format(color(colorKey) + (plain != null ? plain : "")); - } - - public static String label(String configured) { - return format(configured != null ? configured : ""); - } - - public static String guiMessage(String key, Map placeholders) { - return Messages.formatBody(key, placeholders); - } - - public static List formatLoreLines(List lines) { - if (lines == null || lines.isEmpty()) { - return List.of(); - } - List out = new ArrayList<>(lines.size()); - for (String line : lines) { - out.add(format(line != null ? line : "")); - } - return out; - } - - public static String undiscoveredAspectName() { - if (GuiCache.undiscoveredAspectName != null && !GuiCache.undiscoveredAspectName.isBlank()) { - return format(GuiCache.undiscoveredAspectName); - } - return text("progress_unknown", "???"); - } - - public static List undiscoveredAspectLore() { - return formatLoreLines(GuiCache.undiscoveredAspectLore); - } - - public static String aspectName(AspectNameStyle style, String rawName) { - String coloured = format(rawName != null ? rawName : ""); - String plain = ChatColor.stripColor(coloured); - return switch (style) { - case CONFIRMED -> text("aspect_confirmed", plain); - case HIDDEN -> text("aspect_hidden", plain); - case PLAIN -> coloured; - }; - } - - public enum AspectNameStyle { - CONFIRMED, - HIDDEN, - PLAIN - } -} +package net.tfminecraft.research.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.bukkit.ChatColor; + +import net.tfminecraft.tlibs.objects.api.subapi.StringFormatter; +import net.tfminecraft.research.GuiCache; +import net.tfminecraft.research.Messages; + +/** + * GUI text styling via {@code gui.yml} hex tokens and TLibs {@link StringFormatter#formatHex}. + * Chat messages use {@code messages.yml}; those strings are formatted in {@link Messages}. + */ +public final class GuiText { + + private static final String FALLBACK_COLOR = "#ffffff"; + + private GuiText() {} + + public static String format(String template) { + return StringFormatter.formatHex(template != null ? template : ""); + } + + public static String color(String key) { + return GuiCache.color(key, FALLBACK_COLOR); + } + + public static String text(String colorKey, String plain) { + return format(color(colorKey) + (plain != null ? plain : "")); + } + + public static String label(String configured) { + return format(configured != null ? configured : ""); + } + + public static String guiMessage(String key, Map placeholders) { + return Messages.formatBody(key, placeholders); + } + + public static List formatLoreLines(List lines) { + if (lines == null || lines.isEmpty()) { + return List.of(); + } + List out = new ArrayList<>(lines.size()); + for (String line : lines) { + out.add(format(line != null ? line : "")); + } + return out; + } + + public static String undiscoveredAspectName() { + if (GuiCache.undiscoveredAspectName != null && !GuiCache.undiscoveredAspectName.isBlank()) { + return format(GuiCache.undiscoveredAspectName); + } + return text("progress_unknown", "???"); + } + + public static List undiscoveredAspectLore() { + return formatLoreLines(GuiCache.undiscoveredAspectLore); + } + + public static String aspectName(AspectNameStyle style, String rawName) { + String coloured = format(rawName != null ? rawName : ""); + String plain = ChatColor.stripColor(coloured); + return switch (style) { + case CONFIRMED -> text("aspect_confirmed", plain); + case HIDDEN -> text("aspect_hidden", plain); + case PLAIN -> coloured; + }; + } + + public enum AspectNameStyle { + CONFIRMED, + HIDDEN, + PLAIN + } +} diff --git a/src/main/java/net/tfminecraft/research/util/InputMatcher.java b/src/main/java/net/tfminecraft/research/util/InputMatcher.java index f04b071..2a40e95 100644 --- a/src/main/java/net/tfminecraft/research/util/InputMatcher.java +++ b/src/main/java/net/tfminecraft/research/util/InputMatcher.java @@ -1,26 +1,26 @@ -package net.tfminecraft.research.util; - -import org.bukkit.inventory.ItemStack; - -import net.tfminecraft.research.loader.InputLoader; -import net.tfminecraft.research.model.InputDef; - -public final class InputMatcher { - - private InputMatcher() {} - - public static InputDef findByStartItem(ItemStack held) { - if (held == null || held.getType().isAir()) { - return null; - } - for (InputDef input : InputLoader.get().values()) { - if (!input.isEnabled() || !input.requiresStartItem()) { - continue; - } - if (ItemRef.matches(held, input.getStartItemRef())) { - return input; - } - } - return null; - } -} +package net.tfminecraft.research.util; + +import org.bukkit.inventory.ItemStack; + +import net.tfminecraft.research.loader.InputLoader; +import net.tfminecraft.research.model.InputDef; + +public final class InputMatcher { + + private InputMatcher() {} + + public static InputDef findByStartItem(ItemStack held) { + if (held == null || held.getType().isAir()) { + return null; + } + for (InputDef input : InputLoader.get().values()) { + if (!input.isEnabled() || !input.requiresStartItem()) { + continue; + } + if (ItemRef.matches(held, input.getStartItemRef())) { + return input; + } + } + return null; + } +} diff --git a/src/main/java/net/tfminecraft/research/util/ItemRef.java b/src/main/java/net/tfminecraft/research/util/ItemRef.java index 439259e..240efb1 100644 --- a/src/main/java/net/tfminecraft/research/util/ItemRef.java +++ b/src/main/java/net/tfminecraft/research/util/ItemRef.java @@ -7,7 +7,7 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; -import me.Plugins.TLibs.TLibs; +import net.tfminecraft.tlibs.TLibs; /** * Item references in yaml use vanilla.MATERIAL, m.TYPE.ID, or ia.namespace:id. diff --git a/src/main/java/net/tfminecraft/research/util/OutputPicker.java b/src/main/java/net/tfminecraft/research/util/OutputPicker.java index baf4f79..2abf8ab 100644 --- a/src/main/java/net/tfminecraft/research/util/OutputPicker.java +++ b/src/main/java/net/tfminecraft/research/util/OutputPicker.java @@ -1,41 +1,41 @@ -package net.tfminecraft.research.util; - -import java.util.List; -import java.util.concurrent.ThreadLocalRandom; - -import net.tfminecraft.research.model.InputDef; - -public final class OutputPicker { - - private OutputPicker() {} - - public static String roll(InputDef input) { - if (input == null) { - return null; - } - InputDef.WeightedOutput chosen = pickWeighted(input.getOutputs()); - return chosen != null ? chosen.getOutputId() : null; - } - - private static InputDef.WeightedOutput pickWeighted(List outputs) { - if (outputs == null || outputs.isEmpty()) { - return null; - } - double total = 0.0; - for (InputDef.WeightedOutput output : outputs) { - total += output.getWeight(); - } - if (total <= 0.0) { - return outputs.get(0); - } - double roll = ThreadLocalRandom.current().nextDouble(total); - double cumulative = 0.0; - for (InputDef.WeightedOutput output : outputs) { - cumulative += output.getWeight(); - if (roll < cumulative) { - return output; - } - } - return outputs.get(outputs.size() - 1); - } -} +package net.tfminecraft.research.util; + +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +import net.tfminecraft.research.model.InputDef; + +public final class OutputPicker { + + private OutputPicker() {} + + public static String roll(InputDef input) { + if (input == null) { + return null; + } + InputDef.WeightedOutput chosen = pickWeighted(input.getOutputs()); + return chosen != null ? chosen.getOutputId() : null; + } + + private static InputDef.WeightedOutput pickWeighted(List outputs) { + if (outputs == null || outputs.isEmpty()) { + return null; + } + double total = 0.0; + for (InputDef.WeightedOutput output : outputs) { + total += output.getWeight(); + } + if (total <= 0.0) { + return outputs.get(0); + } + double roll = ThreadLocalRandom.current().nextDouble(total); + double cumulative = 0.0; + for (InputDef.WeightedOutput output : outputs) { + cumulative += output.getWeight(); + if (roll < cumulative) { + return output; + } + } + return outputs.get(outputs.size() - 1); + } +} diff --git a/src/main/java/net/tfminecraft/research/util/PlayerInventoryUtil.java b/src/main/java/net/tfminecraft/research/util/PlayerInventoryUtil.java index 6b80932..0eae885 100644 --- a/src/main/java/net/tfminecraft/research/util/PlayerInventoryUtil.java +++ b/src/main/java/net/tfminecraft/research/util/PlayerInventoryUtil.java @@ -1,54 +1,54 @@ -package net.tfminecraft.research.util; - -import org.bukkit.entity.Player; -import org.bukkit.inventory.EquipmentSlot; -import org.bukkit.inventory.ItemStack; - -public final class PlayerInventoryUtil { - - private PlayerInventoryUtil() {} - - public static ItemStack getStackInHand(Player player, EquipmentSlot hand) { - if (player == null) { - return null; - } - if (hand == EquipmentSlot.OFF_HAND) { - return player.getInventory().getItemInOffHand(); - } - return player.getInventory().getItemInMainHand(); - } - - public static void setStackInHand(Player player, EquipmentSlot hand, ItemStack stack) { - if (player == null) { - return; - } - if (hand == EquipmentSlot.OFF_HAND) { - player.getInventory().setItemInOffHand(stack); - } else { - player.getInventory().setItemInMainHand(stack); - } - } - - /** - * Consumes the start item from the hand used to open the station, not from other inventory slots. - */ - public static boolean consumeFromHand(Player player, EquipmentSlot hand, String itemRef, int amount) { - if (player == null || amount <= 0 || itemRef == null || itemRef.isBlank()) { - return amount <= 0; - } - ItemStack stack = getStackInHand(player, hand); - if (stack == null || stack.getType().isAir() || !ItemRef.matches(stack, itemRef)) { - return false; - } - if (stack.getAmount() < amount) { - return false; - } - stack.setAmount(stack.getAmount() - amount); - if (stack.getAmount() <= 0) { - setStackInHand(player, hand, null); - } else { - setStackInHand(player, hand, stack); - } - return true; - } -} +package net.tfminecraft.research.util; + +import org.bukkit.entity.Player; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.ItemStack; + +public final class PlayerInventoryUtil { + + private PlayerInventoryUtil() {} + + public static ItemStack getStackInHand(Player player, EquipmentSlot hand) { + if (player == null) { + return null; + } + if (hand == EquipmentSlot.OFF_HAND) { + return player.getInventory().getItemInOffHand(); + } + return player.getInventory().getItemInMainHand(); + } + + public static void setStackInHand(Player player, EquipmentSlot hand, ItemStack stack) { + if (player == null) { + return; + } + if (hand == EquipmentSlot.OFF_HAND) { + player.getInventory().setItemInOffHand(stack); + } else { + player.getInventory().setItemInMainHand(stack); + } + } + + /** + * Consumes the start item from the hand used to open the station, not from other inventory slots. + */ + public static boolean consumeFromHand(Player player, EquipmentSlot hand, String itemRef, int amount) { + if (player == null || amount <= 0 || itemRef == null || itemRef.isBlank()) { + return amount <= 0; + } + ItemStack stack = getStackInHand(player, hand); + if (stack == null || stack.getType().isAir() || !ItemRef.matches(stack, itemRef)) { + return false; + } + if (stack.getAmount() < amount) { + return false; + } + stack.setAmount(stack.getAmount() - amount); + if (stack.getAmount() <= 0) { + setStackInHand(player, hand, null); + } else { + setStackInHand(player, hand, stack); + } + return true; + } +} diff --git a/src/main/java/net/tfminecraft/research/util/ResultResolver.java b/src/main/java/net/tfminecraft/research/util/ResultResolver.java index 97aa9f6..6c3a2ee 100644 --- a/src/main/java/net/tfminecraft/research/util/ResultResolver.java +++ b/src/main/java/net/tfminecraft/research/util/ResultResolver.java @@ -1,70 +1,70 @@ -package net.tfminecraft.research.util; - -import java.util.List; -import java.util.concurrent.ThreadLocalRandom; - -import net.tfminecraft.research.loader.TemplateLoader; -import net.tfminecraft.research.model.OutputDef; -import net.tfminecraft.research.model.ResultTemplateDef; - -public final class ResultResolver { - - private ResultResolver() {} - - public static String resolve(OutputDef.ResultSpec result) { - if (result == null) { - return null; - } - if (result.hasItem()) { - return result.getItemRef().trim(); - } - if (result.hasTemplate()) { - return resolveToItemRef(result.getTemplateRef()); - } - return null; - } - - private static String resolveToItemRef(String ref) { - if (ref == null || ref.isBlank()) { - return null; - } - if (ResultRef.isTemplateRef(ref)) { - ResultTemplateDef template = TemplateLoader.getById(ResultRef.templateId(ref)); - if (template == null || template.getOutputs().isEmpty()) { - return null; - } - ResultTemplateDef.WeightedOutput chosen = pickWeightedTemplate(template.getOutputs()); - if (chosen == null) { - return null; - } - return resolveToItemRef(chosen.getRef()); - } - if (ResultRef.isItemRef(ref)) { - return ref; - } - return null; - } - - private static ResultTemplateDef.WeightedOutput pickWeightedTemplate( - List outputs) { - if (outputs == null || outputs.isEmpty()) { - return null; - } - double total = 0.0; - for (ResultTemplateDef.WeightedOutput output : outputs) { - total += output.getWeight(); - } - if (total <= 0.0) { - return outputs.get(0); - } - double roll = ThreadLocalRandom.current().nextDouble(total); - double cumulative = 0.0; - for (ResultTemplateDef.WeightedOutput output : outputs) { - cumulative += output.getWeight(); - if (roll < cumulative) { - return output; - } - } - return outputs.get(outputs.size() - 1); - } -} +package net.tfminecraft.research.util; + +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +import net.tfminecraft.research.loader.TemplateLoader; +import net.tfminecraft.research.model.OutputDef; +import net.tfminecraft.research.model.ResultTemplateDef; + +public final class ResultResolver { + + private ResultResolver() {} + + public static String resolve(OutputDef.ResultSpec result) { + if (result == null) { + return null; + } + if (result.hasItem()) { + return result.getItemRef().trim(); + } + if (result.hasTemplate()) { + return resolveToItemRef(result.getTemplateRef()); + } + return null; + } + + private static String resolveToItemRef(String ref) { + if (ref == null || ref.isBlank()) { + return null; + } + if (ResultRef.isTemplateRef(ref)) { + ResultTemplateDef template = TemplateLoader.getById(ResultRef.templateId(ref)); + if (template == null || template.getOutputs().isEmpty()) { + return null; + } + ResultTemplateDef.WeightedOutput chosen = pickWeightedTemplate(template.getOutputs()); + if (chosen == null) { + return null; + } + return resolveToItemRef(chosen.getRef()); + } + if (ResultRef.isItemRef(ref)) { + return ref; + } + return null; + } + + private static ResultTemplateDef.WeightedOutput pickWeightedTemplate( + List outputs) { + if (outputs == null || outputs.isEmpty()) { + return null; + } + double total = 0.0; + for (ResultTemplateDef.WeightedOutput output : outputs) { + total += output.getWeight(); + } + if (total <= 0.0) { + return outputs.get(0); + } + double roll = ThreadLocalRandom.current().nextDouble(total); + double cumulative = 0.0; + for (ResultTemplateDef.WeightedOutput output : outputs) { + cumulative += output.getWeight(); + if (roll < cumulative) { + return output; + } + } + return outputs.get(outputs.size() - 1); + } +} diff --git a/src/main/java/net/tfminecraft/research/util/SoundKeys.java b/src/main/java/net/tfminecraft/research/util/SoundKeys.java index 7e2b13e..dd4415b 100644 --- a/src/main/java/net/tfminecraft/research/util/SoundKeys.java +++ b/src/main/java/net/tfminecraft/research/util/SoundKeys.java @@ -1,52 +1,52 @@ -package net.tfminecraft.research.util; - -import org.bukkit.entity.Player; - -import net.tfminecraft.research.Research; - -/** - * Normalizes Bukkit enum-style sound names and custom namespace keys for Paper 1.21+. - */ -public final class SoundKeys { - - private SoundKeys() {} - - public static String normalize(String raw) { - if (raw == null || raw.isBlank()) { - return raw; - } - String trimmed = raw.trim(); - int colon = trimmed.indexOf(':'); - String namespace; - String path; - if (colon >= 0) { - namespace = trimmed.substring(0, colon).trim().toLowerCase(); - path = trimmed.substring(colon + 1).trim(); - } else { - namespace = "minecraft"; - path = trimmed; - } - if (looksLikeBukkitEnum(path)) { - path = path.toLowerCase().replace('_', '.'); - } else { - path = path.toLowerCase(); - } - return namespace + ":" + path; - } - - private static boolean looksLikeBukkitEnum(String path) { - return path.equals(path.toUpperCase()) && path.contains("_"); - } - - public static void play(Player player, String soundKey, float volume, float pitch) { - if (player == null || soundKey == null || soundKey.isBlank()) { - return; - } - try { - player.playSound(player.getLocation(), normalize(soundKey), volume, pitch); - } catch (RuntimeException ex) { - Research.plugin.getLogger().warning("[Research] Failed to play sound '" + soundKey + "': " - + ex.getMessage()); - } - } -} +package net.tfminecraft.research.util; + +import org.bukkit.entity.Player; + +import net.tfminecraft.research.Research; + +/** + * Normalizes Bukkit enum-style sound names and custom namespace keys for Paper 1.21+. + */ +public final class SoundKeys { + + private SoundKeys() {} + + public static String normalize(String raw) { + if (raw == null || raw.isBlank()) { + return raw; + } + String trimmed = raw.trim(); + int colon = trimmed.indexOf(':'); + String namespace; + String path; + if (colon >= 0) { + namespace = trimmed.substring(0, colon).trim().toLowerCase(); + path = trimmed.substring(colon + 1).trim(); + } else { + namespace = "minecraft"; + path = trimmed; + } + if (looksLikeBukkitEnum(path)) { + path = path.toLowerCase().replace('_', '.'); + } else { + path = path.toLowerCase(); + } + return namespace + ":" + path; + } + + private static boolean looksLikeBukkitEnum(String path) { + return path.equals(path.toUpperCase()) && path.contains("_"); + } + + public static void play(Player player, String soundKey, float volume, float pitch) { + if (player == null || soundKey == null || soundKey.isBlank()) { + return; + } + try { + player.playSound(player.getLocation(), normalize(soundKey), volume, pitch); + } catch (RuntimeException ex) { + Research.plugin.getLogger().warning("[Research] Failed to play sound '" + soundKey + "': " + + ex.getMessage()); + } + } +} diff --git a/src/main/java/net/tfminecraft/research/util/StationCompleteEffects.java b/src/main/java/net/tfminecraft/research/util/StationCompleteEffects.java index ba4139b..0c98ebf 100644 --- a/src/main/java/net/tfminecraft/research/util/StationCompleteEffects.java +++ b/src/main/java/net/tfminecraft/research/util/StationCompleteEffects.java @@ -1,70 +1,70 @@ -package net.tfminecraft.research.util; - -import org.bukkit.Location; -import org.bukkit.Particle; -import org.bukkit.entity.Player; -import org.bukkit.scheduler.BukkitRunnable; - -import net.tfminecraft.research.Cache; -import net.tfminecraft.research.Research; - -public final class StationCompleteEffects { - - private StationCompleteEffects() {} - - public static void play(Player player, Location blockLocation) { - if (player == null || blockLocation == null || blockLocation.getWorld() == null) { - return; - } - - Location center = blockLocation.clone().add(0.5, 1.0, 0.5); - SoundKeys.play(player, Cache.stationCompleteSound, Cache.stationCompleteSoundVolume, - Cache.stationCompleteSoundPitch); - spawnBurst(center, Cache.stationCompleteParticle, Cache.stationCompleteParticleCount, - Cache.stationCompleteParticleRadius); - - if (Cache.stationCompleteExtraSoundDelayTicks <= 0) { - return; - } - - new BukkitRunnable() { - @Override - public void run() { - if (!player.isOnline()) { - return; - } - SoundKeys.play(player, Cache.stationCompleteExtraSound, - Cache.stationCompleteExtraSoundVolume, Cache.stationCompleteExtraSoundPitch); - spawnBurst(center, Cache.stationCompleteExtraParticle, Cache.stationCompleteExtraParticleCount, - Cache.stationCompleteExtraParticleRadius); - } - }.runTaskLater(Research.plugin, Cache.stationCompleteExtraSoundDelayTicks); - } - - private static void spawnBurst(Location center, String particleName, int count, double radius) { - Particle particle = parseParticle(particleName); - if (particle == null || center.getWorld() == null) { - return; - } - center.getWorld().spawnParticle( - particle, - center, - count, - radius, - 0.7, - radius, - 0.02); - } - - private static Particle parseParticle(String name) { - if (name == null || name.isBlank()) { - return null; - } - try { - return Particle.valueOf(name.trim().toUpperCase()); - } catch (IllegalArgumentException ex) { - Research.plugin.getLogger().warning("[Research] Unknown station complete particle: " + name); - return null; - } - } -} +package net.tfminecraft.research.util; + +import org.bukkit.Location; +import org.bukkit.Particle; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; + +import net.tfminecraft.research.Cache; +import net.tfminecraft.research.Research; + +public final class StationCompleteEffects { + + private StationCompleteEffects() {} + + public static void play(Player player, Location blockLocation) { + if (player == null || blockLocation == null || blockLocation.getWorld() == null) { + return; + } + + Location center = blockLocation.clone().add(0.5, 1.0, 0.5); + SoundKeys.play(player, Cache.stationCompleteSound, Cache.stationCompleteSoundVolume, + Cache.stationCompleteSoundPitch); + spawnBurst(center, Cache.stationCompleteParticle, Cache.stationCompleteParticleCount, + Cache.stationCompleteParticleRadius); + + if (Cache.stationCompleteExtraSoundDelayTicks <= 0) { + return; + } + + new BukkitRunnable() { + @Override + public void run() { + if (!player.isOnline()) { + return; + } + SoundKeys.play(player, Cache.stationCompleteExtraSound, + Cache.stationCompleteExtraSoundVolume, Cache.stationCompleteExtraSoundPitch); + spawnBurst(center, Cache.stationCompleteExtraParticle, Cache.stationCompleteExtraParticleCount, + Cache.stationCompleteExtraParticleRadius); + } + }.runTaskLater(Research.plugin, Cache.stationCompleteExtraSoundDelayTicks); + } + + private static void spawnBurst(Location center, String particleName, int count, double radius) { + Particle particle = parseParticle(particleName); + if (particle == null || center.getWorld() == null) { + return; + } + center.getWorld().spawnParticle( + particle, + center, + count, + radius, + 0.7, + radius, + 0.02); + } + + private static Particle parseParticle(String name) { + if (name == null || name.isBlank()) { + return null; + } + try { + return Particle.valueOf(name.trim().toUpperCase()); + } catch (IllegalArgumentException ex) { + Research.plugin.getLogger().warning("[Research] Unknown station complete particle: " + name); + return null; + } + } +} diff --git a/src/main/java/net/tfminecraft/research/util/StationStartEffects.java b/src/main/java/net/tfminecraft/research/util/StationStartEffects.java index 8cfd017..b5d2a3d 100644 --- a/src/main/java/net/tfminecraft/research/util/StationStartEffects.java +++ b/src/main/java/net/tfminecraft/research/util/StationStartEffects.java @@ -1,46 +1,46 @@ -package net.tfminecraft.research.util; - -import org.bukkit.Location; -import org.bukkit.Particle; -import org.bukkit.entity.Player; - -import net.tfminecraft.research.Cache; -import net.tfminecraft.research.Research; - -public final class StationStartEffects { - - private StationStartEffects() {} - - public static void play(Player player, Location blockLocation) { - if (player == null || blockLocation == null || blockLocation.getWorld() == null) { - return; - } - SoundKeys.play(player, Cache.stationStartSound, Cache.stationStartSoundVolume, Cache.stationStartSoundPitch); - - Particle particle = parseParticle(Cache.stationStartParticle); - if (particle == null) { - return; - } - Location center = blockLocation.clone().add(0.5, 1.0, 0.5); - blockLocation.getWorld().spawnParticle( - particle, - center, - Cache.stationStartParticleCount, - Cache.stationStartParticleRadius, - 0.6, - Cache.stationStartParticleRadius, - 0.02); - } - - private static Particle parseParticle(String name) { - if (name == null || name.isBlank()) { - return null; - } - try { - return Particle.valueOf(name.trim().toUpperCase()); - } catch (IllegalArgumentException ex) { - Research.plugin.getLogger().warning("[Research] Unknown station start particle: " + name); - return null; - } - } -} +package net.tfminecraft.research.util; + +import org.bukkit.Location; +import org.bukkit.Particle; +import org.bukkit.entity.Player; + +import net.tfminecraft.research.Cache; +import net.tfminecraft.research.Research; + +public final class StationStartEffects { + + private StationStartEffects() {} + + public static void play(Player player, Location blockLocation) { + if (player == null || blockLocation == null || blockLocation.getWorld() == null) { + return; + } + SoundKeys.play(player, Cache.stationStartSound, Cache.stationStartSoundVolume, Cache.stationStartSoundPitch); + + Particle particle = parseParticle(Cache.stationStartParticle); + if (particle == null) { + return; + } + Location center = blockLocation.clone().add(0.5, 1.0, 0.5); + blockLocation.getWorld().spawnParticle( + particle, + center, + Cache.stationStartParticleCount, + Cache.stationStartParticleRadius, + 0.6, + Cache.stationStartParticleRadius, + 0.02); + } + + private static Particle parseParticle(String name) { + if (name == null || name.isBlank()) { + return null; + } + try { + return Particle.valueOf(name.trim().toUpperCase()); + } catch (IllegalArgumentException ex) { + Research.plugin.getLogger().warning("[Research] Unknown station start particle: " + name); + return null; + } + } +} diff --git a/src/main/resources/gui.yml b/src/main/resources/gui.yml index 6288ee1..388bc8f 100644 --- a/src/main/resources/gui.yml +++ b/src/main/resources/gui.yml @@ -1,41 +1,41 @@ -# GUI configuration for the research station. -# Layout (slot numbers) is in plugin code (GridLayout). This file controls icons, labels, and GUI hex colours. -# Chat messages remain in messages.yml (hex codes there are formatted via TLibs StringFormatter.formatHex). - -items: - confirm_button: ia.mcicons:icon_confirm - cancel_button: ia.mcicons:icon_cancel - filler: vanilla.GRAY_STAINED_GLASS_PANE - grid_filler: vanilla.GRAY_STAINED_GLASS_PANE - left_filler: vanilla.BLACK_STAINED_GLASS_PANE - aspect_progress_inactive: vanilla.GRAY_STAINED_GLASS_PANE - mental_points_icon: vanilla.LIGHT - locked_aspect: m.icons.RESEARCH_UNKNOWN_ICON - aspect_row_divider: m.icons.BLANK - aspect_tested_unknown: m.icons.RESEARCH_UNKNOWN_ICON - aspect_progress_unknown: vanilla.YELLOW_STAINED_GLASS_PANE - aspect_progress_revealed: vanilla.LIME_STAINED_GLASS_PANE - wave_pulse_default: vanilla.LIGHT_BLUE_STAINED_GLASS_PANE - -labels: - inventory_title: "Research Station" - scrap_confirm_title: "Confirm Scrap" - undiscovered_aspect_name: "§8Undiscovered Aspect" - undiscovered_aspect_lore: - - "#7a706aThis aspect has not been identified yet." - scrap_button: "#c45749Scrap" - confirm_experiment: "#82d461Confirm Experiment" - experiment_completed: "#575150Completed" - scrap_confirm_yes: "#82d461Confirm Scrap" - scrap_confirm_no: "#c45749Cancel" - -# Applied via TLibs StringFormatter.formatHex to GUI item names/lore. -colors: - label_muted: "#7a706a" - label_body: "#d4c9ae" - label_accent: "#b8ae61" - inventory_title: "#d4c9ae" - aspect_confirmed: "#82d461" - aspect_hidden: "#575150" - progress_unknown: "#9c9775" - mental_points: "#d1b43f" +# GUI configuration for the research station. +# Layout (slot numbers) is in plugin code (GridLayout). This file controls icons, labels, and GUI hex colours. +# Chat messages remain in messages.yml (hex codes there are formatted via TLibs StringFormatter.formatHex). + +items: + confirm_button: ia.mcicons:icon_confirm + cancel_button: ia.mcicons:icon_cancel + filler: vanilla.GRAY_STAINED_GLASS_PANE + grid_filler: vanilla.GRAY_STAINED_GLASS_PANE + left_filler: vanilla.BLACK_STAINED_GLASS_PANE + aspect_progress_inactive: vanilla.GRAY_STAINED_GLASS_PANE + mental_points_icon: vanilla.LIGHT + locked_aspect: m.icons.RESEARCH_UNKNOWN_ICON + aspect_row_divider: m.icons.BLANK + aspect_tested_unknown: m.icons.RESEARCH_UNKNOWN_ICON + aspect_progress_unknown: vanilla.YELLOW_STAINED_GLASS_PANE + aspect_progress_revealed: vanilla.LIME_STAINED_GLASS_PANE + wave_pulse_default: vanilla.LIGHT_BLUE_STAINED_GLASS_PANE + +labels: + inventory_title: "Research Station" + scrap_confirm_title: "Confirm Scrap" + undiscovered_aspect_name: "§8Undiscovered Aspect" + undiscovered_aspect_lore: + - "#7a706aThis aspect has not been identified yet." + scrap_button: "#c45749Scrap" + confirm_experiment: "#82d461Confirm Experiment" + experiment_completed: "#575150Completed" + scrap_confirm_yes: "#82d461Confirm Scrap" + scrap_confirm_no: "#c45749Cancel" + +# Applied via TLibs StringFormatter.formatHex to GUI item names/lore. +colors: + label_muted: "#7a706a" + label_body: "#d4c9ae" + label_accent: "#b8ae61" + inventory_title: "#d4c9ae" + aspect_confirmed: "#82d461" + aspect_hidden: "#575150" + progress_unknown: "#9c9775" + mental_points: "#d1b43f" diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 35ba0c9..001c943 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,7 +1,7 @@ main: net.tfminecraft.research.Research -api-version: '1.19' +api-version: '1.21.10' name: Research -version: 0.13.0 +version: '${project.version}' author: Drefvelin depend: [MMOItems, MythicLib, ItemsAdder, TLibs, TFMCCore] softdepend: [MMOCore]