Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -67,6 +67,14 @@ jobs:
include-hidden-files: true
if-no-files-found: error

- 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: Upload unit test reports
if: ${{ !cancelled() && hashFiles('target/surefire-reports/*.xml') != '' }}
uses: actions/upload-artifact@v7
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 @@ -83,6 +83,14 @@ jobs:
--stage "$RUNNER_TEMP/plugin-release" \
--dependencies .build/plugin-dependencies.json

- 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

- uses: actions/upload-artifact@v7
with:
name: plugin-release-${{ github.sha }}
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ Each workbench holds a project while its materials and required actions are comp

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

## Tests and coverage

Run `mvn -B --no-transfer-progress clean verify` with Java 21 after installing the pinned plugin dependencies used by CI. JUnit 5, Mockito and MockBukkit exercise project definitions and progress, YAML loaders, GUI pagination, permission checks, bench events, plugin lifecycle, and persisted station recovery.

JaCoCo enforces **100% line, branch and instruction coverage** across all production classes, without exclusions. HTML/XML reports appear in `target/site/jacoco/` and are uploaded by build and release CI. External plugin APIs are mocked; a live Minecraft integration run remains useful for server-specific behavior.

## License

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

<dependencies>
<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>local</groupId>
<artifactId>gson</artifactId>
Expand Down Expand Up @@ -86,6 +104,55 @@
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.4</version>
</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>INSTRUCTION</counter>
<value>COVEREDRATIO</value>
<minimum>1.0</minimum>
</limit>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>1.0</minimum>
</limit>
<limit>
<counter>BRANCH</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
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ public boolean onCommand(CommandSender sender, Command cmd, String label, String
sender.sendMessage("§cOnly players can select a project on a bench.");
return true;
}
if (!Permissions.requireUse(p)) return true;
if (args.length < 2) {
p.sendMessage("§cUsage: /woodworking select <projectId>");
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ public final class StationStore {

private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();

private static final Set<java.nio.file.Path> retained = new HashSet<>();

private StationStore() {
}

Expand All @@ -66,6 +68,7 @@ public static File fileFor(Location loc) {
public static void delete(Location loc) {
if (loc == null) return;
File file = fileFor(loc);
if (retained.contains(file.toPath().toAbsolutePath())) return;
if (file.exists() && !file.delete()) {
Log.warn("Failed to delete station file " + file.getName());
}
Expand All @@ -90,6 +93,7 @@ public static void saveAll(Collection<WoodStation> stations) {
if (files == null) return;
for (File file : files) {
if (!file.isFile() || !file.getName().endsWith(".json")) continue;
if (retained.contains(file.toPath().toAbsolutePath())) continue;
if (!keep.contains(file.getName()) && !file.delete()) {
Log.warn("Failed to delete leftover station file " + file.getName());
}
Expand All @@ -108,7 +112,12 @@ public static List<WoodStation> loadAll() {
for (File file : files) {
if (!file.isFile() || !file.getName().endsWith(".json")) continue;
WoodStation station = loadFile(file);
if (station != null) out.add(station);
if (station != null) {
retained.remove(file.toPath().toAbsolutePath());
out.add(station);
} else {
retained.add(file.toPath().toAbsolutePath());
}
}
return out;
}
Expand All @@ -117,6 +126,15 @@ private static void write(WoodStation station, File file) {
StationData data = toData(station);
if (data == null) return;
file.getParentFile().mkdirs();
if (retained.contains(file.toPath().toAbsolutePath())) {
try {
Files.move(file.toPath(), file.toPath().resolveSibling(file.getName() + ".rejected-" + java.util.UUID.randomUUID()));
Comment thread
ryanbarlow97 marked this conversation as resolved.
retained.remove(file.toPath().toAbsolutePath());
} catch (IOException ex) {
Log.warn("Failed to preserve rejected station " + file.getName() + ": " + ex.getMessage());
return;
}
}
try (Writer writer = Files.newBufferedWriter(file.toPath(), StandardCharsets.UTF_8)) {
GSON.toJson(data, writer);
} catch (IOException ex) {
Expand All @@ -125,32 +143,33 @@ private static void write(WoodStation station, File file) {
}

private static WoodStation loadFile(File file) {
StationData data;
try (Reader reader = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) {
StationData data = GSON.fromJson(reader, StationData.class);
if (data == null || data.world == null || data.project == null) {
Log.warn("Invalid station file " + file.getName() + ", leaving it on disk.");
return null;
}
World world = Bukkit.getWorld(data.world);
if (world == null) {
Log.warn("Station file " + file.getName() + " world '" + data.world + "' is missing, leaving it on disk.");
return null;
}
WoodProject project = ProjectLoader.getByString(data.project);
if (project == null) {
Log.warn("Station file " + file.getName() + " unknown project '" + data.project
+ "', leaving it on disk.");
return null;
}
Location loc = StationManager.key(new Location(world, data.x, data.y, data.z));
WoodStation station = new WoodStation(loc);
station.setProject(project);
station.applySavedProgress(data.materials, data.hits, decodeItems(data.deposited));
return station;
} catch (IOException ex) {
data = GSON.fromJson(reader, StationData.class);
} catch (IOException | com.google.gson.JsonParseException ex) {
Log.warn("Failed to read station file " + file.getName() + ": " + ex.getMessage());
return null;
}
if (data == null || data.world == null || data.project == null) {
Log.warn("Invalid station file " + file.getName() + ", leaving it on disk.");
return null;
}
World world = Bukkit.getWorld(data.world);
if (world == null) {
Log.warn("Station file " + file.getName() + " world '" + data.world + "' is missing, leaving it on disk.");
return null;
}
WoodProject project = ProjectLoader.getByString(data.project);
if (project == null) {
Log.warn("Station file " + file.getName() + " unknown project '" + data.project
+ "', leaving it on disk.");
return null;
}
Location loc = StationManager.key(new Location(world, data.x, data.y, data.z));
WoodStation station = new WoodStation(loc);
station.setProject(project);
station.applySavedProgress(data.materials, data.hits, decodeItems(data.deposited));
return station;
}

private static StationData toData(WoodStation station) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ public WoodProject(String key, String categoryId, ConfigurationSection config) {

/** Splits "whittle.3" into id and amount. Returns null when malformed. */
private static String[] split(String entry) {
if (entry == null) return null;
int i = entry.lastIndexOf('.');
if (i <= 0 || i == entry.length() - 1) return null;
String amount = entry.substring(i + 1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ public static Location key(Location loc) {

@EventHandler
public void onInteract(PlayerInteractEvent e) {
if (e.getHand() != org.bukkit.inventory.EquipmentSlot.HAND) return;
if (e.getClickedBlock() == null) return;
Action action = e.getAction();
if (action != Action.RIGHT_CLICK_BLOCK && action != Action.LEFT_CLICK_BLOCK) return;
Expand Down Expand Up @@ -281,27 +282,15 @@ private void handleRightClick(PlayerInteractEvent e) {

e.setCancelled(true);
markCooldown(p);
StationFeedback feedback = existing.addMaterial(material, hand);
switch (feedback) {
case SUCCESS:
consumeOne(p);
markDirty();
IntCounter bucket = existing.getTypes().get(material.getType());
String progress = bucket == null ? "" : bucket.getCurrent() + "/" + bucket.getNeeded();
p.sendTitle("§aAdded " + material.getName(), MaterialTypeLoader.display(material.getType()) + " §e" + progress, 5, 20, 5);
playWorkFx(existing.getLoc(), Material.OAK_LOG);
p.getWorld().playSound(existing.getLoc(), Sound.ITEM_AXE_WAX_OFF, 0.7f, 2f);
break;
case WRONG_TYPE:
p.sendMessage("§cThis item type is not needed for the project");
p.playSound(p.getLocation(), Sound.ENTITY_VILLAGER_NO, 1f, 1f);
break;
case NO_PROJECT:
p.sendMessage("§cThis bench has no project. Right-click the table to choose one.");
break;
default:
break;
}
existing.addMaterial(material, hand);
consumeOne(p);
markDirty();
IntCounter bucket = existing.getTypes().get(material.getType());
String progress = bucket.getCurrent() + "/" + bucket.getNeeded();
p.sendTitle("§aAdded " + material.getName(), MaterialTypeLoader.display(material.getType()) + " §e" + progress, 5, 20, 5);
playWorkFx(existing.getLoc(), Material.OAK_LOG);
p.getWorld().playSound(existing.getLoc(), Sound.ITEM_AXE_WAX_OFF, 0.7f, 2f);

}

// Keep the existing legacy text representation, formatting, and exact-string comparisons.
Expand Down Expand Up @@ -350,27 +339,21 @@ private void handleLeftClick(PlayerInteractEvent e) {
e.setCancelled(true);
markCooldown(p);
StationFeedback feedback = station.hit(hit);
switch (feedback) {
case SUCCESS:
if (feedback == StationFeedback.SUCCESS) {
markDirty();
IntCounter typeCounter = station.getHitTypes().get(hit.getType());
HitType type = hit.getType();
String typeName = type == null ? "Hits" : type.getName();
String progress = typeCounter == null ? "" : typeCounter.getCurrent() + "/" + typeCounter.getNeeded();
String typeName = type.getName();
String progress = typeCounter.getCurrent() + "/" + typeCounter.getNeeded();
p.sendTitle("§a+1 " + hit.getName(), typeName + " §e" + progress, 5, 20, 5);
playWorkFx(station.getLoc(), Material.OAK_PLANKS);
p.getWorld().playSound(station.getLoc(), Sound.BLOCK_ANVIL_USE, 0.4f, 1f);
break;
case LACKING_ITEMS:
} else if (feedback == StationFeedback.LACKING_ITEMS) {
p.sendMessage("§cYou have to add all the items before working");
p.playSound(p.getLocation(), Sound.ENTITY_VILLAGER_NO, 1f, 1f);
break;
case WRONG_TYPE:
} else {
p.sendMessage("§cThis item cannot be used for woodworking hits");
p.playSound(p.getLocation(), Sound.ENTITY_VILLAGER_NO, 1f, 1f);
break;
default:
break;
}
}

Expand All @@ -393,7 +376,7 @@ private void completeCraft(Player p, WoodStation station) {
WoodProject project = station.getProject();
String path = project.getItem();
ItemStack output = TLibs.getItemAPI().getCreator().getItemFromPath(path);
if (output == null || (path != null && path.toLowerCase().startsWith("ia.") && output.getType() == Material.DIRT)) {
if (output == null || output.getType().isAir() || (path != null && path.toLowerCase().startsWith("ia.") && output.getType() == Material.DIRT)) {
Log.warn("Could not build output for project " + project.getId() + " (" + path + "). Station left intact.");
p.sendMessage("§cCould not create that item. Contact an administrator.");
p.playSound(p.getLocation(), Sound.ENTITY_VILLAGER_NO, 1f, 1f);
Expand Down Expand Up @@ -439,7 +422,6 @@ private boolean isStationFurniture(FurnitureBreakEvent e) {

/** Resolves the MMOItems TYPE.ID of a held tool to its hit, or null when it is not a hit tool. */
private CraftingHit matchHit(ItemStack item) {
if (item == null || item.getType().isAir()) return null;
NBTItem nbt = NBTItem.get(item);
if (!nbt.hasType()) return null;
return HitLoader.getByTool(nbt.getType() + "." + nbt.getString("MMOITEMS_ITEM_ID"));
Expand All @@ -463,7 +445,6 @@ private WoodMaterial matchMaterial(ItemStack item) {

private void consumeOne(Player p) {
ItemStack hand = p.getInventory().getItemInMainHand();
if (hand == null || hand.getType().isAir()) return;
hand.setAmount(hand.getAmount() - 1);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ public boolean checkExactHits() {
if (project == null) return false;
for (CraftingHit required : project.getHits().keySet()) {
IntCounter c = hits.get(required);
if (c == null || !c.isEqual()) return false;
if (!c.isEqual()) return false;
}
for (Map.Entry<CraftingHit, IntCounter> e : hits.entrySet()) {
if (!e.getValue().isEqual()) return false;
Expand Down Expand Up @@ -245,7 +245,7 @@ public double calculatePercentage() {
double d = hits.get(h).getPercentage();
if (d >= 200.0) continue;
if (d <= 100.0) amount = amount + d;
if (d > 100.0 && d <= 200.0) amount = amount + (200.0 - d);
if (d > 100.0) amount = amount + (200.0 - d);
}
if (counter == 0) return 100.0;
return Math.round(amount / counter);
Expand Down
Loading