Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ ChangeLog.txt
target/
repo/
out/
META-INF/
META-INF/
build.log
6 changes: 5 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ This file is the project's committed home for project-intrinsic agent knowledge:
- Build with JDK 25 or newer - `maven-enforcer-plugin` (`requireJavaVersion [25,)`) fails `validate` on anything older. Emitted plugin bytecode is intentionally Java 8 (major version 52) via `<maven.compiler.release>8</maven.compiler.release>` in `pom.xml`; the target runtime is a Java 17+ Spigot server. Do not "modernize" the bytecode level. Build/verify: `JAVA_HOME=<jdk25> mvn -B clean verify`.
- `de.tr7zw:functional-annotations` is pinned to `0.1-SNAPSHOT` because no released version has ever been published (CodeMC metadata lists only the SNAPSHOT). It is a real compile dependency (`de.tr7zw.annotations.FAUtil`, used by the `nbt` package), so it cannot be dropped. Revisit if upstream ever cuts a release.
- Test harness: JUnit 5 + MockBukkit + Mockito + JaCoCo. `mvn verify` runs tests and produces `target/site/jacoco/` coverage report. JaCoCo excludes `**/nbt/**` (vendored) and `**/ArmorPlus.class` (bootstrap).
- Coverage gate (CORE set): JaCoCo `check-core` execution enforces 100% line AND branch on these pure-logic classes: `PlayerDirectionUtil`, `ColorUtil`, `Piece`, `SetType`, `SetDataType`. Build fails if core coverage drops below 100%. Everything else remains report-only. Excluded from core: `LogUtil` (calls `ArmorPlus.get()` singleton), `nbt.NBTType` (vendored `**/nbt/**` exclusion). When modifying a core class, add tests in the same commit.
- Coverage gate (CORE set): JaCoCo `check-core` execution enforces 100% line AND branch on these pure-logic classes: `PlayerDirectionUtil`, `ColorUtil`, `Piece`, `SetType`, `SetDataType`, `ArmorHandItem`, `model.id.TypedString`, `model.id.StringId`. Build fails if core coverage drops below 100%. Everything else remains report-only. Excluded from core: `LogUtil` (calls `ArmorPlus.get()` singleton), `nbt.NBTType` (vendored `**/nbt/**` exclusion). When modifying a core class, add tests in the same commit.
- JDK/bytecode split: main code compiles to Java 8 (`maven.compiler.release=8`), test code compiles to Java 17 (`maven.compiler.testRelease=17`). The `testRelease` property is picked up automatically by maven-compiler-plugin's default-testCompile execution - no custom execution block needed. Both compile under the same JDK 25 build. MockBukkit + paper-api are Java 17 bytecode, so tests cannot target 8.
- MockBukkit limitation: the plugin cannot be loaded via `MockBukkit.load(ArmorPlus.class)` because the vendored NBT-API performs reflection at enable time that MockBukkit does not model. Use pure-logic unit tests or Mockito for testing pieces that interact with Bukkit APIs.
- Mockito on JDK 25: surefire must pass `-Dnet.bytebuddy.experimental=true` (configured in pom.xml `<argLine>`). Without it, ByteBuddy fails to recognize the JDK 25 class file version.
- Characterization test safety net (Phase 4A): `src/test/java/gg/steve/mc/ap/{data,player,armor,message}` contains characterization tests pinning current behavior of damage calc (`HandSetData.calculateFinalDamage`, `BasicSetData.onHit/onDamage`), wearer tracking (`SetPlayerManager`), warp safety (`WarpUtil.isSafe`), XP multiplier (`ExperienceSetData.onTargetDeath`), potion checks (`SetStatusEffectsManager.potionCheck`), set detection (`Set.isWearingSet/verifyPiece`), and messaging (`MessageType/CommandDebug`). Rearch phases 4B+ MUST keep these tests green - they prove behavior is preserved during extraction. Do NOT "fix" surprising behavior these tests pin; document it and defer to a separate bug-fix PR.
- Integration-only (not netted): `Set.isWearingSet` full integration path (NBT reflection in real server), `SetPlayerManager.init()` (iterates Bukkit.getOnlinePlayers), GUI rendering, PAPI expansion, scheduler-dependent abilities (Fairy/ColorWay/Lightning/Engineer/Traveller tick logic).
- Domain model package: `gg.steve.mc.ap.model` with sub-packages `set`, `ability`, `combat`, `effect`, `notification`, `player`, `id`. Contains pure-Java value types with ZERO Bukkit/NMS/NBT imports. Uses Lombok (`@Value`, `@Builder`) for boilerplate elimination. The `lombok.config` at repo root sets `lombok.addLombokGeneratedAnnotation=true` so JaCoCo auto-excludes generated code.
- Domain model conventions: No class may use the `Manager` suffix (use Registry, Service, Store, Factory, Source, Resolver, etc.). Use the typed ID wrappers in model.id (PlayerId, ArmorSetId, DamageCauseId, PotionEffectId, SoundId) as identity currency - never reference Bukkit Player/ItemStack/World in the model layer. ID wrappers extend the `TypedString`/`StringId` base hierarchy: `TypedString` (abstract) rejects null via commons-lang3 `Validate.notNull` in its constructor and implements `equals`/`hashCode` with `getClass()` checks (different subtypes wrapping the same string are never equal); `StringId` extends it adding `Validate.notEmpty`. Leaf classes are final with a private constructor and an `of()` factory; `PlayerId` extends `TypedString` directly, wrapping a `UUID`. Use `@Value` + `@Builder` for multi-field value types. Annotate `List`/`Set`/`Map` fields with `@Singular` so Lombok's builder produces genuinely unmodifiable defensive copies at `build()` time.
- Lombok dependency: `org.projectlombok:lombok:1.18.38` (provided scope). Annotation processor configured in maven-compiler-plugin's `<annotationProcessorPaths>`. Works with JDK 25 and emits Java 8 bytecode.
- No-Bukkit-in-model rule: verified by grep (no ArchUnit yet). The `model` package must have zero imports of `org.bukkit.*`, `net.minecraft.*`, `de.tr7zw.*`, or any plugin class outside `model`.

