Skip to content

Implement type-safe registry keys for Skills and Abilities (#129)#194

Open
dnocturne wants to merge 1 commit into
DiamondDagger590:recodefrom
dnocturne:feature/129-typed-registry-keys
Open

Implement type-safe registry keys for Skills and Abilities (#129)#194
dnocturne wants to merge 1 commit into
DiamondDagger590:recodefrom
dnocturne:feature/129-typed-registry-keys

Conversation

@dnocturne
Copy link
Copy Markdown
Contributor

@dnocturne dnocturne commented Jan 18, 2026

Summary

  • Add AbilityKey<A> and SkillKey<S> for type-safe registry access
  • Add McRPGAbilityKey and McRPGSkillKey with all key constants
  • Refactor codebase to eliminate explicit casts when retrieving abilities/skills

Closes #129

Changes

New Files

  • AbilityKey.java - Generic interface for typed ability keys
  • AbilityKeyImpl.java - Record implementation
  • SkillKey.java - Generic interface for typed skill keys
  • SkillKeyImpl.java - Record implementation
  • McRPGAbilityKey.java - Constants for all McRPG abilities
  • McRPGSkillKey.java - Constants for all McRPG skills

Modified Files

  • AbilityRegistry.java - Added ability(AbilityKey<T>) method and class-based lookup
  • SkillRegistry.java - Added skill(SkillKey<T>) method and class-based lookup
  • 14+ component/command/listener files refactored to use typed keys

Usage

Before (casting required):

HeavySwing heavySwing = (HeavySwing) McRPG.getInstance()
    .registryAccess()
    .registry(McRPGRegistryKey.ABILITY)
    .getRegisteredAbility(HeavySwing.HEAVY_SWING_KEY);

This PR will no doubt require changes, as I made this off my own assumptions reading the issue.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added type-safe ability and skill key system enabling compile-time verified lookups through centralized registries.
  • Refactor

    • Modernized ability registry access patterns across all activation components (herbalism, mining, swords, woodcutting) to leverage new typed key-based retrieval methods.
    • Established centralized ability and skill key repositories for improved consistency and maintainability.

…afety

- Updated various components in the mining, swords, and woodcutting abilities to replace direct ability retrieval with the new McRPGAbilityKey interface.
- Introduced McRPGAbilityKey and McRPGSkillKey interfaces for better organization and access to abilities and skills.
- Enhanced SkillRegistry to support type-safe access to skills using SkillKey.
- Ensured all instances of ability retrieval in components and commands are now using the new key-based approach for consistency and maintainability.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 3, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/us/eunoians/mcrpg/skill/SkillRegistry.java (1)

44-50: ⚠️ Potential issue | 🟠 Major

Prevent class-key collisions in SkillRegistry

register(...) only guards NamespacedKey. If two skills of the same class are registered under different keys, Line 49 overwrites skillsByClass, and later unregister can remove the class mapping for the still-registered entry.

🛠️ Suggested guard + safer remove
     public void register(`@NotNull` Skill skill) {
         if (skills.containsKey(skill.getSkillKey())) {
             throw new IllegalArgumentException("Skill " + skill.getSkillKey() + " already registered");
         }
+        if (skillsByClass.containsKey(skill.getClass())) {
+            throw new IllegalArgumentException("Skill class " + skill.getClass().getName() + " already registered");
+        }
         skills.put(skill.getSkillKey(), skill);
         skillsByClass.put(skill.getClass(), skill);
         Bukkit.getPluginManager().callEvent(new SkillRegisterEvent(skill));
     }
@@
     public void unregisterSkill(`@NotNull` NamespacedKey skillKey) {
         Skill skill = skills.remove(skillKey);
         if (skill != null) {
-            skillsByClass.remove(skill.getClass());
+            skillsByClass.remove(skill.getClass(), skill);
             Bukkit.getPluginManager().callEvent(new SkillUnregisterEvent(skill));
         }
     }

Also applies to: 121-125

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/us/eunoians/mcrpg/skill/SkillRegistry.java` around lines 44 -
50, The register behavior currently only guards by NamespacedKey and writes to
skillsByClass using the skill class, which allows a later registration to
overwrite an existing class entry; update register(Skill) to prevent class-key
collisions by either (a) checking skillsByClass.containsKey(skill.getClass())
and throwing an IllegalArgumentException with a clear message, or (b) changing
skillsByClass from Map<Class<? extends Skill>, Skill> to Map<Class<? extends
Skill>, Collection<Skill>> and adding the skill to the collection (pick one
approach and apply consistently). Also update the corresponding unregister(...)
logic so it only removes the class mapping if the value equals the skill being
removed (or remove the skill from the collection if you choose the collection
approach) to avoid deleting a still-registered sibling; refer to the skills,
skillsByClass fields and the register(...) and unregister(...) methods when
making the changes.
🧹 Nitpick comments (5)
src/main/java/us/eunoians/mcrpg/ability/AbilityKeyImpl.java (1)

12-24: Enforce non-null invariant in the record constructor.

@NotNull is not runtime enforcement; direct construction (new AbilityKeyImpl<>(null)) can violate the contract and cause downstream failures.

Suggested fix
 import org.jetbrains.annotations.NotNull;
+import java.util.Objects;
@@
 public record AbilityKeyImpl<A extends Ability>(`@NotNull` Class<A> ability) implements AbilityKey<A> {
+    public AbilityKeyImpl {
+        Objects.requireNonNull(ability, "ability");
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/us/eunoians/mcrpg/ability/AbilityKeyImpl.java` around lines 12
- 24, The record AbilityKeyImpl currently accepts a null Class at runtime
because `@NotNull` is only a static annotation; add a null-check in the record's
canonical constructor (AbilityKeyImpl) to enforce the invariant (e.g.,
Objects.requireNonNull(ability, "ability")) so direct construction fails fast,
and also apply the same null-check in the static factory method create(`@NotNull`
Class<A> clazz) (or delegate create to the record constructor) to prevent new
AbilityKeyImpl<>(null) or AbilityKeyImpl.create(null) from producing an invalid
instance.
src/main/java/us/eunoians/mcrpg/ability/impl/woodcutting/NymphsVitalityComponents.java (1)

29-29: Consider centralizing repeated NymphsVitality lookup.

Line 29 and Line 44 perform the same registry lookup. Extracting a helper keeps both paths consistent and reduces repeated registry traversal on hot events.

♻️ Proposed refactor
 public class NymphsVitalityComponents {
+    `@NotNull`
+    private static NymphsVitality nymphsVitality() {
+        return McRPG.getInstance().registryAccess().registry(McRPGRegistryKey.ABILITY).ability(McRPGAbilityKey.NYMPHS_VITALITY);
+    }

     private static class NymphsVitalityActivateOnHungerDropComponent implements EventActivatableComponent {
@@
-                NymphsVitality nymphsVitality = McRPG.getInstance().registryAccess().registry(McRPGRegistryKey.ABILITY).ability(McRPGAbilityKey.NYMPHS_VITALITY);
+                NymphsVitality nymphsVitality = nymphsVitality();
@@
     private static class NymphsVitalityActivateOnMoveDropComponent implements EventActivatableComponent {
@@
-                NymphsVitality nymphsVitality = McRPG.getInstance().registryAccess().registry(McRPGRegistryKey.ABILITY).ability(McRPGAbilityKey.NYMPHS_VITALITY);
+                NymphsVitality nymphsVitality = nymphsVitality();

Also applies to: 44-44

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/us/eunoians/mcrpg/ability/impl/woodcutting/NymphsVitalityComponents.java`
at line 29, The NymphsVitality lookup is duplicated (calls to
McRPG.getInstance().registryAccess().registry(McRPGRegistryKey.ABILITY).ability(McRPGAbilityKey.NYMPHS_VITALITY))
— create a single helper method (e.g., a private static getNymphsVitality() in
NymphsVitalityComponents or a shared utility) that returns the NymphsVitality
instance and replace both direct lookups (the uses on line 29 and line 44) with
calls to that helper to centralize the retrieval and avoid repeated registry
traversal.
src/main/java/us/eunoians/mcrpg/gui/ability/slot/remotetransfer/RemoteTransferToggleSlot.java (1)

88-88: Deduplicate RemoteTransfer lookup across slot operations.

Line 88, Line 101, and Line 121 repeat the same registry lookup. A small helper improves readability and reduces repetitive registry access code.

♻️ Proposed refactor
 public class RemoteTransferToggleSlot implements McRPGSlot {
+    `@NotNull`
+    private RemoteTransfer remoteTransfer() {
+        return McRPG.getInstance().registryAccess().registry(McRPGRegistryKey.ABILITY).ability(McRPGAbilityKey.REMOTE_TRANSFER);
+    }
@@
     public boolean isItemDisallowed() {
-        RemoteTransfer remoteTransfer = McRPG.getInstance().registryAccess().registry(McRPGRegistryKey.ABILITY).ability(McRPGAbilityKey.REMOTE_TRANSFER);
+        RemoteTransfer remoteTransfer = remoteTransfer();
@@
     public void toggleItemStack() {
-        RemoteTransfer remoteTransfer = McRPG.getInstance().registryAccess().registry(McRPGRegistryKey.ABILITY).ability(McRPGAbilityKey.REMOTE_TRANSFER);
+        RemoteTransfer remoteTransfer = remoteTransfer();
@@
     public void toggleItemStack(boolean enableItem) {
-        RemoteTransfer remoteTransfer = McRPG.getInstance().registryAccess().registry(McRPGRegistryKey.ABILITY).ability(McRPGAbilityKey.REMOTE_TRANSFER);
+        RemoteTransfer remoteTransfer = remoteTransfer();

Also applies to: 101-101, 121-121

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/us/eunoians/mcrpg/gui/ability/slot/remotetransfer/RemoteTransferToggleSlot.java`
at line 88, The RemoteTransfer registry lookup is repeated in
RemoteTransferToggleSlot; create a single helper to deduplicate it—either add a
private method like getRemoteTransfer() that returns
McRPG.getInstance().registryAccess().registry(McRPGRegistryKey.ABILITY).ability(McRPGAbilityKey.REMOTE_TRANSFER)
or cache the RemoteTransfer in a private field initialized lazily, then replace
the repeated lookups in the methods (the spots currently calling
McRPG.getInstance().registryAccess()...ability(McRPGAbilityKey.REMOTE_TRANSFER))
to call the new helper/field (e.g., getRemoteTransfer() or this.remoteTransfer)
to improve readability and avoid repeated registry access.
src/main/java/us/eunoians/mcrpg/command/link/UnlinkChestCommand.java (1)

65-65: Keep placeholder lookup fully type-safe here too.

Line 65 assigns a typed-key lookup to base Ability; using RemoteTransfer preserves the PR’s type-safety goal end-to-end.

♻️ Proposed refactor
-        Ability ability = RegistryAccess.registryAccess().registry(McRPGRegistryKey.ABILITY).ability(McRPGAbilityKey.REMOTE_TRANSFER);
-        placeholders.put(SKILL.getPlaceholder(), mcRPGPlayer == null ? ability.getName() : ability.getName(mcRPGPlayer));
+        RemoteTransfer remoteTransfer = RegistryAccess.registryAccess().registry(McRPGRegistryKey.ABILITY).ability(McRPGAbilityKey.REMOTE_TRANSFER);
+        placeholders.put(SKILL.getPlaceholder(), mcRPGPlayer == null ? remoteTransfer.getName() : remoteTransfer.getName(mcRPGPlayer));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/us/eunoians/mcrpg/command/link/UnlinkChestCommand.java` at line
65, Replace the raw Ability assignment with a fully typed RemoteTransfer lookup:
change the variable declared at the call to
RegistryAccess.registryAccess().registry(McRPGRegistryKey.ABILITY).ability(McRPGAbilityKey.REMOTE_TRANSFER)
to the concrete RemoteTransfer type (and rename the variable accordingly), add
the RemoteTransfer import if missing, and update any subsequent uses in
UnlinkChestCommand to work with the RemoteTransfer API instead of the base
Ability type.
src/main/java/us/eunoians/mcrpg/ability/impl/mining/RemoteTransfer.java (1)

249-250: Prefer this over registry self-lookup in hot path

Inside RemoteTransfer, fetching RemoteTransfer back from the registry is unnecessary; use this when calling abilityHolder.getAbilityData(...) to avoid repeated registry round-trips.

♻️ Suggested simplification
-        RemoteTransfer remoteTransfer = getPlugin().registryAccess().registry(McRPGRegistryKey.ABILITY).ability(McRPGAbilityKey.REMOTE_TRANSFER);
-        var abilityDataOptional = abilityHolder.getAbilityData(remoteTransfer);
+        var abilityDataOptional = abilityHolder.getAbilityData(this);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/us/eunoians/mcrpg/ability/impl/mining/RemoteTransfer.java`
around lines 249 - 250, The code does an unnecessary registry lookup for
RemoteTransfer before calling abilityHolder.getAbilityData; replace the registry
round‑trip
(getPlugin().registryAccess().registry(McRPGRegistryKey.ABILITY).ability(McRPGAbilityKey.REMOTE_TRANSFER))
with this and call abilityHolder.getAbilityData(this) inside the RemoteTransfer
class to avoid repeated registry lookups and improve hot‑path performance.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/us/eunoians/mcrpg/ability/AbilityRegistry.java`:
- Around line 69-73: In register(Ability) in AbilityRegistry ensure the two maps
(abilities and abilitiesByClass) are updated atomically and prevent silent
overwrites by first checking existing mappings: if abilities contains the
ability.getAbilityKey() mapped to a different instance or abilitiesByClass
contains ability.getClass() mapped to a different instance, throw an exception
(fail-fast) instead of blindly putting; only insert into both maps if both
checks pass (or synchronize the method to make the check+put atomic). Likewise
update the unregister logic (the code around lines handling removal at 149-150)
to only remove entries when the current map value matches the expected ability
(avoid removing a class mapping that was replaced), ensuring coherence between
abilities and abilitiesByClass.

---

Outside diff comments:
In `@src/main/java/us/eunoians/mcrpg/skill/SkillRegistry.java`:
- Around line 44-50: The register behavior currently only guards by
NamespacedKey and writes to skillsByClass using the skill class, which allows a
later registration to overwrite an existing class entry; update register(Skill)
to prevent class-key collisions by either (a) checking
skillsByClass.containsKey(skill.getClass()) and throwing an
IllegalArgumentException with a clear message, or (b) changing skillsByClass
from Map<Class<? extends Skill>, Skill> to Map<Class<? extends Skill>,
Collection<Skill>> and adding the skill to the collection (pick one approach and
apply consistently). Also update the corresponding unregister(...) logic so it
only removes the class mapping if the value equals the skill being removed (or
remove the skill from the collection if you choose the collection approach) to
avoid deleting a still-registered sibling; refer to the skills, skillsByClass
fields and the register(...) and unregister(...) methods when making the
changes.

---

Nitpick comments:
In `@src/main/java/us/eunoians/mcrpg/ability/AbilityKeyImpl.java`:
- Around line 12-24: The record AbilityKeyImpl currently accepts a null Class at
runtime because `@NotNull` is only a static annotation; add a null-check in the
record's canonical constructor (AbilityKeyImpl) to enforce the invariant (e.g.,
Objects.requireNonNull(ability, "ability")) so direct construction fails fast,
and also apply the same null-check in the static factory method create(`@NotNull`
Class<A> clazz) (or delegate create to the record constructor) to prevent new
AbilityKeyImpl<>(null) or AbilityKeyImpl.create(null) from producing an invalid
instance.

In `@src/main/java/us/eunoians/mcrpg/ability/impl/mining/RemoteTransfer.java`:
- Around line 249-250: The code does an unnecessary registry lookup for
RemoteTransfer before calling abilityHolder.getAbilityData; replace the registry
round‑trip
(getPlugin().registryAccess().registry(McRPGRegistryKey.ABILITY).ability(McRPGAbilityKey.REMOTE_TRANSFER))
with this and call abilityHolder.getAbilityData(this) inside the RemoteTransfer
class to avoid repeated registry lookups and improve hot‑path performance.

In
`@src/main/java/us/eunoians/mcrpg/ability/impl/woodcutting/NymphsVitalityComponents.java`:
- Line 29: The NymphsVitality lookup is duplicated (calls to
McRPG.getInstance().registryAccess().registry(McRPGRegistryKey.ABILITY).ability(McRPGAbilityKey.NYMPHS_VITALITY))
— create a single helper method (e.g., a private static getNymphsVitality() in
NymphsVitalityComponents or a shared utility) that returns the NymphsVitality
instance and replace both direct lookups (the uses on line 29 and line 44) with
calls to that helper to centralize the retrieval and avoid repeated registry
traversal.

In `@src/main/java/us/eunoians/mcrpg/command/link/UnlinkChestCommand.java`:
- Line 65: Replace the raw Ability assignment with a fully typed RemoteTransfer
lookup: change the variable declared at the call to
RegistryAccess.registryAccess().registry(McRPGRegistryKey.ABILITY).ability(McRPGAbilityKey.REMOTE_TRANSFER)
to the concrete RemoteTransfer type (and rename the variable accordingly), add
the RemoteTransfer import if missing, and update any subsequent uses in
UnlinkChestCommand to work with the RemoteTransfer API instead of the base
Ability type.

In
`@src/main/java/us/eunoians/mcrpg/gui/ability/slot/remotetransfer/RemoteTransferToggleSlot.java`:
- Line 88: The RemoteTransfer registry lookup is repeated in
RemoteTransferToggleSlot; create a single helper to deduplicate it—either add a
private method like getRemoteTransfer() that returns
McRPG.getInstance().registryAccess().registry(McRPGRegistryKey.ABILITY).ability(McRPGAbilityKey.REMOTE_TRANSFER)
or cache the RemoteTransfer in a private field initialized lazily, then replace
the repeated lookups in the methods (the spots currently calling
McRPG.getInstance().registryAccess()...ability(McRPGAbilityKey.REMOTE_TRANSFER))
to call the new helper/field (e.g., getRemoteTransfer() or this.remoteTransfer)
to improve readability and avoid repeated registry access.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 260a1cf and eda5b88.

📒 Files selected for processing (24)
  • src/main/java/us/eunoians/mcrpg/ability/AbilityKey.java
  • src/main/java/us/eunoians/mcrpg/ability/AbilityKeyImpl.java
  • src/main/java/us/eunoians/mcrpg/ability/AbilityRegistry.java
  • src/main/java/us/eunoians/mcrpg/ability/impl/herbalism/TooManyPlantsComponents.java
  • src/main/java/us/eunoians/mcrpg/ability/impl/mining/ExtraOreComponents.java
  • src/main/java/us/eunoians/mcrpg/ability/impl/mining/ItsATripleComponents.java
  • src/main/java/us/eunoians/mcrpg/ability/impl/mining/RemoteTransfer.java
  • src/main/java/us/eunoians/mcrpg/ability/impl/mining/RemoteTransferComponents.java
  • src/main/java/us/eunoians/mcrpg/ability/impl/swords/DeeperWoundComponents.java
  • src/main/java/us/eunoians/mcrpg/ability/impl/swords/VampireComponents.java
  • src/main/java/us/eunoians/mcrpg/ability/impl/swords/bleed/BleedComponents.java
  • src/main/java/us/eunoians/mcrpg/ability/impl/woodcutting/DryadsGiftComponents.java
  • src/main/java/us/eunoians/mcrpg/ability/impl/woodcutting/ExtraLumberComponents.java
  • src/main/java/us/eunoians/mcrpg/ability/impl/woodcutting/HeavySwingComponents.java
  • src/main/java/us/eunoians/mcrpg/ability/impl/woodcutting/NymphsVitalityComponents.java
  • src/main/java/us/eunoians/mcrpg/command/link/LinkChestCommand.java
  • src/main/java/us/eunoians/mcrpg/command/link/UnlinkChestCommand.java
  • src/main/java/us/eunoians/mcrpg/gui/ability/slot/remotetransfer/RemoteTransferToggleSlot.java
  • src/main/java/us/eunoians/mcrpg/listener/ability/OnBlockDropItemListener.java
  • src/main/java/us/eunoians/mcrpg/registry/McRPGAbilityKey.java
  • src/main/java/us/eunoians/mcrpg/registry/McRPGSkillKey.java
  • src/main/java/us/eunoians/mcrpg/skill/SkillKey.java
  • src/main/java/us/eunoians/mcrpg/skill/SkillKeyImpl.java
  • src/main/java/us/eunoians/mcrpg/skill/SkillRegistry.java

Comment on lines 69 to 73
public void register(@NotNull Ability ability) {
NamespacedKey abilityKey = ability.getAbilityKey();
abilities.put(abilityKey, ability);
abilitiesByClass.put(ability.getClass(), ability);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Prevent abilitiesByClass/abilities desynchronization during registration.

Current put(...) calls allow silent overwrites. Re-registering an existing key/class can leave stale class mappings or remove the wrong class mapping during later unregister, breaking ability(AbilityKey<?>) correctness.

Suggested fail-fast guard (keeps both maps coherent)
 public void register(`@NotNull` Ability ability) {
     NamespacedKey abilityKey = ability.getAbilityKey();
+    if (abilities.containsKey(abilityKey)) {
+        throw new IllegalStateException("Ability key already registered: " + abilityKey);
+    }
+    if (abilitiesByClass.containsKey(ability.getClass())) {
+        throw new IllegalStateException("Ability class already registered: " + ability.getClass().getName());
+    }
     abilities.put(abilityKey, ability);
     abilitiesByClass.put(ability.getClass(), ability);

Also applies to: 149-150

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/us/eunoians/mcrpg/ability/AbilityRegistry.java` around lines 69
- 73, In register(Ability) in AbilityRegistry ensure the two maps (abilities and
abilitiesByClass) are updated atomically and prevent silent overwrites by first
checking existing mappings: if abilities contains the ability.getAbilityKey()
mapped to a different instance or abilitiesByClass contains ability.getClass()
mapped to a different instance, throw an exception (fail-fast) instead of
blindly putting; only insert into both maps if both checks pass (or synchronize
the method to make the check+put atomic). Likewise update the unregister logic
(the code around lines handling removal at 149-150) to only remove entries when
the current map value matches the expected ability (avoid removing a class
mapping that was replaced), ensuring coherence between abilities and
abilitiesByClass.

@github-actions
Copy link
Copy Markdown

GUI/UX Review

No GUI/UX concerns found.

This diff introduces AbilityKey/SkillKey type-safe registry lookup infrastructure and mechanically replaces unchecked casts in ability component classes, listener classes, command classes, and one GUI slot class (RemoteTransferToggleSlot). None of the changes touch slot layout, navigation, localization keys, MiniMessage rendering, filler painting, player feedback, paintInventory/openInventory ordering, GuiManager.trackPlayerGui(), onClick() return values, pagination structure, or any YAML. Every changed line in the GUI-adjacent file (RemoteTransferToggleSlot.java) is a pure internal registry lookup refactor with no behavioral change to what the player sees or interacts with.

@github-actions
Copy link
Copy Markdown

Extensibility Review

Breaking change risk: LOW — New additive API surface with no removals, but one behavioral trap exists that could silently break addon code relying on the class-keyed lookup when ability subclassing is used.


CONCERN: AbilityRegistry.ability(AbilityKey<T>) throws IllegalStateException rather than returning null or Optional<T>, but the older getRegisteredAbility(NamespacedKey) returns null for missing keys. Addon developers who mix both APIs or call ability() before registration is complete (e.g., in an early onEnable hook) will get an unchecked exception with no documented timing contract. | WHY: An addon that defensively calls registered() then ability() across two threads, or calls ability() inside an event listener that fires during server startup before all abilities are registered, will crash without a recoverable path. The Javadoc says "throws IllegalStateException if not registered" but gives no guidance on when registration is guaranteed to be complete. | WHERE: AbilityRegistry#ability(AbilityKey) and SkillRegistry#skill(SkillKey)


CONCERN: McRPGAbilityKey and McRPGSkillKey are interfaces whose fields are public static final constants initialized via AbilityKeyImpl.create() / SkillKeyImpl.create(). Because these are interface constants, they are initialized at class-load time — but AbilityKeyImpl is a record in the same package with a public create() factory. Third-party addons can call AbilityKeyImpl.create(MyCustomAbility.class) directly to register their own typed keys, which is good. However, there is no Javadoc on McRPGAbilityKey or McRPGSkillKey stating that third-party keys are supported, nor is there any example showing how an addon registers and then looks up its own ability via this pattern. | WHY: Addon developers will not know whether creating their own AbilityKey instance and calling ability() is a supported, stable contract or an implementation detail that may change. Without explicit documentation, they may instead fall back to the unsafe cast pattern the diff is trying to eliminate. | WHERE: McRPGAbilityKey (class-level Javadoc), McRPGSkillKey (class-level Javadoc), AbilityKeyImpl#create()


CONCERN: The class-based lookup in abilitiesByClass uses ability.getClass() as the key. If any addon (or future McRPG feature) registers a subclass of an existing concrete ability — e.g., class MyBleed extends Bleed — then McRPGAbilityKey.BLEED (keyed on Bleed.class) will not find MyBleed, while MyBleed.class will shadow or coexist unexpectedly. Conversely, if McRPG internally replaces an ability registration with a subclass, existing addon AbilityKey constants will silently return "not registered." | WHY: Addon developers extending or replacing built-in abilities via the registry are the primary extensibility use case. The class-exact lookup makes this pattern fragile with no documented workaround. | WHERE: AbilityRegistry#register(), AbilityRegistry#ability(), SkillRegistry#register(), SkillRegistry#skill()


CONCERN: McRPGAbilityKey and McRPGSkillKey are interfaces with only constants — no methods. An addon developer implementing McRPGAbilityKey to "extend" it with their own constants is technically valid Java but semantically wrong. There is no final class or @NonExtendable / sealed marker, nor Javadoc warning that the interface is a constant holder and not an extension point. | WHY: Addon developers may implement McRPGAbilityKey expecting some contractual behavior, or may be confused about the intended extension mechanism for adding custom ability keys. | WHERE: McRPGAbilityKey interface declaration, McRPGSkillKey interface declaration


CONCERN: McRPGSkillKey fields (HERBALISM, MINING, SWORDS, WOODCUTTING) have no individual Javadoc. McRPGAbilityKey fields similarly have only grouping comments (// Herbalism abilities) but no per-constant Javadoc describing what skill/tier system governs each ability, what configuration file controls it, or whether it is always registered. | WHY: An addon developer browsing the API cannot determine from the constant alone whether ORE_SCANNER or INSTANT_IRRIGATION might be conditionally absent (e.g., disabled via config), which determines whether they must guard with registered() before calling ability(). | WHERE: All field declarations in McRPGAbilityKey and McRPGSkillKey


CONCERN: AbilityKeyImpl and SkillKeyImpl are public record classes, meaning their equals()/hashCode() are derived from the single Class<A> component. This means two separately constructed AbilityKeyImpl.create(Bleed.class) instances are equal and interchangeable — which is correct for registered() but means addon-defined keys are indistinguishable from McRPGAbilityKey.BLEED in any map or set. This is the intended behavior but is not documented. | WHY: An addon developer who stores their AbilityKey instance expecting identity semantics will be surprised that it equals the built-in constant, and vice versa. Without documentation, this is a latent confusion source. | WHERE: AbilityKeyImpl record declaration, SkillKeyImpl record declaration

@github-actions
Copy link
Copy Markdown

Testing Review

Testing Audit Report


Production files changed:

  • AbilityKey.java (new interface)
  • AbilityKeyImpl.java (new record)
  • AbilityRegistry.java (modified — ability(), registered(), abilitiesByClass map, unregisterAbility())
  • SkillKey.java (new interface)
  • SkillKeyImpl.java (new record)
  • SkillRegistry.java (modified — skill(), registered(), skillsByClass map, unregisterSkill())
  • McRPGAbilityKey.java (new soft-enum interface)
  • McRPGSkillKey.java (new soft-enum interface)
  • TooManyPlantsComponents.java, ExtraOreComponents.java, ItsATripleComponents.java, RemoteTransfer.java, RemoteTransferComponents.java, DeeperWoundComponents.java, VampireComponents.java, BleedComponents.java, DryadsGiftComponents.java, ExtraLumberComponents.java, HeavySwingComponents.java, NymphsVitalityComponents.java (refactored call-sites — cast removed, no logic change)
  • LinkChestCommand.java, UnlinkChestCommand.java, RemoteTransferToggleSlot.java, OnBlockDropItemListener.java (refactored call-sites — cast removed, no logic change)

Test files present: (none identified in the diff)

Coverage gaps: All items listed below.


Findings


CONCERN: AbilityRegistry.ability() has zero test coverage — happy path (registered ability returned with correct type), and the IllegalStateException thrown when the key is not registered.
WHY: This is a new public method with non-trivial logic (null-check, unchecked cast, exception throw — well over 3 lines of effective behaviour). The @SuppressWarnings("unchecked") cast is a silent runtime risk; without a test the cast could silently return the wrong type with no compile-time or test-time detection.
WHERE: AbilityRegistry.java / no corresponding test file


CONCERN: AbilityRegistry.registered() has zero test coverage — neither the true nor false branch.
WHY: New public method with a boolean return that drives control-flow in callers. Both branches must be exercised.
WHERE: AbilityRegistry.java / no corresponding test file


CONCERN: AbilityRegistry.unregisterAbility() modification — the new abilitiesByClass.remove() line is not tested for correctness. Specifically, there is no test confirming that after unregistration abilitiesByClass no longer contains the entry and that a subsequent ability() call throws IllegalStateException.
WHY: The dual-map invariant (both abilities and abilitiesByClass stay in sync through register/unregister cycles) is the most fragile part of this change. A missed remove() on either map would cause stale state. This is exactly the kind of regression a test must pin down.
WHERE: AbilityRegistry.java / no corresponding test file


CONCERN: SkillRegistry.skill() has zero test coverage — happy path and IllegalStateException branch.
WHY: Identical structural risk to AbilityRegistry.ability(): unchecked cast suppressed with @SuppressWarnings, new public method, over 3 lines of logic.
WHERE: SkillRegistry.java / no corresponding test file


CONCERN: SkillRegistry.registered() has zero test coverage — neither the true nor false branch.
WHY: Same rationale as AbilityRegistry.registered().
WHERE: SkillRegistry.java / no corresponding test file


CONCERN: SkillRegistry.unregisterSkill() modification — the new skillsByClass.remove() line is not tested for map-synchronisation correctness.
WHY: Same dual-map invariant risk as in AbilityRegistry. After unregistering, skill() must throw and registered() must return false; neither is asserted anywhere.
WHERE: SkillRegistry.java / no corresponding test file


CONCERN: AbilityKeyImpl.create() and abilityClass() are not tested.
WHY: create() is the public factory; abilityClass() is the only behaviour on the record. While individually trivial, the two together form the contract that every downstream ability() lookup depends on. An identity test (round-trip create(Foo.class).abilityClass() == Foo.class) would pin the contract and catch any future refactor that breaks it.
WHERE: AbilityKeyImpl.java / no corresponding test file


CONCERN: SkillKeyImpl.create() and skillClass() are not tested for the same reason.
WHERE: SkillKeyImpl.java / no corresponding test file


CONCERN: Edge case — ability() called with a key whose abilityClass() matches a superclass of the registered ability (e.g., key holds Ability.class itself, or an intermediate abstract class) is not tested. The abilitiesByClass map uses ability.getClass() as the key at registration time, so a key holding a parent class would silently miss and throw. This boundary is not documented or guarded.
WHY: The @SuppressWarnings("unchecked") cast means a mismatch would surface only at runtime in production. A test with an unregistered-but-related class key would document and enforce the expected behaviour.
WHERE: AbilityRegistry.java / AbilityKeyImpl.java / no corresponding test file


CONCERN: No test validates that McRPGAbilityKey constants are distinct (i.e., two constants do not accidentally share the same Class reference). Since constants are static final fields on an interface initialised at class-load time, a copy-paste error (e.g., create(ExtraOre.class) written twice under different constant names) would be invisible until runtime.
WHY: Coverage completeness — the soft-enum pattern is only safe if uniqueness is guaranteed. A simple loop-over-fields test asserting no duplicate abilityClass() values would catch this class of mistake.
WHERE: McRPGAbilityKey.java / no corresponding test file


CONCERN: Same uniqueness gap applies to McRPGSkillKey constants.
WHERE: McRPGSkillKey.java / no corresponding test file


CONCERN: No test method in the diff follows the givenContext_whenAction_thenOutcome naming convention, and no test carries a @DisplayName annotation — because no tests exist at all for any of the new logic.
WHY: Structural requirement per checklist; every test method must follow the naming convention and carry @DisplayName.
WHERE: All new/modified production classes above


CONCERN: No test method carries at least one assertion — because no tests exist.
WHY: A test with no assertion cannot fail; more precisely here, the absence of any test means no assertion can ever execute.
WHERE: All new/modified production classes above


Summary of coverage gaps:

Gap Risk
AbilityRegistry.ability() — registered and unregistered paths Silent wrong-type cast in production
AbilityRegistry.registered() — true/false Untested control-flow branch
AbilityRegistry.unregisterAbility() — dual-map sync Stale map entry after unregister
SkillRegistry.skill() — registered and unregistered paths Silent wrong-type cast in production
SkillRegistry.registered() — true/false Untested control-flow branch
SkillRegistry.unregisterSkill() — dual-map sync Stale map entry after unregister
AbilityKeyImpl / SkillKeyImpl contract Factory/accessor correctness unverified
McRPGAbilityKey / McRPGSkillKey constant uniqueness Copy-paste duplicates invisible until runtime
Superclass-key miss behaviour Undocumented, unchecked-cast risk

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improve Skill/Ability Registries to use keys

1 participant