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 @@ -4,6 +4,9 @@
import com.eternalcode.core.feature.catboy.CatboyService;
import com.eternalcode.core.feature.home.HomeService;
import com.eternalcode.core.feature.jail.JailService;
import com.eternalcode.core.feature.kit.KitClaimService;
import com.eternalcode.core.feature.kit.KitCooldownService;
import com.eternalcode.core.feature.kit.KitService;
import com.eternalcode.core.feature.msg.MsgService;
import com.eternalcode.core.feature.ignore.IgnoreService;
import com.eternalcode.core.feature.randomteleport.RandomTeleportService;
Expand All @@ -23,6 +26,12 @@ public interface EternalCoreApi {

JailService getJailService();

KitService getKitService();

KitCooldownService getKitCooldownService();

KitClaimService getKitClaimService();

MsgService getMsgService();

RandomTeleportService getRandomTeleportService();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package com.eternalcode.core.feature.kit;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.regex.Pattern;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;

/**
* Immutable kit definition. Every {@link ItemStack} going in or out is defensively cloned,
* so mutating returned items never changes the kit. Use {@link #toBuilder()} to derive a modified copy.
*
* @param name unique kit name, see {@link #isValidName(String)}.
* @param displayName MiniMessage formatted display name.
*/
public record Kit(
String name,
String displayName,
Duration cooldown,
String permission,
List<ItemStack> items,
List<String> commands,
ItemStack icon,
int slot
) {

public static final int MIN_SLOT = 0;

private static final Pattern NAME_PATTERN = Pattern.compile("^[a-z0-9_-]{1,32}$");
private static final Material DEFAULT_ICON = Material.CHEST;

public Kit {
if (!isValidName(name)) {
throw new IllegalArgumentException("Invalid kit name '" + name + "', expected " + NAME_PATTERN.pattern());
}

if (cooldown.isNegative()) {
throw new IllegalArgumentException("cooldown cannot be negative");
}

if (permission.isBlank()) {
throw new IllegalArgumentException("permission cannot be blank");
}

if (icon.getType().isAir()) {
throw new IllegalArgumentException("icon cannot be air");
}

if (slot < MIN_SLOT) {
throw new IllegalArgumentException("slot cannot be lower than " + MIN_SLOT);
}

displayName = displayName == null ? name : displayName;
items = cloneItems(items);
commands = List.copyOf(commands);
icon = icon.clone();
}

public static boolean isValidName(String name) {
return name != null && NAME_PATTERN.matcher(name).matches();
}

public static Builder builder(String name) {
return new Builder(name);
}

public Builder toBuilder() {
return new Builder(this.name)
.displayName(this.displayName)
.cooldown(this.cooldown)
.permission(this.permission)
.items(this.items)
.commands(this.commands)
.icon(this.icon)
.slot(this.slot);
}

public boolean hasCooldown() {
return !this.cooldown.isZero();
}

/**
* @return defensive copy - ItemStack is mutable.
*/
@Override
public List<ItemStack> items() {
return cloneItems(this.items);
}

/**
* @return defensive copy - ItemStack is mutable.
*/
@Override
public ItemStack icon() {
return this.icon.clone();
}

private static List<ItemStack> cloneItems(Collection<ItemStack> source) {
List<ItemStack> copy = new ArrayList<>(source.size());

for (ItemStack item : source) {
if (item != null && !item.getType().isAir()) {
copy.add(item.clone());
}
}

return List.copyOf(copy);
}

/**
* Kits are identified by name only.
*/
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}

if (!(other instanceof Kit kit)) {
return false;
}

return this.name.equals(kit.name);
}

@Override
public int hashCode() {
return this.name.hashCode();
}

@Override
public String toString() {
return "Kit{name='" + this.name + "', cooldown=" + this.cooldown + ", permission='" + this.permission + "'}";
}