## Maintaining this file

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ For more information about the plugin, permissions, and commands please refer to
## Code Coverage

JaCoCo runs on every CI build (`mvn verify`).
The build enforces 100% line and branch coverage on the pure-logic core classes (`PlayerDirectionUtil`, `ColorUtil`, `Piece`, `SetType`, `SetDataType`); a coverage drop on any of these will fail the build.
The build enforces 100% line and branch coverage on the pure-logic core classes (`PlayerDirectionUtil`, `ColorUtil`, `Piece`, `SetType`, `SetDataType`, `ArmorHandItem`, `TypedString`, `StringId`); a coverage drop on any of these will fail the build.
The HTML coverage report is uploaded as the **jacoco-report** artifact on each workflow run - download it from the [Actions tab](https://github.com/nbdSteve/ArmorPlus/actions/workflows/ci.yml).

## Soft Dependencies
Expand Down
2 changes: 2 additions & 0 deletions lombok.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
config.stopBubbling = true
lombok.addLombokGeneratedAnnotation = true
26 changes: 26 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@
<version>3.14.0</version>
<configuration>
<release>${maven.compiler.release}</release>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.38</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
Expand Down Expand Up @@ -107,6 +114,9 @@
<include>gg.steve.mc.ap.armor.Piece</include>
<include>gg.steve.mc.ap.armor.SetType</include>
<include>gg.steve.mc.ap.data.SetDataType</include>
<include>gg.steve.mc.ap.model.ability.ArmorHandItem</include>
<include>gg.steve.mc.ap.model.id.TypedString</include>
<include>gg.steve.mc.ap.model.id.StringId</include>
</includes>
<limits>
<limit>
Expand Down Expand Up @@ -137,6 +147,10 @@
<!-- Replace this with your package! -->
<shadedPattern>gg.steve.mc.ap</shadedPattern>
</relocation>
<relocation>
<pattern>org.apache.commons.lang3</pattern>
<shadedPattern>gg.steve.mc.ap.lib.commons.lang3</shadedPattern>
</relocation>
</relocations>
</configuration>
<executions>
Expand Down Expand Up @@ -229,12 +243,24 @@
<version>1.7</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.17.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.mojang</groupId>
<artifactId>authlib</artifactId>
<version>1.5.21</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.38</version>
<scope>provided</scope>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
Expand Down
25 changes: 25 additions & 0 deletions src/main/java/gg/steve/mc/ap/model/ability/ArmorHandItem.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package gg.steve.mc.ap.model.ability;

import gg.steve.mc.ap.model.id.DamageCauseId;
import lombok.Builder;
import lombok.Value;

@Value
@Builder
public class ArmorHandItem {
double increase;
boolean requireSet;
DamageCauseId damageCause;

/**
* Pure damage calculation mirroring HandSetData.calculateFinalDamage.
* When setIncrease is -1, the set bonus is not applied.
*/
public double calculateFinalDamage(double damage, double setIncrease) {
if (setIncrease != -1) {
double set = setIncrease - 1;
return damage * (set + this.increase);
}
return damage * this.increase;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package gg.steve.mc.ap.model.ability;

public enum ArmorSetAbilityType {
BASIC,
LIGHTNING,
WARP,
POTION,
FALL,
HUNGER,
TRAVELLER,
STUN,
HAND,
ENGINEER,
COLOR_WAY,
EXPERIENCE;
}
17 changes: 17 additions & 0 deletions src/main/java/gg/steve/mc/ap/model/ability/ArmorSetBasicStats.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package gg.steve.mc.ap.model.ability;

import lombok.Builder;
import lombok.Value;

@Value
@Builder
public class ArmorSetBasicStats {
double increase;
double reduction;
double knockback;
double health;
float walkSpeed;
float walkSpeedDefault;
float flySpeed;
float flySpeedDefault;
}
16 changes: 16 additions & 0 deletions src/main/java/gg/steve/mc/ap/model/combat/CombatDamageContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package gg.steve.mc.ap.model.combat;

import gg.steve.mc.ap.model.id.DamageCauseId;
import gg.steve.mc.ap.model.id.PlayerId;
import lombok.Builder;
import lombok.Value;

@Value
@Builder
public class CombatDamageContext {
PlayerId attacker;
PlayerId target;
double baseDamage;
DamageCauseId cause;
boolean projectile;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package gg.steve.mc.ap.model.combat;

import lombok.Value;

/**
* Represents a modification to damage and knockback.
* A null CombatDamageModification at call sites means "no change" - callers should use
* Optional<CombatDamageModification> to represent the absence of modification.
*/
@Value
public class CombatDamageModification {
double newDamage;
double knockbackMultiplier;
}
11 changes: 11 additions & 0 deletions src/main/java/gg/steve/mc/ap/model/effect/NotificationSound.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package gg.steve.mc.ap.model.effect;

import gg.steve.mc.ap.model.id.SoundId;
import lombok.Value;

@Value
public class NotificationSound {
SoundId name;
float volume;
float pitch;
}
11 changes: 11 additions & 0 deletions src/main/java/gg/steve/mc/ap/model/effect/PotionEffect.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package gg.steve.mc.ap.model.effect;

import gg.steve.mc.ap.model.id.PotionEffectId;
import lombok.Value;

@Value
public class PotionEffect {
PotionEffectId type;
int duration;
int amplifier;
}
6 changes: 6 additions & 0 deletions src/main/java/gg/steve/mc/ap/model/id/ArmorSetId.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package gg.steve.mc.ap.model.id;

public final class ArmorSetId extends StringId {
private ArmorSetId(String value) { super(value); }
public static ArmorSetId of(String value) { return new ArmorSetId(value); }
}
6 changes: 6 additions & 0 deletions src/main/java/gg/steve/mc/ap/model/id/DamageCauseId.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package gg.steve.mc.ap.model.id;

public final class DamageCauseId extends StringId {
private DamageCauseId(String value) { super(value); }
public static DamageCauseId of(String value) { return new DamageCauseId(value); }
}
19 changes: 19 additions & 0 deletions src/main/java/gg/steve/mc/ap/model/id/PlayerId.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package gg.steve.mc.ap.model.id;

import java.util.UUID;

public final class PlayerId extends TypedString {

Check warning on line 5 in src/main/java/gg/steve/mc/ap/model/id/PlayerId.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Override the "equals" method in this class.

See more on https://sonarcloud.io/project/issues?id=nbdSteve_Carmor&issues=AZ9JPNL5Dsu0IuwnN0AE&open=AZ9JPNL5Dsu0IuwnN0AE&pullRequest=13
private final UUID uuid;

private PlayerId(UUID value) {
super(value.toString());
this.uuid = value;
}

public UUID getValue() { return uuid; }

public static PlayerId of(UUID value) {
if (value == null) throw new NullPointerException("value must not be null");
return new PlayerId(value);
}
}
6 changes: 6 additions & 0 deletions src/main/java/gg/steve/mc/ap/model/id/PotionEffectId.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package gg.steve.mc.ap.model.id;

public final class PotionEffectId extends StringId {
private PotionEffectId(String value) { super(value); }
public static PotionEffectId of(String value) { return new PotionEffectId(value); }
}
6 changes: 6 additions & 0 deletions src/main/java/gg/steve/mc/ap/model/id/SoundId.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package gg.steve.mc.ap.model.id;

public final class SoundId extends StringId {
private SoundId(String value) { super(value); }
public static SoundId of(String value) { return new SoundId(value); }
}
10 changes: 10 additions & 0 deletions src/main/java/gg/steve/mc/ap/model/id/StringId.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package gg.steve.mc.ap.model.id;

import org.apache.commons.lang3.Validate;

public abstract class StringId extends TypedString {
protected StringId(String id) {
super(id);
Validate.notEmpty(id);
}
}
40 changes: 40 additions & 0 deletions src/main/java/gg/steve/mc/ap/model/id/TypedString.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package gg.steve.mc.ap.model.id;

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;

import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

public abstract class TypedString {
private final String id;

protected TypedString(String id) {
this.id = Objects.requireNonNull(id, "id must not be null");
}

@Override
public int hashCode() {
return new HashCodeBuilder().append(id).toHashCode();
}

@Override
public boolean equals(Object obj) {
if (obj == null) return false;
if (obj == this) return true;
if (obj.getClass() != getClass()) return false;
TypedString other = (TypedString) obj;
return new EqualsBuilder().append(id, other.id).isEquals();
}

@Override
public String toString() {
return id;
}

public static List<String> toStrings(Collection<? extends TypedString> typedStrings) {
return typedStrings.stream().map(TypedString::toString).collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package gg.steve.mc.ap.model.notification;

import gg.steve.mc.ap.model.effect.NotificationSound;
import lombok.Builder;
import lombok.Singular;
import lombok.Value;

import java.util.List;

@Value
@Builder
public class ArmorSetNotification {
@Singular List<String> messages;
NotificationSound sound;
@Singular List<String> commands;
}
11 changes: 11 additions & 0 deletions src/main/java/gg/steve/mc/ap/model/player/ArmorSetWearer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package gg.steve.mc.ap.model.player;

import gg.steve.mc.ap.model.id.ArmorSetId;
import gg.steve.mc.ap.model.id.PlayerId;
import lombok.Value;

@Value
public class ArmorSetWearer {
PlayerId playerId;
ArmorSetId setId;
}
9 changes: 9 additions & 0 deletions src/main/java/gg/steve/mc/ap/model/set/ArmorPieceSlot.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package gg.steve.mc.ap.model.set;

public enum ArmorPieceSlot {
HELMET,
CHESTPLATE,
LEGGINGS,
BOOTS,
HAND;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package gg.steve.mc.ap.model.set;

public enum ArmorSetPurchaseResult {
SUCCESS,
INSUFFICIENT_FUNDS,
NO_PERMISSION;
}
Loading
Loading