From 8ca37e96ebb9eb471c4dad6586e24f65ed39794e Mon Sep 17 00:00:00 2001 From: Exterminate Date: Mon, 23 Mar 2026 14:50:44 +1100 Subject: [PATCH 1/3] Save item cooldown states --- .../hollowcube/mapmaker/map/MapPlayer.java | 30 +++++++++++++++++-- .../mapmaker/runtime/PlayState.java | 21 +++++++++++-- .../runtime/parkour/ParkourState.java | 4 +++ .../runtime/parkour/TempEffectApplicator.java | 1 + .../action/LegacyActionStateManager.java | 4 +++ 5 files changed, 55 insertions(+), 5 deletions(-) diff --git a/modules/map-core/src/main/java/net/hollowcube/mapmaker/map/MapPlayer.java b/modules/map-core/src/main/java/net/hollowcube/mapmaker/map/MapPlayer.java index b1a11ae34..9fed99167 100644 --- a/modules/map-core/src/main/java/net/hollowcube/mapmaker/map/MapPlayer.java +++ b/modules/map-core/src/main/java/net/hollowcube/mapmaker/map/MapPlayer.java @@ -227,6 +227,7 @@ public void update(long time) { super.update(time); physicsTick(); + cooldownTick(); riptideTick(); fallTick(); weatherTick(); @@ -417,15 +418,38 @@ public boolean tryUseItem(@NotNull ItemStack itemStack) { if (useCooldown == null || useCooldown.cooldownGroup() == null) return true; - int cooldownEnd = cooldownGroups.getInt(useCooldown.cooldownGroup()); - if (cooldownEnd > getAliveTicks()) return false; // Still in cooldown + int cooldown = cooldownGroups.getInt(useCooldown.cooldownGroup()); + if (cooldown > 0) return false; // Still in cooldown int cooldownTicks = (int) (useCooldown.seconds() * 20); - cooldownGroups.put(useCooldown.cooldownGroup(), (int) (getAliveTicks() + cooldownTicks)); + cooldownGroups.put(useCooldown.cooldownGroup(), cooldownTicks); sendPacket(new SetCooldownPacket(useCooldown.cooldownGroup(), cooldownTicks)); return true; } + private void cooldownTick() { + // Tick cooldown + for (Object2IntMap.Entry cooldown : cooldownGroups.object2IntEntrySet()) { + int newCooldown = cooldown.getIntValue() - 1; + if (newCooldown <= 0) { + cooldownGroups.removeInt(cooldown.getKey()); + } else { + cooldown.setValue(newCooldown); + } + } + } + + public void setItemCooldowns(Map cooldowns) { + cooldownGroups.putAll(cooldowns); + for (Map.Entry cooldown : cooldowns.entrySet()) { + sendPacket(new SetCooldownPacket(cooldown.getKey(), cooldown.getValue())); + } + } + + public Map getItemCooldowns() { + return Map.copyOf(cooldownGroups); + } + //endregion //region EXT: Per player visibility diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/PlayState.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/PlayState.java index c78bf9c18..c1a18a620 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/PlayState.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/PlayState.java @@ -1,5 +1,7 @@ package net.hollowcube.mapmaker.runtime; +import it.unimi.dsi.fastutil.objects.Object2IntArrayMap; +import it.unimi.dsi.fastutil.objects.Object2IntMap; import net.hollowcube.common.util.OpUtils; import net.hollowcube.common.util.dfu.ExtraCodecs; import net.hollowcube.mapmaker.map.SaveStateType; @@ -37,11 +39,15 @@ public static Attachment attachment(Key key, Codec codec, Function> GHOST_BLOCKS_CODEC = StructCodec.struct( "ghostBlocks", ExtraCodecs.LONG_STRING.mapValue(ExtraCodecs.BLOCK_STATE_STRING).optional(Map.of()), Function.identity(), it -> it); + private static final StructCodec> COOLDOWN_GROUPS_CODEC = StructCodec.struct( + "cooldownGroups", Codec.STRING.mapValue(Codec.INT).optional(Map.of()), Function.identity(), + it -> it); public static Codec CODEC = net.minestom.server.codec.Codec.Recursive(codec -> StructCodec.struct( "lastState", codec.optional(), PlayState::lastState, "history", net.minestom.server.codec.Codec.STRING.list().optional(List.of()), PlayState::history, "pos", ExtraCodecs.POS.optional(), PlayState::pos, StructCodec.INLINE, GHOST_BLOCKS_CODEC.optional(), PlayState::ghostBlocks, + StructCodec.INLINE, COOLDOWN_GROUPS_CODEC.optional(), PlayState::cooldownGroups, StructCodec.INLINE, new ActionDataCodec(Integer.MAX_VALUE), PlayState::actionData, PlayState::new)); @@ -54,21 +60,24 @@ StructCodec.INLINE, new ActionDataCodec(Integer.MAX_VALUE), PlayState::actionDat private final List history; private @Nullable Pos pos; private Map ghostBlocks; + private Map cooldownGroups; private Map, Object> actionData; public PlayState() { - this(null, null, null, null, null); + this(null, null, null, null, null, null); } public PlayState( @Nullable PlayState lastState, @Nullable List statusEffects, @Nullable Pos pos, @Nullable Map ghostBlocks, + @Nullable Map cooldownGroups, @Nullable Map, Object> actionData ) { this.lastState = lastState; this.history = new ArrayList<>(Objects.requireNonNullElse(statusEffects, List.of())); this.pos = pos; this.ghostBlocks = new HashMap<>(Objects.requireNonNullElse(ghostBlocks, Map.of())); + this.cooldownGroups = new HashMap<>(Objects.requireNonNullElse(cooldownGroups, Map.of())); this.actionData = new HashMap<>(Objects.requireNonNullElse(actionData, Map.of())); } @@ -127,6 +136,14 @@ public void setGhostBlocks(Map blockMap) { this.ghostBlocks = new HashMap<>(blockMap); } + public Map cooldownGroups() { + return cooldownGroups; + } + + public void setCooldownGroups(Map cooldownGroups) { + this.cooldownGroups = new HashMap<>(cooldownGroups); + } + public void setLastState(@Nullable PlayState lastState) { this.lastState = lastState == null ? null : lastState.copy(); } @@ -149,7 +166,7 @@ public PlayState copy() { //noinspection unchecked newActionData.put(entry.getKey(), ((Attachment) entry.getKey()).copyValue(entry.getValue())); } - return new PlayState(lastState, history, pos, new HashMap<>(ghostBlocks), newActionData); + return new PlayState(lastState, history, pos, new HashMap<>(ghostBlocks), new HashMap<>(cooldownGroups), newActionData); } public void setFrom(PlayState other) { diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/ParkourState.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/ParkourState.java index 1ae825e05..6b25e29f2 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/ParkourState.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/ParkourState.java @@ -19,6 +19,7 @@ import net.hollowcube.mapmaker.runtime.parkour.item.*; import net.hollowcube.mapmaker.runtime.parkour.setting.OnlySprintSetting; import net.hollowcube.mapmaker.to_be_refactored.ActionBar; +import net.kyori.adventure.nbt.*; import net.kyori.adventure.text.Component; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; @@ -34,6 +35,7 @@ import org.jetbrains.annotations.Nullable; import java.time.Instant; +import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.concurrent.CompletableFuture; @@ -149,6 +151,8 @@ default void resetPlayer(ParkourMapWorld world, Player player, @Nullable Parkour ((MapPlayer) player).removeOwnedEntities(); ((MapPlayer) player).resetTouchingState(); + ((MapPlayer) player).setItemCooldowns(playState.lastState().cooldownGroups()); + // The following is deinit logic which should not happen when switching from play to play (aka checkpoint reset). if (nextState instanceof AnyPlaying) return; diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/TempEffectApplicator.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/TempEffectApplicator.java index 76007085a..498677ffd 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/TempEffectApplicator.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/TempEffectApplicator.java @@ -112,6 +112,7 @@ public static void applyCheckpoint(ParkourMapWorld world, ActionTriggerData data newHistory, respawnPosition, Map.copyOf(playState.ghostBlocks()), + Map.copyOf(playState.cooldownGroups()), Map.copyOf(playState.actionData()) )); diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/action/LegacyActionStateManager.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/action/LegacyActionStateManager.java index f2a8130e8..3f0e1b7e9 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/action/LegacyActionStateManager.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/action/LegacyActionStateManager.java @@ -1,6 +1,7 @@ package net.hollowcube.mapmaker.runtime.parkour.action; import net.hollowcube.common.util.OpUtils; +import net.hollowcube.mapmaker.map.MapPlayer; import net.hollowcube.mapmaker.map.block.ghost.GhostBlockHolder; import net.hollowcube.mapmaker.map.entity.potion.PotionEffectList; import net.hollowcube.mapmaker.runtime.PlayState; @@ -142,6 +143,9 @@ private static void handleUpdateStateFromPlayer(ParkourMapPlayerStateUpdateEvent var ghostBlocks = GhostBlockHolder.forPlayerOptional(player); playState.setGhostBlocks(ghostBlocks == null ? Map.of() : ghostBlocks.save()); + var cooldowns = ((MapPlayer) player).getItemCooldowns(); + playState.setCooldownGroups(cooldowns); + var items = playState.get(Attachments.HOTBAR_ITEMS, HotbarItems.EMPTY); playState.set(Attachments.HOTBAR_ITEMS, new HotbarItems( OpUtils.map(items.item0(), item -> updateItemStack(player, item, 3)), From 7bed7cd178197c7ce2678a1082b8fbfaaebeb061 Mon Sep 17 00:00:00 2001 From: Exterminate Date: Tue, 24 Mar 2026 14:14:47 +1100 Subject: [PATCH 2/3] Implement requested changes --- .../hollowcube/mapmaker/map/MapPlayer.java | 31 +++++++++++++------ .../mapmaker/runtime/PlayState.java | 2 -- .../runtime/parkour/ParkourState.java | 4 +-- .../action/LegacyActionStateManager.java | 2 +- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/modules/map-core/src/main/java/net/hollowcube/mapmaker/map/MapPlayer.java b/modules/map-core/src/main/java/net/hollowcube/mapmaker/map/MapPlayer.java index 9fed99167..02e603fee 100644 --- a/modules/map-core/src/main/java/net/hollowcube/mapmaker/map/MapPlayer.java +++ b/modules/map-core/src/main/java/net/hollowcube/mapmaker/map/MapPlayer.java @@ -133,7 +133,7 @@ public abstract class MapPlayer extends CommandHandlingPlayer implements MiscFun private final IntList ownedEntities = new IntArrayList(); - private final Object2IntMap cooldownGroups = new Object2IntArrayMap<>(); + private final Object2IntMap cooldowns = new Object2IntArrayMap<>(); private Function visibilityFunc = null; // entity id -> visibility ordinal @@ -418,36 +418,47 @@ public boolean tryUseItem(@NotNull ItemStack itemStack) { if (useCooldown == null || useCooldown.cooldownGroup() == null) return true; - int cooldown = cooldownGroups.getInt(useCooldown.cooldownGroup()); + int cooldown = cooldowns.getInt(useCooldown.cooldownGroup()); if (cooldown > 0) return false; // Still in cooldown int cooldownTicks = (int) (useCooldown.seconds() * 20); - cooldownGroups.put(useCooldown.cooldownGroup(), cooldownTicks); + cooldowns.put(useCooldown.cooldownGroup(), cooldownTicks); sendPacket(new SetCooldownPacket(useCooldown.cooldownGroup(), cooldownTicks)); return true; } private void cooldownTick() { // Tick cooldown - for (Object2IntMap.Entry cooldown : cooldownGroups.object2IntEntrySet()) { + var iterator = cooldowns.object2IntEntrySet().iterator(); + while (iterator.hasNext()) { + Object2IntMap.Entry cooldown = iterator.next(); int newCooldown = cooldown.getIntValue() - 1; if (newCooldown <= 0) { - cooldownGroups.removeInt(cooldown.getKey()); + iterator.remove(); + sendPacket(new SetCooldownPacket(cooldown.getKey(), 0)); // Make sure the client is synced } else { cooldown.setValue(newCooldown); } } } - public void setItemCooldowns(Map cooldowns) { - cooldownGroups.putAll(cooldowns); - for (Map.Entry cooldown : cooldowns.entrySet()) { + public void setCooldowns(Map newCooldowns) { + + for (Object2IntMap.Entry cooldown : cooldowns.object2IntEntrySet()) { + if (!newCooldowns.containsKey(cooldown.getKey())) + sendPacket(new SetCooldownPacket(cooldown.getKey(), 0)); // Reset the cooldown for the client + } + + for (Map.Entry cooldown : newCooldowns.entrySet()) { sendPacket(new SetCooldownPacket(cooldown.getKey(), cooldown.getValue())); } + + cooldowns.clear(); + cooldowns.putAll(newCooldowns); } - public Map getItemCooldowns() { - return Map.copyOf(cooldownGroups); + public Map getCooldowns() { + return Map.copyOf(cooldowns); } //endregion diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/PlayState.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/PlayState.java index c1a18a620..cc8e933e6 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/PlayState.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/PlayState.java @@ -1,7 +1,5 @@ package net.hollowcube.mapmaker.runtime; -import it.unimi.dsi.fastutil.objects.Object2IntArrayMap; -import it.unimi.dsi.fastutil.objects.Object2IntMap; import net.hollowcube.common.util.OpUtils; import net.hollowcube.common.util.dfu.ExtraCodecs; import net.hollowcube.mapmaker.map.SaveStateType; diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/ParkourState.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/ParkourState.java index 6b25e29f2..73f7df7ee 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/ParkourState.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/ParkourState.java @@ -19,7 +19,6 @@ import net.hollowcube.mapmaker.runtime.parkour.item.*; import net.hollowcube.mapmaker.runtime.parkour.setting.OnlySprintSetting; import net.hollowcube.mapmaker.to_be_refactored.ActionBar; -import net.kyori.adventure.nbt.*; import net.kyori.adventure.text.Component; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; @@ -35,7 +34,6 @@ import org.jetbrains.annotations.Nullable; import java.time.Instant; -import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.concurrent.CompletableFuture; @@ -151,7 +149,7 @@ default void resetPlayer(ParkourMapWorld world, Player player, @Nullable Parkour ((MapPlayer) player).removeOwnedEntities(); ((MapPlayer) player).resetTouchingState(); - ((MapPlayer) player).setItemCooldowns(playState.lastState().cooldownGroups()); + ((MapPlayer) player).setCooldowns(playState.lastState().cooldownGroups()); // The following is deinit logic which should not happen when switching from play to play (aka checkpoint reset). if (nextState instanceof AnyPlaying) return; diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/action/LegacyActionStateManager.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/action/LegacyActionStateManager.java index 3f0e1b7e9..e2bbcc2ba 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/action/LegacyActionStateManager.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/action/LegacyActionStateManager.java @@ -143,7 +143,7 @@ private static void handleUpdateStateFromPlayer(ParkourMapPlayerStateUpdateEvent var ghostBlocks = GhostBlockHolder.forPlayerOptional(player); playState.setGhostBlocks(ghostBlocks == null ? Map.of() : ghostBlocks.save()); - var cooldowns = ((MapPlayer) player).getItemCooldowns(); + var cooldowns = ((MapPlayer) player).getCooldowns(); playState.setCooldownGroups(cooldowns); var items = playState.get(Attachments.HOTBAR_ITEMS, HotbarItems.EMPTY); From f9889abb901dd3d71bd7945486dbf2182941f23b Mon Sep 17 00:00:00 2001 From: Exterminate Date: Wed, 25 Mar 2026 13:41:47 +1100 Subject: [PATCH 3/3] Fix NPE --- .../net/hollowcube/mapmaker/runtime/parkour/ParkourState.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/ParkourState.java b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/ParkourState.java index 73f7df7ee..7411f5223 100644 --- a/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/ParkourState.java +++ b/modules/map-runtime/src/main/java/net/hollowcube/mapmaker/runtime/parkour/ParkourState.java @@ -149,7 +149,9 @@ default void resetPlayer(ParkourMapWorld world, Player player, @Nullable Parkour ((MapPlayer) player).removeOwnedEntities(); ((MapPlayer) player).resetTouchingState(); - ((MapPlayer) player).setCooldowns(playState.lastState().cooldownGroups()); + if (playState.lastState() != null) { + ((MapPlayer) player).setCooldowns(playState.lastState().cooldownGroups()); + } // The following is deinit logic which should not happen when switching from play to play (aka checkpoint reset). if (nextState instanceof AnyPlaying) return;