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]