Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* and game-specific identifiers or timings.
*
* @author theEvilReaper
* @version 1.0.2
* @version 1.1.0
* @since 1.0.0
**/
public final class Helper {
Expand Down Expand Up @@ -69,6 +69,34 @@ public static int getRandomInt(int maximumValue) {
return ThreadLocalRandom.current().nextInt(0, maximumValue);
}

/**
* Clamps a value to lie within the given bounds, in place of hand-rolling
* {@code Math.min(max, Math.max(min, value))} at every call site.
*
* @param value the value to clamp
* @param min the inclusive lower bound
* @param max the inclusive upper bound
* @return {@code min} if {@code value} is lower, {@code max} if it is higher, {@code value} otherwise
*/
@Contract(pure = true)
public static int clamp(int value, int min, int max) {
return Math.clamp(value, min, max);
}

/**
* Clamps a value to lie within the given bounds, in place of hand-rolling
* {@code Math.min(max, Math.max(min, value))} at every call site.
*
* @param value the value to clamp
* @param min the inclusive lower bound
* @param max the inclusive upper bound
* @return {@code min} if {@code value} is lower, {@code max} if it is higher, {@code value} otherwise
*/
@Contract(pure = true)
public static double clamp(double value, double min, double max) {
return Math.clamp(value, min, max);
}

/**
* Adjusts the placement coordinates of a collectible page entity based on the
* block face/direction it is attached to, ensuring it aligns correctly and remains visible.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package net.onelitefeather.cygnus.common.util;

import net.minestom.server.entity.Player;
import org.jetbrains.annotations.Nullable;

import java.util.Collection;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;

/**
* Keeps one value per player, keyed by {@link Player#getUuid()}.
* <p>
* {@code EquipmentScreenOverlay}, {@code TunnelVisionService}, {@code BloodSplatterService},
* {@code SlenderGazeService} and {@code TunnelVisionCommand} each hand-rolled their own
* {@code Map<UUID, X>} field for this, disagreeing along the way on {@link ConcurrentHashMap} versus
* {@link java.util.LinkedHashMap}. This type settles that: it is backed by a
* {@code ConcurrentHashMap}, because state that outlives a single tick has to survive being written
* from a scheduler task and cleared from a disconnect or death listener in the same round, and
* nothing in this project pins both of those to the same thread. Three of the five call sites this
* type replaces already reached for {@code ConcurrentHashMap} for exactly that reason; the other two
* used a {@code LinkedHashMap} only for its insertion order, which none of the five ever relied on.
* Correctness under a race a caller does not control beats an ordering guarantee nobody asked for.
* </p>
*
* @param <V> the kind of value tracked per player
* @author TheMeinerLP
* @version 1.0.0
* @since 2.7.0
*/
public final class PlayerState<V> {

private final Map<UUID, V> values = new ConcurrentHashMap<>();

/**
* Stores a value for the given player, replacing whatever was tracked before.
*
* @param player the player to store a value for
* @param value the value to store
*/
public void put(Player player, V value) {
this.values.put(player.getUuid(), value);
}

/**
* Reads the value tracked for the given player.
*
* @param player the player to read
* @return the tracked value, or {@code null} if none is tracked
*/
public @Nullable V get(Player player) {
return this.values.get(player.getUuid());
}

/**
* Reads the value tracked for the given player, computing and storing one first if none is
* tracked yet.
*
* @param player the player to read
* @param supplier supplies the value to store when none is tracked yet
* @return the tracked value, existing or freshly computed
*/
public V computeIfAbsent(Player player, Supplier<V> supplier) {
return this.values.computeIfAbsent(player.getUuid(), _ -> supplier.get());
}

/**
* Stops tracking the given player.
*
* @param player the player to forget
* @return the value that was tracked for them, or {@code null} if none was
*/
public @Nullable V remove(Player player) {
return this.values.remove(player.getUuid());
}

/**
* The tracked values, without the players they belong to.
* <p>
* The returned collection is a live view over the backing map: removing through its iterator
* also stops tracking that player, which is what lets a caller fade values out one by one while
* walking them, the way {@code BloodSplatterService} does.
* </p>
*
* @return a live view over the tracked values
*/
public Collection<V> values() {
return this.values.values();
}

/**
* @return {@code true} if no player is currently tracked
*/
public boolean isEmpty() {
return this.values.isEmpty();
}

/**
* Stops tracking every player.
*/
public void clear() {
this.values.clear();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package net.onelitefeather.cygnus.common.util;

import net.minestom.server.MinecraftServer;
import net.minestom.server.timer.Task;
import org.jetbrains.annotations.Nullable;

import java.time.temporal.TemporalUnit;

/**
* Owns a single Minestom repeating scheduler {@link Task}.
* <p>
* {@code AmbientProvider}, {@code TunnelVisionService}, {@code SlenderGazeService} and
* {@code BloodSplatterService} each hand-rolled the same {@code @Nullable Task} field with a
* guard-and-return {@code startTask()}/{@code stopTask()} pair. This type is that field, extracted
* once: start and stop are both idempotent, so a caller never has to remember whether it already
* called either of them.
* </p>
* <p>
* The action to run is constructor-injected rather than passed to {@link #start(long, TemporalUnit)},
* because every one of the four services above ran exactly one action for the lifetime of the task
* and never swapped it out.
* </p>
*
* <p>Usage:</p>
* <pre>{@code
* RepeatingTask task = new RepeatingTask(this::tick);
* task.start(1, ChronoUnit.SECONDS);
* // ...
* task.stop();
* }</pre>
*
* @author TheMeinerLP
* @version 1.0.0
* @since 2.7.0
*/
public final class RepeatingTask {

private final Runnable action;
private @Nullable Task task;

/**
* Creates a task that, once started, runs the given action on every repetition.
*
* @param action the action to run
*/
public RepeatingTask(Runnable action) {
this.action = action;
}

/**
* Starts the task with the given period. Does nothing if the task is already running.
*
* @param period the amount of {@code unit}s between two runs
* @param unit the unit {@code period} is measured in
*/
public void start(long period, TemporalUnit unit) {
if (this.task != null) return;
this.task = MinecraftServer.getSchedulerManager()
.buildTask(this.action)
.repeat(period, unit)
.schedule();
}

/**
* Stops the task. Does nothing if the task is not running.
*/
public void stop() {
if (this.task == null) return;
this.task.cancel();
this.task = null;
}

/**
* @return {@code true} if the task is currently running
*/
public boolean isRunning() {
return this.task != null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package net.onelitefeather.cygnus.common.util;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

/**
* Verifies {@link Helper#clamp(int, int, int)} and {@link Helper#clamp(double, double, double)},
* which replace the hand-rolled {@code Math.min(hi, Math.max(lo, x))} scattered across the tunnel
* vision and slender gaze code.
*
* @author TheMeinerLP
* @version 1.0.0
* @since 2.7.0
*/
class HelperTest {

@Test
@DisplayName("An int within bounds is returned unchanged")
void intWithinBoundsIsUnchanged() {
assertEquals(5, Helper.clamp(5, 0, 10));
}

@Test
@DisplayName("An int below the lower bound is raised to it")
void intBelowLowerBoundIsRaised() {
assertEquals(0, Helper.clamp(-5, 0, 10));
}

@Test
@DisplayName("An int above the upper bound is lowered to it")
void intAboveUpperBoundIsLowered() {
assertEquals(10, Helper.clamp(15, 0, 10));
}

@Test
@DisplayName("A double within bounds is returned unchanged")
void doubleWithinBoundsIsUnchanged() {
assertEquals(0.5D, Helper.clamp(0.5D, 0.0D, 1.0D));
}

@Test
@DisplayName("A double below the lower bound is raised to it")
void doubleBelowLowerBoundIsRaised() {
assertEquals(0.0D, Helper.clamp(-0.5D, 0.0D, 1.0D));
}

@Test
@DisplayName("A double above the upper bound is lowered to it")
void doubleAboveUpperBoundIsLowered() {
assertEquals(1.0D, Helper.clamp(1.5D, 0.0D, 1.0D));
}
}
Loading
Loading