diff --git a/src/main/java/net/tfminecraft/activitytf/config/ActivityConfiguration.java b/src/main/java/net/tfminecraft/activitytf/config/ActivityConfiguration.java index d968be0..42cab5d 100644 --- a/src/main/java/net/tfminecraft/activitytf/config/ActivityConfiguration.java +++ b/src/main/java/net/tfminecraft/activitytf/config/ActivityConfiguration.java @@ -57,6 +57,7 @@ public class ActivityConfiguration { private volatile Map professionActivities = Map.of(); private volatile Map stationActivities = Map.of(); + private volatile Map stationActionAmounts = Map.of(); private volatile DayOfWeek resetDay; private volatile int resetHour; @@ -257,6 +258,7 @@ private void loadActivities(ConfigurationSection section) { craftPaths = List.of(); professionActivities = Map.of(); stationActivities = Map.of(); + stationActionAmounts = Map.of(); guaranteedActivities = List.of(); return; } @@ -266,6 +268,7 @@ private void loadActivities(ConfigurationSection section) { List> paths = new ArrayList<>(); Map professions = new LinkedHashMap<>(); Map stations = new LinkedHashMap<>(); + Map stationAmounts = new LinkedHashMap<>(); List guaranteed = new ArrayList<>(); for (String id : section.getKeys(false)) { ConfigurationSection entry = section.getConfigurationSection(id); @@ -318,7 +321,7 @@ private void loadActivities(ConfigurationSection section) { } } - loadStation(stations, entry, id); + loadStation(stations, stationAmounts, entry, id); if (flag(entry, id, "daily-guaranteed", false)) { guaranteed.add(id); @@ -337,10 +340,12 @@ private void loadActivities(ConfigurationSection section) { craftPaths = List.copyOf(paths); professionActivities = professions; stationActivities = stations; + stationActionAmounts = stationAmounts; guaranteedActivities = List.copyOf(guaranteed); } - private void loadStation(Map stations, ConfigurationSection entry, String id) { + private void loadStation(Map stations, Map amounts, + ConfigurationSection entry, String id) { String station = entry.getString("station"); if (station == null) { return; @@ -376,10 +381,35 @@ private void loadStation(Map stations, ConfigurationSection entr } String previous = stations.put(stationKey, id); + amounts.remove(stationKey); if (previous != null) { plugin.getLogger().warning("Activities '" + previous + "' and '" + id + "' both track station '" + stationKey + "' - only '" + id + "' will be fed."); } + ConfigurationSection alternatives = entry.getConfigurationSection("station-actions"); + if (alternatives == null) { + return; + } + for (String recipe : alternatives.getKeys(false)) { + String key = stationKey(recipe); + Object raw = alternatives.get(recipe); + double value = raw instanceof Number number ? number.doubleValue() : Double.NaN; + if (key.isEmpty() || key.startsWith("/") || key.endsWith("/") || !key.contains("/") + || key.equals(stationKey) || !Double.isFinite(value) || value < 1 + || value > Integer.MAX_VALUE || Math.rint(value) != value) { + plugin.getLogger().warning("Activity '" + id + "' has invalid station-actions entry '" + + Utils.safeForLog(recipe) + "' - expected a different /" + + " with a positive whole-number amount within the int range."); + continue; + } + int amount = (int) value; + String displaced = stations.put(key, id); + if (displaced != null) { + plugin.getLogger().warning("Activities '" + displaced + "' and '" + id + + "' both track station '" + key + "' - only '" + id + "' will be fed."); + } + amounts.put(key, amount); + } } private static String stationKey(String station) { @@ -1094,17 +1124,25 @@ public String craftActivity(ItemStack crafted) { } public Optional stationActivity(String stationId, String recipeId) { + return stationAction(stationId, recipeId).map(StationActionCredit::activityId); + } + + public record StationActionCredit(String activityId, int amount) {} + + public Optional stationAction(String stationId, String recipeId) { if (stationId == null || stationId.isBlank()) { return Optional.empty(); } String station = normalizeStationPart(stationId); if (recipeId != null && !recipeId.isBlank()) { - String specific = stationActivities.get(station + "/" + normalizeStationPart(recipeId)); + String key = station + "/" + normalizeStationPart(recipeId); + String specific = stationActivities.get(key); if (specific != null) { - return Optional.of(specific); + return Optional.of(new StationActionCredit(specific, stationActionAmounts.getOrDefault(key, 1))); } } - return Optional.ofNullable(stationActivities.get(station)); + return Optional.ofNullable(stationActivities.get(station)) + .map(id -> new StationActionCredit(id, 1)); } public String professionActivity(String professionId) { diff --git a/src/main/java/net/tfminecraft/activitytf/listeners/MmoItemsStationListener.java b/src/main/java/net/tfminecraft/activitytf/listeners/MmoItemsStationListener.java index 23a6778..56933cb 100644 --- a/src/main/java/net/tfminecraft/activitytf/listeners/MmoItemsStationListener.java +++ b/src/main/java/net/tfminecraft/activitytf/listeners/MmoItemsStationListener.java @@ -36,7 +36,7 @@ public void onUseCraftingStation(PlayerUseCraftingStationEvent event) { return; } - manager.getConfiguration().stationActivity(station.getId(), recipe.getId()) - .ifPresent(activityId -> manager.recordAction(player.getUniqueId(), activityId, 1)); + manager.getConfiguration().stationAction(station.getId(), recipe.getId()) + .ifPresent(credit -> manager.recordAction(player.getUniqueId(), credit.activityId(), credit.amount())); } } diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 69976db..7a6ecf3 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -87,9 +87,12 @@ reset: # is counted when the craft actually completes - instantly, or when a queued # craft is claimed - so queueing and cancelling count nothing. A recipe with # 'output-item: false' still counts, and one craft counts as one action -# however big the output stack is. An +# regardless of output stack size unless station-actions sets another amount. An # activity with both 'station' and 'craft'/'profession' is a config error: # the extra 'station' is reported at load and ignored. +# station-actions: optional recipe aliases for a station activity. Each +# / key gives the number of actions credited by one craft. +# The primary station recipe still gives one action. Use this for bulk recipes. # click-commands: optional. Console commands run when the player clicks this # activity's task in /activity AFTER it has already been revealed - the # first click on a hidden task only reveals it, every later click runs @@ -280,6 +283,8 @@ activities: points: 1 daily-cap: 1 station: ingot-station/coal + station-actions: + ingot-station/coal-64: 16 # Tool station tool_iron_pickaxe: display: "#e6ca40&lForge Iron Pickaxes" diff --git a/src/test/java/net/tfminecraft/activitytf/config/ActivityConfigurationStationTest.java b/src/test/java/net/tfminecraft/activitytf/config/ActivityConfigurationStationTest.java index 1921253..ce3b3ab 100644 --- a/src/test/java/net/tfminecraft/activitytf/config/ActivityConfigurationStationTest.java +++ b/src/test/java/net/tfminecraft/activitytf/config/ActivityConfigurationStationTest.java @@ -76,6 +76,35 @@ void specificRecipeOnlyMatchesThatRecipe() { assertEquals(Optional.empty(), config.stationActivity("forge", "coal")); } + @Test + void bulkCoalRecipeCreditsSixteenActionsWhileSmallRecipeCreditsOne() { + ActivityConfiguration config = configFor( + " coal:\n points: 1\n every: 16\n station: ingot-station/coal\n" + + " station-actions:\n ingot-station/coal-64: 16\n"); + + assertEquals(Optional.of(new ActivityConfiguration.StationActionCredit("coal", 1)), + config.stationAction("ingot-station", "coal")); + assertEquals(Optional.of(new ActivityConfiguration.StationActionCredit("coal", 16)), + config.stationAction("ingot-station", "coal-64")); + assertEquals(Optional.empty(), config.stationAction("ingot-station", "coke")); + } + + @Test + void invalidBulkAmountsDoNotCreditActions() { + ActivityConfiguration config = configFor( + " coal:\n points: 1\n station: ingot-station/coal\n station-actions:\n" + + " ingot-station/fractional: 16.9\n" + + " ingot-station/zero: 0\n" + + " ingot-station/overflow: 2147483648\n" + + " ingot-station/text: sixteen\n"); + + assertEquals(Optional.of(new ActivityConfiguration.StationActionCredit("coal", 1)), + config.stationAction("ingot-station", "coal")); + for (String recipe : java.util.List.of("fractional", "zero", "overflow", "text")) { + assertEquals(Optional.empty(), config.stationAction("ingot-station", recipe)); + } + } + @Test void specificRecipeBeatsWholeStation() { ActivityConfiguration config = configFor( diff --git a/src/test/java/net/tfminecraft/activitytf/listeners/MmoItemsStationListenerTest.java b/src/test/java/net/tfminecraft/activitytf/listeners/MmoItemsStationListenerTest.java index 707068d..a433449 100644 --- a/src/test/java/net/tfminecraft/activitytf/listeners/MmoItemsStationListenerTest.java +++ b/src/test/java/net/tfminecraft/activitytf/listeners/MmoItemsStationListenerTest.java @@ -252,4 +252,31 @@ void collectingCommandRewardFishingRodUpdatesTheRevealedTask() { org.junit.jupiter.api.Assertions.assertEquals(1, manager.tasks(uuid).points()); } + @Test + void bulkCoalCraftCompletesSixteenActionQuestInOneCraft() { + var def = new ActivityDef("ingot_coal", "Make Coal", Material.COAL, null, 16, 1, 1); + var manager = net.tfminecraft.activitytf.managers.TestManagers.manager(def); + net.tfminecraft.activitytf.managers.TestManagers.bukkit(); + net.tfminecraft.activitytf.managers.TestManagers.storeLoaded(manager); + net.tfminecraft.activitytf.managers.TestManagers.guarantee(manager, def.id()); + setField(manager.getConfiguration(), ActivityConfiguration.class, "stationActivities", + Map.of("ingot-station/coal", "ingot_coal", "ingot-station/coal-64", "ingot_coal")); + setField(manager.getConfiguration(), ActivityConfiguration.class, "stationActionAmounts", + Map.of("ingot-station/coal-64", 16)); + UUID uuid = UUID.randomUUID(); + UUID smallUuid = UUID.randomUUID(); + manager.reveal(uuid, 0); + manager.reveal(smallUuid, 0); + var listener = new MmoItemsStationListener(manager); + var player = stubPlayer(uuid); + + listener.onUseCraftingStation(event(stubPlayer(smallUuid), station("ingot-station"), recipe("coal"), null)); + assertEquals(1, manager.tasks(smallUuid).count(def.id())); + assertEquals(0, manager.tasks(smallUuid).points()); + + listener.onUseCraftingStation(event(player, station("ingot-station"), recipe("coal-64"), null)); + assertEquals(16, manager.tasks(uuid).count(def.id())); + assertEquals(1, manager.tasks(uuid).points()); + } + }