diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 78560bc..b603d93 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -10,14 +10,14 @@ body: id: server attributes: label: Server distribution and version - placeholder: "Paper 1.21.x" + placeholder: "Paper 26.1.x" validations: required: true - type: input id: ezseasons_version attributes: label: EzSeasons version - placeholder: "e.g. 1.0.0" + placeholder: "e.g. 2.0.0" validations: required: true - type: textarea diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7c956a..b0b9330 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,12 +15,17 @@ jobs: - name: Checkout repository uses: actions/checkout@v5 - - name: Set up Java 21 + - name: Set up Java 25 uses: actions/setup-java@v5 with: distribution: temurin - java-version: '21' + java-version: '25' cache: maven + - name: Build and install MockBukkit dev/26.1.1 + run: | + git clone --depth=1 --branch dev/26.1.1 https://github.com/MockBukkit/MockBukkit.git /tmp/MockBukkit + cd /tmp/MockBukkit && ./gradlew publishToMavenLocal -xtest -xjavadoc + - name: Verify plugin build run: mvn -B -ntp clean verify diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..1a4add7 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,44 @@ +name: Documentation + +on: + release: + types: [published] + workflow_dispatch: + +# Required for GitHub Pages deployment via Actions +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment; skip in-progress runs +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build-and-deploy: + name: Build and Deploy + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Configure GitHub Pages + uses: actions/configure-pages@v5 + + - name: Build Jekyll site + uses: actions/jekyll-build-pages@v1 + with: + source: ./docs + destination: ./_site + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..4c9fd3b --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,21 @@ +name: Lint + +on: + pull_request: + paths: + - "docs/**/*.md" + - ".markdownlint.yml" + +jobs: + markdown: + name: Validate Markdown + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Lint documentation Markdown + uses: DavidAnson/markdownlint-cli2-action@v16 + with: + globs: "docs/**/*.md" + config: ".markdownlint.yml" diff --git a/.github/workflows/publish-packages.yml b/.github/workflows/publish-packages.yml index 66b2f58..e69de29 100644 --- a/.github/workflows/publish-packages.yml +++ b/.github/workflows/publish-packages.yml @@ -1,34 +0,0 @@ -name: Publish Package - -on: - release: - types: - - published - -jobs: - publish-github-packages: - name: Publish to GitHub Packages - if: github.event_name == 'release' - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Java 21 - uses: actions/setup-java@v5 - with: - distribution: temurin - java-version: '21' - cache: maven - server-id: github - server-username: GITHUB_ACTOR - server-password: GITHUB_TOKEN - - - name: Publish Maven artifacts - run: mvn -B -ntp -DskipTests deploy -DaltDeploymentRepository=github::https://maven.pkg.github.com/${{ github.repository }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 905fd4a..45f1d78 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,8 +14,12 @@ jobs: - uses: actions/setup-java@v5 with: distribution: temurin - java-version: '21' + java-version: '25' cache: maven + - name: Build and install MockBukkit dev/26.1.1 + run: | + git clone --depth=1 --branch dev/26.1.1 https://github.com/MockBukkit/MockBukkit.git /tmp/MockBukkit + cd /tmp/MockBukkit && ./gradlew publishToMavenLocal -xtest -xjavadoc - name: Run unit tests # Workflow selectors assume tests follow the *UnitTest naming convention. run: mvn -B -Dtest='*UnitTest' test @@ -28,8 +32,12 @@ jobs: - uses: actions/setup-java@v5 with: distribution: temurin - java-version: '21' + java-version: '25' cache: maven + - name: Build and install MockBukkit dev/26.1.1 + run: | + git clone --depth=1 --branch dev/26.1.1 https://github.com/MockBukkit/MockBukkit.git /tmp/MockBukkit + cd /tmp/MockBukkit && ./gradlew publishToMavenLocal -xtest -xjavadoc - name: Generate unit test coverage report run: mvn -B -pl plugin -am -Dtest='*UnitTest' test org.jacoco:jacoco-maven-plugin:report - name: Add coverage summary to workflow report @@ -85,8 +93,12 @@ jobs: - uses: actions/setup-java@v5 with: distribution: temurin - java-version: '21' + java-version: '25' cache: maven + - name: Build and install MockBukkit dev/26.1.1 + run: | + git clone --depth=1 --branch dev/26.1.1 https://github.com/MockBukkit/MockBukkit.git /tmp/MockBukkit + cd /tmp/MockBukkit && ./gradlew publishToMavenLocal -xtest -xjavadoc - name: Check translation coverage run: mvn -B -Dtest='TranslationCoverageUnitTest' test feature-tests: @@ -97,8 +109,12 @@ jobs: - uses: actions/setup-java@v5 with: distribution: temurin - java-version: '21' + java-version: '25' cache: maven + - name: Build and install MockBukkit dev/26.1.1 + run: | + git clone --depth=1 --branch dev/26.1.1 https://github.com/MockBukkit/MockBukkit.git /tmp/MockBukkit + cd /tmp/MockBukkit && ./gradlew publishToMavenLocal -xtest -xjavadoc - name: Run feature tests # Workflow selectors assume feature tests are named *FeatureUnitTest. run: mvn -B -Dtest='*FeatureUnitTest' test diff --git a/.github/workflows/validate-resources.yml b/.github/workflows/validate-resources.yml index 83d07c7..6c02839 100644 --- a/.github/workflows/validate-resources.yml +++ b/.github/workflows/validate-resources.yml @@ -18,9 +18,14 @@ jobs: uses: actions/setup-java@v5 with: distribution: temurin - java-version: '21' + java-version: '25' cache: maven + - name: Build and install MockBukkit dev/26.1.1 + run: | + git clone --depth=1 --branch dev/26.1.1 https://github.com/MockBukkit/MockBukkit.git /tmp/MockBukkit + cd /tmp/MockBukkit && ./gradlew publishToMavenLocal -xtest -xjavadoc + - name: Validate YAML resources # Workflow selectors assume tests follow the *UnitTest naming convention. run: mvn -B -Dtest=ResourceYamlValidationUnitTest test diff --git a/.gitignore b/.gitignore index 4543b28..2837e30 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,11 @@ Thumbs.db # Logs *.log + +# Jekyll build output +docs/_site/ +docs/.bundle/ +docs/vendor/ + +# Node.js (used for tooling like markdownlint-cli) +node_modules/ diff --git a/.markdownlint.yml b/.markdownlint.yml new file mode 100644 index 0000000..598cd88 --- /dev/null +++ b/.markdownlint.yml @@ -0,0 +1,34 @@ +# markdownlint configuration +# https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md + +default: true + +# MD013 — Line length: documentation lines are allowed to exceed 80 chars +MD013: false + +# MD022 — Blanks around headings: Kramdown attribute syntax ({: .no_toc }) must +# immediately follow a heading with no blank line — disable this rule +MD022: false + +# MD025 — Single H1: pages have both front-matter title: and an # H1 heading; +# disable front-matter title detection to avoid false positives +MD025: + front_matter_title: "" + +# MD033 — Inline HTML: needed for Jekyll/Liquid includes and badge images +MD033: false + +# MD036 — Emphasis as heading: just-the-docs sub-sections inside API tables use +# bold labels that markdownlint mistakes for headings +MD036: false + +# MD041 — First line must be H1: front-matter pages don't start with a heading +MD041: false + +# MD024 — No duplicate headings: allow sibling duplicates across sections +MD024: + siblings_only: true + +# MD007 — Unordered list indentation: use 2-space indent under list items +MD007: + indent: 2 diff --git a/.markdownlintignore b/.markdownlintignore new file mode 100644 index 0000000..fad9531 --- /dev/null +++ b/.markdownlintignore @@ -0,0 +1,3 @@ +api/target/ +plugin/target/ +target/ diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index c3e2722..7f85383 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -118,13 +118,13 @@ the community. This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at -https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. +. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). For answers to common questions about this code of conduct, see the FAQ at -https://www.contributor-covenant.org/faq. Translations are available at -https://www.contributor-covenant.org/translations. +. Translations are available at +. [homepage]: https://www.contributor-covenant.org diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 87e7a09..af15543 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ Thanks for your interest in contributing to EzSeasons. - Java toolchain version defined by the parent build (`java.version` in the parent POM). - Maven 3.9+. -- A Paper-compatible test server (API target `1.21`) for runtime checks. +- A Paper-compatible test server (Paper 26.1+, Java 25) for runtime checks. ### Build locally @@ -36,7 +36,6 @@ If this module is built inside the multi-module parent project, run Maven from t - Preserve backwards-compatible behavior for public API interfaces in `com.skyblockexp.lifesteal.seasons.api` when possible. - Add or update documentation (README, templates, config notes) whenever behavior or integration patterns change. - ## Maintainer operations - Code of Conduct reports are received through the maintainer-controlled alias `ezplugins@outlook.com`. diff --git a/README.md b/README.md index 4282c84..a6bf74e 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,8 @@ It runs timed season resets, sends reminder broadcasts, and provides `/season` s - Download **EzSeasons** from Modrinth: [https://modrinth.com/plugin/ezseasons](https://modrinth.com/plugin/ezseasons) - Optional companion plugin: [EzLifesteal](https://modrinth.com/plugin/ezlifesteal) - Supported server software: Paper / Bukkit-compatible server -- API target: `1.21` +- Requires: Java 25, Minecraft 26.1+ +- Plugin version: 2.0.0 ## Quick setup (server owners) diff --git a/SECURITY.md b/SECURITY.md index 20f0690..818c8e7 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,11 +6,12 @@ EzSeasons follows a **latest-stable support policy**: | Version | Supported | | --- | --- | -| 1.x (latest patch release) | ✅ Yes | -| 1.x (older patch releases) | ⚠️ Best effort | +| 2.x (latest patch release) | ✅ Yes | +| 2.x (older patch releases) | ⚠️ Best effort | +| 1.x | ❌ No | | < 1.0.0 | ❌ No | -Please upgrade to the newest `1.x` release before reporting a vulnerability when possible. +Please upgrade to the newest `2.x` release before reporting a vulnerability when possible. ## Reporting a Vulnerability @@ -18,7 +19,7 @@ Please **do not** report security vulnerabilities in public GitHub issues or dis Report vulnerabilities privately by email: -- **ezplugins@outlook.com** +- **[ezplugins@outlook.com](mailto:ezplugins@outlook.com)** Include the following information to help us triage quickly: diff --git a/api/pom.xml b/api/pom.xml index 36c57e3..e41a273 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -6,7 +6,7 @@ com.skyblockexp.lifesteal ezseasons-parent - 1.0.0 + 2.0.0 ezseasons-api diff --git a/api/src/main/java/com/skyblockexp/lifesteal/seasons/api/SeasonsApi.java b/api/src/main/java/com/skyblockexp/lifesteal/seasons/api/SeasonsApi.java index c4072a6..aaa4390 100644 --- a/api/src/main/java/com/skyblockexp/lifesteal/seasons/api/SeasonsApi.java +++ b/api/src/main/java/com/skyblockexp/lifesteal/seasons/api/SeasonsApi.java @@ -11,7 +11,8 @@ public interface SeasonsApi { /** - * Registers a plugin integration with EzSeasons. The integration can listen for season events or interact with the API. + * Registers a plugin integration with EzSeasons. The integration can listen for season events + * or interact with the API. * * Ordering guarantee: internal registration state is updated before * {@link SeasonsIntegration#onRegister(SeasonsApi)} is invoked and before diff --git a/api/src/main/java/com/skyblockexp/lifesteal/seasons/api/events/SeasonResetEvent.java b/api/src/main/java/com/skyblockexp/lifesteal/seasons/api/events/SeasonResetEvent.java index 95c27b9..3cb9cbb 100644 --- a/api/src/main/java/com/skyblockexp/lifesteal/seasons/api/events/SeasonResetEvent.java +++ b/api/src/main/java/com/skyblockexp/lifesteal/seasons/api/events/SeasonResetEvent.java @@ -1,24 +1,27 @@ package com.skyblockexp.lifesteal.seasons.api.events; +import java.util.Objects; import org.bukkit.event.Event; import org.bukkit.event.HandlerList; -import java.util.Objects; - /** * Fired whenever EzSeasons performs a season reset. *

* Ordering guarantee: this event is dispatched only after internal reset timestamps have been updated and persisted. *

