Skip to content

Commit

Permalink
Implement active ability source checking methods (#8)
Browse files Browse the repository at this point in the history
* Implement active ability source checking methods

* Integrate reviews
  • Loading branch information
Pyrofab authored Nov 12, 2021
1 parent 4b35652 commit 1dd2783
Show file tree
Hide file tree
Showing 8 changed files with 202 additions and 28 deletions.
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ indent_size = 2

[*.md]
trim_trailing_whitespace = false

[*.java]
ij_java_doc_do_not_wrap_if_one_line = true
94 changes: 84 additions & 10 deletions src/main/java/io/github/ladysnake/pal/AbilitySource.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.NotNull;

/**
* A source for a {@link PlayerAbility}.
Expand All @@ -32,14 +33,55 @@
* {@link Pal#getAbilitySource(Identifier)} or {@link Pal#getAbilitySource(String, String)}.
* Instances obtained that way are safe to compare by identity.
*/
public final class AbilitySource {
public final class AbilitySource implements Comparable<AbilitySource> {
/** A standard priority for ability sources that are free to use (e.g. active potion effects) */
public static final int FREE = 2000;
/** A standard priority for ability sources that cost some renewable resource to use (e.g. stamina) */
public static final int RENEWABLE = 1000;
/** The default priority for ability sources */
public static final int DEFAULT = 0;
/** A standard priority for ability sources that cost some non-renewable resource to use (e.g. item fuel) */
public static final int CONSUMABLE = -1000;

private final Identifier id;
private final int priority;

/**
* @see Pal#getAbilitySource(String, String)
* @see Pal#getAbilitySource(Identifier)
* @see Pal#getAbilitySource(Identifier, int)
*/
AbilitySource(Identifier id) {
AbilitySource(Identifier id, int priority) {
this.id = id;
this.priority = priority;
}

/**
* Returns the identifier used to create this {@code AbilitySource}.
*
* <p> The returned identifier is unique and can be passed to {@link Pal#getAbilitySource(Identifier)}
* to retrieve this instance.
*
* @return the identifier wrapped by this {@code AbilitySource}
*/
public Identifier getId() {
return this.id;
}

/**
* Returns the priority used to create this {@code AbilitySource}.
*
* <p>If no priority was specified during registration, the {@linkplain #DEFAULT default priority} is used.
*
* @return the priority assigned to this {@code AbilitySource}
* @see Pal#getAbilitySource(Identifier, int)
* @see #FREE
* @see #RENEWABLE
* @see #DEFAULT
* @see #CONSUMABLE
*/
public int getPriority() {
return priority;
}

/**
Expand Down Expand Up @@ -72,7 +114,7 @@ public void revokeFrom(PlayerEntity player, PlayerAbility ability) {
* Returns {@code true} if this ability source is currently granting {@code player}
* the given {@code ability}.
*
* @param player the player to check on
* @param player the player to check on
* @param ability an ability that may be granted by this source
* @return {@code true} if this grants {@code player} the {@code ability}
*/
Expand All @@ -81,20 +123,52 @@ public boolean grants(PlayerEntity player, PlayerAbility ability) {
}

/**
* Returns the identifier used to create this {@code AbilitySource}.
* Returns {@code true} if this ability source is the one actively granting {@code ability}
* to {@code player}.
*
* <p> The returned identifier is unique and can be passed to {@link Pal#getAbilitySource(Identifier)}
* to retrieve this instance.
* <p>At most one {@code AbilitySource} can return {@code true} for a given player and ability.
* When more than one source is granting the same ability, the one considered active is the highest
* one according to {@link #compareTo(AbilitySource)}. This means that an {@code AbilitySource} with
* a higher {@linkplain #getPriority() priority} is more likely to be considered "active".
*
* @return the identifier wrapped by this {@code AbilitySource}
* <p>This method can be used to check if side effects should trigger for a specific ability source,
* e.g. to avoid wasting jetpack fuel when multiple sources are giving flight at the same time.
*
* @param player the player to check on
* @param ability an ability that may be granted by this source
* @return {@code true} if this ability source is the one actively granting {@code ability}
* to {@code player}, {@code false} otherwise.
* @since 1.4.0
*/
public Identifier getId() {
return this.id;
public boolean isActivelyGranting(PlayerEntity player, PlayerAbility ability) {
return ability.getTracker(player).getActiveSource() == this;
}

/**
* Compares two ability sources.
*
* <p>The comparison is based on the assigned {@linkplain #getPriority() priority},
* with ties being resolved arbitrarily. Priorities are compared using their natural ordering,
* meaning a higher priority causes the source to be considered higher.
*
* @param o the source to be compared.
* @return the value {@code 0} if the argument source is equal to
* this source; a value less than {@code 0} if this source
* is considered less than the source argument; and a
* value greater than {@code 0} if this source is
* considered greater than the string argument.
* @implNote the current way to resolve ties is through identifier comparison.
* @since 1.4.0
*/
@Override
public int compareTo(@NotNull AbilitySource o) {
int priorityOrder = Integer.compare(this.priority, o.priority);
return priorityOrder != 0 ? priorityOrder : this.id.compareTo(o.id);
}

@Override
public String toString() {
return "AbilitySource@" + this.id;
return "AbilitySource@" + this.id + "+" + this.priority;
}

}
20 changes: 20 additions & 0 deletions src/main/java/io/github/ladysnake/pal/AbilityTracker.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@
*/
package io.github.ladysnake.pal;

import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.Nullable;

/**
* A tracker for a player ability that can be turned on or off.
Expand Down Expand Up @@ -59,6 +62,23 @@ public interface AbilityTracker {
@Contract(pure = true)
boolean isGrantedBy(AbilitySource abilitySource);

/**
* Returns the ability source actively granting this tracker's ability.
*
* <p>The active source is the one considered highest among the sources currently granting this tracker's ability,
* according to {@link AbilitySource#compareTo(AbilitySource)}.
* This means that an {@code AbilitySource} with
* a higher {@linkplain AbilitySource#getPriority() priority} is more likely to be considered "active".
* .
*
* @return the active source, or {@code null} if no source is granting the ability
* @see Pal#getAbilitySource(Identifier, int)
* @see AbilitySource#isActivelyGranting(PlayerEntity, PlayerAbility)
* @see AbilitySource#compareTo(AbilitySource)
* @since 1.4.0
*/
@Nullable AbilitySource getActiveSource();

/**
* Returns {@code true} if this tracker's ability is currently enabled.
*
Expand Down
46 changes: 37 additions & 9 deletions src/main/java/io/github/ladysnake/pal/Pal.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import net.fabricmc.api.ModInitializer;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.util.Identifier;
import net.minecraft.util.Lazy;
import org.jetbrains.annotations.Nullable;

import java.util.Objects;
Expand Down Expand Up @@ -68,7 +67,7 @@ public final class Pal implements ModInitializer {
*
* @param player the player on which to enable the ability
* @param ability the ability to enable
* @param reason the reason for which the player should get the ability
* @param reason the reason for which the player should get the ability
* @since 1.0.1
*/
public static void grantAbility(PlayerEntity player, PlayerAbility ability, AbilitySource reason) {
Expand All @@ -87,7 +86,7 @@ public static void grantAbility(PlayerEntity player, PlayerAbility ability, Abil
*
* @param player the player to revoke the ability from
* @param ability the ability to revoke
* @param reason the reason for which the player had the ability
* @param reason the reason for which the player had the ability
* @since 1.0.1
*/
public static void revokeAbility(PlayerEntity player, PlayerAbility ability, AbilitySource reason) {
Expand All @@ -108,19 +107,47 @@ public static AbilitySource getAbilitySource(String namespace, String name) {
}

/**
* Returns an {@code AbilitySource} corresponding to the given {abilitySourceId}.
* Returns an {@code AbilitySource} corresponding to the given {@code abilitySourceId}.
*
* <p> Calling this method multiple times with equivalent identifiers results
* <p>Calling this method multiple times with equivalent identifiers results
* in a single instance being returned. More formally, for any two Identifiers
* {@code i1} and {@code i2}, {code getAbilitySource(i1) == getAbilitySource(i1)}
* {@code i1} and {@code i2}, {@code getAbilitySource(i1) == getAbilitySource(i2)}
* is true if and only if {@code i1.equals(i2)}.
*
* @param abilitySourceId a unique identifier for the ability source
* @return an {@code AbilitySource} for {@code abilitySourceId}
* @throws NullPointerException if {@code abilitySourceId} is null
*/
public static AbilitySource getAbilitySource(Identifier abilitySourceId) {
return PalInternals.registerSource(abilitySourceId, AbilitySource::new);
return PalInternals.registerSource(abilitySourceId, null, AbilitySource::new);
}

/**
* Returns an {@code AbilitySource} corresponding to the given {@code abilitySourceId}.
*
* <p>Calling this method multiple times with equivalent identifiers results
* in a single instance being returned. More formally, for any two Identifiers
* {@code i1} and {@code i2}, {@code getAbilitySource(i1) == getAbilitySource(i2)}
* is true if and only if {@code i1.equals(i2)}.
*
* <p>The {@code priority} determines which source will show up as the {@linkplain AbilityTracker#getActiveSource() active one}
* in the event multiple sources are granting the same ability. This can be used to e.g. avoid wasting fuel
* through multiple flight items.
*
* @param abilitySourceId a unique identifier for the ability source
* @return an {@code AbilitySource} for {@code abilitySourceId}
* @throws NullPointerException if {@code abilitySourceId} is null
* @throws IllegalStateException if another source was already registered with a different {@code priority}
* @see AbilitySource#FREE
* @see AbilitySource#RENEWABLE
* @see AbilitySource#DEFAULT
* @see AbilitySource#CONSUMABLE
* @see AbilityTracker#getActiveSource()
* @see AbilitySource#isActivelyGranting(PlayerEntity, PlayerAbility)
* @since 1.4.0
*/
public static AbilitySource getAbilitySource(Identifier abilitySourceId, int priority) {
return PalInternals.registerSource(abilitySourceId, priority, AbilitySource::new);
}

/**
Expand All @@ -131,8 +158,8 @@ public static AbilitySource getAbilitySource(Identifier abilitySourceId) {
* @param path path of this ability's identifier
* @param factory a factory to create {@code ToggleableAbility} instances for every player
* @return a {@code PlayerAbility} registered with the constructed id
* @see #registerAbility(String, String, BiFunction)
* @throws NullPointerException if any of the arguments is null
* @see #registerAbility(String, String, BiFunction)
*/
public static PlayerAbility registerAbility(String namespace, String path, BiFunction<PlayerAbility, PlayerEntity, AbilityTracker> factory) {
return registerAbility(new Identifier(namespace, path), factory);
Expand All @@ -148,9 +175,9 @@ public static PlayerAbility registerAbility(String namespace, String path, BiFun
* @param abilityId a unique identifier for the ability
* @param factory a factory to create {@code ToggleableAbility} instances for every player
* @throws IllegalStateException if {@code abilityId}
* @throws NullPointerException if any of the arguments is null
* @apiNote abilities must be registered during initialization.
* @see SimpleAbilityTracker
* @throws NullPointerException if any of the arguments is null
*/
public static PlayerAbility registerAbility(Identifier abilityId, BiFunction<PlayerAbility, PlayerEntity, AbilityTracker> factory) {
return PalInternals.registerAbility(new PlayerAbility(Objects.requireNonNull(abilityId), Objects.requireNonNull(factory)));
Expand All @@ -175,6 +202,7 @@ public static boolean isAbilityRegistered(@Nullable Identifier abilityId) {
* @throws NullPointerException if {@code abilityId} is null
*/
public static Supplier<PlayerAbility> provideRegisteredAbility(Identifier abilityId) {
Objects.requireNonNull(abilityId, "abilityId cannot be null");
return Suppliers.memoize(() -> Objects.requireNonNull(PalInternals.getAbility(abilityId), abilityId + " has not been registered"));
}

Expand Down
12 changes: 9 additions & 3 deletions src/main/java/io/github/ladysnake/pal/SimpleAbilityTracker.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
*/
package io.github.ladysnake.pal;

import io.github.ladysnake.pal.impl.PalInternals;
import io.github.ladysnake.pal.impl.VanillaAbilityTracker;
import net.fabricmc.fabric.api.util.NbtType;
import net.minecraft.entity.player.PlayerEntity;
Expand All @@ -25,8 +26,8 @@
import net.minecraft.nbt.NbtString;
import net.minecraft.util.Identifier;

import java.util.HashSet;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

/**
* This class provides a basic implementation of the {@code AbilityTracker}
Expand All @@ -41,7 +42,7 @@
*/
public class SimpleAbilityTracker implements AbilityTracker {
protected final PlayerEntity player;
protected final Set<AbilitySource> abilitySources = new HashSet<>();
protected final SortedSet<AbilitySource> abilitySources = new TreeSet<>();
protected final PlayerAbility ability;

public SimpleAbilityTracker(PlayerAbility ability, PlayerEntity player) {
Expand Down Expand Up @@ -73,6 +74,11 @@ public boolean isGrantedBy(AbilitySource abilitySource) {
return this.abilitySources.contains(abilitySource);
}

@Override
public AbilitySource getActiveSource() {
return this.abilitySources.last();
}

@Override
public void refresh(boolean syncVanilla) {
this.updateState(this.shouldBeEnabled());
Expand Down
23 changes: 17 additions & 6 deletions src/main/java/io/github/ladysnake/pal/impl/PalInternals.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@

import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.function.BiFunction;

public final class PalInternals {

Expand Down Expand Up @@ -65,16 +65,27 @@ public static AbilitySource getSource(@Nullable Identifier sourceId) {
return sources.get(sourceId);
}

public static AbilitySource registerSource(Identifier sourceId, Function<Identifier, AbilitySource> factory) {
public static AbilitySource registerSource(Identifier sourceId, @Nullable Integer priority, BiFunction<Identifier, Integer, AbilitySource> factory) {
Preconditions.checkNotNull(sourceId);
AbilitySource value = sources.get(sourceId);
if (value == null) {
AbilitySource existing = sources.get(sourceId);

if (existing == null) {
synchronized (sources) {
return sources.computeIfAbsent(sourceId, factory); // off-chance that someone modifies the map concurrently
existing = sources.get(sourceId);
// off-chance that someone modifies the map concurrently
if (existing == null) {
AbilitySource source = factory.apply(sourceId, priority == null ? AbilitySource.DEFAULT : priority);
sources.put(sourceId, source);
return source;
}
}
}

return value;
if (priority != null && existing.getPriority() != priority) {
throw new IllegalStateException(sourceId + " has been registered twice with differing priorities: " + existing.getPriority() + ", " + priority);
}

return existing;
}

public static boolean isAbilityRegistered(Identifier abilityId) {
Expand Down
Loading

0 comments on commit 1dd2783

Please sign in to comment.