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
8 changes: 8 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ jobs:
- name: Run unit tests and build
run: mvn -B --no-transfer-progress clean verify -DskipTests=false -Dmaven.test.skip=false

- name: Upload coverage reports
if: ${{ !cancelled() && hashFiles('target/site/jacoco/jacoco.xml') != '' }}
uses: actions/upload-artifact@v7
with:
name: coverage-reports-${{ github.run_id }}-${{ github.run_attempt }}
path: target/site/jacoco/
if-no-files-found: error

- name: Verify runtime JAR
env:
ARTIFACT_PATH: ${{ steps.dev.outputs.jar }}
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/maven-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ jobs:
echo "jar=target/$final_name.jar" >> "$GITHUB_OUTPUT"
mvn -B --no-transfer-progress clean verify -DskipTests=false -Dmaven.test.skip=false

- name: Upload coverage reports
if: ${{ !cancelled() && hashFiles('target/site/jacoco/jacoco.xml') != '' }}
uses: actions/upload-artifact@v7
with:
name: coverage-reports-${{ github.run_id }}-${{ github.run_attempt }}
path: target/site/jacoco/
if-no-files-found: error

- name: Stage only the release JAR
env:
ARTIFACT_PATH: ${{ steps.maven.outputs.jar }}
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,21 @@ Magic keeps resonance profiles tied to roleplay characters through RPCharacters.