- * Threading: fired on the thread that initiated the reset. In typical Bukkit usage this should be the main server thread. + * Threading: fired on the thread that initiated the reset. In typical Bukkit usage this should be the main + * server thread. */ public final class SeasonResetEvent extends Event { private static final HandlerList HANDLERS = new HandlerList(); private final long previousResetMillis; + private final long resetMillis; + private final long nextResetMillis; + private final String reason; /** @@ -27,7 +30,8 @@ public final class SeasonResetEvent extends Event { * @param previousResetMillis reset timestamp immediately before this reset in unix epoch milliseconds * @param resetMillis reset timestamp for this reset in unix epoch milliseconds * @param nextResetMillis next scheduled reset timestamp in unix epoch milliseconds, or {@code 0} if unscheduled - * @param reason caller-provided reason; may be {@code null}, in which case {@code "unspecified"} is stored + * @param reason caller-provided reason; may be {@code null}, in which case + * {@code "unspecified"} is stored */ public SeasonResetEvent(long previousResetMillis, long resetMillis, diff --git a/api/src/main/java/com/skyblockexp/lifesteal/seasons/api/events/SeasonsIntegrationRegisteredEvent.java b/api/src/main/java/com/skyblockexp/lifesteal/seasons/api/events/SeasonsIntegrationRegisteredEvent.java index f92f4f2..6275317 100644 --- a/api/src/main/java/com/skyblockexp/lifesteal/seasons/api/events/SeasonsIntegrationRegisteredEvent.java +++ b/api/src/main/java/com/skyblockexp/lifesteal/seasons/api/events/SeasonsIntegrationRegisteredEvent.java @@ -2,11 +2,10 @@ import com.skyblockexp.lifesteal.seasons.api.SeasonsApi; import com.skyblockexp.lifesteal.seasons.api.SeasonsIntegration; +import java.util.Objects; import org.bukkit.event.Event; import org.bukkit.event.HandlerList; -import java.util.Objects; - /** * Fired when a {@link SeasonsIntegration} is registered through {@link SeasonsApi}. *

@@ -20,6 +19,7 @@ public final class SeasonsIntegrationRegisteredEvent extends Event { private static final HandlerList HANDLERS = new HandlerList(); private final SeasonsApi api; + private final SeasonsIntegration integration; /** diff --git a/api/src/main/java/com/skyblockexp/lifesteal/seasons/api/events/SeasonsIntegrationUnregisteredEvent.java b/api/src/main/java/com/skyblockexp/lifesteal/seasons/api/events/SeasonsIntegrationUnregisteredEvent.java index b2fc968..7e9f406 100644 --- a/api/src/main/java/com/skyblockexp/lifesteal/seasons/api/events/SeasonsIntegrationUnregisteredEvent.java +++ b/api/src/main/java/com/skyblockexp/lifesteal/seasons/api/events/SeasonsIntegrationUnregisteredEvent.java @@ -2,11 +2,10 @@ import com.skyblockexp.lifesteal.seasons.api.SeasonsApi; import com.skyblockexp.lifesteal.seasons.api.SeasonsIntegration; +import java.util.Objects; import org.bukkit.event.Event; import org.bukkit.event.HandlerList; -import java.util.Objects; - /** * Fired when a {@link SeasonsIntegration} is unregistered through {@link SeasonsApi}. *

@@ -20,6 +19,7 @@ public final class SeasonsIntegrationUnregisteredEvent extends Event { private static final HandlerList HANDLERS = new HandlerList(); private final SeasonsApi api; + private final SeasonsIntegration integration; /** diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 0000000..9523889 --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/Gemfile b/docs/Gemfile new file mode 100644 index 0000000..7359a80 --- /dev/null +++ b/docs/Gemfile @@ -0,0 +1,4 @@ +source "https://rubygems.org" + +gem "github-pages", group: :jekyll_plugins +gem "just-the-docs" diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000..ac13ce4 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,64 @@ +remote_theme: just-the-docs/just-the-docs + +title: EzSeasons +description: >- + Standalone season scheduling plugin for Minecraft servers. + Supports season resets, multi-language messages, and developer API integration. + +url: "https://ez-plugins.github.io" +baseurl: "/EzSeasons" + +# ── Appearance ──────────────────────────────────────────────────────────────── +color_scheme: ezseasons +heading_anchors: true + +# ── Header links ────────────────────────────────────────────────────────────── +aux_links: + "GitHub": + - "https://github.com/ez-plugins/EzSeasons" + "JitPack": + - "https://jitpack.io/#ez-plugins/EzSeasons" +aux_links_new_tab: true + +# ── Navigation ──────────────────────────────────────────────────────────────── +nav_sort: case_insensitive +nav_external_links: + - title: Changelog + url: "https://github.com/ez-plugins/EzSeasons/releases" + hide_icon: false + +# ── Search ──────────────────────────────────────────────────────────────────── +search_enabled: true +search: + heading_level: 2 + previews: 3 + preview_words_before: 5 + preview_words_after: 10 + tokenizer_separator: /[\s/]+/ + +# ── Footer ──────────────────────────────────────────────────────────────────── +back_to_top: true +back_to_top_text: "Back to top" + +footer_content: >- + Copyright © 2024–2026 Gyvex. + Distributed under the + MIT License. + +# ── Kramdown ────────────────────────────────────────────────────────────────── +kramdown: + syntax_highlighter_opts: + block: + line_numbers: false + +# ── Plugins ─────────────────────────────────────────────────────────────────── +plugins: + - jekyll-remote-theme + - jekyll-seo-tag + +# ── Build exclusions ────────────────────────────────────────────────────────── +exclude: + - Gemfile + - Gemfile.lock + - README.md + - topics/ diff --git a/docs/_sass/color_schemes/ezseasons.scss b/docs/_sass/color_schemes/ezseasons.scss new file mode 100644 index 0000000..de9dab6 --- /dev/null +++ b/docs/_sass/color_schemes/ezseasons.scss @@ -0,0 +1,42 @@ +// EzSeasons — dark/yellow/white color scheme for just-the-docs +// +// Palette: +// Background #141414 (body) +// Surface #1c1c1c (sidebar, cards) +// Elevated #222222 (code blocks, search, table rows) +// Border #2e2e2e +// Text #d0d0d0 (body) / #ffffff (headings) +// Accent #f5c518 (yellow — links, nav highlight, buttons) +// Accent dim #c9a418 (hover state) + +$color-scheme: dark; + +// ── Surfaces ───────────────────────────────────────────────────────────────── +$body-background-color: #141414; +$sidebar-color: #1c1c1c; +$feedback-color: #181818; + +// ── Typography ──────────────────────────────────────────────────────────────── +$body-text-color: #d0d0d0; +$body-heading-color: #ffffff; + +// ── Links & accent ──────────────────────────────────────────────────────────── +$link-color: #f5c518; +$btn-primary-color: #f5c518; + +// ── Borders ─────────────────────────────────────────────────────────────────── +$border-color: #2e2e2e; + +// ── Code ────────────────────────────────────────────────────────────────────── +$code-background-color: #222222; + +// ── Tables ──────────────────────────────────────────────────────────────────── +$table-background-color: #1a1a1a; + +// ── Search ──────────────────────────────────────────────────────────────────── +$search-background-color: #222222; +$search-foreground-color: #c0c0c0; +$search-border-color: #333333; + +// ── Buttons ─────────────────────────────────────────────────────────────────── +$base-button-color: #252525; diff --git a/docs/_sass/custom/custom.scss b/docs/_sass/custom/custom.scss new file mode 100644 index 0000000..375f607 --- /dev/null +++ b/docs/_sass/custom/custom.scss @@ -0,0 +1,98 @@ +// Custom overrides for EzSeasons dark theme +// +// just-the-docs imports this file after everything else, so these +// declarations win over the default Rouge syntax highlighting. + +// ── Inline code ────────────────────────────────────────────────────────────── +code { + color: #e0e0e0; +} + +// ── Fenced code blocks: Monokai-inspired dark syntax highlighting ───────────── +// Neutral base +.highlight { + background-color: #222222; + color: #e0e0e0; +} + +// Comments — muted gray +.highlight .c, +.highlight .cm, +.highlight .cp, +.highlight .c1, +.highlight .cs { color: #6c6c6c; font-style: italic; } + +// Keywords — yellow accent +.highlight .k, +.highlight .kc, +.highlight .kd, +.highlight .kn, +.highlight .kr { color: #f5c518; font-weight: bold; } +.highlight .kp { color: #f5c518; } + +// Types — light blue +.highlight .kt { color: #8fbcdb; font-weight: bold; } + +// Strings — sky blue +.highlight .s, +.highlight .s1, +.highlight .s2, +.highlight .sb, +.highlight .sc, +.highlight .sd, +.highlight .sh, +.highlight .si, +.highlight .ss, +.highlight .sx { color: #87ceeb; } + +// Numbers — soft purple +.highlight .m, +.highlight .mi, +.highlight .mf, +.highlight .mh, +.highlight .mo, +.highlight .il { color: #b5a0e8; } + +// Names / identifiers — neutral light +.highlight .n, +.highlight .nb, +.highlight .ni, +.highlight .nl, +.highlight .nv { color: #e0e0e0; } + +// Class / function / namespace names — light blue +.highlight .na, +.highlight .nc, +.highlight .ne, +.highlight .nf, +.highlight .nn, +.highlight .nx { color: #8fbcdb; } + +// Decorator / constant / tag names — yellow accent +.highlight .nd, +.highlight .no, +.highlight .nt { color: #f5c518; } + +// Operators — neutral light +.highlight .o { color: #c0c0c0; } +.highlight .ow { color: #f5c518; font-weight: bold; } + +// Punctuation — neutral light +.highlight .p, +.highlight .pi { color: #c0c0c0; } + +// Errors — bright red, no garish background +.highlight .err { color: #ff5555; background-color: transparent; border: none; } + +// Diff colors +.highlight .gd { color: #ff5555; } +.highlight .gi { color: #50fa7b; } +.highlight .gp { color: #f5c518; font-weight: bold; } +.highlight .gh { color: #e0e0e0; font-weight: bold; } +.highlight .gu { color: #6c6c6c; font-weight: bold; } +.highlight .ge { font-style: italic; } +.highlight .gs { font-weight: bold; } + +// XML/YAML/properties literal blocks +.highlight .l, +.highlight .ld { color: #87ceeb; } diff --git a/docs/api.md b/docs/api.md index fd003f1..8e2bf8e 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,4 +1,19 @@ +--- +title: API +nav_order: 6 +description: "Developer API documentation for integrating with EzSeasons" +--- + # API +{: .no_toc } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- EzSeasons exposes a Bukkit service API for companion plugins. @@ -24,7 +39,7 @@ You can use either GitHub Packages (official) or JitPack. com.skyblockexp.lifesteal ezseasons-api - 1.0.0 + 2.0.0 provided @@ -46,39 +61,64 @@ You can use either GitHub Packages (official) or JitPack. com.github.ez-plugins.EzSeasons ezseasons-api - v1.0.0 + v2.0.0 provided ``` +## Threading + +All public API methods are safe to call from concurrent threads — the integration registry is internally synchronized. However, **callbacks** (`onRegister`, `onUnregister`) and **Bukkit events** (`SeasonResetEvent`, etc.) are fired on the calling thread. In typical usage this is the Bukkit main thread. + +--- + ## Core interfaces ### `SeasonsApi` -Public service interface for registering integrations and triggering season resets. +Public service interface registered as a Bukkit service. Obtain it via `Bukkit.getServicesManager().getRegistration(SeasonsApi.class)`. + +| Method | Returns | Description | +|---|---|---| +| `registerIntegration(SeasonsIntegration integration)` | `boolean` | Registers an integration. Returns `true` on success, `false` if already registered. Throws `NullPointerException` for `null`. | +| `unregisterIntegration(SeasonsIntegration integration)` | `void` | Unregisters an integration. A `null` argument is a no-op. | +| `getIntegrations()` | `List` | Returns an **immutable snapshot** of currently registered integrations. | +| `triggerSeasonReset(String reason)` | `boolean` | Triggers an immediate reset. Returns `true` if a season manager is active. `reason` may be `null` (stored as `"unspecified"`). | -- `boolean registerIntegration(SeasonsIntegration integration)` -- `void unregisterIntegration(SeasonsIntegration integration)` -- `List getIntegrations()` -- `boolean triggerSeasonReset(String reason)` +**Ordering guarantees:** + +- `registerIntegration` — registry is updated **before** `onRegister` is called and before `SeasonsIntegrationRegisteredEvent` fires. +- `unregisterIntegration` — registry is updated **before** `onUnregister` is called and before `SeasonsIntegrationUnregisteredEvent` fires. +- `triggerSeasonReset` — timestamps are persisted **before** `SeasonResetEvent` fires. ### `SeasonsIntegration` -Implement this in your plugin to receive lifecycle callbacks. +Implement this interface in your plugin to receive registration lifecycle callbacks. + +| Method | Description | +|---|---| +| `onRegister(SeasonsApi api)` | Called after this integration is added to the EzSeasons registry. | +| `onUnregister()` | Called after this integration is removed from the EzSeasons registry. | -- `void onRegister(SeasonsApi api)` -- `void onUnregister()` +Exceptions thrown from either callback propagate to the caller; EzSeasons does not swallow them. -## Reset event hooks (recommended) +--- -To add custom functionality when a season resets, listen for `SeasonResetEvent`. +## Events -`SeasonResetEvent` provides: -- `getPreviousResetMillis()` -- `getResetMillis()` -- `getNextResetMillis()` -- `getReason()` +### `SeasonResetEvent` + +Fired whenever EzSeasons performs a season reset (scheduled or forced). Fired **after** timestamps are updated and persisted. + +| Method | Return type | Description | +|---|---|---| +| `getPreviousResetMillis()` | `long` | Unix epoch ms of the reset immediately before this one. | +| `getResetMillis()` | `long` | Unix epoch ms of this reset. | +| `getNextResetMillis()` | `long` | Unix epoch ms of the next scheduled reset, or `0` if unscheduled. | +| `getReason()` | `String` | Caller-provided reason; never `null` — defaults to `"unspecified"`. | + +**Listening example:** ```java import com.skyblockexp.lifesteal.seasons.api.events.SeasonResetEvent; @@ -94,6 +134,26 @@ public final class SeasonResetListener implements Listener { } ``` +### `SeasonsIntegrationRegisteredEvent` + +Fired after a `SeasonsIntegration` is registered. Fired **after** `onRegister` returns normally. + +| Method | Return type | Description | +|---|---|---| +| `getApi()` | `SeasonsApi` | The API instance that performed the registration. Never `null`. | +| `getIntegration()` | `SeasonsIntegration` | The integration that was registered. Never `null`. | + +### `SeasonsIntegrationUnregisteredEvent` + +Fired after a `SeasonsIntegration` is unregistered. Fired **after** `onUnregister` returns normally. + +| Method | Return type | Description | +|---|---|---| +| `getApi()` | `SeasonsApi` | The API instance that performed the unregistration. Never `null`. | +| `getIntegration()` | `SeasonsIntegration` | The integration that was unregistered. Never `null`. | + +--- + ## Triggering a reset through the API ```java @@ -101,18 +161,59 @@ import com.skyblockexp.lifesteal.seasons.api.SeasonsApi; import org.bukkit.Bukkit; import org.bukkit.plugin.RegisteredServiceProvider; -RegisteredServiceProvider registration = +RegisteredServiceProvider rsp = Bukkit.getServicesManager().getRegistration(SeasonsApi.class); -if (registration != null) { - SeasonsApi api = registration.getProvider(); +if (rsp != null) { + SeasonsApi api = rsp.getProvider(); api.triggerSeasonReset("admin"); } ``` -## Additional lifecycle events +--- -EzSeasons also publishes integration lifecycle events: +## Registering an integration: full example -- `SeasonsIntegrationRegisteredEvent` -- `SeasonsIntegrationUnregisteredEvent` +```java +import com.skyblockexp.lifesteal.seasons.api.SeasonsApi; +import com.skyblockexp.lifesteal.seasons.api.SeasonsIntegration; +import org.bukkit.Bukkit; +import org.bukkit.plugin.RegisteredServiceProvider; +import org.bukkit.plugin.java.JavaPlugin; + +public final class MyPlugin extends JavaPlugin { + + private SeasonsApi seasonsApi; + + @Override + public void onEnable() { + RegisteredServiceProvider rsp = + Bukkit.getServicesManager().getRegistration(SeasonsApi.class); + + if (rsp == null) { + getLogger().warning("EzSeasons not found — season integration disabled."); + return; + } + + seasonsApi = rsp.getProvider(); + seasonsApi.registerIntegration(new MyIntegration()); + } + + @Override + public void onDisable() { + if (seasonsApi != null) { + seasonsApi.unregisterIntegration(/* your instance */ null); + } + } +} +``` + +--- + +## `plugin.yml` soft-dependency + +Add EzSeasons as a soft dependency so your plugin loads after it when both are present: + +```yaml +softdepend: [EzSeasons] +``` diff --git a/docs/commands.md b/docs/commands.md index 298e826..6dd9ebc 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -1,52 +1,159 @@ +--- +title: Commands +nav_order: 3 +description: "Reference for all EzSeasons commands and admin subcommands" +--- + # Commands +{: .no_toc } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +EzSeasons adds one root command (`/season`) with a set of admin subcommands. All commands support full tab completion. -EzSeasons exposes one root command with optional admin subcommands. +--- ## `/season` -Displays the remaining time until the next configured season reset. +Shows the remaining time until the next configured season reset. -- **Usage:** `/season` (alias: `/season status`) -- **Permissions accepted:** `lifesteal.season`, `lifesteal.season.admin`, or `lifesteal.admin` -- **Who can run it:** players and console senders with one of the permissions above -- **Tab completion root suggestions:** `admin`, `status` +| Detail | Value | +|--------|-------| +| Alias | `/season status` | +| Permission | `lifesteal.season`, `lifesteal.season.admin`, or `lifesteal.admin` | +| Available to | Players and console | + +**Example output:** + +```text +[EzSeasons] Next season reset in 12d 4h 33m. +``` + +If scheduling is disabled or `next-reset` is not set, you will see an informational message explaining why no countdown is available. + +--- ## `/season admin ...` -Administrative runtime operations. +Administrative runtime operations. Requires `lifesteal.season.admin` or `lifesteal.admin`. + +**Usage:** `/season admin ` + +### Subcommand overview + +| Subcommand | What it does | +|-----------|-------------| +| `reload` | Reloads `config.yml` and all language files without a server restart | +| `status` | Shows the season countdown plus raw `last-reset` / `next-reset` timestamps | +| `reset [reason] --confirm` | Forces an immediate season reset | +| `setnext ` | Overrides the next reset time to a specific Unix timestamp | +| `clear-next --confirm` | Removes the stored next-reset time so EzSeasons recalculates from scratch | + +--- + +### `/season admin reload` + +Reloads `config.yml` and all message translation files, then rebuilds the active season manager. Use this after editing the config file so you do not need to restart the server. + +```shell +/season admin reload +``` + +--- + +### `/season admin status` + +Shows the same countdown as `/season`, plus the raw stored values for debugging purposes. + +```shell +/season admin status +``` + +**Example output:** + +```text +[EzSeasons] Next season reset in 12d 4h 33m. +[EzSeasons] Raw values -> last-reset=1735689600000, next-reset=1738368000000 +``` + +--- + +### `/season admin reset [reason] --confirm` + +Forces an immediate season reset. This triggers the `SeasonResetEvent`, sends the broadcast message, and recalculates `next-reset`. + +The `--confirm` flag is required to prevent accidental resets. + +```shell +/season admin reset --confirm +/season admin reset maintenance --confirm +/season admin reset manual --confirm +``` + +- If `reason` is omitted, it defaults to `admin`. +- The reason is passed to `SeasonResetEvent` and visible to companion plugins. + +> **Warning:** This is destructive. Companion plugins (e.g. EzLifesteal) will execute their reset logic immediately. + +--- + +### `/season admin setnext ` + +Sets the next season reset to a specific point in time. Useful for pushing a reset forward or delaying it without changing the `config.yml` scheduling settings. + +```shell +/season admin setnext 1738368000000 +``` + +The argument is a **Unix timestamp in milliseconds** (not seconds). + +**How to get a Unix timestamp in milliseconds:** + +- Visit [epochconverter.com](https://www.epochconverter.com/) and multiply the result by 1000 if the site shows seconds. +- Or use tab completion — EzSeasons shows `now+3600000` as an example hint (current time + 1 hour in ms). + +Returns a confirmation with the human-readable date when successful: + +```text +[EzSeasons] Next reset set to 1738368000 (2025-02-01T00:00:00Z). +``` + +--- -- **Usage:** `/season admin ` -- **Permission required:** `lifesteal.season.admin` or `lifesteal.admin` +### `/season admin clear-next --confirm` -### Admin subcommands +Clears the stored `next-reset` value (sets it to `0`). EzSeasons will then recalculate the next reset from the current scheduling configuration on its next check cycle. -- `/season admin reload` - - Reloads `config.yml` and message translations, then rebuilds the active season manager. +The `--confirm` flag is required. -- `/season admin reset [reason] --confirm` - - Triggers an immediate season reset. - - If `reason` is omitted, `admin` is used. - - Requires `--confirm` to prevent accidental destructive actions. +```shell +/season admin clear-next --confirm +``` -- `/season admin setnext ` - - Sets `season.next-reset` to the provided Unix epoch milliseconds and saves it. - - Returns clear errors for non-numeric timestamps and out-of-range values. +Use this if you want to let the plugin recalculate the next reset after changing scheduling settings, or after manually editing the config. -- `/season admin clear-next --confirm` - - Sets `season.next-reset` to `0` and saves it. - - Requires `--confirm` to prevent accidental destructive actions. +--- -- `/season admin status` - - Shows the normal season countdown/status message, then prints raw `last-reset` and `next-reset` values. +## Tab completion reference -### Tab-completion hints +| Input | Suggestions | +|-------|-------------| +| `/season` | `admin`, `status` | +| `/season admin` | `reload`, `reset`, `setnext`, `clear-next`, `status` | +| `/season admin reset` | ``, `maintenance`, `manual`, `--confirm` | +| `/season admin setnext` | ``, `now+3600000` (example hint) | +| `/season admin clear-next` | `--confirm` | -- `/season admin` ⇒ `reload`, `reset`, `setnext`, `clear-next`, `status` -- `/season admin setnext` ⇒ ``, `now+3600000` (hint examples) -- `/season admin reset` ⇒ ``, `maintenance`, `manual`, `--confirm` -- `/season admin clear-next` ⇒ `--confirm` +--- ## Notes -- Most admin subcommands require season scheduling to be enabled (`season.enabled: true`). -- Admin commands return explicit success/failure message keys that can be customized in `config.yml` under `messages.keys`. +- Most admin subcommands require `season.enabled: true` in `config.yml` to work correctly. +- Admin operations that write to the config (such as `setnext` and `clear-next`) save immediately to disk — no reload needed afterward. +- Admin command response messages can be customized under `messages.keys` in `config.yml`. See the [Configuration](configuration) page for the full list of available keys. diff --git a/docs/configuration.md b/docs/configuration.md index 1437256..d281952 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,78 +1,195 @@ +--- +title: Configuration +nav_order: 4 +description: "Complete reference for all EzSeasons config.yml options" +--- + # Configuration +{: .no_toc } -Main file: `plugins/EzSeasons/config.yml` +## Table of contents +{: .no_toc .text-delta } -## `messages` +1. TOC +{:toc} -Controls language selection and prefix. +--- -- `language` (`string`): active language code. Supported values: `en`, `es`, `fr`, `zh`, `ru`, `nl`. -- `prefix` (`string`): prepended to command/admin plain-text responses. -- `keys` (`section`): command/admin UX message overrides loaded after language files. - - Includes keys such as `admin-reload-success`, `admin-reset-confirm-required`, `admin-reset-success`, - `admin-setnext-invalid-timestamp`, `admin-setnext-out-of-range`, and `admin-clear-next-confirm-required`. +EzSeasons stores its main configuration at `plugins/EzSeasons/config.yml`. +Run `/season admin reload` to apply changes without restarting the server. +--- -### `messages.keys` example +## Default config.yml ```yaml +# EzSeasons configuration. +# +# EzSeasons is standalone and can run without EzLifesteal. +# Companion plugins may still integrate through the public API. + messages: + language: "en" + prefix: "&c[EzSeasons]&r " keys: + admin-usage: "&7Usage: /season admin " + admin-reload-success: "&aSeason configuration and messages reloaded." admin-reset-confirm-required: "&eReset is destructive. Re-run with confirmation: &f%command%" admin-reset-success: "&aSeason reset triggered successfully. Reason: &f%reason%" - admin-setnext-invalid-timestamp: "&cInvalid timestamp '%value%'. Expected unix epoch milliseconds (example: 1735689600000)." + admin-setnext-usage: "&7Usage: /season admin setnext " + admin-setnext-success: "&aNext reset set to &f%timestamp%&a (&f%iso%&a)." + admin-setnext-invalid-timestamp: "&cInvalid timestamp '%value%'. Expected %expected%." admin-setnext-out-of-range: "&cTimestamp %value% is out of range. Allowed range: %min%..%max%." + admin-clear-next-confirm-required: "&eClearing next reset is destructive. Re-run with confirmation: &f%command%" + admin-clear-next-success: "&aCleared the stored next reset timestamp." + admin-status-raw: "&7Raw values -> last-reset=%lastReset%, next-reset=%nextReset%" + admin-unknown-subcommand: "&cUnknown subcommand '%subcommand%'. %usage%" + +season: + enabled: false + start: 0 + end: 0 + recurring: false + length-days: 30 + check-interval-minutes: 60 + broadcast-message: "&7A new season has begun! Hearts have been reset." + reminder-minutes: [] + reminder-message: "&7The season will reset in &c%time%&7." + last-reset: 0 + next-reset: 0 ``` -## Message files +--- -Folder: `plugins/EzSeasons/messages/` +## `messages` section -Language files (`en.yml`, `es.yml`, `fr.yml`, `zh.yml`, `ru.yml`, `nl.yml`) contain keys such as: +### `messages.language` -- `no-permission` -- `season-disabled` -- `season-status` (supports `%time%`) -- `season-status-unknown` -- `duration.*` formatter keys +Language code for player-facing messages. The corresponding file is loaded from `plugins/EzSeasons/messages/.yml`. -Load order: -1. `messages/en.yml` -2. selected language file (`messages.language`), overlaying any matching keys +| Value | Language | +|---|---| +| `en` | English (default) | +| `es` | Spanish | +| `fr` | French | +| `zh` | Chinese | +| `ru` | Russian | +| `nl` | Dutch | -## `season` +### `messages.prefix` -Scheduling and reset behavior. +Chat prefix prepended to all EzSeasons messages. Supports `&` color codes. -- `enabled` (`boolean`): enables/disables scheduling features and command status output. -- `start` (`long`, epoch millis): explicit window start timestamp. -- `end` (`long`, epoch millis): explicit window end timestamp. -- `recurring` (`boolean`): if `true`, valid explicit windows repeat. -- `length-days` (`long`): fallback duration scheduling length. -- `check-interval-minutes` (`long`): scheduler check cadence. -- `broadcast-message` (`string`): broadcast sent on reset. -- `reminder-minutes` (`list`): minute offsets before reset. -- `reminder-message` (`string`): reminder template (supports `%time%`). -- `last-reset` (`long`, epoch millis): last reset timestamp (stored/runtime-managed). -- `next-reset` (`long`, epoch millis): next reset timestamp (stored/runtime-managed). +**Default:** `"&c[EzSeasons]&r "` -## Scheduling behavior notes +### `messages.keys` -- When `start` and `end` are both valid (`end > start`), EzSeasons uses explicit-window scheduling. -- If explicit values are invalid, EzSeasons falls back to `length-days` scheduling. -- `recurring` is only meaningful for valid explicit windows. -- If `next-reset` is not set, EzSeasons may calculate from current scheduling mode. +Overrides for individual admin-facing message strings. Each key maps to a specific system message. All keys support `&` color codes and the placeholders listed in the table below. -## Minimal example +| Key | Placeholders | Purpose | +|---|---|---| +| `admin-usage` | — | Shown when `/season admin` is run with no subcommand | +| `admin-reload-success` | — | Shown after a successful reload | +| `admin-reset-confirm-required` | `%command%` | Shown when `--confirm` is missing from a reset call | +| `admin-reset-success` | `%reason%` | Shown after a forced reset succeeds | +| `admin-setnext-usage` | — | Shown when `setnext` is missing its argument | +| `admin-setnext-success` | `%timestamp%`, `%iso%` | Shown after `setnext` succeeds | +| `admin-setnext-invalid-timestamp` | `%value%`, `%expected%` | Shown when the timestamp argument is not a valid long | +| `admin-setnext-out-of-range` | `%value%`, `%min%`, `%max%` | Shown when the timestamp is outside the allowed range | +| `admin-clear-next-confirm-required` | `%command%` | Shown when `--confirm` is missing from a `clear-next` call | +| `admin-clear-next-success` | — | Shown after `clear-next` succeeds | +| `admin-status-raw` | `%lastReset%`, `%nextReset%` | Second line of `/season admin status` output | +| `admin-unknown-subcommand` | `%subcommand%`, `%usage%` | Shown for unrecognised subcommands | -```yaml -messages: - language: "en" - prefix: "&c[EzSeasons]&r " +> Player-facing message keys (e.g. `season-status`, `no-permission`) are defined in the per-language files under `plugins/EzSeasons/messages/`. -season: - enabled: true - length-days: 30 - check-interval-minutes: 60 - reminder-minutes: [60, 10, 1] -``` +--- + +## `season` section + +### `season.enabled` + +Master switch. Set to `true` to activate all scheduling, reminders, and resets. + +**Default:** `false` + +--- + +### `season.start` and `season.end` + +Unix epoch milliseconds defining the boundary of an explicit season window. + +- If both are non-zero and `start < end`, EzSeasons uses the window directly to determine when a reset occurs. +- If both are `0` (default), EzSeasons ignores them and uses `length-days` instead. + +**Default:** `0` (ignored) + +--- + +### `season.recurring` + +Applies only when a valid `start`/`end` window is set. When `true`, the plugin automatically advances the window by its length after each reset, so the schedule repeats indefinitely. + +**Default:** `false` + +--- + +### `season.length-days` + +When `start`/`end` are not set, this is the number of days between resets. The next reset is calculated as `last-reset + length-days * 86400000`. + +**Default:** `30` + +--- + +### `season.check-interval-minutes` + +How often (in minutes) EzSeasons checks whether a reset is due or a reminder should be sent. Lower values increase precision but add minor server overhead. + +**Default:** `60` + +--- + +### `season.broadcast-message` + +Message broadcast to all online players when a season reset occurs. Supports `&` color codes. + +**Default:** `"&7A new season has begun! Hearts have been reset."` + +--- + +### `season.reminder-minutes` + +List of minute offsets before the next reset at which a reminder message is sent. For example, `[1440, 60, 10]` sends reminders 24 hours, 1 hour, and 10 minutes before the reset. + +**Default:** `[]` (no reminders) + +--- + +### `season.reminder-message` + +Message sent to all online players at each reminder interval. Supports `&` color codes. + +| Placeholder | Replaced with | +|---|---| +| `%time%` | Human-readable countdown (e.g. `1 day, 2 hours and 30 minutes`) | + +**Default:** `"&7The season will reset in &c%time%&7."` + +--- + +### `season.last-reset` and `season.next-reset` + +Runtime state written by EzSeasons itself. **Do not edit these manually** unless you know what you are doing; use `/season admin setnext` or `/season admin clear-next` instead. + +| Key | Meaning | +|---|---| +| `last-reset` | Unix epoch milliseconds of the most recent season reset | +| `next-reset` | Unix epoch milliseconds of the next scheduled reset; `0` means unscheduled | + +--- + +## See also + +- [Getting started](getting-started) for a minimal setup walkthrough +- [Commands](commands) for runtime overrides via `/season admin` diff --git a/docs/getting-started.md b/docs/getting-started.md index 8585c2e..60d2ceb 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,31 +1,113 @@ +--- +title: Getting Started +nav_order: 2 +description: "Install EzSeasons and set up your first season schedule" +--- + # Getting Started +{: .no_toc } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- -Quick setup guide for server owners. +## Requirements -## 1) Install +| Requirement | Minimum version | +|---|---| +| Server software | Paper 26.1 (or a compatible fork) | +| Java | 25 | -1. Build or download the EzSeasons jar. -2. Place it in your server's `plugins/` folder. -3. Restart the server. +EzSeasons does **not** require EzLifesteal. It is fully standalone. -## 2) Verify load +--- -On startup, confirm EzSeasons is enabled in console logs. +## Installation -## 3) Configure +1. Stop your server. +2. Download `EzSeasons-2.0.0.jar` from [Modrinth](https://modrinth.com/plugin/ezseasons). +3. Copy the jar into your `plugins/` folder. +4. *(Optional)* Install [EzLifesteal](https://modrinth.com/plugin/ezlifesteal) if you want Lifesteal heart-reset behavior on each season reset. +5. Start the server once. EzSeasons generates `plugins/EzSeasons/config.yml` and the `messages/` folder. +6. Stop the server again and edit `config.yml` (see below). +7. Start the server. -Edit `plugins/EzSeasons/config.yml`: +--- -- Set `season.enabled: true` -- Adjust scheduling values (`length-days`, `start`/`end`, reminders) -- Customize `messages` for your network style +## Basic configuration -## 4) Test in game +Open `plugins/EzSeasons/config.yml`. The minimum required change is enabling the plugin and choosing a scheduling mode. -Run: +### Duration-based schedule (recommended for most servers) -```text -/season +The plugin calculates the next reset by adding `length-days` to the last recorded reset. This is the simplest approach. + +```yaml +season: + enabled: true + length-days: 30 # Season lasts 30 days + check-interval-minutes: 60 + broadcast-message: "&7A new season has begun!" + reminder-minutes: + - 1440 # 24 hours before + - 60 # 1 hour before + - 10 # 10 minutes before + reminder-message: "&7The season will reset in &c%time%&7." ``` -You should see either a countdown or a clear status message describing why a countdown is unavailable. +Leave `start`, `end`, and `recurring` at their defaults (`0` / `false`). + +### Fixed-window schedule + +Use `start` and `end` (Unix millisecond timestamps) for a precise one-time or recurring window. + +```yaml +season: + enabled: true + start: 1735689600000 # Window opens 2025-01-01 00:00 UTC + end: 1738368000000 # Window closes 2025-02-01 00:00 UTC + recurring: true # Repeat the same window length each period + check-interval-minutes: 60 +``` + +To get a Unix millisecond timestamp: + +- Visit [epochconverter.com](https://www.epochconverter.com/) and multiply the displayed value by `1000`. +- Or use `/season admin setnext` at runtime (see [Commands](commands)). + +--- + +## Verify the setup + +After starting the server with a valid config: + +1. Run `/season` in-game or in the console — you should see a countdown. +2. Run `/season admin status` to inspect the raw timestamps. + +If you see `Season resets are currently disabled`, confirm `season.enabled: true` is in your config and that you have reloaded (`/season admin reload`). + +--- + +## Choosing a language + +Edit `messages.language` in `config.yml`: + +```yaml +messages: + language: "en" # Supported: en, es, fr, zh, ru, nl +``` + +The corresponding file in `plugins/EzSeasons/messages/` is loaded automatically on startup and reload. + +--- + +## Next steps + +- Read the full [Configuration reference](configuration) for every available option. +- See [Commands](commands) for admin operations like forcing a reset or adjusting the next reset time. +- See [Permissions](permissions) to lock down admin commands on your server. +- If you are a developer, read the [API guide](api) to integrate with EzSeasons from your own plugin. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..9b59e85 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,47 @@ +--- +title: Home +nav_order: 1 +description: "EzSeasons — standalone season reset scheduler for Minecraft servers" +permalink: / +--- + +# EzSeasons +{: .no_toc } + +EzSeasons is a **standalone season reset scheduler** for Paper / Bukkit-compatible Minecraft servers. +It schedules timed season resets, broadcasts announcements, sends configurable countdown reminders, and exposes a developer API so companion plugins can react to each reset. + +--- + +## Feature overview + +| Feature | Details | +|---|---| +| Automatic season resets | Duration-based (`length-days`) or explicit `start`/`end` windows | +| Recurring windows | Optional; re-uses a `start`/`end` window every period | +| Countdown reminders | `reminder-minutes` list; fires a chat message before each reset | +| Broadcast on reset | Configurable broadcast message sent to all online players | +| `/season` command | Players can check the time remaining until the next reset | +| Admin subcommands | `reload`, `reset`, `setnext`, `clear-next`, `status` | +| Multi-language support | `en`, `es`, `fr`, `zh`, `ru`, `nl` | +| Developer API | Bukkit service + Bukkit events; no runtime class-loading tricks | + +--- + +## Compatibility + +| Requirement | Version | +|---|---| +| Minecraft / Paper | 26.1 or later | +| Java | 25 or later | +| Plugin version | 2.0.0 | + +--- + +## Quick navigation + +- [Getting started](getting-started) — install and first-time setup +- [Configuration](configuration) — full `config.yml` reference +- [Commands](commands) — all `/season` commands and their options +- [Permissions](permissions) — permission node reference +- [API](api) — developer integration guide diff --git a/docs/permissions.md b/docs/permissions.md index 1170511..aed2337 100644 --- a/docs/permissions.md +++ b/docs/permissions.md @@ -1,26 +1,48 @@ +--- +title: Permissions +nav_order: 5 +description: "Permission node reference for EzSeasons" +--- + # Permissions +{: .no_toc } + +## Table of contents +{: .no_toc .text-delta } + +1. TOC +{:toc} + +--- + +## Permission nodes + +| Node | Default | Grants access to | +|---|---|---| +| `lifesteal.season` | **All players** (`true`) | `/season` — view the time until the next reset | +| `lifesteal.season.admin` | **Operators** (`op`) | `/season admin ` | +| `lifesteal.admin` | **Operators** (`op`) | Legacy fallback — accepted for both `/season` and `/season admin` commands | -EzSeasons defines three permission nodes. +--- -## `lifesteal.season` +## Notes -- **Default:** `true` -- **Purpose:** allows `/season` status usage (`/season` and `/season status`). +- `lifesteal.admin` is a **legacy fallback**. It was carried over from EzLifesteal for backward compatibility. Prefer granting `lifesteal.season` and `lifesteal.season.admin` explicitly on standalone EzSeasons servers. +- Setting a permission node `default` to `op` in `plugin.yml` means only operators receive it by default. You can override this with any permissions plugin (e.g. LuckPerms). +- There is no separate per-subcommand permission. A player with `lifesteal.season.admin` (or `lifesteal.admin`) can run all admin subcommands. -## `lifesteal.season.admin` +--- -- **Default:** `op` -- **Purpose:** primary admin permission for `/season admin |clear-next|status>`. -- **Also accepted for:** `/season` status command. +## LuckPerms examples -## `lifesteal.admin` +Grant a `moderator` group access to admin commands: -- **Default:** `op` -- **Purpose:** legacy admin fallback for `/season admin |clear-next|status>`. -- **Also accepted for:** `/season` status command. +```shell +/lp group moderator permission set lifesteal.season.admin true +``` -## Recommended setup +Revoke player access to `/season` for a `restricted` group: -- Keep `lifesteal.season` at default `true` for all players. -- Grant `lifesteal.season.admin` to trusted staff. -- Keep `lifesteal.admin` only for backward compatibility with existing permission setups. +```shell +/lp group restricted permission set lifesteal.season false +``` diff --git a/docs/topics/markdown.md b/docs/topics/markdown.md index fe1df33..f54e95a 100644 --- a/docs/topics/markdown.md +++ b/docs/topics/markdown.md @@ -8,6 +8,7 @@ Standalone season reset scheduler for Minecraft servers - EzSeasons EzSeasons schedules and broadcasts season resets for Bukkit and Paper servers. It is a focused reset Minecraft plugin designed for server owners who run periodic wipes, seasons, or server resets. ## Features + - Automatic season reset scheduling (explicit windows or duration-based) - Explicit scheduling: `season.start` and `season.end` - Fallback duration scheduling: `season.length-days` @@ -18,26 +19,29 @@ EzSeasons schedules and broadcasts season resets for Bukkit and Paper servers. I - Developer API: `SeasonsApi`, `SeasonsIntegration`, `SeasonResetEvent` ## Quick Install (Bukkit / Paper) + 1. Drop the plugin jar into your server's `plugins` folder. 2. Start or restart the server. 3. Edit `config.yml` to set schedule and messages. ## Example configuration (config.yml) + ```yaml season: - enabled: true - # Use explicit timestamps (millis) for a specific window, or leave 0 to use length-days - start: 0 - end: 0 - recurring: false - length-days: 30 - check-interval-minutes: 5 - broadcast-message: "Season resets in {time}!" - reminder-minutes: [60, 30, 10, 1] - reminder-message: "Season resets in {minutes} minute(s)!" + enabled: true + # Use explicit timestamps (millis) for a specific window, or leave 0 to use length-days + start: 0 + end: 0 + recurring: false + length-days: 30 + check-interval-minutes: 5 + broadcast-message: "Season resets in {time}!" + reminder-minutes: [60, 30, 10, 1] + reminder-message: "Season resets in {minutes} minute(s)!" ``` ## Commands + - `/season` - show current status and next reset - `/season admin reload` - reload plugin config - `/season admin reset [reason]` - trigger an immediate reset (admin only) @@ -45,28 +49,34 @@ season: - `/season admin clear-next` - clear next-reset override ## Permissions + - `lifesteal.season` (default: true) - allows `/season` - `lifesteal.admin` (default: op) - allows all admin commands ## Developer API + Obtain `SeasonsApi` via Bukkit Services Manager. Available methods: + - `registerIntegration(SeasonsIntegration integration)` - `unregisterIntegration(SeasonsIntegration integration)` - `getIntegrations()` - `triggerSeasonReset(String reason)` Events published: + - `SeasonResetEvent` - `SeasonsIntegrationRegisteredEvent` - `SeasonsIntegrationUnregisteredEvent` ## Best Practices for Server Owners + - Always test reset behavior on a staging server before scheduling production wipes. - Combine EzSeasons with your backup tooling - EzSeasons does not perform backups. - Use `season.reminder-minutes` to announce the reset early and reduce player frustration. ## Support & License -- Source & docs: https://github.com/ez-plugins/EzSeasons -- Download (Modrinth): https://modrinth.com/plugin/ezseasons -- Report issues: https://github.com/ez-plugins/EzSeasons/issues -- License: MIT \ No newline at end of file + +- Source & docs: +- Download (Modrinth): +- Report issues: +- License: MIT diff --git a/jitpack.yml b/jitpack.yml index f7fe680..741ddc1 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -1,5 +1,5 @@ jdk: - - openjdk21 + - openjdk25 install: - mvn --batch-mode --no-transfer-progress clean install -DskipTests diff --git a/plugin/pom.xml b/plugin/pom.xml index 5731dd1..026bca4 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -6,7 +6,7 @@ com.skyblockexp.lifesteal ezseasons-parent - 1.0.0 + 2.0.0 ezseasons-plugin @@ -15,55 +15,77 @@ EzSeasons Plugin Standalone season scheduling plugin with optional integrations + + dev-d245e0a + 6.0.3 + 5.23.0 + 0.8.13 + + - - com.skyblockexp.lifesteal - ezseasons-api - ${project.version} - + io.papermc.paper paper-api ${paper.version} provided + + + + com.skyblockexp.lifesteal + ezseasons-api + ${project.version} + compile + + + - com.github.seeseemelk - MockBukkit-v1.21 - 3.133.2 + org.mockbukkit.mockbukkit + mockbukkit-v26.1 + ${mockbukkit.version} test + + org.junit.jupiter junit-jupiter - 5.12.2 + ${junit.version} test + + org.mockito mockito-core - 5.18.0 + ${mockito.version} test - ezseasons-v${jar.version} + ezseasons-v${project.version} + + src/main/resources true + org.apache.maven.plugins maven-compiler-plugin + + org.apache.maven.plugins maven-shade-plugin - 3.5.3 + 3.6.2 package @@ -81,14 +103,12 @@ - - org.apache.maven.plugins - maven-surefire-plugin - + + org.jacoco jacoco-maven-plugin - 0.8.12 + ${jacoco.version} prepare-agent @@ -96,13 +116,6 @@ prepare-agent - - report - verify - - report - - diff --git a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/Bootstrap.java b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/Bootstrap.java index 1d7cf08..a02082e 100644 --- a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/Bootstrap.java +++ b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/Bootstrap.java @@ -2,7 +2,13 @@ import com.skyblockexp.lifesteal.seasons.api.SeasonsApi; import com.skyblockexp.lifesteal.seasons.command.season.SeasonCommand; +import com.skyblockexp.lifesteal.seasons.compatibility.ServerEnvironment; import com.skyblockexp.lifesteal.seasons.config.MessageService; +import java.io.File; +import java.time.Duration; +import java.util.Locale; +import java.util.Optional; +import java.util.Set; import org.bukkit.Bukkit; import org.bukkit.command.PluginCommand; import org.bukkit.configuration.ConfigurationSection; @@ -11,16 +17,12 @@ import org.bukkit.plugin.ServicePriority; import org.bukkit.scheduler.BukkitTask; -import java.io.File; -import java.time.Duration; -import java.util.Locale; -import java.util.Set; - public class Bootstrap { private static final Set SUPPORTED_LANGUAGES = Set.of("en", "es", "fr", "zh", "ru", "nl"); private final EzSeasonsPlugin plugin; + private final Registry registry; public Bootstrap(EzSeasonsPlugin plugin, Registry registry) { @@ -33,13 +35,13 @@ public void start() { registerApi(); reloadSeasonConfiguration(); registerCommands(); - plugin.getLogger().info("EzSeasons ready. Awaiting plugin integrations via API registration."); + logStartupSummary(); } public void stop() { cancelSeasonCheckTask(); registry.setSeasonManager(null); - SeasonsApiImpl seasonsApi = registry.getSeasonsApi(); + final SeasonsApiImpl seasonsApi = registry.getSeasonsApi(); if (seasonsApi != null) { Bukkit.getServicesManager().unregister(seasonsApi); seasonsApi.clear(); @@ -50,20 +52,57 @@ public void stop() { public synchronized void reloadSeasonConfiguration() { plugin.reloadConfig(); loadMessages(); - ConfigurationSection seasonSection = plugin.getConfig().getConfigurationSection("season"); + final ConfigurationSection seasonSection = plugin.getConfig().getConfigurationSection("season"); registry.setSeasonManager(new SeasonManager(plugin, seasonSection, registry.getMessageService())); restartSeasonCheckTask(); } + private void logStartupSummary() { + final SeasonManager manager = registry.getSeasonManager(); + final String version = plugin.getDescription().getVersion(); + final String language = plugin.getConfig().getString("messages.language", "en"); + final String java = Runtime.version().feature() + "." + Runtime.version().interim(); + + plugin.getLogger().info("EzSeasons v" + version + " | " + ServerEnvironment.brand() + " | Java " + java); + + if (manager != null && manager.isEnabled()) { + final long intervalMins = manager.getCheckInterval().toMinutes(); + plugin.getLogger().info("Schedule : enabled | check every " + intervalMins + " min | language: " + language); + + final Optional timeUntil = manager.getTimeUntilReset(); + if (timeUntil.isPresent()) { + plugin.getLogger().info("Next reset: in " + formatStartupDuration(timeUntil.get())); + } else { + plugin.getLogger().info("Next reset: not yet scheduled (run /season admin setnext to set one)"); + } + } else { + plugin.getLogger().info("Schedule : disabled | language: " + language); + } + } + + private static String formatStartupDuration(Duration d) { + final long totalSeconds = d.getSeconds(); + final long days = totalSeconds / 86400; + final long hours = (totalSeconds % 86400) / 3600; + final long minutes = (totalSeconds % 3600) / 60; + if (days > 0) { + return days + "d " + hours + "h " + minutes + "m"; + } + if (hours > 0) { + return hours + "h " + minutes + "m"; + } + return Math.max(minutes, 1) + "m"; + } + private void registerApi() { - SeasonsApiImpl seasonsApi = new SeasonsApiImpl(plugin); + final SeasonsApiImpl seasonsApi = new SeasonsApiImpl(plugin); registry.setSeasonsApi(seasonsApi); Bukkit.getServicesManager().register(SeasonsApi.class, seasonsApi, plugin, ServicePriority.Normal); } public void loadMessages() { - FileConfiguration configuration = plugin.getConfig(); - String prefix = configuration.getString("messages.prefix", "&c[EzSeasons]&r "); + final FileConfiguration configuration = plugin.getConfig(); + final String prefix = configuration.getString("messages.prefix", "&c[EzSeasons]&r "); String selectedLanguage = configuration.getString("messages.language", "en").toLowerCase(Locale.ROOT); if (!SUPPORTED_LANGUAGES.contains(selectedLanguage)) { plugin.getLogger().warning("Unsupported messages.language '" + selectedLanguage + "'. Falling back to 'en'."); @@ -74,7 +113,7 @@ public void loadMessages() { ensureMessageFiles(); - File messagesFolder = new File(plugin.getDataFolder(), "messages"); + final File messagesFolder = new File(plugin.getDataFolder(), "messages"); registerMessages(loadLanguageSection(messagesFolder, "en"), "", Set.of()); if (!"en".equals(selectedLanguage)) { @@ -85,25 +124,25 @@ public void loadMessages() { } private ConfigurationSection loadLanguageSection(File messagesFolder, String language) { - File languageFile = new File(messagesFolder, language + ".yml"); + final File languageFile = new File(messagesFolder, language + ".yml"); if (!languageFile.exists()) { plugin.getLogger().warning("Missing message file: " + languageFile.getName() + "."); return null; } - YamlConfiguration languageConfiguration = YamlConfiguration.loadConfiguration(languageFile); + final YamlConfiguration languageConfiguration = YamlConfiguration.loadConfiguration(languageFile); return languageConfiguration.getConfigurationSection("messages") == null ? languageConfiguration : languageConfiguration.getConfigurationSection("messages"); } private void ensureMessageFiles() { - File messagesFolder = new File(plugin.getDataFolder(), "messages"); + final File messagesFolder = new File(plugin.getDataFolder(), "messages"); if (!messagesFolder.exists() && !messagesFolder.mkdirs()) { plugin.getLogger().warning("Unable to create messages folder at " + messagesFolder.getAbsolutePath()); return; } for (String language : SUPPORTED_LANGUAGES) { - File destination = new File(messagesFolder, language + ".yml"); + final File destination = new File(messagesFolder, language + ".yml"); if (!destination.exists()) { plugin.saveResource("messages/" + language + ".yml", false); } @@ -118,7 +157,7 @@ private void registerMessages(ConfigurationSection section, String path, Set { - SeasonManager manager = registry.getSeasonManager(); + final long periodTicks = durationToTicks(seasonManager.getCheckInterval()); + final BukkitTask seasonCheckTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> { + final SeasonManager manager = registry.getSeasonManager(); if (manager == null || !manager.isEnabled()) { return; } @@ -160,7 +199,7 @@ private synchronized void restartSeasonCheckTask() { } private synchronized void cancelSeasonCheckTask() { - BukkitTask seasonCheckTask = registry.getSeasonCheckTask(); + final BukkitTask seasonCheckTask = registry.getSeasonCheckTask(); if (seasonCheckTask != null) { seasonCheckTask.cancel(); registry.setSeasonCheckTask(null); @@ -168,7 +207,7 @@ private synchronized void cancelSeasonCheckTask() { } private long durationToTicks(Duration duration) { - long seconds = Math.max(1L, duration.getSeconds()); + final long seconds = Math.max(1L, duration.getSeconds()); return Math.max(20L, seconds * 20L); } } diff --git a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/EzSeasonsPlugin.java b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/EzSeasonsPlugin.java index 84c4fd5..7b4ec97 100644 --- a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/EzSeasonsPlugin.java +++ b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/EzSeasonsPlugin.java @@ -6,6 +6,7 @@ public class EzSeasonsPlugin extends JavaPlugin { private final Registry registry = new Registry(); + private final Bootstrap bootstrap = new Bootstrap(this, registry); @Override @@ -30,6 +31,8 @@ private void loadMessages() { /** * Called by external integrations (for example EzLifesteal) to provide the * active {@link SeasonManager} implementation used by EzSeasons. + * + * @param manager the season manager to register; must not be {@code null} */ public void registerSeasonManager(SeasonManager manager) { registry.setSeasonManager(manager); diff --git a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/Registry.java b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/Registry.java index 6d7be86..537ad30 100644 --- a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/Registry.java +++ b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/Registry.java @@ -6,8 +6,11 @@ public class Registry { private SeasonsApiImpl seasonsApi; + private SeasonManager seasonManager; + private MessageService messageService; + private BukkitTask seasonCheckTask; public SeasonsApiImpl getSeasonsApi() { diff --git a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/SeasonManager.java b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/SeasonManager.java index aa4014d..c3367fb 100644 --- a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/SeasonManager.java +++ b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/SeasonManager.java @@ -2,31 +2,43 @@ import com.skyblockexp.lifesteal.seasons.api.events.SeasonResetEvent; import com.skyblockexp.lifesteal.seasons.config.MessageService; -import org.bukkit.Bukkit; -import org.bukkit.ChatColor; -import org.bukkit.configuration.ConfigurationSection; - import java.time.Duration; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Optional; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.configuration.ConfigurationSection; public class SeasonManager { private final EzSeasonsPlugin plugin; + private final MessageService messageService; + private final boolean enabled; + private final Duration seasonLength; + private final Duration checkInterval; + private final boolean explicitSeason; + private final boolean recurring; + private long seasonStartMillis; + private long seasonEndMillis; + private final String broadcastMessage; + private final String reminderMessage; + private final List reminderOffsets; + private long lastResetMillis; + private long nextResetMillis; private final ConfigurationSection configSection; @@ -38,7 +50,7 @@ public SeasonManager(EzSeasonsPlugin plugin, this.messageService = messageService; this.configSection = section; - SeasonSettings settings = SeasonSettings.from(section).validate(plugin); + final SeasonSettings settings = SeasonSettings.from(section).validate(plugin); this.enabled = settings.enabled; this.explicitSeason = settings.explicit; @@ -63,8 +75,8 @@ public Duration getCheckInterval() { } public synchronized void triggerSeasonReset(String reason) { - long previousReset = lastResetMillis; - long resetAt = System.currentTimeMillis(); + final long previousReset = lastResetMillis; + final long resetAt = System.currentTimeMillis(); lastResetMillis = resetAt; nextResetMillis = seasonLength.toMillis() > 0 ? resetAt + seasonLength.toMillis() : 0L; persistResetTimestamps(); @@ -105,8 +117,8 @@ public boolean shouldTriggerReset(long nowMillis) { return false; } - long scheduledNextReset; - long recordedLastReset; + final long scheduledNextReset; + final long recordedLastReset; synchronized (this) { scheduledNextReset = nextResetMillis; recordedLastReset = lastResetMillis; @@ -125,24 +137,24 @@ public boolean shouldTriggerReset(long nowMillis) { } public Optional getTimeUntilReset() { - long scheduledNextReset; - long recordedLastReset; + final long scheduledNextReset; + final long recordedLastReset; synchronized (this) { scheduledNextReset = nextResetMillis; recordedLastReset = lastResetMillis; } if (scheduledNextReset > 0) { - long now = System.currentTimeMillis(); - long diff = scheduledNextReset - now; + final long now = System.currentTimeMillis(); + final long diff = scheduledNextReset - now; if (diff > 0) { return Optional.of(Duration.ofMillis(diff)); } } if (seasonLength.toMillis() > 0 && recordedLastReset > 0) { - long next = recordedLastReset + seasonLength.toMillis(); - long now = System.currentTimeMillis(); - long diff = next - now; + final long next = recordedLastReset + seasonLength.toMillis(); + final long now = System.currentTimeMillis(); + final long diff = next - now; if (diff > 0) { return Optional.of(Duration.ofMillis(diff)); } @@ -151,9 +163,9 @@ public Optional getTimeUntilReset() { } public String formatDuration(Duration duration) { - long seconds = duration.getSeconds(); - long absSeconds = Math.abs(seconds); - String positive = String.format("%d:%02d:%02d", absSeconds / 3600, (absSeconds % 3600) / 60, absSeconds % 60); + final long seconds = duration.getSeconds(); + final long absSeconds = Math.abs(seconds); + final String positive = String.format("%d:%02d:%02d", absSeconds / 3600, (absSeconds % 3600) / 60, absSeconds % 60); return seconds < 0 ? "-" + positive : positive; } @@ -170,19 +182,31 @@ private synchronized void persistResetTimestamps() { private static final class SeasonSettings { private static final String DEFAULT_BROADCAST = "&7A new season has begun! Hearts have been reset."; + private static final String DEFAULT_REMINDER = "&7The season will reset in &c%time%&7."; private final boolean enabled; + private final long start; + private final long end; + private final Duration length; + private final Duration checkInterval; + private final String broadcastMessage; + private final String reminderMessage; + private final List reminderOffsets; + private final long lastReset; + private final long nextReset; + private final boolean recurring; + private final boolean explicit; private SeasonSettings(boolean enabled, @@ -212,7 +236,8 @@ private SeasonSettings(boolean enabled, } private SeasonSettings withRecurring(boolean recurring) { - return new SeasonSettings(enabled, start, end, length, checkInterval, broadcastMessage, reminderMessage, reminderOffsets, lastReset, nextReset, recurring, explicit); + return new SeasonSettings(enabled, start, end, length, checkInterval, + broadcastMessage, reminderMessage, reminderOffsets, lastReset, nextReset, recurring, explicit); } private static SeasonSettings from(ConfigurationSection section) { @@ -233,18 +258,18 @@ private static SeasonSettings from(ConfigurationSection section) { ); } - boolean enabled = section.getBoolean("enabled", false); - long start = section.getLong("start", 0L); - long end = section.getLong("end", 0L); - Duration length = safeDurationDays(section.getLong("length-days", 30L)); - Duration checkInterval = safeDurationMinutes(section.getLong("check-interval-minutes", 60L)); - String message = section.getString("broadcast-message", DEFAULT_BROADCAST); - String reminder = section.getString("reminder-message", DEFAULT_REMINDER); - long lastReset = section.getLong("last-reset", 0L); - long nextReset = section.getLong("next-reset", 0L); - boolean recurring = section.getBoolean("recurring", false); - List reminders = parseAndSortReminderOffsets(section.getIntegerList("reminder-minutes")); - boolean explicit = start > 0L && end > start; + final boolean enabled = section.getBoolean("enabled", false); + final long start = section.getLong("start", 0L); + final long end = section.getLong("end", 0L); + final Duration length = safeDurationDays(section.getLong("length-days", 30L)); + final Duration checkInterval = safeDurationMinutes(section.getLong("check-interval-minutes", 60L)); + final String message = section.getString("broadcast-message", DEFAULT_BROADCAST); + final String reminder = section.getString("reminder-message", DEFAULT_REMINDER); + final long lastReset = section.getLong("last-reset", 0L); + final long nextReset = section.getLong("next-reset", 0L); + final boolean recurring = section.getBoolean("recurring", false); + final List reminders = parseAndSortReminderOffsets(section.getIntegerList("reminder-minutes")); + final boolean explicit = start > 0L && end > start; return new SeasonSettings(enabled, start, end, length, checkInterval, message, reminder, reminders, lastReset, nextReset, recurring, explicit); } @@ -252,10 +277,12 @@ private static SeasonSettings from(ConfigurationSection section) { private SeasonSettings validate(EzSeasonsPlugin plugin) { SeasonSettings settings = this; if (plugin != null && (start > 0L || end > 0L) && !explicit) { - plugin.getLogger().warning("Season end must be after the start time. Falling back to duration based scheduling."); + plugin.getLogger().warning( + "Season end must be after the start time. Falling back to duration based scheduling."); } if (plugin != null && recurring && !explicit) { - plugin.getLogger().warning("Season recurrence requires explicit start and end timestamps. Ignoring the recurring setting."); + plugin.getLogger().warning( + "Season recurrence requires explicit start and end timestamps. Ignoring the recurring setting."); settings = settings.withRecurring(false); } return settings; @@ -270,7 +297,7 @@ private static Duration safeDurationMinutes(long minutes) { } private static List parseAndSortReminderOffsets(List reminderMinutes) { - List reminders = new ArrayList<>(); + final List reminders = new ArrayList<>(); for (Integer minutes : reminderMinutes) { if (minutes != null && minutes > 0) { reminders.add(Duration.ofMinutes(minutes)); diff --git a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/SeasonsApiImpl.java b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/SeasonsApiImpl.java index 242e6ad..d0cacf8 100644 --- a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/SeasonsApiImpl.java +++ b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/SeasonsApiImpl.java @@ -4,15 +4,15 @@ import com.skyblockexp.lifesteal.seasons.api.SeasonsIntegration; import com.skyblockexp.lifesteal.seasons.api.events.SeasonsIntegrationRegisteredEvent; import com.skyblockexp.lifesteal.seasons.api.events.SeasonsIntegrationUnregisteredEvent; -import org.bukkit.Bukkit; - import java.util.Objects; import java.util.logging.Level; import java.util.logging.Logger; +import org.bukkit.Bukkit; final class SeasonsApiImpl implements SeasonsApi { private final EzSeasonsPlugin plugin; + private final java.util.List integrations = new java.util.ArrayList<>(); SeasonsApiImpl(EzSeasonsPlugin plugin) { @@ -28,7 +28,8 @@ public synchronized boolean registerIntegration(SeasonsIntegration integration) integrations.add(integration); try { integration.onRegister(this); - } catch (Throwable throwable) { + } + catch (Throwable throwable) { integrations.remove(integration); logLifecycleFailure("register", integration, throwable); throw propagate(throwable); @@ -42,7 +43,8 @@ public synchronized void unregisterIntegration(SeasonsIntegration integration) { if (integrations.remove(integration)) { try { integration.onUnregister(); - } catch (Throwable throwable) { + } + catch (Throwable throwable) { integrations.add(integration); logLifecycleFailure("unregister", integration, throwable); throw propagate(throwable); diff --git a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/SeasonPaperCommand.java b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/SeasonPaperCommand.java index d245774..1bc9cff 100644 --- a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/SeasonPaperCommand.java +++ b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/SeasonPaperCommand.java @@ -2,19 +2,19 @@ import com.skyblockexp.lifesteal.seasons.EzSeasonsPlugin; import com.skyblockexp.lifesteal.seasons.command.season.SeasonCommand; +import java.util.Objects; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.command.PluginIdentifiableCommand; import org.bukkit.plugin.Plugin; -import java.util.Objects; - /** * Paper command adapter that delegates execution to the structured {@link SeasonCommand} handler. */ public final class SeasonPaperCommand extends Command implements PluginIdentifiableCommand { private final Plugin plugin; + private final SeasonCommand executor; public SeasonPaperCommand(EzSeasonsPlugin plugin, SeasonCommand executor) { diff --git a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/framework/Command.java b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/framework/Command.java index 5ea6729..2b5657e 100644 --- a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/framework/Command.java +++ b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/framework/Command.java @@ -1,17 +1,17 @@ package com.skyblockexp.lifesteal.seasons.command.framework; -import org.bukkit.command.CommandSender; - import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Optional; +import org.bukkit.command.CommandSender; public abstract class Command { private final String name; + private final List subcommands = new ArrayList<>(); protected Command(String name) { @@ -35,27 +35,27 @@ public boolean execute(CommandSender sender, String[] args) { return onDefault(sender); } - Optional subcommand = findSubcommand(args[0]); + final Optional subcommand = findSubcommand(args[0]); if (subcommand.isEmpty()) { return onUnknownSubcommand(sender, args[0]); } - String[] remaining = Arrays.copyOfRange(args, 1, args.length); + final String[] remaining = Arrays.copyOfRange(args, 1, args.length); return subcommand.get().execute(sender, remaining); } public List tabComplete(CommandSender sender, String[] args) { if (args.length <= 1) { - String input = args.length == 0 ? "" : args[0]; + final String input = args.length == 0 ? "" : args[0]; return filterSubcommandNames(input); } - Optional subcommand = findSubcommand(args[0]); + final Optional subcommand = findSubcommand(args[0]); if (subcommand.isEmpty()) { return List.of(); } - String[] remaining = Arrays.copyOfRange(args, 1, args.length); + final String[] remaining = Arrays.copyOfRange(args, 1, args.length); return subcommand.get().tabComplete(sender, remaining); } @@ -68,14 +68,14 @@ protected Optional findSubcommand(String input) { } protected List filterSubcommandNames(String input) { - String lowerInput = input.toLowerCase(Locale.ROOT); - LinkedHashSet names = new LinkedHashSet<>(); + final String lowerInput = input.toLowerCase(Locale.ROOT); + final LinkedHashSet names = new LinkedHashSet<>(); for (Subcommand subcommand : subcommands) { names.addAll(subcommand.names()); } - LinkedHashSet matches = new LinkedHashSet<>(); + final LinkedHashSet matches = new LinkedHashSet<>(); for (String value : names) { - String lowerValue = value.toLowerCase(Locale.ROOT); + final String lowerValue = value.toLowerCase(Locale.ROOT); if (lowerValue.startsWith(lowerInput)) { matches.add(value); } diff --git a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/framework/Subcommand.java b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/framework/Subcommand.java index 9678437..9317f47 100644 --- a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/framework/Subcommand.java +++ b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/framework/Subcommand.java @@ -1,15 +1,15 @@ package com.skyblockexp.lifesteal.seasons.command.framework; -import org.bukkit.command.CommandSender; - import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.stream.Stream; +import org.bukkit.command.CommandSender; public abstract class Subcommand { private final String name; + private final List aliases; protected Subcommand(String name, List aliases) { @@ -32,7 +32,7 @@ public List names() { if (aliases.isEmpty()) { return List.of(name); } - List names = new ArrayList<>(); + final List names = new ArrayList<>(); names.add(name); names.addAll(aliases); return names; @@ -45,9 +45,9 @@ public List tabComplete(CommandSender sender, String[] args) { } protected List filter(List values, String input) { - String lowerInput = input.toLowerCase(Locale.ROOT); - List matches = new ArrayList<>(); - Stream stream = values.stream(); + final String lowerInput = input.toLowerCase(Locale.ROOT); + final List matches = new ArrayList<>(); + final Stream stream = values.stream(); stream.filter(value -> value.toLowerCase(Locale.ROOT).startsWith(lowerInput)).forEach(matches::add); return matches; } diff --git a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/SeasonCommand.java b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/SeasonCommand.java index ecd3583..2c78090 100644 --- a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/SeasonCommand.java +++ b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/SeasonCommand.java @@ -5,16 +5,16 @@ import com.skyblockexp.lifesteal.seasons.command.season.subcommand.AdminSubcommand; import com.skyblockexp.lifesteal.seasons.command.season.subcommand.ReloadSubcommand; import com.skyblockexp.lifesteal.seasons.command.season.subcommand.StatusSubcommand; +import java.util.List; +import java.util.Map; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; import org.bukkit.command.TabCompleter; -import java.util.List; -import java.util.Map; - public final class SeasonCommand extends Command implements CommandExecutor, TabCompleter { private final SeasonCommandContext context; + private final StatusSubcommand statusSubcommand; public SeasonCommand(EzSeasonsPlugin plugin) { diff --git a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/SeasonCommandContext.java b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/SeasonCommandContext.java index 581349e..3cd72d4 100644 --- a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/SeasonCommandContext.java +++ b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/SeasonCommandContext.java @@ -2,8 +2,6 @@ import com.skyblockexp.lifesteal.seasons.EzSeasonsPlugin; import com.skyblockexp.lifesteal.seasons.SeasonManager; -import org.bukkit.command.CommandSender; - import java.time.Duration; import java.time.Instant; import java.time.ZoneOffset; @@ -11,13 +9,17 @@ import java.util.Arrays; import java.util.Map; import java.util.Optional; +import org.bukkit.command.CommandSender; public final class SeasonCommandContext { static final String SEASON_ADMIN_NODE = "lifesteal.season.admin"; + static final String LEGACY_ADMIN_NODE = "lifesteal.admin"; - private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(ZoneOffset.UTC); + private static final DateTimeFormatter TIME_FORMATTER = + DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(ZoneOffset.UTC); + private static final long MAX_TIMESTAMP_MILLIS = 32_503_680_000_000L; private final EzSeasonsPlugin plugin; @@ -35,7 +37,7 @@ public boolean hasAdminPermission(CommandSender sender) { } public SeasonManager requireEnabledSeasonManager(CommandSender sender) { - SeasonManager seasonManager = plugin.getSeasonManager(); + final SeasonManager seasonManager = plugin.getSeasonManager(); if (seasonManager == null || !seasonManager.isEnabled()) { plugin.getMessageService().sendMessage(sender, "season-disabled"); return null; @@ -44,16 +46,16 @@ public SeasonManager requireEnabledSeasonManager(CommandSender sender) { } public boolean sendSeasonStatus(CommandSender sender) { - SeasonManager seasonManager = requireEnabledSeasonManager(sender); + final SeasonManager seasonManager = requireEnabledSeasonManager(sender); if (seasonManager == null) { return true; } - Optional timeUntilReset = seasonManager.getTimeUntilReset(); + final Optional timeUntilReset = seasonManager.getTimeUntilReset(); if (timeUntilReset.isEmpty()) { plugin.getMessageService().sendMessage(sender, "season-status-unknown"); return true; } - String formatted = seasonManager.formatDuration(timeUntilReset.get()); + final String formatted = seasonManager.formatDuration(timeUntilReset.get()); plugin.getMessageService().sendMessage(sender, "season-status", Map.of("time", formatted)); return true; } @@ -62,7 +64,8 @@ public Long parseTimestamp(String value, CommandSender sender) { final long millis; try { millis = Long.parseLong(value); - } catch (NumberFormatException ex) { + } + catch (NumberFormatException ex) { plugin.getMessageService().sendMessage(sender, "admin-setnext-invalid-timestamp", Map.of("value", value, "expected", "unix epoch milliseconds (example: 1735689600000)")); return null; @@ -89,7 +92,7 @@ public String extractReason(String[] args) { if (args.length == 0) { return "admin"; } - String reason = Arrays.stream(args) + final String reason = Arrays.stream(args) .filter(token -> !"--confirm".equalsIgnoreCase(token)) .reduce((left, right) -> left + " " + right) .orElse("") diff --git a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/AdminSubcommand.java b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/AdminSubcommand.java index 413c61a..f3e2156 100644 --- a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/AdminSubcommand.java +++ b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/AdminSubcommand.java @@ -7,16 +7,16 @@ import com.skyblockexp.lifesteal.seasons.command.season.subcommand.admin.AdminResetSubcommand; import com.skyblockexp.lifesteal.seasons.command.season.subcommand.admin.AdminSetNextSubcommand; import com.skyblockexp.lifesteal.seasons.command.season.subcommand.admin.AdminStatusSubcommand; -import org.bukkit.command.CommandSender; - import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Optional; +import org.bukkit.command.CommandSender; public final class AdminSubcommand extends Subcommand { private final SeasonCommandContext context; + private final List subcommands; public AdminSubcommand(SeasonCommandContext context) { @@ -43,7 +43,7 @@ public boolean execute(CommandSender sender, String[] args) { return true; } - Optional selected = findSubcommand(args[0]); + final Optional selected = findSubcommand(args[0]); if (selected.isEmpty()) { context.getPlugin().getMessageService().sendMessage(sender, "admin-unknown-subcommand", Map.of("subcommand", args[0], "usage", "/season admin ")); @@ -56,10 +56,10 @@ public boolean execute(CommandSender sender, String[] args) { @Override public List tabComplete(CommandSender sender, String[] args) { if (args.length <= 1) { - String input = args.length == 0 ? "" : args[0]; + final String input = args.length == 0 ? "" : args[0]; return filter(subcommands.stream().flatMap(s -> s.names().stream()).toList(), input); } - Optional selected = findSubcommand(args[0]); + final Optional selected = findSubcommand(args[0]); if (selected.isEmpty()) { return List.of(); } diff --git a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/ReloadSubcommand.java b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/ReloadSubcommand.java index ea04245..12fc5a6 100644 --- a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/ReloadSubcommand.java +++ b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/ReloadSubcommand.java @@ -2,9 +2,8 @@ import com.skyblockexp.lifesteal.seasons.command.framework.Subcommand; import com.skyblockexp.lifesteal.seasons.command.season.SeasonCommandContext; -import org.bukkit.command.CommandSender; - import java.util.List; +import org.bukkit.command.CommandSender; public final class ReloadSubcommand extends Subcommand { diff --git a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/StatusSubcommand.java b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/StatusSubcommand.java index b522bd7..7a34342 100644 --- a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/StatusSubcommand.java +++ b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/StatusSubcommand.java @@ -2,9 +2,8 @@ import com.skyblockexp.lifesteal.seasons.command.framework.Subcommand; import com.skyblockexp.lifesteal.seasons.command.season.SeasonCommandContext; -import org.bukkit.command.CommandSender; - import java.util.List; +import org.bukkit.command.CommandSender; public final class StatusSubcommand extends Subcommand { diff --git a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/admin/AdminClearNextSubcommand.java b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/admin/AdminClearNextSubcommand.java index 2015d9c..dec3c5b 100644 --- a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/admin/AdminClearNextSubcommand.java +++ b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/admin/AdminClearNextSubcommand.java @@ -3,10 +3,9 @@ import com.skyblockexp.lifesteal.seasons.SeasonManager; import com.skyblockexp.lifesteal.seasons.command.framework.Subcommand; import com.skyblockexp.lifesteal.seasons.command.season.SeasonCommandContext; -import org.bukkit.command.CommandSender; - import java.util.List; import java.util.Map; +import org.bukkit.command.CommandSender; public final class AdminClearNextSubcommand extends Subcommand { @@ -19,7 +18,7 @@ public AdminClearNextSubcommand(SeasonCommandContext context) { @Override public boolean execute(CommandSender sender, String[] args) { - SeasonManager seasonManager = context.getPlugin().getSeasonManager(); + final SeasonManager seasonManager = context.getPlugin().getSeasonManager(); if (seasonManager == null || !seasonManager.isEnabled()) { context.getPlugin().getMessageService().sendMessage(sender, "season-disabled"); return true; diff --git a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/admin/AdminReloadSubcommand.java b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/admin/AdminReloadSubcommand.java index 1c15091..ca3122d 100644 --- a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/admin/AdminReloadSubcommand.java +++ b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/admin/AdminReloadSubcommand.java @@ -2,9 +2,8 @@ import com.skyblockexp.lifesteal.seasons.command.framework.Subcommand; import com.skyblockexp.lifesteal.seasons.command.season.SeasonCommandContext; -import org.bukkit.command.CommandSender; - import java.util.List; +import org.bukkit.command.CommandSender; public final class AdminReloadSubcommand extends Subcommand { diff --git a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/admin/AdminResetSubcommand.java b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/admin/AdminResetSubcommand.java index a6c0174..502f57c 100644 --- a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/admin/AdminResetSubcommand.java +++ b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/admin/AdminResetSubcommand.java @@ -3,10 +3,9 @@ import com.skyblockexp.lifesteal.seasons.SeasonManager; import com.skyblockexp.lifesteal.seasons.command.framework.Subcommand; import com.skyblockexp.lifesteal.seasons.command.season.SeasonCommandContext; -import org.bukkit.command.CommandSender; - import java.util.List; import java.util.Map; +import org.bukkit.command.CommandSender; public final class AdminResetSubcommand extends Subcommand { @@ -19,7 +18,7 @@ public AdminResetSubcommand(SeasonCommandContext context) { @Override public boolean execute(CommandSender sender, String[] args) { - SeasonManager seasonManager = context.requireEnabledSeasonManager(sender); + final SeasonManager seasonManager = context.requireEnabledSeasonManager(sender); if (seasonManager == null) { return true; } @@ -30,7 +29,7 @@ public boolean execute(CommandSender sender, String[] args) { return true; } - String reason = context.extractReason(args); + final String reason = context.extractReason(args); seasonManager.triggerSeasonReset(reason); context.getPlugin().getMessageService().sendMessage(sender, "admin-reset-success", Map.of("reason", reason)); return true; diff --git a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/admin/AdminSetNextSubcommand.java b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/admin/AdminSetNextSubcommand.java index a4725be..dd2e470 100644 --- a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/admin/AdminSetNextSubcommand.java +++ b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/admin/AdminSetNextSubcommand.java @@ -3,10 +3,9 @@ import com.skyblockexp.lifesteal.seasons.SeasonManager; import com.skyblockexp.lifesteal.seasons.command.framework.Subcommand; import com.skyblockexp.lifesteal.seasons.command.season.SeasonCommandContext; -import org.bukkit.command.CommandSender; - import java.util.List; import java.util.Map; +import org.bukkit.command.CommandSender; public final class AdminSetNextSubcommand extends Subcommand { @@ -19,7 +18,7 @@ public AdminSetNextSubcommand(SeasonCommandContext context) { @Override public boolean execute(CommandSender sender, String[] args) { - SeasonManager seasonManager = context.requireEnabledSeasonManager(sender); + final SeasonManager seasonManager = context.requireEnabledSeasonManager(sender); if (seasonManager == null) { return true; } @@ -28,7 +27,7 @@ public boolean execute(CommandSender sender, String[] args) { return true; } - Long millis = context.parseTimestamp(args[0], sender); + final Long millis = context.parseTimestamp(args[0], sender); if (millis == null) { return true; } diff --git a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/admin/AdminStatusSubcommand.java b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/admin/AdminStatusSubcommand.java index 826dde0..b9388f3 100644 --- a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/admin/AdminStatusSubcommand.java +++ b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/command/season/subcommand/admin/AdminStatusSubcommand.java @@ -3,10 +3,9 @@ import com.skyblockexp.lifesteal.seasons.SeasonManager; import com.skyblockexp.lifesteal.seasons.command.framework.Subcommand; import com.skyblockexp.lifesteal.seasons.command.season.SeasonCommandContext; -import org.bukkit.command.CommandSender; - import java.util.List; import java.util.Map; +import org.bukkit.command.CommandSender; public final class AdminStatusSubcommand extends Subcommand { @@ -19,7 +18,7 @@ public AdminStatusSubcommand(SeasonCommandContext context) { @Override public boolean execute(CommandSender sender, String[] args) { - SeasonManager seasonManager = context.requireEnabledSeasonManager(sender); + final SeasonManager seasonManager = context.requireEnabledSeasonManager(sender); if (seasonManager == null) { return true; } diff --git a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/compatibility/ReflectionScheduler.java b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/compatibility/ReflectionScheduler.java index 9d446f2..27147b2 100644 --- a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/compatibility/ReflectionScheduler.java +++ b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/compatibility/ReflectionScheduler.java @@ -5,7 +5,7 @@ public final class ReflectionScheduler { - private ReflectionScheduler() {} + private ReflectionScheduler() { } // Minimal main thread execution public static void runMain(Plugin plugin, Runnable r) { @@ -19,7 +19,7 @@ public static Object runDelayed(Plugin plugin, Runnable r, long delayTicks) { // Minimal repeating async execution public static Object runRepeatingAsync(Plugin plugin, Runnable r, long seconds) { - long ticks = Math.max(20L, seconds * 20L); + final long ticks = Math.max(20L, seconds * 20L); return Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, r, ticks, ticks); } diff --git a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/compatibility/ServerEnvironment.java b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/compatibility/ServerEnvironment.java index 9278828..cba625e 100644 --- a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/compatibility/ServerEnvironment.java +++ b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/compatibility/ServerEnvironment.java @@ -3,34 +3,66 @@ import org.bukkit.Bukkit; public final class ServerEnvironment { + private static final boolean HAS_ASYNC_SCHEDULER = hasMethod(Bukkit.class, "getAsyncScheduler"); + private static final boolean HAS_GLOBAL_REGION_SCHEDULER = hasMethod(Bukkit.class, "getGlobalRegionScheduler"); - private static final boolean FOLIA = HAS_ASYNC_SCHEDULER || hasClass("io.papermc.paper.threadedregions.scheduler.AsyncScheduler"); + private static final boolean FOLIA = + HAS_ASYNC_SCHEDULER || hasClass("io.papermc.paper.threadedregions.scheduler.AsyncScheduler"); + private static final String BRAND; static { - if (FOLIA) BRAND = "Folia"; - else if (hasClass("io.papermc.paper.util.TickThread") || hasClass("com.destroystokyo.paper.PaperVersionFetcher")) BRAND = "Paper"; - else BRAND = Bukkit.getServer().getName(); // CraftBukkit/Bukkit/etc. + if (FOLIA) { + BRAND = "Folia"; + } + else if (hasClass("io.papermc.paper.util.TickThread") + || hasClass("com.destroystokyo.paper.PaperVersionFetcher")) { + BRAND = "Paper"; + } + else { + BRAND = Bukkit.getServer().getName(); // CraftBukkit/Bukkit/etc. + } + } + + private ServerEnvironment() { } + + public static boolean isFolia() { + return FOLIA; } - private ServerEnvironment() {} + public static boolean hasAsyncScheduler() { + return HAS_ASYNC_SCHEDULER; + } - public static boolean isFolia() { return FOLIA; } - public static boolean hasAsyncScheduler() { return HAS_ASYNC_SCHEDULER; } - public static boolean hasGlobalRegionScheduler() { return HAS_GLOBAL_REGION_SCHEDULER; } - public static String brand() { return BRAND; } + public static boolean hasGlobalRegionScheduler() { + return HAS_GLOBAL_REGION_SCHEDULER; + } + + public static String brand() { + return BRAND; + } private static boolean hasMethod(Class type, String name, Class... params) { - try { type.getMethod(name, params); return true; } - catch (NoSuchMethodException e) { return false; } + try { + type.getMethod(name, params); + return true; + } + catch (NoSuchMethodException e) { + return false; + } } private static boolean hasClass(String name) { - try { Class.forName(name, false, ServerEnvironment.class.getClassLoader()); return true; } - catch (ClassNotFoundException e) { return false; } + try { + Class.forName(name, false, ServerEnvironment.class.getClassLoader()); + return true; + } + catch (ClassNotFoundException e) { + return false; + } } } diff --git a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/config/MessageService.java b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/config/MessageService.java index 05eb2b9..3901761 100644 --- a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/config/MessageService.java +++ b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/config/MessageService.java @@ -1,13 +1,13 @@ package com.skyblockexp.lifesteal.seasons.config; -import org.bukkit.ChatColor; -import org.bukkit.command.CommandSender; - import java.util.HashMap; import java.util.Map; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; public class MessageService { private final String prefix; + private final Map messages = new HashMap<>(); public MessageService(String prefix) { @@ -23,7 +23,7 @@ public String getMessage(String key) { } public void sendMessage(CommandSender sender, String key, Map placeholders) { - String message = render(key, placeholders); + final String message = render(key, placeholders); if (message.isEmpty()) { return; } @@ -35,7 +35,7 @@ public void sendMessage(CommandSender sender, String key) { } public String format(String key, Map placeholders) { - String message = render(key, placeholders); + final String message = render(key, placeholders); return prefix + message; } diff --git a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/integration/LifestealIntegration.java b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/integration/LifestealIntegration.java index b7594d0..f6839c7 100644 --- a/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/integration/LifestealIntegration.java +++ b/plugin/src/main/java/com/skyblockexp/lifesteal/seasons/integration/LifestealIntegration.java @@ -1,10 +1,9 @@ package com.skyblockexp.lifesteal.seasons.integration; -import org.bukkit.entity.Player; - import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import org.bukkit.entity.Player; public interface LifestealIntegration { diff --git a/plugin/src/main/resources/plugin.yml b/plugin/src/main/resources/plugin.yml index ea43c62..857816f 100644 --- a/plugin/src/main/resources/plugin.yml +++ b/plugin/src/main/resources/plugin.yml @@ -1,9 +1,9 @@ name: EzSeasons version: ${project.version} main: com.skyblockexp.lifesteal.seasons.EzSeasonsPlugin -author: EzTeam +author: Shadow48402 description: Standalone season scheduling plugin with optional integrations -api-version: 1.21 +api-version: 26.1 commands: season: description: View season status or run admin season subcommands diff --git a/plugin/src/test/java/com/skyblockexp/lifesteal/seasons/BootstrapUnitTest.java b/plugin/src/test/java/com/skyblockexp/lifesteal/seasons/BootstrapUnitTest.java index 9db05ec..b8df5e6 100644 --- a/plugin/src/test/java/com/skyblockexp/lifesteal/seasons/BootstrapUnitTest.java +++ b/plugin/src/test/java/com/skyblockexp/lifesteal/seasons/BootstrapUnitTest.java @@ -1,7 +1,7 @@ package com.skyblockexp.lifesteal.seasons; -import be.seeseemelk.mockbukkit.MockBukkit; -import be.seeseemelk.mockbukkit.ServerMock; +import org.mockbukkit.mockbukkit.MockBukkit; +import org.mockbukkit.mockbukkit.ServerMock; import com.skyblockexp.lifesteal.seasons.config.MessageService; import org.bukkit.configuration.MemoryConfiguration; import org.bukkit.configuration.file.YamlConfiguration; diff --git a/plugin/src/test/java/com/skyblockexp/lifesteal/seasons/SeasonManagerUnitTest.java b/plugin/src/test/java/com/skyblockexp/lifesteal/seasons/SeasonManagerUnitTest.java index 9473dd8..4c4cfcc 100644 --- a/plugin/src/test/java/com/skyblockexp/lifesteal/seasons/SeasonManagerUnitTest.java +++ b/plugin/src/test/java/com/skyblockexp/lifesteal/seasons/SeasonManagerUnitTest.java @@ -1,7 +1,7 @@ package com.skyblockexp.lifesteal.seasons; -import be.seeseemelk.mockbukkit.MockBukkit; -import be.seeseemelk.mockbukkit.ServerMock; +import org.mockbukkit.mockbukkit.MockBukkit; +import org.mockbukkit.mockbukkit.ServerMock; import com.skyblockexp.lifesteal.seasons.api.events.SeasonResetEvent; import org.bukkit.configuration.MemoryConfiguration; import org.bukkit.event.EventHandler; diff --git a/plugin/src/test/java/com/skyblockexp/lifesteal/seasons/SeasonsApiImplEventUnitTest.java b/plugin/src/test/java/com/skyblockexp/lifesteal/seasons/SeasonsApiImplEventUnitTest.java index 799de05..60bd6e2 100644 --- a/plugin/src/test/java/com/skyblockexp/lifesteal/seasons/SeasonsApiImplEventUnitTest.java +++ b/plugin/src/test/java/com/skyblockexp/lifesteal/seasons/SeasonsApiImplEventUnitTest.java @@ -1,7 +1,7 @@ package com.skyblockexp.lifesteal.seasons; -import be.seeseemelk.mockbukkit.MockBukkit; -import be.seeseemelk.mockbukkit.ServerMock; +import org.mockbukkit.mockbukkit.MockBukkit; +import org.mockbukkit.mockbukkit.ServerMock; import com.skyblockexp.lifesteal.seasons.api.SeasonsApi; import com.skyblockexp.lifesteal.seasons.api.SeasonsIntegration; import com.skyblockexp.lifesteal.seasons.api.events.SeasonResetEvent; diff --git a/plugin/src/test/java/com/skyblockexp/lifesteal/seasons/command/SeasonCommandFeatureUnitTest.java b/plugin/src/test/java/com/skyblockexp/lifesteal/seasons/command/SeasonCommandFeatureUnitTest.java index d0de619..39968cd 100644 --- a/plugin/src/test/java/com/skyblockexp/lifesteal/seasons/command/SeasonCommandFeatureUnitTest.java +++ b/plugin/src/test/java/com/skyblockexp/lifesteal/seasons/command/SeasonCommandFeatureUnitTest.java @@ -1,8 +1,8 @@ package com.skyblockexp.lifesteal.seasons.command; -import be.seeseemelk.mockbukkit.MockBukkit; -import be.seeseemelk.mockbukkit.ServerMock; -import be.seeseemelk.mockbukkit.entity.PlayerMock; +import org.mockbukkit.mockbukkit.MockBukkit; +import org.mockbukkit.mockbukkit.ServerMock; +import org.mockbukkit.mockbukkit.entity.PlayerMock; import com.skyblockexp.lifesteal.seasons.EzSeasonsPlugin; import com.skyblockexp.lifesteal.seasons.SeasonManager; import org.bukkit.command.CommandExecutor; diff --git a/plugin/src/test/java/com/skyblockexp/lifesteal/seasons/compatibility/CompatibilityAndPaperCommandUnitTest.java b/plugin/src/test/java/com/skyblockexp/lifesteal/seasons/compatibility/CompatibilityAndPaperCommandUnitTest.java index 7a12cc5..a04af21 100644 --- a/plugin/src/test/java/com/skyblockexp/lifesteal/seasons/compatibility/CompatibilityAndPaperCommandUnitTest.java +++ b/plugin/src/test/java/com/skyblockexp/lifesteal/seasons/compatibility/CompatibilityAndPaperCommandUnitTest.java @@ -1,7 +1,7 @@ package com.skyblockexp.lifesteal.seasons.compatibility; -import be.seeseemelk.mockbukkit.MockBukkit; -import be.seeseemelk.mockbukkit.ServerMock; +import org.mockbukkit.mockbukkit.MockBukkit; +import org.mockbukkit.mockbukkit.ServerMock; import com.skyblockexp.lifesteal.seasons.EzSeasonsPlugin; import com.skyblockexp.lifesteal.seasons.command.SeasonPaperCommand; import com.skyblockexp.lifesteal.seasons.command.season.SeasonCommand; diff --git a/pom.xml b/pom.xml index 88dad57..2807520 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ com.skyblockexp.lifesteal ezseasons-parent - 1.0.0 + 2.0.0 pom EzSeasons @@ -52,8 +52,8 @@ - 21 - 1.21.1-R0.1-SNAPSHOT + 25 + [26.1.2.build,) ${project.version} UTF-8