extend ability system to support dynamic slot counts, add skill tree validation, new prerequisites API, and extended mod commands

This commit is contained in:
2026-05-25 12:43:13 +02:00
parent 77fbada438
commit 7f5830d409
8 changed files with 56 additions and 22 deletions
@@ -115,10 +115,11 @@ public final class SkillEngineAPI {
if (unlockPlan == null) return false; if (unlockPlan == null) return false;
for (SkillNode plannedNode : unlockPlan) { for (SkillNode plannedNode : unlockPlan) {
data.unlockNode(plannedNode); data.unlockNode(plannedNode);
if (plannedNode.getCost() > 0) { int cost = Math.max(0, plannedNode.getCost());
if (cost > 0) {
MinecraftForge.EVENT_BUS.post(new SkillPointsChangedEvent( MinecraftForge.EVENT_BUS.post(new SkillPointsChangedEvent(
player, player,
plannedNode.getCost(), cost,
SkillPointsChangedEvent.Reason.SPENT, SkillPointsChangedEvent.Reason.SPENT,
plannedNode.getId() plannedNode.getId()
)); ));
@@ -131,9 +132,10 @@ public final class SkillEngineAPI {
PlayerSkillData data = getPlayerData(player); PlayerSkillData data = getPlayerData(player);
SkillNode node = SkillNodeRegistry.get(id); SkillNode node = SkillNodeRegistry.get(id);
if (data == null || node == null || !data.refundNode(id)) return false; if (data == null || node == null || !data.refundNode(id)) return false;
int refunded = Math.max(0, node.getCost());
MinecraftForge.EVENT_BUS.post(new SkillPointsChangedEvent( MinecraftForge.EVENT_BUS.post(new SkillPointsChangedEvent(
player, player,
node.getCost(), refunded,
SkillPointsChangedEvent.Reason.REFUNDED, SkillPointsChangedEvent.Reason.REFUNDED,
id id
)); ));
@@ -55,7 +55,7 @@ public class PlayerSkillData {
if (node == null) return; if (node == null) return;
if (unlockedNodes.add(node.getId())) { if (unlockedNodes.add(node.getId())) {
activeTags.addAll(node.getTags()); activeTags.addAll(node.getTags());
skillPoints -= node.getCost(); skillPoints -= Math.max(0, node.getCost());
} }
} }
@@ -126,7 +126,7 @@ public class PlayerSkillData {
ensureSlotCount(); ensureSlotCount();
int idx = slot - 1; int idx = slot - 1;
if (idx < 0 || idx >= abilityCooldowns.length) return; if (idx < 0 || idx >= abilityCooldowns.length) return;
abilityCooldowns[idx] = ticks; abilityCooldowns[idx] = Math.max(0, ticks);
} }
public int getCooldown(int slot) { public int getCooldown(int slot) {
@@ -226,7 +226,7 @@ public class PlayerSkillData {
int total = 0; int total = 0;
for (ResourceLocation id : unlockedNodes) { for (ResourceLocation id : unlockedNodes) {
SkillNode node = SkillNodeRegistry.get(id); SkillNode node = SkillNodeRegistry.get(id);
if (node != null) total += node.getCost(); if (node != null) total += Math.max(0, node.getCost());
} }
return total; return total;
} }
@@ -247,7 +247,7 @@ public class PlayerSkillData {
return false; return false;
} }
} }
skillPoints += node.getCost(); skillPoints += Math.max(0, node.getCost());
rebuildActiveTags(); rebuildActiveTags();
return true; return true;
} }
@@ -259,8 +259,9 @@ public class PlayerSkillData {
for (ResourceLocation id : toRefund) { for (ResourceLocation id : toRefund) {
SkillNode node = SkillNodeRegistry.get(id); SkillNode node = SkillNodeRegistry.get(id);
if (node != null && unlockedNodes.remove(id)) { if (node != null && unlockedNodes.remove(id)) {
skillPoints += node.getCost(); int cost = Math.max(0, node.getCost());
refunded += node.getCost(); skillPoints += cost;
refunded += cost;
} }
} }
rebuildActiveTags(); rebuildActiveTags();
@@ -20,6 +20,10 @@ import java.util.function.Supplier;
public class ClientboundSyncSkillDefinitionsPacket { 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<SkillNode> skillNodes; private final List<SkillNode> skillNodes;
private final List<AbilityNode> abilityNodes; private final List<AbilityNode> abilityNodes;
@@ -42,13 +46,13 @@ public class ClientboundSyncSkillDefinitionsPacket {
} }
public static ClientboundSyncSkillDefinitionsPacket decode(FriendlyByteBuf buf) { public static ClientboundSyncSkillDefinitionsPacket decode(FriendlyByteBuf buf) {
int skillNodeCount = buf.readInt(); int skillNodeCount = readBoundedCount(buf, MAX_SYNCED_NODES, "skill node definitions");
List<SkillNode> skillNodes = new ArrayList<>(skillNodeCount); List<SkillNode> skillNodes = new ArrayList<>(skillNodeCount);
for (int i = 0; i < skillNodeCount; i++) { for (int i = 0; i < skillNodeCount; i++) {
skillNodes.add(readSkillNode(buf)); skillNodes.add(readSkillNode(buf));
} }
int abilityNodeCount = buf.readInt(); int abilityNodeCount = readBoundedCount(buf, MAX_SYNCED_NODES, "ability node definitions");
List<AbilityNode> abilityNodes = new ArrayList<>(abilityNodeCount); List<AbilityNode> abilityNodes = new ArrayList<>(abilityNodeCount);
for (int i = 0; i < abilityNodeCount; i++) { for (int i = 0; i < abilityNodeCount; i++) {
abilityNodes.add(readAbilityNode(buf)); abilityNodes.add(readAbilityNode(buf));
@@ -136,7 +140,7 @@ public class ClientboundSyncSkillDefinitionsPacket {
} }
private static List<ResourceLocation> readResourceLocationList(FriendlyByteBuf buf) { private static List<ResourceLocation> readResourceLocationList(FriendlyByteBuf buf) {
int size = buf.readInt(); int size = readBoundedCount(buf, MAX_LIST_ENTRIES, "resource location list");
List<ResourceLocation> ids = new ArrayList<>(size); List<ResourceLocation> ids = new ArrayList<>(size);
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
ids.add(buf.readResourceLocation()); ids.add(buf.readResourceLocation());
@@ -153,11 +157,19 @@ public class ClientboundSyncSkillDefinitionsPacket {
} }
private static Map<String, Integer> readPrerequisites(FriendlyByteBuf buf) { private static Map<String, Integer> readPrerequisites(FriendlyByteBuf buf) {
int size = buf.readInt(); int size = readBoundedCount(buf, MAX_PREREQUISITES, "prerequisites");
Map<String, Integer> prerequisites = new HashMap<>(size); Map<String, Integer> prerequisites = new HashMap<>(size);
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
prerequisites.put(buf.readUtf(), buf.readInt()); prerequisites.put(buf.readUtf(), buf.readInt());
} }
return prerequisites; 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;
}
} }
@@ -12,6 +12,9 @@ import java.util.function.Supplier;
public class ClientboundSyncSkillsPacket { public class ClientboundSyncSkillsPacket {
private static final int MAX_SYNCED_NODES = 4096;
private static final int MAX_ABILITY_SLOTS = 64;
private final Set<ResourceLocation> unlocked; private final Set<ResourceLocation> unlocked;
private final Set<ResourceLocation> unlockedAbilities; private final Set<ResourceLocation> unlockedAbilities;
private final int skillPoints; private final int skillPoints;
@@ -46,15 +49,15 @@ public class ClientboundSyncSkillsPacket {
} }
public static ClientboundSyncSkillsPacket decode(FriendlyByteBuf buf) { public static ClientboundSyncSkillsPacket decode(FriendlyByteBuf buf) {
int size = buf.readInt(); int size = readBoundedCount(buf, MAX_SYNCED_NODES, "unlocked skill nodes");
Set<ResourceLocation> unlocked = new HashSet<>(); Set<ResourceLocation> unlocked = new HashSet<>();
for (int i = 0; i < size; i++) unlocked.add(buf.readResourceLocation()); 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<ResourceLocation> unlockedAbilities = new HashSet<>(); Set<ResourceLocation> unlockedAbilities = new HashSet<>();
for (int i = 0; i < abilitySize; i++) unlockedAbilities.add(buf.readResourceLocation()); for (int i = 0; i < abilitySize; i++) unlockedAbilities.add(buf.readResourceLocation());
int points = buf.readInt(); int points = buf.readInt();
int slotsCount = buf.readInt(); int slotsCount = readBoundedCount(buf, MAX_ABILITY_SLOTS, "ability slots");
ResourceLocation[] slots = new ResourceLocation[Math.max(slotsCount, 0)]; ResourceLocation[] slots = new ResourceLocation[slotsCount];
for (int i = 0; i < slots.length; i++) { for (int i = 0; i < slots.length; i++) {
boolean present = buf.readBoolean(); boolean present = buf.readBoolean();
slots[i] = present ? buf.readResourceLocation() : null; slots[i] = present ? buf.readResourceLocation() : null;
@@ -76,4 +79,12 @@ public class ClientboundSyncSkillsPacket {
}); });
ctx.get().setPacketHandled(true); 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;
}
} }
@@ -46,10 +46,11 @@ public class ServerboundActivateAbilityPacket {
int cd = data.getCooldown(msg.slot); int cd = data.getCooldown(msg.slot);
if (cd > 0) return; if (cd > 0) return;
ability.execute(player); ability.execute(player);
data.setCooldown(msg.slot, ability.getCooldown()); int cooldown = Math.max(0, ability.getCooldown());
data.setCooldown(msg.slot, cooldown);
PacketHandler.CHANNEL.send( PacketHandler.CHANNEL.send(
PacketDistributor.PLAYER.with(() -> player), PacketDistributor.PLAYER.with(() -> player),
new ClientboundSyncCooldownPacket(msg.slot, ability.getCooldown()) new ClientboundSyncCooldownPacket(msg.slot, cooldown)
); );
}); });
}); });
@@ -42,10 +42,11 @@ public class ServerboundUnlockNodePacket {
if (unlockPlan != null) { if (unlockPlan != null) {
for (SkillNode plannedNode : unlockPlan) { for (SkillNode plannedNode : unlockPlan) {
data.unlockNode(plannedNode); data.unlockNode(plannedNode);
if (plannedNode.getCost() > 0) { int cost = Math.max(0, plannedNode.getCost());
if (cost > 0) {
MinecraftForge.EVENT_BUS.post(new SkillPointsChangedEvent( MinecraftForge.EVENT_BUS.post(new SkillPointsChangedEvent(
player, player,
plannedNode.getCost(), cost,
SkillPointsChangedEvent.Reason.SPENT, SkillPointsChangedEvent.Reason.SPENT,
plannedNode.getId() plannedNode.getId()
)); ));
@@ -33,7 +33,7 @@ public class SkillLogic {
int totalCost = 0; int totalCost = 0;
for (SkillNode plannedNode : plan) { for (SkillNode plannedNode : plan) {
totalCost += plannedNode.getCost(); totalCost += Math.max(0, plannedNode.getCost());
} }
if (data.getSkillPoints() < totalCost) { if (data.getSkillPoints() < totalCost) {
return Optional.empty(); return Optional.empty();
@@ -33,6 +33,9 @@ public final class SkillTreeValidator {
if (!isPngTexture(node.getIcons())) { if (!isPngTexture(node.getIcons())) {
issues.add(node.getId() + " has invalid icon path " + node.getIcons() + " (expected a .png texture)"); 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()) { for (ResourceLocation parent : node.getLinks()) {
if (SkillNodeRegistry.get(parent) == null) { if (SkillNodeRegistry.get(parent) == null) {
issues.add(node.getId() + " links to missing skill node " + parent); issues.add(node.getId() + " links to missing skill node " + parent);
@@ -83,6 +86,9 @@ public final class SkillTreeValidator {
if (!isPngTexture(ability.getIcon())) { if (!isPngTexture(ability.getIcon())) {
issues.add(ability.getId() + " has invalid icon path " + ability.getIcon() + " (expected a .png texture)"); 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()) { for (ResourceLocation parent : ability.getLinks()) {
if (SkillNodeRegistry.get(parent) == null && AbilityNodeRegistry.get(parent) == null) { if (SkillNodeRegistry.get(parent) == null && AbilityNodeRegistry.get(parent) == null) {
issues.add(ability.getId() + " links to missing node or ability " + parent); issues.add(ability.getId() + " links to missing node or ability " + parent);