From 7f5830d40948b258f68ceabe4d5f4b2ddee97f77 Mon Sep 17 00:00:00 2001 From: fretuks Date: Mon, 25 May 2026 12:43:13 +0200 Subject: [PATCH] extend ability system to support dynamic slot counts, add skill tree validation, new prerequisites API, and extended mod commands --- .../skillengine/api/SkillEngineAPI.java | 8 +++++--- .../capability/PlayerSkillData.java | 13 ++++++------ ...ClientboundSyncSkillDefinitionsPacket.java | 20 +++++++++++++++---- .../network/ClientboundSyncSkillsPacket.java | 19 ++++++++++++++---- .../ServerboundActivateAbilityPacket.java | 5 +++-- .../network/ServerboundUnlockNodePacket.java | 5 +++-- .../skillengine/skilltree/SkillLogic.java | 2 +- .../skilltree/SkillTreeValidator.java | 6 ++++++ 8 files changed, 56 insertions(+), 22 deletions(-) diff --git a/src/main/java/net/fretux/skillengine/api/SkillEngineAPI.java b/src/main/java/net/fretux/skillengine/api/SkillEngineAPI.java index 126ff9b..a0a2778 100644 --- a/src/main/java/net/fretux/skillengine/api/SkillEngineAPI.java +++ b/src/main/java/net/fretux/skillengine/api/SkillEngineAPI.java @@ -115,10 +115,11 @@ public final class SkillEngineAPI { if (unlockPlan == null) return false; for (SkillNode plannedNode : unlockPlan) { data.unlockNode(plannedNode); - if (plannedNode.getCost() > 0) { + int cost = Math.max(0, plannedNode.getCost()); + if (cost > 0) { MinecraftForge.EVENT_BUS.post(new SkillPointsChangedEvent( player, - plannedNode.getCost(), + cost, SkillPointsChangedEvent.Reason.SPENT, plannedNode.getId() )); @@ -131,9 +132,10 @@ public final class SkillEngineAPI { PlayerSkillData data = getPlayerData(player); SkillNode node = SkillNodeRegistry.get(id); if (data == null || node == null || !data.refundNode(id)) return false; + int refunded = Math.max(0, node.getCost()); MinecraftForge.EVENT_BUS.post(new SkillPointsChangedEvent( player, - node.getCost(), + refunded, SkillPointsChangedEvent.Reason.REFUNDED, id )); diff --git a/src/main/java/net/fretux/skillengine/capability/PlayerSkillData.java b/src/main/java/net/fretux/skillengine/capability/PlayerSkillData.java index 00ea730..68083e1 100644 --- a/src/main/java/net/fretux/skillengine/capability/PlayerSkillData.java +++ b/src/main/java/net/fretux/skillengine/capability/PlayerSkillData.java @@ -55,7 +55,7 @@ public class PlayerSkillData { if (node == null) return; if (unlockedNodes.add(node.getId())) { activeTags.addAll(node.getTags()); - skillPoints -= node.getCost(); + skillPoints -= Math.max(0, node.getCost()); } } @@ -126,7 +126,7 @@ public class PlayerSkillData { ensureSlotCount(); int idx = slot - 1; if (idx < 0 || idx >= abilityCooldowns.length) return; - abilityCooldowns[idx] = ticks; + abilityCooldowns[idx] = Math.max(0, ticks); } public int getCooldown(int slot) { @@ -226,7 +226,7 @@ public class PlayerSkillData { int total = 0; for (ResourceLocation id : unlockedNodes) { SkillNode node = SkillNodeRegistry.get(id); - if (node != null) total += node.getCost(); + if (node != null) total += Math.max(0, node.getCost()); } return total; } @@ -247,7 +247,7 @@ public class PlayerSkillData { return false; } } - skillPoints += node.getCost(); + skillPoints += Math.max(0, node.getCost()); rebuildActiveTags(); return true; } @@ -259,8 +259,9 @@ public class PlayerSkillData { for (ResourceLocation id : toRefund) { SkillNode node = SkillNodeRegistry.get(id); if (node != null && unlockedNodes.remove(id)) { - skillPoints += node.getCost(); - refunded += node.getCost(); + int cost = Math.max(0, node.getCost()); + skillPoints += cost; + refunded += cost; } } rebuildActiveTags(); diff --git a/src/main/java/net/fretux/skillengine/network/ClientboundSyncSkillDefinitionsPacket.java b/src/main/java/net/fretux/skillengine/network/ClientboundSyncSkillDefinitionsPacket.java index 6488241..7626914 100644 --- a/src/main/java/net/fretux/skillengine/network/ClientboundSyncSkillDefinitionsPacket.java +++ b/src/main/java/net/fretux/skillengine/network/ClientboundSyncSkillDefinitionsPacket.java @@ -20,6 +20,10 @@ import java.util.function.Supplier; public class ClientboundSyncSkillDefinitionsPacket { + private static final int MAX_SYNCED_NODES = 4096; + private static final int MAX_LIST_ENTRIES = 512; + private static final int MAX_PREREQUISITES = 256; + private final List skillNodes; private final List abilityNodes; @@ -42,13 +46,13 @@ public class ClientboundSyncSkillDefinitionsPacket { } public static ClientboundSyncSkillDefinitionsPacket decode(FriendlyByteBuf buf) { - int skillNodeCount = buf.readInt(); + int skillNodeCount = readBoundedCount(buf, MAX_SYNCED_NODES, "skill node definitions"); List skillNodes = new ArrayList<>(skillNodeCount); for (int i = 0; i < skillNodeCount; i++) { skillNodes.add(readSkillNode(buf)); } - int abilityNodeCount = buf.readInt(); + int abilityNodeCount = readBoundedCount(buf, MAX_SYNCED_NODES, "ability node definitions"); List abilityNodes = new ArrayList<>(abilityNodeCount); for (int i = 0; i < abilityNodeCount; i++) { abilityNodes.add(readAbilityNode(buf)); @@ -136,7 +140,7 @@ public class ClientboundSyncSkillDefinitionsPacket { } private static List readResourceLocationList(FriendlyByteBuf buf) { - int size = buf.readInt(); + int size = readBoundedCount(buf, MAX_LIST_ENTRIES, "resource location list"); List ids = new ArrayList<>(size); for (int i = 0; i < size; i++) { ids.add(buf.readResourceLocation()); @@ -153,11 +157,19 @@ public class ClientboundSyncSkillDefinitionsPacket { } private static Map readPrerequisites(FriendlyByteBuf buf) { - int size = buf.readInt(); + int size = readBoundedCount(buf, MAX_PREREQUISITES, "prerequisites"); Map prerequisites = new HashMap<>(size); for (int i = 0; i < size; i++) { prerequisites.put(buf.readUtf(), buf.readInt()); } return prerequisites; } + + private static int readBoundedCount(FriendlyByteBuf buf, int max, String fieldName) { + int count = buf.readInt(); + if (count < 0 || count > max) { + throw new IllegalArgumentException("Invalid " + fieldName + " count: " + count); + } + return count; + } } diff --git a/src/main/java/net/fretux/skillengine/network/ClientboundSyncSkillsPacket.java b/src/main/java/net/fretux/skillengine/network/ClientboundSyncSkillsPacket.java index c6719a0..7ce7ec8 100644 --- a/src/main/java/net/fretux/skillengine/network/ClientboundSyncSkillsPacket.java +++ b/src/main/java/net/fretux/skillengine/network/ClientboundSyncSkillsPacket.java @@ -12,6 +12,9 @@ import java.util.function.Supplier; public class ClientboundSyncSkillsPacket { + private static final int MAX_SYNCED_NODES = 4096; + private static final int MAX_ABILITY_SLOTS = 64; + private final Set unlocked; private final Set unlockedAbilities; private final int skillPoints; @@ -46,15 +49,15 @@ public class ClientboundSyncSkillsPacket { } public static ClientboundSyncSkillsPacket decode(FriendlyByteBuf buf) { - int size = buf.readInt(); + int size = readBoundedCount(buf, MAX_SYNCED_NODES, "unlocked skill nodes"); Set unlocked = new HashSet<>(); for (int i = 0; i < size; i++) unlocked.add(buf.readResourceLocation()); - int abilitySize = buf.readInt(); + int abilitySize = readBoundedCount(buf, MAX_SYNCED_NODES, "unlocked abilities"); Set unlockedAbilities = new HashSet<>(); for (int i = 0; i < abilitySize; i++) unlockedAbilities.add(buf.readResourceLocation()); int points = buf.readInt(); - int slotsCount = buf.readInt(); - ResourceLocation[] slots = new ResourceLocation[Math.max(slotsCount, 0)]; + int slotsCount = readBoundedCount(buf, MAX_ABILITY_SLOTS, "ability slots"); + ResourceLocation[] slots = new ResourceLocation[slotsCount]; for (int i = 0; i < slots.length; i++) { boolean present = buf.readBoolean(); slots[i] = present ? buf.readResourceLocation() : null; @@ -76,4 +79,12 @@ public class ClientboundSyncSkillsPacket { }); ctx.get().setPacketHandled(true); } + + private static int readBoundedCount(FriendlyByteBuf buf, int max, String fieldName) { + int count = buf.readInt(); + if (count < 0 || count > max) { + throw new IllegalArgumentException("Invalid " + fieldName + " count: " + count); + } + return count; + } } diff --git a/src/main/java/net/fretux/skillengine/network/ServerboundActivateAbilityPacket.java b/src/main/java/net/fretux/skillengine/network/ServerboundActivateAbilityPacket.java index 975a5d7..70eaf8c 100644 --- a/src/main/java/net/fretux/skillengine/network/ServerboundActivateAbilityPacket.java +++ b/src/main/java/net/fretux/skillengine/network/ServerboundActivateAbilityPacket.java @@ -46,10 +46,11 @@ public class ServerboundActivateAbilityPacket { int cd = data.getCooldown(msg.slot); if (cd > 0) return; ability.execute(player); - data.setCooldown(msg.slot, ability.getCooldown()); + int cooldown = Math.max(0, ability.getCooldown()); + data.setCooldown(msg.slot, cooldown); PacketHandler.CHANNEL.send( PacketDistributor.PLAYER.with(() -> player), - new ClientboundSyncCooldownPacket(msg.slot, ability.getCooldown()) + new ClientboundSyncCooldownPacket(msg.slot, cooldown) ); }); }); diff --git a/src/main/java/net/fretux/skillengine/network/ServerboundUnlockNodePacket.java b/src/main/java/net/fretux/skillengine/network/ServerboundUnlockNodePacket.java index 352162c..9577ecd 100644 --- a/src/main/java/net/fretux/skillengine/network/ServerboundUnlockNodePacket.java +++ b/src/main/java/net/fretux/skillengine/network/ServerboundUnlockNodePacket.java @@ -42,10 +42,11 @@ public class ServerboundUnlockNodePacket { if (unlockPlan != null) { for (SkillNode plannedNode : unlockPlan) { data.unlockNode(plannedNode); - if (plannedNode.getCost() > 0) { + int cost = Math.max(0, plannedNode.getCost()); + if (cost > 0) { MinecraftForge.EVENT_BUS.post(new SkillPointsChangedEvent( player, - plannedNode.getCost(), + cost, SkillPointsChangedEvent.Reason.SPENT, plannedNode.getId() )); diff --git a/src/main/java/net/fretux/skillengine/skilltree/SkillLogic.java b/src/main/java/net/fretux/skillengine/skilltree/SkillLogic.java index d7692d8..8bc13b2 100644 --- a/src/main/java/net/fretux/skillengine/skilltree/SkillLogic.java +++ b/src/main/java/net/fretux/skillengine/skilltree/SkillLogic.java @@ -33,7 +33,7 @@ public class SkillLogic { int totalCost = 0; for (SkillNode plannedNode : plan) { - totalCost += plannedNode.getCost(); + totalCost += Math.max(0, plannedNode.getCost()); } if (data.getSkillPoints() < totalCost) { return Optional.empty(); diff --git a/src/main/java/net/fretux/skillengine/skilltree/SkillTreeValidator.java b/src/main/java/net/fretux/skillengine/skilltree/SkillTreeValidator.java index 6a8984a..927b75e 100644 --- a/src/main/java/net/fretux/skillengine/skilltree/SkillTreeValidator.java +++ b/src/main/java/net/fretux/skillengine/skilltree/SkillTreeValidator.java @@ -33,6 +33,9 @@ public final class SkillTreeValidator { if (!isPngTexture(node.getIcons())) { issues.add(node.getId() + " has invalid icon path " + node.getIcons() + " (expected a .png texture)"); } + if (node.getCost() < 0) { + issues.add(node.getId() + " has negative cost " + node.getCost()); + } for (ResourceLocation parent : node.getLinks()) { if (SkillNodeRegistry.get(parent) == null) { issues.add(node.getId() + " links to missing skill node " + parent); @@ -83,6 +86,9 @@ public final class SkillTreeValidator { if (!isPngTexture(ability.getIcon())) { issues.add(ability.getId() + " has invalid icon path " + ability.getIcon() + " (expected a .png texture)"); } + if (ability.getCooldown() < 0) { + issues.add(ability.getId() + " has negative cooldown " + ability.getCooldown()); + } for (ResourceLocation parent : ability.getLinks()) { if (SkillNodeRegistry.get(parent) == null && AbilityNodeRegistry.get(parent) == null) { issues.add(ability.getId() + " links to missing node or ability " + parent);