Merge upstream adc8380 (release 2.5.0)
This commit is contained in:
commit
b82292478a
89 files changed
+7165
-1405
No files matched your search
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: commit-preparation
|
||||
description: Supports preparing commits, pre-commit hooks, Spotless, formatting or linting, validation before commit, staging files, and writing conventional commits.
|
||||
---
|
||||
|
||||
# Commit Preparation
|
||||
|
||||
Use this skill whenever an agent is preparing a repository state for commit, validating changes before commit, handling pre-commit hooks, or choosing staging and commit-message details.
|
||||
|
||||
## Required checks before committing
|
||||
|
||||
- Inspect `git status`, `git diff`, and recent commits before committing.
|
||||
- Run `./gradlew spotlessCheck` before committing.
|
||||
- Run `./gradlew check` before committing, unless a narrower validation is clearly justified for the change and that justification is reported in chat.
|
||||
- Run relevant integration tests for behaviour, compatibility, module, or bug-fix changes.
|
||||
- Use `integration-test-verification` to choose an integration-test matrix and to triage integration-test failures.
|
||||
- Never skip, weaken, disable, or version-gate tests to make validation pass. Fix the root cause or report the failure as a blocker unless the user explicitly approves a coverage reduction.
|
||||
|
||||
## Staging and commit messages
|
||||
|
||||
- Stage only intended files, and re-check the staged diff before committing.
|
||||
- Use conventional commits.
|
||||
- Keep commit messages subject-only unless the user explicitly asks for a body.
|
||||
- Make the subject release-note-friendly and comprehensible without the type prefix or body.
|
||||
- For issue, ticket, or bug-report driven work, include the ticket number in the commit subject, such as `fix(modesets): correct stale stored world modesets (#865)`.
|
||||
|
||||
## Reporting
|
||||
|
||||
- Keep validation findings in chat unless committing is explicitly requested.
|
||||
- If validation is narrowed, skipped, blocked, or failing, report the reason and any remaining risk before committing.
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: compatibility-strategy
|
||||
description: Use for Java 8 backports, Bukkit/Paper version differences, NMS/reflection, PacketEvents compatibility, fake-player implementation choices, and feature-detection design; do not use for release publishing, PR wording, or simple test-running decisions.
|
||||
---
|
||||
|
||||
# Compatibility Strategy
|
||||
|
||||
Use this skill when a change must behave across legacy and modern Bukkit/Paper versions or across different Java runtimes.
|
||||
|
||||
## When to use
|
||||
|
||||
- Java 8 source compatibility or dependency updates that might introduce newer APIs.
|
||||
- NMS, CraftBukkit, Paper, PacketEvents, XSeries, or reflection-sensitive changes.
|
||||
- Fake-player internals, attack delivery, fire ticks, entity ticking, or synthetic network plumbing.
|
||||
- Decisions about feature detection versus hard-coded Minecraft version checks.
|
||||
- Fallback behaviour on Spigot, Paper, legacy 1.12/1.9, or modern 1.20.5+ servers.
|
||||
|
||||
## When not to use
|
||||
|
||||
- Do not use for release asset or publishing checks; use `release-readiness-review`.
|
||||
- Do not use for config/modeset-only changes unless compatibility affects the implementation; use `module-config-change` first.
|
||||
- Do not use for PR prose only; use `pr-draft-summary`.
|
||||
|
||||
## Required stance
|
||||
|
||||
- Prefer direct supported APIs when present.
|
||||
- Use reflection only as a fallback or compatibility bridge, especially off hot paths.
|
||||
- Prefer feature detection by class, method, field, component, or capability over hard-coded version numbers.
|
||||
- Keep Java 8-targeted code free of Java 9+ collection factories, `Stream.toList()`, records, pattern matching, and other newer language/runtime features.
|
||||
- For NMS access, prefer `utilities.reflection.Reflector` helpers where they fit the problem.
|
||||
- Avoid hard-coded versioned NMS class names unless no heuristic or API-based path is viable.
|
||||
|
||||
## Compatibility checklist
|
||||
|
||||
1. Identify the minimum supported Java runtime for the touched source set.
|
||||
2. Identify whether Bukkit/Paper exposes a direct API for the target behaviour.
|
||||
3. If an API may be missing or backported, add runtime feature detection.
|
||||
4. Choose a safe fallback that preserves existing behaviour where feature detection fails.
|
||||
5. Keep diagnostics actionable when every candidate path fails.
|
||||
6. Select validation versions that cover the compatibility boundary, usually a legacy version plus a modern Paper version.
|
||||
|
||||
## Java 8 reminders
|
||||
|
||||
- Use `collect(Collectors.toList())` instead of `Stream.toList()`.
|
||||
- Use mutable collections wrapped with `Collections.unmodifiable*` where immutable Java 9 factories would otherwise be tempting.
|
||||
- Avoid records, sealed classes, switch expressions, pattern matching, and `var` in Java source.
|
||||
- Main build config targets Java 8 via `options.release.set(8)` and Kotlin `jvmTarget = 1.8`.
|
||||
|
||||
## Fake-player and NMS reminders
|
||||
|
||||
- The primary fake-player path is modern-NMS-oriented and relies on reflection remapping.
|
||||
- Legacy 1.12 uses versioned NMS and currently needs dedicated or version-aware handling for realistic fake-player tests.
|
||||
- Fake players are simulated login/network entities, not real network clients.
|
||||
- PacketEvents may need fake users or explicit client-version seeding in synthetic scenarios.
|
||||
|
||||
## References
|
||||
|
||||
- Broader test-harness and historical notes: `../integration-test-verification/references/relocated-agents-notes.md`.
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
name: dependabot-pr-review
|
||||
description: Use for Dependabot PRs, dependency bumps, Gradle or Maven dependency updates, GitHub Actions updates, dependency changelog/licence/release-note review, JVM/classfile checks, and validation recommendations.
|
||||
---
|
||||
|
||||
# Dependabot PR Review
|
||||
|
||||
Use this skill when reviewing dependency or workflow version updates. Keep the review evidence-based, scoped to the changed dependency, and explicit about what must be checked before merge.
|
||||
|
||||
## Cross-load related skills
|
||||
|
||||
- Use `integration-test-verification` for integration-test matrix selection, compact failure triage, Kotest registration, or any full integration log hand-off.
|
||||
- Use `compatibility-strategy` for Java 8, Bukkit/Paper/Spigot, NMS/reflection, PacketEvents, fake-player, or API compatibility concerns.
|
||||
- Use `release-readiness-review` for licence, shaded dependency, release-note, publishing, or asset-impact questions.
|
||||
|
||||
## Review workflow
|
||||
|
||||
1. Identify the update manager, dependency scope, touched files, and whether the dependency is production, compile-only, test-only, integration-test-only, or GitHub Actions.
|
||||
2. Read the PR diff and the upstream release notes, changelog, migration guide, and security notes for every updated version range.
|
||||
3. Check licence changes and whether the dependency ships in the release jar, shaded jar, or integration-test jar.
|
||||
4. Check JVM bytecode, classfile version, toolchain, and Gradle module metadata against main Java 8 support and the integration-test Java bands.
|
||||
5. Check Bukkit/Paper/Spigot compatibility when the dependency is server-facing, API-adjacent, PacketEvents-adjacent, or affects plugin loading.
|
||||
6. Check shading, relocation, service files, transitive dependency, and duplicate class risks, especially for anything bundled into the plugin jar.
|
||||
7. For GitHub Actions bumps, check Node/runtime changes, runner image requirements, permission model changes, and deprecated input/output behaviour.
|
||||
8. Inspect CI status and rerun the narrowest relevant validation where appropriate; prefer static checks for documentation-only or metadata-only bumps.
|
||||
9. Root and user-facing agents must not open `build/integration-test-logs/*.log` directly. If compact summaries are insufficient, delegate full integration log inspection to a subagent.
|
||||
|
||||
## Validation expectations
|
||||
|
||||
- Do not skip, weaken, disable, or version-gate tests to make a dependency update pass.
|
||||
- Match validation to risk: build metadata can often use wrapper/config checks; runtime libraries may need unit or integration tests; server-facing changes may need selected Paper matrix runs.
|
||||
- Prefer checking the produced dependency graph or shaded jar contents when packaging impact is plausible.
|
||||
|
||||
## Verdicts
|
||||
|
||||
- `merge`: low-risk update, relevant notes reviewed, CI/validation is green, and no follow-up is needed.
|
||||
- `merge after checks`: likely safe, but wait for named CI jobs, dependency graph checks, or narrow validation.
|
||||
- `hold`: compatibility, licence, packaging, JVM, server API, or validation concerns need investigation or code changes.
|
||||
- `close/ignore`: update is incompatible, not useful for this branch, duplicates another update, or should be deferred by policy.
|
||||
|
||||
## Review output template
|
||||
|
||||
```markdown
|
||||
### Dependency review
|
||||
- Scope: production / compile-only / test-only / integration-test-only / GitHub Actions
|
||||
- Release notes checked:
|
||||
- Licence/packaging impact:
|
||||
- JVM and server compatibility:
|
||||
- Shading/duplicate class risk:
|
||||
- Validation:
|
||||
- Verdict: merge / merge after checks / hold / close/ignore
|
||||
```
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
name: github-issue-investigation
|
||||
description: Use for GitHub issue triage and local investigation, including gh issue workflows, picking a fresh issue, reproducing issue reports, and planning regression coverage.
|
||||
---
|
||||
|
||||
# GitHub Issue Investigation
|
||||
|
||||
Use this skill when investigating GitHub issues locally, especially bug reports that may need reproduction and durable regression coverage before any fix is attempted.
|
||||
|
||||
## Default posture
|
||||
|
||||
- Treat GitHub as read-only by default.
|
||||
- `gh issue list` and `gh issue view` are allowed for investigation.
|
||||
- Do not comment, label, assign, close, edit milestones, or otherwise mutate public GitHub state unless the user explicitly asks later.
|
||||
- Keep findings chat-only by default; do not create plans, state files, or persistent notes unless the user asks.
|
||||
- Try to reproduce before fixing. Prefer a regression test that can remain in the repository when practical.
|
||||
- After local findings, let the user decide whether to fix, comment, or abandon the issue.
|
||||
|
||||
## When an issue number or URL is provided
|
||||
|
||||
Inspect that specific issue with `gh issue view`. Summarise:
|
||||
|
||||
- reported behaviour and expected behaviour;
|
||||
- environment, configuration, server, plugin, or version hints;
|
||||
- missing information that would affect local reproduction;
|
||||
- related code paths and existing tests worth checking;
|
||||
- a local reproduction recommendation, including the narrowest likely test or manual scenario.
|
||||
|
||||
Avoid changing GitHub state while doing this. If the issue is clearly a duplicate, support question, feature request, or waiting for reporter information, report that and explain whether local investigation is still worthwhile.
|
||||
|
||||
## When no issue is provided
|
||||
|
||||
Use `gh issue list` to build a shortlist rather than choosing silently. Present about 3-5 candidate issues with:
|
||||
|
||||
- issue number and title;
|
||||
- why the issue looks actionable;
|
||||
- likely affected area;
|
||||
- reproduction feasibility;
|
||||
- reasons to avoid it, if any.
|
||||
|
||||
Do not require or prefer positive labels. Actively avoid issues labelled exactly `awaiting testing` or effectively equivalent, because those are usually awaiting reporter or user verification rather than fresh local investigation.
|
||||
|
||||
Prefer issues with observable broken behaviour and enough detail to reproduce. Avoid issues that are obvious duplicates, support questions, feature requests, waiting on reporter information, already assigned or visibly in progress, dependent on unavailable external services, private servers, client-only behaviour, or anything requiring GitHub-side mutation before local investigation can proceed.
|
||||
|
||||
## Reproduction and coverage
|
||||
|
||||
- Reproduce before implementing a fix where practical.
|
||||
- Prefer integration tests when the behaviour depends on Bukkit/Paper server behaviour, plugin lifecycle, events, fake players, PacketEvents, or cross-version compatibility.
|
||||
- Use or trigger `integration-test-verification` for integration-test matrix selection, runs, Kotest registration, compact result triage, and failure investigation.
|
||||
- Respect the repository rule that root and user-facing agents must not read full `build/integration-test-logs/*.log` files directly; use compact summaries first and delegate log inspection only when needed.
|
||||
- If integration coverage is not feasible, explain why and propose the next best durable coverage, such as a focused unit-level test, synthetic event test, or documented manual reproduction.
|
||||
|
||||
## Routing after investigation
|
||||
|
||||
Keep this skill investigation-focused. If the user later requests a fix:
|
||||
|
||||
- use `TDD` for repro-test-first work;
|
||||
- use `Planner` when the fix needs planned multi-step work or broader design decisions;
|
||||
- use `BuildReview` for small standalone quick changes.
|
||||
|
||||
Do not start a fix, comment on GitHub, or create persistent planning artefacts merely because an issue looks actionable.
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
name: integration-test-verification
|
||||
description: Use when running, selecting, authoring, or triaging integration tests, Kotest specs, Gradle matrix tasks, FakePlayer-backed test cases, or compact test-result files; do not use for unrelated production-only edits, release publishing, or general PR prose.
|
||||
---
|
||||
|
||||
# Integration Test Verification
|
||||
|
||||
Use this skill for the repository's Paper-backed integration-test harness and for deciding how to validate a change without opening large server logs by default.
|
||||
|
||||
## When to use
|
||||
|
||||
- Selecting `./gradlew integrationTest` versus a version-limited matrix run.
|
||||
- Adding or changing Kotlin integration tests under `src/integrationTest/kotlin`.
|
||||
- Registering a new spec in `KotestRunner`.
|
||||
- Triaging `checkTestResults<version>` output or `plugins/OldCombatMechanicsTest/test-failures.txt`.
|
||||
- Reasoning about tests that use `FakePlayer`, synthetic Bukkit events, PacketEvents wrappers, or direct module handler calls.
|
||||
|
||||
## When not to use
|
||||
|
||||
- Do not use for release asset, Hangar, CurseForge, or licence checks; use `release-readiness-review`.
|
||||
- Do not use for module assignment or config migration design unless the task is specifically about test coverage; use `module-config-change`.
|
||||
- Do not use for broad Java 8/NMS design unless the immediate question is test-harness behaviour; use `compatibility-strategy`.
|
||||
|
||||
## Non-negotiable rules
|
||||
|
||||
- Root and user-facing agents must not read `build/integration-test-logs/*.log` directly; log inspection is allowed only by subagents, and only when needed for integration-test triage or when explicitly requested.
|
||||
- Prefer compact console output and `plugins/OldCombatMechanicsTest/test-failures.txt` first for failure triage.
|
||||
- Do not disable, skip, weaken, or version-gate tests as a way to make validation pass. Fix the root cause, or report the failure as a blocker unless the user explicitly approves a coverage reduction.
|
||||
- New integration specs must be added to `KotestRunner.withClasses(...)`; autoscan is disabled.
|
||||
- Keep paths relative to the repository root, plugin data folder, or server run directory.
|
||||
- Avoid starting servers for documentation-only or opencode-config-only changes.
|
||||
|
||||
## Harness quick reference
|
||||
|
||||
- Full matrix: `./gradlew integrationTest`.
|
||||
- Selected matrix: `./gradlew integrationTest -PintegrationTestVersions=1.19.2,1.21.11,1.12`.
|
||||
- Wrapper helper: `scripts/run-integration-matrix.sh -v 1.19.2,1.21.11 [-s '*SpecName*'] [-t '*test name*']` keeps Gradle and Kotest filter arguments consistent.
|
||||
- Compact result summary: `scripts/summarise-integration-results.sh` reads only `run/*/plugins/OldCombatMechanicsTest/test-results.txt` and `test-failures.txt`; it does not open full server logs.
|
||||
- Matrix suggestion helper: `scripts/suggest-integration-matrix.sh [changed-path ...]` gives a conservative starting point for versions and filters.
|
||||
- Individual task names follow `integrationTest<version>` with punctuation mapped to underscores, for example `integrationTest1_19_2`.
|
||||
- Test plugin entrypoint: `kernitus.plugin.OldCombatMechanics.OCMTestMain`.
|
||||
- Success handoff: `plugins/OldCombatMechanicsTest/test-results.txt` contains `PASS`.
|
||||
- Failure summary: `plugins/OldCombatMechanicsTest/test-failures.txt` contains a compact list, capped for CI readability.
|
||||
- Java 11+ server targets use Kotest 6 (`KotestRunner`, `KotestProjectConfig`).
|
||||
- Java 8 targets use `LegacyTestRunner`, currently only a smoke path.
|
||||
|
||||
## Validation selection template
|
||||
|
||||
1. Identify the changed feature and affected server bands.
|
||||
2. Prefer the narrowest version set that covers the compatibility boundary.
|
||||
3. If the change touches packet handling, include a modern PacketEvents-covered version.
|
||||
4. If the change touches legacy behaviour or Java 8 APIs, include `1.12` where practical.
|
||||
5. If the change is documentation or opencode-only, run filesystem/schema checks rather than server tests.
|
||||
|
||||
Example recommendation:
|
||||
|
||||
```text
|
||||
Run: ./gradlew integrationTest -PintegrationTestVersions=1.19.2,1.21.11
|
||||
Reason: covers modern Paper plus the PacketEvents path affected by the change.
|
||||
Avoid: opening build/integration-test-logs from a root or user-facing agent; use a subagent only when compact failure output is insufficient or log inspection is explicitly requested.
|
||||
```
|
||||
|
||||
## Known shortcut patterns
|
||||
|
||||
Some tests intentionally exercise module code without fully realistic gameplay plumbing:
|
||||
|
||||
- `GoldenAppleIntegrationTest`, `OldPotionEffectsIntegrationTest`, `OldArmourDurabilityIntegrationTest`, `PlayerKnockbackIntegrationTest`, `SwordBlockingIntegrationTest`, and `SwordSweepIntegrationTest` use synthetic events in places.
|
||||
- `PlayerKnockbackIntegrationTest` and `SwordSweepIntegrationTest` directly invoke module handlers.
|
||||
- Packet cancellation tests construct and dispatch PacketEvents events directly for deterministic coverage.
|
||||
|
||||
Treat these shortcuts as design choices unless the task asks for end-to-end realism.
|
||||
|
||||
## References
|
||||
|
||||
- Historical integration and regression notes: `references/relocated-agents-notes.md`.
|
||||
@@ -0,0 +1,98 @@
|
||||
# Relocated AGENTS.md Historical Notes
|
||||
|
||||
This reference preserves detailed repository context that previously made the root `AGENTS.md` difficult to scan. Keep always-on routing rules in `AGENTS.md`; keep operational detail here or in a more specific skill.
|
||||
|
||||
## Integration test harness
|
||||
|
||||
- Integration tests live in `src/integrationTest/kotlin` and are packaged into `OldCombatMechanics-<version>-tests.jar`.
|
||||
- Entrypoint plugin class: `kernitus.plugin.OldCombatMechanics.OCMTestMain`.
|
||||
- Tests run inside a real Paper server started by the Gradle `run-paper` plugin.
|
||||
- RunServer output is redirected to `build/integration-test-logs/<version>.log`; `checkTestResults<version>` prints only a compact summary/failures to the console.
|
||||
- `KotestRunner` writes `plugins/OldCombatMechanicsTest/test-failures.txt` so CI can surface failure reasons without opening the full server log.
|
||||
- PacketEvents is shaded into the plugin; integration tests do not inject external packet libraries.
|
||||
- `relocateIntegrationTestClasses` relocates PacketEvents references in test classes only.
|
||||
- `integrationTestJar` embeds relocated test classes plus runtime dependencies, excluding PacketEvents so the test plugin resolves PacketEvents from the main plugin’s shaded copy.
|
||||
- `PacketCancellationIntegrationTest` uses a cancellable `CompletableFuture.await()` and drives PacketEvents via `PacketEventsImplHelper.handleClientBoundPacket` with a synthetic `User`.
|
||||
- `PacketCancellationIntegrationTest` sets the Bukkit player on the synthetic PacketEvents event so module listeners can match `PacketSendEvent#getPlayer`.
|
||||
- Matrix task: `integrationTest` depends on `integrationTestMatrix`.
|
||||
- `OCMTestMain` writes `plugins/OldCombatMechanicsTest/test-results.txt` containing `PASS` or `FAIL`.
|
||||
|
||||
## Version matrix and Java selection
|
||||
|
||||
- Config is in `build.gradle.kts`.
|
||||
- `integrationTestVersions` comes from the property or defaults.
|
||||
- `requiredJavaVersion` selects Java based on version.
|
||||
- Pre-1.13 versions use `integrationTestJavaVersionLegacyPre13` (default 8).
|
||||
- Modern versions use Java 25 if `>=1.20.5`, else Java 17.
|
||||
- Legacy vanilla jar cache: `downloadVanilla<version>` downloads Mojang server jars for `<=1.12` and writes `run/<version>/cache/mojang_<version>.jar`.
|
||||
- Paper 1.12 sometimes fails to download the legacy vanilla jar from old Mojang endpoints; the custom `downloadVanilla` task uses the v2 manifest.
|
||||
|
||||
## Kotlin runner split
|
||||
|
||||
- Kotest 6 (Java 11+) is used for 1.19.2 and 1.21.11.
|
||||
- Kotest runner class: `kernitus.plugin.OldCombatMechanics.KotestRunner`.
|
||||
- Project config: `KotestProjectConfig`.
|
||||
- Java 8 uses `kernitus.plugin.OldCombatMechanics.LegacyTestRunner`.
|
||||
- The Java 8 path is currently a smoke test only, verifying the plugin is enabled and `WeaponDamages` is loaded.
|
||||
|
||||
## Fake player notes
|
||||
|
||||
- Primary implementation: `src/integrationTest/kotlin/kernitus/plugin/OldCombatMechanics/FakePlayer.kt`.
|
||||
- It uses `xyz.jpenilla:reflection-remapper` to map modern NMS names.
|
||||
- On legacy servers, mappings are unavailable and code falls back to `ReflectionRemapper.noop()`.
|
||||
- As-is, `FakePlayer` uses modern NMS class names; Paper 1.12 needs a dedicated fake-player path or version-aware class mapping.
|
||||
- Fake player implementations use simulated login/network plumbing with `EmbeddedChannel` and manual login/join/quit events, not a real networked client.
|
||||
- `FakePlayer` uses a plain `EmbeddedChannel` with dummy `decoder`/`encoder` handlers so PacketEvents treats it as fake and does not disallow login.
|
||||
- Modern fake players schedule a manual NMS tick, preferring `doTick`, then `tick`, then `baseTick`.
|
||||
- The tick shim invokes `baseTick` while burning to ensure fire tick damage events still occur on Paper 1.21+, and prefers normal ticking when in water so extinguish logic can run.
|
||||
- `FakePlayer` prefers `PlayerList.placeNewPlayer` and forces a world add when the Bukkit world does not report the fake player entity after placement.
|
||||
- `FakePlayer` clears invulnerability/instabuild abilities after spawn, plus a legacy fallback.
|
||||
|
||||
## Test harness shortcuts
|
||||
|
||||
- Synthetic event tests: `GoldenAppleIntegrationTest`, `OldPotionEffectsIntegrationTest`, `OldArmourDurabilityIntegrationTest`, `PlayerKnockbackIntegrationTest`, `SwordBlockingIntegrationTest`, and `SwordSweepIntegrationTest`.
|
||||
- Direct module handler tests: `PlayerKnockbackIntegrationTest` and `SwordSweepIntegrationTest`.
|
||||
- `AttributeModifierCompat` synthesises a fallback attack-damage modifier from `NewWeaponDamage` when API attributes are missing.
|
||||
|
||||
## Registration and notable test history
|
||||
|
||||
- `KotestRunner` must include each new integration test class explicitly.
|
||||
- `OldToolDamageMobIntegrationTest`, `WeaponDurabilityIntegrationTest`, `DisableOffhandIntegrationTest`, `ToolDamageTooltipIntegrationTest`, `PacketCancellationIntegrationTest`, `ModesetRulesIntegrationTest`, `ConfigMigrationIntegrationTest`, `ChorusFruitIntegrationTest`, `DisableOffhandReflectionIntegrationTest`, `AttackRangeIntegrationTest`, `PaperSwordBlockingDamageReductionIntegrationTest`, and `ConsumableComponentIntegrationTest` have all been registered historically.
|
||||
- `Kotest` filters (`kotest.filter.specs`, `kotest.filter.tests`) pass through Gradle into the run-paper JVM args.
|
||||
- `WeaponDurabilityIntegrationTest` and `OldToolDamageMobIntegrationTest` resolve debug paths relative to the repo root rather than hard-coded home paths.
|
||||
- `1.9` integration tests are currently on hold per user request.
|
||||
- 1.21.11 servers may log hostname and Unsafe warnings; tests can still pass.
|
||||
|
||||
## PacketEvents and packet cancellation history
|
||||
|
||||
- `ModuleSwordSweepParticles` and `ModuleAttackSounds` use PacketEvents listeners/wrappers instead of ProtocolLib.
|
||||
- `PacketCancellationIntegrationTest` builds a `PacketSendEvent` directly from the transformed buffer, sets packet id/type explicitly, and dispatches it through the PacketEvents event manager.
|
||||
- PacketEvents dependency moved to `2.11.2-SNAPSHOT` from CodeMC snapshots for 1.21.11 support.
|
||||
|
||||
## Sword blocking and consumable component history
|
||||
|
||||
- Paper-only sword blocking uses runtime-gated helper `kernitus.plugin.OldCombatMechanics.paper.PaperSwordBlocking` to apply consumable and blocking components via cached reflection.
|
||||
- 1.8-style reduction is applied via `EntityDamageByEntityListener` using the `BLOCKING` modifier, while legacy/non-Paper keeps the shield swap path.
|
||||
- `ModuleSwordBlocking` gates the Paper consumable animation by PacketEvents client version `>=1.20.5`; older clients fall back to offhand shield behaviour.
|
||||
- Unknown PacketEvents client versions fall back to shield behaviour only when a PacketEvents `User` exists; early-login/synthetic players keep animation support unless resolver failure requires fail-safe fallback.
|
||||
- Inventory-click and drag handlers follow a minimal-mutation policy; lifecycle and transition handlers clean stale consumable state.
|
||||
- Lifecycle cleanup covers modeset change, reload, world change, quit, join, held-slot changes, and swap paths.
|
||||
- `sword-blocking.paper-animation` can hard-disable the Paper animation path and force legacy shield fallback.
|
||||
- Legacy shield-drop reconciliation is safety-first and avoids guessing plain shield drops as temporary shields when marker APIs are unavailable.
|
||||
|
||||
## Fire aspect and fire tick notes
|
||||
|
||||
- `FireAspectOverdamageIntegrationTest` uses a Zombie victim for real fire tick sampling with boosted max health.
|
||||
- Initial tests fire synthetic `EntityDamageEvent` with `FIRE_TICK` for deterministic baseline checks.
|
||||
- Paper 1.12 applies attack-cooldown scaling before Bukkit damage events; tests wait a few ticks before the first attack to stabilise the first hit.
|
||||
- Extra coverage includes fire resistance, fire-immune victims, water extinguish behaviour, fire protection versus protection, and alternating attackers during invulnerability.
|
||||
- Legacy player-specific fire tick sampling differs, so player afterburn-vs-environment comparisons are no-ops on legacy; Zombie variants still run across versions.
|
||||
|
||||
## Miscellaneous historical fixes
|
||||
|
||||
- Removed dead reflection utility `ClassType` and unused `Reflector#getClass(ClassType, String)` overload.
|
||||
- `SpigotFunctionChooser` now only falls back for compatibility-style failures and rethrows ordinary runtime logic failures.
|
||||
- `AttackCompat` requires observable living-target hit signals for Bukkit `Player#attack` success and treats boolean-return NMS methods returning `false` as failed attempts.
|
||||
- Several modules replaced per-player/per-hit scheduled tasks with shared or lazy-pruned maps: sword blocking dedupe, fishing rod velocity, attack cooldown tracking, player knockback, shield damage reduction, old armour durability, player regen, disable enderpearl cooldown, and stored last-damage expiry.
|
||||
- `ModuleLoader` clears the static module list on initialise to prevent duplicate registrations after hot reloads.
|
||||
- `README` includes licence notes: source remains MPL-2.0, while pre-built jars bundling PacketEvents are distributed under GPLv3; builds without PacketEvents can remain MPL-2.0.
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
name: module-config-change
|
||||
description: Use for config.yml, module enablement, modesets, config migration, configurable module assignment, and per-module option changes; do not use for unrelated integration-test triage, release publishing, or PR summaries.
|
||||
---
|
||||
|
||||
# Module Config Change
|
||||
|
||||
Use this skill when changing module configuration, defaults, migration, or runtime handling driven by `config.yml`.
|
||||
|
||||
## When to use
|
||||
|
||||
- Editing bundled `config.yml` defaults or comments.
|
||||
- Adding or changing module settings such as attack cooldown, attack range, sword blocking, old tool damage, tooltip, potion effects, or durability behaviour.
|
||||
- Changing `always_enabled_modules`, `disabled_modules`, modesets, or config migration logic.
|
||||
- Validating reload behaviour after config changes.
|
||||
- Writing tests around config assignment, migration, or modeset-specific behaviour.
|
||||
|
||||
## When not to use
|
||||
|
||||
- Do not use for ordinary test execution unless the test is config-specific; use `integration-test-verification`.
|
||||
- Do not use for Java/NMS compatibility strategy unless the config option depends on runtime feature detection; use `compatibility-strategy` as well.
|
||||
- Do not use for release workflow edits; use `release-readiness-review`.
|
||||
|
||||
## Mandatory configuration rules
|
||||
|
||||
- Configurable modules must appear in only one top-level assignment category: `always_enabled_modules`, `disabled_modules`, or the aggregate `modesets` category. The same module may appear in more than one individual modeset because modesets are alternative player modes.
|
||||
- Duplicate entries are still invalid within the same exact list or individual modeset.
|
||||
- Internal modules (`modeset-listener`, `attack-cooldown-tracker`, `entity-damage-listener`) are always enabled and must not be listed.
|
||||
- Reload/enable should fail for invalid module assignment.
|
||||
- Do not add per-module `enabled:` toggles; the module lists and modesets are authoritative.
|
||||
- Keep bundled comments clear for server owners and use British English.
|
||||
|
||||
## Change checklist
|
||||
|
||||
1. Identify whether the change is a default, migration, reload, or runtime behaviour change.
|
||||
2. Update bundled config comments and defaults together when needed.
|
||||
3. Preserve existing user config where migration should be non-destructive.
|
||||
4. Run `scripts/validate-module-assignments.sh` for a quick static check that bundled configurable lists do not conflict across `always_enabled_modules`, `disabled_modules`, and the aggregate `modesets` category; do not duplicate entries within the same exact list or modeset; and do not include internal modules.
|
||||
5. Add or update integration coverage where practical.
|
||||
6. If adding an integration spec, register it in `KotestRunner.withClasses(...)`.
|
||||
7. Validate both enabled and disabled/modeset-scoped behaviour for options that change item state or player state.
|
||||
|
||||
## Useful known behaviours
|
||||
|
||||
- `old-tool-damage.tooltip.enabled` is enabled by default so players can see configured damage in-game.
|
||||
- `attack-range` is listed in `disabled_modules` by default and is Paper 1.21.11+ only.
|
||||
- `sword-blocking.paper-animation` defaults to `true`; when false, sword blocking uses the legacy shield fallback and strips stale Paper consumable state.
|
||||
- Attack cooldown supports `disable-attack-cooldown.held-item-attack-speeds.<MATERIAL>` with XMaterial/Material matching and restores vanilla attack speed when disabled.
|
||||
|
||||
## Validation template
|
||||
|
||||
```text
|
||||
Config area:
|
||||
Default changed:
|
||||
Migration needed:
|
||||
Enabled-path validation:
|
||||
Disabled/modeset validation:
|
||||
KotestRunner registration needed:
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- Historical integration and module notes: `../integration-test-verification/references/relocated-agents-notes.md`.
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
name: pr-draft-summary
|
||||
description: Use when drafting pull-request titles, descriptions, change summaries, risk notes, validation sections, or reviewer handoff text; do not use for implementation design, release publishing checks, or raw test failure triage.
|
||||
---
|
||||
|
||||
# PR Draft Summary
|
||||
|
||||
Use this skill to turn completed work into reviewer-facing prose. Keep wording concise, accurate, and in British English.
|
||||
|
||||
## When to use
|
||||
|
||||
- Drafting a PR title or body.
|
||||
- Summarising changes after a slice or branch of work.
|
||||
- Writing reviewer notes, risk notes, validation sections, or follow-up items.
|
||||
- Turning technical details into a changelog-style summary.
|
||||
|
||||
## When not to use
|
||||
|
||||
- Do not use before implementation decisions are made; use the relevant implementation skill first.
|
||||
- Do not use for release readiness unless the requested output is a PR description; use `release-readiness-review` for release checks.
|
||||
- Do not use to interpret raw integration test failures; use `integration-test-verification`.
|
||||
|
||||
## Preferred structure
|
||||
|
||||
```markdown
|
||||
## Summary
|
||||
-
|
||||
-
|
||||
|
||||
## Validation
|
||||
-
|
||||
|
||||
## Risks / notes
|
||||
-
|
||||
```
|
||||
|
||||
## Conventional commit prompt
|
||||
|
||||
When asked for a commit message, use conventional commits format, for example:
|
||||
|
||||
```text
|
||||
docs: add repo-local agent skill guidance
|
||||
```
|
||||
|
||||
## Reviewer handoff checklist
|
||||
|
||||
- Name the changed area before the mechanism.
|
||||
- Mention validation commands exactly as run.
|
||||
- If validation was not run, say why.
|
||||
- Separate behaviour changes from documentation/configuration changes.
|
||||
- Avoid overstating coverage; note known gaps plainly.
|
||||
|
||||
## Example phrasing
|
||||
|
||||
```markdown
|
||||
## Summary
|
||||
- Added repo-local agent skills for integration-test verification, compatibility strategy, module config changes, release readiness, and PR summaries.
|
||||
- Slimmed root `AGENTS.md` to always-on rules plus routing guidance, with historical details relocated into skill references.
|
||||
|
||||
## Validation
|
||||
- Checked skill frontmatter and folder-name matches.
|
||||
- Confirmed no plugin runtime files changed.
|
||||
|
||||
## Risks / notes
|
||||
- opencode must be restarted before new or changed skills are available in a running session.
|
||||
```
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
name: release-readiness-review
|
||||
description: Use for GitHub release, Hangar, CurseForge/BukkitDev upload, licence, asset naming, supported-version, and workflow readiness checks; do not use for day-to-day feature implementation, test authoring, or PR prose only.
|
||||
---
|
||||
|
||||
# Release Readiness Review
|
||||
|
||||
Use this skill before changing or reviewing release workflows, publishing metadata, supported game versions, licence notes, and final release assets.
|
||||
|
||||
## When to use
|
||||
|
||||
- Editing `.github/workflows/*release*` or publishing-related Gradle configuration.
|
||||
- Checking Hangar, CurseForge/BukkitDev, or GitHub release upload behaviour.
|
||||
- Reviewing licence implications of shaded dependencies such as PacketEvents.
|
||||
- Updating supported Minecraft versions, `gradle.properties` release metadata, or asset names.
|
||||
- Preparing a release-readiness checklist for maintainers.
|
||||
|
||||
## When not to use
|
||||
|
||||
- Do not use for ordinary implementation or test selection; use the feature-specific skill.
|
||||
- Do not use for PR description wording unless release notes are the main task; use `pr-draft-summary`.
|
||||
- Do not use for module config behaviour unless it affects packaging or documented release defaults.
|
||||
|
||||
## Current release notes
|
||||
|
||||
- GitHub release asset uses stable filename `OldCombatMechanics.jar` without a version suffix.
|
||||
- CurseForge upload uses the same path.
|
||||
- Bukkit-compatible CurseForge game-version entries use `1:<version>` prefixes so type-1 Bukkit versions are selected.
|
||||
- Hangar publish configuration expects `HANGAR_API_TOKEN`.
|
||||
- README licence note: source remains MPL-2.0; pre-built jars bundling PacketEvents are distributed under GPLv3; builds without PacketEvents can remain MPL-2.0.
|
||||
- `gradle.properties` game versions currently include 1.21.11 down to 1.21.1, plus 1.21, ahead of existing entries.
|
||||
|
||||
## Review checklist
|
||||
|
||||
1. Confirm secrets are referenced by environment variable or GitHub secret name, never hard-coded.
|
||||
2. Confirm the built jar path and uploaded asset path match.
|
||||
3. Confirm supported game versions map to the publishing platform’s expected identifiers.
|
||||
4. Confirm shaded dependency licence implications are reflected in release notes or README where needed.
|
||||
5. Confirm workflow changes do not alter plugin runtime behaviour.
|
||||
6. Prefer dry, static checks unless the user explicitly requests a release run.
|
||||
|
||||
## Release note template
|
||||
|
||||
```markdown
|
||||
### Release readiness
|
||||
- Asset: `OldCombatMechanics.jar`
|
||||
- Platforms checked: GitHub / Hangar / CurseForge
|
||||
- Version metadata checked:
|
||||
- Licence notes checked:
|
||||
- Secrets touched: none / list variable names only
|
||||
- Remaining manual steps:
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- Broader historical notes: `../integration-test-verification/references/relocated-agents-notes.md`.
|
||||
+642
@@ -0,0 +1,642 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
max_line_length = 120
|
||||
tab_width = 4
|
||||
ij_continuation_indent_size = 8
|
||||
ij_formatter_off_tag = @formatter:off
|
||||
ij_formatter_on_tag = @formatter:on
|
||||
ij_formatter_tags_enabled = false
|
||||
ij_smart_tabs = false
|
||||
ij_visual_guides = none
|
||||
ij_wrap_on_typing = false
|
||||
|
||||
[*.css]
|
||||
ij_css_align_closing_brace_with_properties = false
|
||||
ij_css_blank_lines_around_nested_selector = 1
|
||||
ij_css_blank_lines_between_blocks = 1
|
||||
ij_css_brace_placement = end_of_line
|
||||
ij_css_enforce_quotes_on_format = false
|
||||
ij_css_hex_color_long_format = false
|
||||
ij_css_hex_color_lower_case = false
|
||||
ij_css_hex_color_short_format = false
|
||||
ij_css_hex_color_upper_case = false
|
||||
ij_css_keep_blank_lines_in_code = 2
|
||||
ij_css_keep_indents_on_empty_lines = false
|
||||
ij_css_keep_single_line_blocks = false
|
||||
ij_css_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow
|
||||
ij_css_space_after_colon = true
|
||||
ij_css_space_before_opening_brace = true
|
||||
ij_css_use_double_quotes = true
|
||||
ij_css_value_alignment = do_not_align
|
||||
|
||||
[*.feature]
|
||||
indent_size = 2
|
||||
ij_gherkin_keep_indents_on_empty_lines = false
|
||||
|
||||
[*.gsp]
|
||||
ij_gsp_keep_indents_on_empty_lines = false
|
||||
|
||||
[*.haml]
|
||||
indent_size = 2
|
||||
ij_haml_keep_indents_on_empty_lines = false
|
||||
|
||||
[*.java]
|
||||
ij_java_align_consecutive_assignments = false
|
||||
ij_java_align_consecutive_variable_declarations = false
|
||||
ij_java_align_group_field_declarations = false
|
||||
ij_java_align_multiline_annotation_parameters = false
|
||||
ij_java_align_multiline_array_initializer_expression = false
|
||||
ij_java_align_multiline_assignment = false
|
||||
ij_java_align_multiline_binary_operation = false
|
||||
ij_java_align_multiline_chained_methods = false
|
||||
ij_java_align_multiline_extends_list = false
|
||||
ij_java_align_multiline_for = true
|
||||
ij_java_align_multiline_method_parentheses = false
|
||||
ij_java_align_multiline_parameters = true
|
||||
ij_java_align_multiline_parameters_in_calls = false
|
||||
ij_java_align_multiline_parenthesized_expression = false
|
||||
ij_java_align_multiline_records = true
|
||||
ij_java_align_multiline_resources = true
|
||||
ij_java_align_multiline_ternary_operation = false
|
||||
ij_java_align_multiline_text_blocks = false
|
||||
ij_java_align_multiline_throws_list = false
|
||||
ij_java_align_subsequent_simple_methods = false
|
||||
ij_java_align_throws_keyword = false
|
||||
ij_java_annotation_parameter_wrap = off
|
||||
ij_java_array_initializer_new_line_after_left_brace = false
|
||||
ij_java_array_initializer_right_brace_on_new_line = false
|
||||
ij_java_array_initializer_wrap = off
|
||||
ij_java_assert_statement_colon_on_next_line = false
|
||||
ij_java_assert_statement_wrap = off
|
||||
ij_java_assignment_wrap = off
|
||||
ij_java_binary_operation_sign_on_next_line = false
|
||||
ij_java_binary_operation_wrap = off
|
||||
ij_java_blank_lines_after_anonymous_class_header = 0
|
||||
ij_java_blank_lines_after_class_header = 0
|
||||
ij_java_blank_lines_after_imports = 1
|
||||
ij_java_blank_lines_after_package = 1
|
||||
ij_java_blank_lines_around_class = 1
|
||||
ij_java_blank_lines_around_field = 0
|
||||
ij_java_blank_lines_around_field_in_interface = 0
|
||||
ij_java_blank_lines_around_initializer = 1
|
||||
ij_java_blank_lines_around_method = 1
|
||||
ij_java_blank_lines_around_method_in_interface = 1
|
||||
ij_java_blank_lines_before_class_end = 0
|
||||
ij_java_blank_lines_before_imports = 1
|
||||
ij_java_blank_lines_before_method_body = 0
|
||||
ij_java_blank_lines_before_package = 0
|
||||
ij_java_block_brace_style = end_of_line
|
||||
ij_java_block_comment_at_first_column = true
|
||||
ij_java_call_parameters_new_line_after_left_paren = false
|
||||
ij_java_call_parameters_right_paren_on_new_line = false
|
||||
ij_java_call_parameters_wrap = off
|
||||
ij_java_case_statement_on_separate_line = true
|
||||
ij_java_catch_on_new_line = false
|
||||
ij_java_class_annotation_wrap = split_into_lines
|
||||
ij_java_class_brace_style = end_of_line
|
||||
ij_java_class_count_to_use_import_on_demand = 5
|
||||
ij_java_class_names_in_javadoc = 1
|
||||
ij_java_do_not_indent_top_level_class_members = false
|
||||
ij_java_do_not_wrap_after_single_annotation = false
|
||||
ij_java_do_while_brace_force = never
|
||||
ij_java_doc_add_blank_line_after_description = true
|
||||
ij_java_doc_add_blank_line_after_param_comments = false
|
||||
ij_java_doc_add_blank_line_after_return = false
|
||||
ij_java_doc_add_p_tag_on_empty_lines = true
|
||||
ij_java_doc_align_exception_comments = true
|
||||
ij_java_doc_align_param_comments = true
|
||||
ij_java_doc_do_not_wrap_if_one_line = false
|
||||
ij_java_doc_enable_formatting = true
|
||||
ij_java_doc_enable_leading_asterisks = true
|
||||
ij_java_doc_indent_on_continuation = false
|
||||
ij_java_doc_keep_empty_lines = true
|
||||
ij_java_doc_keep_empty_parameter_tag = true
|
||||
ij_java_doc_keep_empty_return_tag = true
|
||||
ij_java_doc_keep_empty_throws_tag = true
|
||||
ij_java_doc_keep_invalid_tags = true
|
||||
ij_java_doc_param_description_on_new_line = false
|
||||
ij_java_doc_preserve_line_breaks = false
|
||||
ij_java_doc_use_throws_not_exception_tag = true
|
||||
ij_java_else_on_new_line = false
|
||||
ij_java_entity_dd_suffix = EJB
|
||||
ij_java_entity_eb_suffix = Bean
|
||||
ij_java_entity_hi_suffix = Home
|
||||
ij_java_entity_lhi_prefix = Local
|
||||
ij_java_entity_lhi_suffix = Home
|
||||
ij_java_entity_li_prefix = Local
|
||||
ij_java_entity_pk_class = java.lang.String
|
||||
ij_java_entity_vo_suffix = VO
|
||||
ij_java_enum_constants_wrap = off
|
||||
ij_java_extends_keyword_wrap = off
|
||||
ij_java_extends_list_wrap = off
|
||||
ij_java_field_annotation_wrap = split_into_lines
|
||||
ij_java_finally_on_new_line = false
|
||||
ij_java_for_brace_force = never
|
||||
ij_java_for_statement_new_line_after_left_paren = false
|
||||
ij_java_for_statement_right_paren_on_new_line = false
|
||||
ij_java_for_statement_wrap = off
|
||||
ij_java_generate_final_locals = false
|
||||
ij_java_generate_final_parameters = false
|
||||
ij_java_if_brace_force = never
|
||||
ij_java_imports_layout = *,|,javax.**,java.**,|,$*
|
||||
ij_java_indent_case_from_switch = true
|
||||
ij_java_insert_inner_class_imports = false
|
||||
ij_java_insert_override_annotation = true
|
||||
ij_java_keep_blank_lines_before_right_brace = 2
|
||||
ij_java_keep_blank_lines_between_package_declaration_and_header = 2
|
||||
ij_java_keep_blank_lines_in_code = 2
|
||||
ij_java_keep_blank_lines_in_declarations = 2
|
||||
ij_java_keep_control_statement_in_one_line = true
|
||||
ij_java_keep_first_column_comment = true
|
||||
ij_java_keep_indents_on_empty_lines = false
|
||||
ij_java_keep_line_breaks = true
|
||||
ij_java_keep_multiple_expressions_in_one_line = false
|
||||
ij_java_keep_simple_blocks_in_one_line = false
|
||||
ij_java_keep_simple_classes_in_one_line = false
|
||||
ij_java_keep_simple_lambdas_in_one_line = false
|
||||
ij_java_keep_simple_methods_in_one_line = false
|
||||
ij_java_label_indent_absolute = false
|
||||
ij_java_label_indent_size = 0
|
||||
ij_java_lambda_brace_style = end_of_line
|
||||
ij_java_layout_static_imports_separately = true
|
||||
ij_java_line_comment_add_space = false
|
||||
ij_java_line_comment_at_first_column = true
|
||||
ij_java_message_dd_suffix = EJB
|
||||
ij_java_message_eb_suffix = Bean
|
||||
ij_java_method_annotation_wrap = split_into_lines
|
||||
ij_java_method_brace_style = end_of_line
|
||||
ij_java_method_call_chain_wrap = off
|
||||
ij_java_method_parameters_new_line_after_left_paren = false
|
||||
ij_java_method_parameters_right_paren_on_new_line = false
|
||||
ij_java_method_parameters_wrap = off
|
||||
ij_java_modifier_list_wrap = false
|
||||
ij_java_names_count_to_use_import_on_demand = 3
|
||||
ij_java_new_line_after_lparen_in_record_header = false
|
||||
ij_java_packages_to_use_import_on_demand = java.awt.*,javax.swing.*
|
||||
ij_java_parameter_annotation_wrap = off
|
||||
ij_java_parentheses_expression_new_line_after_left_paren = false
|
||||
ij_java_parentheses_expression_right_paren_on_new_line = false
|
||||
ij_java_place_assignment_sign_on_next_line = false
|
||||
ij_java_prefer_longer_names = true
|
||||
ij_java_prefer_parameters_wrap = false
|
||||
ij_java_record_components_wrap = normal
|
||||
ij_java_repeat_synchronized = true
|
||||
ij_java_replace_instanceof_and_cast = false
|
||||
ij_java_replace_null_check = true
|
||||
ij_java_replace_sum_lambda_with_method_ref = true
|
||||
ij_java_resource_list_new_line_after_left_paren = false
|
||||
ij_java_resource_list_right_paren_on_new_line = false
|
||||
ij_java_resource_list_wrap = off
|
||||
ij_java_rparen_on_new_line_in_record_header = false
|
||||
ij_java_session_dd_suffix = EJB
|
||||
ij_java_session_eb_suffix = Bean
|
||||
ij_java_session_hi_suffix = Home
|
||||
ij_java_session_lhi_prefix = Local
|
||||
ij_java_session_lhi_suffix = Home
|
||||
ij_java_session_li_prefix = Local
|
||||
ij_java_session_si_suffix = Service
|
||||
ij_java_space_after_closing_angle_bracket_in_type_argument = false
|
||||
ij_java_space_after_colon = true
|
||||
ij_java_space_after_comma = true
|
||||
ij_java_space_after_comma_in_type_arguments = true
|
||||
ij_java_space_after_for_semicolon = true
|
||||
ij_java_space_after_quest = true
|
||||
ij_java_space_after_type_cast = true
|
||||
ij_java_space_before_annotation_array_initializer_left_brace = false
|
||||
ij_java_space_before_annotation_parameter_list = false
|
||||
ij_java_space_before_array_initializer_left_brace = false
|
||||
ij_java_space_before_catch_keyword = true
|
||||
ij_java_space_before_catch_left_brace = true
|
||||
ij_java_space_before_catch_parentheses = true
|
||||
ij_java_space_before_class_left_brace = true
|
||||
ij_java_space_before_colon = true
|
||||
ij_java_space_before_colon_in_foreach = true
|
||||
ij_java_space_before_comma = false
|
||||
ij_java_space_before_do_left_brace = true
|
||||
ij_java_space_before_else_keyword = true
|
||||
ij_java_space_before_else_left_brace = true
|
||||
ij_java_space_before_finally_keyword = true
|
||||
ij_java_space_before_finally_left_brace = true
|
||||
ij_java_space_before_for_left_brace = true
|
||||
ij_java_space_before_for_parentheses = true
|
||||
ij_java_space_before_for_semicolon = false
|
||||
ij_java_space_before_if_left_brace = true
|
||||
ij_java_space_before_if_parentheses = true
|
||||
ij_java_space_before_method_call_parentheses = false
|
||||
ij_java_space_before_method_left_brace = true
|
||||
ij_java_space_before_method_parentheses = false
|
||||
ij_java_space_before_opening_angle_bracket_in_type_parameter = false
|
||||
ij_java_space_before_quest = true
|
||||
ij_java_space_before_switch_left_brace = true
|
||||
ij_java_space_before_switch_parentheses = true
|
||||
ij_java_space_before_synchronized_left_brace = true
|
||||
ij_java_space_before_synchronized_parentheses = true
|
||||
ij_java_space_before_try_left_brace = true
|
||||
ij_java_space_before_try_parentheses = true
|
||||
ij_java_space_before_type_parameter_list = false
|
||||
ij_java_space_before_while_keyword = true
|
||||
ij_java_space_before_while_left_brace = true
|
||||
ij_java_space_before_while_parentheses = true
|
||||
ij_java_space_inside_one_line_enum_braces = false
|
||||
ij_java_space_within_empty_array_initializer_braces = false
|
||||
ij_java_space_within_empty_method_call_parentheses = false
|
||||
ij_java_space_within_empty_method_parentheses = false
|
||||
ij_java_spaces_around_additive_operators = true
|
||||
ij_java_spaces_around_assignment_operators = true
|
||||
ij_java_spaces_around_bitwise_operators = true
|
||||
ij_java_spaces_around_equality_operators = true
|
||||
ij_java_spaces_around_lambda_arrow = true
|
||||
ij_java_spaces_around_logical_operators = true
|
||||
ij_java_spaces_around_method_ref_dbl_colon = false
|
||||
ij_java_spaces_around_multiplicative_operators = true
|
||||
ij_java_spaces_around_relational_operators = true
|
||||
ij_java_spaces_around_shift_operators = true
|
||||
ij_java_spaces_around_type_bounds_in_type_parameters = true
|
||||
ij_java_spaces_around_unary_operator = false
|
||||
ij_java_spaces_within_angle_brackets = false
|
||||
ij_java_spaces_within_annotation_parentheses = false
|
||||
ij_java_spaces_within_array_initializer_braces = false
|
||||
ij_java_spaces_within_braces = false
|
||||
ij_java_spaces_within_brackets = false
|
||||
ij_java_spaces_within_cast_parentheses = false
|
||||
ij_java_spaces_within_catch_parentheses = false
|
||||
ij_java_spaces_within_for_parentheses = false
|
||||
ij_java_spaces_within_if_parentheses = false
|
||||
ij_java_spaces_within_method_call_parentheses = false
|
||||
ij_java_spaces_within_method_parentheses = false
|
||||
ij_java_spaces_within_parentheses = false
|
||||
ij_java_spaces_within_record_header = false
|
||||
ij_java_spaces_within_switch_parentheses = false
|
||||
ij_java_spaces_within_synchronized_parentheses = false
|
||||
ij_java_spaces_within_try_parentheses = false
|
||||
ij_java_spaces_within_while_parentheses = false
|
||||
ij_java_special_else_if_treatment = true
|
||||
ij_java_subclass_name_suffix = Impl
|
||||
ij_java_ternary_operation_signs_on_next_line = false
|
||||
ij_java_ternary_operation_wrap = off
|
||||
ij_java_test_name_suffix = Test
|
||||
ij_java_throws_keyword_wrap = off
|
||||
ij_java_throws_list_wrap = off
|
||||
ij_java_use_external_annotations = false
|
||||
ij_java_use_fq_class_names = false
|
||||
ij_java_use_relative_indents = false
|
||||
ij_java_use_single_class_imports = true
|
||||
ij_java_variable_annotation_wrap = off
|
||||
ij_java_visibility = public
|
||||
ij_java_while_brace_force = never
|
||||
ij_java_while_on_new_line = false
|
||||
ij_java_wrap_comments = false
|
||||
ij_java_wrap_first_method_in_call_chain = false
|
||||
ij_java_wrap_long_lines = false
|
||||
|
||||
[.editorconfig]
|
||||
ij_editorconfig_align_group_field_declarations = false
|
||||
ij_editorconfig_space_after_colon = false
|
||||
ij_editorconfig_space_after_comma = true
|
||||
ij_editorconfig_space_before_colon = false
|
||||
ij_editorconfig_space_before_comma = false
|
||||
ij_editorconfig_spaces_around_assignment_operators = true
|
||||
|
||||
[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.pom,*.qrc,*.rng,*.tld,*.wadl,*.wsdd,*.wsdl,*.xjb,*.xml,*.xsd,*.xsl,*.xslt,*.xul}]
|
||||
ij_xml_align_attributes = true
|
||||
ij_xml_align_text = false
|
||||
ij_xml_attribute_wrap = normal
|
||||
ij_xml_block_comment_at_first_column = true
|
||||
ij_xml_keep_blank_lines = 2
|
||||
ij_xml_keep_indents_on_empty_lines = false
|
||||
ij_xml_keep_line_breaks = true
|
||||
ij_xml_keep_line_breaks_in_text = true
|
||||
ij_xml_keep_whitespaces = false
|
||||
ij_xml_keep_whitespaces_around_cdata = preserve
|
||||
ij_xml_keep_whitespaces_inside_cdata = false
|
||||
ij_xml_line_comment_at_first_column = true
|
||||
ij_xml_space_after_tag_name = false
|
||||
ij_xml_space_around_equals_in_attribute = false
|
||||
ij_xml_space_inside_empty_tag = false
|
||||
ij_xml_text_wrap = normal
|
||||
ij_xml_use_custom_settings = false
|
||||
|
||||
[{*.ft,*.vm,*.vsl}]
|
||||
ij_vtl_keep_indents_on_empty_lines = false
|
||||
|
||||
[{*.gant,*.gradle,*.groovy,*.gson,*.gy}]
|
||||
ij_groovy_align_group_field_declarations = false
|
||||
ij_groovy_align_multiline_array_initializer_expression = false
|
||||
ij_groovy_align_multiline_assignment = false
|
||||
ij_groovy_align_multiline_binary_operation = false
|
||||
ij_groovy_align_multiline_chained_methods = false
|
||||
ij_groovy_align_multiline_extends_list = false
|
||||
ij_groovy_align_multiline_for = true
|
||||
ij_groovy_align_multiline_list_or_map = true
|
||||
ij_groovy_align_multiline_method_parentheses = false
|
||||
ij_groovy_align_multiline_parameters = true
|
||||
ij_groovy_align_multiline_parameters_in_calls = false
|
||||
ij_groovy_align_multiline_resources = true
|
||||
ij_groovy_align_multiline_ternary_operation = false
|
||||
ij_groovy_align_multiline_throws_list = false
|
||||
ij_groovy_align_named_args_in_map = true
|
||||
ij_groovy_align_throws_keyword = false
|
||||
ij_groovy_array_initializer_new_line_after_left_brace = false
|
||||
ij_groovy_array_initializer_right_brace_on_new_line = false
|
||||
ij_groovy_array_initializer_wrap = off
|
||||
ij_groovy_assert_statement_wrap = off
|
||||
ij_groovy_assignment_wrap = off
|
||||
ij_groovy_binary_operation_wrap = off
|
||||
ij_groovy_blank_lines_after_class_header = 0
|
||||
ij_groovy_blank_lines_after_imports = 1
|
||||
ij_groovy_blank_lines_after_package = 1
|
||||
ij_groovy_blank_lines_around_class = 1
|
||||
ij_groovy_blank_lines_around_field = 0
|
||||
ij_groovy_blank_lines_around_field_in_interface = 0
|
||||
ij_groovy_blank_lines_around_method = 1
|
||||
ij_groovy_blank_lines_around_method_in_interface = 1
|
||||
ij_groovy_blank_lines_before_imports = 1
|
||||
ij_groovy_blank_lines_before_method_body = 0
|
||||
ij_groovy_blank_lines_before_package = 0
|
||||
ij_groovy_block_brace_style = end_of_line
|
||||
ij_groovy_block_comment_at_first_column = true
|
||||
ij_groovy_call_parameters_new_line_after_left_paren = false
|
||||
ij_groovy_call_parameters_right_paren_on_new_line = false
|
||||
ij_groovy_call_parameters_wrap = off
|
||||
ij_groovy_catch_on_new_line = false
|
||||
ij_groovy_class_annotation_wrap = split_into_lines
|
||||
ij_groovy_class_brace_style = end_of_line
|
||||
ij_groovy_class_count_to_use_import_on_demand = 5
|
||||
ij_groovy_do_while_brace_force = never
|
||||
ij_groovy_else_on_new_line = false
|
||||
ij_groovy_enum_constants_wrap = off
|
||||
ij_groovy_extends_keyword_wrap = off
|
||||
ij_groovy_extends_list_wrap = off
|
||||
ij_groovy_field_annotation_wrap = split_into_lines
|
||||
ij_groovy_finally_on_new_line = false
|
||||
ij_groovy_for_brace_force = never
|
||||
ij_groovy_for_statement_new_line_after_left_paren = false
|
||||
ij_groovy_for_statement_right_paren_on_new_line = false
|
||||
ij_groovy_for_statement_wrap = off
|
||||
ij_groovy_if_brace_force = never
|
||||
ij_groovy_import_annotation_wrap = 2
|
||||
ij_groovy_imports_layout = *,|,javax.**,java.**,|,$*
|
||||
ij_groovy_indent_case_from_switch = true
|
||||
ij_groovy_indent_label_blocks = true
|
||||
ij_groovy_insert_inner_class_imports = false
|
||||
ij_groovy_keep_blank_lines_before_right_brace = 2
|
||||
ij_groovy_keep_blank_lines_in_code = 2
|
||||
ij_groovy_keep_blank_lines_in_declarations = 2
|
||||
ij_groovy_keep_control_statement_in_one_line = true
|
||||
ij_groovy_keep_first_column_comment = true
|
||||
ij_groovy_keep_indents_on_empty_lines = false
|
||||
ij_groovy_keep_line_breaks = true
|
||||
ij_groovy_keep_multiple_expressions_in_one_line = false
|
||||
ij_groovy_keep_simple_blocks_in_one_line = false
|
||||
ij_groovy_keep_simple_classes_in_one_line = true
|
||||
ij_groovy_keep_simple_lambdas_in_one_line = true
|
||||
ij_groovy_keep_simple_methods_in_one_line = true
|
||||
ij_groovy_label_indent_absolute = false
|
||||
ij_groovy_label_indent_size = 0
|
||||
ij_groovy_lambda_brace_style = end_of_line
|
||||
ij_groovy_layout_static_imports_separately = true
|
||||
ij_groovy_line_comment_add_space = false
|
||||
ij_groovy_line_comment_at_first_column = true
|
||||
ij_groovy_method_annotation_wrap = split_into_lines
|
||||
ij_groovy_method_brace_style = end_of_line
|
||||
ij_groovy_method_call_chain_wrap = off
|
||||
ij_groovy_method_parameters_new_line_after_left_paren = false
|
||||
ij_groovy_method_parameters_right_paren_on_new_line = false
|
||||
ij_groovy_method_parameters_wrap = off
|
||||
ij_groovy_modifier_list_wrap = false
|
||||
ij_groovy_names_count_to_use_import_on_demand = 3
|
||||
ij_groovy_parameter_annotation_wrap = off
|
||||
ij_groovy_parentheses_expression_new_line_after_left_paren = false
|
||||
ij_groovy_parentheses_expression_right_paren_on_new_line = false
|
||||
ij_groovy_prefer_parameters_wrap = false
|
||||
ij_groovy_resource_list_new_line_after_left_paren = false
|
||||
ij_groovy_resource_list_right_paren_on_new_line = false
|
||||
ij_groovy_resource_list_wrap = off
|
||||
ij_groovy_space_after_assert_separator = true
|
||||
ij_groovy_space_after_colon = true
|
||||
ij_groovy_space_after_comma = true
|
||||
ij_groovy_space_after_comma_in_type_arguments = true
|
||||
ij_groovy_space_after_for_semicolon = true
|
||||
ij_groovy_space_after_quest = true
|
||||
ij_groovy_space_after_type_cast = true
|
||||
ij_groovy_space_before_annotation_parameter_list = false
|
||||
ij_groovy_space_before_array_initializer_left_brace = false
|
||||
ij_groovy_space_before_assert_separator = false
|
||||
ij_groovy_space_before_catch_keyword = true
|
||||
ij_groovy_space_before_catch_left_brace = true
|
||||
ij_groovy_space_before_catch_parentheses = true
|
||||
ij_groovy_space_before_class_left_brace = true
|
||||
ij_groovy_space_before_closure_left_brace = true
|
||||
ij_groovy_space_before_colon = true
|
||||
ij_groovy_space_before_comma = false
|
||||
ij_groovy_space_before_do_left_brace = true
|
||||
ij_groovy_space_before_else_keyword = true
|
||||
ij_groovy_space_before_else_left_brace = true
|
||||
ij_groovy_space_before_finally_keyword = true
|
||||
ij_groovy_space_before_finally_left_brace = true
|
||||
ij_groovy_space_before_for_left_brace = true
|
||||
ij_groovy_space_before_for_parentheses = true
|
||||
ij_groovy_space_before_for_semicolon = false
|
||||
ij_groovy_space_before_if_left_brace = true
|
||||
ij_groovy_space_before_if_parentheses = true
|
||||
ij_groovy_space_before_method_call_parentheses = false
|
||||
ij_groovy_space_before_method_left_brace = true
|
||||
ij_groovy_space_before_method_parentheses = false
|
||||
ij_groovy_space_before_quest = true
|
||||
ij_groovy_space_before_switch_left_brace = true
|
||||
ij_groovy_space_before_switch_parentheses = true
|
||||
ij_groovy_space_before_synchronized_left_brace = true
|
||||
ij_groovy_space_before_synchronized_parentheses = true
|
||||
ij_groovy_space_before_try_left_brace = true
|
||||
ij_groovy_space_before_try_parentheses = true
|
||||
ij_groovy_space_before_while_keyword = true
|
||||
ij_groovy_space_before_while_left_brace = true
|
||||
ij_groovy_space_before_while_parentheses = true
|
||||
ij_groovy_space_in_named_argument = true
|
||||
ij_groovy_space_in_named_argument_before_colon = false
|
||||
ij_groovy_space_within_empty_array_initializer_braces = false
|
||||
ij_groovy_space_within_empty_method_call_parentheses = false
|
||||
ij_groovy_spaces_around_additive_operators = true
|
||||
ij_groovy_spaces_around_assignment_operators = true
|
||||
ij_groovy_spaces_around_bitwise_operators = true
|
||||
ij_groovy_spaces_around_equality_operators = true
|
||||
ij_groovy_spaces_around_lambda_arrow = true
|
||||
ij_groovy_spaces_around_logical_operators = true
|
||||
ij_groovy_spaces_around_multiplicative_operators = true
|
||||
ij_groovy_spaces_around_regex_operators = true
|
||||
ij_groovy_spaces_around_relational_operators = true
|
||||
ij_groovy_spaces_around_shift_operators = true
|
||||
ij_groovy_spaces_within_annotation_parentheses = false
|
||||
ij_groovy_spaces_within_array_initializer_braces = false
|
||||
ij_groovy_spaces_within_braces = true
|
||||
ij_groovy_spaces_within_brackets = false
|
||||
ij_groovy_spaces_within_cast_parentheses = false
|
||||
ij_groovy_spaces_within_catch_parentheses = false
|
||||
ij_groovy_spaces_within_for_parentheses = false
|
||||
ij_groovy_spaces_within_gstring_injection_braces = false
|
||||
ij_groovy_spaces_within_if_parentheses = false
|
||||
ij_groovy_spaces_within_list_or_map = false
|
||||
ij_groovy_spaces_within_method_call_parentheses = false
|
||||
ij_groovy_spaces_within_method_parentheses = false
|
||||
ij_groovy_spaces_within_parentheses = false
|
||||
ij_groovy_spaces_within_switch_parentheses = false
|
||||
ij_groovy_spaces_within_synchronized_parentheses = false
|
||||
ij_groovy_spaces_within_try_parentheses = false
|
||||
ij_groovy_spaces_within_tuple_expression = false
|
||||
ij_groovy_spaces_within_while_parentheses = false
|
||||
ij_groovy_special_else_if_treatment = true
|
||||
ij_groovy_ternary_operation_wrap = off
|
||||
ij_groovy_throws_keyword_wrap = off
|
||||
ij_groovy_throws_list_wrap = off
|
||||
ij_groovy_use_flying_geese_braces = false
|
||||
ij_groovy_use_fq_class_names = false
|
||||
ij_groovy_use_fq_class_names_in_javadoc = true
|
||||
ij_groovy_use_relative_indents = false
|
||||
ij_groovy_use_single_class_imports = true
|
||||
ij_groovy_variable_annotation_wrap = off
|
||||
ij_groovy_while_brace_force = never
|
||||
ij_groovy_while_on_new_line = false
|
||||
ij_groovy_wrap_long_lines = false
|
||||
|
||||
[{*.gradle.kts,*.kt,*.kts,*.main.kts}]
|
||||
ij_kotlin_align_in_columns_case_branch = false
|
||||
ij_kotlin_align_multiline_binary_operation = false
|
||||
ij_kotlin_align_multiline_extends_list = false
|
||||
ij_kotlin_align_multiline_method_parentheses = false
|
||||
ij_kotlin_align_multiline_parameters = true
|
||||
ij_kotlin_align_multiline_parameters_in_calls = false
|
||||
ij_kotlin_allow_trailing_comma = false
|
||||
ij_kotlin_allow_trailing_comma_on_call_site = false
|
||||
ij_kotlin_assignment_wrap = normal
|
||||
ij_kotlin_blank_lines_after_class_header = 0
|
||||
ij_kotlin_blank_lines_around_block_when_branches = 0
|
||||
ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1
|
||||
ij_kotlin_block_comment_at_first_column = true
|
||||
ij_kotlin_call_parameters_new_line_after_left_paren = true
|
||||
ij_kotlin_call_parameters_right_paren_on_new_line = true
|
||||
ij_kotlin_call_parameters_wrap = on_every_item
|
||||
ij_kotlin_catch_on_new_line = false
|
||||
ij_kotlin_class_annotation_wrap = split_into_lines
|
||||
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
|
||||
ij_kotlin_continuation_indent_for_chained_calls = false
|
||||
ij_kotlin_continuation_indent_for_expression_bodies = false
|
||||
ij_kotlin_continuation_indent_in_argument_lists = false
|
||||
ij_kotlin_continuation_indent_in_elvis = false
|
||||
ij_kotlin_continuation_indent_in_if_conditions = false
|
||||
ij_kotlin_continuation_indent_in_parameter_lists = false
|
||||
ij_kotlin_continuation_indent_in_supertype_lists = false
|
||||
ij_kotlin_else_on_new_line = false
|
||||
ij_kotlin_enum_constants_wrap = off
|
||||
ij_kotlin_extends_list_wrap = normal
|
||||
ij_kotlin_field_annotation_wrap = split_into_lines
|
||||
ij_kotlin_finally_on_new_line = false
|
||||
ij_kotlin_if_rparen_on_new_line = true
|
||||
ij_kotlin_import_nested_classes = false
|
||||
ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^
|
||||
ij_kotlin_insert_whitespaces_in_simple_one_line_method = true
|
||||
ij_kotlin_keep_blank_lines_before_right_brace = 2
|
||||
ij_kotlin_keep_blank_lines_in_code = 2
|
||||
ij_kotlin_keep_blank_lines_in_declarations = 2
|
||||
ij_kotlin_keep_first_column_comment = true
|
||||
ij_kotlin_keep_indents_on_empty_lines = false
|
||||
ij_kotlin_keep_line_breaks = true
|
||||
ij_kotlin_lbrace_on_next_line = false
|
||||
ij_kotlin_line_comment_add_space = false
|
||||
ij_kotlin_line_comment_at_first_column = true
|
||||
ij_kotlin_method_annotation_wrap = split_into_lines
|
||||
ij_kotlin_method_call_chain_wrap = normal
|
||||
ij_kotlin_method_parameters_new_line_after_left_paren = true
|
||||
ij_kotlin_method_parameters_right_paren_on_new_line = true
|
||||
ij_kotlin_method_parameters_wrap = on_every_item
|
||||
ij_kotlin_name_count_to_use_star_import = 5
|
||||
ij_kotlin_name_count_to_use_star_import_for_members = 3
|
||||
ij_kotlin_packages_to_use_import_on_demand = java.util.*,kotlinx.android.synthetic.**,io.ktor.**
|
||||
ij_kotlin_parameter_annotation_wrap = off
|
||||
ij_kotlin_space_after_comma = true
|
||||
ij_kotlin_space_after_extend_colon = true
|
||||
ij_kotlin_space_after_type_colon = true
|
||||
ij_kotlin_space_before_catch_parentheses = true
|
||||
ij_kotlin_space_before_comma = false
|
||||
ij_kotlin_space_before_extend_colon = true
|
||||
ij_kotlin_space_before_for_parentheses = true
|
||||
ij_kotlin_space_before_if_parentheses = true
|
||||
ij_kotlin_space_before_lambda_arrow = true
|
||||
ij_kotlin_space_before_type_colon = false
|
||||
ij_kotlin_space_before_when_parentheses = true
|
||||
ij_kotlin_space_before_while_parentheses = true
|
||||
ij_kotlin_spaces_around_additive_operators = true
|
||||
ij_kotlin_spaces_around_assignment_operators = true
|
||||
ij_kotlin_spaces_around_equality_operators = true
|
||||
ij_kotlin_spaces_around_function_type_arrow = true
|
||||
ij_kotlin_spaces_around_logical_operators = true
|
||||
ij_kotlin_spaces_around_multiplicative_operators = true
|
||||
ij_kotlin_spaces_around_range = false
|
||||
ij_kotlin_spaces_around_relational_operators = true
|
||||
ij_kotlin_spaces_around_unary_operator = false
|
||||
ij_kotlin_spaces_around_when_arrow = true
|
||||
ij_kotlin_variable_annotation_wrap = off
|
||||
ij_kotlin_while_on_new_line = false
|
||||
ij_kotlin_wrap_elvis_expressions = 1
|
||||
ij_kotlin_wrap_expression_body_functions = 1
|
||||
ij_kotlin_wrap_first_method_in_call_chain = false
|
||||
|
||||
[{*.htm,*.html,*.ng,*.sht,*.shtm,*.shtml}]
|
||||
ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3
|
||||
ij_html_align_attributes = true
|
||||
ij_html_align_text = false
|
||||
ij_html_attribute_wrap = normal
|
||||
ij_html_block_comment_at_first_column = true
|
||||
ij_html_do_not_align_children_of_min_lines = 0
|
||||
ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p
|
||||
ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot
|
||||
ij_html_enforce_quotes = false
|
||||
ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var
|
||||
ij_html_keep_blank_lines = 2
|
||||
ij_html_keep_indents_on_empty_lines = false
|
||||
ij_html_keep_line_breaks = true
|
||||
ij_html_keep_line_breaks_in_text = true
|
||||
ij_html_keep_whitespaces = false
|
||||
ij_html_keep_whitespaces_inside = span,pre,textarea
|
||||
ij_html_line_comment_at_first_column = true
|
||||
ij_html_new_line_after_last_attribute = never
|
||||
ij_html_new_line_before_first_attribute = never
|
||||
ij_html_quote_style = double
|
||||
ij_html_remove_new_line_before_tags = br
|
||||
ij_html_space_after_tag_name = false
|
||||
ij_html_space_around_equality_in_attribute = false
|
||||
ij_html_space_inside_empty_tag = false
|
||||
ij_html_text_wrap = normal
|
||||
ij_html_uniform_ident = false
|
||||
|
||||
[{*.markdown,*.md}]
|
||||
ij_markdown_force_one_space_after_blockquote_symbol = true
|
||||
ij_markdown_force_one_space_after_header_symbol = true
|
||||
ij_markdown_force_one_space_after_list_bullet = true
|
||||
ij_markdown_force_one_space_between_words = true
|
||||
ij_markdown_keep_indents_on_empty_lines = false
|
||||
ij_markdown_max_lines_around_block_elements = 1
|
||||
ij_markdown_max_lines_around_header = 1
|
||||
ij_markdown_max_lines_between_paragraphs = 1
|
||||
ij_markdown_min_lines_around_block_elements = 1
|
||||
ij_markdown_min_lines_around_header = 1
|
||||
ij_markdown_min_lines_between_paragraphs = 1
|
||||
|
||||
[{*.properties,spring.handlers,spring.schemas}]
|
||||
ij_properties_align_group_field_declarations = false
|
||||
ij_properties_keep_blank_lines = false
|
||||
ij_properties_key_value_delimiter = equals
|
||||
ij_properties_spaces_around_key_value_delimiter = false
|
||||
|
||||
[{*.yaml,*.yml}]
|
||||
indent_size = 2
|
||||
ij_yaml_keep_indents_on_empty_lines = false
|
||||
ij_yaml_keep_line_breaks = true
|
||||
ij_yaml_space_before_colon = true
|
||||
ij_yaml_spaces_within_braces = true
|
||||
ij_yaml_spaces_within_brackets = true
|
||||
+27
-645
@@ -5,7 +5,7 @@
|
||||
-->
|
||||
|
||||
# Contributing
|
||||
All contributions should be in the form of [pull requests](https://github.com/kernitus/BukkitOldCombatMechanics/pulls), and should use the formatting profiles provided at the bottom of this page. All pull requests must be fully functional and able to compile, and should be fully tested. Please submit **monofocal** pull requests, i.e.if you're making unrelated changes to two different modules, or decide to also update the version of a dependency, *those should be separate pull requests*.
|
||||
All contributions should be in the form of [pull requests](https://github.com/kernitus/BukkitOldCombatMechanics/pulls), and should follow the formatting guidance below. All pull requests must be fully functional and able to compile, and should be fully tested. Please submit **monofocal** pull requests, i.e.if you're making unrelated changes to two different modules, or decide to also update the version of a dependency, *those should be separate pull requests*.
|
||||
|
||||
## Testing expectations
|
||||
Contributions are expected to include automated test coverage within the existing test framework where practical. For behaviour changes, bug fixes, and new features, prefer adding or updating tests in the existing Kotlin integration-test harness rather than relying on manual testing alone.
|
||||
@@ -13,6 +13,29 @@ Contributions are expected to include automated test coverage within the existin
|
||||
## Language expectations
|
||||
New classes should be written in Kotlin by default. If you are modifying an existing Java class, keep the surrounding file consistent unless there is a clear reason to migrate it as part of the same change.
|
||||
|
||||
## Local setup
|
||||
Install [pre-commit](https://pre-commit.com/) with your preferred Python package manager, for example:
|
||||
|
||||
```shell
|
||||
pipx install pre-commit
|
||||
```
|
||||
|
||||
Then install and test the repository hooks:
|
||||
|
||||
```shell
|
||||
pre-commit install
|
||||
pre-commit run --all-files
|
||||
```
|
||||
|
||||
Before committing or opening a pull request, run:
|
||||
|
||||
```shell
|
||||
./gradlew spotlessCheck
|
||||
./gradlew check
|
||||
```
|
||||
|
||||
Also run the relevant integration tests when behaviour, compatibility, or bug-fix work warrants them, such as `./gradlew integrationTest` or a targeted task like `./gradlew integrationTest1_19_2`.
|
||||
|
||||
## Module system
|
||||
OldCombatMechanics uses a modular system to make sure each feature is completely independent from any other, and can be toggled off and have no impact on server performance. This means each new module must extend the `Module` class, and implement a public constructor which takes an instance of the plugin and passes the module name to the superconstructor. The `Module` class also provides an overloadable `reload()` method which is called whenever the plugin is reloaded. If you are using any class-level variables they should probably be updated in here. This should also be used for any initialisation that might need to be done if the config section is changed and `ocm reload` is called. You may then call `reload()` from the constructor.
|
||||
|
||||
@@ -28,648 +51,7 @@ There is relative freedom when it codes to coding style, but please adhere to th
|
||||
* Use `if !condition return;` pattern to avoid eccessive nested if-statements. That is, to check multiple prerequisite conditions, invert the condition and return, and after returning continue with the code.
|
||||
* Use internal utilies where possible. There is a `Messenger` class for sending messages to users or console, a `Reflector` class for simple reflection, and `DualVersionedMaterial` for support across minecraft versions where the Bukkit item names have changed. There are many more in the `utilities` package, the usage of which can be found in already existing modules.
|
||||
|
||||
## Formatting profiles
|
||||
Please use the [default formatting profile](https://www.jetbrains.com/help/idea/reformat-and-rearrange-code.html) provided with IntelliJ. If using a different IDE, here is the [.editconfig](https://www.jetbrains.com/help/idea/configuring-code-style.html#editorconfig) file for those same settings:
|
||||
## Formatting
|
||||
The repository root `.editorconfig` is the source of truth for IntelliJ and EditorConfig-aware editors. Use `./gradlew spotlessCheck` to verify formatting and `./gradlew spotlessApply` to apply formatting fixes.
|
||||
|
||||
```
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = false
|
||||
max_line_length = 120
|
||||
tab_width = 4
|
||||
ij_continuation_indent_size = 8
|
||||
ij_formatter_off_tag = @formatter:off
|
||||
ij_formatter_on_tag = @formatter:on
|
||||
ij_formatter_tags_enabled = false
|
||||
ij_smart_tabs = false
|
||||
ij_visual_guides = none
|
||||
ij_wrap_on_typing = false
|
||||
|
||||
[*.css]
|
||||
ij_css_align_closing_brace_with_properties = false
|
||||
ij_css_blank_lines_around_nested_selector = 1
|
||||
ij_css_blank_lines_between_blocks = 1
|
||||
ij_css_brace_placement = end_of_line
|
||||
ij_css_enforce_quotes_on_format = false
|
||||
ij_css_hex_color_long_format = false
|
||||
ij_css_hex_color_lower_case = false
|
||||
ij_css_hex_color_short_format = false
|
||||
ij_css_hex_color_upper_case = false
|
||||
ij_css_keep_blank_lines_in_code = 2
|
||||
ij_css_keep_indents_on_empty_lines = false
|
||||
ij_css_keep_single_line_blocks = false
|
||||
ij_css_properties_order = font,font-family,font-size,font-weight,font-style,font-variant,font-size-adjust,font-stretch,line-height,position,z-index,top,right,bottom,left,display,visibility,float,clear,overflow,overflow-x,overflow-y,clip,zoom,align-content,align-items,align-self,flex,flex-flow,flex-basis,flex-direction,flex-grow,flex-shrink,flex-wrap,justify-content,order,box-sizing,width,min-width,max-width,height,min-height,max-height,margin,margin-top,margin-right,margin-bottom,margin-left,padding,padding-top,padding-right,padding-bottom,padding-left,table-layout,empty-cells,caption-side,border-spacing,border-collapse,list-style,list-style-position,list-style-type,list-style-image,content,quotes,counter-reset,counter-increment,resize,cursor,user-select,nav-index,nav-up,nav-right,nav-down,nav-left,transition,transition-delay,transition-timing-function,transition-duration,transition-property,transform,transform-origin,animation,animation-name,animation-duration,animation-play-state,animation-timing-function,animation-delay,animation-iteration-count,animation-direction,text-align,text-align-last,vertical-align,white-space,text-decoration,text-emphasis,text-emphasis-color,text-emphasis-style,text-emphasis-position,text-indent,text-justify,letter-spacing,word-spacing,text-outline,text-transform,text-wrap,text-overflow,text-overflow-ellipsis,text-overflow-mode,word-wrap,word-break,tab-size,hyphens,pointer-events,opacity,color,border,border-width,border-style,border-color,border-top,border-top-width,border-top-style,border-top-color,border-right,border-right-width,border-right-style,border-right-color,border-bottom,border-bottom-width,border-bottom-style,border-bottom-color,border-left,border-left-width,border-left-style,border-left-color,border-radius,border-top-left-radius,border-top-right-radius,border-bottom-right-radius,border-bottom-left-radius,border-image,border-image-source,border-image-slice,border-image-width,border-image-outset,border-image-repeat,outline,outline-width,outline-style,outline-color,outline-offset,background,background-color,background-image,background-repeat,background-attachment,background-position,background-position-x,background-position-y,background-clip,background-origin,background-size,box-decoration-break,box-shadow,text-shadow
|
||||
ij_css_space_after_colon = true
|
||||
ij_css_space_before_opening_brace = true
|
||||
ij_css_use_double_quotes = true
|
||||
ij_css_value_alignment = do_not_align
|
||||
|
||||
[*.feature]
|
||||
indent_size = 2
|
||||
ij_gherkin_keep_indents_on_empty_lines = false
|
||||
|
||||
[*.gsp]
|
||||
ij_gsp_keep_indents_on_empty_lines = false
|
||||
|
||||
[*.haml]
|
||||
indent_size = 2
|
||||
ij_haml_keep_indents_on_empty_lines = false
|
||||
|
||||
[*.java]
|
||||
ij_java_align_consecutive_assignments = false
|
||||
ij_java_align_consecutive_variable_declarations = false
|
||||
ij_java_align_group_field_declarations = false
|
||||
ij_java_align_multiline_annotation_parameters = false
|
||||
ij_java_align_multiline_array_initializer_expression = false
|
||||
ij_java_align_multiline_assignment = false
|
||||
ij_java_align_multiline_binary_operation = false
|
||||
ij_java_align_multiline_chained_methods = false
|
||||
ij_java_align_multiline_extends_list = false
|
||||
ij_java_align_multiline_for = true
|
||||
ij_java_align_multiline_method_parentheses = false
|
||||
ij_java_align_multiline_parameters = true
|
||||
ij_java_align_multiline_parameters_in_calls = false
|
||||
ij_java_align_multiline_parenthesized_expression = false
|
||||
ij_java_align_multiline_records = true
|
||||
ij_java_align_multiline_resources = true
|
||||
ij_java_align_multiline_ternary_operation = false
|
||||
ij_java_align_multiline_text_blocks = false
|
||||
ij_java_align_multiline_throws_list = false
|
||||
ij_java_align_subsequent_simple_methods = false
|
||||
ij_java_align_throws_keyword = false
|
||||
ij_java_annotation_parameter_wrap = off
|
||||
ij_java_array_initializer_new_line_after_left_brace = false
|
||||
ij_java_array_initializer_right_brace_on_new_line = false
|
||||
ij_java_array_initializer_wrap = off
|
||||
ij_java_assert_statement_colon_on_next_line = false
|
||||
ij_java_assert_statement_wrap = off
|
||||
ij_java_assignment_wrap = off
|
||||
ij_java_binary_operation_sign_on_next_line = false
|
||||
ij_java_binary_operation_wrap = off
|
||||
ij_java_blank_lines_after_anonymous_class_header = 0
|
||||
ij_java_blank_lines_after_class_header = 0
|
||||
ij_java_blank_lines_after_imports = 1
|
||||
ij_java_blank_lines_after_package = 1
|
||||
ij_java_blank_lines_around_class = 1
|
||||
ij_java_blank_lines_around_field = 0
|
||||
ij_java_blank_lines_around_field_in_interface = 0
|
||||
ij_java_blank_lines_around_initializer = 1
|
||||
ij_java_blank_lines_around_method = 1
|
||||
ij_java_blank_lines_around_method_in_interface = 1
|
||||
ij_java_blank_lines_before_class_end = 0
|
||||
ij_java_blank_lines_before_imports = 1
|
||||
ij_java_blank_lines_before_method_body = 0
|
||||
ij_java_blank_lines_before_package = 0
|
||||
ij_java_block_brace_style = end_of_line
|
||||
ij_java_block_comment_at_first_column = true
|
||||
ij_java_call_parameters_new_line_after_left_paren = false
|
||||
ij_java_call_parameters_right_paren_on_new_line = false
|
||||
ij_java_call_parameters_wrap = off
|
||||
ij_java_case_statement_on_separate_line = true
|
||||
ij_java_catch_on_new_line = false
|
||||
ij_java_class_annotation_wrap = split_into_lines
|
||||
ij_java_class_brace_style = end_of_line
|
||||
ij_java_class_count_to_use_import_on_demand = 5
|
||||
ij_java_class_names_in_javadoc = 1
|
||||
ij_java_do_not_indent_top_level_class_members = false
|
||||
ij_java_do_not_wrap_after_single_annotation = false
|
||||
ij_java_do_while_brace_force = never
|
||||
ij_java_doc_add_blank_line_after_description = true
|
||||
ij_java_doc_add_blank_line_after_param_comments = false
|
||||
ij_java_doc_add_blank_line_after_return = false
|
||||
ij_java_doc_add_p_tag_on_empty_lines = true
|
||||
ij_java_doc_align_exception_comments = true
|
||||
ij_java_doc_align_param_comments = true
|
||||
ij_java_doc_do_not_wrap_if_one_line = false
|
||||
ij_java_doc_enable_formatting = true
|
||||
ij_java_doc_enable_leading_asterisks = true
|
||||
ij_java_doc_indent_on_continuation = false
|
||||
ij_java_doc_keep_empty_lines = true
|
||||
ij_java_doc_keep_empty_parameter_tag = true
|
||||
ij_java_doc_keep_empty_return_tag = true
|
||||
ij_java_doc_keep_empty_throws_tag = true
|
||||
ij_java_doc_keep_invalid_tags = true
|
||||
ij_java_doc_param_description_on_new_line = false
|
||||
ij_java_doc_preserve_line_breaks = false
|
||||
ij_java_doc_use_throws_not_exception_tag = true
|
||||
ij_java_else_on_new_line = false
|
||||
ij_java_entity_dd_suffix = EJB
|
||||
ij_java_entity_eb_suffix = Bean
|
||||
ij_java_entity_hi_suffix = Home
|
||||
ij_java_entity_lhi_prefix = Local
|
||||
ij_java_entity_lhi_suffix = Home
|
||||
ij_java_entity_li_prefix = Local
|
||||
ij_java_entity_pk_class = java.lang.String
|
||||
ij_java_entity_vo_suffix = VO
|
||||
ij_java_enum_constants_wrap = off
|
||||
ij_java_extends_keyword_wrap = off
|
||||
ij_java_extends_list_wrap = off
|
||||
ij_java_field_annotation_wrap = split_into_lines
|
||||
ij_java_finally_on_new_line = false
|
||||
ij_java_for_brace_force = never
|
||||
ij_java_for_statement_new_line_after_left_paren = false
|
||||
ij_java_for_statement_right_paren_on_new_line = false
|
||||
ij_java_for_statement_wrap = off
|
||||
ij_java_generate_final_locals = false
|
||||
ij_java_generate_final_parameters = false
|
||||
ij_java_if_brace_force = never
|
||||
ij_java_imports_layout = *,|,javax.**,java.**,|,$*
|
||||
ij_java_indent_case_from_switch = true
|
||||
ij_java_insert_inner_class_imports = false
|
||||
ij_java_insert_override_annotation = true
|
||||
ij_java_keep_blank_lines_before_right_brace = 2
|
||||
ij_java_keep_blank_lines_between_package_declaration_and_header = 2
|
||||
ij_java_keep_blank_lines_in_code = 2
|
||||
ij_java_keep_blank_lines_in_declarations = 2
|
||||
ij_java_keep_control_statement_in_one_line = true
|
||||
ij_java_keep_first_column_comment = true
|
||||
ij_java_keep_indents_on_empty_lines = false
|
||||
ij_java_keep_line_breaks = true
|
||||
ij_java_keep_multiple_expressions_in_one_line = false
|
||||
ij_java_keep_simple_blocks_in_one_line = false
|
||||
ij_java_keep_simple_classes_in_one_line = false
|
||||
ij_java_keep_simple_lambdas_in_one_line = false
|
||||
ij_java_keep_simple_methods_in_one_line = false
|
||||
ij_java_label_indent_absolute = false
|
||||
ij_java_label_indent_size = 0
|
||||
ij_java_lambda_brace_style = end_of_line
|
||||
ij_java_layout_static_imports_separately = true
|
||||
ij_java_line_comment_add_space = false
|
||||
ij_java_line_comment_at_first_column = true
|
||||
ij_java_message_dd_suffix = EJB
|
||||
ij_java_message_eb_suffix = Bean
|
||||
ij_java_method_annotation_wrap = split_into_lines
|
||||
ij_java_method_brace_style = end_of_line
|
||||
ij_java_method_call_chain_wrap = off
|
||||
ij_java_method_parameters_new_line_after_left_paren = false
|
||||
ij_java_method_parameters_right_paren_on_new_line = false
|
||||
ij_java_method_parameters_wrap = off
|
||||
ij_java_modifier_list_wrap = false
|
||||
ij_java_names_count_to_use_import_on_demand = 3
|
||||
ij_java_new_line_after_lparen_in_record_header = false
|
||||
ij_java_packages_to_use_import_on_demand = java.awt.*,javax.swing.*
|
||||
ij_java_parameter_annotation_wrap = off
|
||||
ij_java_parentheses_expression_new_line_after_left_paren = false
|
||||
ij_java_parentheses_expression_right_paren_on_new_line = false
|
||||
ij_java_place_assignment_sign_on_next_line = false
|
||||
ij_java_prefer_longer_names = true
|
||||
ij_java_prefer_parameters_wrap = false
|
||||
ij_java_record_components_wrap = normal
|
||||
ij_java_repeat_synchronized = true
|
||||
ij_java_replace_instanceof_and_cast = false
|
||||
ij_java_replace_null_check = true
|
||||
ij_java_replace_sum_lambda_with_method_ref = true
|
||||
ij_java_resource_list_new_line_after_left_paren = false
|
||||
ij_java_resource_list_right_paren_on_new_line = false
|
||||
ij_java_resource_list_wrap = off
|
||||
ij_java_rparen_on_new_line_in_record_header = false
|
||||
ij_java_session_dd_suffix = EJB
|
||||
ij_java_session_eb_suffix = Bean
|
||||
ij_java_session_hi_suffix = Home
|
||||
ij_java_session_lhi_prefix = Local
|
||||
ij_java_session_lhi_suffix = Home
|
||||
ij_java_session_li_prefix = Local
|
||||
ij_java_session_si_suffix = Service
|
||||
ij_java_space_after_closing_angle_bracket_in_type_argument = false
|
||||
ij_java_space_after_colon = true
|
||||
ij_java_space_after_comma = true
|
||||
ij_java_space_after_comma_in_type_arguments = true
|
||||
ij_java_space_after_for_semicolon = true
|
||||
ij_java_space_after_quest = true
|
||||
ij_java_space_after_type_cast = true
|
||||
ij_java_space_before_annotation_array_initializer_left_brace = false
|
||||
ij_java_space_before_annotation_parameter_list = false
|
||||
ij_java_space_before_array_initializer_left_brace = false
|
||||
ij_java_space_before_catch_keyword = true
|
||||
ij_java_space_before_catch_left_brace = true
|
||||
ij_java_space_before_catch_parentheses = true
|
||||
ij_java_space_before_class_left_brace = true
|
||||
ij_java_space_before_colon = true
|
||||
ij_java_space_before_colon_in_foreach = true
|
||||
ij_java_space_before_comma = false
|
||||
ij_java_space_before_do_left_brace = true
|
||||
ij_java_space_before_else_keyword = true
|
||||
ij_java_space_before_else_left_brace = true
|
||||
ij_java_space_before_finally_keyword = true
|
||||
ij_java_space_before_finally_left_brace = true
|
||||
ij_java_space_before_for_left_brace = true
|
||||
ij_java_space_before_for_parentheses = true
|
||||
ij_java_space_before_for_semicolon = false
|
||||
ij_java_space_before_if_left_brace = true
|
||||
ij_java_space_before_if_parentheses = true
|
||||
ij_java_space_before_method_call_parentheses = false
|
||||
ij_java_space_before_method_left_brace = true
|
||||
ij_java_space_before_method_parentheses = false
|
||||
ij_java_space_before_opening_angle_bracket_in_type_parameter = false
|
||||
ij_java_space_before_quest = true
|
||||
ij_java_space_before_switch_left_brace = true
|
||||
ij_java_space_before_switch_parentheses = true
|
||||
ij_java_space_before_synchronized_left_brace = true
|
||||
ij_java_space_before_synchronized_parentheses = true
|
||||
ij_java_space_before_try_left_brace = true
|
||||
ij_java_space_before_try_parentheses = true
|
||||
ij_java_space_before_type_parameter_list = false
|
||||
ij_java_space_before_while_keyword = true
|
||||
ij_java_space_before_while_left_brace = true
|
||||
ij_java_space_before_while_parentheses = true
|
||||
ij_java_space_inside_one_line_enum_braces = false
|
||||
ij_java_space_within_empty_array_initializer_braces = false
|
||||
ij_java_space_within_empty_method_call_parentheses = false
|
||||
ij_java_space_within_empty_method_parentheses = false
|
||||
ij_java_spaces_around_additive_operators = true
|
||||
ij_java_spaces_around_assignment_operators = true
|
||||
ij_java_spaces_around_bitwise_operators = true
|
||||
ij_java_spaces_around_equality_operators = true
|
||||
ij_java_spaces_around_lambda_arrow = true
|
||||
ij_java_spaces_around_logical_operators = true
|
||||
ij_java_spaces_around_method_ref_dbl_colon = false
|
||||
ij_java_spaces_around_multiplicative_operators = true
|
||||
ij_java_spaces_around_relational_operators = true
|
||||
ij_java_spaces_around_shift_operators = true
|
||||
ij_java_spaces_around_type_bounds_in_type_parameters = true
|
||||
ij_java_spaces_around_unary_operator = false
|
||||
ij_java_spaces_within_angle_brackets = false
|
||||
ij_java_spaces_within_annotation_parentheses = false
|
||||
ij_java_spaces_within_array_initializer_braces = false
|
||||
ij_java_spaces_within_braces = false
|
||||
ij_java_spaces_within_brackets = false
|
||||
ij_java_spaces_within_cast_parentheses = false
|
||||
ij_java_spaces_within_catch_parentheses = false
|
||||
ij_java_spaces_within_for_parentheses = false
|
||||
ij_java_spaces_within_if_parentheses = false
|
||||
ij_java_spaces_within_method_call_parentheses = false
|
||||
ij_java_spaces_within_method_parentheses = false
|
||||
ij_java_spaces_within_parentheses = false
|
||||
ij_java_spaces_within_record_header = false
|
||||
ij_java_spaces_within_switch_parentheses = false
|
||||
ij_java_spaces_within_synchronized_parentheses = false
|
||||
ij_java_spaces_within_try_parentheses = false
|
||||
ij_java_spaces_within_while_parentheses = false
|
||||
ij_java_special_else_if_treatment = true
|
||||
ij_java_subclass_name_suffix = Impl
|
||||
ij_java_ternary_operation_signs_on_next_line = false
|
||||
ij_java_ternary_operation_wrap = off
|
||||
ij_java_test_name_suffix = Test
|
||||
ij_java_throws_keyword_wrap = off
|
||||
ij_java_throws_list_wrap = off
|
||||
ij_java_use_external_annotations = false
|
||||
ij_java_use_fq_class_names = false
|
||||
ij_java_use_relative_indents = false
|
||||
ij_java_use_single_class_imports = true
|
||||
ij_java_variable_annotation_wrap = off
|
||||
ij_java_visibility = public
|
||||
ij_java_while_brace_force = never
|
||||
ij_java_while_on_new_line = false
|
||||
ij_java_wrap_comments = false
|
||||
ij_java_wrap_first_method_in_call_chain = false
|
||||
ij_java_wrap_long_lines = false
|
||||
|
||||
[.editorconfig]
|
||||
ij_editorconfig_align_group_field_declarations = false
|
||||
ij_editorconfig_space_after_colon = false
|
||||
ij_editorconfig_space_after_comma = true
|
||||
ij_editorconfig_space_before_colon = false
|
||||
ij_editorconfig_space_before_comma = false
|
||||
ij_editorconfig_spaces_around_assignment_operators = true
|
||||
|
||||
[{*.ant,*.fxml,*.jhm,*.jnlp,*.jrxml,*.pom,*.qrc,*.rng,*.tld,*.wadl,*.wsdd,*.wsdl,*.xjb,*.xml,*.xsd,*.xsl,*.xslt,*.xul}]
|
||||
ij_xml_align_attributes = true
|
||||
ij_xml_align_text = false
|
||||
ij_xml_attribute_wrap = normal
|
||||
ij_xml_block_comment_at_first_column = true
|
||||
ij_xml_keep_blank_lines = 2
|
||||
ij_xml_keep_indents_on_empty_lines = false
|
||||
ij_xml_keep_line_breaks = true
|
||||
ij_xml_keep_line_breaks_in_text = true
|
||||
ij_xml_keep_whitespaces = false
|
||||
ij_xml_keep_whitespaces_around_cdata = preserve
|
||||
ij_xml_keep_whitespaces_inside_cdata = false
|
||||
ij_xml_line_comment_at_first_column = true
|
||||
ij_xml_space_after_tag_name = false
|
||||
ij_xml_space_around_equals_in_attribute = false
|
||||
ij_xml_space_inside_empty_tag = false
|
||||
ij_xml_text_wrap = normal
|
||||
ij_xml_use_custom_settings = false
|
||||
|
||||
[{*.ft,*.vm,*.vsl}]
|
||||
ij_vtl_keep_indents_on_empty_lines = false
|
||||
|
||||
[{*.gant,*.gradle,*.groovy,*.gson,*.gy}]
|
||||
ij_groovy_align_group_field_declarations = false
|
||||
ij_groovy_align_multiline_array_initializer_expression = false
|
||||
ij_groovy_align_multiline_assignment = false
|
||||
ij_groovy_align_multiline_binary_operation = false
|
||||
ij_groovy_align_multiline_chained_methods = false
|
||||
ij_groovy_align_multiline_extends_list = false
|
||||
ij_groovy_align_multiline_for = true
|
||||
ij_groovy_align_multiline_list_or_map = true
|
||||
ij_groovy_align_multiline_method_parentheses = false
|
||||
ij_groovy_align_multiline_parameters = true
|
||||
ij_groovy_align_multiline_parameters_in_calls = false
|
||||
ij_groovy_align_multiline_resources = true
|
||||
ij_groovy_align_multiline_ternary_operation = false
|
||||
ij_groovy_align_multiline_throws_list = false
|
||||
ij_groovy_align_named_args_in_map = true
|
||||
ij_groovy_align_throws_keyword = false
|
||||
ij_groovy_array_initializer_new_line_after_left_brace = false
|
||||
ij_groovy_array_initializer_right_brace_on_new_line = false
|
||||
ij_groovy_array_initializer_wrap = off
|
||||
ij_groovy_assert_statement_wrap = off
|
||||
ij_groovy_assignment_wrap = off
|
||||
ij_groovy_binary_operation_wrap = off
|
||||
ij_groovy_blank_lines_after_class_header = 0
|
||||
ij_groovy_blank_lines_after_imports = 1
|
||||
ij_groovy_blank_lines_after_package = 1
|
||||
ij_groovy_blank_lines_around_class = 1
|
||||
ij_groovy_blank_lines_around_field = 0
|
||||
ij_groovy_blank_lines_around_field_in_interface = 0
|
||||
ij_groovy_blank_lines_around_method = 1
|
||||
ij_groovy_blank_lines_around_method_in_interface = 1
|
||||
ij_groovy_blank_lines_before_imports = 1
|
||||
ij_groovy_blank_lines_before_method_body = 0
|
||||
ij_groovy_blank_lines_before_package = 0
|
||||
ij_groovy_block_brace_style = end_of_line
|
||||
ij_groovy_block_comment_at_first_column = true
|
||||
ij_groovy_call_parameters_new_line_after_left_paren = false
|
||||
ij_groovy_call_parameters_right_paren_on_new_line = false
|
||||
ij_groovy_call_parameters_wrap = off
|
||||
ij_groovy_catch_on_new_line = false
|
||||
ij_groovy_class_annotation_wrap = split_into_lines
|
||||
ij_groovy_class_brace_style = end_of_line
|
||||
ij_groovy_class_count_to_use_import_on_demand = 5
|
||||
ij_groovy_do_while_brace_force = never
|
||||
ij_groovy_else_on_new_line = false
|
||||
ij_groovy_enum_constants_wrap = off
|
||||
ij_groovy_extends_keyword_wrap = off
|
||||
ij_groovy_extends_list_wrap = off
|
||||
ij_groovy_field_annotation_wrap = split_into_lines
|
||||
ij_groovy_finally_on_new_line = false
|
||||
ij_groovy_for_brace_force = never
|
||||
ij_groovy_for_statement_new_line_after_left_paren = false
|
||||
ij_groovy_for_statement_right_paren_on_new_line = false
|
||||
ij_groovy_for_statement_wrap = off
|
||||
ij_groovy_if_brace_force = never
|
||||
ij_groovy_import_annotation_wrap = 2
|
||||
ij_groovy_imports_layout = *,|,javax.**,java.**,|,$*
|
||||
ij_groovy_indent_case_from_switch = true
|
||||
ij_groovy_indent_label_blocks = true
|
||||
ij_groovy_insert_inner_class_imports = false
|
||||
ij_groovy_keep_blank_lines_before_right_brace = 2
|
||||
ij_groovy_keep_blank_lines_in_code = 2
|
||||
ij_groovy_keep_blank_lines_in_declarations = 2
|
||||
ij_groovy_keep_control_statement_in_one_line = true
|
||||
ij_groovy_keep_first_column_comment = true
|
||||
ij_groovy_keep_indents_on_empty_lines = false
|
||||
ij_groovy_keep_line_breaks = true
|
||||
ij_groovy_keep_multiple_expressions_in_one_line = false
|
||||
ij_groovy_keep_simple_blocks_in_one_line = false
|
||||
ij_groovy_keep_simple_classes_in_one_line = true
|
||||
ij_groovy_keep_simple_lambdas_in_one_line = true
|
||||
ij_groovy_keep_simple_methods_in_one_line = true
|
||||
ij_groovy_label_indent_absolute = false
|
||||
ij_groovy_label_indent_size = 0
|
||||
ij_groovy_lambda_brace_style = end_of_line
|
||||
ij_groovy_layout_static_imports_separately = true
|
||||
ij_groovy_line_comment_add_space = false
|
||||
ij_groovy_line_comment_at_first_column = true
|
||||
ij_groovy_method_annotation_wrap = split_into_lines
|
||||
ij_groovy_method_brace_style = end_of_line
|
||||
ij_groovy_method_call_chain_wrap = off
|
||||
ij_groovy_method_parameters_new_line_after_left_paren = false
|
||||
ij_groovy_method_parameters_right_paren_on_new_line = false
|
||||
ij_groovy_method_parameters_wrap = off
|
||||
ij_groovy_modifier_list_wrap = false
|
||||
ij_groovy_names_count_to_use_import_on_demand = 3
|
||||
ij_groovy_parameter_annotation_wrap = off
|
||||
ij_groovy_parentheses_expression_new_line_after_left_paren = false
|
||||
ij_groovy_parentheses_expression_right_paren_on_new_line = false
|
||||
ij_groovy_prefer_parameters_wrap = false
|
||||
ij_groovy_resource_list_new_line_after_left_paren = false
|
||||
ij_groovy_resource_list_right_paren_on_new_line = false
|
||||
ij_groovy_resource_list_wrap = off
|
||||
ij_groovy_space_after_assert_separator = true
|
||||
ij_groovy_space_after_colon = true
|
||||
ij_groovy_space_after_comma = true
|
||||
ij_groovy_space_after_comma_in_type_arguments = true
|
||||
ij_groovy_space_after_for_semicolon = true
|
||||
ij_groovy_space_after_quest = true
|
||||
ij_groovy_space_after_type_cast = true
|
||||
ij_groovy_space_before_annotation_parameter_list = false
|
||||
ij_groovy_space_before_array_initializer_left_brace = false
|
||||
ij_groovy_space_before_assert_separator = false
|
||||
ij_groovy_space_before_catch_keyword = true
|
||||
ij_groovy_space_before_catch_left_brace = true
|
||||
ij_groovy_space_before_catch_parentheses = true
|
||||
ij_groovy_space_before_class_left_brace = true
|
||||
ij_groovy_space_before_closure_left_brace = true
|
||||
ij_groovy_space_before_colon = true
|
||||
ij_groovy_space_before_comma = false
|
||||
ij_groovy_space_before_do_left_brace = true
|
||||
ij_groovy_space_before_else_keyword = true
|
||||
ij_groovy_space_before_else_left_brace = true
|
||||
ij_groovy_space_before_finally_keyword = true
|
||||
ij_groovy_space_before_finally_left_brace = true
|
||||
ij_groovy_space_before_for_left_brace = true
|
||||
ij_groovy_space_before_for_parentheses = true
|
||||
ij_groovy_space_before_for_semicolon = false
|
||||
ij_groovy_space_before_if_left_brace = true
|
||||
ij_groovy_space_before_if_parentheses = true
|
||||
ij_groovy_space_before_method_call_parentheses = false
|
||||
ij_groovy_space_before_method_left_brace = true
|
||||
ij_groovy_space_before_method_parentheses = false
|
||||
ij_groovy_space_before_quest = true
|
||||
ij_groovy_space_before_switch_left_brace = true
|
||||
ij_groovy_space_before_switch_parentheses = true
|
||||
ij_groovy_space_before_synchronized_left_brace = true
|
||||
ij_groovy_space_before_synchronized_parentheses = true
|
||||
ij_groovy_space_before_try_left_brace = true
|
||||
ij_groovy_space_before_try_parentheses = true
|
||||
ij_groovy_space_before_while_keyword = true
|
||||
ij_groovy_space_before_while_left_brace = true
|
||||
ij_groovy_space_before_while_parentheses = true
|
||||
ij_groovy_space_in_named_argument = true
|
||||
ij_groovy_space_in_named_argument_before_colon = false
|
||||
ij_groovy_space_within_empty_array_initializer_braces = false
|
||||
ij_groovy_space_within_empty_method_call_parentheses = false
|
||||
ij_groovy_spaces_around_additive_operators = true
|
||||
ij_groovy_spaces_around_assignment_operators = true
|
||||
ij_groovy_spaces_around_bitwise_operators = true
|
||||
ij_groovy_spaces_around_equality_operators = true
|
||||
ij_groovy_spaces_around_lambda_arrow = true
|
||||
ij_groovy_spaces_around_logical_operators = true
|
||||
ij_groovy_spaces_around_multiplicative_operators = true
|
||||
ij_groovy_spaces_around_regex_operators = true
|
||||
ij_groovy_spaces_around_relational_operators = true
|
||||
ij_groovy_spaces_around_shift_operators = true
|
||||
ij_groovy_spaces_within_annotation_parentheses = false
|
||||
ij_groovy_spaces_within_array_initializer_braces = false
|
||||
ij_groovy_spaces_within_braces = true
|
||||
ij_groovy_spaces_within_brackets = false
|
||||
ij_groovy_spaces_within_cast_parentheses = false
|
||||
ij_groovy_spaces_within_catch_parentheses = false
|
||||
ij_groovy_spaces_within_for_parentheses = false
|
||||
ij_groovy_spaces_within_gstring_injection_braces = false
|
||||
ij_groovy_spaces_within_if_parentheses = false
|
||||
ij_groovy_spaces_within_list_or_map = false
|
||||
ij_groovy_spaces_within_method_call_parentheses = false
|
||||
ij_groovy_spaces_within_method_parentheses = false
|
||||
ij_groovy_spaces_within_parentheses = false
|
||||
ij_groovy_spaces_within_switch_parentheses = false
|
||||
ij_groovy_spaces_within_synchronized_parentheses = false
|
||||
ij_groovy_spaces_within_try_parentheses = false
|
||||
ij_groovy_spaces_within_tuple_expression = false
|
||||
ij_groovy_spaces_within_while_parentheses = false
|
||||
ij_groovy_special_else_if_treatment = true
|
||||
ij_groovy_ternary_operation_wrap = off
|
||||
ij_groovy_throws_keyword_wrap = off
|
||||
ij_groovy_throws_list_wrap = off
|
||||
ij_groovy_use_flying_geese_braces = false
|
||||
ij_groovy_use_fq_class_names = false
|
||||
ij_groovy_use_fq_class_names_in_javadoc = true
|
||||
ij_groovy_use_relative_indents = false
|
||||
ij_groovy_use_single_class_imports = true
|
||||
ij_groovy_variable_annotation_wrap = off
|
||||
ij_groovy_while_brace_force = never
|
||||
ij_groovy_while_on_new_line = false
|
||||
ij_groovy_wrap_long_lines = false
|
||||
|
||||
[{*.gradle.kts,*.kt,*.kts,*.main.kts}]
|
||||
ij_kotlin_align_in_columns_case_branch = false
|
||||
ij_kotlin_align_multiline_binary_operation = false
|
||||
ij_kotlin_align_multiline_extends_list = false
|
||||
ij_kotlin_align_multiline_method_parentheses = false
|
||||
ij_kotlin_align_multiline_parameters = true
|
||||
ij_kotlin_align_multiline_parameters_in_calls = false
|
||||
ij_kotlin_allow_trailing_comma = false
|
||||
ij_kotlin_allow_trailing_comma_on_call_site = false
|
||||
ij_kotlin_assignment_wrap = normal
|
||||
ij_kotlin_blank_lines_after_class_header = 0
|
||||
ij_kotlin_blank_lines_around_block_when_branches = 0
|
||||
ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1
|
||||
ij_kotlin_block_comment_at_first_column = true
|
||||
ij_kotlin_call_parameters_new_line_after_left_paren = true
|
||||
ij_kotlin_call_parameters_right_paren_on_new_line = true
|
||||
ij_kotlin_call_parameters_wrap = on_every_item
|
||||
ij_kotlin_catch_on_new_line = false
|
||||
ij_kotlin_class_annotation_wrap = split_into_lines
|
||||
ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
|
||||
ij_kotlin_continuation_indent_for_chained_calls = false
|
||||
ij_kotlin_continuation_indent_for_expression_bodies = false
|
||||
ij_kotlin_continuation_indent_in_argument_lists = false
|
||||
ij_kotlin_continuation_indent_in_elvis = false
|
||||
ij_kotlin_continuation_indent_in_if_conditions = false
|
||||
ij_kotlin_continuation_indent_in_parameter_lists = false
|
||||
ij_kotlin_continuation_indent_in_supertype_lists = false
|
||||
ij_kotlin_else_on_new_line = false
|
||||
ij_kotlin_enum_constants_wrap = off
|
||||
ij_kotlin_extends_list_wrap = normal
|
||||
ij_kotlin_field_annotation_wrap = split_into_lines
|
||||
ij_kotlin_finally_on_new_line = false
|
||||
ij_kotlin_if_rparen_on_new_line = true
|
||||
ij_kotlin_import_nested_classes = false
|
||||
ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^
|
||||
ij_kotlin_insert_whitespaces_in_simple_one_line_method = true
|
||||
ij_kotlin_keep_blank_lines_before_right_brace = 2
|
||||
ij_kotlin_keep_blank_lines_in_code = 2
|
||||
ij_kotlin_keep_blank_lines_in_declarations = 2
|
||||
ij_kotlin_keep_first_column_comment = true
|
||||
ij_kotlin_keep_indents_on_empty_lines = false
|
||||
ij_kotlin_keep_line_breaks = true
|
||||
ij_kotlin_lbrace_on_next_line = false
|
||||
ij_kotlin_line_comment_add_space = false
|
||||
ij_kotlin_line_comment_at_first_column = true
|
||||
ij_kotlin_method_annotation_wrap = split_into_lines
|
||||
ij_kotlin_method_call_chain_wrap = normal
|
||||
ij_kotlin_method_parameters_new_line_after_left_paren = true
|
||||
ij_kotlin_method_parameters_right_paren_on_new_line = true
|
||||
ij_kotlin_method_parameters_wrap = on_every_item
|
||||
ij_kotlin_name_count_to_use_star_import = 5
|
||||
ij_kotlin_name_count_to_use_star_import_for_members = 3
|
||||
ij_kotlin_packages_to_use_import_on_demand = java.util.*,kotlinx.android.synthetic.**,io.ktor.**
|
||||
ij_kotlin_parameter_annotation_wrap = off
|
||||
ij_kotlin_space_after_comma = true
|
||||
ij_kotlin_space_after_extend_colon = true
|
||||
ij_kotlin_space_after_type_colon = true
|
||||
ij_kotlin_space_before_catch_parentheses = true
|
||||
ij_kotlin_space_before_comma = false
|
||||
ij_kotlin_space_before_extend_colon = true
|
||||
ij_kotlin_space_before_for_parentheses = true
|
||||
ij_kotlin_space_before_if_parentheses = true
|
||||
ij_kotlin_space_before_lambda_arrow = true
|
||||
ij_kotlin_space_before_type_colon = false
|
||||
ij_kotlin_space_before_when_parentheses = true
|
||||
ij_kotlin_space_before_while_parentheses = true
|
||||
ij_kotlin_spaces_around_additive_operators = true
|
||||
ij_kotlin_spaces_around_assignment_operators = true
|
||||
ij_kotlin_spaces_around_equality_operators = true
|
||||
ij_kotlin_spaces_around_function_type_arrow = true
|
||||
ij_kotlin_spaces_around_logical_operators = true
|
||||
ij_kotlin_spaces_around_multiplicative_operators = true
|
||||
ij_kotlin_spaces_around_range = false
|
||||
ij_kotlin_spaces_around_relational_operators = true
|
||||
ij_kotlin_spaces_around_unary_operator = false
|
||||
ij_kotlin_spaces_around_when_arrow = true
|
||||
ij_kotlin_variable_annotation_wrap = off
|
||||
ij_kotlin_while_on_new_line = false
|
||||
ij_kotlin_wrap_elvis_expressions = 1
|
||||
ij_kotlin_wrap_expression_body_functions = 1
|
||||
ij_kotlin_wrap_first_method_in_call_chain = false
|
||||
|
||||
[{*.htm,*.html,*.ng,*.sht,*.shtm,*.shtml}]
|
||||
ij_html_add_new_line_before_tags = body,div,p,form,h1,h2,h3
|
||||
ij_html_align_attributes = true
|
||||
ij_html_align_text = false
|
||||
ij_html_attribute_wrap = normal
|
||||
ij_html_block_comment_at_first_column = true
|
||||
ij_html_do_not_align_children_of_min_lines = 0
|
||||
ij_html_do_not_break_if_inline_tags = title,h1,h2,h3,h4,h5,h6,p
|
||||
ij_html_do_not_indent_children_of_tags = html,body,thead,tbody,tfoot
|
||||
ij_html_enforce_quotes = false
|
||||
ij_html_inline_tags = a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var
|
||||
ij_html_keep_blank_lines = 2
|
||||
ij_html_keep_indents_on_empty_lines = false
|
||||
ij_html_keep_line_breaks = true
|
||||
ij_html_keep_line_breaks_in_text = true
|
||||
ij_html_keep_whitespaces = false
|
||||
ij_html_keep_whitespaces_inside = span,pre,textarea
|
||||
ij_html_line_comment_at_first_column = true
|
||||
ij_html_new_line_after_last_attribute = never
|
||||
ij_html_new_line_before_first_attribute = never
|
||||
ij_html_quote_style = double
|
||||
ij_html_remove_new_line_before_tags = br
|
||||
ij_html_space_after_tag_name = false
|
||||
ij_html_space_around_equality_in_attribute = false
|
||||
ij_html_space_inside_empty_tag = false
|
||||
ij_html_text_wrap = normal
|
||||
ij_html_uniform_ident = false
|
||||
|
||||
[{*.markdown,*.md}]
|
||||
ij_markdown_force_one_space_after_blockquote_symbol = true
|
||||
ij_markdown_force_one_space_after_header_symbol = true
|
||||
ij_markdown_force_one_space_after_list_bullet = true
|
||||
ij_markdown_force_one_space_between_words = true
|
||||
ij_markdown_keep_indents_on_empty_lines = false
|
||||
ij_markdown_max_lines_around_block_elements = 1
|
||||
ij_markdown_max_lines_around_header = 1
|
||||
ij_markdown_max_lines_between_paragraphs = 1
|
||||
ij_markdown_min_lines_around_block_elements = 1
|
||||
ij_markdown_min_lines_around_header = 1
|
||||
ij_markdown_min_lines_between_paragraphs = 1
|
||||
|
||||
[{*.properties,spring.handlers,spring.schemas}]
|
||||
ij_properties_align_group_field_declarations = false
|
||||
ij_properties_keep_blank_lines = false
|
||||
ij_properties_key_value_delimiter = equals
|
||||
ij_properties_spaces_around_key_value_delimiter = false
|
||||
|
||||
[{*.yaml,*.yml}]
|
||||
indent_size = 2
|
||||
ij_yaml_keep_indents_on_empty_lines = false
|
||||
ij_yaml_keep_line_breaks = true
|
||||
ij_yaml_space_before_colon = true
|
||||
ij_yaml_spaces_within_braces = true
|
||||
ij_yaml_spaces_within_brackets = true
|
||||
```
|
||||
Java is not formatted with Google Java Format. Continue to follow the repository IntelliJ/EditorConfig style for Java changes.
|
||||
@@ -0,0 +1,24 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gradle"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
commit-message:
|
||||
prefix: "chore(deps)"
|
||||
ignore:
|
||||
- dependency-name: "io.netty:netty-all"
|
||||
# Kotest 6 requires JDK/JVM 11+, while legacy integration-test servers still
|
||||
# load the test plugin on Java 8; keep Kotest 5.x updates allowed until the
|
||||
# harness is split or migrated.
|
||||
- dependency-name: "io.kotest:kotest-runner-junit5-jvm"
|
||||
versions: ["[6,)"]
|
||||
- dependency-name: "io.kotest:kotest-assertions-core-jvm"
|
||||
versions: ["[6,)"]
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
commit-message:
|
||||
prefix: "chore(deps)"
|
||||
@@ -0,0 +1 @@
|
||||
{".":"2.5.0"}
|
||||
@@ -0,0 +1,91 @@
|
||||
name: Build and Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
# This permission is still required by the new action
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'adopt'
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
with:
|
||||
gradle-version: wrapper
|
||||
|
||||
- name: Run Spotless Check
|
||||
run: |
|
||||
./gradlew spotlessCheck
|
||||
|
||||
# Build Step - No changes needed here
|
||||
- name: Run Gradle Build
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" == "release" ]; then
|
||||
VERSION=${{ github.event.release.tag_name }}
|
||||
VERSION=${VERSION#v} # Strip 'v' if present
|
||||
./gradlew clean build -Pversion=$VERSION
|
||||
else
|
||||
./gradlew clean build
|
||||
fi
|
||||
|
||||
- name: Upload Artifact to GitHub Release
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
# The 'files' input takes a path to the asset(s) you want to upload.
|
||||
# Keep a stable filename for external download links.
|
||||
files: ./build/libs/OldCombatMechanics.jar
|
||||
|
||||
- name: Read game versions from gradle.properties
|
||||
run: |
|
||||
RAW=$(grep ^gameVersions gradle.properties | cut -d'=' -f2-)
|
||||
# Convert "1.21, 1.20.6" -> "1:1.21,1:1.20.6" to select Bukkit-compatible typeId 1 entries via the minecraft endpoint
|
||||
GAME_VERSIONS=$(echo "$RAW" | tr -d ' ' | awk -F',' '{
|
||||
out="";
|
||||
for (i=1; i<=NF; i++) {
|
||||
v=$i;
|
||||
if (v!= "") {
|
||||
if (out!= "") out=out ",";
|
||||
out=out "1:" v;
|
||||
}
|
||||
}
|
||||
print out;
|
||||
}')
|
||||
echo "GAME_VERSIONS=$GAME_VERSIONS" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Upload to CurseForge (Bukkit)
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
uses: itsmeow/curseforge-upload@v3
|
||||
with:
|
||||
token: ${{ secrets.DBO_UPLOAD_API_TOKEN }}
|
||||
project_id: '98233'
|
||||
game_endpoint: 'minecraft'
|
||||
file_path: './build/libs/OldCombatMechanics.jar'
|
||||
changelog: ${{ github.event.release.body }}
|
||||
changelog_type: 'markdown'
|
||||
release_type: 'release'
|
||||
display_name: 'OldCombatMechanics ${{ github.event.release.tag_name }}'
|
||||
game_versions: ${{ env.GAME_VERSIONS }}
|
||||
|
||||
|
||||
- name: Publish to Hangar
|
||||
env:
|
||||
HANGAR_API_TOKEN: ${{ secrets.HANGAR_API_TOKEN }}
|
||||
HANGAR_CHANGELOG: ${{ github.event.release.body }}
|
||||
run: ./gradlew build publishPluginPublicationToHangar --stacktrace
|
||||
@@ -0,0 +1,58 @@
|
||||
name: Dev builds
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
- 'ingametesting'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'adopt'
|
||||
cache: gradle
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v6
|
||||
with:
|
||||
gradle-version: wrapper
|
||||
|
||||
- name: Determine if this is a release version
|
||||
run: |
|
||||
IS_RELEASE=$(./gradlew -q printIsRelease)
|
||||
echo "Is release? $IS_RELEASE"
|
||||
echo "IS_RELEASE=$IS_RELEASE" >> $GITHUB_ENV
|
||||
|
||||
- name: Run Spotless Check
|
||||
run: |
|
||||
./gradlew spotlessCheck
|
||||
|
||||
- name: Run Gradle Build
|
||||
run: |
|
||||
./gradlew clean build
|
||||
|
||||
- name: Archive jar file
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: OldCombatMechanics
|
||||
path: build/libs/OldCombatMechanics.jar
|
||||
|
||||
- name: Publish Snapshot to Hangar
|
||||
if: ${{ github.event_name == 'push' && github.event.pull_request == null && env.IS_RELEASE != 'true' }}
|
||||
env:
|
||||
HANGAR_API_TOKEN: ${{ secrets.HANGAR_API_TOKEN }}
|
||||
run: |
|
||||
./gradlew publishPluginPublicationToHangar
|
||||
@@ -0,0 +1,24 @@
|
||||
name: Release Please
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: release-please
|
||||
uses: googleapis/release-please-action@v5
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
|
||||
config-file: .github/release-please-config.json
|
||||
manifest-file: .github/release-please-manifest.json
|
||||
@@ -0,0 +1,63 @@
|
||||
name: Wrap issue form code blocks
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
wrap:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
// Avoid loops: when this workflow updates the issue, it triggers "edited" again.
|
||||
if (context.actor === "github-actions[bot]") return;
|
||||
|
||||
const issue = context.payload.issue;
|
||||
if (!issue || !issue.body) return;
|
||||
|
||||
// On edits, bail unless the body actually changed
|
||||
if (context.payload.action === "edited") {
|
||||
const changed = context.payload.changes && context.payload.changes.body;
|
||||
if (!changed) return;
|
||||
}
|
||||
|
||||
function escapeRegExp(s) {
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function wrapFirstCodeBlockAfterHeading(body, headingText, summaryText) {
|
||||
const re = new RegExp(
|
||||
`(^###\\s+${escapeRegExp(headingText)}\\s*\\n+)(\`\`\`[\\s\\S]*?\\n\`\`\`)(?=\\n|$)`,
|
||||
"m"
|
||||
);
|
||||
|
||||
return body.replace(re, (_m, heading, codeblock) => {
|
||||
return (
|
||||
`${heading}` +
|
||||
`<details>\n` +
|
||||
`<summary>${summaryText}</summary>\n\n` +
|
||||
`${codeblock}\n\n` +
|
||||
`</details>`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
let body = issue.body;
|
||||
|
||||
body = wrapFirstCodeBlockAfterHeading(body, "Server Log File", "Server log");
|
||||
body = wrapFirstCodeBlockAfterHeading(body, "OldCombatMechanics config.yml", "config.yml");
|
||||
|
||||
if (body === issue.body) return;
|
||||
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
body
|
||||
});
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
repos:
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: spotless-check
|
||||
name: Spotless check
|
||||
entry: ./gradlew spotlessCheck
|
||||
language: system
|
||||
pass_filenames: false
|
||||
require_serial: true
|
||||
@@ -2,257 +2,98 @@
|
||||
|
||||
**Prime directive:** If you are reading this file, it is your responsibility as an agent to keep it up to date with any changes you make in this repository.
|
||||
|
||||
This file captures repo-specific context discovered while working on this branch.
|
||||
This file holds always-on repository guidance and routing hints. Detailed workflows and historical notes live in repo-local agent skills under `.agents/skills/`.
|
||||
|
||||
## Always-on rules
|
||||
|
||||
- Use British English spelling and phraseology at all times.
|
||||
- Do **not** use American English spelling or phraseology.
|
||||
- Use conventional commit format.
|
||||
- Keep commits subject-only unless the user explicitly asks for a body.
|
||||
- Release Please renders commit subjects into changelog entries, so subjects must be release-note-friendly and comprehensible without the type prefix or body.
|
||||
- Prefer scoped subjects where useful, such as `feat(config): add default modesets for unlisted worlds`.
|
||||
- For issue, ticket, or bug-report driven work, include the ticket number in the commit subject, such as `fix(modesets): correct stale stored world modesets (#865)`.
|
||||
- Avoid vague subjects such as `feat: support ...`, `feat: migrate ...`, or `test: cover ...` when a clearer user-facing summary is possible.
|
||||
- Root and user-facing agents must not open or read `build/integration-test-logs/*.log` directly. Log inspection is allowed only by subagents, and only when needed for integration-test triage or when explicitly requested. Prefer compact console summaries and `plugins/OldCombatMechanicsTest/test-failures.txt` first.
|
||||
- Never hard-code absolute filesystem paths in tests or production code; resolve locations relative to the repo root, plugin data folder, or server run directory.
|
||||
- Prefer feature detection over hard-coded Minecraft version gates because server implementations may backport APIs.
|
||||
- For NMS/reflection access, prefer the project `utilities.reflection.Reflector` helpers over ad-hoc reflection where practical.
|
||||
- Reflection should be a compatibility fallback, not the first choice on hot paths.
|
||||
- For optional methods on already-present Bukkit types, especially in hot paths, prefer direct calls with cached `NoSuchMethodError` or narrow linkage fallbacks over reflective invocation. Use reflection when classes, parameter or return types, signatures, NMS shapes, or cross-version linkage would make direct calls unsafe.
|
||||
- Do not gate plugin behaviour on assumptions from one Paper/Spigot version without checking compatible API presence.
|
||||
- Agents must not disable, skip, weaken, or version-gate tests as a way to make validation pass. Fix failing tests at the root cause, or report the failure as a blocker unless the user explicitly approves a coverage reduction.
|
||||
|
||||
## Repo overview
|
||||
- Project: OldCombatMechanics (Bukkit/Paper plugin)
|
||||
- Branch context: working from `kotlin-tests` branch
|
||||
- Build tool: Gradle (wrapper currently at 9.2.1)
|
||||
- JDKs used locally: 8, 11, 17, 25
|
||||
|
||||
## Integration test harness (Kotlin)
|
||||
- Integration tests live in `src/integrationTest/kotlin` and are packaged into `OldCombatMechanics-<version>-tests.jar`.
|
||||
- Entrypoint plugin class: `kernitus.plugin.OldCombatMechanics.OCMTestMain`.
|
||||
- Tests run inside a real Paper server started by the Gradle `run-paper` plugin.
|
||||
- RunServer output is redirected to `build/integration-test-logs/<version>.log`; `checkTestResults<version>` prints only a compact summary/failures to the console.
|
||||
- `KotestRunner` writes a compact `plugins/OldCombatMechanicsTest/test-failures.txt` file (up to 25 failures) so CI can surface failure reasons without opening the full server log.
|
||||
- Token hygiene: do **not** open/read `build/integration-test-logs/*.log` unless the user explicitly asks for log inspection; rely on the compact console summary by default and ask for permission before digging into full logs.
|
||||
- PacketEvents is shaded into the plugin; integration tests do not inject external packet libraries.
|
||||
- `relocateIntegrationTestClasses` (ShadowJar) relocates PacketEvents references in test classes only.
|
||||
- `integrationTestJar` is a plain `Jar` task that embeds the relocated test classes plus runtime deps, excluding PacketEvents so the test plugin resolves PacketEvents from the main plugin’s shaded copy.
|
||||
- `PacketCancellationIntegrationTest` now uses a cancellable `CompletableFuture.await()` so `withTimeout` actually aborts when no packet arrives.
|
||||
- `PacketCancellationIntegrationTest` now drives PacketEvents via `PacketEventsImplHelper.handleClientBoundPacket` with a synthetic `User` instead of relying on live channel injection; this avoids timeouts when PacketEvents does not emit send events for fake channels.
|
||||
- `PacketCancellationIntegrationTest` sets the Bukkit player on the synthetic PacketEvents event so module listeners can match `PacketSendEvent#getPlayer`.
|
||||
- Matrix task: `integrationTest` depends on `integrationTestMatrix` which runs per-version tasks like:
|
||||
- `integrationTest1_19_2`, `integrationTest1_21_11`, `integrationTest1_12`, `integrationTest1_9`
|
||||
- Test result handoff:
|
||||
- `OCMTestMain` writes `plugins/OldCombatMechanicsTest/test-results.txt` containing `PASS` or `FAIL`.
|
||||
- `KotestRunner` also writes `plugins/OldCombatMechanicsTest/test-failures.txt` (small failure summary).
|
||||
- Gradle `checkTestResults<version>` fails build if file missing, or content is not `PASS`.
|
||||
- Project: OldCombatMechanics, a Bukkit/Paper plugin.
|
||||
- Branch context: working from `kotlin-tests`.
|
||||
- Build tool: Gradle wrapper, currently 9.2.1.
|
||||
- JDKs used locally: 8, 11, 17, 25.
|
||||
- Main integration tests live in `src/integrationTest/kotlin` and are packaged into `OldCombatMechanics-<version>-tests.jar`.
|
||||
- Entrypoint test plugin class: `kernitus.plugin.OldCombatMechanics.OCMTestMain`.
|
||||
- PacketEvents is shaded into the main plugin; integration tests resolve PacketEvents from that shaded copy rather than injecting a separate external packet library.
|
||||
|
||||
## Version matrix + Java selection
|
||||
- Config is in `build.gradle.kts`:
|
||||
- `integrationTestVersions` from property or defaults.
|
||||
- `requiredJavaVersion` selects Java based on version.
|
||||
- Pre-1.13 versions use `integrationTestJavaVersionLegacyPre13` (default 8).
|
||||
- Modern versions use Java 25 if `>=1.20.5` else Java 17.
|
||||
- Legacy vanilla jar cache:
|
||||
- `downloadVanilla<version>` task downloads Mojang server jars for <=1.12 and writes `run/<version>/cache/mojang_<version>.jar`.
|
||||
## Build and validation quick reference
|
||||
|
||||
## Kotlin test runner split (Java 11+ vs Java 8)
|
||||
- Kotest 6 (Java 11+) is used for 1.19.2 and 1.21.11.
|
||||
- Kotest runner class: `kernitus.plugin.OldCombatMechanics.KotestRunner`
|
||||
- Project config: `KotestProjectConfig`
|
||||
- Java 8 uses a separate runner:
|
||||
- `kernitus.plugin.OldCombatMechanics.LegacyTestRunner`
|
||||
- Currently a **smoke test** only (verifies plugin enabled + `WeaponDamages` loaded).
|
||||
- This is a placeholder and **not a full integration test**.
|
||||
- Full integration matrix: `./gradlew integrationTest`.
|
||||
- Selected versions: `./gradlew integrationTest -PintegrationTestVersions=1.19.2,1.21.11,1.12`.
|
||||
- Matrix task examples: `integrationTest1_19_2`, `integrationTest1_21_11`, `integrationTest1_12`, `integrationTest1_9`.
|
||||
- Java toolchain override example: `ORG_GRADLE_JAVA_INSTALLATIONS_PATHS=/path/to/jdk8:/path/to/jdk17:/path/to/jdk25 ./gradlew integrationTest`.
|
||||
- `checkTestResults<version>` fails the build if `plugins/OldCombatMechanicsTest/test-results.txt` is missing or not `PASS`.
|
||||
- `KotestRunner` writes a compact `plugins/OldCombatMechanicsTest/test-failures.txt` summary for CI-friendly failure triage.
|
||||
|
||||
## Fake player implementation notes
|
||||
- Primary fake player implementation is `src/integrationTest/kotlin/kernitus/plugin/OldCombatMechanics/FakePlayer.kt`.
|
||||
- It relies on `xyz.jpenilla:reflection-remapper` to map modern NMS names.
|
||||
- On legacy servers (1.12), reflection remapper mappings are unavailable; code falls back to `ReflectionRemapper.noop()`.
|
||||
- As-is, `FakePlayer` uses modern NMS class names (`net.minecraft.server.MinecraftServer`, etc.).
|
||||
- This fails on 1.12 which uses versioned NMS (`net.minecraft.server.v1_12_R1.*`).
|
||||
- 1.12 requires a dedicated fake player path or version-aware class mapping.
|
||||
## Skill selection guide
|
||||
|
||||
## Java 8 compatibility work
|
||||
- Java 8 compatibility backports already done in main code (records/pattern matching removed).
|
||||
- Java 8-incompatible APIs replaced:
|
||||
- `Stream.toList()` -> `collect(Collectors.toList())`
|
||||
- `Set.of`/`List.of` -> `Collections.unmodifiableSet(new HashSet<>(Arrays.asList(...)))` etc.
|
||||
- Added missing import in `PotionTypeCompat` for `PotionData`.
|
||||
- Build config sets `options.release.set(8)` for Java, Kotlin `jvmTarget = 1.8`.
|
||||
- **Mandatory trigger:** Use `dependabot-pr-review` for Dependabot PRs, dependency bumps, Gradle/Maven dependency updates, GitHub Actions version updates, dependency changelog/licence/release-note review, and dependency validation recommendations. Other skills remain relevant for deeper integration-test selection (`integration-test-verification`), compatibility strategy (`compatibility-strategy`), and release-readiness concerns (`release-readiness-review`).
|
||||
- **Mandatory trigger:** Use `github-issue-investigation` for GitHub issue triage, `gh issue` investigation, picking a fresh issue, reproducing issue reports, or identifying durable regression coverage. Do not use it for GitHub-side mutation or implementation once the investigation is complete.
|
||||
- **Mandatory trigger:** Use `integration-test-verification` for integration-test runs, matrix selection, compact failure triage, Kotlin integration test authoring, or `KotestRunner` registration questions. Do not use it for ordinary unit-free source edits with no test-harness impact.
|
||||
- **Mandatory trigger:** Use `compatibility-strategy` for Java 8 compatibility, Bukkit/Paper version differences, NMS/reflection, fake players, PacketEvents compatibility, feature detection, or legacy-server fallbacks. Do not use it for release notes or configuration-only routing.
|
||||
- **Mandatory trigger:** Use `module-config-change` for `config.yml`, module enablement, modesets, migrations, configurable module assignment, or per-module settings such as tool damage, attack cooldown, attack range, or sword blocking options. Do not use it for pure test execution or release publishing.
|
||||
- **Mandatory trigger:** Use `commit-preparation` for preparing commits, pre-commit hooks, Spotless, formatting or linting, validation before commit, staging files, and conventional commit messages. Do not use it for implementation design or PR prose unless commit readiness is also in scope.
|
||||
- **Mandatory trigger:** Use `release-readiness-review` for GitHub release, Hangar, CurseForge/BukkitDev, licence, workflow, supported-version, or release asset checks. Do not use it for feature implementation unless the change affects release packaging.
|
||||
- **Mandatory trigger:** Use `pr-draft-summary` when drafting PR descriptions, change summaries, reviewer notes, or risk/test sections. Do not use it while still deciding implementation strategy.
|
||||
|
||||
## Dependency / build updates
|
||||
- Kotlin: 2.3.0
|
||||
- Kotest: 6.0.7
|
||||
- run-paper plugin: 3.0.2
|
||||
- Hangar publish plugin: 0.1.4
|
||||
- Other deps updated (bstats, netty, BSON, XSeries, authlib, reflection-remapper, adventure).
|
||||
- PacketEvents is now used for packet interception (shaded and relocated in the main jar).
|
||||
- PacketEvents dependency moved to `2.11.2-SNAPSHOT` (CodeMC snapshots) for 1.21.11 support.
|
||||
- JSR-305 added for `javax.annotation.Nullable` (compileOnly).
|
||||
## Current project state
|
||||
|
||||
## Current failing area
|
||||
- Paper 1.12 integration tests **do not run real fake player tests yet**.
|
||||
- Desired fix: add proper 1.12 fake player implementation (e.g., version-specific NMS path like `v1_12_R1`).
|
||||
- Example 1.12 fake player implementation provided by user (NMS, PlayerList manipulation, packet send, etc.).
|
||||
- We should integrate a conditional path in `FakePlayer` for 1.12 using versioned NMS classes or an alternate helper.
|
||||
- Paper 1.12 integration tests do not yet run the full fake-player test suite; legacy fake-player work needs a dedicated or version-aware NMS path.
|
||||
- Java 8 compatibility is required for main code. Avoid APIs such as records, pattern matching, `Stream.toList()`, and `Set.of`/`List.of` in Java 8-targeted code.
|
||||
- Main Java/Kotlin compilation targets Java 8 (`options.release.set(8)`, Kotlin `jvmTarget = 1.8`).
|
||||
- Pre-1.13 integration-test versions use `integrationTestJavaVersionLegacyPre13` (default 8); modern versions use Java 25 for `>=1.20.5` and Java 17 otherwise.
|
||||
- Legacy vanilla jars for `<=1.12` are downloaded by `downloadVanilla<version>` into `run/<version>/cache/mojang_<version>.jar`.
|
||||
|
||||
## Local test commands
|
||||
- Run full matrix:
|
||||
- `./gradlew integrationTest`
|
||||
- Change matrix:
|
||||
- `./gradlew integrationTest -PintegrationTestVersions=1.19.2,1.21.11,1.12`
|
||||
- Set Java toolchain paths (example):
|
||||
- `ORG_GRADLE_JAVA_INSTALLATIONS_PATHS=/path/to/jdk8:/path/to/jdk17:/path/to/jdk25 ./gradlew integrationTest`
|
||||
## Core implementation constraints
|
||||
|
||||
## Notes
|
||||
- Removed the dead reflection utility `ClassType` and the unused `Reflector#getClass(ClassType, String)` overload; `Reflector#getClass(String)` remains the supported class-resolution helper.
|
||||
- `SpigotFunctionChooser` now only falls back for compatibility-style failures (LinkageError family, missing-method/class reflection failures, and explicit compatibility-signalled `UnsupportedOperationException` via `compat`/`compatibility` markers), and rethrows ordinary runtime logic failures instead of silently selecting fallback (for example generic "incompatible" wording does not trigger fallback).
|
||||
- `AttackCompat` now only treats Bukkit `Player#attack` as success when it yields an observable living-target hit (health/lastDamage/noDamageTicks signal); otherwise it falls back to NMS attack candidates.
|
||||
- `AttackCompat` now treats boolean-return NMS attack methods that return `false` as failed attempts and continues trying other candidates, with expanded failure diagnostics including false-result and exception counts.
|
||||
- Entity-click dedupe in `ModuleSwordBlocking` now uses a taskless lazy-prune timestamp map (`System.nanoTime`) instead of a one-tick scheduled set clear, with a short dedupe window and periodic/size-triggered expiry pruning.
|
||||
- `ModuleSwordBlocking` now handles `PlayerInteractEntityEvent` and `PlayerInteractAtEntityEvent` for main-hand sword blocking, with short time-window dedupe to prevent duplicate side effects when both events fire for the same click.
|
||||
- Removed the unused main-source Java tester package (`kernitus.plugin.OldCombatMechanics.tester`), deleted stale commented test-command code from `OCMCommandHandler`, and dropped the now-obsolete Gradle source-set exclusion for that package.
|
||||
- Paper 1.12 sometimes fails to download legacy vanilla jar from old Mojang endpoint. The custom `downloadVanilla` task fixes that by using the v2 manifest.
|
||||
- 1.21.11 servers log hostname warnings and Unsafe warnings; tests still pass.
|
||||
- 1.9 integration tests are currently on hold per user request.
|
||||
- Kotest filters (`kotest.filter.specs`, `kotest.filter.tests`) are now passed through Gradle into the run-paper JVM args for integration tests.
|
||||
- Reflection should be used only as a fallback (performance cost); prefer direct API/code paths when available.
|
||||
- The Hangar publish workflow exports `HANGAR_API_TOKEN` to match the Gradle Hangar publish configuration.
|
||||
- `ModuleFishingRodVelocity` uses a single shared per-tick task (1.14+) to adjust hook gravity for all active hooks, instead of one scheduled task per hook.
|
||||
- `AttackCooldownTracker#getLastCooldown` is safe to call when the tracker is not registered (returns null) and uses a `HashMap` rather than a `WeakHashMap`.
|
||||
- `AttackCooldownTracker` defensively feature-detects `HumanEntity#getAttackCooldown` and avoids scheduling its per-tick sampler when the API exists (modern/backported servers).
|
||||
- `ModulePlayerKnockback` uses a `HashMap` + a single shared 1-tick expiry cleaner for pending velocity overrides, instead of `WeakHashMap` + one scheduled task per hit.
|
||||
- `ModuleShieldDamageReduction` uses a `HashMap` + a single shared 1-tick expiry cleaner for the “fully blocked” armour-durability suppression, instead of `WeakHashMap` + one scheduled task per hit.
|
||||
- `ModuleOldArmourDurability` uses a `HashMap` + a single shared 1-tick expiry cleaner for the “explosion damaged armour” suppression, instead of `WeakHashMap` + one scheduled task per explosion.
|
||||
- `ModulePlayerRegen` uses tick-based interval tracking (1.8-like; TPS aware) with a `HashMap` and a single shared tick counter task that runs only while players are tracked.
|
||||
- `ModuleDisableEnderpearlCooldown` uses a `HashMap` and lazily drops expired cooldown entries during checks (wall-clock cooldown; no recurring task).
|
||||
- `ModuleSwordBlocking` no longer version-gates Paper support; it feature-detects Paper data component APIs and avoids ConcurrentModificationException by iterating legacy tick state over a snapshot.
|
||||
- Do not gate behaviour on hard-coded Minecraft version numbers; use feature detection (class/method presence) because some servers backport APIs.
|
||||
- Weapon/armour unknown-enchantment warnings only fire for non-`minecraft` namespaces; legacy servers fall back to a known vanilla-enchantment list to avoid warning on built-ins.
|
||||
- For NMS access, prefer the project Reflector helpers (`utilities.reflection.Reflector`) over ad-hoc reflection, and avoid hard-coded versioned class names where heuristics (signatures/fields) can locate methods safely.
|
||||
- Added integration tests in `OldPotionEffectsIntegrationTest` for strength addend scaling (Strength II and III), a distinct modifier value check, and strength multiplier scaling.
|
||||
- Added integration test ensuring vanilla strength addend applies when `old-potion-effects` is disabled.
|
||||
- Strength modifier in `OCMEntityDamageByEntityEvent` now stores per-level value (3) and applies level when reconstructing base damage.
|
||||
- Added `OldToolDamageMobIntegrationTest` to assert old-tool-damage config affects vindicator iron-axe hits.
|
||||
- `KotestRunner` class list updated to include `OldToolDamageMobIntegrationTest`.
|
||||
- `ModuleOldToolDamage` now adjusts mob weapon damage by shifting base damage with the configured-vs-vanilla delta for non-player damagers.
|
||||
- `ModuleOldToolDamage` documents that mob custom weapons are not detected; the delta is always applied for mobs and may conflict with other plugins.
|
||||
- Added `WeaponDurabilityIntegrationTest` covering tool durability vs hit counts during invulnerability and after it expires (FakePlayer attacker vs FakePlayer victim); registered in `KotestRunner`.
|
||||
- `WeaponDurabilityIntegrationTest` writes debug summaries to `build/weapon-durability-debug-<version>.txt`.
|
||||
- `WeaponDurabilityIntegrationTest` and `OldToolDamageMobIntegrationTest` now resolve debug output paths relative to the repo root (based on the server run directory), avoiding hard-coded home paths.
|
||||
- `OldToolDamageMobIntegrationTest` uses a Villager victim and waits for a real Vindicator hit by setting a target and retrying with tick delays (plus a best-effort NMS attackCompat call) before asserting the mob tool-damage delta.
|
||||
- Module assignment is strict for configurable modules: every non-internal module must appear in exactly one of `always_enabled_modules`, `disabled_modules`, or a modeset. Internal modules (`modeset-listener`, `attack-cooldown-tracker`, `entity-damage-listener`) are always enabled and must not be listed; reload/enable fails if they are configured.
|
||||
- Use British English spelling and phraseology at all times.
|
||||
- DO NOT use American English spelling or phraseology under any circumstances.
|
||||
- Never hard-code absolute filesystem paths in tests or production code; resolve locations relative to the repo root or server run directory.
|
||||
- Added `DisableOffhandIntegrationTest` to assert the disable-offhand modeset-change handler does not clear the offhand when the module is not enabled for the player.
|
||||
- `KotestRunner` now includes `DisableOffhandIntegrationTest` in its explicit class list.
|
||||
- Module assignment is strict for configurable modules at top-level category scope: every non-internal module must appear in at most one of `always_enabled_modules`, `disabled_modules`, or the aggregate `modesets` category. A module may appear in more than one individual modeset because modesets are alternative player modes.
|
||||
- Internal modules (`modeset-listener`, `attack-cooldown-tracker`, `entity-damage-listener`) are always enabled and must not be listed in configurable module groups.
|
||||
- Reload/enable must fail for invalid module assignment rather than silently choosing a fallback.
|
||||
- Modules are enabled/disabled solely via `always_enabled_modules`, `disabled_modules`, and `modesets`; there is no per-module `enabled:` toggle.
|
||||
- When adding new integration test specs, add them to the explicit `.withClasses(...)` list in `KotestRunner` because autoscan is disabled.
|
||||
- Added `ToolDamageTooltipIntegrationTest` (in `KotestRunner` list) to define behaviour for an opt-in “configured tool damage tooltip” feature (lore line) under `old-tool-damage.tooltip` (`enabled`, `prefix`).
|
||||
- `old-tool-damage.tooltip.enabled` is now enabled by default in the bundled config so players can see the configured damage in-game.
|
||||
- Modules are enabled/disabled solely via `always_enabled_modules`, `disabled_modules`, and `modesets` (no per-module `enabled:` toggle).
|
||||
- `SwordBlockingIntegrationTest` uses synthetic `PlayerInteractEvent` right-clicks; on legacy (offhand-shield) path this cannot reliably assert `isBlocking`/`isHandRaised` (client-driven), so tests treat “blocking applied” as either a shield injection or a raised hand depending on path.
|
||||
- Added `PacketCancellationIntegrationTest` to cover PacketEvents sweep-particle and attack-sound cancellation using PacketEvents wrappers/listeners (registered in `KotestRunner`).
|
||||
- Added `ModesetRulesIntegrationTest` to cover always-enabled, disabled, and modeset-scoped module rules plus reload failures for invalid assignments.
|
||||
- Added `ConfigMigrationIntegrationTest` to cover config upgrade migration into always/disabled module lists and preservation of custom modesets.
|
||||
- Added a `weakness should not store negative last damage values` test in `InvulnerabilityDamageIntegrationTest` that forces `old-potion-effects.weakness.modifier = -10`, consumes a weakness potion (amplifier -1), attacks once, and asserts the stored last damage is non-negative; passes on 1.12/1.19.2 after clamping, but 1.21.11 can still fail with `No stored last damage for victim (events=0)` (attack event not recorded).
|
||||
- `EntityDamageByEntityListener.checkOverdamage` now clamps the stored `lastDamages` value to a minimum of 0 to avoid negative last-damage entries when weakness (or other modifiers) drives pre-clamp damage below zero.
|
||||
- Weakness amplifier clamping changed in 1.20+: attempts to use amplifier `-1` are clamped to `0` (Weakness I). With low-damage weapons this can yield zero vanilla damage, and Paper 1.21 does not fire `EntityDamageByEntityEvent` for zero damage, so tests that rely on an EDBE hit must use a stronger weapon or account for the clamp.
|
||||
- `OldPotionEffectsIntegrationTest` now disables the module by moving `old-potion-effects` into `disabled_modules` (and removing it from modesets/always lists), then uses `Config.reload()`; the `withConfig` helper restores module lists/modesets and saves+reloads config to keep state consistent.
|
||||
- `FireAspectOverdamageIntegrationTest` includes afterburn-vs-environmental fire-tick checks for both player and zombie victims, with and without Protection IV armour (mirrors issue 707 MRE).
|
||||
- Release workflow (`.github/workflows/build-upload-release.yml`) now uploads Bukkit files via `itsmeow/curseforge-upload@v3` against the `minecraft` endpoint, reusing `DBO_UPLOAD_API_TOKEN`, and prefixes `GAME_VERSIONS` as `1:<ver>` so CurseForge selects the Bukkit-compatible type-1 version entries.
|
||||
- New classes should be written in Kotlin by default where practical, matching `.github/CONTRIBUTING.md`.
|
||||
|
||||
## Test harness shortcuts (known non-realistic paths)
|
||||
- Several integration tests manually construct and fire Bukkit events rather than triggering real in-world actions:
|
||||
- `GoldenAppleIntegrationTest` (manual `PlayerItemConsumeEvent` and `PrepareItemCraftEvent`)
|
||||
- `OldPotionEffectsIntegrationTest` (manual `PlayerItemConsumeEvent`, `PlayerInteractEvent`, `BlockDispenseEvent`)
|
||||
- `OldArmourDurabilityIntegrationTest` (manual `PlayerItemDamageEvent`, `EntityDamageEvent`)
|
||||
- `PlayerKnockbackIntegrationTest` (manual `EntityDamageByEntityEvent`, `PlayerVelocityEvent`)
|
||||
- `SwordBlockingIntegrationTest` (manual `PlayerInteractEvent`)
|
||||
- `SwordSweepIntegrationTest` (manual `EntityDamageByEntityEvent`)
|
||||
- Some tests directly invoke module handlers instead of going through the event bus:
|
||||
- `PlayerKnockbackIntegrationTest` (direct `module.onEntityDamageEntity`)
|
||||
- `SwordSweepIntegrationTest` (direct `module.onEntityDamaged`)
|
||||
- `AttributeModifierCompat` synthesises a fallback attack-damage modifier from `NewWeaponDamage` when API attributes are missing.
|
||||
- Fake player implementations use simulated login/network plumbing (EmbeddedChannel + manual login/join/quit events), not a real networked client.
|
||||
- FakePlayer uses a plain `EmbeddedChannel` (not an anonymous subclass) so PacketEvents treats it as fake and does not disallow login; dummy `decoder`/`encoder` handlers are still added to the pipeline.
|
||||
- FakePlayer now schedules a manual NMS tick for non-legacy servers (prefers `doTick`, then `tick`, falls back to `baseTick`) to drive vanilla ticking like fire and passive effects.
|
||||
- FakePlayer tick shim invokes `baseTick` whenever `remainingFireTicks > 0` (burning) to ensure fire tick damage events still occur on Paper 1.21+ (which can short-circuit `doTick`/`tick` for fake players). When the fake player is in water, it prefers `doTick`/`tick` so the server can properly clear fire ticks (extinguish) before any fire damage is applied.
|
||||
- FakePlayer now prefers `PlayerList.placeNewPlayer` over the legacy `load`/manual list insertion path to better mirror vanilla login initialisation (helps player fire-tick damage on modern servers).
|
||||
- FakePlayer does not emulate fire-tick damage; fire ticks should be driven by the NMS tick path.
|
||||
- FakePlayer now forces a world add when the Bukkit world does not report the fake player entity after `placeNewPlayer` to keep PvP interactions reliable.
|
||||
- FakePlayer now clears invulnerability/instabuild abilities after spawn (plus a legacy fallback) to improve PvP interactions between fake players.
|
||||
- EntityDamageByEntityListener now logs extra debug about lastDamage restoration for non-entity damage, and documents the vanilla 1.12 damage flow in checkOverdamage.
|
||||
- EntityDamageByEntityListener no longer overwrites the stored last-damage baseline when cancelling “fake overdamage” (e.g. cancelled fire tick during invulnerability), preventing subsequent hits from incorrectly bypassing immunity.
|
||||
- Stored last-damage baselines now use a single lightweight expiry sweeper (tick-based TTL) instead of scheduling one Bukkit task per damage event; this keeps the hot path allocation-free. Expiry is monotonic (only extended, never shortened) and has a small minimum TTL to tolerate `maximumNoDamageTicks = 0`.
|
||||
- `WeaponDurabilityIntegrationTest` now uses a Zombie victim (fake-player attacker) and prefers the Bukkit `Player#attack` API before falling back to reflective NMS attack resolution, to make hit delivery reliable on modern servers.
|
||||
- `ModuleSwordSweepParticles` and `ModuleAttackSounds` now use PacketEvents listeners/wrappers instead of ProtocolLib.
|
||||
- `PacketCancellationIntegrationTest` now builds a `PacketSendEvent` directly (reflection) from the transformed buffer, sets the packet id/type explicitly, and dispatches it via the PacketEvents `eventManager` so module listeners can cancel reliably.
|
||||
- README now includes a licence note: source remains MPL‑2.0, but pre-built jars bundling PacketEvents are distributed under GPLv3; builds without PacketEvents can remain MPL‑2.0.
|
||||
- Legacy fake player (1.9) now uses a plain `EmbeddedChannel` with dummy `decoder`/`encoder` handlers, mirroring the modern fake player setup so PacketEvents treats it as fake.
|
||||
- `ModuleChorusFruit` now reimplements the chorus teleport search (16 attempts, world-border aware, passable feet/head, solid ground) for custom teleport distances; falls back to vanilla target if no safe spot found.
|
||||
- Added `ChorusFruitIntegrationTest` (in KotestRunner list) to assert custom chorus teleport distance lands on a safe block within the configured radius.
|
||||
- Chorus fruit safety test now handles legacy 1.12 by using solid/non-solid checks when `Block#isPassable` is absent; passes on 1.12, 1.19.2, and 1.21.11.
|
||||
- `ModuleOldToolDamage` now supports configurable TRIDENT (melee), TRIDENT_THROWN, and MACE damage; mace preserves its vanilla fall bonus while overriding base damage. New defaults added to `old-tool-damage.damages`.
|
||||
- Added `ModuleAttackRange` (Paper 1.21.11+ only) to apply a configurable attack_range data component (default 1.8-like: 0–3 range, creative 0–4, margin 0.1, mob factor 1.0); auto-disables on Spigot/older versions. `attack-range` module listed in `disabled_modules` by default.
|
||||
- `ModuleSwordBlocking` now only strips the Paper `CONSUMABLE` component from sword items, preventing food and other consumables from inheriting a `!minecraft:consumable` patch when inventory events fire on 1.20.5+.
|
||||
- Added `DisableOffhandReflectionIntegrationTest` (in `KotestRunner` list) to ensure reflective access to `InventoryView#getBottomInventory`/`getTopInventory` works on non-public CraftBukkit view implementations.
|
||||
- `Reflector.getMethod` overloads now include declared methods and call `setAccessible(true)` to avoid `IllegalAccessException` when CraftBukkit uses non-public view classes (e.g. `CraftContainer$1` on 1.20.1).
|
||||
- Added `AttackRangeIntegrationTest` (1.21.11+) to assert vanilla hits at ~3.6 blocks and 1.8-style attack_range reduces reach so the same hit misses; registered in `KotestRunner`.
|
||||
- Removed the cancelled S3-only `AttackRangeIntegrationTest` case `swap hand keeps attack-range off offhand sword (Paper 1.21.11+)` so ongoing S6 validation is not blocked by a parked slice artefact.
|
||||
- InvulnerabilityDamageIntegrationTest adds a case asserting environmental damage above the baseline applies during invulnerability (manual EntityDamageEvent).
|
||||
- `gradle.properties` gameVersions list now includes 1.21.11 down to 1.21.1 (plus 1.21) ahead of existing entries.
|
||||
- GitHub release asset now keeps a stable filename `OldCombatMechanics.jar` (no version suffix); the CurseForge upload uses the same path.
|
||||
- Disabled the plain Gradle `jar` task; `assemble` now produces only the shaded `shadowJar` output `build/libs/OldCombatMechanics.jar`, avoiding an unshaded duplicate `OldCombatMechanics-<version>.jar`.
|
||||
- Expanded `SwordBlockingIntegrationTest` to cover right-click blocking, non-sword handling, offhand restoration (hotbar change and drop cancel), permission gating, and preserving an existing real shield.
|
||||
- Added `PaperSwordBlockingDamageReductionIntegrationTest` (in KotestRunner list) to regression-test that Paper sword blocking is recognised server-side (via `ModuleSwordBlocking.isPaperSwordBlocking`) and produces a non-zero 1.8-style reduction from `applyPaperBlockingReduction`. This uses a synthetic damage event to avoid flakiness from mob AI / PvP settings.
|
||||
- Paper-only sword blocking uses a runtime-gated helper (`kernitus.plugin.OldCombatMechanics.paper.PaperSwordBlocking`) that applies consumable + blocking components via cached reflection (lookup once, MethodHandle invoke thereafter). 1.8-style reduction is applied via `EntityDamageByEntityListener` using the `BLOCKING` damage modifier, while legacy/non-Paper keeps the shield swap path.
|
||||
- Fixed legacy (1.9.x) sweep detection flakiness by tracking priming per-attacker UUID (not `Location`, which is unstable due to yaw/pitch) and clearing the set on module reload; stabilises `SwordSweepIntegrationTest` on 1.9.4.
|
||||
- `ModuleSwordSweep` now uses a one-shot next-tick clear (single scheduled task guarded by `pendingClearTask`) instead of a repeating every-tick task, to avoid doing any work on ticks where no sword hits occurred.
|
||||
- `ModuleSwordBlocking` legacy (offhand shield) path no longer schedules per-player repeating tasks; it now uses a single shared tick runner with per-player tick deadlines (10-tick warmup + 2-tick poll + restoreDelay), which reduces task churn under heavy right-click spam.
|
||||
- `ModuleLoader` now clears the static module list on initialise to prevent duplicate registrations after hot reloads.
|
||||
- Added `ConsumableComponentIntegrationTest` coverage to assert the Paper sword-blocking consumable cleaner leaves swords untouched when the module is disabled (modeset/world) and does not mutate swords when no component change is required.
|
||||
- `ConsumableComponentIntegrationTest` now seeds and asserts the CONSUMABLE component via NMS reflection (not Paper API) and uses standalone CraftItemStacks for cursor/current items to avoid classloader mismatches and fake-player cursor side effects.
|
||||
- `ModuleSwordBlocking#onModesetChange` now strips the Paper CONSUMABLE component from the player’s main hand/offhand and stored swords when sword-blocking is disabled for that player, preventing component “taint” lingering after mode changes.
|
||||
- `ModuleSwordBlocking#reload` now strips Paper sword-blocking consumable components from online players when the module is disabled globally (disabled_modules) to avoid lingering taint after config reloads.
|
||||
- `ModuleSwordBlocking` now gates the Paper consumable animation by PacketEvents client version (>=1.20.5); older clients fall back to the offhand shield and unknown versions default to the animation path.
|
||||
- `config.yml` now documents that Paper 1.20.5+ uses the consumable-based sword-blocking animation, with older/Paperless servers falling back to an offhand shield.
|
||||
- `config.yml` now notes that ViaVersion clients older than 1.20.5 also fall back to the shield behaviour.
|
||||
- Added `ConsumableComponentIntegrationTest` coverage for disabling sword-blocking via `disabled_modules` and asserting the consumable component is cleared after config reload.
|
||||
- Extended `ConsumableComponentIntegrationTest` to cover disabled-module right-click suppression, reload toggling, stored-inventory cleanup, offhand stability, and modeset-change behaviour after a disabled reload.
|
||||
- Added `ConsumableComponentIntegrationTest` coverage for forcing an older client version and asserting sword-blocking falls back to an offhand shield without applying consumable components.
|
||||
- `ConsumableComponentIntegrationTest` now uses PacketEvents reflection to seed a User/client version for fake players when PacketEvents has not registered one yet.
|
||||
- `ModuleSwordBlocking#restore` no longer skips offhand restoration just because Paper support is present; older clients using the shield fallback now restore their original offhand item correctly.
|
||||
- Added `ConsumableComponentIntegrationTest` coverage asserting the older-client shield fallback restores the offhand item on hotbar change.
|
||||
- Added `ConsumableComponentIntegrationTest` coverage for inventory-glitch regressions: unknown PacketEvents client versions should use shield fallback, middle-clicking custom GUIs should not mutate held sword components, and custom-GUI drag events should not rewrite top-inventory swords.
|
||||
- Those inventory-glitch regressions now pass on 1.21.11 after the unknown-client fallback + click/drag scope fixes.
|
||||
- `ModuleSwordBlocking#supportsPaperAnimation` now treats unknown PacketEvents client versions as shield-fallback only when a PacketEvents `User` exists; if no user is registered yet (early login/synthetic test player), it keeps animation support to avoid regressing normal modern-client behaviour.
|
||||
- `ModuleSwordBlocking.ConsumableCleaner#onInventoryClickPost` now re-applies the main-hand consumable component only for click paths that can actually affect the selected hotbar slot (selected NUMBER_KEY swaps or direct selected-slot player-inventory clicks), and ignores middle-clicks.
|
||||
- `ModuleSwordBlocking.ConsumableCleaner#onInventoryDrag` now ignores top-inventory raw slots and skips main-hand re-application when the drag only touched top inventory slots, preventing custom-GUI slot rewrites.
|
||||
- Added `ConsumableComponentIntegrationTest` coverage for legacy fallback shield-guard scope: custom GUI shield-icon clicks and unrelated shield drops should not be cancelled while fallback is active; temporary offhand shield swap blocking should remain enforced.
|
||||
- `ModuleSwordBlocking#onInventoryClick` legacy shield-guard scope now only cancels direct offhand temporary-shield interactions (player inventory slot 40), avoiding cancellation of unrelated custom-GUI shield clicks.
|
||||
- `ModuleSwordBlocking#onInventoryClick` also blocks `ClickType.SWAP_OFFHAND` while temporary legacy shield state is active, preventing offhand shield extraction via inventory swap-clicks.
|
||||
- `ModuleSwordBlocking#onItemDrop` no longer cancels shield drops while legacy fallback state is active; it now force-restores the temporary shield state immediately to avoid trapping unrelated shield drops.
|
||||
- `ModuleSwordBlocking#isPlayerBlocking` now requires an actual offhand shield before treating `isBlocking`/`isHandRaised` as legacy shield-blocking, preventing stale hand-use state from suppressing fallback shield injection.
|
||||
- `ModuleSwordBlocking#supportsPaperAnimation` now falls back to `User#getClientVersion` when `PlayerManager#getClientVersion` is null, improving old-client fallback stability in synthetic/integration scenarios.
|
||||
- `ModuleSwordBlocking#supportsPaperAnimation` now fails safe to legacy shield fallback when PacketEvents client-version resolution is unavailable (resolver initialisation missing, resolver methods/objects null, or reflection errors), while preserving existing behaviour for normal early-login/synthetic-player cases.
|
||||
- `ModuleSwordBlocking#onPlayerSwapHandItems` now treats stale legacy stored-shield state as non-authoritative for Paper-animation players: it clears stale legacy entries instead of cancelling swaps, so synthetic swap listeners still run.
|
||||
- `ModuleSwordBlocking#onPlayerSwapHandItems` now restores any stale legacy stored offhand item (via `restore(..., true)`) before clearing legacy state on the Paper-animation path, so stale entries are not silently discarded.
|
||||
- `ConsumableCleaner#onSwap` now snapshots the held hotbar slot and only reapplies the Paper consumable component when the slot is unchanged on the deferred tick; offhand stripping remains deferred as before.
|
||||
- The new legacy-scope regressions now pass on 1.19.2 and 1.21.11.
|
||||
- `ModuleSwordBlocking.ConsumableCleaner#onInventoryClickPost` now snapshots click context (held slot and open inventory top/bottom) and skips next-tick reapply when that context is stale, preventing deferred reapply from tainting a newly selected main-hand sword.
|
||||
- Added `ConsumableComponentIntegrationTest` coverage for stale `PlayerSwapHandItemsEvent` deferred reapply: if held slot changes before next tick, the newly selected main-hand sword must not gain a consumable component, while swapped-offhand sword cleanup still occurs.
|
||||
- Added `ConsumableComponentIntegrationTest` regression coverage for stale `PlayerSwapHandItemsEvent` deferred reapply when the open inventory view changes before next tick: the new main-hand context must not be tainted, while swapped-offhand consumable cleanup must still run.
|
||||
- `ModuleSwordBlocking.ConsumableCleaner#onSwap` now snapshots open-inventory top/bottom identity at swap time and requires a view match only for deferred main-hand reapply; deferred offhand consumable cleanup still runs even when the view changed.
|
||||
- `ModuleSwordBlocking.ConsumableCleaner` click/drag handlers now follow a minimal-mutation policy (no proactive consumable strip/apply in `InventoryClickEvent` or `InventoryDragEvent` paths); cleanup is handled opportunistically via lifecycle/transition handlers (`onModesetChange`/`reload`/world-change, held-slot, and swap paths) using a shared per-player consumable sweep helper.
|
||||
- `ModuleSwordBlocking#onModesetChange` now force-restores stale legacy fallback shield state (`restore(..., true)`) when sword-blocking becomes disabled for a player, before sweeping Paper consumable components.
|
||||
- `ModuleSwordBlocking.ConsumableCleaner#onWorldChange` now strips consumable components from main hand, offhand, and stored swords without re-applying, so world changes clear stale consumable state consistently.
|
||||
- `ModuleSwordBlocking.ConsumableCleaner#onQuit` now uses the same strip-only full sweep as world-change cleanup (main hand, offhand, and stored swords), ensuring logout clears stale consumable state from storage as well.
|
||||
- `ModuleSwordBlocking` now handles `PlayerJoinEvent` with a force legacy restore (`restore(..., true)`) followed by strip-only consumable cleanup (main hand, offhand, and stored swords), so join clears stale state without re-applying consumable components.
|
||||
- `ModuleSwordBlocking` inner listener `ConsumableCleaner` was renamed to `ConsumableLifecycleHandler` (registration updated; behaviour unchanged).
|
||||
- Added `sword-blocking.paper-animation` config (default `true`) to hard-disable the Paper consumable animation path; when false, sword-blocking always uses the legacy shield fallback and reload/lifecycle cleanup strips stale consumable state without re-applying it.
|
||||
- Legacy sword-blocking now marks injected temporary offhand shields when marker APIs are available so death handling can identify the temporary drop path reliably.
|
||||
- `ModuleSwordBlocking#onPlayerDeath` now pops stored offhand exactly once, clears legacy state deterministically, respects `keepInventory` without rewriting drops, rewrites at most one temporary shield drop, and adds the stored offhand drop when no temporary shield drop is found.
|
||||
- Legacy shield-drop reconciliation now uses a strict safety-first fallback when marker APIs are unavailable: it does not guess temporary shields from plain shield shape, avoiding accidental replacement of legitimate shields (stored offhand is appended instead).
|
||||
- For synthetic/manual death events where drop metadata may not preserve the temporary-shield marker, `ModuleSwordBlocking#onPlayerDeath` allows a single shield-drop rewrite only when the player offhand was verified as marker-tagged at death time.
|
||||
- `ModuleAttackCooldown` now supports `disable-attack-cooldown.held-item-attack-speeds.<MATERIAL>` overrides with XMaterial/Material matching, falls back to `generic-attack-speed`, reconciles on join/world/modeset/hotbar/swap, restores vanilla base ATTACK_SPEED 4.0 when disabled, and no longer calls `player.saveData()` on attack-speed updates. The bundled config defaults keep most items on the no-cooldown fallback, with `TRIDENT`, `MACE`, and spear variants listed as the built-in slower exceptions; keys for newer-version materials are ignored safely on older servers.
|
||||
- `ModuleAttackCooldown` hotbar/swap handling is immediate best-effort only: at `EventPriority.HIGHEST`, it re-reads the live inventory only when the event is already cancelled, otherwise it trusts the event payload and applies straight away; later plugin mutations may win until the next ordinary reconcile trigger.
|
||||
- `.github/CONTRIBUTING.md` now states that contributions should include automated coverage within the existing test framework where practical, and that new classes should be written in Kotlin by default.
|
||||
## Integration test essentials
|
||||
|
||||
## Fire aspect / fire tick test notes
|
||||
- `FireAspectOverdamageIntegrationTest` now uses a Zombie victim for real fire tick sampling, with max health boosted (via MAX_HEALTH attribute) to survive rapid clicking.
|
||||
- The first two tests fire a synthetic `EntityDamageEvent` with `FIRE_TICK` to control timing and make the baseline check deterministic.
|
||||
- Paper 1.12 applies attack-cooldown scaling before the Bukkit damage event fires; fake players can start with a low initial cooldown, producing a weak first hit and allowing a stronger second hit as legitimate “overdamage”. `FireAspectOverdamageIntegrationTest` now waits a few ticks before the first attack to make the first hit stable on 1.12.
|
||||
- Added extra fire edge-case coverage in `FireAspectOverdamageIntegrationTest`: fire resistance + fire-immune victim rapid-click parity, water extinguish behaviour (no fire tick damage while submerged), fire protection vs protection comparisons (uses a Zombie victim for consistency), and alternating attackers during invulnerability.
|
||||
- Legacy (1.12) fake player behaviour differs for player-specific fire tick sampling, so the player afterburn-vs-environmental comparisons are no-ops on legacy; the Zombie variants still run across all versions.
|
||||
- Tests run inside a real Paper server started by the Gradle `run-paper` plugin.
|
||||
- RunServer output is redirected to `build/integration-test-logs/<version>.log`; root and user-facing agents must leave any needed log inspection to subagents after compact summaries and `plugins/OldCombatMechanicsTest/test-failures.txt` prove insufficient.
|
||||
- Kotlin tests use Kotest 6 for Java 11+ server targets (`KotestRunner`, `KotestProjectConfig`).
|
||||
- Java 8 targets use `LegacyTestRunner`, currently a smoke-test path rather than the full Kotest suite.
|
||||
- Several integration tests intentionally use synthetic Bukkit events or direct module handler calls. Consult `integration-test-verification` before assuming a test represents a real in-world action.
|
||||
|
||||
## Historical notes index
|
||||
|
||||
- Detailed relocated history from the former long `AGENTS.md` lives in `.agents/skills/integration-test-verification/references/relocated-agents-notes.md`.
|
||||
- Dependabot, dependency bump, changelog, licence, JVM/classfile, GitHub Actions, and validation recommendation reviews: `dependabot-pr-review`.
|
||||
- GitHub issue triage, `gh issue` investigation, fresh issue shortlisting, reproduction recommendations, and regression coverage notes: `github-issue-investigation`.
|
||||
- Integration harness, fake-player, PacketEvents, and test-shortcut details: `integration-test-verification` and its references.
|
||||
- Compatibility, Java 8, reflection, NMS, and version strategy details: `compatibility-strategy`.
|
||||
- Module configuration, modeset, migration, and option-change details: `module-config-change`.
|
||||
- Commit preparation, pre-commit hooks, Spotless, staging, validation-before-commit, and conventional commit details: `commit-preparation`.
|
||||
- Publishing, release workflow, licence, and asset notes: `release-readiness-review`.
|
||||
- PR summary templates and reviewer-facing change grouping: `pr-draft-summary`.
|
||||
|
||||
## TDAID reminders (this repo)
|
||||
|
||||
- Plan → Red → Green → Refactor → Validate.
|
||||
- Red phase: only touch tests. Do not modify production code.
|
||||
- Green phase: only touch production code. Do not modify tests.
|
||||
- Refactor phase: cleanups only; keep behavior unchanged and tests green.
|
||||
- Validate phase: rerun tests and do a human sanity check before declaring done.
|
||||
- Red phase: only touch tests.
|
||||
- Green phase: only touch production code.
|
||||
- Refactor phase: clean-ups only; keep behaviour unchanged and tests green.
|
||||
- Validate phase: rerun the narrowest relevant tests and do a human sanity check before declaring done.
|
||||
@@ -0,0 +1,517 @@
|
||||
# OldCombatMechanics API
|
||||
|
||||
The OldCombatMechanics API exposes both configured modesets and temporary per-player module overrides. Prefer modesets for arena and minigame combat modes, such as offering separate 1.8-style and modern PvP experiences. Modesets are named configured bundles, selected per player and world, and changes made through `setModesetForPlayer` use the normal player-data persistence.
|
||||
|
||||
Module overrides are different: they are temporary API-only per-player exceptions for individual modules, intended for narrow runtime cases where a plugin needs to force one module on or off for one currently online player. Overrides are online-session-only, in-memory state. They are cleared when the player quits, when OldCombatMechanics is disabled, or when a plugin explicitly clears them through the API. Module overrides are not a replacement for configured modesets.
|
||||
|
||||
| Use case | Modesets | Module overrides |
|
||||
| --- | --- | --- |
|
||||
| Arena/minigame combat style, such as 1.8-style versus modern PvP | Preferred: configure named bundles and select them per player/world | Not intended for this |
|
||||
| Persistence | Uses normal player-data persistence when changed through `setModesetForPlayer` | Online-session-only, in-memory |
|
||||
| Scope | Whole configured bundle of module choices | Individual module exception for one player |
|
||||
| Configuration source | Server configuration and allowed world modesets | API-only runtime state |
|
||||
|
||||
Overrides are shared runtime state. If multiple plugins set an override for the same player and module, the last write wins. Any plugin that clears that override clears the same shared state for that player and module.
|
||||
|
||||
The API also exposes configured modesets, world-allowed modesets, the player's stored modeset for their current world, modeset changes, and Bukkit events for module override and modeset changes.
|
||||
|
||||
---
|
||||
|
||||
## Dependency setup
|
||||
|
||||
There is currently no separate slim API artefact. Compile against the same OldCombatMechanics plugin jar that you deploy or test with, and mark it as compile-only/provided so your plugin does not shade or bundle OCM.
|
||||
|
||||
Gradle Kotlin DSL with a local jar:
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
compileOnly(files("libs/OldCombatMechanics.jar"))
|
||||
compileOnly("org.spigotmc:spigot-api:1.21.11-R0.1-SNAPSHOT")
|
||||
}
|
||||
```
|
||||
|
||||
Maven with a local or system-provided jar:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>kernitus.plugin</groupId>
|
||||
<artifactId>OldCombatMechanics</artifactId>
|
||||
<version>your-ocm-version</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
If you use a local Maven repository or an internal repository manager, publish the OCM jar there and keep the dependency `provided`/`compileOnly`.
|
||||
|
||||
Do not use OCM internal packages. Public API classes live under:
|
||||
|
||||
```java
|
||||
import kernitus.plugin.OldCombatMechanics.api.OldCombatMechanicsAPI;
|
||||
import kernitus.plugin.OldCombatMechanics.api.PlayerModesetChangeEvent;
|
||||
import kernitus.plugin.OldCombatMechanics.api.PlayerModuleOverride;
|
||||
import kernitus.plugin.OldCombatMechanics.api.PlayerModuleOverrideChangeEvent;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `plugin.yml` load order
|
||||
|
||||
Use `depend` when your plugin cannot enable or function without OCM:
|
||||
|
||||
```yaml
|
||||
depend: [OldCombatMechanics]
|
||||
```
|
||||
|
||||
Use `softdepend` when OCM integration is optional and your plugin can run without it:
|
||||
|
||||
```yaml
|
||||
softdepend: [OldCombatMechanics]
|
||||
```
|
||||
|
||||
With `softdepend`, always treat the API service as optional and disable only your OCM-specific integration if the service is absent.
|
||||
|
||||
---
|
||||
|
||||
## Getting the API instance
|
||||
|
||||
Always null-check the Bukkit service registration before using the API. The service is only available while OldCombatMechanics is loaded and enabled.
|
||||
|
||||
```java
|
||||
RegisteredServiceProvider<OldCombatMechanicsAPI> registration = Bukkit.getServicesManager()
|
||||
.getRegistration(OldCombatMechanicsAPI.class);
|
||||
if (registration == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
OldCombatMechanicsAPI api = registration.getProvider();
|
||||
```
|
||||
|
||||
Kotlin callers should use the same service lookup and null-check the registration:
|
||||
|
||||
```kotlin
|
||||
val api = Bukkit.getServicesManager()
|
||||
.getRegistration(OldCombatMechanicsAPI::class.java)
|
||||
?.provider
|
||||
?: return
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Threading
|
||||
|
||||
Call API methods from the Bukkit main server thread. The current contract does not guarantee async-safe access to Bukkit `Player` state, module state-change notifications, or future implementation details unless a future method explicitly documents different threading behaviour.
|
||||
|
||||
---
|
||||
|
||||
## Module names
|
||||
|
||||
All methods that accept a `moduleName` parameter expect a value from the set returned by [`getModuleNames()`](#getmodulenames). Only configurable modules are valid. Internal modules, such as listener/helper modules, cannot be overridden and are not returned by `getModuleNames()`.
|
||||
|
||||
Passing an unknown or non-configurable module name throws `IllegalArgumentException`.
|
||||
|
||||
---
|
||||
|
||||
## Effective module precedence
|
||||
|
||||
When OldCombatMechanics determines whether a configurable module is enabled for a player, it resolves state in this order:
|
||||
|
||||
1. per-player `FORCE_ENABLED` or `FORCE_DISABLED` override;
|
||||
2. configured disabled modules;
|
||||
3. configured always-enabled modules;
|
||||
4. player modeset and world configuration.
|
||||
|
||||
`PlayerModuleOverride.DEFAULT` means no per-player override is set, so the configured rules above continue to decide the module state.
|
||||
|
||||
---
|
||||
|
||||
## Methods
|
||||
|
||||
### `getModesetNames`
|
||||
|
||||
Returns all configured modeset names in config iteration order.
|
||||
```java
|
||||
Set<String> getModesetNames()
|
||||
```
|
||||
```java
|
||||
for (String modeset : api.getModesetNames()) {
|
||||
player.sendMessage(modeset);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `getAllowedModesets`
|
||||
|
||||
Returns the modesets allowed in a world as an unmodifiable defensive copy.
|
||||
```java
|
||||
Set<String> getAllowedModesets(World world)
|
||||
```
|
||||
|
||||
Iteration order is meaningful: it follows the configured world list when present, the `worlds.__default__` list for worlds without their own list, or the configured modeset order when no world list applies.
|
||||
|
||||
There is no public API for a default or fallback modeset. If you need to present a default choice, use your own policy, such as the player's stored modeset when present or the first entry from `getAllowedModesets(world)`.
|
||||
|
||||
```java
|
||||
Set<String> allowed = api.getAllowedModesets(player.getWorld());
|
||||
for (String modeset : allowed) {
|
||||
player.sendMessage("Allowed here: " + modeset);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `getModesetForPlayer`
|
||||
|
||||
Returns the stored modeset for the player in their current world, or `null` when no player-specific modeset has been stored yet.
|
||||
```java
|
||||
String getModesetForPlayer(Player player)
|
||||
```
|
||||
```java
|
||||
String modeset = api.getModesetForPlayer(player);
|
||||
if (modeset == null) {
|
||||
player.sendMessage("No modeset selected for this world.");
|
||||
} else {
|
||||
player.sendMessage("Current modeset: " + modeset);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `setModesetForPlayer`
|
||||
|
||||
Stores the modeset for the player in their current world and fires `PlayerModesetChangeEvent` with reason `API` when the stored value changes.
|
||||
```java
|
||||
void setModesetForPlayer(Player player, String modesetName)
|
||||
```
|
||||
|
||||
The input name is normalised before validation and storage. Unknown modesets, or modesets not allowed in the player's current world, throw `IllegalArgumentException`.
|
||||
|
||||
Modeset changes made through the API follow normal player data persistence semantics: OCM stores the selected modeset in player data and schedules the usual save. They are not session-only overrides.
|
||||
|
||||
```java
|
||||
if (api.getAllowedModesets(player.getWorld()).contains("arena")) {
|
||||
api.setModesetForPlayer(player, "arena");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `forceEnableModuleForPlayer`
|
||||
|
||||
Forces a configurable module on for an online player until the override is cleared, the player quits, or OldCombatMechanics is disabled.
|
||||
```java
|
||||
void forceEnableModuleForPlayer(Player player, String moduleName)
|
||||
```
|
||||
|
||||
**Throws:** `IllegalArgumentException` if `moduleName` is unknown or non-configurable.
|
||||
```java
|
||||
api.forceEnableModuleForPlayer(player, "attack-frequency");
|
||||
```
|
||||
|
||||
Kotlin:
|
||||
```kotlin
|
||||
api.forceEnableModuleForPlayer(player, "attack-frequency")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `forceDisableModuleForPlayer`
|
||||
|
||||
Forces a configurable module off for an online player until the override is cleared, the player quits, or OldCombatMechanics is disabled.
|
||||
```java
|
||||
void forceDisableModuleForPlayer(Player player, String moduleName)
|
||||
```
|
||||
|
||||
**Throws:** `IllegalArgumentException` if `moduleName` is unknown or non-configurable.
|
||||
```java
|
||||
api.forceDisableModuleForPlayer(player, "sword-blocking");
|
||||
```
|
||||
|
||||
Kotlin:
|
||||
```kotlin
|
||||
api.forceDisableModuleForPlayer(player, "sword-blocking")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `clearModuleOverrideForPlayer`
|
||||
|
||||
Clears the override for a single configurable module, reverting the player to configured module behaviour. No-op if no override is set.
|
||||
```java
|
||||
void clearModuleOverrideForPlayer(Player player, String moduleName)
|
||||
```
|
||||
|
||||
**Throws:** `IllegalArgumentException` if `moduleName` is unknown or non-configurable.
|
||||
```java
|
||||
api.clearModuleOverrideForPlayer(player, "attack-frequency");
|
||||
```
|
||||
|
||||
Kotlin:
|
||||
```kotlin
|
||||
api.clearModuleOverrideForPlayer(player, "attack-frequency")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `clearAllModuleOverridesForPlayer`
|
||||
|
||||
Clears all module overrides for a player at once, reverting them to configured module behaviour for every module. No-op if no overrides are set.
|
||||
```java
|
||||
void clearAllModuleOverridesForPlayer(Player player)
|
||||
```
|
||||
```java
|
||||
api.clearAllModuleOverridesForPlayer(player);
|
||||
```
|
||||
|
||||
Kotlin:
|
||||
```kotlin
|
||||
api.clearAllModuleOverridesForPlayer(player)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `getModuleOverrideForPlayer`
|
||||
|
||||
Returns the current override state for a single configurable module for a player. Returns `PlayerModuleOverride.DEFAULT` if no override is set.
|
||||
```java
|
||||
PlayerModuleOverride getModuleOverrideForPlayer(Player player, String moduleName)
|
||||
```
|
||||
|
||||
**Throws:** `IllegalArgumentException` if `moduleName` is unknown or non-configurable.
|
||||
```java
|
||||
PlayerModuleOverride override = api.getModuleOverrideForPlayer(player, "sword-blocking");
|
||||
if (override == PlayerModuleOverride.FORCE_ENABLED) {
|
||||
player.sendMessage("Forced on");
|
||||
} else if (override == PlayerModuleOverride.FORCE_DISABLED) {
|
||||
player.sendMessage("Forced off");
|
||||
} else {
|
||||
player.sendMessage("Following configured rules");
|
||||
}
|
||||
```
|
||||
|
||||
Kotlin:
|
||||
```kotlin
|
||||
when (api.getModuleOverrideForPlayer(player, "sword-blocking")) {
|
||||
PlayerModuleOverride.FORCE_ENABLED -> player.sendMessage("Forced on")
|
||||
PlayerModuleOverride.FORCE_DISABLED -> player.sendMessage("Forced off")
|
||||
PlayerModuleOverride.DEFAULT -> player.sendMessage("Following configured rules")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `getModuleOverridesForPlayer`
|
||||
|
||||
Returns all active overrides for a player as a map of module name to `PlayerModuleOverride`. Only modules with a non-`DEFAULT` override are included.
|
||||
```java
|
||||
Map<String, PlayerModuleOverride> getModuleOverridesForPlayer(Player player)
|
||||
```
|
||||
```java
|
||||
Map<String, PlayerModuleOverride> overrides = api.getModuleOverridesForPlayer(player);
|
||||
overrides.forEach((module, override) ->
|
||||
player.sendMessage(module + ": " + override)
|
||||
);
|
||||
```
|
||||
|
||||
Kotlin:
|
||||
```kotlin
|
||||
api.getModuleOverridesForPlayer(player).forEach { (module, override) ->
|
||||
player.sendMessage("$module: $override")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `setModuleOverridesForPlayer`
|
||||
|
||||
Sets multiple module overrides for a player at once. Entries with `PlayerModuleOverride.DEFAULT` are treated as clears. All module names and override values are validated before any override state changes, so an invalid entry leaves existing overrides unchanged.
|
||||
```java
|
||||
void setModuleOverridesForPlayer(Player player, Map<String, PlayerModuleOverride> overrides)
|
||||
```
|
||||
|
||||
**Throws:** `IllegalArgumentException` if any module name is unknown or non-configurable, or if a Java caller supplies a null module name or override value.
|
||||
```java
|
||||
Map<String, PlayerModuleOverride> overrides = new HashMap<>();
|
||||
overrides.put("attack-frequency", PlayerModuleOverride.FORCE_ENABLED);
|
||||
overrides.put("sword-blocking", PlayerModuleOverride.FORCE_ENABLED);
|
||||
overrides.put("disable-offhand", PlayerModuleOverride.FORCE_DISABLED);
|
||||
api.setModuleOverridesForPlayer(player, overrides);
|
||||
```
|
||||
|
||||
Kotlin:
|
||||
```kotlin
|
||||
api.setModuleOverridesForPlayer(
|
||||
player,
|
||||
mapOf(
|
||||
"attack-frequency" to PlayerModuleOverride.FORCE_ENABLED,
|
||||
"sword-blocking" to PlayerModuleOverride.FORCE_ENABLED,
|
||||
"disable-offhand" to PlayerModuleOverride.FORCE_DISABLED,
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `isModuleEnabledForPlayer`
|
||||
|
||||
Returns whether a configurable module is effectively active for a player, accounting for configured rules and any per-player override.
|
||||
```java
|
||||
boolean isModuleEnabledForPlayer(Player player, String moduleName)
|
||||
```
|
||||
|
||||
**Throws:** `IllegalArgumentException` if `moduleName` is unknown or non-configurable.
|
||||
```java
|
||||
if (api.isModuleEnabledForPlayer(player, "attack-frequency")) {
|
||||
// module is active for this player
|
||||
}
|
||||
```
|
||||
|
||||
Kotlin:
|
||||
```kotlin
|
||||
if (api.isModuleEnabledForPlayer(player, "attack-frequency")) {
|
||||
// module is active for this player
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `hasAnyOverrideForPlayer`
|
||||
|
||||
Returns whether a player has any non-default override set.
|
||||
```java
|
||||
boolean hasAnyOverrideForPlayer(Player player)
|
||||
```
|
||||
```java
|
||||
if (api.hasAnyOverrideForPlayer(player)) {
|
||||
player.sendMessage("You have active overrides.");
|
||||
}
|
||||
```
|
||||
|
||||
Kotlin:
|
||||
```kotlin
|
||||
if (api.hasAnyOverrideForPlayer(player)) {
|
||||
player.sendMessage("You have active overrides.")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `getModuleNames`
|
||||
|
||||
Returns the names of all configurable modules that support per-player overrides. Only names in this set are valid inputs for the other API methods.
|
||||
```java
|
||||
Set<String> getModuleNames()
|
||||
```
|
||||
```java
|
||||
for (String name : api.getModuleNames()) {
|
||||
player.sendMessage(name);
|
||||
}
|
||||
```
|
||||
|
||||
Kotlin:
|
||||
```kotlin
|
||||
api.getModuleNames().forEach { name ->
|
||||
player.sendMessage(name)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `PlayerModuleOverride`
|
||||
|
||||
| Value | Description |
|
||||
|------------------|----------------------------------------------------------------------------------------|
|
||||
| `DEFAULT` | No per-player override; the player follows configured module and modeset rules. |
|
||||
| `FORCE_ENABLED` | Module is forced on for the player before configured disabled modules are considered. |
|
||||
| `FORCE_DISABLED` | Module is forced off for the player before configured enabled modules are considered. |
|
||||
|
||||
---
|
||||
|
||||
## Events
|
||||
|
||||
Register event listeners with Bukkit as usual. Events are fired after OCM changes the stored API state and before it reapplies module state for the affected player.
|
||||
|
||||
### `PlayerModesetChangeEvent`
|
||||
|
||||
Fired when a player's stored modeset changes. Reasons are `API`, `COMMAND`, `WORLD_CHANGE`, and `JOIN`.
|
||||
|
||||
```java
|
||||
@EventHandler
|
||||
public void onModesetChange(PlayerModesetChangeEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
String previous = event.getPreviousModeset();
|
||||
String current = event.getNewModeset();
|
||||
|
||||
player.sendMessage("Modeset changed from " + previous + " to " + current
|
||||
+ " because of " + event.getReason());
|
||||
}
|
||||
```
|
||||
|
||||
`getPreviousModeset()` and `getNewModeset()` can return `null` when no modeset was previously stored or no replacement is available.
|
||||
|
||||
### `PlayerModuleOverrideChangeEvent`
|
||||
|
||||
Fired when a player's per-player module override changes.
|
||||
|
||||
```java
|
||||
@EventHandler
|
||||
public void onModuleOverrideChange(PlayerModuleOverrideChangeEvent event) {
|
||||
if (event.getNewOverride() == PlayerModuleOverride.DEFAULT) {
|
||||
event.getPlayer().sendMessage(event.getModuleName() + " is following configured rules again.");
|
||||
return;
|
||||
}
|
||||
|
||||
event.getPlayer().sendMessage(event.getModuleName() + " changed from "
|
||||
+ event.getPreviousOverride() + " to " + event.getNewOverride());
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Java example
|
||||
```java
|
||||
public class ArenaCombatManager {
|
||||
|
||||
private final OldCombatMechanicsAPI api;
|
||||
|
||||
private static final Map<String, PlayerModuleOverride> ARENA_OVERRIDES;
|
||||
static {
|
||||
ARENA_OVERRIDES = new HashMap<>();
|
||||
ARENA_OVERRIDES.put("attack-frequency", PlayerModuleOverride.FORCE_ENABLED);
|
||||
ARENA_OVERRIDES.put("sword-blocking", PlayerModuleOverride.FORCE_ENABLED);
|
||||
ARENA_OVERRIDES.put("disable-offhand", PlayerModuleOverride.FORCE_ENABLED);
|
||||
}
|
||||
|
||||
public ArenaCombatManager(OldCombatMechanicsAPI api) {
|
||||
this.api = api;
|
||||
}
|
||||
|
||||
public void onPlayerJoinArena(Player player) {
|
||||
api.setModuleOverridesForPlayer(player, ARENA_OVERRIDES);
|
||||
}
|
||||
|
||||
public void onPlayerLeaveArena(Player player) {
|
||||
api.clearAllModuleOverridesForPlayer(player);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Kotlin example
|
||||
```kotlin
|
||||
class ArenaCombatManager(private val api: OldCombatMechanicsAPI) {
|
||||
|
||||
private val arenaOverrides = mapOf(
|
||||
"attack-frequency" to PlayerModuleOverride.FORCE_ENABLED,
|
||||
"sword-blocking" to PlayerModuleOverride.FORCE_ENABLED,
|
||||
"disable-offhand" to PlayerModuleOverride.FORCE_ENABLED,
|
||||
)
|
||||
|
||||
fun onPlayerJoinArena(player: Player) {
|
||||
api.setModuleOverridesForPlayer(player, arenaOverrides)
|
||||
}
|
||||
|
||||
fun onPlayerLeaveArena(player: Player) {
|
||||
api.clearAllModuleOverridesForPlayer(player)
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,5 +1,38 @@
|
||||
# Changelog
|
||||
|
||||
## [2.5.0](https://github.com/kernitus/BukkitOldCombatMechanics/compare/v2.4.0...v2.5.0) (2026-05-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add per-player module override api ([53780bc](https://github.com/kernitus/BukkitOldCombatMechanics/commit/53780bc3b286dd8a0bd1e685df114326c24db96d))
|
||||
* add per-player module override api ([c2043e2](https://github.com/kernitus/BukkitOldCombatMechanics/commit/c2043e23e064b2893f8ee32c113191f6952d639f))
|
||||
* **api:** expose per-player modeset controls ([b3a6039](https://github.com/kernitus/BukkitOldCombatMechanics/commit/b3a603991814fe6840014d9949a995cb59b22d60))
|
||||
* **api:** publish player state change events ([6f33807](https://github.com/kernitus/BukkitOldCombatMechanics/commit/6f338073663e2720b5c7a390f986246ee82545ef))
|
||||
* improve API & introduce API.md ([a091b06](https://github.com/kernitus/BukkitOldCombatMechanics/commit/a091b0688982fffc6b2fb862ca5750f4575adf38))
|
||||
* migrate default world modesets ([8e9ee90](https://github.com/kernitus/BukkitOldCombatMechanics/commit/8e9ee90cec78c34034b4bfddec1a529f732b2cab))
|
||||
* per-item cooldown ([890c41f](https://github.com/kernitus/BukkitOldCombatMechanics/commit/890c41f442ed2ba5570096856b206829c1159ea8)), closes [#856](https://github.com/kernitus/BukkitOldCombatMechanics/issues/856)
|
||||
* rewrite in kotlin ([d4eee15](https://github.com/kernitus/BukkitOldCombatMechanics/commit/d4eee15c54952a68a8c3635b497835d4fd969165))
|
||||
* show build commit in version output ([e20fe98](https://github.com/kernitus/BukkitOldCombatMechanics/commit/e20fe988e9cc3fb3f174fe50cf8e8941d465232f))
|
||||
* support default world modesets ([7d3b417](https://github.com/kernitus/BukkitOldCombatMechanics/commit/7d3b41707bb879b81e753182292a7c51a2e9ee99))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **armour:** apply mixed-mode armour strength by defender ([f32671b](https://github.com/kernitus/BukkitOldCombatMechanics/commit/f32671bc02c71da8978a5e5d33513b995ccc1ea0))
|
||||
* **armour:** respect modesets for explosion defence ([#861](https://github.com/kernitus/BukkitOldCombatMechanics/issues/861)) ([729c311](https://github.com/kernitus/BukkitOldCombatMechanics/commit/729c311e6e77df42281084e0a3a276376c834050))
|
||||
* **attack-range:** detect Paper data component support ([b12829d](https://github.com/kernitus/BukkitOldCombatMechanics/commit/b12829d7c300ad5c516bb481e3a28c8c2653d723))
|
||||
* **compat:** avoid hot-path optional API reflection ([8f14b8b](https://github.com/kernitus/BukkitOldCombatMechanics/commit/8f14b8b8e2f7d8d1e71a059e85c4bebf20f899a8))
|
||||
* **compat:** clarify semantic version gates ([b5341f3](https://github.com/kernitus/BukkitOldCombatMechanics/commit/b5341f3898308f34b4aa8ca81ac91d8695cb6158))
|
||||
* **compat:** detect API capabilities for version gates ([bfbb2e4](https://github.com/kernitus/BukkitOldCombatMechanics/commit/bfbb2e420ffeeb6665a1c73afb6f66eb32265de7))
|
||||
* **damage:** respect bypasses_cooldown damage types [#864](https://github.com/kernitus/BukkitOldCombatMechanics/issues/864) ([83a7154](https://github.com/kernitus/BukkitOldCombatMechanics/commit/83a7154d8cea60265be6ea46acb0165d1767ce68))
|
||||
* **modesets:** correct stale stored world modesets ([#865](https://github.com/kernitus/BukkitOldCombatMechanics/issues/865)) ([b0f451d](https://github.com/kernitus/BukkitOldCombatMechanics/commit/b0f451d7de4a30e40047f4a382da77ffeef6127d))
|
||||
* **reflection:** compare Minecraft versions lexicographically ([67e266c](https://github.com/kernitus/BukkitOldCombatMechanics/commit/67e266c938dc51fef17b68ba90ef29785c69e9f9))
|
||||
* respect MythicMobs no-knockback damage ([#860](https://github.com/kernitus/BukkitOldCombatMechanics/issues/860)) ([a25e0ed](https://github.com/kernitus/BukkitOldCombatMechanics/commit/a25e0ed24c87477f6ae912230568c323d55ed576))
|
||||
* **shields:** apply mixed-mode blocking by defender ([20249db](https://github.com/kernitus/BukkitOldCombatMechanics/commit/20249db1ee46875494f96942890ed318ec7101f0))
|
||||
* **tool-damage:** apply mixed-mode weapon damage by attacker ([eb905f2](https://github.com/kernitus/BukkitOldCombatMechanics/commit/eb905f21b8eabed8ad2029d352ff99cb7978a287))
|
||||
* validate module override bulk updates ([c6e14ba](https://github.com/kernitus/BukkitOldCombatMechanics/commit/c6e14bac3dd270738572064d101ad1acd73a5636))
|
||||
|
||||
## [2.4.0](https://github.com/kernitus/BukkitOldCombatMechanics/compare/v2.3.0...v2.4.0) (2026-03-08)
|
||||
|
||||
|
||||
|
||||
@@ -11,16 +11,13 @@
|
||||
|
||||
## by kernitus and Rayzr522
|
||||
|
||||
|
||||
**TrueOG Network 1.19.4 Branch**
|
||||
|
||||
|
||||
Fine‑tune Minecraft combat, movement, and item balance without breaking your server. OldCombatMechanics is a free, open‑source toolkit for Spigot & Paper that lets you mix 1.8‑style snappiness with modern features, per world and per player.
|
||||
Fine‑tune Minecraft combat, movement, and item balance without breaking your server. OldCombatMechanics is a free, open‑source toolkit for Spigot & Paper that lets you mix 1.8‑style snappiness with modern features, per world and per player. It is built for PvP networks, minigames, BedWars/SkyWars-style games, duels, KitPvP, practice arenas, and mixed PvP/PvE worlds that need different combat rules in different places.
|
||||
|
||||
**Why servers pick OCM** ✨
|
||||
- 🧩 **Modular:** enable only what you need: cooldowns, tool damage, knockback, shields, potions, reach, sounds, more.
|
||||
- 🚀 **Performant:** lean listeners only enabled as needed; reflection lookups are cached and recurring tasks are minimised (shared where possible) to keep tick time low on busy PvP servers.
|
||||
- 🗺️ **Modesets:** ship different rules for different worlds or players; perfect for mixed PvP/PvE, minigames, or duels.
|
||||
- 🗺️ **Modesets:** ship different rules for different worlds or players; perfect for PvP networks, BedWars/SkyWars-style minigames, duels, KitPvP, FFA, practice arenas, and mixed PvP/PvE.
|
||||
- 🔌 **Plugin-friendly API:** integrate OCM with game logic by applying session-only per-player module overrides, managing player modesets, and checking whether modules are effectively enabled.
|
||||
- ⏪ **Backwards‑friendly:** runs on Java 8+, supports 1.9 to latest; integrates cleanly with PlaceholderAPI and PacketEvents.
|
||||
- ✅ **Tested for you:** live integration tests run real Paper servers across multiple versions every build.
|
||||
- 💸 **Zero cost:** fully open source, optional basic telemetry (bStats only), no paywalls.
|
||||
@@ -49,6 +46,18 @@ Fine‑tune Minecraft combat, movement, and item balance without breaking your s
|
||||
- Per-player/per-world presets that decide which features are active; each world has an allowed list and a default modeset.
|
||||
- Let players pick ( `/ocm modeset <name>` ) to run, for example, 1.8-style PvP in an arena world while keeping vanilla rules in survival.
|
||||
|
||||
## 🏟 Built for minigames and PvP networks
|
||||
- Run old-style combat in competitive arenas while keeping modern mechanics in lobbies, survival, or event worlds.
|
||||
- Tailor rules for BedWars, SkyWars, duels, KitPvP, FFA, practice arenas, tournaments, and temporary events without splitting your network across extra plugins.
|
||||
- Keep configuration readable for staff: combine world defaults, player-selected modesets, and targeted plugin overrides for special match phases.
|
||||
|
||||
## 🔌 Developer API
|
||||
- OCM registers its API through Bukkit's services manager, so other plugins can look it up without depending on internal implementation classes.
|
||||
- The current API supports session-only per-player module overrides, player modeset queries/changes, and events for reacting to OCM state changes.
|
||||
- Plugins can also check whether a module is effectively enabled for a player after world rules, player choices, and overrides have been considered.
|
||||
- Overrides are not persisted across restarts or reconnects; reapply them from your game state when needed.
|
||||
- See [API.md](API.md) for dependency setup, `plugin.yml` guidance, method details, and Java/Kotlin examples.
|
||||
|
||||
## ⚙ Configurable Features
|
||||
Features are grouped in `module`s as listed below, and can be individually configured and disabled. Disabled modules will have no impact on server performance.
|
||||
|
||||
@@ -98,8 +107,8 @@ Features are grouped in `module`s as listed below, and can be individually confi
|
||||
## 🔌 Compatibility & Testing
|
||||
- OCM targets Spigot 1.9+ and runs on Java 8 and up.
|
||||
- We stick to Spigot/Paper APIs for forward compatibility; NMS/reflection is used only when necessary.
|
||||
- Integration tests boot real servers on 1.9.4, 1.12, 1.19.2, and 1.21.11 each build to verify behaviour.
|
||||
- Most plugins work fine with OCM. Explicitly tested integrations include PlaceholderAPI (see [wiki](https://github.com/kernitus/BukkitOldCombatMechanics/wiki/PlaceholderAPI)).
|
||||
- Integration tests boot real servers on 1.9.4, 1.12, 1.19.2, 1.21.11, and 26.1.2 each build to verify behaviour.
|
||||
- Most plugins work fine with OCM. Targeted compatibility includes PlaceholderAPI (see [wiki](https://github.com/kernitus/BukkitOldCombatMechanics/wiki/PlaceholderAPI)) and MythicMobs no-knockback damage skills with old-player-knockback.
|
||||
|
||||
## 🧾 Licence
|
||||
- Source code in this repository is under the Mozilla Public License 2.0 (MPL‑2.0).
|
||||
@@ -111,12 +120,12 @@ Oftentimes a particular bug fix or feature has already been implemented, but a n
|
||||
yet. You can find the most up-to-date version of the plugin
|
||||
on [Hangar](https://hangar.papermc.io/kernitus/OldCombatMechanics/versions?channel=Snapshot&platform=PAPER).
|
||||
|
||||
(??)
|
||||
(??)## 🤝 Contributions
|
||||
(??)If you are interested in contributing, please [check this page first](.github/CONTRIBUTING.md).
|
||||
(??)<hr/>
|
||||
(??)
|
||||
(??)
|
||||
(??)<a href="https://bstats.org/plugin/bukkit/OldCombatMechanics">
|
||||
(??) <img src="https://bstats.org/signatures/bukkit/OldCombatMechanics.svg" alt="bStats">
|
||||
(??)</a>
|
||||
|
||||
## 🤝 Contributions
|
||||
If you are interested in contributing, please [check this page first](.github/CONTRIBUTING.md).
|
||||
<hr/>
|
||||
|
||||
|
||||
<a href="https://bstats.org/plugin/bukkit/OldCombatMechanics">
|
||||
<img src="https://bstats.org/signatures/bukkit/OldCombatMechanics.svg" alt="bStats">
|
||||
</a>
|
||||
+217
-25
@@ -13,7 +13,6 @@ import org.gradle.api.file.FileCopyDetails
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import xyz.jpenilla.runpaper.task.RunServer
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.Closeable
|
||||
import java.io.Serializable
|
||||
import java.net.URI
|
||||
@@ -27,14 +26,40 @@ val paperVersion: List<String> =
|
||||
|
||||
plugins {
|
||||
`java-library`
|
||||
kotlin("jvm") version "2.3.0"
|
||||
id("com.gradleup.shadow") version "9.3.0"
|
||||
kotlin("jvm") version "2.3.21"
|
||||
id("com.diffplug.spotless") version "8.5.1"
|
||||
id("com.gradleup.shadow") version "9.4.1"
|
||||
id("xyz.jpenilla.run-paper") version "3.0.2"
|
||||
idea
|
||||
eclipse
|
||||
id("io.papermc.hangar-publish-plugin") version "0.1.4"
|
||||
}
|
||||
|
||||
spotless {
|
||||
// Avoid a noisy first formatter rollout; enforce Spotless on changed files.
|
||||
ratchetFrom("origin/master")
|
||||
|
||||
kotlin {
|
||||
target("src/**/*.kt")
|
||||
ktlint()
|
||||
}
|
||||
|
||||
kotlinGradle {
|
||||
target("*.gradle.kts", "gradle/**/*.gradle.kts")
|
||||
ktlint()
|
||||
endWithNewline()
|
||||
}
|
||||
|
||||
format("misc") {
|
||||
target("*.md", "*.yml", "*.yaml", "*.properties", ".editorconfig")
|
||||
trimTrailingWhitespace()
|
||||
endWithNewline()
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("check") {
|
||||
dependsOn("spotlessCheck")
|
||||
}
|
||||
|
||||
// Make sure javadocs are available to IDE
|
||||
idea {
|
||||
module {
|
||||
@@ -55,12 +80,14 @@ repositories {
|
||||
maven("https://repo.codemc.io/repository/maven-snapshots/")
|
||||
// Placeholder API
|
||||
maven("https://repo.extendedclip.com/content/repositories/placeholderapi/")
|
||||
// CodeMC Repo for bStats
|
||||
maven("https://repo.codemc.org/repository/maven-public/")
|
||||
// Auth library from Minecraft
|
||||
maven("https://libraries.minecraft.net/")
|
||||
}
|
||||
|
||||
group = "kernitus.plugin.OldCombatMechanics"
|
||||
version = "2.5.0-beta" // x-release-please-version
|
||||
version = "2.5.0" // x-release-please-version
|
||||
description = "OldCombatMechanics"
|
||||
|
||||
java {
|
||||
@@ -78,6 +105,12 @@ sourceSets {
|
||||
compileClasspath += main.get().output
|
||||
runtimeClasspath += output + main.get().output
|
||||
}
|
||||
val apiSmokeTest by creating {
|
||||
java.setSrcDirs(listOf("src/apiSmokeTest/java"))
|
||||
resources.setSrcDirs(listOf("src/apiSmokeTest/resources"))
|
||||
compileClasspath += main.get().output
|
||||
runtimeClasspath += output + main.get().output
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
@@ -96,25 +129,34 @@ configurations.named("compileClasspath") {
|
||||
configurations.named("integrationTestCompileClasspath") {
|
||||
attributes.attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 17)
|
||||
}
|
||||
configurations.named("apiSmokeTestCompileClasspath") {
|
||||
attributes.attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 17)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Shaded in by Bukkit
|
||||
compileOnly("io.netty:netty-all:4.1.130.Final")
|
||||
implementation("org.bstats:bstats-bukkit:3.2.1")
|
||||
// Server-provided: intentionally pinned to the oldest supported runtime baseline
|
||||
// (Minecraft 1.9.4 / Netty 4.0.23.Final) so compile-only code cannot use newer Netty APIs unavailable on legacy servers.
|
||||
compileOnly("io.netty:netty-all:4.0.23.Final")
|
||||
// Placeholder API
|
||||
compileOnly("me.clip:placeholderapi:2.11.6")
|
||||
compileOnly("me.clip:placeholderapi:2.12.2")
|
||||
// For BSON file serialisation
|
||||
implementation("org.mongodb:bson:5.6.2")
|
||||
implementation("org.mongodb:bson:5.7.0")
|
||||
// Spigot
|
||||
compileOnly("org.spigotmc:spigot-api:1.21.11-R0.1-SNAPSHOT")
|
||||
compileOnly("org.spigotmc:spigot-api:26.1.2-R0.1-SNAPSHOT")
|
||||
testImplementation("io.kotest:kotest-runner-junit5-jvm:5.9.1")
|
||||
testImplementation("io.kotest:kotest-assertions-core-jvm:5.9.1")
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
// JSR-305 annotations (javax.annotation.Nullable)
|
||||
compileOnly("com.google.code.findbugs:jsr305:3.0.2")
|
||||
// PacketEvents
|
||||
implementation("com.github.retrooper:packetevents-spigot:2.11.2")
|
||||
implementation("com.github.retrooper:packetevents-spigot:2.12.1")
|
||||
// XSeries
|
||||
implementation("com.github.cryptomorin:XSeries:13.6.0")
|
||||
implementation("com.github.cryptomorin:XSeries:13.7.0")
|
||||
|
||||
// For ingametesting
|
||||
// Mojang mappings for NMS
|
||||
|
||||
/*
|
||||
compileOnly("com.mojang:authlib:6.0.54")
|
||||
paperweight.paperDevBundle("1.19.2-R0.1-SNAPSHOT")
|
||||
@@ -123,12 +165,25 @@ dependencies {
|
||||
*/
|
||||
|
||||
// Integration test dependencies
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.3.0")
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.3.21")
|
||||
add("integrationTestImplementation", "org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.3.21")
|
||||
add("integrationTestImplementation", "org.jetbrains.kotlin:kotlin-test:2.3.21")
|
||||
add("integrationTestImplementation", "org.jetbrains.kotlin:kotlin-reflect:2.3.21")
|
||||
add("integrationTestImplementation", "io.kotest:kotest-runner-junit5-jvm:5.9.1")
|
||||
add("integrationTestImplementation", "io.kotest:kotest-assertions-core-jvm:5.9.1")
|
||||
add("integrationTestImplementation", "xyz.jpenilla:reflection-remapper:0.1.3")
|
||||
add("integrationTestCompileOnly", "org.spigotmc:spigot-api:26.1.2-R0.1-SNAPSHOT")
|
||||
add("integrationTestCompileOnly", "com.mojang:authlib:6.0.54")
|
||||
add("integrationTestCompileOnly", "io.netty:netty-all:4.0.23.Final")
|
||||
|
||||
// Java-only API smoke plugin dependencies
|
||||
add("apiSmokeTestCompileOnly", sourceSets.main.get().output)
|
||||
add("apiSmokeTestCompileOnly", "org.spigotmc:spigot-api:1.21.11-R0.1-SNAPSHOT")
|
||||
}
|
||||
|
||||
// Substitute ${pluginVersion} in plugin.yml with version defined above
|
||||
class ExpandPluginVersionAction(
|
||||
private val version: String,
|
||||
private val version: String
|
||||
) : Action<FileCopyDetails>,
|
||||
Serializable {
|
||||
override fun execute(details: FileCopyDetails) {
|
||||
@@ -136,11 +191,39 @@ class ExpandPluginVersionAction(
|
||||
}
|
||||
}
|
||||
|
||||
class ExpandBuildCommitAction(
|
||||
private val commit: String
|
||||
) : Action<FileCopyDetails>,
|
||||
Serializable {
|
||||
override fun execute(details: FileCopyDetails) {
|
||||
details.expand(mapOf("buildCommit" to commit))
|
||||
}
|
||||
}
|
||||
|
||||
val pluginVersion = project.version.toString()
|
||||
val buildCommit =
|
||||
runCatching {
|
||||
val process =
|
||||
ProcessBuilder("git", "rev-parse", "--short", "HEAD")
|
||||
.directory(rootDir)
|
||||
.redirectError(ProcessBuilder.Redirect.DISCARD)
|
||||
.start()
|
||||
val output = process.inputStream.bufferedReader().use { it.readText() }
|
||||
val exitValue = process.waitFor()
|
||||
|
||||
if (exitValue == 0) {
|
||||
output.trim()
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}.getOrDefault("")
|
||||
val expandPluginVersionAction = ExpandPluginVersionAction(pluginVersion)
|
||||
val expandBuildCommitAction = ExpandBuildCommitAction(buildCommit)
|
||||
tasks.named<Copy>("processResources") {
|
||||
inputs.property("pluginVersion", pluginVersion)
|
||||
inputs.property("buildCommit", buildCommit)
|
||||
filesMatching("plugin.yml", expandPluginVersionAction)
|
||||
filesMatching("ocm-build.properties", expandBuildCommitAction)
|
||||
}
|
||||
|
||||
tasks.withType<JavaCompile> {
|
||||
@@ -148,8 +231,8 @@ tasks.withType<JavaCompile> {
|
||||
options.release.set(8)
|
||||
}
|
||||
|
||||
tasks.named<Jar>("jar") {
|
||||
enabled = false
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
val shadowJarTask =
|
||||
@@ -157,7 +240,7 @@ val shadowJarTask =
|
||||
dependsOn("jar")
|
||||
archiveFileName.set("${project.name}.jar")
|
||||
dependencies {
|
||||
exclude(dependency("org.jetbrains.kotlin:.*"))
|
||||
relocate("org.bstats", "kernitus.plugin.OldCombatMechanics.lib.bstats")
|
||||
relocate("com.cryptomorin.xseries", "kernitus.plugin.OldCombatMechanics.lib.xseries")
|
||||
relocate("com.github.retrooper.packetevents", "kernitus.plugin.OldCombatMechanics.lib.packetevents.api")
|
||||
relocate("io.github.retrooper.packetevents", "kernitus.plugin.OldCombatMechanics.lib.packetevents.impl")
|
||||
@@ -165,6 +248,7 @@ val shadowJarTask =
|
||||
}
|
||||
|
||||
// For ingametesting
|
||||
|
||||
/*
|
||||
tasks.reobfJar {
|
||||
outputJar.set(File(buildDir, "libs/${project.name}.jar"))
|
||||
@@ -220,11 +304,29 @@ val integrationTestJarTask =
|
||||
exclude("META-INF/versions/**/module-info.class")
|
||||
}
|
||||
|
||||
val apiSmokeTestJarTask =
|
||||
tasks.register<Jar>("apiSmokeTestJar") {
|
||||
archiveClassifier.set("api-smoke-test")
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
|
||||
dependsOn("apiSmokeTestClasses")
|
||||
|
||||
from(sourceSets["apiSmokeTest"].output)
|
||||
|
||||
exclude("META-INF/*.SF")
|
||||
exclude("META-INF/*.DSA")
|
||||
exclude("META-INF/*.RSA")
|
||||
exclude("META-INF/*.EC")
|
||||
exclude("META-INF/*.MF")
|
||||
exclude("module-info.class")
|
||||
exclude("META-INF/versions/**/module-info.class")
|
||||
}
|
||||
|
||||
val integrationTestMinecraftVersion =
|
||||
(findProperty("integrationTestMinecraftVersion") as String?) ?: "1.19.2"
|
||||
|
||||
val defaultIntegrationTestVersions =
|
||||
listOf(integrationTestMinecraftVersion, "1.21.11", "1.12", "1.9.4")
|
||||
listOf(integrationTestMinecraftVersion, "26.1.2", "1.21.11", "1.12", "1.9.4")
|
||||
.distinct()
|
||||
|
||||
val integrationTestVersions: List<String> =
|
||||
@@ -243,6 +345,8 @@ val integrationTestJavaVersionLegacy16 =
|
||||
(findProperty("integrationTestJavaVersionLegacy16") as String?)?.toInt() ?: 11
|
||||
val integrationTestJavaVersionModern =
|
||||
(findProperty("integrationTestJavaVersionModern") as String?)?.toInt() ?: 25
|
||||
val apiSmokeTestMinecraftVersion =
|
||||
(findProperty("apiSmokeTestMinecraftVersion") as String?) ?: "1.19.2"
|
||||
|
||||
fun parseMinecraftVersion(version: String): Triple<Int, Int, Int> {
|
||||
val parts = version.split(".")
|
||||
@@ -266,9 +370,18 @@ fun requiresModernJava(version: String): Boolean {
|
||||
|
||||
fun requiredJavaVersion(version: String): Int {
|
||||
if (needsLegacyVanillaJar(version)) return integrationTestJavaVersionLegacyPre13
|
||||
if (requiresModernJava(version)) return integrationTestJavaVersionModern
|
||||
val (_, minor, _) = parseMinecraftVersion(version)
|
||||
if (minor <= 16) return integrationTestJavaVersionLegacy16
|
||||
return if (requiresModernJava(version)) integrationTestJavaVersionModern else integrationTestJavaVersionLegacy
|
||||
return integrationTestJavaVersionLegacy
|
||||
}
|
||||
|
||||
fun supportsDamageTypeIntegrationDatapack(version: String): Boolean {
|
||||
val (major, minor, patch) = parseMinecraftVersion(version)
|
||||
if (major > 1) return true
|
||||
if (major < 1) return false
|
||||
if (minor > 21) return true
|
||||
return minor == 21 && patch >= 11
|
||||
}
|
||||
|
||||
data class KotestSummary(
|
||||
@@ -280,7 +393,7 @@ data class KotestSummary(
|
||||
val testsIgnored: Int?,
|
||||
val testsTotal: Int?,
|
||||
val failures: List<String>,
|
||||
val failureDetails: List<String>,
|
||||
val failureDetails: List<String>
|
||||
)
|
||||
|
||||
fun parseKotestSummary(logFile: File): KotestSummary? {
|
||||
@@ -424,7 +537,7 @@ fun parseKotestSummary(logFile: File): KotestSummary? {
|
||||
testsIgnored,
|
||||
testsTotal,
|
||||
failures,
|
||||
failureDetails.distinct(),
|
||||
failureDetails.distinct()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -471,6 +584,7 @@ for (version in integrationTestVersions) {
|
||||
val failuresFile = runDir.resolve("plugins/OldCombatMechanicsTest/test-failures.txt")
|
||||
val vanillaCacheFile = runDir.resolve("cache/mojang_$version.jar")
|
||||
val logFile = layout.buildDirectory.file("integration-test-logs/$suffix.log")
|
||||
val datapackSourceDir = layout.projectDirectory.dir("src/integrationTest/datapacks")
|
||||
|
||||
val writePropsTask =
|
||||
tasks.register<WriteProperties>("writeProperties$suffix") {
|
||||
@@ -522,7 +636,7 @@ for (version in integrationTestVersions) {
|
||||
if (!downloadedSha1.equals(serverSha1, ignoreCase = true)) {
|
||||
tmpFile.delete()
|
||||
throw GradleException(
|
||||
"Downloaded Minecraft server jar hash mismatch for $version. Expected $serverSha1, got $downloadedSha1.",
|
||||
"Downloaded Minecraft server jar hash mismatch for $version. Expected $serverSha1, got $downloadedSha1."
|
||||
)
|
||||
}
|
||||
tmpFile.copyTo(vanillaCacheFile, overwrite = true)
|
||||
@@ -553,7 +667,7 @@ for (version in integrationTestVersions) {
|
||||
javaLauncher.set(
|
||||
javaToolchains.launcherFor {
|
||||
languageVersion.set(JavaLanguageVersion.of(requiredJavaVersion(version)))
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
pluginJars.from(shadowJarTask.flatMap { it.archiveFile })
|
||||
@@ -561,6 +675,16 @@ for (version in integrationTestVersions) {
|
||||
pluginJars.from(configurations["integrationTestServerPlugins"])
|
||||
|
||||
doFirst {
|
||||
val damageTypeDatapackDir = runDir.resolve("world/datapacks/ocmtest-bypasses-cooldown")
|
||||
if (supportsDamageTypeIntegrationDatapack(version)) {
|
||||
copy {
|
||||
from(datapackSourceDir)
|
||||
into(runDir.resolve("world/datapacks"))
|
||||
}
|
||||
} else if (damageTypeDatapackDir.exists()) {
|
||||
delete(damageTypeDatapackDir)
|
||||
}
|
||||
|
||||
val log = logFile.get().asFile
|
||||
log.parentFile.mkdirs()
|
||||
val stream = log.outputStream()
|
||||
@@ -598,11 +722,13 @@ for (version in integrationTestVersions) {
|
||||
summary?.let {
|
||||
val parts = mutableListOf<String>()
|
||||
if (it.specsTotal != null) {
|
||||
parts.add("Specs: ${it.specsPassed ?: "?"} passed, ${it.specsFailed ?: "?"} failed, ${it.specsTotal} total")
|
||||
parts.add(
|
||||
"Specs: ${it.specsPassed ?: "?"} passed, ${it.specsFailed ?: "?"} failed, ${it.specsTotal} total"
|
||||
)
|
||||
}
|
||||
if (it.testsTotal != null) {
|
||||
parts.add(
|
||||
"Tests: ${it.testsPassed ?: "?"} passed, ${it.testsFailed ?: "?"} failed, ${it.testsIgnored ?: 0} ignored, ${it.testsTotal} total",
|
||||
"Tests: ${it.testsPassed ?: "?"} passed, ${it.testsFailed ?: "?"} failed, ${it.testsIgnored ?: 0} ignored, ${it.testsTotal} total"
|
||||
)
|
||||
}
|
||||
if (it.failures.isNotEmpty()) {
|
||||
@@ -660,6 +786,72 @@ tasks.register("integrationTestMatrix") {
|
||||
dependsOn(integrationTestMatrixTasks)
|
||||
}
|
||||
|
||||
val apiSmokeTestSuffix = versionTaskSuffix(apiSmokeTestMinecraftVersion)
|
||||
val apiSmokeRunDir = file("run/api-smoke-$apiSmokeTestMinecraftVersion")
|
||||
val apiSmokeResultFile = apiSmokeRunDir.resolve("plugins/OldCombatMechanicsApiSmokeTest/test-results.txt")
|
||||
val apiSmokeWritePropertiesTask =
|
||||
tasks.register<WriteProperties>("writeApiSmokeProperties$apiSmokeTestSuffix") {
|
||||
encoding = "UTF-8"
|
||||
property("online-mode", false)
|
||||
destinationFile.set(apiSmokeRunDir.resolve("server.properties"))
|
||||
}
|
||||
|
||||
val runApiSmokeServerTask =
|
||||
tasks.register<RunServer>("runApiSmokeServer$apiSmokeTestSuffix") {
|
||||
dependsOn(apiSmokeWritePropertiesTask, shadowJarTask, apiSmokeTestJarTask)
|
||||
runDirectory.set(apiSmokeRunDir)
|
||||
minecraftVersion(apiSmokeTestMinecraftVersion)
|
||||
jvmArgs("-Dcom.mojang.eula.agree=true")
|
||||
javaLauncher.set(
|
||||
javaToolchains.launcherFor {
|
||||
languageVersion.set(JavaLanguageVersion.of(requiredJavaVersion(apiSmokeTestMinecraftVersion)))
|
||||
}
|
||||
)
|
||||
|
||||
pluginJars.from(shadowJarTask.flatMap { it.archiveFile })
|
||||
pluginJars.from(apiSmokeTestJarTask.flatMap { it.archiveFile })
|
||||
|
||||
doFirst {
|
||||
if (apiSmokeResultFile.exists()) {
|
||||
apiSmokeResultFile.delete()
|
||||
}
|
||||
val ocmConfigFile = apiSmokeRunDir.resolve("plugins/OldCombatMechanics/config.yml")
|
||||
if (ocmConfigFile.exists()) {
|
||||
ocmConfigFile.delete()
|
||||
}
|
||||
val pluginNames = pluginJars.files.map { it.name }
|
||||
if (pluginNames.any { it.contains("tests", ignoreCase = true) }) {
|
||||
throw GradleException("apiSmokeTest must not load the integration-test plugin jar: $pluginNames")
|
||||
}
|
||||
logger.lifecycle("API smoke server plugins: ${pluginNames.joinToString(", ")}")
|
||||
}
|
||||
}
|
||||
|
||||
val checkApiSmokeResultsTask =
|
||||
tasks.register("checkApiSmokeResults$apiSmokeTestSuffix") {
|
||||
doLast {
|
||||
if (!apiSmokeResultFile.exists()) {
|
||||
throw GradleException(
|
||||
"API smoke result file not found for $apiSmokeTestMinecraftVersion: ${apiSmokeResultFile.relativeTo(
|
||||
projectDir
|
||||
)}"
|
||||
)
|
||||
}
|
||||
val result = apiSmokeResultFile.readText().trim()
|
||||
logger.lifecycle("API smoke result for $apiSmokeTestMinecraftVersion: $result")
|
||||
if (result != "PASS") {
|
||||
throw GradleException("API smoke test failed for $apiSmokeTestMinecraftVersion: $result")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("apiSmokeTest") {
|
||||
group = "verification"
|
||||
description = "Runs the Java API smoke plugin against a live Paper server ($apiSmokeTestMinecraftVersion)."
|
||||
dependsOn(runApiSmokeServerTask)
|
||||
finalizedBy(checkApiSmokeResultsTask)
|
||||
}
|
||||
|
||||
val versionStringProvider = providers.provider { project.version.toString() }
|
||||
val isReleaseProvider = versionStringProvider.map { !it.contains('-') }
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
gameVersions=1.21.11, 1.21.10, 1.21.9, 1.21.8, 1.21.7, 1.21.6, 1.21.5, 1.21.4, 1.21.3, 1.21.2, 1.21.1, 1.21, 1.20.6, 1.20.5, 1.20.4, 1.20.3, 1.20.2, 1.20.1, 1.20, 1.19.4, 1.19.3, 1.19.2, 1.19.1, 1.19, 1.18.2, 1.18.1, 1.18, 1.17, 1.16, 1.15, 1.14, 1.13, 1.12, 1.11, 1.10, 1.9
|
||||
gameVersions=26.1.2, 26.1.1, 1.21.11, 1.21.10, 1.21.9, 1.21.8, 1.21.7, 1.21.6, 1.21.5, 1.21.4, 1.21.3, 1.21.2, 1.21.1, 1.21, 1.20.6, 1.20.5, 1.20.4, 1.20.3, 1.20.2, 1.20.1, 1.20, 1.19.4, 1.19.3, 1.19.2, 1.19.1, 1.19, 1.18.2, 1.18.1, 1.18, 1.17, 1.16, 1.15, 1.14, 1.13, 1.12, 1.11, 1.10, 1.9
|
||||
|
||||
# Disable configuration cache (run-paper tasks are not compatible).
|
||||
org.gradle.configuration-cache=false
|
||||
Vendored
BIN
Binary file not shown.
+3
-1
@@ -1,7 +1,9 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
|
||||
networkTimeout=10000
|
||||
retries=0
|
||||
retryBackOffMs=500
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
@@ -57,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
|
||||
Vendored
+82
-93
@@ -1,93 +1,82 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables, and ensure extensions are enabled
|
||||
setlocal EnableExtensions
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
"%COMSPEC%" /c exit 1
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
"%COMSPEC%" /c exit 1
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
||||
@rem which allows us to clear the local environment before executing the java command
|
||||
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
|
||||
|
||||
:exitWithErrorLevel
|
||||
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
||||
"%COMSPEC%" /c exit %ERRORLEVEL%
|
||||
Executable
+70
@@ -0,0 +1,70 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: scripts/run-integration-matrix.sh [-v versions] [-s spec-filter] [-t test-filter] [--] [extra Gradle args...]
|
||||
|
||||
Wraps ./gradlew integrationTest using repository-relative paths.
|
||||
|
||||
Options:
|
||||
-v, --versions LIST Comma-separated Minecraft versions, for example 1.19.2,1.21.11
|
||||
-s, --spec-filter TEXT Kotest spec filter passed as -Dkotest.filter.specs=TEXT
|
||||
-t, --test-filter TEXT Kotest test filter passed as -Dkotest.filter.tests=TEXT
|
||||
-h, --help Show this help text
|
||||
|
||||
This command may start Paper servers. For a non-invasive check, use
|
||||
scripts/validate-module-assignments.sh or scripts/summarise-integration-results.sh.
|
||||
USAGE
|
||||
}
|
||||
|
||||
repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
versions=
|
||||
spec_filter=
|
||||
test_filter=
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
-v|--versions)
|
||||
[ "$#" -ge 2 ] || { printf '%s\n' "Missing value for $1" >&2; usage >&2; exit 2; }
|
||||
versions=$2
|
||||
shift 2
|
||||
;;
|
||||
-s|--spec-filter)
|
||||
[ "$#" -ge 2 ] || { printf '%s\n' "Missing value for $1" >&2; usage >&2; exit 2; }
|
||||
spec_filter=$2
|
||||
shift 2
|
||||
;;
|
||||
-t|--test-filter)
|
||||
[ "$#" -ge 2 ] || { printf '%s\n' "Missing value for $1" >&2; usage >&2; exit 2; }
|
||||
test_filter=$2
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
--)
|
||||
shift
|
||||
break
|
||||
;;
|
||||
-*)
|
||||
printf '%s\n' "Unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
set -- integrationTest "$@"
|
||||
[ -z "$versions" ] || set -- "$@" "-PintegrationTestVersions=$versions"
|
||||
[ -z "$spec_filter" ] || set -- "$@" "-Dkotest.filter.specs=$spec_filter"
|
||||
[ -z "$test_filter" ] || set -- "$@" "-Dkotest.filter.tests=$test_filter"
|
||||
|
||||
printf '%s\n' "Repository: $repo_root"
|
||||
printf '%s\n' "Running: ./gradlew $*"
|
||||
cd "$repo_root"
|
||||
exec ./gradlew "$@"
|
||||
Executable
+56
@@ -0,0 +1,56 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: scripts/suggest-integration-matrix.sh [-h] [changed-path ...]
|
||||
|
||||
Suggests a small integration-test matrix from changed paths. With no paths, it
|
||||
tries git diff --name-only. Suggestions are conservative hints, not a quality
|
||||
gate.
|
||||
USAGE
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
|
||||
if [ "$#" -eq 0 ]; then
|
||||
if command -v git >/dev/null 2>&1 && git -C "$repo_root" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
set -- $(git -C "$repo_root" diff --name-only -- src src/integrationTest build.gradle.kts gradle.properties || true)
|
||||
fi
|
||||
fi
|
||||
|
||||
versions="1.19.2,1.21.11"
|
||||
reason="default modern coverage for uncertain changes"
|
||||
filters=""
|
||||
|
||||
for path in "$@"; do
|
||||
case "$path" in
|
||||
src/main/resources/config.yml|*config*|*modeset*|*Module*)
|
||||
versions="1.12,1.19.2,1.21.11"
|
||||
reason="config or module assignment changes benefit from legacy and modern coverage"
|
||||
filters="-Dkotest.filter.specs=*Module*,*Config*,*Modeset*"
|
||||
;;
|
||||
*Packet*|*packet*|*FakePlayer*|*fake-player*)
|
||||
versions="1.19.2,1.21.11"
|
||||
reason="packet or fake-player areas need modern Paper/PacketEvents coverage"
|
||||
;;
|
||||
*reflection*|*nms*|*legacy*|*1_12*)
|
||||
versions="1.12,1.19.2"
|
||||
reason="legacy or reflective code should include the Java 8-era path"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
printf 'Recommended versions: %s\n' "$versions"
|
||||
printf 'Reason: %s\n' "$reason"
|
||||
[ -z "$filters" ] || printf 'Suggested filter: %s\n' "$filters"
|
||||
printf 'Run with: scripts/run-integration-matrix.sh -v %s' "$versions"
|
||||
[ -z "$filters" ] || printf ' -- %s' "$filters"
|
||||
printf '\n'
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: scripts/summarise-integration-results.sh [-h]
|
||||
|
||||
Reports compact integration-test outcomes from run/* plugin result files.
|
||||
It deliberately does not open build/integration-test-logs/*.log. If compact
|
||||
results are insufficient, ask explicitly before inspecting full server logs.
|
||||
USAGE
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
'') ;;
|
||||
*)
|
||||
printf '%s\n' "Unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
found=0
|
||||
failed=0
|
||||
|
||||
for run_dir in "$repo_root"/run/*; do
|
||||
[ -d "$run_dir" ] || continue
|
||||
version=${run_dir##*/}
|
||||
result_file=$run_dir/plugins/OldCombatMechanicsTest/test-results.txt
|
||||
failures_file=$run_dir/plugins/OldCombatMechanicsTest/test-failures.txt
|
||||
|
||||
if [ ! -f "$result_file" ] && [ ! -f "$failures_file" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
found=1
|
||||
result="MISSING"
|
||||
[ ! -f "$result_file" ] || result=$(tr -d '\r' < "$result_file" | sed '/^[[:space:]]*$/d' | sed -n '1p')
|
||||
|
||||
printf '%s: %s\n' "$version" "$result"
|
||||
if [ "$result" != "PASS" ]; then
|
||||
failed=1
|
||||
fi
|
||||
|
||||
if [ -s "$failures_file" ]; then
|
||||
sed 's/^/ /' "$failures_file"
|
||||
failed=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$found" -eq 0 ]; then
|
||||
printf '%s\n' 'No compact integration-test result files found under run/*/plugins/OldCombatMechanicsTest/.' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit "$failed"
|
||||
Executable
+111
@@ -0,0 +1,111 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: scripts/validate-module-assignments.sh [-h] [config-file]
|
||||
|
||||
Best-effort static validation for bundled module assignment lists.
|
||||
Checks that every listed module is assigned to only one configurable top-level
|
||||
category: always_enabled_modules, disabled_modules, or the aggregate modesets
|
||||
category. It also checks duplicate entries within the same exact list or
|
||||
modeset, and that internal modules are not listed in those configurable
|
||||
sections. Reusing a module in more than one modeset is allowed because modesets
|
||||
are alternative player modes.
|
||||
|
||||
The default config file is src/main/resources/config.yml, resolved relative to
|
||||
the repository root. This script does not start servers.
|
||||
USAGE
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
repo_root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
config_file=${1:-"$repo_root/src/main/resources/config.yml"}
|
||||
|
||||
python3 - "$config_file" <<'PY'
|
||||
import re
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
config = Path(sys.argv[1])
|
||||
if not config.is_file():
|
||||
print(f"Config file not found: {config}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
internal = {"modeset-listener", "attack-cooldown-tracker", "entity-damage-listener"}
|
||||
assignments = defaultdict(list)
|
||||
section = None
|
||||
modeset = None
|
||||
|
||||
top_level = re.compile(r"^([A-Za-z0-9_.-]+):\s*(?:#.*)?$")
|
||||
mode_name = re.compile(r"^ ([A-Za-z0-9_.-]+):\s*(?:#.*)?$")
|
||||
list_item = re.compile(r"^\s*-\s*['\"]?([^'\"#\s]+)['\"]?\s*(?:#.*)?$")
|
||||
|
||||
for number, raw in enumerate(config.read_text(encoding="utf-8").splitlines(), 1):
|
||||
top = top_level.match(raw)
|
||||
if top:
|
||||
section = top.group(1)
|
||||
modeset = None
|
||||
continue
|
||||
|
||||
if section == "modesets":
|
||||
mode = mode_name.match(raw)
|
||||
if mode:
|
||||
modeset = mode.group(1)
|
||||
continue
|
||||
|
||||
item = list_item.match(raw)
|
||||
if not item:
|
||||
continue
|
||||
|
||||
module = item.group(1)
|
||||
if section == "always_enabled_modules":
|
||||
category = "always_enabled_modules"
|
||||
bucket = "always_enabled_modules"
|
||||
elif section == "disabled_modules":
|
||||
category = "disabled_modules"
|
||||
bucket = "disabled_modules"
|
||||
elif section == "modesets" and modeset:
|
||||
category = "modesets"
|
||||
bucket = f"modesets.{modeset}"
|
||||
else:
|
||||
continue
|
||||
assignments[module].append((category, bucket, number))
|
||||
|
||||
errors = []
|
||||
for module, locations in sorted(assignments.items()):
|
||||
if module in internal:
|
||||
for _category, bucket, number in locations:
|
||||
errors.append(f"internal module listed: {module} in {bucket} at line {number}")
|
||||
exact_locations = defaultdict(list)
|
||||
category_locations = defaultdict(list)
|
||||
for category, bucket, number in locations:
|
||||
exact_locations[bucket].append(number)
|
||||
category_locations[category].append((bucket, number))
|
||||
if len(category_locations) > 1:
|
||||
where = ", ".join(
|
||||
f"{bucket}:line {number}"
|
||||
for _category, bucket, number in locations
|
||||
)
|
||||
errors.append(f"conflicting module assignment: {module} in {where}")
|
||||
for bucket, numbers in sorted(exact_locations.items()):
|
||||
if len(numbers) > 1:
|
||||
where = ", ".join(f"line {number}" for number in numbers)
|
||||
errors.append(f"duplicate module assignment within {bucket}: {module} at {where}")
|
||||
|
||||
if errors:
|
||||
print("Module assignment validation FAILED:")
|
||||
for error in errors:
|
||||
print(f"- {error}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Module assignment validation passed for {config}")
|
||||
print(f"Checked {len(assignments)} assigned module entr{'y' if len(assignments) == 1 else 'ies'}.")
|
||||
PY
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
package kernitus.plugin.OldCombatMechanics.apiSmokeTest;
|
||||
|
||||
import kernitus.plugin.OldCombatMechanics.api.OldCombatMechanicsAPI;
|
||||
import kernitus.plugin.OldCombatMechanics.api.PlayerModesetChangeEvent;
|
||||
import kernitus.plugin.OldCombatMechanics.api.PlayerModuleOverride;
|
||||
import kernitus.plugin.OldCombatMechanics.api.PlayerModuleOverrideChangeEvent;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.RegisteredServiceProvider;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Set;
|
||||
|
||||
public final class ApiSmokeTestPlugin extends JavaPlugin {
|
||||
|
||||
private static final String KNOWN_CONFIGURABLE_MODULE = "sword-blocking";
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
try {
|
||||
RegisteredServiceProvider<OldCombatMechanicsAPI> registration =
|
||||
Bukkit.getServicesManager().getRegistration(OldCombatMechanicsAPI.class);
|
||||
if (registration == null) {
|
||||
throw new IllegalStateException("OldCombatMechanicsAPI service is not registered");
|
||||
}
|
||||
|
||||
OldCombatMechanicsAPI api = registration.getProvider();
|
||||
if (!api.getModuleNames().contains(KNOWN_CONFIGURABLE_MODULE)) {
|
||||
throw new IllegalStateException("OldCombatMechanicsAPI is missing known configurable module "
|
||||
+ KNOWN_CONFIGURABLE_MODULE);
|
||||
}
|
||||
Set<String> modesetNames = api.getModesetNames();
|
||||
if (modesetNames.isEmpty()) {
|
||||
throw new IllegalStateException("OldCombatMechanicsAPI did not expose any modesets");
|
||||
}
|
||||
World world = Bukkit.getWorlds().get(0);
|
||||
Set<String> allowedModesets = api.getAllowedModesets(world);
|
||||
if (allowedModesets.isEmpty()) {
|
||||
throw new IllegalStateException("OldCombatMechanicsAPI did not expose allowed modesets for "
|
||||
+ world.getName());
|
||||
}
|
||||
writeResult("PASS");
|
||||
} catch (Throwable throwable) {
|
||||
getLogger().severe("API smoke test failed: " + throwable.getMessage());
|
||||
try {
|
||||
writeResult("FAIL");
|
||||
} catch (IOException ioException) {
|
||||
getLogger().severe("Could not write API smoke test result: " + ioException.getMessage());
|
||||
}
|
||||
} finally {
|
||||
Bukkit.getScheduler().runTask(this, Bukkit::shutdown);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeResult(String result) throws IOException {
|
||||
File resultFile = new File(getDataFolder(), "test-results.txt");
|
||||
File parent = resultFile.getParentFile();
|
||||
if (parent != null) {
|
||||
parent.mkdirs();
|
||||
}
|
||||
Files.write(resultFile.toPath(), result.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private void verifyPlayerSpecificApiCompiles(OldCombatMechanicsAPI api, Player player) {
|
||||
api.forceEnableModuleForPlayer(player, KNOWN_CONFIGURABLE_MODULE);
|
||||
api.isModuleEnabledForPlayer(player, KNOWN_CONFIGURABLE_MODULE);
|
||||
api.clearModuleOverrideForPlayer(player, KNOWN_CONFIGURABLE_MODULE);
|
||||
api.setModuleOverridesForPlayer(player, java.util.Collections.singletonMap(
|
||||
KNOWN_CONFIGURABLE_MODULE,
|
||||
PlayerModuleOverride.FORCE_ENABLED
|
||||
));
|
||||
String currentModeset = api.getModesetForPlayer(player);
|
||||
if (currentModeset != null) {
|
||||
api.setModesetForPlayer(player, currentModeset);
|
||||
}
|
||||
PlayerModesetChangeEvent modesetEvent = new PlayerModesetChangeEvent(
|
||||
player,
|
||||
player.getWorld(),
|
||||
currentModeset,
|
||||
currentModeset,
|
||||
PlayerModesetChangeEvent.Reason.API
|
||||
);
|
||||
modesetEvent.getReason();
|
||||
PlayerModuleOverrideChangeEvent overrideEvent = new PlayerModuleOverrideChangeEvent(
|
||||
player,
|
||||
KNOWN_CONFIGURABLE_MODULE,
|
||||
PlayerModuleOverride.DEFAULT,
|
||||
PlayerModuleOverride.FORCE_ENABLED
|
||||
);
|
||||
overrideEvent.getNewOverride();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
main: kernitus.plugin.OldCombatMechanics.apiSmokeTest.ApiSmokeTestPlugin
|
||||
name: OldCombatMechanicsApiSmokeTest
|
||||
version: 1.0.0
|
||||
depend: [OldCombatMechanics]
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"replace": false,
|
||||
"values": [
|
||||
"ocmtest:tick_damage"
|
||||
]
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"message_id": "ocmtest.tick_damage",
|
||||
"scaling": "never",
|
||||
"exhaustion": 0.0
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"pack": {
|
||||
"pack_format": 81,
|
||||
"description": "OCM bypasses cooldown integration-test datapack"
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
package kernitus.plugin.OldCombatMechanics;
|
||||
|
||||
import kernitus.plugin.OldCombatMechanics.api.OldCombatMechanicsAPI;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
final class PlayerModuleOverrideJavaInterop {
|
||||
|
||||
private PlayerModuleOverrideJavaInterop() {
|
||||
}
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
static void setOverridesWithNullValue(OldCombatMechanicsAPI api, Player player, String moduleName) {
|
||||
Map overrides = new HashMap();
|
||||
overrides.put(moduleName, null);
|
||||
api.setModuleOverridesForPlayer(player, overrides);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@
|
||||
package kernitus.plugin.OldCombatMechanics
|
||||
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.CompatibilityCapabilities
|
||||
import org.bukkit.entity.Entity
|
||||
import org.bukkit.entity.LivingEntity
|
||||
import org.bukkit.entity.Player
|
||||
@@ -19,12 +20,13 @@ fun attackCompat(attacker: Player, target: Entity) {
|
||||
method.parameterCount == 1 &&
|
||||
Entity::class.java.isAssignableFrom(method.parameterTypes[0])
|
||||
}
|
||||
val useApiAttack = Reflector.versionIsNewerOrEqualTo(1, 12, 0)
|
||||
if (useApiAttack && apiAttack != null) {
|
||||
val useApiAttack = apiAttack != null && CompatibilityCapabilities.isPlayerAttackApiAvailable(attacker.javaClass)
|
||||
if (useApiAttack) {
|
||||
val attackMethod = apiAttack
|
||||
val beforeApiAttack = captureLivingAttackSignal(target)
|
||||
try {
|
||||
val apiResult = apiAttack.invoke(attacker, target)
|
||||
if (apiAttack.returnType == java.lang.Boolean.TYPE && apiResult == false) {
|
||||
val apiResult = attackMethod.invoke(attacker, target)
|
||||
if (attackMethod.returnType == java.lang.Boolean.TYPE && apiResult == false) {
|
||||
// Explicit attack failure; continue with NMS candidates.
|
||||
} else if (beforeApiAttack == null) {
|
||||
return
|
||||
@@ -39,7 +41,7 @@ fun attackCompat(attacker: Player, target: Entity) {
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to NMS attack on legacy servers.
|
||||
// Fall back to NMS attack when the Bukkit API attack path is absent or unobservable.
|
||||
val handleMethod = attacker.javaClass.methods.firstOrNull { method ->
|
||||
method.name == "getHandle" && method.parameterCount == 0
|
||||
} ?: error("Failed to resolve CraftPlayer#getHandle for ${attacker.javaClass.name}")
|
||||
@@ -71,7 +73,7 @@ fun attackCompat(attacker: Player, target: Entity) {
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy fallback: try Bukkit API even if we prefer NMS (helps 1.12 fake players)
|
||||
// Last-resort fallback: try Bukkit API even when the capability probe was inconclusive.
|
||||
if (!useApiAttack && apiAttack != null) {
|
||||
runCatching { apiAttack.invoke(attacker, target); return }
|
||||
}
|
||||
|
||||
+9
-6
@@ -240,17 +240,20 @@ class AttackCooldownHeldItemIntegrationTest :
|
||||
}
|
||||
runSync { currentAttackSpeed(spawned.player) } shouldBe (19.0 plusOrMinus 0.01)
|
||||
|
||||
var currentPlayer = spawned.player
|
||||
runSync {
|
||||
spawned.player.teleport(Location(otherWorld, 0.0, 100.0, 0.0, 0f, 0f))
|
||||
Bukkit.getPluginManager().callEvent(PlayerChangedWorldEvent(spawned.player, world))
|
||||
spawned.fake.teleport(Location(otherWorld, 0.0, 100.0, 0.0, 0f, 0f)) shouldBe true
|
||||
currentPlayer = spawned.fake.requireBukkitPlayer()
|
||||
Bukkit.getPluginManager().callEvent(PlayerChangedWorldEvent(currentPlayer, world))
|
||||
}
|
||||
runSync { currentAttackSpeed(spawned.player) } shouldBe (4.0 plusOrMinus 0.01)
|
||||
runSync { currentAttackSpeed(currentPlayer) } shouldBe (4.0 plusOrMinus 0.01)
|
||||
|
||||
runSync {
|
||||
spawned.player.teleport(Location(world, 0.0, 100.0, 0.0, 0f, 0f))
|
||||
Bukkit.getPluginManager().callEvent(PlayerChangedWorldEvent(spawned.player, otherWorld))
|
||||
spawned.fake.teleport(Location(world, 0.0, 100.0, 0.0, 0f, 0f)) shouldBe true
|
||||
currentPlayer = spawned.fake.requireBukkitPlayer()
|
||||
Bukkit.getPluginManager().callEvent(PlayerChangedWorldEvent(currentPlayer, otherWorld))
|
||||
}
|
||||
runSync { currentAttackSpeed(spawned.player) } shouldBe (19.0 plusOrMinus 0.01)
|
||||
runSync { currentAttackSpeed(currentPlayer) } shouldBe (19.0 plusOrMinus 0.01)
|
||||
} finally {
|
||||
cleanup(spawned)
|
||||
}
|
||||
|
||||
+9
-9
@@ -11,7 +11,6 @@ import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.doubles.shouldBeLessThan
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kernitus.plugin.OldCombatMechanics.module.ModuleAttackRange
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.GameMode
|
||||
import org.bukkit.Location
|
||||
@@ -39,6 +38,7 @@ class AttackRangeIntegrationTest :
|
||||
val testPlugin = JavaPlugin.getPlugin(OCMTestMain::class.java)
|
||||
val ocm = JavaPlugin.getPlugin(OCMMain::class.java)
|
||||
val attackRangeModule = ModuleLoader.getModules().filterIsInstance<ModuleAttackRange>().firstOrNull()
|
||||
val attackRangeApiAvailable = ModuleAttackRange.isAttackRangeApiAvailable()
|
||||
val applyMethod =
|
||||
attackRangeModule?.javaClass?.getDeclaredMethod("applyToHeld", Player::class.java)?.apply {
|
||||
isAccessible = true
|
||||
@@ -206,8 +206,8 @@ class AttackRangeIntegrationTest :
|
||||
}
|
||||
}
|
||||
|
||||
test("extended reach hits just outside vanilla range when module enabled (Paper 1.21.11+)") {
|
||||
if (!Reflector.versionIsNewerOrEqualTo(1, 21, 11)) return@test
|
||||
test("extended reach hits just outside vanilla range when module enabled (AttackRange API available)") {
|
||||
if (!attackRangeApiAvailable) return@test
|
||||
if (attackRangeModule == null) return@test
|
||||
|
||||
withModuleState(enabled = true, maxRange = 5.0, margin = 0.1) {
|
||||
@@ -230,8 +230,8 @@ class AttackRangeIntegrationTest :
|
||||
}
|
||||
}
|
||||
|
||||
test("reach boost is removed after disabling while holding the same item (Paper 1.21.11+)") {
|
||||
if (!Reflector.versionIsNewerOrEqualTo(1, 21, 11)) return@test
|
||||
test("reach boost is removed after disabling while holding the same item (AttackRange API available)") {
|
||||
if (!attackRangeApiAvailable) return@test
|
||||
if (attackRangeModule == null) return@test
|
||||
|
||||
val actors = spawnActors()
|
||||
@@ -262,8 +262,8 @@ class AttackRangeIntegrationTest :
|
||||
cleanup(actors)
|
||||
}
|
||||
|
||||
test("dropped items do not retain reach boost (Paper 1.21.11+)") {
|
||||
if (!Reflector.versionIsNewerOrEqualTo(1, 21, 11)) return@test
|
||||
test("dropped items do not retain reach boost (AttackRange API available)") {
|
||||
if (!attackRangeApiAvailable) return@test
|
||||
if (attackRangeModule == null) return@test
|
||||
|
||||
val actors = spawnActors()
|
||||
@@ -306,8 +306,8 @@ class AttackRangeIntegrationTest :
|
||||
cleanup(actors)
|
||||
}
|
||||
|
||||
test("extended reach does not apply when module disabled; close swing still hits (Paper 1.21.11+)") {
|
||||
if (!Reflector.versionIsNewerOrEqualTo(1, 21, 11)) return@test
|
||||
test("extended reach does not apply when module disabled; close swing still hits (AttackRange API available)") {
|
||||
if (!attackRangeApiAvailable) return@test
|
||||
if (attackRangeModule == null) return@test
|
||||
|
||||
withModuleState(enabled = false) {
|
||||
|
||||
+3
-3
@@ -12,6 +12,7 @@ import io.kotest.core.test.TestScope
|
||||
import io.kotest.matchers.booleans.shouldBeTrue
|
||||
import io.kotest.matchers.doubles.shouldBeLessThanOrEqual
|
||||
import kernitus.plugin.OldCombatMechanics.module.ModuleChorusFruit
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.CompatibilityCapabilities
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.GameMode
|
||||
import org.bukkit.Location
|
||||
@@ -103,9 +104,8 @@ class ChorusFruitIntegrationTest : FunSpec({
|
||||
val feet = block
|
||||
val head = feet.getRelative(BlockFace.UP)
|
||||
val below = feet.getRelative(BlockFace.DOWN)
|
||||
val legacy = !kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector.versionIsNewerOrEqualTo(1, 13, 0)
|
||||
val feetPassable = if (legacy) !feet.type.isSolid else feet.isPassable
|
||||
val headPassable = if (legacy) !head.type.isSolid else head.isPassable
|
||||
val feetPassable = CompatibilityCapabilities.isBlockPassable(feet)
|
||||
val headPassable = CompatibilityCapabilities.isBlockPassable(head)
|
||||
return feetPassable && headPassable && below.type.isSolid
|
||||
}
|
||||
|
||||
|
||||
+38
-1
@@ -74,12 +74,14 @@ class ConfigMigrationIntegrationTest : FunSpec({
|
||||
|
||||
oldConfig.set(
|
||||
"modesets",
|
||||
mapOf(
|
||||
linkedMapOf(
|
||||
"custom" to listOf("disable-offhand"),
|
||||
"alt" to listOf("old-golden-apples")
|
||||
)
|
||||
)
|
||||
oldConfig.set("worlds.__default__", null)
|
||||
oldConfig.set("worlds.world", listOf("custom", "alt"))
|
||||
oldConfig.set("worlds.nether", listOf("alt"))
|
||||
|
||||
oldConfig.set("disable-offhand.enabled", false)
|
||||
oldConfig.set("old-golden-apples.enabled", true)
|
||||
@@ -109,6 +111,41 @@ class ConfigMigrationIntegrationTest : FunSpec({
|
||||
upgradedConfig.getStringList("modesets.custom").shouldNotContain("disable-offhand")
|
||||
upgradedConfig.getStringList("modesets.alt").shouldContain("old-golden-apples")
|
||||
upgradedConfig.getStringList("worlds.world") shouldBe listOf("custom", "alt")
|
||||
upgradedConfig.getStringList("worlds.nether") shouldBe listOf("alt")
|
||||
upgradedConfig.getStringList("worlds.__default__") shouldBe listOf("custom", "alt")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("config upgrade preserves existing default world modesets") {
|
||||
runSync {
|
||||
withConfigFile {
|
||||
val configFile = File(ocm.dataFolder, "config.yml")
|
||||
val oldConfig = YamlConfiguration.loadConfiguration(configFile)
|
||||
val currentVersion = oldConfig.getInt("config-version")
|
||||
val oldVersion = currentVersion - 1
|
||||
|
||||
oldConfig.set("config-version", oldVersion)
|
||||
oldConfig.set("force-below-1-18-1-config-upgrade", true)
|
||||
|
||||
oldConfig.set(
|
||||
"modesets",
|
||||
linkedMapOf(
|
||||
"custom" to listOf("disable-offhand"),
|
||||
"alt" to listOf("old-golden-apples")
|
||||
)
|
||||
)
|
||||
oldConfig.set("worlds.__default__", listOf("legacy-default"))
|
||||
oldConfig.set("worlds.world", listOf("custom"))
|
||||
|
||||
oldConfig.save(configFile)
|
||||
|
||||
Config.reload()
|
||||
|
||||
val upgradedConfig = ocm.config
|
||||
upgradedConfig.getInt("config-version") shouldBe currentVersion
|
||||
upgradedConfig.getStringList("worlds.__default__") shouldBe listOf("legacy-default")
|
||||
upgradedConfig.getStringList("worlds.world") shouldBe listOf("custom")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-2
@@ -13,8 +13,8 @@ import io.kotest.matchers.doubles.plusOrMinus
|
||||
import io.kotest.matchers.doubles.shouldBeExactly
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kernitus.plugin.OldCombatMechanics.module.ModuleOldToolDamage
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.CompatibilityCapabilities
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.damage.WeaponDamages
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.GameMode
|
||||
import org.bukkit.Location
|
||||
@@ -108,6 +108,7 @@ class CustomWeaponDamageIntegrationTest : FunSpec({
|
||||
}
|
||||
|
||||
test("trident melee uses configured base damage") {
|
||||
if (!CompatibilityCapabilities.isMaterialAvailable("TRIDENT")) return@test
|
||||
val tridentMat = Material.matchMaterial("TRIDENT") ?: return@test
|
||||
withWeaponConfig(tridentMelee = 12.0, tridentThrown = null, mace = null) {
|
||||
val world = checkNotNull(Bukkit.getWorld("world"))
|
||||
@@ -139,8 +140,9 @@ class CustomWeaponDamageIntegrationTest : FunSpec({
|
||||
}
|
||||
|
||||
test("thrown trident uses configured damage") {
|
||||
if (!CompatibilityCapabilities.isMaterialAvailable("TRIDENT") ||
|
||||
!CompatibilityCapabilities.isBukkitClassAvailable("org.bukkit.entity.Trident")) return@test
|
||||
val tridentMat = Material.matchMaterial("TRIDENT") ?: return@test
|
||||
if (!Reflector.versionIsNewerOrEqualTo(1, 13, 0)) return@test
|
||||
withWeaponConfig(tridentMelee = null, tridentThrown = 15.0, mace = null) {
|
||||
val world = checkNotNull(Bukkit.getWorld("world"))
|
||||
val victim = spawnFake(Location(world, 0.0, 100.0, 0.0))
|
||||
@@ -177,6 +179,7 @@ class CustomWeaponDamageIntegrationTest : FunSpec({
|
||||
}
|
||||
|
||||
test("mace melee uses configured base damage") {
|
||||
if (!CompatibilityCapabilities.isMaterialAvailable("MACE")) return@test
|
||||
val maceMat = Material.matchMaterial("MACE") ?: return@test
|
||||
withWeaponConfig(tridentMelee = null, tridentThrown = null, mace = 10.0) {
|
||||
val world = checkNotNull(Bukkit.getWorld("world"))
|
||||
|
||||
@@ -139,6 +139,26 @@ class FakePlayer(private val plugin: JavaPlugin) {
|
||||
plugin.logger.info("Spawn: completed successfully")
|
||||
}
|
||||
|
||||
fun teleport(location: Location): Boolean {
|
||||
if (isLegacy9) {
|
||||
val teleported = legacyImpl9!!.teleport(location)
|
||||
if (teleported) {
|
||||
serverPlayer = legacyImpl9.entityPlayer ?: serverPlayer
|
||||
bukkitPlayer = legacyImpl9.bukkitPlayer ?: Bukkit.getPlayer(uuid)
|
||||
}
|
||||
return teleported
|
||||
}
|
||||
|
||||
val teleported = bukkitPlayer?.teleport(location) ?: false
|
||||
if (teleported) {
|
||||
bukkitPlayer = Bukkit.getPlayer(uuid) ?: bukkitPlayer
|
||||
}
|
||||
return teleported
|
||||
}
|
||||
|
||||
fun requireBukkitPlayer(): Player = Bukkit.getPlayer(uuid) ?: bukkitPlayer
|
||||
?: error("Bukkit player with UUID $uuid not found!")
|
||||
|
||||
private fun setupPlayerConnection(minecraftServer: Any, worldServer: Any) {
|
||||
// Access ServerGamePacketListenerImpl class
|
||||
val serverGamePacketListenerImplClass = getNMSClass("net.minecraft.server.network.ServerGamePacketListenerImpl")
|
||||
|
||||
+126
@@ -20,6 +20,7 @@ import org.bukkit.Bukkit
|
||||
import org.bukkit.GameMode
|
||||
import org.bukkit.Location
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.NamespacedKey
|
||||
import org.bukkit.attribute.AttributeModifier
|
||||
import org.bukkit.entity.LivingEntity
|
||||
import org.bukkit.entity.Player
|
||||
@@ -175,6 +176,77 @@ class InvulnerabilityDamageIntegrationTest : FunSpec({
|
||||
?: error("Weakness effect missing from potion meta")
|
||||
}
|
||||
|
||||
fun supportsDamageTypeApi(): Boolean {
|
||||
return try {
|
||||
Class.forName("org.bukkit.damage.DamageType")
|
||||
Class.forName("org.bukkit.damage.DamageSource")
|
||||
Class.forName("org.bukkit.Registry").getField("DAMAGE_TYPE")
|
||||
EntityDamageEvent::class.java.getMethod("getDamageSource")
|
||||
true
|
||||
} catch (_: ReflectiveOperationException) {
|
||||
false
|
||||
} catch (_: LinkageError) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun damageTypeClass(): Class<*> = Class.forName("org.bukkit.damage.DamageType")
|
||||
|
||||
fun resolveDamageType(key: NamespacedKey): Any {
|
||||
val registry = Class.forName("org.bukkit.Registry").getField("DAMAGE_TYPE").get(null)
|
||||
return registry.javaClass.getMethod("get", NamespacedKey::class.java).invoke(registry, key)
|
||||
?: error("Damage type $key was not registered")
|
||||
}
|
||||
|
||||
fun bypassesCooldownTagContains(damageType: Any): Boolean {
|
||||
val getTag = Bukkit::class.java.getMethod(
|
||||
"getTag",
|
||||
String::class.java,
|
||||
NamespacedKey::class.java,
|
||||
Class::class.java
|
||||
)
|
||||
val tag = listOf("damage_type", "damage_types").firstNotNullOfOrNull { registry ->
|
||||
try {
|
||||
getTag.invoke(null, registry, NamespacedKey.minecraft("bypasses_cooldown"), damageTypeClass())
|
||||
} catch (_: ReflectiveOperationException) {
|
||||
null
|
||||
} catch (_: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
} ?: error("minecraft:bypasses_cooldown damage-type tag was not available")
|
||||
val values = (tag as org.bukkit.Tag<*>).values
|
||||
return values.contains(damageType)
|
||||
}
|
||||
|
||||
fun buildDamageSource(damageType: Any): Any {
|
||||
val sourceClass = Class.forName("org.bukkit.damage.DamageSource")
|
||||
val builder = sourceClass.getMethod("builder", damageTypeClass()).invoke(null, damageType)
|
||||
return builder.javaClass.getMethod("build").invoke(builder)
|
||||
}
|
||||
|
||||
fun createDamageEvent(victim: LivingEntity, damageSource: Any, damage: Double): EntityDamageEvent {
|
||||
return EntityDamageEvent::class.java.getConstructor(
|
||||
org.bukkit.entity.Entity::class.java,
|
||||
EntityDamageEvent.DamageCause::class.java,
|
||||
Class.forName("org.bukkit.damage.DamageSource"),
|
||||
java.lang.Double.TYPE
|
||||
).newInstance(
|
||||
victim,
|
||||
EntityDamageEvent.DamageCause.CUSTOM,
|
||||
damageSource,
|
||||
damage
|
||||
) as EntityDamageEvent
|
||||
}
|
||||
|
||||
fun configuredDamageListener(): Pair<Listener, java.lang.reflect.Field> {
|
||||
val listener = HandlerList.getRegisteredListeners(ocm)
|
||||
.map { it.listener }
|
||||
.firstOrNull { it.javaClass.name.endsWith("EntityDamageByEntityListener") }
|
||||
?: error("EntityDamageByEntityListener not registered")
|
||||
val field = listener.javaClass.getDeclaredField("lastDamages").apply { isAccessible = true }
|
||||
return listener to field
|
||||
}
|
||||
|
||||
test("second hit in the same tick should still fire inside invulnerability").config(
|
||||
enabled = false
|
||||
) {
|
||||
@@ -251,6 +323,60 @@ class InvulnerabilityDamageIntegrationTest : FunSpec({
|
||||
}
|
||||
}
|
||||
|
||||
test("custom bypasses_cooldown damage type skips OCM invulnerability overdamage") {
|
||||
if (!supportsDamageTypeApi()) {
|
||||
plugin.logger.info("Skipping bypasses_cooldown damage-type test: Bukkit damage-source API is unavailable")
|
||||
return@test
|
||||
}
|
||||
|
||||
val tickDamageType = resolveDamageType(NamespacedKey("ocmtest", "tick_damage"))
|
||||
bypassesCooldownTagContains(tickDamageType).shouldBe(true)
|
||||
|
||||
val genericDamageType = resolveDamageType(NamespacedKey.minecraft("generic"))
|
||||
bypassesCooldownTagContains(genericDamageType).shouldBe(false)
|
||||
|
||||
var taggedVictim: LivingEntity? = null
|
||||
var controlVictim: LivingEntity? = null
|
||||
|
||||
try {
|
||||
runSync {
|
||||
val (edbelListener, lastDamagesField) = configuredDamageListener()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val lastDamages = lastDamagesField.get(edbelListener) as MutableMap<UUID, Double>
|
||||
|
||||
val world = checkNotNull(Bukkit.getWorld("world"))
|
||||
taggedVictim = spawnVictim(Location(world, 1.2, 100.0, 0.0)).apply {
|
||||
noDamageTicks = maximumNoDamageTicks
|
||||
lastDamage = 4.0
|
||||
}
|
||||
controlVictim = spawnVictim(Location(world, 3.2, 100.0, 0.0)).apply {
|
||||
noDamageTicks = maximumNoDamageTicks
|
||||
lastDamage = 4.0
|
||||
}
|
||||
|
||||
val currentTaggedVictim = checkNotNull(taggedVictim)
|
||||
val currentControlVictim = checkNotNull(controlVictim)
|
||||
lastDamages[currentTaggedVictim.uniqueId] = 4.0
|
||||
lastDamages[currentControlVictim.uniqueId] = 4.0
|
||||
|
||||
val taggedEvent = createDamageEvent(currentTaggedVictim, buildDamageSource(tickDamageType), 4.0)
|
||||
Bukkit.getPluginManager().callEvent(taggedEvent)
|
||||
taggedEvent.isCancelled.shouldBe(false)
|
||||
taggedEvent.damage.shouldBe(4.0)
|
||||
|
||||
val controlEvent = createDamageEvent(currentControlVictim, buildDamageSource(genericDamageType), 4.0)
|
||||
Bukkit.getPluginManager().callEvent(controlEvent)
|
||||
controlEvent.isCancelled.shouldBe(true)
|
||||
controlEvent.damage.shouldBe(0.0)
|
||||
}
|
||||
} finally {
|
||||
runSync {
|
||||
taggedVictim?.remove()
|
||||
controlVictim?.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("slightly higher base damage inside invulnerability should fire")
|
||||
.config(enabled = false) {
|
||||
// Ignored for now: behaviour differs across versions and is not addressed yet.
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
@file:Suppress("ktlint:standard:package-name")
|
||||
|
||||
package kernitus.plugin.OldCombatMechanics
|
||||
|
||||
import com.github.ajalt.mordant.TermColors
|
||||
@@ -19,12 +21,12 @@ import io.kotest.engine.TestEngineLauncher
|
||||
import io.kotest.engine.listener.AbstractTestEngineListener
|
||||
import io.kotest.engine.listener.CompositeTestEngineListener
|
||||
import io.kotest.engine.listener.EnhancedConsoleTestEngineListener
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.coroutineContext
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.coroutineContext
|
||||
|
||||
@OptIn(ExperimentalKotest::class)
|
||||
object KotestProjectConfig : AbstractProjectConfig() {
|
||||
@@ -35,22 +37,22 @@ object KotestProjectConfig : AbstractProjectConfig() {
|
||||
}
|
||||
|
||||
class BukkitMainThreadDispatcher(
|
||||
private val plugin: JavaPlugin,
|
||||
private val plugin: JavaPlugin
|
||||
) : CoroutineDispatcher() {
|
||||
override fun dispatch(
|
||||
context: CoroutineContext,
|
||||
block: Runnable,
|
||||
block: Runnable
|
||||
) {
|
||||
Bukkit.getScheduler().runTask(plugin, block)
|
||||
}
|
||||
}
|
||||
|
||||
class MainThreadDispatcherExtension(
|
||||
private val plugin: JavaPlugin,
|
||||
private val plugin: JavaPlugin
|
||||
) : TestCaseExtension {
|
||||
override suspend fun intercept(
|
||||
testCase: TestCase,
|
||||
execute: suspend (TestCase) -> TestResult,
|
||||
execute: suspend (TestCase) -> TestResult
|
||||
): TestResult {
|
||||
val dispatcher = BukkitMainThreadDispatcher(plugin)
|
||||
val newContext = coroutineContext + dispatcher
|
||||
@@ -79,10 +81,12 @@ object KotestRunner {
|
||||
"getErrorOrNull",
|
||||
"getCauseOrNull",
|
||||
"getThrowableOrNull",
|
||||
"getFailureOrNull",
|
||||
"getFailureOrNull"
|
||||
)
|
||||
for (getter in candidateGetters) {
|
||||
val m = result::class.java.methods.firstOrNull { it.name == getter && it.parameterCount == 0 } ?: continue
|
||||
val m =
|
||||
result::class.java.methods.firstOrNull { it.name == getter && it.parameterCount == 0 }
|
||||
?: continue
|
||||
val t = runCatching { m.invoke(result) }.getOrNull() as? Throwable
|
||||
if (t != null) return t
|
||||
}
|
||||
@@ -91,7 +95,7 @@ object KotestRunner {
|
||||
|
||||
fun formatFailure(
|
||||
testCase: TestCase,
|
||||
result: TestResult,
|
||||
result: TestResult
|
||||
): String {
|
||||
val specName = testCase.spec::class.qualifiedName ?: testCase.spec::class.java.name
|
||||
val testName = testCase.displayName
|
||||
@@ -104,9 +108,19 @@ object KotestRunner {
|
||||
?.firstOrNull()
|
||||
?.trim()
|
||||
.orEmpty()
|
||||
val head = if (message.isNotEmpty()) "${t::class.java.simpleName}: $message" else t::class.java.simpleName
|
||||
val head =
|
||||
if (message.isNotEmpty()) {
|
||||
"${t::class.java.simpleName}: $message"
|
||||
} else {
|
||||
t::class.java.simpleName
|
||||
}
|
||||
val frame = t.stackTrace.firstOrNull()
|
||||
val at = if (frame != null) " (${frame.fileName}:${frame.lineNumber})" else ""
|
||||
val at =
|
||||
if (frame != null) {
|
||||
" (${frame.fileName}:${frame.lineNumber})"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
return "$specName, $testName -- $head$at"
|
||||
}
|
||||
|
||||
@@ -114,7 +128,7 @@ object KotestRunner {
|
||||
object : AbstractTestEngineListener() {
|
||||
override suspend fun testFinished(
|
||||
testCase: TestCase,
|
||||
result: TestResult,
|
||||
result: TestResult
|
||||
) {
|
||||
if (result.isFailure || result.isError) {
|
||||
hasFailures = true
|
||||
@@ -135,8 +149,8 @@ object KotestRunner {
|
||||
CompositeTestEngineListener(
|
||||
listOf(
|
||||
EnhancedConsoleTestEngineListener(TermColors()),
|
||||
listener,
|
||||
),
|
||||
listener
|
||||
)
|
||||
)
|
||||
|
||||
TestEngineLauncher()
|
||||
@@ -145,6 +159,7 @@ object KotestRunner {
|
||||
.withClasses(
|
||||
ConfigMigrationIntegrationTest::class,
|
||||
ModesetRulesIntegrationTest::class,
|
||||
PlayerModuleOverrideApiIntegrationTest::class,
|
||||
DisableOffhandIntegrationTest::class,
|
||||
DisableOffhandReflectionIntegrationTest::class,
|
||||
InGameTesterIntegrationTest::class,
|
||||
@@ -157,6 +172,8 @@ object KotestRunner {
|
||||
WeaponDurabilityIntegrationTest::class,
|
||||
GoldenAppleIntegrationTest::class,
|
||||
OldArmourDurabilityIntegrationTest::class,
|
||||
OldArmourStrengthModesetIntegrationTest::class,
|
||||
MixedModePvPIntegrationTest::class,
|
||||
PlayerKnockbackIntegrationTest::class,
|
||||
AttackCooldownTrackerIntegrationTest::class,
|
||||
AttackCooldownHeldItemIntegrationTest::class,
|
||||
@@ -172,13 +189,13 @@ object KotestRunner {
|
||||
SwordBlockingIntegrationTest::class,
|
||||
ConsumableComponentIntegrationTest::class,
|
||||
PaperSwordBlockingDamageReductionIntegrationTest::class,
|
||||
AttackRangeIntegrationTest::class,
|
||||
AttackRangeIntegrationTest::class
|
||||
).launch()
|
||||
} catch (e: Throwable) {
|
||||
plugin.logger.severe("Failed to execute Kotest runner: ${e.message}")
|
||||
TestResultWriter.writeAndShutdown(plugin, false, e)
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -234,6 +234,41 @@ internal class LegacyFakePlayer9(
|
||||
}
|
||||
}
|
||||
|
||||
fun teleport(location: Location): Boolean {
|
||||
val ep = entityPlayer ?: return false
|
||||
val bp = bukkitPlayer ?: return false
|
||||
val targetWorld = location.world ?: return false
|
||||
|
||||
if (bp.world.uid == targetWorld.uid) {
|
||||
return bp.teleport(location)
|
||||
}
|
||||
|
||||
val craftWorld = craft("CraftWorld").cast(targetWorld)
|
||||
val targetWorldServer = craftWorld.javaClass.getMethod("getHandle").invoke(craftWorld)
|
||||
val dimension = targetWorldServer.javaClass.getField("dimension").getInt(targetWorldServer)
|
||||
|
||||
val craftServer = craft("CraftServer").cast(Bukkit.getServer())
|
||||
val mcServer = craftServer.javaClass.getMethod("getServer").invoke(craftServer)
|
||||
val playerList = mcServer.javaClass.getMethod("getPlayerList").invoke(mcServer)
|
||||
|
||||
val moveMethod =
|
||||
playerList.javaClass.methods.firstOrNull {
|
||||
it.name == "moveToWorld" &&
|
||||
it.parameterTypes.size == 5 &&
|
||||
it.parameterTypes[0].isAssignableFrom(ep.javaClass) &&
|
||||
it.parameterTypes[1] == Int::class.javaPrimitiveType &&
|
||||
it.parameterTypes[2] == Boolean::class.javaPrimitiveType &&
|
||||
it.parameterTypes[3].isAssignableFrom(Location::class.java) &&
|
||||
it.parameterTypes[4] == Boolean::class.javaPrimitiveType
|
||||
} ?: return false
|
||||
|
||||
val moved = moveMethod.invoke(playerList, ep, dimension, false, location, true) ?: return false
|
||||
entityPlayer = moved
|
||||
bukkitPlayer = moved.javaClass.getMethod("getBukkitEntity").invoke(moved) as Player
|
||||
updateCraftMaps(craftServer, bukkitPlayer!!)
|
||||
return bukkitPlayer!!.world.uid == targetWorld.uid
|
||||
}
|
||||
|
||||
fun getConnection(serverPlayer: Any): Any {
|
||||
val field = serverPlayer.javaClass.getField("playerConnection")
|
||||
return field.get(serverPlayer)
|
||||
|
||||
+1002
File diff suppressed because it is too large.
Load diff
+546
-232
@@ -4,287 +4,601 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
@file:Suppress("ktlint:standard:package-name")
|
||||
|
||||
package kernitus.plugin.OldCombatMechanics
|
||||
|
||||
import io.kotest.assertions.throwables.shouldThrow
|
||||
import io.kotest.common.ExperimentalKotest
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.booleans.shouldBeFalse
|
||||
import io.kotest.matchers.booleans.shouldBeTrue
|
||||
import io.kotest.assertions.throwables.shouldThrow
|
||||
import io.kotest.matchers.collections.shouldContain
|
||||
import io.kotest.matchers.collections.shouldContainExactly
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kernitus.plugin.OldCombatMechanics.api.OldCombatMechanicsAPI
|
||||
import kernitus.plugin.OldCombatMechanics.api.PlayerModesetChangeEvent
|
||||
import kernitus.plugin.OldCombatMechanics.commands.OCMCommandCompleter
|
||||
import kernitus.plugin.OldCombatMechanics.module.ModuleDisableOffHand
|
||||
import kernitus.plugin.OldCombatMechanics.module.ModuleSwordBlocking
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.Config
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.storage.PlayerStorage
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.storage.PlayerStorage.getPlayerData
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.storage.PlayerStorage.setPlayerData
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.Location
|
||||
import org.bukkit.configuration.ConfigurationSection
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.event.EventHandler
|
||||
import org.bukkit.event.HandlerList
|
||||
import org.bukkit.event.Listener
|
||||
import org.bukkit.event.player.PlayerJoinEvent
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import java.util.concurrent.Callable
|
||||
|
||||
@OptIn(ExperimentalKotest::class)
|
||||
class ModesetRulesIntegrationTest : FunSpec({
|
||||
val testPlugin = JavaPlugin.getPlugin(OCMTestMain::class.java)
|
||||
val ocm = JavaPlugin.getPlugin(OCMMain::class.java)
|
||||
val module = ModuleLoader.getModules()
|
||||
.filterIsInstance<ModuleDisableOffHand>()
|
||||
.firstOrNull() ?: error("ModuleDisableOffHand not registered")
|
||||
val internalModules = setOf(
|
||||
"modeset-listener",
|
||||
"attack-cooldown-tracker",
|
||||
"entity-damage-listener"
|
||||
)
|
||||
val optionalModules = setOf(
|
||||
"disable-attack-sounds",
|
||||
"disable-sword-sweep-particles"
|
||||
)
|
||||
class ModesetRulesIntegrationTest :
|
||||
FunSpec({
|
||||
val testPlugin = JavaPlugin.getPlugin(OCMTestMain::class.java)
|
||||
val ocm = JavaPlugin.getPlugin(OCMMain::class.java)
|
||||
val api = Bukkit.getServicesManager().getRegistration(OldCombatMechanicsAPI::class.java)?.provider
|
||||
?: error("OldCombatMechanicsAPI service not registered")
|
||||
val module =
|
||||
ModuleLoader
|
||||
.getModules()
|
||||
.filterIsInstance<ModuleDisableOffHand>()
|
||||
.firstOrNull() ?: error("ModuleDisableOffHand not registered")
|
||||
val swordBlockingModule =
|
||||
ModuleLoader
|
||||
.getModules()
|
||||
.filterIsInstance<ModuleSwordBlocking>()
|
||||
.firstOrNull() ?: error("ModuleSwordBlocking not registered")
|
||||
val internalModules =
|
||||
setOf(
|
||||
"modeset-listener",
|
||||
"attack-cooldown-tracker",
|
||||
"entity-damage-listener"
|
||||
)
|
||||
val optionalModules =
|
||||
setOf(
|
||||
"disable-attack-sounds",
|
||||
"disable-sword-sweep-particles"
|
||||
)
|
||||
|
||||
lateinit var player: Player
|
||||
lateinit var fakePlayer: FakePlayer
|
||||
lateinit var player: Player
|
||||
lateinit var fakePlayer: FakePlayer
|
||||
|
||||
extensions(MainThreadDispatcherExtension(testPlugin))
|
||||
extensions(MainThreadDispatcherExtension(testPlugin))
|
||||
|
||||
fun runSync(action: () -> Unit) {
|
||||
if (Bukkit.isPrimaryThread()) {
|
||||
action()
|
||||
} else {
|
||||
Bukkit.getScheduler().callSyncMethod(testPlugin, Callable {
|
||||
fun runSync(action: () -> Unit) {
|
||||
if (Bukkit.isPrimaryThread()) {
|
||||
action()
|
||||
null
|
||||
}).get()
|
||||
}
|
||||
}
|
||||
|
||||
fun setModeset(player: Player, modeset: String) {
|
||||
val playerData = getPlayerData(player.uniqueId)
|
||||
playerData.setModesetForWorld(player.world.uid, modeset)
|
||||
setPlayerData(player.uniqueId, playerData)
|
||||
}
|
||||
|
||||
fun snapshotSection(path: String): Any? {
|
||||
val section = ocm.config.getConfigurationSection(path)
|
||||
return section?.getValues(false) ?: ocm.config.get(path)
|
||||
}
|
||||
|
||||
fun restoreSection(path: String, value: Any?) {
|
||||
ocm.config.set(path, null)
|
||||
when (value) {
|
||||
null -> Unit
|
||||
is Map<*, *> -> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
ocm.config.createSection(path, value as Map<String, Any?>)
|
||||
}
|
||||
else -> ocm.config.set(path, value)
|
||||
}
|
||||
}
|
||||
|
||||
fun applyConfig(
|
||||
always: List<String>,
|
||||
disabled: List<String>,
|
||||
modesets: Map<String, List<String>>,
|
||||
worldModesets: List<String>
|
||||
) {
|
||||
ocm.config.set("always_enabled_modules", always)
|
||||
ocm.config.set("disabled_modules", disabled)
|
||||
ocm.config.set("modesets", null)
|
||||
ocm.config.createSection("modesets", modesets)
|
||||
ocm.config.set("worlds.world", worldModesets)
|
||||
ocm.saveConfig()
|
||||
Config.reload()
|
||||
}
|
||||
|
||||
fun completeAlways(
|
||||
always: List<String>,
|
||||
disabled: List<String>,
|
||||
modesets: Map<String, List<String>>
|
||||
): List<String> {
|
||||
val assigned = HashSet<String>()
|
||||
always.forEach { assigned.add(it.lowercase(Locale.ROOT)) }
|
||||
disabled.forEach { assigned.add(it.lowercase(Locale.ROOT)) }
|
||||
modesets.values.flatten().forEach { assigned.add(it.lowercase(Locale.ROOT)) }
|
||||
|
||||
val filled = LinkedHashSet<String>()
|
||||
filled.addAll(always)
|
||||
ModuleLoader.getModules()
|
||||
.map { it.configName.lowercase(Locale.ROOT) }
|
||||
.sorted()
|
||||
.filterNot { assigned.contains(it) }
|
||||
.filterNot { internalModules.contains(it) }
|
||||
.forEach { filled.add(it) }
|
||||
optionalModules
|
||||
.filterNot { assigned.contains(it) }
|
||||
.forEach { filled.add(it) }
|
||||
return filled.toList()
|
||||
}
|
||||
|
||||
suspend fun withConfig(block: suspend () -> Unit) {
|
||||
val originalAlways = ocm.config.get("always_enabled_modules")
|
||||
val originalDisabled = ocm.config.get("disabled_modules")
|
||||
val originalModesets = snapshotSection("modesets")
|
||||
val originalWorlds = snapshotSection("worlds")
|
||||
|
||||
try {
|
||||
block()
|
||||
} finally {
|
||||
runSync {
|
||||
ocm.config.set("always_enabled_modules", originalAlways)
|
||||
ocm.config.set("disabled_modules", originalDisabled)
|
||||
restoreSection("modesets", originalModesets)
|
||||
restoreSection("worlds", originalWorlds)
|
||||
ocm.saveConfig()
|
||||
Config.reload()
|
||||
} else {
|
||||
Bukkit
|
||||
.getScheduler()
|
||||
.callSyncMethod(
|
||||
testPlugin,
|
||||
Callable {
|
||||
action()
|
||||
null
|
||||
}
|
||||
).get()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
beforeSpec {
|
||||
runSync {
|
||||
val world = checkNotNull(Bukkit.getServer().getWorld("world"))
|
||||
fakePlayer = FakePlayer(testPlugin)
|
||||
fakePlayer.spawn(Location(world, 0.0, 100.0, 0.0))
|
||||
player = checkNotNull(Bukkit.getPlayer(fakePlayer.uuid))
|
||||
fun setModeset(
|
||||
player: Player,
|
||||
modeset: String
|
||||
) {
|
||||
val playerData = getPlayerData(player.uniqueId)
|
||||
playerData.setModesetForWorld(player.world.uid, modeset)
|
||||
setPlayerData(player.uniqueId, playerData)
|
||||
}
|
||||
}
|
||||
|
||||
afterSpec {
|
||||
runSync {
|
||||
fakePlayer.removePlayer()
|
||||
fun snapshotSection(path: String): Any? {
|
||||
val section = ocm.config.getConfigurationSection(path)
|
||||
return section?.getValues(false) ?: ocm.config.get(path)
|
||||
}
|
||||
}
|
||||
|
||||
test("always-enabled modules apply regardless of modeset") {
|
||||
withConfig {
|
||||
runSync {
|
||||
applyConfig(
|
||||
always = completeAlways(
|
||||
always = listOf("disable-offhand"),
|
||||
disabled = emptyList(),
|
||||
modesets = mapOf(
|
||||
"old" to listOf("old-golden-apples"),
|
||||
"new" to listOf("old-potion-effects")
|
||||
)
|
||||
),
|
||||
disabled = emptyList(),
|
||||
modesets = mapOf(
|
||||
"old" to listOf("old-golden-apples"),
|
||||
"new" to listOf("old-potion-effects")
|
||||
),
|
||||
worldModesets = listOf("old", "new")
|
||||
fun restoreSection(
|
||||
path: String,
|
||||
value: Any?
|
||||
) {
|
||||
ocm.config.set(path, null)
|
||||
when (value) {
|
||||
null -> {
|
||||
Unit
|
||||
}
|
||||
|
||||
is Map<*, *> -> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
ocm.config.createSection(path, value as Map<String, Any?>)
|
||||
}
|
||||
|
||||
else -> {
|
||||
ocm.config.set(path, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun applyConfigWithWorlds(
|
||||
always: List<String>,
|
||||
disabled: List<String>,
|
||||
modesets: Map<String, List<String>>,
|
||||
worlds: Map<String, List<String>>
|
||||
) {
|
||||
ocm.config.set("always_enabled_modules", always)
|
||||
ocm.config.set("disabled_modules", disabled)
|
||||
ocm.config.set("modesets", null)
|
||||
ocm.config.createSection("modesets", modesets)
|
||||
ocm.config.set("worlds", null)
|
||||
ocm.config.createSection("worlds", worlds)
|
||||
ocm.saveConfig()
|
||||
Config.reload()
|
||||
}
|
||||
|
||||
fun applyConfig(
|
||||
always: List<String>,
|
||||
disabled: List<String>,
|
||||
modesets: Map<String, List<String>>,
|
||||
worldModesets: List<String>
|
||||
) {
|
||||
applyConfigWithWorlds(
|
||||
always = always,
|
||||
disabled = disabled,
|
||||
modesets = modesets,
|
||||
worlds = mapOf("world" to worldModesets)
|
||||
)
|
||||
}
|
||||
|
||||
fun completeAlways(
|
||||
always: List<String>,
|
||||
disabled: List<String>,
|
||||
modesets: Map<String, List<String>>
|
||||
): List<String> {
|
||||
val assigned = HashSet<String>()
|
||||
always.forEach { assigned.add(it.lowercase(Locale.ROOT)) }
|
||||
disabled.forEach { assigned.add(it.lowercase(Locale.ROOT)) }
|
||||
modesets.values.flatten().forEach { assigned.add(it.lowercase(Locale.ROOT)) }
|
||||
|
||||
val filled = LinkedHashSet<String>()
|
||||
filled.addAll(always)
|
||||
ModuleLoader
|
||||
.getModules()
|
||||
.map { it.configName.lowercase(Locale.ROOT) }
|
||||
.sorted()
|
||||
.filterNot { assigned.contains(it) }
|
||||
.filterNot { internalModules.contains(it) }
|
||||
.forEach { filled.add(it) }
|
||||
optionalModules
|
||||
.filterNot { assigned.contains(it) }
|
||||
.forEach { filled.add(it) }
|
||||
return filled.toList()
|
||||
}
|
||||
|
||||
fun applySimpleModesetWorlds(worlds: Map<String, List<String>>) {
|
||||
val modesets =
|
||||
mapOf(
|
||||
"old" to listOf("disable-offhand"),
|
||||
"new" to listOf("old-potion-effects")
|
||||
)
|
||||
|
||||
setModeset(player, "old")
|
||||
module.isEnabled(player).shouldBeTrue()
|
||||
|
||||
setModeset(player, "new")
|
||||
module.isEnabled(player).shouldBeTrue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("disabled modules never apply") {
|
||||
withConfig {
|
||||
runSync {
|
||||
applyConfig(
|
||||
always = completeAlways(
|
||||
always = emptyList(),
|
||||
disabled = listOf("disable-offhand"),
|
||||
modesets = mapOf(
|
||||
"old" to listOf("old-golden-apples"),
|
||||
"new" to listOf("old-potion-effects")
|
||||
)
|
||||
),
|
||||
disabled = listOf("disable-offhand"),
|
||||
modesets = mapOf(
|
||||
"old" to listOf("old-golden-apples"),
|
||||
"new" to listOf("old-potion-effects")
|
||||
),
|
||||
worldModesets = listOf("old", "new")
|
||||
)
|
||||
|
||||
setModeset(player, "old")
|
||||
module.isEnabled(player).shouldBeFalse()
|
||||
|
||||
setModeset(player, "new")
|
||||
module.isEnabled(player).shouldBeFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("modeset membership controls module activation") {
|
||||
withConfig {
|
||||
runSync {
|
||||
applyConfig(
|
||||
always = completeAlways(
|
||||
applyConfigWithWorlds(
|
||||
always =
|
||||
completeAlways(
|
||||
always = emptyList(),
|
||||
disabled = emptyList(),
|
||||
modesets = mapOf(
|
||||
"old" to listOf("disable-offhand"),
|
||||
"new" to listOf("old-potion-effects")
|
||||
)
|
||||
modesets = modesets
|
||||
),
|
||||
disabled = emptyList(),
|
||||
modesets = mapOf(
|
||||
"old" to listOf("disable-offhand"),
|
||||
"new" to listOf("old-potion-effects")
|
||||
),
|
||||
worldModesets = listOf("old", "new")
|
||||
)
|
||||
disabled = emptyList(),
|
||||
modesets = modesets,
|
||||
worlds = worlds
|
||||
)
|
||||
}
|
||||
|
||||
setModeset(player, "old")
|
||||
module.isEnabled(player).shouldBeTrue()
|
||||
fun clearPendingSaveTask() {
|
||||
val field = PlayerStorage::class.java.getDeclaredField("saveTask")
|
||||
field.isAccessible = true
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val saveTask = field.get(null) as AtomicReference<Any?>
|
||||
val pending = saveTask.getAndSet(null)
|
||||
pending?.javaClass?.getMethod("cancel")?.invoke(pending)
|
||||
}
|
||||
|
||||
setModeset(player, "new")
|
||||
module.isEnabled(player).shouldBeFalse()
|
||||
suspend fun withConfig(block: suspend () -> Unit) {
|
||||
val originalAlways = ocm.config.get("always_enabled_modules")
|
||||
val originalDisabled = ocm.config.get("disabled_modules")
|
||||
val originalModesets = snapshotSection("modesets")
|
||||
val originalWorlds = snapshotSection("worlds")
|
||||
|
||||
try {
|
||||
block()
|
||||
} finally {
|
||||
runSync {
|
||||
ocm.config.set("always_enabled_modules", originalAlways)
|
||||
ocm.config.set("disabled_modules", originalDisabled)
|
||||
restoreSection("modesets", originalModesets)
|
||||
restoreSection("worlds", originalWorlds)
|
||||
ocm.saveConfig()
|
||||
Config.reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("modules in disabled and another list fail reload") {
|
||||
withConfig {
|
||||
beforeSpec {
|
||||
runSync {
|
||||
shouldThrow<IllegalStateException> {
|
||||
val world = checkNotNull(Bukkit.getServer().getWorld("world"))
|
||||
fakePlayer = FakePlayer(testPlugin)
|
||||
fakePlayer.spawn(Location(world, 0.0, 100.0, 0.0))
|
||||
player = checkNotNull(Bukkit.getPlayer(fakePlayer.uuid))
|
||||
}
|
||||
}
|
||||
|
||||
afterSpec {
|
||||
runSync {
|
||||
fakePlayer.removePlayer()
|
||||
}
|
||||
}
|
||||
|
||||
test("always-enabled modules apply regardless of modeset") {
|
||||
withConfig {
|
||||
runSync {
|
||||
applyConfig(
|
||||
always = completeAlways(
|
||||
always = listOf("disable-offhand"),
|
||||
disabled = listOf("disable-offhand"),
|
||||
modesets = mapOf(
|
||||
"old" to listOf("old-potion-effects"),
|
||||
"new" to listOf("old-golden-apples")
|
||||
)
|
||||
),
|
||||
disabled = listOf("disable-offhand"),
|
||||
modesets = mapOf(
|
||||
"old" to listOf("old-potion-effects"),
|
||||
"new" to listOf("old-golden-apples")
|
||||
),
|
||||
always =
|
||||
completeAlways(
|
||||
always = listOf("disable-offhand"),
|
||||
disabled = emptyList(),
|
||||
modesets =
|
||||
mapOf(
|
||||
"old" to listOf("old-golden-apples"),
|
||||
"new" to listOf("old-potion-effects")
|
||||
)
|
||||
),
|
||||
disabled = emptyList(),
|
||||
modesets =
|
||||
mapOf(
|
||||
"old" to listOf("old-golden-apples"),
|
||||
"new" to listOf("old-potion-effects")
|
||||
),
|
||||
worldModesets = listOf("old", "new")
|
||||
)
|
||||
|
||||
setModeset(player, "old")
|
||||
module.isEnabled(player).shouldBeTrue()
|
||||
|
||||
setModeset(player, "new")
|
||||
module.isEnabled(player).shouldBeTrue()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("modules missing from all lists fail reload") {
|
||||
withConfig {
|
||||
runSync {
|
||||
val moduleNames = (ModuleLoader.getModules()
|
||||
.map { it.configName }
|
||||
.filterNot { internalModules.contains(it) } + optionalModules)
|
||||
.distinct()
|
||||
.sorted()
|
||||
val missing = moduleNames.firstOrNull() ?: error("No modules registered")
|
||||
val always = moduleNames.filterNot { it == missing }
|
||||
|
||||
shouldThrow<IllegalStateException> {
|
||||
test("disabled modules never apply") {
|
||||
withConfig {
|
||||
runSync {
|
||||
applyConfig(
|
||||
always = always,
|
||||
disabled = emptyList(),
|
||||
modesets = mapOf("old" to emptyList()),
|
||||
worldModesets = listOf("old")
|
||||
always =
|
||||
completeAlways(
|
||||
always = emptyList(),
|
||||
disabled = listOf("disable-offhand"),
|
||||
modesets =
|
||||
mapOf(
|
||||
"old" to listOf("old-golden-apples"),
|
||||
"new" to listOf("old-potion-effects")
|
||||
)
|
||||
),
|
||||
disabled = listOf("disable-offhand"),
|
||||
modesets =
|
||||
mapOf(
|
||||
"old" to listOf("old-golden-apples"),
|
||||
"new" to listOf("old-potion-effects")
|
||||
),
|
||||
worldModesets = listOf("old", "new")
|
||||
)
|
||||
|
||||
setModeset(player, "old")
|
||||
module.isEnabled(player).shouldBeFalse()
|
||||
|
||||
setModeset(player, "new")
|
||||
module.isEnabled(player).shouldBeFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test("modeset membership controls module activation") {
|
||||
withConfig {
|
||||
runSync {
|
||||
applyConfig(
|
||||
always =
|
||||
completeAlways(
|
||||
always = emptyList(),
|
||||
disabled = emptyList(),
|
||||
modesets =
|
||||
mapOf(
|
||||
"old" to listOf("disable-offhand"),
|
||||
"new" to listOf("old-potion-effects")
|
||||
)
|
||||
),
|
||||
disabled = emptyList(),
|
||||
modesets =
|
||||
mapOf(
|
||||
"old" to listOf("disable-offhand"),
|
||||
"new" to listOf("old-potion-effects")
|
||||
),
|
||||
worldModesets = listOf("old", "new")
|
||||
)
|
||||
|
||||
setModeset(player, "old")
|
||||
module.isEnabled(player).shouldBeTrue()
|
||||
|
||||
setModeset(player, "new")
|
||||
module.isEnabled(player).shouldBeFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("stale stored modeset is not trusted when current world only allows another modeset") {
|
||||
withConfig {
|
||||
runSync {
|
||||
applyConfig(
|
||||
always =
|
||||
completeAlways(
|
||||
always = emptyList(),
|
||||
disabled = emptyList(),
|
||||
modesets =
|
||||
mapOf(
|
||||
"old" to listOf("sword-blocking"),
|
||||
"new" to listOf("old-potion-effects")
|
||||
)
|
||||
),
|
||||
disabled = emptyList(),
|
||||
modesets =
|
||||
mapOf(
|
||||
"old" to listOf("sword-blocking"),
|
||||
"new" to listOf("old-potion-effects")
|
||||
),
|
||||
worldModesets = listOf("new")
|
||||
)
|
||||
|
||||
setModeset(player, "old")
|
||||
Bukkit.getPluginManager().callEvent(PlayerJoinEvent(player, ""))
|
||||
|
||||
getPlayerData(player.uniqueId).getModesetForWorld(player.world.uid) shouldBe "new"
|
||||
swordBlockingModule.isEnabled(player).shouldBeFalse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("listed world modesets override default world modesets") {
|
||||
withConfig {
|
||||
runSync {
|
||||
applySimpleModesetWorlds(
|
||||
mapOf(
|
||||
"__default__" to listOf("old"),
|
||||
"world" to listOf("new", "old")
|
||||
)
|
||||
)
|
||||
|
||||
Config.getAllowedModesets(player.world.uid).toList().shouldContainExactly("new", "old")
|
||||
Config.getDefaultModeset(player.world.uid) shouldBe Config.getModesets()["new"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("unlisted worlds use default world modesets") {
|
||||
withConfig {
|
||||
runSync {
|
||||
applySimpleModesetWorlds(mapOf("__default__" to listOf("old")))
|
||||
|
||||
Config.getAllowedModesets(player.world.uid).toList().shouldContainExactly("old")
|
||||
Config.getDefaultModeset(player.world.uid) shouldBe Config.getModesets()["old"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("empty listed world modesets remain unrestricted despite default world modesets") {
|
||||
withConfig {
|
||||
runSync {
|
||||
applySimpleModesetWorlds(
|
||||
mapOf(
|
||||
"__default__" to listOf("old"),
|
||||
"world" to emptyList()
|
||||
)
|
||||
)
|
||||
|
||||
val allowedModesets = Config.getAllowedModesets(player.world.uid)
|
||||
allowedModesets.shouldContain("old")
|
||||
allowedModesets.shouldContain("new")
|
||||
Config.getDefaultModeset(player.world.uid) shouldBe null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("mode command and tab completion use default world modesets") {
|
||||
withConfig {
|
||||
runSync {
|
||||
applySimpleModesetWorlds(mapOf("__default__" to listOf("old")))
|
||||
player.addAttachment(ocm, "oldcombatmechanics.commands", true)
|
||||
player.addAttachment(ocm, "oldcombatmechanics.mode", true)
|
||||
player.addAttachment(ocm, "oldcombatmechanics.mode.own", true)
|
||||
|
||||
val command = checkNotNull(Bukkit.getPluginCommand("oldcombatmechanics"))
|
||||
val completions = OCMCommandCompleter().onTabComplete(player, command, "ocm", arrayOf("mode", ""))
|
||||
completions.shouldContainExactly("old")
|
||||
|
||||
setModeset(player, "old")
|
||||
Bukkit.dispatchCommand(player, "ocm mode new")
|
||||
getPlayerData(player.uniqueId).getModesetForWorld(player.world.uid) shouldBe "old"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("modeset API stores valid modesets and normalises names") {
|
||||
withConfig {
|
||||
runSync {
|
||||
applySimpleModesetWorlds(mapOf("world" to listOf("old", "new")))
|
||||
val events = mutableListOf<PlayerModesetChangeEvent>()
|
||||
val listener = object : Listener {
|
||||
@EventHandler
|
||||
fun onModesetChange(event: PlayerModesetChangeEvent) {
|
||||
if (event.player.uniqueId == player.uniqueId) {
|
||||
events += event
|
||||
}
|
||||
}
|
||||
}
|
||||
Bukkit.getPluginManager().registerEvents(listener, testPlugin)
|
||||
|
||||
try {
|
||||
setModeset(player, "old")
|
||||
api.getModesetNames().toList().shouldContainExactly("old", "new")
|
||||
api.getAllowedModesets(player.world).toList().shouldContainExactly("old", "new")
|
||||
|
||||
api.setModesetForPlayer(player, "new")
|
||||
api.getModesetForPlayer(player) shouldBe "new"
|
||||
getPlayerData(player.uniqueId).getModesetForWorld(player.world.uid) shouldBe "new"
|
||||
events.single().previousModeset shouldBe "old"
|
||||
events.single().newModeset shouldBe "new"
|
||||
events.single().reason shouldBe PlayerModesetChangeEvent.Reason.API
|
||||
|
||||
events.clear()
|
||||
api.setModesetForPlayer(player, "new")
|
||||
events shouldBe emptyList()
|
||||
|
||||
api.setModesetForPlayer(player, "OLD")
|
||||
api.getModesetForPlayer(player) shouldBe "old"
|
||||
getPlayerData(player.uniqueId).getModesetForWorld(player.world.uid) shouldBe "old"
|
||||
events.single().previousModeset shouldBe "new"
|
||||
events.single().newModeset shouldBe "old"
|
||||
events.single().reason shouldBe PlayerModesetChangeEvent.Reason.API
|
||||
} finally {
|
||||
HandlerList.unregisterAll(listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("mode command emits command-reason modeset change events") {
|
||||
withConfig {
|
||||
runSync {
|
||||
applySimpleModesetWorlds(mapOf("world" to listOf("old", "new")))
|
||||
player.addAttachment(ocm, "oldcombatmechanics.commands", true)
|
||||
player.addAttachment(ocm, "oldcombatmechanics.mode", true)
|
||||
player.addAttachment(ocm, "oldcombatmechanics.mode.own", true)
|
||||
setModeset(player, "old")
|
||||
val events = mutableListOf<PlayerModesetChangeEvent>()
|
||||
val listener = object : Listener {
|
||||
@EventHandler
|
||||
fun onModesetChange(event: PlayerModesetChangeEvent) {
|
||||
if (event.player.uniqueId == player.uniqueId) {
|
||||
events += event
|
||||
}
|
||||
}
|
||||
}
|
||||
Bukkit.getPluginManager().registerEvents(listener, testPlugin)
|
||||
|
||||
try {
|
||||
Bukkit.dispatchCommand(player, "ocm mode new") shouldBe true
|
||||
getPlayerData(player.uniqueId).getModesetForWorld(player.world.uid) shouldBe "new"
|
||||
events.single().previousModeset shouldBe "old"
|
||||
events.single().newModeset shouldBe "new"
|
||||
events.single().reason shouldBe PlayerModesetChangeEvent.Reason.COMMAND
|
||||
|
||||
events.clear()
|
||||
Bukkit.dispatchCommand(player, "ocm mode new") shouldBe true
|
||||
events shouldBe emptyList()
|
||||
} finally {
|
||||
HandlerList.unregisterAll(listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("modeset API rejects modesets disallowed in the player world") {
|
||||
withConfig {
|
||||
runSync {
|
||||
applySimpleModesetWorlds(mapOf("world" to listOf("old")))
|
||||
setModeset(player, "old")
|
||||
|
||||
shouldThrow<IllegalArgumentException> {
|
||||
api.setModesetForPlayer(player, "new")
|
||||
}
|
||||
|
||||
api.getModesetForPlayer(player) shouldBe "old"
|
||||
getPlayerData(player.uniqueId).getModesetForWorld(player.world.uid) shouldBe "old"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("modeset API no-op set does not schedule a player data save") {
|
||||
withConfig {
|
||||
runSync {
|
||||
applySimpleModesetWorlds(mapOf("world" to listOf("old", "new")))
|
||||
setModeset(player, "old")
|
||||
clearPendingSaveTask()
|
||||
|
||||
api.setModesetForPlayer(player, "old")
|
||||
|
||||
val field = PlayerStorage::class.java.getDeclaredField("saveTask")
|
||||
field.isAccessible = true
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val saveTask = field.get(null) as AtomicReference<Any?>
|
||||
saveTask.get() shouldBe null
|
||||
api.getModesetForPlayer(player) shouldBe "old"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("modules in disabled and another list fail reload") {
|
||||
withConfig {
|
||||
runSync {
|
||||
shouldThrow<IllegalStateException> {
|
||||
applyConfig(
|
||||
always =
|
||||
completeAlways(
|
||||
always = listOf("disable-offhand"),
|
||||
disabled = listOf("disable-offhand"),
|
||||
modesets =
|
||||
mapOf(
|
||||
"old" to listOf("old-potion-effects"),
|
||||
"new" to listOf("old-golden-apples")
|
||||
)
|
||||
),
|
||||
disabled = listOf("disable-offhand"),
|
||||
modesets =
|
||||
mapOf(
|
||||
"old" to listOf("old-potion-effects"),
|
||||
"new" to listOf("old-golden-apples")
|
||||
),
|
||||
worldModesets = listOf("old", "new")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("modules missing from all lists fail reload") {
|
||||
withConfig {
|
||||
runSync {
|
||||
val moduleNames =
|
||||
(
|
||||
ModuleLoader
|
||||
.getModules()
|
||||
.map { it.configName }
|
||||
.filterNot { internalModules.contains(it) } + optionalModules
|
||||
).distinct()
|
||||
.sorted()
|
||||
val missing = moduleNames.firstOrNull() ?: error("No modules registered")
|
||||
val always = moduleNames.filterNot { it == missing }
|
||||
|
||||
shouldThrow<IllegalStateException> {
|
||||
applyConfig(
|
||||
always = always,
|
||||
disabled = emptyList(),
|
||||
modesets = mapOf("old" to emptyList()),
|
||||
worldModesets = listOf("old")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
@file:Suppress("ktlint:standard:package-name")
|
||||
|
||||
package kernitus.plugin.OldCombatMechanics
|
||||
|
||||
import com.google.common.base.Function
|
||||
import io.kotest.common.ExperimentalKotest
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.doubles.plusOrMinus
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kernitus.plugin.OldCombatMechanics.module.ModuleOldArmourStrength
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.Config
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.storage.PlayerStorage.getPlayerData
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.storage.PlayerStorage.setPlayerData
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.Location
|
||||
import org.bukkit.Material
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.event.entity.EntityDamageEvent
|
||||
import org.bukkit.inventory.ItemStack
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
import java.util.EnumMap
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.Callable
|
||||
|
||||
@OptIn(ExperimentalKotest::class)
|
||||
class OldArmourStrengthModesetIntegrationTest :
|
||||
FunSpec({
|
||||
val testPlugin = JavaPlugin.getPlugin(OCMTestMain::class.java)
|
||||
val ocm = JavaPlugin.getPlugin(OCMMain::class.java)
|
||||
val module =
|
||||
ModuleLoader
|
||||
.getModules()
|
||||
.filterIsInstance<ModuleOldArmourStrength>()
|
||||
.firstOrNull() ?: error("ModuleOldArmourStrength not registered")
|
||||
|
||||
lateinit var player: Player
|
||||
lateinit var fakePlayer: FakePlayer
|
||||
|
||||
extensions(MainThreadDispatcherExtension(testPlugin))
|
||||
|
||||
fun runSync(action: () -> Unit) {
|
||||
if (Bukkit.isPrimaryThread()) {
|
||||
action()
|
||||
} else {
|
||||
Bukkit
|
||||
.getScheduler()
|
||||
.callSyncMethod(
|
||||
testPlugin,
|
||||
Callable {
|
||||
action()
|
||||
null
|
||||
}
|
||||
).get()
|
||||
}
|
||||
}
|
||||
|
||||
fun snapshotSection(path: String): Any? {
|
||||
val section = ocm.config.getConfigurationSection(path)
|
||||
return section?.getValues(false) ?: ocm.config.get(path)
|
||||
}
|
||||
|
||||
fun restoreSection(
|
||||
path: String,
|
||||
value: Any?
|
||||
) {
|
||||
ocm.config.set(path, null)
|
||||
when (value) {
|
||||
null -> {
|
||||
Unit
|
||||
}
|
||||
|
||||
is Map<*, *> -> {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
ocm.config.createSection(path, value as Map<String, Any?>)
|
||||
}
|
||||
|
||||
else -> {
|
||||
ocm.config.set(path, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun configurableModules(): List<String> {
|
||||
val internalModules =
|
||||
setOf(
|
||||
"modeset-listener",
|
||||
"attack-cooldown-tracker",
|
||||
"entity-damage-listener"
|
||||
)
|
||||
val optionalModules =
|
||||
setOf(
|
||||
"disable-attack-sounds",
|
||||
"disable-sword-sweep-particles"
|
||||
)
|
||||
|
||||
return (
|
||||
ModuleLoader
|
||||
.getModules()
|
||||
.map { it.configName.lowercase(Locale.ROOT) }
|
||||
.filterNot { internalModules.contains(it) } + optionalModules
|
||||
).distinct().sorted()
|
||||
}
|
||||
|
||||
fun reloadConfigAndModules() {
|
||||
ocm.saveConfig()
|
||||
Config.reload()
|
||||
ModuleLoader.toggleModules()
|
||||
module.reload()
|
||||
}
|
||||
|
||||
fun applyIssue861Config() {
|
||||
val oldArmourStrength = "old-armour-strength"
|
||||
ocm.config.set("always_enabled_modules", emptyList<String>())
|
||||
ocm.config.set("disabled_modules", configurableModules().filterNot { it == oldArmourStrength })
|
||||
ocm.config.set("modesets", null)
|
||||
ocm.config.createSection(
|
||||
"modesets",
|
||||
mapOf(
|
||||
"old" to listOf(oldArmourStrength),
|
||||
"new" to emptyList<String>()
|
||||
)
|
||||
)
|
||||
ocm.config.set("worlds", null)
|
||||
ocm.config.createSection("worlds", mapOf("world" to listOf("new")))
|
||||
reloadConfigAndModules()
|
||||
}
|
||||
|
||||
suspend fun withIssue861Config(block: suspend () -> Unit) {
|
||||
val originalAlways = ocm.config.get("always_enabled_modules")
|
||||
val originalDisabled = ocm.config.get("disabled_modules")
|
||||
val originalModesets = snapshotSection("modesets")
|
||||
val originalWorlds = snapshotSection("worlds")
|
||||
|
||||
try {
|
||||
applyIssue861Config()
|
||||
block()
|
||||
} finally {
|
||||
runSync {
|
||||
ocm.config.set("always_enabled_modules", originalAlways)
|
||||
ocm.config.set("disabled_modules", originalDisabled)
|
||||
restoreSection("modesets", originalModesets)
|
||||
restoreSection("worlds", originalWorlds)
|
||||
reloadConfigAndModules()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setModeset(modeset: String) {
|
||||
val playerData = getPlayerData(player.uniqueId)
|
||||
playerData.setModesetForWorld(player.world.uid, modeset)
|
||||
setPlayerData(player.uniqueId, playerData)
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun createExplosionDamageEvent(): EntityDamageEvent {
|
||||
val modifiers =
|
||||
EnumMap<EntityDamageEvent.DamageModifier, Double>(
|
||||
EntityDamageEvent.DamageModifier::class.java
|
||||
)
|
||||
modifiers[EntityDamageEvent.DamageModifier.BASE] = 20.0
|
||||
modifiers[EntityDamageEvent.DamageModifier.ARMOR] = -8.0
|
||||
modifiers[EntityDamageEvent.DamageModifier.MAGIC] = -2.0
|
||||
|
||||
val identity =
|
||||
object : Function<Double, Double> {
|
||||
override fun apply(input: Double): Double = input
|
||||
}
|
||||
val modifierFunctions =
|
||||
EnumMap<EntityDamageEvent.DamageModifier, Function<in Double, Double>>(
|
||||
EntityDamageEvent.DamageModifier::class.java
|
||||
)
|
||||
modifiers.keys.forEach { modifierFunctions[it] = identity }
|
||||
|
||||
return EntityDamageEvent(
|
||||
player,
|
||||
EntityDamageEvent.DamageCause.BLOCK_EXPLOSION,
|
||||
modifiers,
|
||||
modifierFunctions
|
||||
)
|
||||
}
|
||||
|
||||
beforeSpec {
|
||||
runSync {
|
||||
val world = checkNotNull(Bukkit.getServer().getWorld("world"))
|
||||
fakePlayer = FakePlayer(testPlugin)
|
||||
fakePlayer.spawn(Location(world, 0.0, 100.0, 0.0))
|
||||
player = checkNotNull(Bukkit.getPlayer(fakePlayer.uuid))
|
||||
}
|
||||
}
|
||||
|
||||
afterSpec {
|
||||
runSync {
|
||||
fakePlayer.removePlayer()
|
||||
}
|
||||
}
|
||||
|
||||
beforeTest {
|
||||
runSync {
|
||||
player.inventory.clear()
|
||||
player.activePotionEffects.forEach { player.removePotionEffect(it.type) }
|
||||
player.inventory.chestplate = ItemStack(Material.DIAMOND_CHESTPLATE)
|
||||
}
|
||||
}
|
||||
|
||||
test("old-armour-strength leaves explosion modifiers unchanged in new modeset") {
|
||||
withIssue861Config {
|
||||
runSync {
|
||||
setModeset("new")
|
||||
|
||||
val event = createExplosionDamageEvent()
|
||||
val originalArmour = event.getDamage(EntityDamageEvent.DamageModifier.ARMOR)
|
||||
val originalMagic = event.getDamage(EntityDamageEvent.DamageModifier.MAGIC)
|
||||
val originalFinalDamage = event.finalDamage
|
||||
|
||||
Bukkit.getPluginManager().callEvent(event)
|
||||
|
||||
event.getDamage(EntityDamageEvent.DamageModifier.ARMOR) shouldBe (originalArmour plusOrMinus 0.0001)
|
||||
event.getDamage(EntityDamageEvent.DamageModifier.MAGIC) shouldBe (originalMagic plusOrMinus 0.0001)
|
||||
event.finalDamage shouldBe (originalFinalDamage plusOrMinus 0.0001)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("old-armour-strength applies old armour modifiers to explosion damage in old modeset") {
|
||||
withIssue861Config {
|
||||
runSync {
|
||||
setModeset("old")
|
||||
|
||||
val event = createExplosionDamageEvent()
|
||||
|
||||
Bukkit.getPluginManager().callEvent(event)
|
||||
|
||||
event.getDamage(EntityDamageEvent.DamageModifier.ARMOR) shouldBe (-6.4 plusOrMinus 0.0001)
|
||||
event.getDamage(EntityDamageEvent.DamageModifier.MAGIC) shouldBe (0.0 plusOrMinus 0.0001)
|
||||
event.finalDamage shouldBe (13.6 plusOrMinus 0.0001)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
+12
-5
@@ -13,7 +13,7 @@ import io.kotest.matchers.doubles.plusOrMinus
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kernitus.plugin.OldCombatMechanics.module.ModulePlayerKnockback
|
||||
import com.cryptomorin.xseries.XAttribute
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.CompatibilityCapabilities
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.Location
|
||||
import org.bukkit.Material
|
||||
@@ -124,9 +124,16 @@ class PlayerKnockbackIntegrationTest : FunSpec({
|
||||
val pendingClass = ModulePlayerKnockback::class.java.declaredClasses
|
||||
.firstOrNull { it.simpleName == "PendingKnockback" }
|
||||
?: error("PendingKnockback inner class not found")
|
||||
val ctor = pendingClass.getDeclaredConstructor(Vector::class.java, Long::class.javaPrimitiveType)
|
||||
val ctor = pendingClass.declaredConstructors.firstOrNull { constructor ->
|
||||
val parameterTypes = constructor.parameterTypes.toList()
|
||||
parameterTypes == listOf(Vector::class.java, UUID::class.java, Long::class.javaPrimitiveType) ||
|
||||
parameterTypes == listOf(Vector::class.java, Long::class.javaPrimitiveType)
|
||||
} ?: error("No supported PendingKnockback constructor found")
|
||||
ctor.isAccessible = true
|
||||
val pending = ctor.newInstance(vector, Long.MAX_VALUE)
|
||||
val pending = when (ctor.parameterTypes.size) {
|
||||
3 -> ctor.newInstance(vector, attacker.uniqueId, Long.MAX_VALUE)
|
||||
else -> ctor.newInstance(vector, Long.MAX_VALUE)
|
||||
}
|
||||
pendingKnockbackMap()[uuid] = pending
|
||||
}
|
||||
|
||||
@@ -284,7 +291,7 @@ class PlayerKnockbackIntegrationTest : FunSpec({
|
||||
}
|
||||
|
||||
test("modifiers remain when resistance enabled and supported") {
|
||||
if (!Reflector.versionIsNewerOrEqualTo(1, 16, 0)) return@test
|
||||
if (!CompatibilityCapabilities.isKnockbackResistanceAvailable()) return@test
|
||||
withConfig {
|
||||
ocm.config.set("old-player-knockback.enable-knockback-resistance", true)
|
||||
module.reload()
|
||||
@@ -302,7 +309,7 @@ class PlayerKnockbackIntegrationTest : FunSpec({
|
||||
}
|
||||
|
||||
test("enabled resistance scales horizontal knockback") {
|
||||
if (!Reflector.versionIsNewerOrEqualTo(1, 16, 0)) return@test
|
||||
if (!CompatibilityCapabilities.isKnockbackResistanceAvailable()) return@test
|
||||
withConfig {
|
||||
ocm.config.set("old-player-knockback.knockback-horizontal", 0.4)
|
||||
ocm.config.set("old-player-knockback.knockback-vertical", 0.4)
|
||||
|
||||
+480
@@ -0,0 +1,480 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package kernitus.plugin.OldCombatMechanics
|
||||
|
||||
import io.kotest.common.ExperimentalKotest
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.assertions.throwables.shouldThrow
|
||||
import io.kotest.matchers.collections.shouldContain
|
||||
import io.kotest.matchers.collections.shouldNotContain
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kernitus.plugin.OldCombatMechanics.api.OldCombatMechanicsAPI
|
||||
import kernitus.plugin.OldCombatMechanics.api.PlayerModesetChangeEvent
|
||||
import kernitus.plugin.OldCombatMechanics.api.PlayerModuleOverrideChangeEvent
|
||||
import kernitus.plugin.OldCombatMechanics.api.PlayerModuleOverride
|
||||
import kernitus.plugin.OldCombatMechanics.module.ModuleDisableOffHand
|
||||
import kernitus.plugin.OldCombatMechanics.module.OCMModule
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.Config
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.storage.PlayerStorage.getPlayerData
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.storage.PlayerStorage.setPlayerData
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.Location
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.event.EventHandler
|
||||
import org.bukkit.event.HandlerList
|
||||
import org.bukkit.event.Listener
|
||||
import org.bukkit.event.player.PlayerQuitEvent
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
|
||||
@OptIn(ExperimentalKotest::class)
|
||||
class PlayerModuleOverrideApiIntegrationTest : FunSpec({
|
||||
val testPlugin = JavaPlugin.getPlugin(OCMTestMain::class.java)
|
||||
val api = Bukkit.getServicesManager().getRegistration(OldCombatMechanicsAPI::class.java)?.provider
|
||||
?: error("OldCombatMechanicsApi service not registered")
|
||||
val module = ModuleLoader.getModules()
|
||||
.filterIsInstance<ModuleDisableOffHand>()
|
||||
.firstOrNull() ?: error("ModuleDisableOffHand not registered")
|
||||
val alwaysEnabledModule = moduleByName("attack-frequency")
|
||||
val modesetModule = moduleByName("old-tool-damage")
|
||||
|
||||
lateinit var player: Player
|
||||
lateinit var fakePlayer: FakePlayer
|
||||
|
||||
fun runSync(action: () -> Unit) {
|
||||
if (Bukkit.isPrimaryThread()) {
|
||||
action()
|
||||
} else {
|
||||
Bukkit.getScheduler().callSyncMethod(testPlugin) {
|
||||
action()
|
||||
null
|
||||
}.get()
|
||||
}
|
||||
}
|
||||
|
||||
fun setModeset(player: Player, modeset: String) {
|
||||
val playerData = getPlayerData(player.uniqueId)
|
||||
playerData.setModesetForWorld(player.world.uid, modeset)
|
||||
setPlayerData(player.uniqueId, playerData)
|
||||
}
|
||||
|
||||
extensions(MainThreadDispatcherExtension(testPlugin))
|
||||
|
||||
beforeSpec {
|
||||
runSync {
|
||||
val world = checkNotNull(Bukkit.getServer().getWorld("world"))
|
||||
fakePlayer = FakePlayer(testPlugin)
|
||||
fakePlayer.spawn(Location(world, 0.0, 100.0, 0.0))
|
||||
player = checkNotNull(Bukkit.getPlayer(fakePlayer.uuid))
|
||||
}
|
||||
}
|
||||
|
||||
beforeTest {
|
||||
runSync {
|
||||
api.clearAllModuleOverridesForPlayer(player)
|
||||
setModeset(player, "old")
|
||||
}
|
||||
}
|
||||
|
||||
afterTest {
|
||||
runSync {
|
||||
api.clearAllModuleOverridesForPlayer(player)
|
||||
setModeset(player, "old")
|
||||
}
|
||||
}
|
||||
|
||||
afterSpec {
|
||||
runSync {
|
||||
api.clearAllModuleOverridesForPlayer(player)
|
||||
fakePlayer.removePlayer()
|
||||
}
|
||||
}
|
||||
|
||||
test("API can force module enable and disable per player") {
|
||||
runSync {
|
||||
val moduleName = module.configName
|
||||
val disabledModeset = Config.getModesets().entries.firstOrNull { (_, modules) ->
|
||||
!modules.contains(moduleName)
|
||||
}?.key
|
||||
if (disabledModeset != null) {
|
||||
setModeset(player, disabledModeset)
|
||||
}
|
||||
|
||||
val baseline = module.isEnabled(player)
|
||||
|
||||
api.forceEnableModuleForPlayer(player, moduleName)
|
||||
module.isEnabled(player) shouldBe true
|
||||
|
||||
api.forceDisableModuleForPlayer(player, moduleName)
|
||||
module.isEnabled(player) shouldBe false
|
||||
|
||||
api.clearModuleOverrideForPlayer(player, moduleName)
|
||||
module.isEnabled(player) shouldBe baseline
|
||||
}
|
||||
}
|
||||
|
||||
test("unknown and internal module names are rejected") {
|
||||
runSync {
|
||||
shouldThrow<IllegalArgumentException> {
|
||||
api.forceEnableModuleForPlayer(player, "not-a-real-module")
|
||||
}
|
||||
shouldThrow<IllegalArgumentException> {
|
||||
api.forceDisableModuleForPlayer(player, "modeset-listener")
|
||||
}
|
||||
shouldThrow<IllegalArgumentException> {
|
||||
api.getModuleOverrideForPlayer(player, "attack-cooldown-tracker")
|
||||
}
|
||||
|
||||
api.hasAnyOverrideForPlayer(player) shouldBe false
|
||||
}
|
||||
}
|
||||
|
||||
test("bulk override setter rejects invalid keys before mutating overrides") {
|
||||
runSync {
|
||||
val moduleName = module.configName
|
||||
val alwaysEnabledModuleName = alwaysEnabledModule.configName
|
||||
val events = mutableListOf<PlayerModuleOverrideChangeEvent>()
|
||||
val listener = object : Listener {
|
||||
@EventHandler
|
||||
fun onOverrideChange(event: PlayerModuleOverrideChangeEvent) {
|
||||
if (event.player.uniqueId == player.uniqueId) {
|
||||
events += event
|
||||
}
|
||||
}
|
||||
}
|
||||
Bukkit.getPluginManager().registerEvents(listener, testPlugin)
|
||||
api.forceEnableModuleForPlayer(player, moduleName)
|
||||
events.clear()
|
||||
|
||||
try {
|
||||
val exception = shouldThrow<IllegalArgumentException> {
|
||||
api.setModuleOverridesForPlayer(
|
||||
player,
|
||||
mapOf(
|
||||
moduleName to PlayerModuleOverride.FORCE_DISABLED,
|
||||
alwaysEnabledModuleName to PlayerModuleOverride.FORCE_DISABLED,
|
||||
"missing-module" to PlayerModuleOverride.FORCE_ENABLED,
|
||||
),
|
||||
)
|
||||
}
|
||||
exception.message shouldBe "Unknown or non-configurable module: missing-module"
|
||||
} finally {
|
||||
HandlerList.unregisterAll(listener)
|
||||
}
|
||||
|
||||
api.getModuleOverridesForPlayer(player) shouldBe mapOf(moduleName to PlayerModuleOverride.FORCE_ENABLED)
|
||||
api.getModuleOverrideForPlayer(player, alwaysEnabledModuleName) shouldBe PlayerModuleOverride.DEFAULT
|
||||
events shouldBe emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
test("bulk override setter rejects duplicate normalised keys before mutating overrides") {
|
||||
runSync {
|
||||
val moduleName = module.configName
|
||||
val alwaysEnabledModuleName = alwaysEnabledModule.configName
|
||||
val events = mutableListOf<PlayerModuleOverrideChangeEvent>()
|
||||
val listener = object : Listener {
|
||||
@EventHandler
|
||||
fun onOverrideChange(event: PlayerModuleOverrideChangeEvent) {
|
||||
if (event.player.uniqueId == player.uniqueId) {
|
||||
events += event
|
||||
}
|
||||
}
|
||||
}
|
||||
Bukkit.getPluginManager().registerEvents(listener, testPlugin)
|
||||
api.forceEnableModuleForPlayer(player, moduleName)
|
||||
events.clear()
|
||||
|
||||
try {
|
||||
shouldThrow<IllegalArgumentException> {
|
||||
api.setModuleOverridesForPlayer(
|
||||
player,
|
||||
mapOf(
|
||||
moduleName to PlayerModuleOverride.FORCE_DISABLED,
|
||||
" ${moduleName.replaceFirstChar { it.uppercase() }} " to PlayerModuleOverride.DEFAULT,
|
||||
alwaysEnabledModuleName to PlayerModuleOverride.FORCE_DISABLED,
|
||||
),
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
HandlerList.unregisterAll(listener)
|
||||
}
|
||||
|
||||
api.getModuleOverridesForPlayer(player) shouldBe mapOf(moduleName to PlayerModuleOverride.FORCE_ENABLED)
|
||||
api.getModuleOverrideForPlayer(player, alwaysEnabledModuleName) shouldBe PlayerModuleOverride.DEFAULT
|
||||
events shouldBe emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
test("module override API emits only changed override events in sorted bulk order") {
|
||||
runSync {
|
||||
val configDisabledModuleName = module.configName
|
||||
val alwaysEnabledModuleName = alwaysEnabledModule.configName
|
||||
val events = mutableListOf<PlayerModuleOverrideChangeEvent>()
|
||||
val listener = object : Listener {
|
||||
@EventHandler
|
||||
fun onOverrideChange(event: PlayerModuleOverrideChangeEvent) {
|
||||
if (event.player.uniqueId == player.uniqueId) {
|
||||
events += event
|
||||
}
|
||||
}
|
||||
}
|
||||
Bukkit.getPluginManager().registerEvents(listener, testPlugin)
|
||||
|
||||
try {
|
||||
api.forceEnableModuleForPlayer(player, configDisabledModuleName)
|
||||
api.forceEnableModuleForPlayer(player, configDisabledModuleName)
|
||||
events.map { it.moduleName } shouldBe listOf(configDisabledModuleName)
|
||||
events.single().previousOverride shouldBe PlayerModuleOverride.DEFAULT
|
||||
events.single().newOverride shouldBe PlayerModuleOverride.FORCE_ENABLED
|
||||
|
||||
events.clear()
|
||||
api.setModuleOverridesForPlayer(
|
||||
player,
|
||||
mapOf(
|
||||
configDisabledModuleName to PlayerModuleOverride.FORCE_DISABLED,
|
||||
alwaysEnabledModuleName to PlayerModuleOverride.FORCE_DISABLED,
|
||||
),
|
||||
)
|
||||
events.map { it.moduleName } shouldBe listOf(alwaysEnabledModuleName, configDisabledModuleName).sorted()
|
||||
events.map { it.newOverride } shouldBe listOf(
|
||||
PlayerModuleOverride.FORCE_DISABLED,
|
||||
PlayerModuleOverride.FORCE_DISABLED,
|
||||
)
|
||||
|
||||
events.clear()
|
||||
api.setModuleOverridesForPlayer(
|
||||
player,
|
||||
mapOf(
|
||||
configDisabledModuleName to PlayerModuleOverride.FORCE_DISABLED,
|
||||
alwaysEnabledModuleName to PlayerModuleOverride.FORCE_DISABLED,
|
||||
),
|
||||
)
|
||||
events shouldBe emptyList()
|
||||
|
||||
events.clear()
|
||||
api.clearAllModuleOverridesForPlayer(player)
|
||||
events.map { it.moduleName } shouldBe listOf(alwaysEnabledModuleName, configDisabledModuleName).sorted()
|
||||
events.map { it.newOverride } shouldBe listOf(
|
||||
PlayerModuleOverride.DEFAULT,
|
||||
PlayerModuleOverride.DEFAULT,
|
||||
)
|
||||
|
||||
events.clear()
|
||||
api.clearAllModuleOverridesForPlayer(player)
|
||||
events shouldBe emptyList()
|
||||
} finally {
|
||||
HandlerList.unregisterAll(listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("modeset API emits events only for stored changes") {
|
||||
runSync {
|
||||
setModeset(player, "old")
|
||||
val events = mutableListOf<PlayerModesetChangeEvent>()
|
||||
val listener = object : Listener {
|
||||
@EventHandler
|
||||
fun onModesetChange(event: PlayerModesetChangeEvent) {
|
||||
if (event.player.uniqueId == player.uniqueId) {
|
||||
events += event
|
||||
}
|
||||
}
|
||||
}
|
||||
Bukkit.getPluginManager().registerEvents(listener, testPlugin)
|
||||
|
||||
try {
|
||||
api.setModesetForPlayer(player, "new")
|
||||
events.size shouldBe 1
|
||||
events.single().previousModeset shouldBe "old"
|
||||
events.single().newModeset shouldBe "new"
|
||||
events.single().reason shouldBe PlayerModesetChangeEvent.Reason.API
|
||||
|
||||
api.setModesetForPlayer(player, "new")
|
||||
events.size shouldBe 1
|
||||
} finally {
|
||||
HandlerList.unregisterAll(listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("modeset command emits command reason event") {
|
||||
runSync {
|
||||
setModeset(player, "old")
|
||||
val events = mutableListOf<PlayerModesetChangeEvent>()
|
||||
val listener = object : Listener {
|
||||
@EventHandler
|
||||
fun onModesetChange(event: PlayerModesetChangeEvent) {
|
||||
if (event.player.uniqueId == player.uniqueId) {
|
||||
events += event
|
||||
}
|
||||
}
|
||||
}
|
||||
Bukkit.getPluginManager().registerEvents(listener, testPlugin)
|
||||
|
||||
try {
|
||||
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "ocm mode new ${player.name}") shouldBe true
|
||||
events.size shouldBe 1
|
||||
events.single().previousModeset shouldBe "old"
|
||||
events.single().newModeset shouldBe "new"
|
||||
events.single().reason shouldBe PlayerModesetChangeEvent.Reason.COMMAND
|
||||
} finally {
|
||||
HandlerList.unregisterAll(listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("Java null bulk override value is rejected before mutating overrides") {
|
||||
runSync {
|
||||
val moduleName = module.configName
|
||||
api.forceEnableModuleForPlayer(player, moduleName)
|
||||
api.getModuleOverrideForPlayer(player, moduleName) shouldBe PlayerModuleOverride.FORCE_ENABLED
|
||||
|
||||
shouldThrow<IllegalArgumentException> {
|
||||
callJavaNullBulkOverrideValue(api, player, moduleName)
|
||||
}
|
||||
|
||||
api.getModuleOverrideForPlayer(player, moduleName) shouldBe PlayerModuleOverride.FORCE_ENABLED
|
||||
api.clearModuleOverrideForPlayer(player, moduleName)
|
||||
}
|
||||
}
|
||||
|
||||
test("force enable overrides config-disabled and modeset-disabled modules") {
|
||||
runSync {
|
||||
val configDisabledModuleName = module.configName
|
||||
module.isEnabled(player) shouldBe false
|
||||
api.isModuleEnabledForPlayer(player, configDisabledModuleName) shouldBe false
|
||||
|
||||
api.forceEnableModuleForPlayer(player, configDisabledModuleName)
|
||||
|
||||
module.isEnabled(player) shouldBe true
|
||||
api.isModuleEnabledForPlayer(player, configDisabledModuleName) shouldBe true
|
||||
|
||||
val modesetModuleName = modesetModule.configName
|
||||
setModeset(player, "new")
|
||||
modesetModule.isEnabled(player) shouldBe false
|
||||
api.isModuleEnabledForPlayer(player, modesetModuleName) shouldBe false
|
||||
|
||||
api.forceEnableModuleForPlayer(player, modesetModuleName)
|
||||
|
||||
modesetModule.isEnabled(player) shouldBe true
|
||||
api.isModuleEnabledForPlayer(player, modesetModuleName) shouldBe true
|
||||
}
|
||||
}
|
||||
|
||||
test("force disable overrides config-enabled modules") {
|
||||
runSync {
|
||||
val moduleName = alwaysEnabledModule.configName
|
||||
alwaysEnabledModule.isEnabled(player) shouldBe true
|
||||
api.isModuleEnabledForPlayer(player, moduleName) shouldBe true
|
||||
|
||||
api.forceDisableModuleForPlayer(player, moduleName)
|
||||
|
||||
alwaysEnabledModule.isEnabled(player) shouldBe false
|
||||
api.isModuleEnabledForPlayer(player, moduleName) shouldBe false
|
||||
}
|
||||
}
|
||||
|
||||
test("clearing overrides returns to configured and modeset behaviour") {
|
||||
runSync {
|
||||
val configDisabledModuleName = module.configName
|
||||
api.forceEnableModuleForPlayer(player, configDisabledModuleName)
|
||||
api.clearModuleOverrideForPlayer(player, configDisabledModuleName)
|
||||
api.getModuleOverrideForPlayer(player, configDisabledModuleName) shouldBe PlayerModuleOverride.DEFAULT
|
||||
module.isEnabled(player) shouldBe false
|
||||
|
||||
val alwaysEnabledModuleName = alwaysEnabledModule.configName
|
||||
api.forceDisableModuleForPlayer(player, alwaysEnabledModuleName)
|
||||
api.clearModuleOverrideForPlayer(player, alwaysEnabledModuleName)
|
||||
api.getModuleOverrideForPlayer(player, alwaysEnabledModuleName) shouldBe PlayerModuleOverride.DEFAULT
|
||||
alwaysEnabledModule.isEnabled(player) shouldBe true
|
||||
|
||||
val modesetModuleName = modesetModule.configName
|
||||
setModeset(player, "new")
|
||||
api.forceEnableModuleForPlayer(player, modesetModuleName)
|
||||
api.clearModuleOverrideForPlayer(player, modesetModuleName)
|
||||
modesetModule.isEnabled(player) shouldBe false
|
||||
|
||||
setModeset(player, "old")
|
||||
modesetModule.isEnabled(player) shouldBe true
|
||||
}
|
||||
}
|
||||
|
||||
test("override query methods expose only active configurable overrides") {
|
||||
runSync {
|
||||
val configDisabledModuleName = module.configName
|
||||
val alwaysEnabledModuleName = alwaysEnabledModule.configName
|
||||
val moduleNames = api.getModuleNames()
|
||||
|
||||
moduleNames shouldContain configDisabledModuleName
|
||||
moduleNames shouldContain alwaysEnabledModuleName
|
||||
moduleNames shouldContain modesetModule.configName
|
||||
moduleNames shouldNotContain "modeset-listener"
|
||||
moduleNames shouldNotContain "attack-cooldown-tracker"
|
||||
moduleNames shouldNotContain "entity-damage-listener"
|
||||
|
||||
api.hasAnyOverrideForPlayer(player) shouldBe false
|
||||
api.setModuleOverridesForPlayer(
|
||||
player,
|
||||
mapOf(
|
||||
configDisabledModuleName to PlayerModuleOverride.FORCE_ENABLED,
|
||||
alwaysEnabledModuleName to PlayerModuleOverride.FORCE_DISABLED,
|
||||
),
|
||||
)
|
||||
|
||||
api.hasAnyOverrideForPlayer(player) shouldBe true
|
||||
api.getModuleOverridesForPlayer(player) shouldBe mapOf(
|
||||
configDisabledModuleName to PlayerModuleOverride.FORCE_ENABLED,
|
||||
alwaysEnabledModuleName to PlayerModuleOverride.FORCE_DISABLED,
|
||||
)
|
||||
|
||||
api.setModuleOverridesForPlayer(
|
||||
player,
|
||||
mapOf(configDisabledModuleName to PlayerModuleOverride.DEFAULT),
|
||||
)
|
||||
|
||||
api.getModuleOverridesForPlayer(player) shouldBe mapOf(
|
||||
alwaysEnabledModuleName to PlayerModuleOverride.FORCE_DISABLED,
|
||||
)
|
||||
api.clearAllModuleOverridesForPlayer(player)
|
||||
api.hasAnyOverrideForPlayer(player) shouldBe false
|
||||
}
|
||||
}
|
||||
|
||||
test("player quit lifecycle clears override state") {
|
||||
runSync {
|
||||
val moduleName = module.configName
|
||||
api.forceEnableModuleForPlayer(player, moduleName)
|
||||
api.hasAnyOverrideForPlayer(player) shouldBe true
|
||||
|
||||
Bukkit.getPluginManager().callEvent(PlayerQuitEvent(player, ""))
|
||||
|
||||
api.hasAnyOverrideForPlayer(player) shouldBe false
|
||||
api.getModuleOverrideForPlayer(player, moduleName) shouldBe PlayerModuleOverride.DEFAULT
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
private fun moduleByName(moduleName: String): OCMModule =
|
||||
ModuleLoader.getModules()
|
||||
.firstOrNull { it.configName == moduleName }
|
||||
?: error("$moduleName module not registered")
|
||||
|
||||
private fun callJavaNullBulkOverrideValue(api: OldCombatMechanicsAPI, player: Player, moduleName: String) {
|
||||
val helper = Class.forName("kernitus.plugin.OldCombatMechanics.PlayerModuleOverrideJavaInterop")
|
||||
val method = helper.getDeclaredMethod(
|
||||
"setOverridesWithNullValue",
|
||||
OldCombatMechanicsAPI::class.java,
|
||||
Player::class.java,
|
||||
String::class.java,
|
||||
)
|
||||
method.isAccessible = true
|
||||
try {
|
||||
method.invoke(null, api, player, moduleName)
|
||||
} catch (exception: java.lang.reflect.InvocationTargetException) {
|
||||
throw exception.cause ?: exception
|
||||
}
|
||||
}
|
||||
+15
-3
@@ -11,13 +11,14 @@ import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.doubles.plusOrMinus
|
||||
import io.kotest.matchers.shouldBe
|
||||
import kernitus.plugin.OldCombatMechanics.module.ModulePlayerRegen
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.Location
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.event.entity.EntityRegainHealthEvent
|
||||
import org.bukkit.plugin.java.JavaPlugin
|
||||
import java.util.concurrent.Callable
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
@OptIn(ExperimentalKotest::class)
|
||||
class PlayerRegenIntegrationTest : FunSpec({
|
||||
@@ -42,6 +43,17 @@ class PlayerRegenIntegrationTest : FunSpec({
|
||||
ModuleLoader.toggleModules()
|
||||
}
|
||||
|
||||
suspend fun waitServerTicks(ticks: Long) {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
val task = Bukkit.getScheduler().runTaskLater(testPlugin, Runnable {
|
||||
if (continuation.isActive) {
|
||||
continuation.resume(Unit)
|
||||
}
|
||||
}, ticks)
|
||||
continuation.invokeOnCancellation { task.cancel() }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun withConfig(intervalMs: Long, amount: Int, exhaustion: Double, block: suspend () -> Unit) {
|
||||
val oldInterval = ocm.config.getLong("old-player-regen.interval")
|
||||
val oldAmount = ocm.config.getInt("old-player-regen.amount")
|
||||
@@ -154,7 +166,7 @@ class PlayerRegenIntegrationTest : FunSpec({
|
||||
player.exhaustion = 2.0f
|
||||
}
|
||||
|
||||
delay(2 * 50L)
|
||||
waitServerTicks(2L)
|
||||
|
||||
runSync {
|
||||
player.exhaustion.toDouble() shouldBe (4.0 plusOrMinus 0.0001) // previous(1.0) + config(3.0)
|
||||
@@ -191,7 +203,7 @@ class PlayerRegenIntegrationTest : FunSpec({
|
||||
player.exhaustion = 3.5f
|
||||
}
|
||||
|
||||
delay(2 * 50L)
|
||||
waitServerTicks(2L)
|
||||
|
||||
runSync {
|
||||
player.exhaustion.toDouble() shouldBe (1.0 plusOrMinus 0.0001)
|
||||
|
||||
+21
-5
@@ -55,6 +55,17 @@ class WeaponDurabilityIntegrationTest :
|
||||
delay(ticks * 50L)
|
||||
}
|
||||
|
||||
suspend fun waitUntilTicks(
|
||||
maxTicks: Long,
|
||||
condition: () -> Boolean,
|
||||
): Boolean {
|
||||
repeat(maxTicks.toInt()) {
|
||||
if (condition()) return true
|
||||
delayTicks(1)
|
||||
}
|
||||
return condition()
|
||||
}
|
||||
|
||||
fun scoreAttackMethodLocal(method: java.lang.reflect.Method): Int {
|
||||
var score = 0
|
||||
val name = method.name
|
||||
@@ -524,10 +535,13 @@ class WeaponDurabilityIntegrationTest :
|
||||
}
|
||||
delayTicks(1)
|
||||
appendDebug("expire:afterManualEvent allDamageEvents=${allDamageEventCount.get()}")
|
||||
runSync { attackNms(attacker, victim) }
|
||||
delayTicks(12)
|
||||
runSync { attackNms(attacker, victim) }
|
||||
delayTicks(2)
|
||||
repeat(2) { attempt ->
|
||||
runSync { attackNms(attacker, victim) }
|
||||
waitUntilTicks(4) { hitCount.get() > attempt }
|
||||
waitUntilTicks((victim.maximumNoDamageTicks + 2).toLong()) {
|
||||
!victim.isValid || victim.noDamageTicks <= 0
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
runSync { HandlerList.unregisterAll(listener) }
|
||||
}
|
||||
@@ -558,7 +572,9 @@ class WeaponDurabilityIntegrationTest :
|
||||
if (hits < 2) {
|
||||
repeat(4) {
|
||||
runSync { attackNms(attacker, victim) }
|
||||
delayTicks(2)
|
||||
waitUntilTicks((victim.maximumNoDamageTicks + 2).toLong()) {
|
||||
hitCount.get() >= 2 || !victim.isValid || victim.noDamageTicks <= 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,14 +8,27 @@ package kernitus.plugin.OldCombatMechanics;
|
||||
import kernitus.plugin.OldCombatMechanics.module.OCMModule;
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.EventRegistry;
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.Messenger;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
public class ModuleLoader {
|
||||
|
||||
private static EventRegistry eventRegistry;
|
||||
private static final List<OCMModule> modules = new ArrayList<>();
|
||||
private static final Set<String> internalModules = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
|
||||
"modeset-listener",
|
||||
"attack-cooldown-tracker",
|
||||
"entity-damage-listener"
|
||||
)));
|
||||
|
||||
public static void initialise(OCMMain plugin) {
|
||||
modules.clear();
|
||||
@@ -45,4 +58,33 @@ public class ModuleLoader {
|
||||
public static List<OCMModule> getModules() {
|
||||
return modules;
|
||||
}
|
||||
|
||||
public static @Nullable OCMModule getConfigurableModule(String moduleName) {
|
||||
final String normalised = normaliseModuleName(moduleName);
|
||||
if (internalModules.contains(normalised)) return null;
|
||||
|
||||
return modules.stream()
|
||||
.filter(module -> normaliseModuleName(module.getConfigName()).equals(normalised))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public static Set<String> getConfigurableModuleNames() {
|
||||
final Set<String> names = new HashSet<>();
|
||||
for (OCMModule module : modules) {
|
||||
final String name = normaliseModuleName(module.getConfigName());
|
||||
if (!internalModules.contains(name)) {
|
||||
names.add(name);
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableSet(names);
|
||||
}
|
||||
|
||||
public static void notifyPlayerStateChanged(@NotNull Player player) {
|
||||
modules.forEach(module -> module.onModesetChange(player));
|
||||
}
|
||||
|
||||
public static String normaliseModuleName(String moduleName) {
|
||||
return moduleName == null ? "" : moduleName.toLowerCase(Locale.ROOT).trim();
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
package kernitus.plugin.OldCombatMechanics;
|
||||
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.Config;
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector;
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.CompatibilityCapabilities;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
|
||||
@@ -36,8 +36,8 @@ public class OCMConfigHandler {
|
||||
final File backup = getFile("config-backup.yml");
|
||||
if (backup.exists()) backup.delete();
|
||||
|
||||
// Keeping YAML comments not available in lower versions
|
||||
if (Reflector.versionIsNewerOrEqualTo(1, 18, 1) ||
|
||||
// Only auto-upgrade when Bukkit can parse and save YAML comments without stripping the default config.
|
||||
if (CompatibilityCapabilities.canPreserveYamlComments() ||
|
||||
Config.getConfig().getBoolean("force-below-1-18-1-config-upgrade", false)
|
||||
) {
|
||||
plugin.getLogger().warning("Config version does not match, upgrading old config");
|
||||
@@ -54,8 +54,8 @@ public class OCMConfigHandler {
|
||||
plugin.saveResource(CONFIG_NAME, true);
|
||||
|
||||
// Now, load the old values from the backup and the new config from the fresh file
|
||||
final YamlConfiguration oldConfig = YamlConfiguration.loadConfiguration(backup);
|
||||
final YamlConfiguration newConfig = YamlConfiguration.loadConfiguration(configFile);
|
||||
final YamlConfiguration oldConfig = CompatibilityCapabilities.loadCommentPreservingYaml(backup);
|
||||
final YamlConfiguration newConfig = CompatibilityCapabilities.loadCommentPreservingYaml(configFile);
|
||||
|
||||
// Copy user's values for keys that still exist
|
||||
for (String key : newConfig.getKeys(true)) {
|
||||
@@ -197,11 +197,19 @@ public class OCMConfigHandler {
|
||||
}
|
||||
|
||||
final ConfigurationSection oldWorlds = oldConfig.getConfigurationSection("worlds");
|
||||
boolean hasDefaultWorld = false;
|
||||
if (oldWorlds != null) {
|
||||
hasDefaultWorld = oldWorlds.contains("__default__");
|
||||
for (String worldName : oldWorlds.getKeys(false)) {
|
||||
newConfig.set("worlds." + worldName, oldWorlds.getStringList(worldName));
|
||||
}
|
||||
}
|
||||
if (!hasDefaultWorld) {
|
||||
final ConfigurationSection upgradedModesets = newConfig.getConfigurationSection("modesets");
|
||||
if (upgradedModesets != null) {
|
||||
newConfig.set("worlds.__default__", new ArrayList<>(upgradedModesets.getKeys(false)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public YamlConfiguration getConfig(String fileName) {
|
||||
|
||||
@@ -7,6 +7,8 @@ package kernitus.plugin.OldCombatMechanics;
|
||||
|
||||
import com.github.retrooper.packetevents.PacketEvents;
|
||||
import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder;
|
||||
import kernitus.plugin.OldCombatMechanics.api.OldCombatMechanicsAPI;
|
||||
import kernitus.plugin.OldCombatMechanics.api.OldCombatMechanicsAPIImpl;
|
||||
import kernitus.plugin.OldCombatMechanics.commands.OCMCommandCompleter;
|
||||
import kernitus.plugin.OldCombatMechanics.commands.OCMCommandHandler;
|
||||
import kernitus.plugin.OldCombatMechanics.hooks.PlaceholderAPIHook;
|
||||
@@ -19,6 +21,7 @@ import kernitus.plugin.OldCombatMechanics.utilities.damage.AttackCooldownTracker
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.damage.EntityDamageByEntityListener;
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector;
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.storage.ModesetListener;
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.storage.PlayerModuleOverrides;
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.storage.PlayerStorage;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.HumanEntity;
|
||||
@@ -27,24 +30,32 @@ import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.bukkit.plugin.PluginDescriptionFile;
|
||||
import org.bukkit.plugin.RegisteredListener;
|
||||
import org.bukkit.plugin.ServicePriority;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class OCMMain extends JavaPlugin {
|
||||
|
||||
private static final String BUILD_PROPERTIES_RESOURCE = "/ocm-build.properties";
|
||||
|
||||
private static OCMMain INSTANCE;
|
||||
private static String buildCommit;
|
||||
private final Logger logger = getLogger();
|
||||
private final OCMConfigHandler CH = new OCMConfigHandler(this);
|
||||
private final List<Runnable> disableListeners = new ArrayList<>();
|
||||
private final List<Runnable> enableListeners = new ArrayList<>();
|
||||
private final List<Hook> hooks = new ArrayList<>();
|
||||
private OldCombatMechanicsAPI api;
|
||||
public OCMMain() {
|
||||
super();
|
||||
}
|
||||
@@ -78,6 +89,10 @@ public class OCMMain extends JavaPlugin {
|
||||
// Register all the modules
|
||||
registerModules();
|
||||
|
||||
// Register public API service
|
||||
api = new OldCombatMechanicsAPIImpl(this);
|
||||
Bukkit.getServicesManager().register(OldCombatMechanicsAPI.class, api, this, ServicePriority.Normal);
|
||||
|
||||
// Register all hooks for integrating with other plugins
|
||||
registerHooks();
|
||||
|
||||
@@ -115,7 +130,7 @@ public class OCMMain extends JavaPlugin {
|
||||
|
||||
// Logging to console the enabling of OCM
|
||||
final PluginDescriptionFile pdfFile = this.getDescription();
|
||||
logger.info(pdfFile.getName() + " v" + pdfFile.getVersion() + " has been enabled");
|
||||
logger.info(pdfFile.getName() + " v" + getDisplayVersion() + " has been enabled");
|
||||
|
||||
if (Config.moduleEnabled("update-checker"))
|
||||
Bukkit.getScheduler().runTaskLaterAsynchronously(this,
|
||||
@@ -153,6 +168,13 @@ public class OCMMain extends JavaPlugin {
|
||||
|
||||
PlayerStorage.instantSave();
|
||||
|
||||
PlayerModuleOverrides.clearAll();
|
||||
|
||||
if (api != null) {
|
||||
Bukkit.getServicesManager().unregister(OldCombatMechanicsAPI.class, api);
|
||||
api = null;
|
||||
}
|
||||
|
||||
PacketEvents.getAPI().terminate();
|
||||
|
||||
// Logging to console the disabling of OCM
|
||||
@@ -271,4 +293,38 @@ public class OCMMain extends JavaPlugin {
|
||||
return INSTANCE.getDescription().getVersion();
|
||||
}
|
||||
|
||||
public String getDisplayVersion() {
|
||||
return formatDisplayVersion(getDescription().getVersion());
|
||||
}
|
||||
|
||||
public static String formatDisplayVersion(String version) {
|
||||
final String commit = getBuildCommit();
|
||||
if (commit.isEmpty()) return version;
|
||||
|
||||
return version + " (commit " + commit + ")";
|
||||
}
|
||||
|
||||
private static synchronized String getBuildCommit() {
|
||||
if (buildCommit == null) buildCommit = loadBuildCommit();
|
||||
|
||||
return buildCommit;
|
||||
}
|
||||
|
||||
private static String loadBuildCommit() {
|
||||
final Properties properties = new Properties();
|
||||
|
||||
try (InputStream stream = OCMMain.class.getResourceAsStream(BUILD_PROPERTIES_RESOURCE)) {
|
||||
if (stream == null) return "";
|
||||
|
||||
properties.load(stream);
|
||||
} catch (IOException e) {
|
||||
return "";
|
||||
}
|
||||
|
||||
final String commit = properties.getProperty("commit", "").trim();
|
||||
if (commit.isEmpty() || commit.contains("${")) return "";
|
||||
|
||||
return commit;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,8 +7,8 @@ package kernitus.plugin.OldCombatMechanics;
|
||||
|
||||
import kernitus.plugin.OldCombatMechanics.updater.SpigetUpdateChecker;
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.Config;
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.CompatibilityCapabilities;
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.Messenger;
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
@@ -27,7 +27,7 @@ public class UpdateChecker {
|
||||
this.plugin = plugin;
|
||||
// We don't really want to auto update if the config is not going to be upgraded automatically
|
||||
autoDownload = Config.moduleSettingEnabled("update-checker", "auto-update") &&
|
||||
(Reflector.versionIsNewerOrEqualTo(1, 18, 1) ||
|
||||
(CompatibilityCapabilities.canPreserveYamlComments() ||
|
||||
Config.getConfig().getBoolean("force-below-1-18-1-config-upgrade", false)
|
||||
);
|
||||
}
|
||||
@@ -65,4 +65,4 @@ public class UpdateChecker {
|
||||
|
||||
messages.forEach(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
package kernitus.plugin.OldCombatMechanics.api
|
||||
|
||||
import org.bukkit.entity.Player
|
||||
import org.bukkit.World
|
||||
|
||||
/**
|
||||
* Java-facing public API for managing per-player module overrides.
|
||||
*
|
||||
* Overrides are online-session-only, in-memory state for currently online players.
|
||||
* They are cleared when the player quits, when OldCombatMechanics is disabled, or
|
||||
* when a plugin explicitly clears them through this API.
|
||||
*
|
||||
* API methods must be called from the Bukkit main server thread. The current
|
||||
* contract does not guarantee async-safe access to Bukkit [Player] state,
|
||||
* module state-change notifications, or future implementation details.
|
||||
*
|
||||
* Overrides are shared runtime state. If multiple plugins set an override for
|
||||
* the same player and module, the last write wins. Any plugin that clears that
|
||||
* override clears the same shared state for that player and module.
|
||||
*
|
||||
* Only configurable modules are valid inputs for methods that accept a module
|
||||
* name. Internal or otherwise non-configurable modules are rejected with
|
||||
* [IllegalArgumentException] and are not returned by [getModuleNames].
|
||||
*
|
||||
* Effective module state is resolved by checking per-player overrides first,
|
||||
* then configured disabled modules, configured always-enabled modules, and
|
||||
* finally the player's modeset and world configuration.
|
||||
*
|
||||
* Keep this surface friendly to Java callers. Future public API additions should use
|
||||
* simple JVM-visible types and should avoid Kotlin-only shapes such as suspend functions,
|
||||
* default parameters, function types, sealed or value classes, and companion-only access
|
||||
* patterns.
|
||||
*
|
||||
* Obtain an instance from Bukkit's services manager and always null-check the
|
||||
* registration before using it. The service is only available while
|
||||
* OldCombatMechanics is loaded and enabled:
|
||||
* ```
|
||||
* RegisteredServiceProvider<OldCombatMechanicsAPI> registration = Bukkit.getServicesManager()
|
||||
* .getRegistration(OldCombatMechanicsAPI.class);
|
||||
* if (registration == null) {
|
||||
* return;
|
||||
* }
|
||||
* OldCombatMechanicsAPI api = registration.getProvider();
|
||||
* ```
|
||||
*/
|
||||
interface OldCombatMechanicsAPI {
|
||||
|
||||
/**
|
||||
* Returns all configured modeset names in config iteration order.
|
||||
*/
|
||||
fun getModesetNames(): Set<String>
|
||||
|
||||
/**
|
||||
* Returns the modesets allowed in [world] as an unmodifiable defensive copy.
|
||||
*
|
||||
* Iteration order follows the configured world list when present, the
|
||||
* `worlds.__default__` list for worlds without their own list, or the
|
||||
* configured modeset order when no world list applies.
|
||||
*/
|
||||
fun getAllowedModesets(world: World): Set<String>
|
||||
|
||||
/**
|
||||
* Returns the stored modeset for [player] in their current world, or null
|
||||
* when no player-specific modeset has been stored yet.
|
||||
*/
|
||||
fun getModesetForPlayer(player: Player): String?
|
||||
|
||||
/**
|
||||
* Stores [modesetName] for [player] in their current world.
|
||||
*
|
||||
* The input name is normalised with [java.util.Locale.ROOT] before
|
||||
* validation and storage. Unknown modesets, or modesets not allowed in the
|
||||
* player's current world, are rejected.
|
||||
*
|
||||
* @throws IllegalArgumentException if [modesetName] is unknown or disallowed.
|
||||
*/
|
||||
fun setModesetForPlayer(player: Player, modesetName: String)
|
||||
|
||||
/**
|
||||
* Forces configurable [moduleName] on for online [player]. The override is
|
||||
* online-session-only and is cleared when explicitly cleared, when [player]
|
||||
* quits, or when OldCombatMechanics is disabled.
|
||||
*
|
||||
* @throws IllegalArgumentException if [moduleName] is unknown or non-configurable.
|
||||
*/
|
||||
fun forceEnableModuleForPlayer(player: Player, moduleName: String)
|
||||
|
||||
/**
|
||||
* Forces configurable [moduleName] off for online [player]. The override is
|
||||
* online-session-only and is cleared when explicitly cleared, when [player]
|
||||
* quits, or when OldCombatMechanics is disabled.
|
||||
*
|
||||
* @throws IllegalArgumentException if [moduleName] is unknown or non-configurable.
|
||||
*/
|
||||
fun forceDisableModuleForPlayer(player: Player, moduleName: String)
|
||||
|
||||
/**
|
||||
* Clears the override for configurable [moduleName] for [player], reverting
|
||||
* to configured module behaviour.
|
||||
* No-op if no override is set.
|
||||
*
|
||||
* @throws IllegalArgumentException if [moduleName] is unknown or non-configurable.
|
||||
*/
|
||||
fun clearModuleOverrideForPlayer(player: Player, moduleName: String)
|
||||
|
||||
/**
|
||||
* Clears all module overrides for [player], reverting to configured module behaviour.
|
||||
* No-op if no overrides are set.
|
||||
*/
|
||||
fun clearAllModuleOverridesForPlayer(player: Player)
|
||||
|
||||
/**
|
||||
* Returns the current override state for configurable [moduleName] for [player].
|
||||
* Returns [PlayerModuleOverride.DEFAULT] if no override is set.
|
||||
*
|
||||
* @throws IllegalArgumentException if [moduleName] is unknown or non-configurable.
|
||||
*/
|
||||
fun getModuleOverrideForPlayer(player: Player, moduleName: String): PlayerModuleOverride
|
||||
|
||||
/**
|
||||
* Returns all active overrides for [player] as a map of module name to [PlayerModuleOverride].
|
||||
* Only modules with a non-DEFAULT override are included.
|
||||
*/
|
||||
fun getModuleOverridesForPlayer(player: Player): Map<String, PlayerModuleOverride>
|
||||
|
||||
/**
|
||||
* Sets multiple module overrides for [player] at once.
|
||||
* Entries with [PlayerModuleOverride.DEFAULT] are treated as clears.
|
||||
* All entries are validated before any override state changes are applied.
|
||||
*
|
||||
* @throws IllegalArgumentException if any module name is unknown, non-configurable, or null,
|
||||
* or if any override value is null.
|
||||
*/
|
||||
fun setModuleOverridesForPlayer(player: Player, overrides: Map<String, PlayerModuleOverride>)
|
||||
|
||||
/**
|
||||
* Returns whether configurable [moduleName] is effectively enabled for [player],
|
||||
* accounting for configured rules and any per-player override.
|
||||
*
|
||||
* @throws IllegalArgumentException if [moduleName] is unknown or non-configurable.
|
||||
*/
|
||||
fun isModuleEnabledForPlayer(player: Player, moduleName: String): Boolean
|
||||
|
||||
/**
|
||||
* Returns whether [player] has any non-default override set.
|
||||
*/
|
||||
fun hasAnyOverrideForPlayer(player: Player): Boolean
|
||||
|
||||
/**
|
||||
* Returns the names of all configurable modules that support per-player overrides.
|
||||
*/
|
||||
fun getModuleNames(): Set<String>
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
package kernitus.plugin.OldCombatMechanics.api
|
||||
|
||||
import kernitus.plugin.OldCombatMechanics.ModuleLoader
|
||||
import kernitus.plugin.OldCombatMechanics.OCMMain
|
||||
import kernitus.plugin.OldCombatMechanics.module.OCMModule
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.Config
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.storage.PlayerModuleOverrides
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.storage.PlayerStorage
|
||||
import org.bukkit.Bukkit
|
||||
import org.bukkit.World
|
||||
import org.bukkit.entity.Player
|
||||
import java.util.Locale
|
||||
|
||||
class OldCombatMechanicsAPIImpl(private val plugin: OCMMain) : OldCombatMechanicsAPI {
|
||||
|
||||
override fun getModesetNames(): Set<String> = Config.getModesetNames()
|
||||
|
||||
override fun getAllowedModesets(world: World): Set<String> = Config.getAllowedModesets(world)
|
||||
|
||||
override fun getModesetForPlayer(player: Player): String? =
|
||||
PlayerStorage.getPlayerData(player.uniqueId).getModesetForWorld(player.world.uid)
|
||||
|
||||
override fun setModesetForPlayer(player: Player, modesetName: String) {
|
||||
val normalisedModesetName = modesetName.lowercase(Locale.ROOT)
|
||||
validateModesetForPlayer(player, normalisedModesetName)
|
||||
|
||||
val worldId = player.world.uid
|
||||
val playerData = PlayerStorage.getPlayerData(player.uniqueId)
|
||||
val previousModeset = playerData.getModesetForWorld(worldId)
|
||||
if (previousModeset == normalisedModesetName) {
|
||||
return
|
||||
}
|
||||
|
||||
playerData.setModesetForWorld(worldId, normalisedModesetName)
|
||||
PlayerStorage.setPlayerData(player.uniqueId, playerData)
|
||||
PlayerStorage.scheduleSave()
|
||||
fireModesetChangeEvent(player, previousModeset, normalisedModesetName, PlayerModesetChangeEvent.Reason.API)
|
||||
notifyPlayerStateChanged(player)
|
||||
}
|
||||
|
||||
override fun forceEnableModuleForPlayer(player: Player, moduleName: String) {
|
||||
setSingleModuleOverride(player, moduleName, PlayerModuleOverride.FORCE_ENABLED)
|
||||
}
|
||||
|
||||
override fun forceDisableModuleForPlayer(player: Player, moduleName: String) {
|
||||
setSingleModuleOverride(player, moduleName, PlayerModuleOverride.FORCE_DISABLED)
|
||||
}
|
||||
|
||||
override fun clearModuleOverrideForPlayer(player: Player, moduleName: String) {
|
||||
val normalisedModuleName = validateModuleName(moduleName)
|
||||
val previousOverride = PlayerModuleOverrides.getOverride(player, normalisedModuleName)
|
||||
if (previousOverride != PlayerModuleOverride.DEFAULT && PlayerModuleOverrides.clearOverride(player, normalisedModuleName)) {
|
||||
fireModuleOverrideChangeEvent(player, normalisedModuleName, previousOverride, PlayerModuleOverride.DEFAULT)
|
||||
notifyPlayerStateChanged(player)
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearAllModuleOverridesForPlayer(player: Player) {
|
||||
val changedOverrides = getModuleOverridesForPlayer(player).toSortedMap()
|
||||
if (changedOverrides.isNotEmpty() && PlayerModuleOverrides.clearAll(player)) {
|
||||
changedOverrides.forEach { (moduleName, previousOverride) ->
|
||||
fireModuleOverrideChangeEvent(player, moduleName, previousOverride, PlayerModuleOverride.DEFAULT)
|
||||
}
|
||||
notifyPlayerStateChanged(player)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getModuleOverrideForPlayer(player: Player, moduleName: String): PlayerModuleOverride {
|
||||
val normalisedModuleName = validateModuleName(moduleName)
|
||||
return PlayerModuleOverrides.getOverride(player, normalisedModuleName)
|
||||
}
|
||||
|
||||
override fun getModuleOverridesForPlayer(player: Player): Map<String, PlayerModuleOverride> =
|
||||
ModuleLoader.getConfigurableModuleNames()
|
||||
.associateWith { PlayerModuleOverrides.getOverride(player, it) }
|
||||
.filter { (_, override) -> override != PlayerModuleOverride.DEFAULT }
|
||||
|
||||
override fun setModuleOverridesForPlayer(player: Player, overrides: Map<String, PlayerModuleOverride>) {
|
||||
val validatedOverrides = validateModuleOverrides(overrides)
|
||||
val changes = validatedOverrides
|
||||
.mapNotNull { (moduleName, override) ->
|
||||
val previousOverride = PlayerModuleOverrides.getOverride(player, moduleName)
|
||||
if (previousOverride == override) null else ModuleOverrideChange(moduleName, previousOverride, override)
|
||||
}
|
||||
.sortedBy { it.moduleName }
|
||||
|
||||
if (changes.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
changes.forEach { (moduleName, _, override) ->
|
||||
if (override == PlayerModuleOverride.DEFAULT) {
|
||||
PlayerModuleOverrides.clearOverride(player, moduleName)
|
||||
} else {
|
||||
PlayerModuleOverrides.setOverride(player, moduleName, override)
|
||||
}
|
||||
}
|
||||
changes.forEach { (moduleName, previousOverride, newOverride) ->
|
||||
fireModuleOverrideChangeEvent(player, moduleName, previousOverride, newOverride)
|
||||
}
|
||||
notifyPlayerStateChanged(player)
|
||||
}
|
||||
|
||||
override fun isModuleEnabledForPlayer(player: Player, moduleName: String): Boolean =
|
||||
getConfigurableModule(moduleName).isEnabled(player)
|
||||
|
||||
override fun hasAnyOverrideForPlayer(player: Player): Boolean =
|
||||
ModuleLoader.getConfigurableModuleNames()
|
||||
.any { PlayerModuleOverrides.getOverride(player, it) != PlayerModuleOverride.DEFAULT }
|
||||
|
||||
override fun getModuleNames(): Set<String> = ModuleLoader.getConfigurableModuleNames()
|
||||
|
||||
private fun validateModesetForPlayer(player: Player, modesetName: String) {
|
||||
if (!Config.getModesets().containsKey(modesetName)) {
|
||||
throw IllegalArgumentException("Unknown modeset: $modesetName")
|
||||
}
|
||||
if (!Config.getAllowedModesets(player.world).contains(modesetName)) {
|
||||
throw IllegalArgumentException("Modeset is not allowed in the player's current world: $modesetName")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getConfigurableModule(moduleName: String): OCMModule =
|
||||
ModuleLoader.getConfigurableModule(moduleName)
|
||||
?: throw IllegalArgumentException("Unknown or non-configurable module: $moduleName")
|
||||
|
||||
private fun validateModuleName(moduleName: String): String {
|
||||
val normalisedModuleName = ModuleLoader.normaliseModuleName(moduleName)
|
||||
getConfigurableModule(normalisedModuleName)
|
||||
return normalisedModuleName
|
||||
}
|
||||
|
||||
private fun validateModuleOverrides(overrides: Map<String, PlayerModuleOverride>): List<Pair<String, PlayerModuleOverride>> {
|
||||
val seenModuleNames = mutableSetOf<String>()
|
||||
return (overrides as Map<*, *>).map { (moduleName, override) ->
|
||||
val validatedModuleName = moduleName as? String
|
||||
?: throw IllegalArgumentException("Module override name must not be null")
|
||||
val validatedOverride = override as? PlayerModuleOverride
|
||||
?: throw IllegalArgumentException("Module override value for $validatedModuleName must not be null")
|
||||
val normalisedModuleName = validateModuleName(validatedModuleName)
|
||||
if (!seenModuleNames.add(normalisedModuleName)) {
|
||||
throw IllegalArgumentException("Duplicate module override name: $normalisedModuleName")
|
||||
}
|
||||
normalisedModuleName to validatedOverride
|
||||
}
|
||||
}
|
||||
|
||||
private fun setSingleModuleOverride(player: Player, moduleName: String, newOverride: PlayerModuleOverride) {
|
||||
val normalisedModuleName = validateModuleName(moduleName)
|
||||
val previousOverride = PlayerModuleOverrides.getOverride(player, normalisedModuleName)
|
||||
if (previousOverride == newOverride) {
|
||||
return
|
||||
}
|
||||
PlayerModuleOverrides.setOverride(player, normalisedModuleName, newOverride)
|
||||
fireModuleOverrideChangeEvent(player, normalisedModuleName, previousOverride, newOverride)
|
||||
notifyPlayerStateChanged(player)
|
||||
}
|
||||
|
||||
private fun fireModesetChangeEvent(
|
||||
player: Player,
|
||||
previousModeset: String?,
|
||||
newModeset: String?,
|
||||
reason: PlayerModesetChangeEvent.Reason,
|
||||
) {
|
||||
Bukkit.getPluginManager().callEvent(
|
||||
PlayerModesetChangeEvent(player, player.world, previousModeset, newModeset, reason),
|
||||
)
|
||||
}
|
||||
|
||||
private fun fireModuleOverrideChangeEvent(
|
||||
player: Player,
|
||||
moduleName: String,
|
||||
previousOverride: PlayerModuleOverride,
|
||||
newOverride: PlayerModuleOverride,
|
||||
) {
|
||||
Bukkit.getPluginManager().callEvent(
|
||||
PlayerModuleOverrideChangeEvent(player, moduleName, previousOverride, newOverride),
|
||||
)
|
||||
}
|
||||
|
||||
private fun notifyPlayerStateChanged(player: Player) {
|
||||
if (Bukkit.isPrimaryThread()) {
|
||||
ModuleLoader.notifyPlayerStateChanged(player)
|
||||
return
|
||||
}
|
||||
Bukkit.getScheduler().runTask(plugin, Runnable { ModuleLoader.notifyPlayerStateChanged(player) })
|
||||
}
|
||||
|
||||
private data class ModuleOverrideChange(
|
||||
val moduleName: String,
|
||||
val previousOverride: PlayerModuleOverride,
|
||||
val newOverride: PlayerModuleOverride,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
package kernitus.plugin.OldCombatMechanics.api;
|
||||
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Fired after a player's stored modeset changes and before OldCombatMechanics reapplies module state.
|
||||
*/
|
||||
public class PlayerModesetChangeEvent extends Event {
|
||||
private static final HandlerList HANDLERS = new HandlerList();
|
||||
|
||||
private final Player player;
|
||||
private final World world;
|
||||
private final String previousModeset;
|
||||
private final String newModeset;
|
||||
private final Reason reason;
|
||||
|
||||
public PlayerModesetChangeEvent(
|
||||
@NotNull Player player,
|
||||
@NotNull World world,
|
||||
@Nullable String previousModeset,
|
||||
@Nullable String newModeset,
|
||||
@NotNull Reason reason
|
||||
) {
|
||||
this.player = player;
|
||||
this.world = world;
|
||||
this.previousModeset = previousModeset;
|
||||
this.newModeset = newModeset;
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Player getPlayer() {
|
||||
return player;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public World getWorld() {
|
||||
return world;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getPreviousModeset() {
|
||||
return previousModeset;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getNewModeset() {
|
||||
return newModeset;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Reason getReason() {
|
||||
return reason;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
public enum Reason {
|
||||
API,
|
||||
COMMAND,
|
||||
WORLD_CHANGE,
|
||||
JOIN
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
package kernitus.plugin.OldCombatMechanics.api
|
||||
|
||||
/**
|
||||
* Per-player override state for a module.
|
||||
*/
|
||||
enum class PlayerModuleOverride {
|
||||
DEFAULT,
|
||||
FORCE_ENABLED,
|
||||
FORCE_DISABLED
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
package kernitus.plugin.OldCombatMechanics.api;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Fired after a player's stored module override changes and before OldCombatMechanics reapplies module state.
|
||||
*/
|
||||
public class PlayerModuleOverrideChangeEvent extends Event {
|
||||
private static final HandlerList HANDLERS = new HandlerList();
|
||||
|
||||
private final Player player;
|
||||
private final String moduleName;
|
||||
private final PlayerModuleOverride previousOverride;
|
||||
private final PlayerModuleOverride newOverride;
|
||||
|
||||
public PlayerModuleOverrideChangeEvent(
|
||||
@NotNull Player player,
|
||||
@NotNull String moduleName,
|
||||
@NotNull PlayerModuleOverride previousOverride,
|
||||
@NotNull PlayerModuleOverride newOverride
|
||||
) {
|
||||
this.player = player;
|
||||
this.moduleName = moduleName;
|
||||
this.previousOverride = previousOverride;
|
||||
this.newOverride = newOverride;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Player getPlayer() {
|
||||
return player;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getModuleName() {
|
||||
return moduleName;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public PlayerModuleOverride getPreviousOverride() {
|
||||
return previousOverride;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public PlayerModuleOverride getNewOverride() {
|
||||
return newOverride;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLERS;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLERS;
|
||||
}
|
||||
}
|
||||
@@ -45,9 +45,7 @@ public class OCMCommandCompleter implements TabCompleter {
|
||||
if (sender instanceof Player) { // Get the modesets allowed in the world player is in
|
||||
final World world = ((Player) sender).getWorld();
|
||||
completions.addAll(
|
||||
Config.getWorlds()
|
||||
// If world not in config, all modesets allowed
|
||||
.getOrDefault(world.getUID(), Config.getModesets().keySet())
|
||||
Config.getAllowedModesets(world.getUID())
|
||||
.stream()
|
||||
.filter(ms -> ms.startsWith(args[1]))
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
@@ -7,6 +7,7 @@ package kernitus.plugin.OldCombatMechanics.commands;
|
||||
|
||||
import kernitus.plugin.OldCombatMechanics.ModuleLoader;
|
||||
import kernitus.plugin.OldCombatMechanics.OCMMain;
|
||||
import kernitus.plugin.OldCombatMechanics.api.PlayerModesetChangeEvent;
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.Config;
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.Messenger;
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.storage.PlayerData;
|
||||
@@ -17,7 +18,6 @@ import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.PluginDescriptionFile;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Locale;
|
||||
@@ -38,11 +38,9 @@ public class OCMCommandHandler implements CommandExecutor {
|
||||
}
|
||||
|
||||
private void help(OCMMain plugin, CommandSender sender) {
|
||||
final PluginDescriptionFile description = plugin.getDescription();
|
||||
|
||||
Messenger.sendNoPrefix(sender, ChatColor.DARK_GRAY + Messenger.HORIZONTAL_BAR);
|
||||
Messenger.sendNoPrefix(sender, "&6&lOldCombatMechanics&e by &ckernitus&e and &cRayzr522&e version &6%s",
|
||||
description.getVersion());
|
||||
plugin.getDisplayVersion());
|
||||
|
||||
if (checkPermissions(sender, Subcommand.reload))
|
||||
Messenger.sendNoPrefix(sender, "&eYou can use &c/ocm reload&e to reload the config file");
|
||||
@@ -120,10 +118,10 @@ public class OCMCommandHandler implements CommandExecutor {
|
||||
}
|
||||
|
||||
final UUID worldId = player.getWorld().getUID();
|
||||
final Set<String> worldModesets = Config.getWorlds().get(worldId);
|
||||
final Set<String> worldModesets = Config.getAllowedModesets(worldId);
|
||||
|
||||
// If modesets null or empty it means not configured, so all are allowed
|
||||
if (worldModesets != null && !worldModesets.isEmpty() && !worldModesets.contains(modesetName)) {
|
||||
// Empty named world lists are resolved as unrestricted, so all are allowed
|
||||
if (!worldModesets.contains(modesetName)) {
|
||||
// Modeset not allowed in current world
|
||||
Messenger.send(sender,
|
||||
Config.getConfig().getString("mode-messages.invalid-modeset",
|
||||
@@ -132,6 +130,11 @@ public class OCMCommandHandler implements CommandExecutor {
|
||||
}
|
||||
|
||||
final PlayerData playerData = PlayerStorage.getPlayerData(player.getUniqueId());
|
||||
final String previousModeset = playerData.getModesetForWorld(worldId);
|
||||
if (modesetName.equals(previousModeset)) {
|
||||
return;
|
||||
}
|
||||
|
||||
playerData.setModesetForWorld(worldId, modesetName);
|
||||
PlayerStorage.setPlayerData(player.getUniqueId(), playerData);
|
||||
PlayerStorage.scheduleSave();
|
||||
@@ -141,9 +144,16 @@ public class OCMCommandHandler implements CommandExecutor {
|
||||
"&4ERROR: &rmode-messages.mode-set string missing"),
|
||||
modesetName);
|
||||
|
||||
Bukkit.getPluginManager().callEvent(new PlayerModesetChangeEvent(
|
||||
player,
|
||||
player.getWorld(),
|
||||
previousModeset,
|
||||
modesetName,
|
||||
PlayerModesetChangeEvent.Reason.COMMAND
|
||||
));
|
||||
|
||||
// Re-apply things like attack speed and collision team
|
||||
final Player playerCopy = player;
|
||||
ModuleLoader.getModules().forEach(module -> module.onModesetChange(playerCopy));
|
||||
ModuleLoader.notifyPlayerStateChanged(player);
|
||||
}
|
||||
|
||||
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command cmd, @NotNull String label,
|
||||
|
||||
@@ -7,7 +7,6 @@ package kernitus.plugin.OldCombatMechanics.module;
|
||||
|
||||
import kernitus.plugin.OldCombatMechanics.OCMMain;
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.Messenger;
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
@@ -30,8 +29,9 @@ import java.util.function.Predicate;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* Applies the 1.8-style attack range (reach + hitbox margin) to melee weapons on 1.21.11+ Paper.
|
||||
* Gracefully disables itself on Spigot or older versions where the AttackRange data component is absent.
|
||||
* Applies the 1.8-style attack range (reach + hitbox margin) to melee weapons when Paper exposes
|
||||
* the required AttackRange data-component API.
|
||||
* Gracefully disables itself on Spigot or other servers where the component API is absent.
|
||||
*/
|
||||
public class ModuleAttackRange extends OCMModule implements Listener {
|
||||
|
||||
@@ -55,19 +55,23 @@ public class ModuleAttackRange extends OCMModule implements Listener {
|
||||
}
|
||||
|
||||
private void initialiseReflection() {
|
||||
if (!Reflector.versionIsNewerOrEqualTo(1, 21, 11)) {
|
||||
supported = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
paperAdapter = new PaperAttackRangeAdapter();
|
||||
supported = true;
|
||||
} catch (Throwable t) {
|
||||
supported = false;
|
||||
Messenger.warn("Attack range component API not available (Paper 1.21.11+ required); module disabled. (" + t.getClass().getSimpleName() + ": " + t.getMessage() + ")");
|
||||
Messenger.warn("Attack range data-component API not available; module disabled. Required Paper data-component classes or methods were not detected. (" + t.getClass().getSimpleName() + ": " + t.getMessage() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether the runtime exposes the Paper AttackRange data-component API this module needs.
|
||||
* Kept public so integration tests can use the same capability signal as production code.
|
||||
*/
|
||||
public static boolean isAttackRangeApiAvailable() {
|
||||
return PaperAttackRangeAdapter.isAvailable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reload() {
|
||||
if (!supported) return;
|
||||
@@ -264,6 +268,15 @@ public class ModuleAttackRange extends OCMModule implements Listener {
|
||||
itemCopyDataFrom = copyMethod;
|
||||
}
|
||||
|
||||
private static boolean isAvailable() {
|
||||
try {
|
||||
new PaperAttackRangeAdapter();
|
||||
return true;
|
||||
} catch (Throwable ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Method findSetDataMethod(Class<?> dctClass, Class<?> valueClass) throws NoSuchMethodException {
|
||||
for (Method m : ItemStack.class.getMethods()) {
|
||||
if (!m.getName().equals("setData")) continue;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package kernitus.plugin.OldCombatMechanics.module;
|
||||
|
||||
import kernitus.plugin.OldCombatMechanics.OCMMain;
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.CompatibilityCapabilities;
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.MathsHelper;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
@@ -17,7 +18,6 @@ import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.player.PlayerItemConsumeEvent;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector;
|
||||
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
@@ -129,9 +129,8 @@ public class ModuleChorusFruit extends OCMModule {
|
||||
Block head = feet.getRelative(BlockFace.UP);
|
||||
Block below = feet.getRelative(BlockFace.DOWN);
|
||||
|
||||
boolean modern = Reflector.versionIsNewerOrEqualTo(1, 13, 0);
|
||||
boolean feetPassable = modern ? feet.isPassable() : !feet.getType().isSolid();
|
||||
boolean headPassable = modern ? head.isPassable() : !head.getType().isSolid();
|
||||
boolean feetPassable = CompatibilityCapabilities.isBlockPassable(feet);
|
||||
boolean headPassable = CompatibilityCapabilities.isBlockPassable(head);
|
||||
|
||||
if (!feetPassable || !headPassable) return false;
|
||||
if (!below.getType().isSolid()) return false;
|
||||
|
||||
+2
-1
@@ -49,7 +49,8 @@ public class ModuleFishingRodVelocity extends OCMModule {
|
||||
public void reload() {
|
||||
random = new Random();
|
||||
|
||||
// Versions 1.14+ have different gravity than previous versions
|
||||
// Semantic gate: Bukkit/Minecraft changed fish-hook gravity in 1.14; there is no API capability that
|
||||
// exposes the vanilla gravity constant, so this boundary deliberately models gameplay behaviour.
|
||||
hasDifferentGravity = Reflector.versionIsNewerOrEqualTo(1, 14, 0);
|
||||
|
||||
if (gravityTask != null) {
|
||||
|
||||
@@ -203,6 +203,8 @@ public class ModuleGoldenApple extends OCMModule {
|
||||
}
|
||||
|
||||
private void applyEffects(LivingEntity target, List<PotionEffect> newEffects) {
|
||||
// Semantic gate: pre-1.13 Bukkit potion replacement needs the explicit override path to preserve the
|
||||
// legacy apple-effect split; capability detection would change behaviour on compatible-looking APIs.
|
||||
final boolean forceOverride = !kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector
|
||||
.versionIsNewerOrEqualTo(1, 13, 0);
|
||||
for (PotionEffect newEffect : newEffects) {
|
||||
|
||||
@@ -43,6 +43,7 @@ public class ModuleOldArmourDurability extends OCMModule {
|
||||
public void onItemDamage(PlayerItemDamageEvent e) {
|
||||
final Player player = e.getPlayer();
|
||||
|
||||
// Armour durability is defender-owned, so do not use the attacker/defender helper here.
|
||||
if (!isEnabled(player)) return;
|
||||
final ItemStack item = e.getItem();
|
||||
final Material itemType = item.getType();
|
||||
|
||||
+2
-7
@@ -7,11 +7,9 @@ package kernitus.plugin.OldCombatMechanics.module;
|
||||
|
||||
import kernitus.plugin.OldCombatMechanics.OCMMain;
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.damage.DefenceUtils;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||
import org.bukkit.event.entity.EntityDamageEvent;
|
||||
|
||||
import java.util.Arrays;
|
||||
@@ -48,11 +46,8 @@ public class ModuleOldArmourStrength extends OCMModule {
|
||||
|
||||
final LivingEntity damagedEntity = (LivingEntity) e.getEntity();
|
||||
|
||||
// If there was an attacker, and he does not have this module enabled, return
|
||||
if (e.getCause() == EntityDamageEvent.DamageCause.ENTITY_ATTACK && e instanceof EntityDamageByEntityEvent) {
|
||||
final Entity damager = ((EntityDamageByEntityEvent) e).getDamager();
|
||||
if(!isEnabled(damager, damagedEntity)) return;
|
||||
}
|
||||
// Armour strength is defensive, so direct attacks and environmental damage are gated by the damaged entity.
|
||||
if (!isEnabled(damagedEntity)) return;
|
||||
|
||||
final Map<EntityDamageEvent.DamageModifier, Double> damageModifiers =
|
||||
Arrays.stream(EntityDamageEvent.DamageModifier.values())
|
||||
|
||||
@@ -15,13 +15,16 @@ import org.bukkit.Bukkit;
|
||||
import org.bukkit.ChatColor;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.HumanEntity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Projectile;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||
import org.bukkit.event.entity.EntityDamageEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.projectiles.ProjectileSource;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Arrays;
|
||||
@@ -80,7 +83,7 @@ public class ModuleOldToolDamage extends OCMModule {
|
||||
final Entity damager = event.getDamager();
|
||||
if (event.getCause() == EntityDamageEvent.DamageCause.THORNS) return;
|
||||
|
||||
if (!isEnabled(damager, event.getDamagee())) return;
|
||||
if (!isEnabledForAttacker(damager)) return;
|
||||
|
||||
final ItemStack weapon = event.getWeapon();
|
||||
final Material weaponMaterial = weapon.getType();
|
||||
@@ -109,8 +112,11 @@ public class ModuleOldToolDamage extends OCMModule {
|
||||
} else {
|
||||
// We check difference as calculation inaccuracies can make it not match
|
||||
if (Math.abs(diff) > 0.0001) {
|
||||
debug("Expected " + expectedBaseDamage + " got " + oldBaseDamage + " ignoring weapon...");
|
||||
return;
|
||||
if (Math.abs(event.getRawDamage() - expectedBaseDamage) > 0.0001 || event.getRawDamage() <= 0) {
|
||||
debug("Expected " + expectedBaseDamage + " got " + oldBaseDamage + " ignoring weapon...");
|
||||
return;
|
||||
}
|
||||
adjustedBase *= oldBaseDamage / event.getRawDamage();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -160,7 +166,7 @@ public class ModuleOldToolDamage extends OCMModule {
|
||||
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
|
||||
public void onTridentProjectile(org.bukkit.event.entity.EntityDamageByEntityEvent event) {
|
||||
if (!HAS_TRIDENT || !TRIDENT_CLASS.isInstance(event.getDamager())) return;
|
||||
if (!isEnabled(event.getDamager(), event.getEntity())) return;
|
||||
if (!isEnabledForAttacker(event.getDamager())) return;
|
||||
|
||||
final double configured = WeaponDamages.getDamage("TRIDENT_THROWN");
|
||||
if (configured <= 0) return;
|
||||
@@ -174,6 +180,20 @@ public class ModuleOldToolDamage extends OCMModule {
|
||||
return isEnabled(player);
|
||||
}
|
||||
|
||||
private boolean isEnabledForAttacker(Entity damager) {
|
||||
// Tool damage is offensive, so player attackers and projectile shooters own the modeset check.
|
||||
if (damager instanceof HumanEntity) {
|
||||
return isEnabled((HumanEntity) damager);
|
||||
}
|
||||
if (damager instanceof Projectile) {
|
||||
final ProjectileSource shooter = ((Projectile) damager).getShooter();
|
||||
if (shooter instanceof HumanEntity) {
|
||||
return isEnabled((HumanEntity) shooter);
|
||||
}
|
||||
}
|
||||
return isEnabled(damager.getWorld());
|
||||
}
|
||||
|
||||
private String formatDamage(double value) {
|
||||
if (value == (long) value) {
|
||||
return Long.toString((long) value);
|
||||
|
||||
@@ -7,10 +7,12 @@ package kernitus.plugin.OldCombatMechanics.module;
|
||||
|
||||
import com.cryptomorin.xseries.XAttribute;
|
||||
import kernitus.plugin.OldCombatMechanics.OCMMain;
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.reflection.Reflector;
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.CompatibilityCapabilities;
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.compatibility.MythicMobsKnockbackBridge;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.attribute.Attribute;
|
||||
import org.bukkit.attribute.AttributeInstance;
|
||||
import com.cryptomorin.xseries.XEnchantment;
|
||||
import org.bukkit.entity.Entity;
|
||||
@@ -53,11 +55,14 @@ public class ModulePlayerKnockback extends OCMModule {
|
||||
// a later, unrelated velocity event (explosions, plugins, etc.).
|
||||
// - Avoid scheduling one task per hit: we run one shared cleanup task only while there is anything pending.
|
||||
private final Map<UUID, PendingKnockback> pendingKnockback = new HashMap<>();
|
||||
private final MythicMobsKnockbackBridge mythicMobsKnockbackBridge;
|
||||
private BukkitTask pendingCleanupTask;
|
||||
private long pendingTickCounter;
|
||||
private final Attribute knockbackResistanceAttribute = XAttribute.KNOCKBACK_RESISTANCE.get();
|
||||
|
||||
public ModulePlayerKnockback(OCMMain plugin) {
|
||||
super(plugin, "old-player-knockback");
|
||||
mythicMobsKnockbackBridge = new MythicMobsKnockbackBridge(plugin);
|
||||
reload();
|
||||
}
|
||||
|
||||
@@ -69,7 +74,7 @@ public class ModulePlayerKnockback extends OCMModule {
|
||||
knockbackExtraHorizontal = module().getDouble("knockback-extra-horizontal", 0.5);
|
||||
knockbackExtraVertical = module().getDouble("knockback-extra-vertical", 0.1);
|
||||
netheriteKnockbackResistance = module().getBoolean("enable-knockback-resistance", false)
|
||||
&& Reflector.versionIsNewerOrEqualTo(1, 16, 0);
|
||||
&& CompatibilityCapabilities.isKnockbackResistanceAvailable();
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
@@ -86,6 +91,10 @@ public class ModulePlayerKnockback extends OCMModule {
|
||||
final UUID uuid = event.getPlayer().getUniqueId();
|
||||
final PendingKnockback pending = pendingKnockback.remove(uuid);
|
||||
if (pending == null) return;
|
||||
if (mythicMobsKnockbackBridge.consumeNoKnockback(uuid, pending.sourceId)) {
|
||||
stopCleanupTaskIfIdle();
|
||||
return;
|
||||
}
|
||||
event.setVelocity(pending.velocity);
|
||||
stopCleanupTaskIfIdle();
|
||||
}
|
||||
@@ -110,7 +119,8 @@ public class ModulePlayerKnockback extends OCMModule {
|
||||
return;
|
||||
}
|
||||
|
||||
final AttributeInstance attribute = damagee.getAttribute(XAttribute.KNOCKBACK_RESISTANCE.get());
|
||||
final AttributeInstance attribute = knockbackResistanceAttribute == null ? null : damagee.getAttribute(knockbackResistanceAttribute);
|
||||
if (attribute == null) return;
|
||||
attribute.getModifiers().forEach(attribute::removeModifier);
|
||||
}
|
||||
|
||||
@@ -191,15 +201,21 @@ public class ModulePlayerKnockback extends OCMModule {
|
||||
if (netheriteKnockbackResistance) {
|
||||
// Allow netherite to affect the horizontal knockback. Each piece of armour
|
||||
// yields 10% resistance
|
||||
final double resistance = 1 - victim.getAttribute(XAttribute.KNOCKBACK_RESISTANCE.get()).getValue();
|
||||
playerVelocity.multiply(new Vector(resistance, 1, resistance));
|
||||
final AttributeInstance attribute = knockbackResistanceAttribute == null ? null : victim.getAttribute(knockbackResistanceAttribute);
|
||||
if (attribute != null) {
|
||||
final double resistance = 1 - attribute.getValue();
|
||||
playerVelocity.multiply(new Vector(resistance, 1, resistance));
|
||||
}
|
||||
}
|
||||
|
||||
final UUID victimId = victim.getUniqueId();
|
||||
if (mythicMobsKnockbackBridge.consumeNoKnockback(victimId, attacker.getUniqueId())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Knockback is sent immediately in 1.8+, there is no reason to send packets
|
||||
// manually
|
||||
pendingKnockback.put(victimId, new PendingKnockback(playerVelocity, pendingTickCounter + 1));
|
||||
pendingKnockback.put(victimId, new PendingKnockback(playerVelocity, attacker.getUniqueId(), pendingTickCounter + 1));
|
||||
ensureCleanupTaskRunning();
|
||||
}
|
||||
|
||||
@@ -236,11 +252,13 @@ public class ModulePlayerKnockback extends OCMModule {
|
||||
|
||||
private static final class PendingKnockback {
|
||||
private final Vector velocity;
|
||||
private final UUID sourceId;
|
||||
private final long expiresAtTick;
|
||||
|
||||
private PendingKnockback(Vector velocity, long expiresAtTick) {
|
||||
private PendingKnockback(Vector velocity, UUID sourceId, long expiresAtTick) {
|
||||
// Defensive clone: callers may re-use/mutate the Vector instance.
|
||||
this.velocity = velocity == null ? new Vector() : velocity.clone();
|
||||
this.sourceId = sourceId;
|
||||
this.expiresAtTick = expiresAtTick;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -55,6 +55,7 @@ public class ModuleShieldDamageReduction extends OCMModule {
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void onItemDamage(PlayerItemDamageEvent e) {
|
||||
final Player player = e.getPlayer();
|
||||
// Shield-blocked armour wear is defender-owned, even when a player attacker caused it.
|
||||
if (!isEnabled(player)) return;
|
||||
final UUID uuid = player.getUniqueId();
|
||||
final ItemStack item = e.getItem();
|
||||
@@ -82,7 +83,8 @@ public class ModuleShieldDamageReduction extends OCMModule {
|
||||
|
||||
final Player player = (Player) entity;
|
||||
|
||||
if (!isEnabled(e.getDamager(), player)) return;
|
||||
// Shield reduction is defender-owned, even when the incoming hit has a player attacker.
|
||||
if (!isEnabled(player)) return;
|
||||
|
||||
// Paper sword blocking sets the BLOCKING modifier to emulate 1.8 sword blocking. This module is for
|
||||
// shield blocking only; do not double-apply a second reduction when the player is blocking with a sword.
|
||||
|
||||
@@ -49,6 +49,7 @@ public class ModuleSwordBlocking extends OCMModule {
|
||||
private java.lang.reflect.Method paperIsBlockingSword;
|
||||
private Method startUsingItemMethod;
|
||||
private boolean startUsingItemMethodResolved;
|
||||
private boolean playerIsHandRaisedUnavailable;
|
||||
private Method craftPlayerGetHandleMethod;
|
||||
private final Map<Class<?>, Method> nmsStartUsingItemCache = new HashMap<>();
|
||||
private Object minClientVersion;
|
||||
@@ -69,17 +70,19 @@ public class ModuleSwordBlocking extends OCMModule {
|
||||
private final Map<EntityInteractionKey, Long> handledEntityInteractions = new HashMap<>();
|
||||
private long nextEntityInteractionPruneAtNanos;
|
||||
private static ModuleSwordBlocking INSTANCE;
|
||||
private boolean blockCanBuildEventPlayerAvailable;
|
||||
private boolean blockCanBuildEventPlayerUnavailable;
|
||||
|
||||
// Only used <1.13, where BlockCanBuildEvent.getPlayer() is not available
|
||||
// Used until BlockCanBuildEvent#getPlayer() is confirmed available, and retained on legacy Bukkit APIs where it is not available.
|
||||
private Map<Location, UUID> lastInteractedBlocks;
|
||||
|
||||
public ModuleSwordBlocking(OCMMain plugin) {
|
||||
super(plugin, "sword-blocking");
|
||||
INSTANCE = this;
|
||||
|
||||
if (!Reflector.versionIsNewerOrEqualTo(1, 13, 0)) {
|
||||
lastInteractedBlocks = new WeakHashMap<>();
|
||||
}
|
||||
// Legacy semantic/API bridge: older servers expose failed block placement without the player, so keep
|
||||
// the last clicked block until direct BlockCanBuildEvent#getPlayer() is known to work on this runtime.
|
||||
lastInteractedBlocks = new WeakHashMap<>();
|
||||
|
||||
initialisePaperAdapter();
|
||||
initialisePacketEventsClientVersion();
|
||||
@@ -265,14 +268,12 @@ public class ModuleSwordBlocking extends OCMModule {
|
||||
public void onBlockPlace(BlockCanBuildEvent e) {
|
||||
if (e.isBuildable()) return;
|
||||
|
||||
Player player;
|
||||
|
||||
// If <1.13 get player who last interacted with block
|
||||
if (lastInteractedBlocks != null) {
|
||||
Player player = getBlockCanBuildEventPlayerIfSupported(e);
|
||||
if (player == null && blockCanBuildEventPlayerUnavailable) {
|
||||
final Location blockLocation = e.getBlock().getLocation();
|
||||
final UUID uuid = lastInteractedBlocks.remove(blockLocation);
|
||||
player = Bukkit.getServer().getPlayer(uuid);
|
||||
} else player = e.getPlayer();
|
||||
final UUID uuid = lastInteractedBlocks == null ? null : lastInteractedBlocks.remove(blockLocation);
|
||||
player = uuid == null ? null : Bukkit.getServer().getPlayer(uuid);
|
||||
}
|
||||
|
||||
if (player == null || !isEnabled(player)) return;
|
||||
|
||||
@@ -291,7 +292,7 @@ public class ModuleSwordBlocking extends OCMModule {
|
||||
// This is also the case if the main hand item was used, e.g. a bow
|
||||
if (action == Action.RIGHT_CLICK_BLOCK && e.getHand() == EquipmentSlot.HAND) return;
|
||||
if (e.isBlockInHand()) {
|
||||
if (lastInteractedBlocks != null) {
|
||||
if (!blockCanBuildEventPlayerAvailable && lastInteractedBlocks != null) {
|
||||
final Block clickedBlock = e.getClickedBlock();
|
||||
if (clickedBlock != null)
|
||||
lastInteractedBlocks.put(clickedBlock.getLocation(), player.getUniqueId());
|
||||
@@ -655,7 +656,33 @@ public class ModuleSwordBlocking extends OCMModule {
|
||||
if (!hasShield(player.getInventory())) return false;
|
||||
|
||||
return player.isBlocking() ||
|
||||
(Reflector.versionIsNewerOrEqualTo(1, 11, 0) && player.isHandRaised());
|
||||
isHandRaisedIfSupported(player);
|
||||
}
|
||||
|
||||
private boolean isHandRaisedIfSupported(Player player) {
|
||||
if (player == null || playerIsHandRaisedUnavailable) return false;
|
||||
try {
|
||||
return player.isHandRaised();
|
||||
} catch (NoSuchMethodError | AbstractMethodError ignored) {
|
||||
playerIsHandRaisedUnavailable = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Player getBlockCanBuildEventPlayerIfSupported(BlockCanBuildEvent e) {
|
||||
if (blockCanBuildEventPlayerUnavailable) return null;
|
||||
try {
|
||||
final Player player = e.getPlayer();
|
||||
blockCanBuildEventPlayerAvailable = true;
|
||||
lastInteractedBlocks = null;
|
||||
return player;
|
||||
} catch (NoSuchMethodError | AbstractMethodError ignored) {
|
||||
blockCanBuildEventPlayerUnavailable = true;
|
||||
if (lastInteractedBlocks == null) {
|
||||
lastInteractedBlocks = new WeakHashMap<>();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasShield(PlayerInventory inventory) {
|
||||
@@ -708,7 +735,7 @@ public class ModuleSwordBlocking extends OCMModule {
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
// Fallback: may work on some combinations, but is not reliable for consumable-based sword blocking.
|
||||
return player.isBlocking() || (Reflector.versionIsNewerOrEqualTo(1, 11, 0) && player.isHandRaised());
|
||||
return player.isBlocking() || isHandRaisedIfSupported(player);
|
||||
}
|
||||
|
||||
/* ---------- Paper consumable component (animation-only) ---------- */
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
*/
|
||||
package kernitus.plugin.OldCombatMechanics.module;
|
||||
|
||||
import kernitus.plugin.OldCombatMechanics.api.PlayerModuleOverride;
|
||||
import kernitus.plugin.OldCombatMechanics.OCMMain;
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.Config;
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.Messenger;
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.storage.PlayerModuleOverrides;
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.storage.PlayerStorage;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.command.CommandSender;
|
||||
@@ -67,6 +69,14 @@ public abstract class OCMModule implements Listener {
|
||||
* Whether this module should be enabled for this player given his current modeset
|
||||
*/
|
||||
public boolean isEnabled(@NotNull HumanEntity humanEntity) {
|
||||
final PlayerModuleOverride playerOverride = PlayerModuleOverrides.getOverride(humanEntity, configName);
|
||||
if (playerOverride == PlayerModuleOverride.FORCE_ENABLED) {
|
||||
return true;
|
||||
}
|
||||
if (playerOverride == PlayerModuleOverride.FORCE_DISABLED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Config.isModuleDisabled(configName)) {
|
||||
return false;
|
||||
}
|
||||
@@ -94,7 +104,7 @@ public abstract class OCMModule implements Listener {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if module should be enabled, giving priority to the attacker, if a human.
|
||||
* Returns if an offensive direct PvP module should be enabled, giving priority to the attacker, if a human.
|
||||
* If neither entity is a human, checks if module should be enabled in the defender's world.
|
||||
*
|
||||
* @param attacker The entity that is performing the attack
|
||||
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
package kernitus.plugin.OldCombatMechanics.utilities;
|
||||
|
||||
import com.cryptomorin.xseries.XAttribute;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.attribute.Attribute;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.configuration.InvalidConfigurationException;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* Runtime API capability checks shared by compatibility-sensitive call sites.
|
||||
*/
|
||||
public final class CompatibilityCapabilities {
|
||||
private static final Method PLAYER_ATTACK_METHOD = findMethod(Player.class, "attack", Entity.class);
|
||||
private static final Method YAML_PARSE_COMMENTS_METHOD = findYamlParseCommentsMethod();
|
||||
private static volatile boolean blockIsPassableAvailable = true;
|
||||
|
||||
private CompatibilityCapabilities() {
|
||||
}
|
||||
|
||||
public static boolean isBlockPassable(Block block) {
|
||||
if (!blockIsPassableAvailable) {
|
||||
return legacyBlockPassable(block);
|
||||
}
|
||||
|
||||
try {
|
||||
// Chorus-fruit safe-location checks may call passability several times per teleport attempt;
|
||||
// direct calls with a cached linkage fallback avoid repeated reflective invocation on supported runtimes.
|
||||
return block.isPassable();
|
||||
} catch (IncompatibleClassChangeError ignored) {
|
||||
blockIsPassableAvailable = false;
|
||||
return legacyBlockPassable(block);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean legacyBlockPassable(Block block) {
|
||||
return !block.getType().isSolid();
|
||||
}
|
||||
|
||||
public static boolean canPreserveYamlComments() {
|
||||
return YAML_PARSE_COMMENTS_METHOD != null;
|
||||
}
|
||||
|
||||
public static boolean isMaterialAvailable(String materialName) {
|
||||
return Material.matchMaterial(materialName) != null;
|
||||
}
|
||||
|
||||
public static boolean isBukkitClassAvailable(String className) {
|
||||
try {
|
||||
Class.forName(className, false, CompatibilityCapabilities.class.getClassLoader());
|
||||
return true;
|
||||
} catch (ClassNotFoundException | LinkageError ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isPlayerAttackApiAvailable(Class<?> playerClass) {
|
||||
return PLAYER_ATTACK_METHOD != null && findMethod(playerClass, "attack", Entity.class) != null;
|
||||
}
|
||||
|
||||
public static boolean isKnockbackResistanceAvailable() {
|
||||
final Attribute knockbackResistanceAttribute = XAttribute.KNOCKBACK_RESISTANCE.get();
|
||||
return isMaterialAvailable("NETHERITE_BOOTS") && knockbackResistanceAttribute != null;
|
||||
}
|
||||
|
||||
public static YamlConfiguration loadCommentPreservingYaml(File file) {
|
||||
final YamlConfiguration configuration = new YamlConfiguration();
|
||||
if (YAML_PARSE_COMMENTS_METHOD != null) {
|
||||
try {
|
||||
YAML_PARSE_COMMENTS_METHOD.invoke(configuration.options(), true);
|
||||
configuration.load(file);
|
||||
return configuration;
|
||||
} catch (IllegalAccessException | InvocationTargetException | IOException | InvalidConfigurationException ignored) {
|
||||
// Fall through to Bukkit's standard loader; the caller can still migrate values safely.
|
||||
}
|
||||
}
|
||||
return YamlConfiguration.loadConfiguration(file);
|
||||
}
|
||||
|
||||
private static Method findMethod(Class<?> clazz, String name) {
|
||||
try {
|
||||
return clazz.getMethod(name);
|
||||
} catch (NoSuchMethodException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Method findMethod(Class<?> clazz, String name, Class<?>... parameterTypes) {
|
||||
try {
|
||||
return clazz.getMethod(name, parameterTypes);
|
||||
} catch (NoSuchMethodException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Method findYamlParseCommentsMethod() {
|
||||
final Method method = findMethod(YamlConfiguration.class, "options");
|
||||
if (method == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return method.getReturnType().getMethod("parseComments", boolean.class);
|
||||
} catch (NoSuchMethodException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,9 +25,10 @@ import java.util.stream.Collectors;
|
||||
public class Config {
|
||||
|
||||
private static final String CONFIG_NAME = "config.yml";
|
||||
private static final String DEFAULT_WORLD_KEY = "__default__";
|
||||
private static OCMMain plugin;
|
||||
private static FileConfiguration config;
|
||||
private static final Map<String, Set<String>> modesets = new HashMap<>();
|
||||
private static final Map<String, Set<String>> modesets = new LinkedHashMap<>();
|
||||
private static final Map<UUID, Set<String>> worlds = new HashMap<>();
|
||||
private static final Set<String> alwaysEnabledModules = new HashSet<>();
|
||||
private static final Set<String> disabledModules = new HashSet<>();
|
||||
@@ -163,6 +164,7 @@ public class Config {
|
||||
|
||||
// Iterate over each modeset
|
||||
for (String key : modesetsSection.getKeys(false)) {
|
||||
final String modesetName = normaliseModesetName(key);
|
||||
// Retrieve the list of module names for the current modeset
|
||||
final List<String> moduleList = modesetsSection.getStringList(key);
|
||||
final Set<String> moduleSet = new HashSet<>();
|
||||
@@ -188,7 +190,7 @@ public class Config {
|
||||
}
|
||||
|
||||
// Add the current modeset and its modules to the map
|
||||
modesets.put(key, moduleSet);
|
||||
modesets.put(modesetName, moduleSet);
|
||||
|
||||
// Add all modules in the current modeset to the tracking set
|
||||
modulesInModesets.addAll(moduleSet);
|
||||
@@ -220,9 +222,12 @@ public class Config {
|
||||
worlds.clear();
|
||||
|
||||
final ConfigurationSection worldsSection = config.getConfigurationSection("worlds");
|
||||
if (worldsSection == null) return;
|
||||
|
||||
// Iterate over each world
|
||||
for (String worldName : worldsSection.getKeys(false)) {
|
||||
if (!worldsSection.isSet(worldName)) continue;
|
||||
if (DEFAULT_WORLD_KEY.equals(worldName)) continue;
|
||||
final World world = Bukkit.getWorld(worldName);
|
||||
if(world == null){
|
||||
Messenger.warn("Configured world " + worldName + " not found, skipping (might be loaded later?)...");
|
||||
@@ -230,6 +235,14 @@ public class Config {
|
||||
}
|
||||
addWorld(world, worldsSection);
|
||||
}
|
||||
|
||||
if (worldsSection.isSet(DEFAULT_WORLD_KEY)) {
|
||||
for (World world : Bukkit.getWorlds()) {
|
||||
if (!worlds.containsKey(world.getUID())) {
|
||||
addWorld(world, worldsSection);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void addWorld(World world){
|
||||
@@ -238,14 +251,41 @@ public class Config {
|
||||
}
|
||||
|
||||
public static void addWorld(World world, ConfigurationSection worldsSection) {
|
||||
if (worldsSection == null) return;
|
||||
|
||||
final String worldKey = worldsSection.isSet(world.getName()) ? world.getName() : DEFAULT_WORLD_KEY;
|
||||
if (!worldsSection.isSet(worldKey)) return;
|
||||
|
||||
// Retrieve the list of modeset names for the current world
|
||||
// Using a linkedhashset to remove duplicates but retain insertion order (important for default modeset)
|
||||
final LinkedHashSet<String> modesetsSet = new LinkedHashSet<>(worldsSection.getStringList(world.getName()));
|
||||
final LinkedHashSet<String> modesetsSet = new LinkedHashSet<>();
|
||||
for (String modesetName : worldsSection.getStringList(worldKey)) {
|
||||
modesetsSet.add(normaliseModesetName(modesetName));
|
||||
}
|
||||
|
||||
// Add the current world and its modesets to the map
|
||||
worlds.put(world.getUID(), modesetsSet);
|
||||
}
|
||||
|
||||
public static Set<String> getAllowedModesets(UUID worldId) {
|
||||
final Set<String> worldModesets = worlds.get(worldId);
|
||||
if (worldModesets == null || worldModesets.isEmpty()) {
|
||||
return getModesetNames();
|
||||
}
|
||||
return Collections.unmodifiableSet(new LinkedHashSet<>(worldModesets));
|
||||
}
|
||||
|
||||
public static Set<String> getAllowedModesets(World world) {
|
||||
if (world == null) {
|
||||
return getModesetNames();
|
||||
}
|
||||
return getAllowedModesets(world.getUID());
|
||||
}
|
||||
|
||||
public static Set<String> getModesetNames() {
|
||||
return Collections.unmodifiableSet(new LinkedHashSet<>(modesets.keySet()));
|
||||
}
|
||||
|
||||
public static void removeWorld(World world){
|
||||
worlds.remove(world.getUID());
|
||||
}
|
||||
@@ -342,4 +382,8 @@ public class Config {
|
||||
private static String normaliseModuleName(String moduleName) {
|
||||
return moduleName == null ? "" : moduleName.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
public static String normaliseModesetName(String modesetName) {
|
||||
return modesetName == null ? "" : modesetName.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
}
|
||||
+403
@@ -0,0 +1,403 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
package kernitus.plugin.OldCombatMechanics.utilities.compatibility;
|
||||
|
||||
import kernitus.plugin.OldCombatMechanics.OCMMain;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.EventException;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.plugin.EventExecutor;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Optional MythicMobs bridge for damage mechanics that explicitly prevent knockback.
|
||||
*
|
||||
* <p>This class deliberately uses reflection only. MythicMobs is not a compile-time
|
||||
* dependency and all public API access is feature-detected at runtime.</p>
|
||||
*/
|
||||
public final class MythicMobsKnockbackBridge {
|
||||
|
||||
private static final String MYTHIC_MOBS_PLUGIN = "MythicMobs";
|
||||
private static final String MYTHIC_DAMAGE_EVENT = "io.lumine.mythic.bukkit.events.MythicDamageEvent";
|
||||
private static final String NO_IMPACT_TAG = "no_impact";
|
||||
|
||||
private final OCMMain plugin;
|
||||
private final Listener listener = new Listener() { };
|
||||
private final Map<UUID, NoKnockbackMarker> markers = new HashMap<>();
|
||||
|
||||
private Method getDamageMetadata;
|
||||
private Method getTarget;
|
||||
private Method getPreventsKnockback;
|
||||
private Method getTags;
|
||||
private Method getData;
|
||||
private Method getCaster;
|
||||
private Method getSource;
|
||||
private BukkitTask cleanupTask;
|
||||
private long tickCounter;
|
||||
private boolean registered;
|
||||
private boolean disabled;
|
||||
private boolean warnedDisabled;
|
||||
private boolean warnedMissingAccessors;
|
||||
|
||||
public MythicMobsKnockbackBridge(OCMMain plugin) {
|
||||
this.plugin = plugin;
|
||||
registerIfAvailable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Consumes a one-shot no-knockback marker for the supplied target and optional source.
|
||||
*
|
||||
* @param targetId target entity UUID
|
||||
* @param sourceId source entity UUID, or {@code null} if the source cannot be mapped cleanly
|
||||
* @return true when OCM knockback should be suppressed
|
||||
*/
|
||||
public boolean consumeNoKnockback(UUID targetId, UUID sourceId) {
|
||||
if (targetId == null) return false;
|
||||
|
||||
final NoKnockbackMarker marker = markers.get(targetId);
|
||||
if (marker == null) return false;
|
||||
if (marker.expiresAtTick <= tickCounter) {
|
||||
markers.remove(targetId);
|
||||
stopCleanupTaskIfIdle();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (marker.sourceId != null && sourceId != null && !marker.sourceId.equals(sourceId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
markers.remove(targetId);
|
||||
stopCleanupTaskIfIdle();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void registerIfAvailable() {
|
||||
try {
|
||||
registerIfAvailableSafely();
|
||||
} catch (LinkageError | RuntimeException ex) {
|
||||
disableBridge("MythicMobs knockback bridge disabled: incompatible MythicMobs API could not be loaded safely", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void registerIfAvailableSafely() {
|
||||
if (!plugin.getServer().getPluginManager().isPluginEnabled(MYTHIC_MOBS_PLUGIN)) return;
|
||||
|
||||
final Class<? extends Event> eventClass;
|
||||
try {
|
||||
eventClass = Class.forName(MYTHIC_DAMAGE_EVENT).asSubclass(Event.class);
|
||||
} catch (ClassNotFoundException | ClassCastException ex) {
|
||||
plugin.getLogger().fine("MythicMobs knockback bridge unavailable: MythicDamageEvent was not found");
|
||||
return;
|
||||
} catch (LinkageError | RuntimeException ex) {
|
||||
disableBridge("MythicMobs knockback bridge disabled: MythicDamageEvent could not be loaded safely", ex);
|
||||
return;
|
||||
}
|
||||
|
||||
getDamageMetadata = publicNoArgMethod(eventClass, "getDamageMetadata");
|
||||
if (getDamageMetadata == null) getDamageMetadata = publicNoArgMethod(eventClass, "getMetadata");
|
||||
getTarget = publicNoArgMethod(eventClass, "getTarget");
|
||||
getCaster = publicNoArgMethod(eventClass, "getCaster");
|
||||
getSource = publicNoArgMethod(eventClass, "getSource");
|
||||
if (getSource == null) getSource = publicNoArgMethod(eventClass, "getDamager");
|
||||
if (getSource == null) getSource = publicNoArgMethod(eventClass, "getAttacker");
|
||||
if (disabled) return;
|
||||
if (getSource == null && getCaster == null) {
|
||||
plugin.getLogger().warning("MythicMobs knockback bridge found no source or caster accessor; no-knockback markers will not be source-specific");
|
||||
}
|
||||
|
||||
if (getDamageMetadata == null) {
|
||||
plugin.getLogger().warning("MythicMobs knockback bridge unavailable: MythicDamageEvent exposes no damage metadata accessor");
|
||||
return;
|
||||
}
|
||||
if (getTarget == null) {
|
||||
plugin.getLogger().warning("MythicMobs knockback bridge unavailable: MythicDamageEvent exposes no target accessor");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
plugin.getServer().getPluginManager().registerEvent(eventClass, listener, EventPriority.MONITOR,
|
||||
new EventExecutor() {
|
||||
@Override
|
||||
public void execute(Listener ignored, Event event) throws EventException {
|
||||
try {
|
||||
handleMythicDamageEvent(event);
|
||||
} catch (LinkageError | RuntimeException ex) {
|
||||
disableBridge("MythicMobs knockback bridge disabled: incompatible MythicMobs API failed while handling damage", ex);
|
||||
}
|
||||
}
|
||||
}, plugin, true);
|
||||
} catch (LinkageError | RuntimeException ex) {
|
||||
disableBridge("MythicMobs knockback bridge disabled: event registration failed for incompatible MythicMobs API", ex);
|
||||
return;
|
||||
}
|
||||
plugin.addDisableListener(() -> HandlerList.unregisterAll(listener));
|
||||
registered = true;
|
||||
}
|
||||
|
||||
private void handleMythicDamageEvent(Event event) {
|
||||
if (disabled) return;
|
||||
|
||||
final Object metadata = invoke(getDamageMetadata, event);
|
||||
if (!preventsKnockback(metadata)) return;
|
||||
|
||||
final Entity target = toBukkitEntity(invoke(getTarget, event));
|
||||
if (target == null) {
|
||||
warnMissingAccessors("MythicMobs knockback bridge could not resolve the MythicDamageEvent target entity");
|
||||
return;
|
||||
}
|
||||
|
||||
final Entity source = toBukkitEntity(invoke(getSource, event));
|
||||
if (source == null) {
|
||||
final Entity caster = toBukkitEntity(invoke(getCaster, event));
|
||||
markNoKnockback(target.getUniqueId(), caster == null ? null : caster.getUniqueId());
|
||||
} else {
|
||||
markNoKnockback(target.getUniqueId(), source.getUniqueId());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean preventsKnockback(Object metadata) {
|
||||
if (disabled) return false;
|
||||
if (metadata == null) return false;
|
||||
|
||||
if (getPreventsKnockback == null || !getPreventsKnockback.getDeclaringClass().isInstance(metadata)) {
|
||||
getPreventsKnockback = publicNoArgMethod(metadata.getClass(), "getPreventsKnockback");
|
||||
}
|
||||
if (Boolean.TRUE.equals(invoke(getPreventsKnockback, metadata))) return true;
|
||||
|
||||
for (String key : knockbackKeys()) {
|
||||
if (Boolean.TRUE.equals(invokeSingleArg(metadata, "getBoolean", key))) return true;
|
||||
if (metadataValuePreventsKnockback(invokeSingleArg(metadata, "getValue", key), 0)) return true;
|
||||
if (metadataValuePreventsKnockback(invokeTwoArgs(metadata, "getOrDefault", key, Boolean.FALSE), 0)) return true;
|
||||
}
|
||||
|
||||
if (getTags == null || !getTags.getDeclaringClass().isInstance(metadata)) {
|
||||
getTags = publicNoArgMethod(metadata.getClass(), "getTags");
|
||||
}
|
||||
if (hasNoImpactTag(invoke(getTags, metadata))) return true;
|
||||
|
||||
if (getData == null || !getData.getDeclaringClass().isInstance(metadata)) {
|
||||
getData = publicNoArgMethod(metadata.getClass(), "getData");
|
||||
}
|
||||
return metadataValuePreventsKnockback(invoke(getData, metadata), 0);
|
||||
}
|
||||
|
||||
private String[] knockbackKeys() {
|
||||
return new String[] {"preventsKnockback", "preventsknockback", "preventKnockback", "preventknockback"};
|
||||
}
|
||||
|
||||
private boolean metadataValuePreventsKnockback(Object value, int depth) {
|
||||
if (value == null || depth > 4) return false;
|
||||
if (Boolean.TRUE.equals(value)) return true;
|
||||
if (value instanceof Map<?, ?>) {
|
||||
for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
|
||||
if (isNoImpactTag(entry.getKey()) || metadataValuePreventsKnockback(entry.getValue(), depth + 1)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (value instanceof Collection<?>) {
|
||||
for (Object item : (Collection<?>) value) {
|
||||
if (metadataValuePreventsKnockback(item, depth + 1)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (value.getClass().isArray()) {
|
||||
final int length = Array.getLength(value);
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (metadataValuePreventsKnockback(Array.get(value, i), depth + 1)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return isNoImpactTag(value);
|
||||
}
|
||||
|
||||
private boolean hasNoImpactTag(Object tags) {
|
||||
if (tags == null) return false;
|
||||
if (tags instanceof Collection<?>) {
|
||||
for (Object tag : (Collection<?>) tags) {
|
||||
if (isNoImpactTag(tag)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (tags.getClass().isArray()) {
|
||||
final int length = Array.getLength(tags);
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (isNoImpactTag(Array.get(tags, i))) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return isNoImpactTag(tags);
|
||||
}
|
||||
|
||||
private boolean isNoImpactTag(Object tag) {
|
||||
if (tag == null) return false;
|
||||
final String value = String.valueOf(tag).toLowerCase(Locale.ROOT);
|
||||
return value.equals(NO_IMPACT_TAG) || value.endsWith("." + NO_IMPACT_TAG)
|
||||
|| value.contains("damagetags.no_impact") || value.contains(NO_IMPACT_TAG);
|
||||
}
|
||||
|
||||
private Entity toBukkitEntity(Object object) {
|
||||
if (object == null) return null;
|
||||
if (object instanceof Entity) return (Entity) object;
|
||||
|
||||
final Object bukkitEntity = invoke(publicNoArgMethod(object.getClass(), "getBukkitEntity"), object);
|
||||
if (bukkitEntity instanceof Entity) return (Entity) bukkitEntity;
|
||||
|
||||
final Object entity = invoke(publicNoArgMethod(object.getClass(), "getEntity"), object);
|
||||
if (entity instanceof Entity) return (Entity) entity;
|
||||
if (entity != object) return toBukkitEntity(entity);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void markNoKnockback(UUID targetId, UUID sourceId) {
|
||||
markers.put(targetId, new NoKnockbackMarker(sourceId, tickCounter + 2));
|
||||
ensureCleanupTaskRunning();
|
||||
}
|
||||
|
||||
private void ensureCleanupTaskRunning() {
|
||||
if (cleanupTask != null) return;
|
||||
cleanupTask = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
|
||||
tickCounter++;
|
||||
if (markers.isEmpty()) {
|
||||
stopCleanupTaskIfIdle();
|
||||
return;
|
||||
}
|
||||
|
||||
final Iterator<Map.Entry<UUID, NoKnockbackMarker>> it = markers.entrySet().iterator();
|
||||
while (it.hasNext()) {
|
||||
if (it.next().getValue().expiresAtTick <= tickCounter) {
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
|
||||
stopCleanupTaskIfIdle();
|
||||
}, 1L, 1L);
|
||||
}
|
||||
|
||||
private void stopCleanupTaskIfIdle() {
|
||||
if (cleanupTask == null || !markers.isEmpty()) return;
|
||||
cleanupTask.cancel();
|
||||
cleanupTask = null;
|
||||
}
|
||||
|
||||
private Object invoke(Method method, Object handle) {
|
||||
return invoke(method, handle, new Object[0]);
|
||||
}
|
||||
|
||||
private Object invokeSingleArg(Object handle, String name, Object arg) {
|
||||
return invoke(publicMethod(handle == null ? null : handle.getClass(), name, arg), handle, arg);
|
||||
}
|
||||
|
||||
private Object invokeTwoArgs(Object handle, String name, Object first, Object second) {
|
||||
return invoke(publicMethod(handle == null ? null : handle.getClass(), name, first, second), handle, first, second);
|
||||
}
|
||||
|
||||
private Object invoke(Method method, Object handle, Object... args) {
|
||||
if (disabled || method == null || handle == null) return null;
|
||||
try {
|
||||
return method.invoke(handle, args);
|
||||
} catch (ReflectiveOperationException | RuntimeException ex) {
|
||||
if (registered) plugin.getLogger().fine("MythicMobs knockback bridge reflection failed: " + ex.getMessage());
|
||||
return null;
|
||||
} catch (LinkageError ex) {
|
||||
disableBridge("MythicMobs knockback bridge disabled: incompatible MythicMobs API failed during reflective access", ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Method publicNoArgMethod(Class<?> type, String name) {
|
||||
if (type == null || name == null) return null;
|
||||
try {
|
||||
for (Method method : type.getMethods()) {
|
||||
if (method.getParameterCount() == 0 && method.getName().equals(name)
|
||||
&& Modifier.isPublic(method.getModifiers())) {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
} catch (LinkageError | RuntimeException ex) {
|
||||
disableBridge("MythicMobs knockback bridge disabled: incompatible MythicMobs API failed during method discovery", ex);
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Method publicMethod(Class<?> type, String name, Object... args) {
|
||||
if (type == null || name == null) return null;
|
||||
try {
|
||||
for (Method method : type.getMethods()) {
|
||||
if (!method.getName().equals(name) || !Modifier.isPublic(method.getModifiers())
|
||||
|| method.getParameterCount() != args.length) continue;
|
||||
if (parametersAccept(method.getParameterTypes(), args)) return method;
|
||||
}
|
||||
} catch (LinkageError | RuntimeException ex) {
|
||||
disableBridge("MythicMobs knockback bridge disabled: incompatible MythicMobs API failed during method discovery", ex);
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void disableBridge(String message, Throwable ex) {
|
||||
disabled = true;
|
||||
if (registered) HandlerList.unregisterAll(listener);
|
||||
registered = false;
|
||||
if (warnedDisabled) return;
|
||||
warnedDisabled = true;
|
||||
plugin.getLogger().log(Level.WARNING, message, ex);
|
||||
}
|
||||
|
||||
private boolean parametersAccept(Class<?>[] parameterTypes, Object[] args) {
|
||||
for (int i = 0; i < parameterTypes.length; i++) {
|
||||
if (args[i] == null) continue;
|
||||
final Class<?> parameterType = wrapPrimitive(parameterTypes[i]);
|
||||
if (!parameterType.isInstance(args[i])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private Class<?> wrapPrimitive(Class<?> type) {
|
||||
if (!type.isPrimitive()) return type;
|
||||
if (type == boolean.class) return Boolean.class;
|
||||
if (type == byte.class) return Byte.class;
|
||||
if (type == short.class) return Short.class;
|
||||
if (type == int.class) return Integer.class;
|
||||
if (type == long.class) return Long.class;
|
||||
if (type == float.class) return Float.class;
|
||||
if (type == double.class) return Double.class;
|
||||
if (type == char.class) return Character.class;
|
||||
return Void.class;
|
||||
}
|
||||
|
||||
private void warnMissingAccessors(String message) {
|
||||
if (warnedMissingAccessors) return;
|
||||
warnedMissingAccessors = true;
|
||||
plugin.getLogger().log(Level.WARNING, message);
|
||||
}
|
||||
|
||||
private static final class NoKnockbackMarker {
|
||||
private final UUID sourceId;
|
||||
private final long expiresAtTick;
|
||||
|
||||
private NoKnockbackMarker(UUID sourceId, long expiresAtTick) {
|
||||
this.sourceId = sourceId;
|
||||
this.expiresAtTick = expiresAtTick;
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
-13
@@ -59,14 +59,13 @@ public class DefenceUtils {
|
||||
);
|
||||
private static final Set<String> warnedUnknownArmourEnchants = new HashSet<>();
|
||||
private static final Set<Enchantment> VANILLA_ENCHANTMENTS = initVanillaEnchantments();
|
||||
private static final Material POINTED_DRIPSTONE = Material.matchMaterial("POINTED_DRIPSTONE");
|
||||
|
||||
// Stalagmite ignores armour but other blocks under CONTACT do not, explicitly
|
||||
// checked below
|
||||
static {
|
||||
if (Reflector.versionIsNewerOrEqualTo(1, 11, 0))
|
||||
ARMOUR_IGNORING_CAUSES.add(EntityDamageEvent.DamageCause.CRAMMING);
|
||||
if (Reflector.versionIsNewerOrEqualTo(1, 17, 0))
|
||||
ARMOUR_IGNORING_CAUSES.add(EntityDamageEvent.DamageCause.FREEZE);
|
||||
addDamageCauseIfPresent(ARMOUR_IGNORING_CAUSES, "CRAMMING");
|
||||
addDamageCauseIfPresent(ARMOUR_IGNORING_CAUSES, "FREEZE");
|
||||
}
|
||||
|
||||
// Method added in 1.15
|
||||
@@ -106,9 +105,9 @@ public class DefenceUtils {
|
||||
// If the damage cause does not ignore armour
|
||||
// If the block they are in is a stalagmite, also ignore armour
|
||||
if (!ARMOUR_IGNORING_CAUSES.contains(damageCause) &&
|
||||
!(Reflector.versionIsNewerOrEqualTo(1, 19, 0) &&
|
||||
!(POINTED_DRIPSTONE != null &&
|
||||
damageCause == EntityDamageEvent.DamageCause.CONTACT &&
|
||||
damagedEntity.getLocation().getBlock().getType() == Material.POINTED_DRIPSTONE)) {
|
||||
damagedEntity.getLocation().getBlock().getType() == POINTED_DRIPSTONE)) {
|
||||
armourReduction = currentDamage * -armourReductionFactor;
|
||||
}
|
||||
damageModifiers.put(EntityDamageEvent.DamageModifier.ARMOR, armourReduction);
|
||||
@@ -316,6 +315,21 @@ public class DefenceUtils {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void addDamageCauseIfPresent(Set<EntityDamageEvent.DamageCause> damageCauses, String name) {
|
||||
final EntityDamageEvent.DamageCause cause = getDamageCause(name);
|
||||
if (cause != null) {
|
||||
damageCauses.add(cause);
|
||||
}
|
||||
}
|
||||
|
||||
private static EntityDamageEvent.DamageCause getDamageCause(String name) {
|
||||
try {
|
||||
return EntityDamageEvent.DamageCause.valueOf(name);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private enum EnchantmentType {
|
||||
// Data from https://minecraft.fandom.com/wiki/Armor#Mechanics
|
||||
PROTECTION(() -> {
|
||||
@@ -335,10 +349,8 @@ public class DefenceUtils {
|
||||
EntityDamageEvent.DamageCause.FALLING_BLOCK,
|
||||
EntityDamageEvent.DamageCause.THORNS,
|
||||
EntityDamageEvent.DamageCause.DRAGON_BREATH);
|
||||
if (Reflector.versionIsNewerOrEqualTo(1, 10, 0))
|
||||
damageCauses.add(EntityDamageEvent.DamageCause.HOT_FLOOR);
|
||||
if (Reflector.versionIsNewerOrEqualTo(1, 12, 0))
|
||||
damageCauses.add(EntityDamageEvent.DamageCause.ENTITY_SWEEP_ATTACK);
|
||||
addDamageCauseIfPresent(damageCauses, "HOT_FLOOR");
|
||||
addDamageCauseIfPresent(damageCauses, "ENTITY_SWEEP_ATTACK");
|
||||
|
||||
return damageCauses;
|
||||
},
|
||||
@@ -349,9 +361,7 @@ public class DefenceUtils {
|
||||
EntityDamageEvent.DamageCause.FIRE_TICK,
|
||||
EntityDamageEvent.DamageCause.LAVA);
|
||||
|
||||
if (Reflector.versionIsNewerOrEqualTo(1, 10, 0)) {
|
||||
damageCauses.add(EntityDamageEvent.DamageCause.HOT_FLOOR);
|
||||
}
|
||||
addDamageCauseIfPresent(damageCauses, "HOT_FLOOR");
|
||||
|
||||
return damageCauses;
|
||||
}, 1.25, XEnchantment.FIRE_PROTECTION.getEnchant()),
|
||||
|
||||
+94
@@ -9,6 +9,7 @@ import kernitus.plugin.OldCombatMechanics.OCMMain;
|
||||
import kernitus.plugin.OldCombatMechanics.module.OCMModule;
|
||||
import kernitus.plugin.OldCombatMechanics.module.ModuleSwordBlocking;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.HumanEntity;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
@@ -16,6 +17,7 @@ import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||
import org.bukkit.event.entity.EntityDamageEvent;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.HashMap;
|
||||
@@ -31,6 +33,9 @@ public class EntityDamageByEntityListener extends OCMModule {
|
||||
private int expirySweepTaskId = -1;
|
||||
private static final long EXPIRY_SWEEP_INTERVAL_TICKS = 20L;
|
||||
private static final long MIN_LAST_DAMAGE_TTL_TICKS = 20L;
|
||||
private static final String[] DAMAGE_TYPE_REGISTRIES = {"damage_type", "damage_types"};
|
||||
private static final String BYPASSES_COOLDOWN_TAG = "bypasses_cooldown";
|
||||
private DamageSourceCooldownBypassSupport damageSourceCooldownBypassSupport;
|
||||
|
||||
public EntityDamageByEntityListener(OCMMain plugin) {
|
||||
super(plugin, "entity-damage-listener");
|
||||
@@ -305,6 +310,12 @@ public class EntityDamageByEntityListener extends OCMModule {
|
||||
}
|
||||
|
||||
private double checkOverdamage(LivingEntity livingDamagee, EntityDamageEvent event, double newDamage) {
|
||||
if (damageSourceBypassesCooldown(event)) {
|
||||
debug("Damage source bypasses cooldown, skipping overdamage immunity", livingDamagee);
|
||||
debug("Damage source bypasses cooldown, skipping overdamage immunity");
|
||||
return newDamage;
|
||||
}
|
||||
|
||||
final double incomingDamage = newDamage; // base damage (before defence), used for baseline tracking
|
||||
final double newLastDamage = Math.max(0, incomingDamage);
|
||||
|
||||
@@ -372,4 +383,87 @@ public class EntityDamageByEntityListener extends OCMModule {
|
||||
lastDamages.remove(uuid);
|
||||
}
|
||||
|
||||
private boolean damageSourceBypassesCooldown(EntityDamageEvent event) {
|
||||
if (damageSourceCooldownBypassSupport == null) {
|
||||
damageSourceCooldownBypassSupport = DamageSourceCooldownBypassSupport.create();
|
||||
}
|
||||
return damageSourceCooldownBypassSupport.isBypassed(event);
|
||||
}
|
||||
|
||||
private static final class DamageSourceCooldownBypassSupport {
|
||||
private final Method getDamageSourceMethod;
|
||||
private final Method getDamageTypeMethod;
|
||||
private final Method isTaggedMethod;
|
||||
private final Object bypassesCooldownTag;
|
||||
|
||||
private DamageSourceCooldownBypassSupport(
|
||||
Method getDamageSourceMethod,
|
||||
Method getDamageTypeMethod,
|
||||
Method isTaggedMethod,
|
||||
Object bypassesCooldownTag
|
||||
) {
|
||||
this.getDamageSourceMethod = getDamageSourceMethod;
|
||||
this.getDamageTypeMethod = getDamageTypeMethod;
|
||||
this.isTaggedMethod = isTaggedMethod;
|
||||
this.bypassesCooldownTag = bypassesCooldownTag;
|
||||
}
|
||||
|
||||
private static DamageSourceCooldownBypassSupport create() {
|
||||
try {
|
||||
final Class<?> damageSourceClass = Class.forName("org.bukkit.damage.DamageSource");
|
||||
final Class<?> damageTypeClass = Class.forName("org.bukkit.damage.DamageType");
|
||||
final Method getDamageSourceMethod = EntityDamageEvent.class.getMethod("getDamageSource");
|
||||
final Method getDamageTypeMethod = damageSourceClass.getMethod("getDamageType");
|
||||
final Object tag = findBypassesCooldownTag(damageTypeClass);
|
||||
if (tag == null) return unsupported();
|
||||
final Method isTaggedMethod = findIsTaggedMethod(tag);
|
||||
if (isTaggedMethod == null) return unsupported();
|
||||
return new DamageSourceCooldownBypassSupport(getDamageSourceMethod, getDamageTypeMethod, isTaggedMethod, tag);
|
||||
} catch (ClassNotFoundException | NoSuchMethodException ignored) {
|
||||
return unsupported();
|
||||
} catch (Exception ignored) {
|
||||
return unsupported();
|
||||
}
|
||||
}
|
||||
|
||||
private static DamageSourceCooldownBypassSupport unsupported() {
|
||||
return new DamageSourceCooldownBypassSupport(null, null, null, null);
|
||||
}
|
||||
|
||||
private static Object findBypassesCooldownTag(Class<?> damageTypeClass) {
|
||||
for (String registry : DAMAGE_TYPE_REGISTRIES) {
|
||||
try {
|
||||
final Object tag = Bukkit.class
|
||||
.getMethod("getTag", String.class, NamespacedKey.class, Class.class)
|
||||
.invoke(null, registry, NamespacedKey.minecraft(BYPASSES_COOLDOWN_TAG), damageTypeClass);
|
||||
if (tag != null) return tag;
|
||||
} catch (Exception ignored) {
|
||||
// Try the next registry spelling; older APIs may not expose damage-type tags at all.
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Method findIsTaggedMethod(Object tag) {
|
||||
for (Method method : tag.getClass().getMethods()) {
|
||||
if (!"isTagged".equals(method.getName()) || method.getParameterTypes().length != 1) continue;
|
||||
return method;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isBypassed(EntityDamageEvent event) {
|
||||
if (bypassesCooldownTag == null) return false;
|
||||
try {
|
||||
final Object damageSource = getDamageSourceMethod.invoke(event);
|
||||
if (damageSource == null) return false;
|
||||
final Object damageType = getDamageTypeMethod.invoke(damageSource);
|
||||
if (damageType == null) return false;
|
||||
return Boolean.TRUE.equals(isTaggedMethod.invoke(bypassesCooldownTag, damageType));
|
||||
} catch (Exception ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+8
-3
@@ -51,9 +51,14 @@ public class Reflector {
|
||||
* @return true if the server version is newer or equal to the one provided
|
||||
*/
|
||||
public static boolean versionIsNewerOrEqualTo(int major, int minor, int patch) {
|
||||
if (getMajorVersion() < major) return false;
|
||||
if (getMinorVersion() < minor) return false;
|
||||
return getPatchVersion() >= patch;
|
||||
return VersionComparator.isNewerOrEqualTo(
|
||||
getMajorVersion(),
|
||||
getMinorVersion(),
|
||||
getPatchVersion(),
|
||||
major,
|
||||
minor,
|
||||
patch
|
||||
);
|
||||
}
|
||||
|
||||
private static int getMajorVersion() {
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
package kernitus.plugin.OldCombatMechanics.utilities.reflection;
|
||||
|
||||
final class VersionComparator {
|
||||
private VersionComparator() {
|
||||
}
|
||||
|
||||
static boolean isNewerOrEqualTo(
|
||||
int currentMajor,
|
||||
int currentMinor,
|
||||
int currentPatch,
|
||||
int targetMajor,
|
||||
int targetMinor,
|
||||
int targetPatch
|
||||
) {
|
||||
if (currentMajor != targetMajor) {
|
||||
return currentMajor > targetMajor;
|
||||
}
|
||||
if (currentMinor != targetMinor) {
|
||||
return currentMinor > targetMinor;
|
||||
}
|
||||
return currentPatch >= targetPatch;
|
||||
}
|
||||
}
|
||||
+33
-8
@@ -7,15 +7,19 @@
|
||||
package kernitus.plugin.OldCombatMechanics.utilities.storage;
|
||||
|
||||
import kernitus.plugin.OldCombatMechanics.OCMMain;
|
||||
import kernitus.plugin.OldCombatMechanics.ModuleLoader;
|
||||
import kernitus.plugin.OldCombatMechanics.api.PlayerModesetChangeEvent;
|
||||
import kernitus.plugin.OldCombatMechanics.module.OCMModule;
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.Config;
|
||||
import kernitus.plugin.OldCombatMechanics.utilities.Messenger;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.player.PlayerChangedWorldEvent;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.bukkit.event.world.WorldLoadEvent;
|
||||
import org.bukkit.event.world.WorldUnloadEvent;
|
||||
|
||||
@@ -38,22 +42,26 @@ public class ModesetListener extends OCMModule {
|
||||
final UUID playerId = player.getUniqueId();
|
||||
final PlayerData playerData = PlayerStorage.getPlayerData(playerId);
|
||||
final String modesetFromName = playerData.getModesetForWorld(event.getFrom().getUID());
|
||||
updateModeset(player, player.getWorld().getUID(), modesetFromName);
|
||||
updateModeset(player, player.getWorld(), modesetFromName, PlayerModesetChangeEvent.Reason.WORLD_CHANGE);
|
||||
}
|
||||
|
||||
private static void updateModeset(Player player, UUID worldId, String modesetFromName) {
|
||||
private static void updateModeset(
|
||||
Player player,
|
||||
World world,
|
||||
String modesetFromName,
|
||||
PlayerModesetChangeEvent.Reason reason
|
||||
) {
|
||||
final UUID worldId = world.getUID();
|
||||
final UUID playerId = player.getUniqueId();
|
||||
final PlayerData playerData = PlayerStorage.getPlayerData(playerId);
|
||||
final String originalModeset = playerData.getModesetForWorld(worldId);
|
||||
String modesetName = playerData.getModesetForWorld(worldId);
|
||||
|
||||
// Get modesets allowed in to world
|
||||
Set<String> allowedModesets = Config.getWorlds().get(worldId);
|
||||
if (allowedModesets == null || allowedModesets.isEmpty())
|
||||
allowedModesets = Config.getModesets().keySet();
|
||||
Set<String> allowedModesets = Config.getAllowedModesets(worldId);
|
||||
|
||||
// If they don't have a modeset in toWorld yet
|
||||
if (modesetName == null) {
|
||||
// If they don't have a modeset in toWorld yet, or the stored one is not allowed there
|
||||
if (modesetName == null || !allowedModesets.contains(modesetName)) {
|
||||
// Try to use modeset of world they are coming from
|
||||
if (modesetFromName != null && allowedModesets.contains(modesetFromName))
|
||||
modesetName = modesetFromName;
|
||||
@@ -72,13 +80,30 @@ public class ModesetListener extends OCMModule {
|
||||
"&4ERROR: &rmode-messages.mode-set string missing"),
|
||||
modesetName
|
||||
);
|
||||
|
||||
Bukkit.getPluginManager().callEvent(new PlayerModesetChangeEvent(
|
||||
player,
|
||||
world,
|
||||
originalModeset,
|
||||
modesetName,
|
||||
reason
|
||||
));
|
||||
|
||||
// Re-apply things like attack speed and collision team
|
||||
ModuleLoader.notifyPlayerStateChanged(player);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||
final Player player = event.getPlayer();
|
||||
updateModeset(player, player.getWorld().getUID(), null);
|
||||
updateModeset(player, player.getWorld(), null, PlayerModesetChangeEvent.Reason.JOIN);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void onPlayerQuit(PlayerQuitEvent event) {
|
||||
final Player player = event.getPlayer();
|
||||
PlayerModuleOverrides.clearAll(player);
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = false)
|
||||
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
package kernitus.plugin.OldCombatMechanics.utilities.storage
|
||||
|
||||
import kernitus.plugin.OldCombatMechanics.ModuleLoader
|
||||
import kernitus.plugin.OldCombatMechanics.api.PlayerModuleOverride
|
||||
import org.bukkit.OfflinePlayer
|
||||
import org.bukkit.entity.HumanEntity
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* In-memory per-player module override store.
|
||||
*/
|
||||
object PlayerModuleOverrides {
|
||||
|
||||
private val overridesByPlayer = ConcurrentHashMap<UUID, ConcurrentHashMap<String, PlayerModuleOverride>>()
|
||||
|
||||
@JvmStatic
|
||||
fun setOverride(human: HumanEntity, moduleName: String, state: PlayerModuleOverride) {
|
||||
requireOnline(human)
|
||||
val normalised = ModuleLoader.normaliseModuleName(moduleName)
|
||||
if (state == PlayerModuleOverride.DEFAULT) {
|
||||
clearOverride(human, normalised)
|
||||
return
|
||||
}
|
||||
overridesByPlayer.computeIfAbsent(human.uniqueId) { ConcurrentHashMap() }[normalised] = state
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getOverride(human: HumanEntity, moduleName: String): PlayerModuleOverride {
|
||||
requireOnline(human)
|
||||
val perPlayer = overridesByPlayer[human.uniqueId] ?: return PlayerModuleOverride.DEFAULT
|
||||
return perPlayer.getOrDefault(ModuleLoader.normaliseModuleName(moduleName), PlayerModuleOverride.DEFAULT)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun clearOverride(human: HumanEntity, moduleName: String): Boolean {
|
||||
requireOnline(human)
|
||||
val perPlayer = overridesByPlayer[human.uniqueId] ?: return false
|
||||
val removed = perPlayer.remove(ModuleLoader.normaliseModuleName(moduleName))
|
||||
if (perPlayer.isEmpty()) overridesByPlayer.remove(human.uniqueId)
|
||||
return removed != null
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun clearAll(human: HumanEntity): Boolean {
|
||||
requireOnline(human)
|
||||
return overridesByPlayer.remove(human.uniqueId) != null
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun clearAll() {
|
||||
overridesByPlayer.clear()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
private fun requireOnline(human: HumanEntity) {
|
||||
val isOnline = when (human) {
|
||||
is OfflinePlayer -> human.isOnline
|
||||
else -> true // HumanEntity (e.g. a living NPC) — assume present in world
|
||||
}
|
||||
require(isOnline) { "HumanEntity ${human.name} is not online" }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
commit=${buildCommit}
|
||||
@@ -8,7 +8,7 @@ authors: [kernitus, Rayzr522]
|
||||
description: Reverts to pre-1.9 combat mechanics
|
||||
website: https://github.com/kernitus/BukkitOldCombatMechanics
|
||||
load: POSTWORLD
|
||||
softdepend: [PlaceholderAPI]
|
||||
softdepend: [PlaceholderAPI, MythicMobs]
|
||||
api-version: 1.13
|
||||
|
||||
commands:
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
package kernitus.plugin.OldCombatMechanics.utilities.reflection
|
||||
|
||||
import io.kotest.core.spec.style.StringSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
|
||||
class VersionComparatorTest : StringSpec({
|
||||
"treats same version as newer or equal" {
|
||||
assertCurrentVersionIsNewerOrEqualTo(1, 21, 11)
|
||||
}
|
||||
|
||||
"treats patch increment as newer than current version" {
|
||||
VersionComparator.isNewerOrEqualTo(1, 21, 12, 1, 21, 11) shouldBe true
|
||||
}
|
||||
|
||||
"treats minor increment as newer than current version" {
|
||||
VersionComparator.isNewerOrEqualTo(1, 22, 0, 1, 21, 11) shouldBe true
|
||||
}
|
||||
|
||||
"treats newest line style version as newer than current version" {
|
||||
VersionComparator.isNewerOrEqualTo(26, 1, 2, 1, 21, 11) shouldBe true
|
||||
}
|
||||
|
||||
"treats older version as older than current version" {
|
||||
VersionComparator.isNewerOrEqualTo(1, 20, 6, 1, 21, 11) shouldBe false
|
||||
}
|
||||
})
|
||||
|
||||
private fun assertCurrentVersionIsNewerOrEqualTo(
|
||||
targetMajor: Int,
|
||||
targetMinor: Int,
|
||||
targetPatch: Int,
|
||||
) {
|
||||
VersionComparator.isNewerOrEqualTo(1, 21, 11, targetMajor, targetMinor, targetPatch) shouldBe true
|
||||
}
|
||||
Reference in new issue
Block a user