Technical documentation is maintained in [TF-Minecraft/Docs](https://github.com/TF-Minecraft/Docs).

## Tests and coverage

Use Java 21 and the pinned dependencies installed by `.github/scripts/prepare-release.sh`, then run:

```sh
mvn -B --no-transfer-progress clean verify
```

JUnit exercises domain calculations, configuration loading, item persistence, artifact generation,
character sessions, and plugin lifecycle through MockBukkit and isolated external integration mocks.
JaCoCo measures every production class with no coverage exclusions. Maven `verify` requires
100% line, branch, and instruction coverage. The HTML report is
`target/site/jacoco/index.html`; XML and CSV reports are alongside it. CI uploads coverage reports
for builds and releases. Test dependencies are not bundled into the plugin JAR.

## License

Copyright (c) 2026 TF-Minecraft contributors.
Expand Down
77 changes: 77 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,40 @@
</properties>

<dependencies>
<dependency>
<groupId>com.googlecode.json-simple</groupId>
<artifactId>json-simple</artifactId>
<version>1.1.1</version>
<scope>test</scope>
<exclusions>
<exclusion><groupId>junit</groupId><artifactId>junit</artifactId></exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.17</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.20.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockbukkit.mockbukkit</groupId>
<artifactId>mockbukkit-v1.21</artifactId>
<version>4.95.0</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
Expand Down Expand Up @@ -129,6 +163,49 @@
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.4</version>
<configuration>
<systemPropertyVariables>
<slf4j.provider>org.slf4j.simple.SimpleServiceProvider</slf4j.provider>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.15</version>
<executions>
<execution>
<goals><goal>prepare-agent</goal></goals>
</execution>
<execution>
<id>coverage-report</id>
<phase>verify</phase>
<goals><goal>report</goal></goals>
</execution>
<execution>
<id>coverage-check</id>
<phase>verify</phase>
<goals><goal>check</goal></goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit><counter>LINE</counter><value>COVEREDRATIO</value><minimum>1.0</minimum></limit>
<limit><counter>BRANCH</counter><value>COVEREDRATIO</value><minimum>1.0</minimum></limit>
<limit><counter>INSTRUCTION</counter><value>COVEREDRATIO</value><minimum>1.0</minimum></limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
Expand Down
23 changes: 17 additions & 6 deletions src/main/java/net/tfminecraft/magic/Magic.java
Original file line number Diff line number Diff line change
Expand Up @@ -320,14 +320,25 @@ private void copyResourceIfMissing(String relativePath) {
return;
}
target.getParentFile().mkdirs();
try (InputStream in = getResource(relativePath)) {
if (in == null) {
getLogger().warning("Missing bundled resource: " + relativePath);
return;
}
Files.copy(in, target.toPath());
InputStream resource = getResource(relativePath);
if (resource == null) {
getLogger().warning("Missing bundled resource: " + relativePath);
return;
}
try (InputStream in = resource) {
copyDefault(in, target);
} catch (IOException ex) {
getLogger().severe("Failed to copy default resource " + relativePath + ": " + ex.getMessage());
}
}

private static void copyDefault(InputStream in, File target) throws IOException {
var staged = Files.createTempFile(target.getParentFile().toPath(), ".magic-default-", ".tmp");
try {
Files.copy(in, staged, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
Files.move(staged, target.toPath());
} finally {
Files.deleteIfExists(staged);
}
}
}
17 changes: 10 additions & 7 deletions src/main/java/net/tfminecraft/magic/Messages.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,16 @@ public static void loadFromResources() {
}

private static FileConfiguration readBundled() {
try (InputStream in = Magic.plugin.getResource("messages.yml")) {
if (in == null) {
try {
InputStream resource = Magic.plugin.getResource("messages.yml");
if (resource == null) {
return null;
}
FileConfiguration loaded = new YamlConfiguration();
loaded.loadFromString(new String(in.readAllBytes()));
return loaded;
try (InputStream in = resource) {
FileConfiguration loaded = new YamlConfiguration();
loaded.loadFromString(new String(in.readAllBytes()));
return loaded;
}
} catch (Exception ex) {
Magic.plugin.getLogger().warning("[Magic] Failed to load bundled messages.yml: " + ex.getMessage());
return null;
Expand All @@ -68,13 +71,13 @@ public static String get(String path, String... keyValues) {
public static String getRaw(String path) {
if (config != null && config.contains(path)) {
String live = config.getString(path);
if (live != null && !live.isBlank()) {
if (!live.isBlank()) {
return live;
}
}
if (bundled != null && bundled.contains(path)) {
String fallback = bundled.getString(path);
if (fallback != null && !fallback.isBlank()) {
if (!fallback.isBlank()) {
return fallback;
}
}
Expand Down
8 changes: 1 addition & 7 deletions src/main/java/net/tfminecraft/magic/artifact/Artifact.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,7 @@ public static Artifact fromItem(ItemStack stack) {
}

private static String readPrimary(ItemStack stack) {
if (stack == null || !stack.hasItemMeta()) {
return "";
}
ItemMeta meta = stack.getItemMeta();
if (meta == null) {
return "";
}
String stored = meta.getPersistentDataContainer().get(
ArtifactKeys.artifactPrimary(), PersistentDataType.STRING);
return stored == null || stored.isBlank() ? "" : stored.trim().toLowerCase(Locale.ROOT);
Expand Down Expand Up @@ -116,7 +110,7 @@ public Set<String> getCappedElementIds() {

@Override
public String primaryElementId() {
return primary != null && !primary.isBlank() ? primary : aura.highestCapElementId();
return !primary.isBlank() ? primary : aura.highestCapElementId();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,6 @@ public static double forItem(ItemStack stack) {
return clamp(Cache.artifactAuraCap);
}
ItemMeta meta = stack.getItemMeta();
if (meta == null) {
return clamp(Cache.artifactAuraCap);
}
String rarityId = meta.getPersistentDataContainer().get(
ArtifactKeys.artifactRarity(), PersistentDataType.STRING);
String primaryId = meta.getPersistentDataContainer().get(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public static void pruneUsers(ItemStack stack, long nowMs) {
Map<String, Long> users = parseUsers(meta.getPersistentDataContainer().get(
ArtifactKeys.careUsers(), PersistentDataType.STRING));
int before = users.size();
users.entrySet().removeIf(entry -> entry.getValue() == null || entry.getValue() <= nowMs);
users.entrySet().removeIf(entry -> entry.getValue() <= nowMs);
if (users.size() == before) {
return;
}
Expand All @@ -62,7 +62,7 @@ public static double usableFill(double fill, double cap, double muffle) {

public static int activeUserCount(ItemStack stack, long nowMs) {
Map<String, Long> users = readUsers(stack);
users.entrySet().removeIf(entry -> entry.getValue() == null || entry.getValue() <= nowMs);
users.entrySet().removeIf(entry -> entry.getValue() <= nowMs);
return users.size();
}

Expand All @@ -80,9 +80,6 @@ public static boolean apply(ItemStack stack, boolean housed, long nowMs, Persist
return false;
}
ItemMeta meta = metaOf(stack);
if (meta == null) {
return false;
}
PersistentDataContainer root = meta.getPersistentDataContainer();
long last = readLong(root, ArtifactKeys.careLastTick());
double elapsedSec = 0.0;
Expand Down Expand Up @@ -128,7 +125,7 @@ public static void stampUser(ItemStack stack, String characterId, long nowMs) {
String id = characterId.trim();
Map<String, Long> users = parseUsers(meta.getPersistentDataContainer().get(
ArtifactKeys.careUsers(), PersistentDataType.STRING));
users.entrySet().removeIf(entry -> entry.getValue() == null || entry.getValue() <= nowMs);
users.entrySet().removeIf(entry -> entry.getValue() <= nowMs);
users.put(id, nowMs + ArtifactCareCache.usersTtlMs());
writeUsers(meta, users);
stack.setItemMeta(meta);
Expand Down Expand Up @@ -182,7 +179,7 @@ private static Map<String, Long> parseUsers(String raw) {
return users;
}
for (String part : raw.split(";")) {
if (part == null || part.isBlank()) {
if (part.isBlank()) {
continue;
}
int colon = part.lastIndexOf(':');
Expand All @@ -203,24 +200,17 @@ private static Map<String, Long> parseUsers(String raw) {
}

private static void writeUsers(ItemMeta meta, Map<String, Long> users) {
if (users == null || users.isEmpty()) {
if (users.isEmpty()) {
meta.getPersistentDataContainer().remove(ArtifactKeys.careUsers());
return;
}
StringBuilder out = new StringBuilder();
for (Map.Entry<String, Long> entry : users.entrySet()) {
if (entry.getKey() == null || entry.getKey().isBlank() || entry.getValue() == null) {
continue;
}
if (out.length() > 0) {
out.append(';');
}
out.append(entry.getKey()).append(':').append(entry.getValue());
}
if (out.length() == 0) {
meta.getPersistentDataContainer().remove(ArtifactKeys.careUsers());
return;
}
meta.getPersistentDataContainer().set(
ArtifactKeys.careUsers(), PersistentDataType.STRING, out.toString());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public static boolean hasKey(ItemStack stack) {
return false;
}
ItemMeta meta = stack.getItemMeta();
return meta != null && meta.getPersistentDataContainer().has(
return meta.getPersistentDataContainer().has(
ArtifactKeys.artifactId(), PersistentDataType.STRING);
}

Expand All @@ -24,9 +24,6 @@ public static UUID read(ItemStack stack) {
return null;
}
ItemMeta meta = stack.getItemMeta();
if (meta == null) {
return null;
}
String raw = meta.getPersistentDataContainer().get(
ArtifactKeys.artifactId(), PersistentDataType.STRING);
if (raw == null || raw.isBlank()) {
Expand All @@ -48,9 +45,6 @@ public static void writeNew(ItemStack stack) {
return;
}
ItemMeta meta = stack.getItemMeta();
if (meta == null) {
return;
}
meta.getPersistentDataContainer().set(
ArtifactKeys.artifactId(),
PersistentDataType.STRING,
Expand Down
Loading