public static final class Builder {

private final String name;
private String displayName;
private Duration cooldown = Duration.ZERO;
private String permission;
private List<ItemStack> items = List.of();
private List<String> commands = List.of();
private ItemStack icon = new ItemStack(DEFAULT_ICON);
private int slot = MIN_SLOT;

private Builder(String name) {
this.name = name;
}

public Builder displayName(String displayName) {
this.displayName = displayName;
return this;
}

public Builder cooldown(Duration cooldown) {
this.cooldown = cooldown;
return this;
}

public Builder permission(String permission) {
this.permission = permission;
return this;
}

public Builder items(Collection<ItemStack> items) {
this.items = new ArrayList<>(items);
return this;
}

public Builder commands(Collection<String> commands) {
this.commands = List.copyOf(commands);
return this;
}

public Builder icon(ItemStack icon) {
this.icon = icon;
return this;
}

public Builder slot(int slot) {
this.slot = slot;
return this;
}

/**
* Validation and defensive copying happen in the record's compact constructor.
*/
public Kit build() {
return new Kit(
this.name,
this.displayName,
this.cooldown,
this.permission,
this.items,
this.commands,
this.icon,
this.slot
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.eternalcode.core.feature.kit;

import java.time.Duration;
import java.util.Objects;

/**
* @param status result of the claim.
* @param remainingCooldown remaining cooldown, {@link Duration#ZERO} unless status is {@link KitClaimStatus#ON_COOLDOWN}.
*/
public record KitClaimResult(KitClaimStatus status, Duration remainingCooldown) {

public static KitClaimResult of(KitClaimStatus status) {
return new KitClaimResult(status, Duration.ZERO);
}

public static KitClaimResult onCooldown(Duration remainingCooldown) {
return new KitClaimResult(KitClaimStatus.ON_COOLDOWN, remainingCooldown);
}

public boolean isSuccess() {
return this.status == KitClaimStatus.SUCCESS;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.eternalcode.core.feature.kit;

import org.bukkit.entity.Player;

public interface KitClaimService {

/**
* Full claim flow: permission check, cooldown check, {@link com.eternalcode.core.feature.kit.event.KitClaimEvent},
* cooldown and item/command delivery. Must be called from the player's thread (main thread on Paper).
*/
KitClaimResult claimKit(Player player, Kit kit);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.eternalcode.core.feature.kit;

public enum KitClaimStatus {

SUCCESS,
NO_PERMISSION,
ON_COOLDOWN,
CANCELLED,
/**
* Player's cooldowns are still being loaded from the database (right after join) or loading failed.
*/
COOLDOWNS_NOT_LOADED

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.eternalcode.core.feature.kit;

import java.time.Duration;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;

/**
* Kit cooldowns. Cooldowns of online players are kept in memory (loaded on join),
* every change is persisted to the database, so cooldowns are shared across the whole network.
*/
public interface KitCooldownService {

/**
* @return remaining cooldown, or empty if the kit can be claimed or the player is offline.
*/
Optional<Duration> getRemainingCooldown(UUID playerUniqueId, Kit kit);

/**
* @return remaining cooldowns of all kits that are still blocked, keyed by kit name (empty for offline players).
*/
Map<String, Duration> getRemainingCooldowns(UUID playerUniqueId);

/**
* Starts the kit cooldown for the player. Kits without cooldown are ignored.
*/
void applyCooldown(UUID playerUniqueId, Kit kit);

/**
* Resets the kit cooldown, works for offline players too.
*/
void resetCooldown(UUID playerUniqueId, Kit kit);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.eternalcode.core.feature.kit;

import java.util.List;
import java.util.Optional;

/**
* Manages kit definitions stored in {@code plugins/EternalCore/kits/<kit name>.yml}.
* Reads are served from memory, writes update memory immediately and are persisted in the background.
*/
public interface KitService {

Optional<Kit> findKit(String name);

boolean exists(String name);

/**
* @return all kits sorted by their GUI slot.
*/
List<Kit> getKits();

/**
* Creates the kit or replaces an existing kit with the same name.
*/
void saveKit(Kit kit);

/**
* Deletes the kit file together with all cooldowns bound to it.
*/
void deleteKit(String name);

}
Loading
Loading