docs: add repo-local agent workflow guidance

This commit is contained in:
kernitus committed 2026-05-18 22:49:23 +01:00
1 parent c228809ac9
commit 7627b24863
11 files changed
+777 -237

No files matched your search

@@ -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,74 @@
---
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
- Do not read `build/integration-test-logs/*.log` unless the user explicitly asks for log inspection.
- Prefer compact console output and `plugins/OldCombatMechanicsTest/test-failures.txt` for failure triage.
- 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 unless compact failure output is insufficient and the user approves.
```
## 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`.
+66
View File
@@ -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`.
+64 -237
View File
@@ -2,257 +2,84 @@
**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.
- Do **not** open or read `build/integration-test-logs/*.log` unless the user explicitly asks for log inspection. Prefer compact console summaries and `plugins/OldCombatMechanicsTest/test-failures.txt`.
- 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.
- Do not gate plugin behaviour on assumptions from one Paper/Spigot version without checking compatible API presence.
## 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 `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 `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).
- XSeries is now 13.7.0 for MC/Paper 26.x version parsing support (#862).
- 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.
- 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`; avoid reading those logs without explicit user approval.
- 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`.
- 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`.
- 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.
+70
View File
@@ -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 "$@"
+56
View File
@@ -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'
+61
View File
@@ -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"
+111
View File
@@ -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