Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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 @@ -133,6 +133,88 @@ public static Component serverInfo(String serverName) {
*/
public static final Component LOBBY_RETURN_KICK = Component.text("Returning to the lobby", NamedTextColor.GRAY);

/** @since 2.21.0 */
public static final Component PASS_UNAVAILABLE = withPrefix(Component.text("Slender passes are not available on this server.", NamedTextColor.RED));
/** @since 2.21.0 */
public static final Component PASS_CLOSED = withPrefix(Component.text("Slender passes can only be reserved in the lobby.", NamedTextColor.RED));
/** @since 2.21.0 */
public static final Component PASS_NO_BALANCE = withPrefix(Component.text("You have no slender pass to reserve.", NamedTextColor.RED));
/** @since 2.21.0 */
public static final Component PASS_TAKEN = withPrefix(Component.text("Another player already holds the slender pass for this round.", NamedTextColor.RED));
/** @since 2.21.0 */
public static final Component PASS_ALREADY_RESERVED = withPrefix(Component.text("You already hold the slender pass for this round.", NamedTextColor.YELLOW));
/** @since 2.21.0 */
public static final Component PASS_RESERVED = withPrefix(Component.text("You will play the Slender this round. The pass is spent when the round starts.", NamedTextColor.GREEN));
/** @since 2.21.0 */
public static final Component PASS_CANCELLED = withPrefix(Component.text("Your slender pass reservation is cancelled.", NamedTextColor.GRAY));
/** @since 2.21.0 */
public static final Component PASS_NOTHING_TO_CANCEL = withPrefix(Component.text("You do not hold a slender pass reservation.", NamedTextColor.RED));
/** Lobby announcement, it names nobody on purpose. @since 2.21.0 */
public static final Component PASS_REDEEMED = withPrefix(Component.text("A slender pass has been redeemed for this round.", NamedTextColor.GRAY));
/** @since 2.21.0 */
public static final Component PASS_ONLY_PLAYERS = withPrefix(Component.text("Only players can hold a slender pass.", NamedTextColor.RED));
/** @since 2.21.0 */
public static final Component PASS_NO_PERMISSION = withPrefix(Component.text("You are not allowed to change slender passes.", NamedTextColor.RED));
/** @since 2.21.0 */
public static final Component PASS_TARGET_OFFLINE = withPrefix(Component.text("That player is not online.", NamedTextColor.RED));
/** @since 2.21.0 */
public static final Component PASS_NEGATIVE_AMOUNT = withPrefix(Component.text("The amount cannot be negative.", NamedTextColor.RED));
/** @since 2.21.0 */
public static final Component PASS_NOTHING_REMOVED = withPrefix(Component.text("Nothing was removed, the player has no slender passes.", NamedTextColor.RED));

/**
* Tells a player how many slender passes they have.
*
* @param balance the passes
* @return the message
* @since 2.21.0
*/
public static Component passBalance(int balance) {
return withPrefix(Component.text("You have ", NamedTextColor.GRAY)
.append(Component.text(String.valueOf(balance), NamedTextColor.GREEN))
.append(Component.text(" slender pass(es).", NamedTextColor.GRAY)));
}

/**
* Tells a player that a Survivor round earned a pass.
*
* @param balance the new balance
* @return the message
* @since 2.21.0
*/
public static Component passEarnedPity(int balance) {
return withPrefix(Component.text("You earned a slender pass for playing as a Survivor. You have ", NamedTextColor.GREEN)
.append(Component.text(String.valueOf(balance), NamedTextColor.GREEN))
.append(Component.text(" slender pass(es).", NamedTextColor.GREEN)));
}

/**
* Tells a player that the monthly quota of their rank was credited.
*
* @param balance the new balance
* @return the message
* @since 2.21.0
*/
public static Component passEarnedQuota(int balance) {
return withPrefix(Component.text("Your rank grants slender passes for this month. You have ", NamedTextColor.GREEN)
.append(Component.text(String.valueOf(balance), NamedTextColor.GREEN))
.append(Component.text(" slender pass(es).", NamedTextColor.GREEN)));
}

/**
* Tells an administrator the new balance of a player.
*
* @param name the player's name
* @param balance the new balance
* @return the message
* @since 2.21.0
*/
public static Component passChanged(String name, int balance) {
return withPrefix(Component.text(name + " has ", NamedTextColor.GRAY)
.append(Component.text(String.valueOf(balance), NamedTextColor.GREEN))
.append(Component.text(" slender pass(es) now.", NamedTextColor.GRAY)));
}

@Contract(value = "_ -> new", pure = true)
public static Component withPrefix(String component) {
return PREFIX.append(MINI_MESSAGE.deserialize(component));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public record GameConfig(
ClientGuardConfig clientGuard,
ClientApiConfig clientApi,
LunarConfig lunar,
SlenderPassConfig pass,
float lobbyAtmosphereShare
) {

Expand Down Expand Up @@ -152,6 +153,7 @@ public record GameConfig(
ClientGuardConfig.DEFAULT,
ClientApiConfig.DEFAULT,
LunarConfig.DEFAULT,
SlenderPassConfig.DEFAULT,
DEFAULT_LOBBY_ATMOSPHERE_SHARE
);

Expand Down Expand Up @@ -190,6 +192,7 @@ public static GameConfig read(ConfigSection root) {
ClientGuardConfig.read(root.section("client-guard.")),
ClientApiConfig.read(root.section("client-api.")),
LunarConfig.read(root.section("lunar.")),
SlenderPassConfig.read(root.section("pass.")),
readLobbyAtmosphereShare(root)
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package net.onelitefeather.cygnus.common.config;

import static net.onelitefeather.cygnus.common.config.ConfigChecks.atLeast;
import static net.onelitefeather.cygnus.common.config.ConfigChecks.notNegative;

/**
* The settings of the slender pass earning: how many Survivor rounds earn one pass, and the cap that earned passes
* never lift a balance above. Read from the keys {@code pass.pity-rounds} and {@code pass.max-balance}.
*
* @param pityRounds the Survivor rounds until one pass is earned, at least 1
* @param maxBalance the cap for earned passes, zero or more
* @author TheMeinerLP
* @version 1.0.0
* @since 2.21.0
*/
public record SlenderPassConfig(int pityRounds, int maxBalance) {

/** The defaults: a pass after ten Survivor rounds, at most five passes from earning. */
public static final SlenderPassConfig DEFAULT = new SlenderPassConfig(10, 5);

/**
* Rejects values that would make the earning meaningless. Invalid values stop the start; they are not replaced by
* the defaults.
*/
public SlenderPassConfig {
atLeast("pass.pity-rounds", pityRounds, 1);
notNegative("pass.max-balance", maxBalance);
}

/**
* Reads the section {@code pass.*}. A missing key takes its default, an invalid value fails the read.
*
* @param section the section that holds the {@code pass.} keys
* @return the settings
*/
public static SlenderPassConfig read(ConfigSection section) {
return new SlenderPassConfig(
section.getInt("pity-rounds", DEFAULT.pityRounds()),
section.getInt("max-balance", DEFAULT.maxBalance())
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
import me.lucko.luckperms.minestom.app.LuckPermsMinestomHandle;
import me.lucko.luckperms.minestom.app.LuckPermsMinestomOptions;
import me.lucko.luckperms.minestom.loader.MinestomLoader;
import net.luckperms.api.LuckPermsProvider;
import net.luckperms.api.model.user.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.file.Path;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;

/**
Expand All @@ -28,6 +31,8 @@
public final class LuckPermsSupport {

private static final Logger LOGGER = LoggerFactory.getLogger(LuckPermsSupport.class);
/** The LuckPerms meta that gives the monthly quota of slender passes of a player. @since 2.21.0 */
public static final String MONTHLY_QUOTA_META = "cygnus.slender-pass.monthly-quota";
private static final List<String> COMMAND_ALIASES =
List.of("luckperms", "lp", "perm", "perms", "permission", "permissions");
private static final String LOADER_CLASS = "me.lucko.luckperms.minestom.loader.MinestomLoader";
Expand Down Expand Up @@ -91,6 +96,35 @@ private static void startLuckPerms() {
*
* @return {@code true} if the class is available, {@code false} after logging a warning
*/
/**
* Reads the monthly quota of slender passes from the LuckPerms meta {@link #MONTHLY_QUOTA_META} of a player. The
* cached user data is read, so no I/O happens.
*
* @param player the player
* @return the quota, {@code 0} without LuckPerms, without user data, without the meta, or for a value that is not
* an integer
* @since 2.21.0
*/
public static int monthlyQuota(UUID player) {
if (!PRESENT) {
return 0;
}
User user = LuckPermsProvider.get().getUserManager().getUser(player);
if (user == null) {
return 0;
}
String value = user.getCachedData().getMetaData().getMetaValue(MONTHLY_QUOTA_META);
if (value == null) {
return 0;
}
try {
return Math.max(0, Integer.parseInt(value.trim()));
} catch (NumberFormatException exception) {
LOGGER.warn("The meta {} of {} is not a whole number: {}", MONTHLY_QUOTA_META, player, value);
return 0;
}
}

private static boolean detect() {
try {
Class.forName(LOADER_CLASS, false, LuckPermsSupport.class.getClassLoader());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ void testInvalidLobbyAtmosphereShare() {
assertRejected("lobbyAtmosphereShare (1.5) must be between 0 and 1", () -> new GameConfig(
defaults.round(), defaults.teams(), null, defaults.resourcePack(), defaults.pageProximity(),
defaults.damageSound(), defaults.glitch(), defaults.pageGlitch(), defaults.creek(),
defaults.sanity(), defaults.stamina(), defaults.adrenaline(), defaults.footprint(), defaults.possession(), defaults.telemetry(), defaults.minimap(), defaults.eightPages(), defaults.regeneration(), defaults.lure(), defaults.clientGuard(), defaults.clientApi(), defaults.lunar(), 1.5F));
defaults.sanity(), defaults.stamina(), defaults.adrenaline(), defaults.footprint(), defaults.possession(), defaults.telemetry(), defaults.minimap(), defaults.eightPages(), defaults.regeneration(), defaults.lure(), defaults.clientGuard(), defaults.clientApi(), defaults.lunar(), defaults.pass(), 1.5F));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package net.onelitefeather.cygnus.common.config;

import org.junit.jupiter.api.Test;

import java.util.Properties;

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

/**
* Tests the {@code pass.*} keys of the slender pass earning. Every test builds its own properties.
*
* @author TheMeinerLP
* @version 1.0.0
* @since 2.21.0
*/
class SlenderPassConfigTest {

@Test
void theDefaultsAreTenSurvivorRoundsAndACapOfFive() {
assertEquals(new SlenderPassConfig(10, 5), read(new Properties()));
}

@Test
void theConfiguredValuesAreRead() {
Properties properties = new Properties();
properties.setProperty("pass.pity-rounds", "4");
properties.setProperty("pass.max-balance", "2");

assertEquals(new SlenderPassConfig(4, 2), read(properties));
}

@Test
void aPityThresholdBelowOneIsRefusedWithItsKey() {
IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> new SlenderPassConfig(0, 5));

assertTrue(error.getMessage().contains("pass.pity-rounds (0) must be at least 1"), error.getMessage());
}

@Test
void aNegativeCapIsRefusedWithItsKey() {
IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> new SlenderPassConfig(10, -1));

assertTrue(error.getMessage().contains("pass.max-balance"), error.getMessage());
}

@Test
void anInvalidValueFromTheFileFailsTheStartInsteadOfFallingBackToTheDefault() {
Properties properties = new Properties();
properties.setProperty("pass.pity-rounds", "0");

assertThrows(IllegalArgumentException.class, () -> read(properties));
}

private static SlenderPassConfig read(Properties properties) {
return SlenderPassConfig.read(ConfigSection.root(properties).section("pass."));
}
}
11 changes: 11 additions & 0 deletions config.properties.example
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,17 @@ survivorTeamSize=12
#lunar.enabled=true
#lunar.disabled-mods=minimap,waypoints,freelook,zoom,coordinates,directionhud

# --- Slender pass -----------------------------------------------------------
# Passes are earned by playing and by a monthly rank quota (the LuckPerms meta
# cygnus.slender-pass.monthly-quota, once per UTC month in the lobby). Both need the statistics
# database, the same as the feature itself.
#
# pity-rounds Survivor rounds played to the end until one pass is earned. At least 1.
# max-balance The cap for earned passes: earning never lifts a balance above it. Administrator
# changes are not capped. Zero or more.
#pass.pity-rounds=10
#pass.max-balance=5

# --- Statistics -------------------------------------------------------------
# Yearly player statistics. Every finished round is written to a PostgreSQL database: one row
# per player and round, plus one aggregate per player and calendar year. The schema is created
Expand Down
Loading
Loading