feat: initial CR-Core library (team + player + command framework)

Pure Maven library for CR Minecraft game plugins, targeting Paper 1.16.5.

Common abstractions (fr.luc.crcore.common): Identifiable, Named, ScoreHolder,
AbstractEntity, Repository<T>.

Team domain (fr.luc.crcore.team): Team entity with name/tag/color/leader/
visibility (PUBLIC|PRIVATE)/members/scores/spawn point, TeamMember,
TeamRole/TeamColor/TeamVisibility enums, TeamRanking record, TeamService with
overridable hooks (factories, validations, lifecycle events), in-memory
repository, dedicated exception hierarchy.

Player domain (fr.luc.crcore.player): PlayerProfile with named scores per
player, PlayerProfileService with auto-creation, individual rankings,
exception hierarchy. Both Team and PlayerProfile implement ScoreHolder.

Command framework (fr.luc.crcore.command): Command interface,
AbstractCommand base, BaseCommand (CommandExecutor + TabCompleter), SubCommand,
CommandContext, CommandResult, ArgumentType<T> + ArgumentTypes catalogue
(STRING, INTEGER, DOUBLE, BOOLEAN, ONLINE_PLAYER, enumOf, choice).

Docs (docs/) is the single source of truth: README, setup, features,
decisions log, and 6 PlantUML diagrams (team class/sequence/activity/join,
player class, command class).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Antone Barbaud
2026-06-08 17:17:56 +02:00
commit ffc77c4213
53 changed files with 3642 additions and 0 deletions
@@ -0,0 +1,133 @@
package fr.luc.crcore.command;
import org.bukkit.command.CommandSender;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public abstract class AbstractCommand implements Command {
private final String name;
private final List<String> aliases = new ArrayList<>();
private final List<ArgumentDef> arguments = new ArrayList<>();
private String permission;
private boolean playerOnly;
private String description = "";
private String usage;
protected AbstractCommand(String name, String... aliases) {
this.name = Objects.requireNonNull(name, "name").toLowerCase();
for (String alias : aliases) {
this.aliases.add(alias.toLowerCase());
}
}
@Override
public final String getName() {
return name;
}
@Override
public final List<String> getAliases() {
return Collections.unmodifiableList(aliases);
}
@Override
public final String getPermission() {
return permission;
}
@Override
public final boolean isPlayerOnly() {
return playerOnly;
}
@Override
public final String getDescription() {
return description;
}
public final String getUsage() {
return usage != null ? usage : buildDefaultUsage();
}
protected final void addAlias(String... aliases) {
for (String alias : aliases) {
this.aliases.add(alias.toLowerCase());
}
}
protected final void permission(String permission) {
this.permission = permission;
}
protected final void playerOnly() {
this.playerOnly = true;
}
protected final void description(String description) {
this.description = Objects.requireNonNullElse(description, "");
}
protected final void usage(String usage) {
this.usage = usage;
}
protected final void argument(String name, ArgumentType<?> type) {
arguments.add(new ArgumentDef(name, type, true));
}
protected final void optionalArgument(String name, ArgumentType<?> type) {
arguments.add(new ArgumentDef(name, type, false));
}
public final int getRequiredArgumentCount() {
return (int) arguments.stream().filter(ArgumentDef::isRequired).count();
}
public final int getTotalArgumentCount() {
return arguments.size();
}
final List<ArgumentDef> getArgumentDefs() {
return arguments;
}
protected String buildDefaultUsage() {
StringBuilder sb = new StringBuilder("/").append(name);
for (ArgumentDef def : arguments) {
sb.append(' ');
sb.append(def.isRequired() ? '<' : '[');
sb.append(def.getName());
sb.append(def.isRequired() ? '>' : ']');
}
return sb.toString();
}
@Override
public List<String> tabComplete(CommandSender sender, int argIndex, String partial) {
if (argIndex >= 0 && argIndex < arguments.size()) {
return arguments.get(argIndex).getType().suggestions(sender, partial);
}
return Collections.emptyList();
}
protected CommandContext buildContext(CommandSender sender, String label, String[] subArgs) {
Map<String, Object> parsed = new LinkedHashMap<>();
int max = Math.min(subArgs.length, arguments.size());
for (int i = 0; i < max; i++) {
ArgumentDef def = arguments.get(i);
try {
Object value = def.getType().parse(subArgs[i]);
parsed.put(def.getName(), value);
} catch (CommandException ex) {
throw new CommandException("Argument '" + def.getName() + "': " + ex.getMessage());
}
}
return new CommandContext(sender, label, subArgs, parsed);
}
}
@@ -0,0 +1,28 @@
package fr.luc.crcore.command;
import java.util.Objects;
final class ArgumentDef {
private final String name;
private final ArgumentType<?> type;
private final boolean required;
ArgumentDef(String name, ArgumentType<?> type, boolean required) {
this.name = Objects.requireNonNull(name, "name");
this.type = Objects.requireNonNull(type, "type");
this.required = required;
}
String getName() {
return name;
}
ArgumentType<?> getType() {
return type;
}
boolean isRequired() {
return required;
}
}
@@ -0,0 +1,15 @@
package fr.luc.crcore.command;
import org.bukkit.command.CommandSender;
import java.util.Collections;
import java.util.List;
public interface ArgumentType<T> {
T parse(String input) throws CommandException;
default List<String> suggestions(CommandSender sender, String partial) {
return Collections.emptyList();
}
}
@@ -0,0 +1,127 @@
package fr.luc.crcore.command;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public final class ArgumentTypes {
private ArgumentTypes() {
}
public static final ArgumentType<String> STRING = new ArgumentType<>() {
@Override
public String parse(String input) {
return input;
}
};
public static final ArgumentType<Integer> INTEGER = new ArgumentType<>() {
@Override
public Integer parse(String input) {
try {
return Integer.parseInt(input);
} catch (NumberFormatException ex) {
throw new CommandException("Invalid integer: " + input);
}
}
};
public static final ArgumentType<Double> DOUBLE = new ArgumentType<>() {
@Override
public Double parse(String input) {
try {
return Double.parseDouble(input);
} catch (NumberFormatException ex) {
throw new CommandException("Invalid number: " + input);
}
}
};
public static final ArgumentType<Boolean> BOOLEAN = new ArgumentType<>() {
@Override
public Boolean parse(String input) {
return switch (input.toLowerCase()) {
case "true", "yes", "y", "1", "on" -> true;
case "false", "no", "n", "0", "off" -> false;
default -> throw new CommandException("Invalid boolean: " + input);
};
}
@Override
public List<String> suggestions(CommandSender sender, String partial) {
return filter(List.of("true", "false"), partial);
}
};
public static final ArgumentType<Player> ONLINE_PLAYER = new ArgumentType<>() {
@Override
public Player parse(String input) {
Player player = Bukkit.getPlayerExact(input);
if (player == null) {
throw new CommandException("Player not found: " + input);
}
return player;
}
@Override
public List<String> suggestions(CommandSender sender, String partial) {
return filter(Bukkit.getOnlinePlayers().stream()
.map(Player::getName)
.collect(Collectors.toList()), partial);
}
};
public static <E extends Enum<E>> ArgumentType<E> enumOf(Class<E> type) {
Objects.requireNonNull(type, "type");
return new ArgumentType<>() {
@Override
public E parse(String input) {
try {
return Enum.valueOf(type, input.toUpperCase());
} catch (IllegalArgumentException ex) {
throw new CommandException(
"Invalid value for " + type.getSimpleName() + ": " + input);
}
}
@Override
public List<String> suggestions(CommandSender sender, String partial) {
return filter(Arrays.stream(type.getEnumConstants())
.map(Enum::name)
.collect(Collectors.toList()), partial);
}
};
}
public static ArgumentType<String> choice(String... choices) {
Objects.requireNonNull(choices, "choices");
List<String> list = List.of(choices);
return new ArgumentType<>() {
@Override
public String parse(String input) {
if (!list.contains(input)) {
throw new CommandException("Invalid choice: " + input + " (expected: " + list + ")");
}
return input;
}
@Override
public List<String> suggestions(CommandSender sender, String partial) {
return filter(list, partial);
}
};
}
private static List<String> filter(List<String> source, String partial) {
String lower = partial.toLowerCase();
return source.stream()
.filter(value -> value.toLowerCase().startsWith(lower))
.collect(Collectors.toList());
}
}
@@ -0,0 +1,188 @@
package fr.luc.crcore.command;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
public abstract class BaseCommand extends AbstractCommand
implements CommandExecutor, TabCompleter {
private final Map<String, SubCommand> subCommandsByName = new LinkedHashMap<>();
private final Map<String, SubCommand> subCommandsByAlias = new HashMap<>();
protected BaseCommand(String name, String... aliases) {
super(name, aliases);
}
protected final void addSubCommand(SubCommand sub) {
Objects.requireNonNull(sub, "sub");
if (subCommandsByName.containsKey(sub.getName())) {
throw new IllegalStateException("Sub-command already registered: " + sub.getName());
}
subCommandsByName.put(sub.getName(), sub);
for (String alias : sub.getAliases()) {
subCommandsByAlias.put(alias, sub);
}
}
public final Collection<SubCommand> getSubCommands() {
return Collections.unmodifiableCollection(subCommandsByName.values());
}
public final Optional<SubCommand> findSubCommand(String label) {
if (label == null) return Optional.empty();
String lc = label.toLowerCase();
SubCommand sub = subCommandsByName.get(lc);
if (sub != null) return Optional.of(sub);
return Optional.ofNullable(subCommandsByAlias.get(lc));
}
/**
* Called when no sub-command matches the first argument. Override for custom
* fallback behavior. Default: lists available sub-commands.
*/
protected CommandResult execute(CommandContext context) {
if (subCommandsByName.isEmpty()) {
return CommandResult.invalidUsage("No action specified.");
}
StringBuilder sb = new StringBuilder(ChatColor.YELLOW + "Available sub-commands:");
for (SubCommand sub : subCommandsByName.values()) {
sb.append('\n').append(ChatColor.GRAY).append(" - ")
.append(ChatColor.WHITE).append(sub.getName());
if (!sub.getDescription().isEmpty()) {
sb.append(ChatColor.GRAY).append("").append(sub.getDescription());
}
}
context.reply(sb.toString());
return CommandResult.success();
}
@Override
public final boolean onCommand(CommandSender sender, org.bukkit.command.Command command,
String label, String[] args) {
if (!checkAccess(sender, this)) {
return true;
}
if (args.length == 0 || findSubCommand(args[0]).isEmpty()) {
CommandContext ctx = new CommandContext(sender, label, args, Collections.emptyMap());
try {
handleResult(sender, execute(ctx));
} catch (CommandException ex) {
sender.sendMessage(ChatColor.RED + ex.getMessage());
}
return true;
}
SubCommand sub = findSubCommand(args[0]).orElseThrow();
if (!checkAccess(sender, sub)) {
return true;
}
String[] subArgs = Arrays.copyOfRange(args, 1, args.length);
if (subArgs.length < sub.getRequiredArgumentCount()) {
sender.sendMessage(ChatColor.RED + "Usage: " + buildUsageFor(label, sub));
return true;
}
CommandContext ctx;
try {
ctx = sub.buildContext(sender, label, subArgs);
} catch (CommandException ex) {
sender.sendMessage(ChatColor.RED + ex.getMessage());
return true;
}
try {
handleResult(sender, sub.execute(ctx));
} catch (CommandException ex) {
sender.sendMessage(ChatColor.RED + ex.getMessage());
}
return true;
}
@Override
public final List<String> onTabComplete(CommandSender sender, org.bukkit.command.Command command,
String alias, String[] args) {
if (args.length == 0) return Collections.emptyList();
if (args.length == 1) {
String partial = args[0].toLowerCase();
List<String> names = new ArrayList<>();
for (SubCommand sub : subCommandsByName.values()) {
if (sub.getPermission() != null && !sender.hasPermission(sub.getPermission())) {
continue;
}
names.add(sub.getName());
names.addAll(sub.getAliases());
}
return names.stream()
.filter(n -> n.startsWith(partial))
.distinct()
.collect(Collectors.toList());
}
Optional<SubCommand> subOpt = findSubCommand(args[0]);
if (subOpt.isEmpty()) return Collections.emptyList();
SubCommand sub = subOpt.get();
if (sub.getPermission() != null && !sender.hasPermission(sub.getPermission())) {
return Collections.emptyList();
}
int argIndex = args.length - 2;
String partial = args[args.length - 1];
return sub.tabComplete(sender, argIndex, partial);
}
protected boolean checkAccess(CommandSender sender, Command target) {
if (target.getPermission() != null && !sender.hasPermission(target.getPermission())) {
sender.sendMessage(ChatColor.RED + "You don't have permission.");
return false;
}
if (target.isPlayerOnly() && !(sender instanceof Player)) {
sender.sendMessage(ChatColor.RED + "Only players can use this command.");
return false;
}
return true;
}
protected String buildUsageFor(String label, SubCommand sub) {
StringBuilder sb = new StringBuilder("/").append(label).append(' ').append(sub.getName());
for (ArgumentDef def : sub.getArgumentDefs()) {
sb.append(' ');
sb.append(def.isRequired() ? '<' : '[');
sb.append(def.getName());
sb.append(def.isRequired() ? '>' : ']');
}
return sb.toString();
}
protected void handleResult(CommandSender sender, CommandResult result) {
switch (result.getType()) {
case SUCCESS -> {
if (result.getMessage() != null) {
sender.sendMessage(ChatColor.GREEN + result.getMessage());
}
}
case FAILURE -> sender.sendMessage(ChatColor.RED +
(result.getMessage() != null ? result.getMessage() : "Command failed."));
case INVALID_USAGE -> sender.sendMessage(ChatColor.RED +
(result.getMessage() != null ? result.getMessage() : "Invalid usage."));
case NO_PERMISSION -> sender.sendMessage(ChatColor.RED + "You don't have permission.");
case PLAYER_ONLY -> sender.sendMessage(ChatColor.RED + "Only players can use this command.");
}
}
}
@@ -0,0 +1,32 @@
package fr.luc.crcore.command;
import org.bukkit.command.CommandSender;
import java.util.List;
public interface Command {
String getName();
List<String> getAliases();
String getPermission();
boolean isPlayerOnly();
String getDescription();
CommandResult execute(CommandContext context);
List<String> tabComplete(CommandSender sender, int argIndex, String partial);
default boolean matches(String label) {
if (label == null) return false;
String lc = label.toLowerCase();
if (getName().equalsIgnoreCase(lc)) return true;
for (String alias : getAliases()) {
if (alias.equalsIgnoreCase(lc)) return true;
}
return false;
}
}
@@ -0,0 +1,76 @@
package fr.luc.crcore.command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
public class CommandContext {
private final CommandSender sender;
private final String label;
private final String[] rawArgs;
private final Map<String, Object> parsedArgs;
public CommandContext(CommandSender sender, String label, String[] rawArgs,
Map<String, Object> parsedArgs) {
this.sender = Objects.requireNonNull(sender, "sender");
this.label = Objects.requireNonNull(label, "label");
this.rawArgs = Objects.requireNonNull(rawArgs, "rawArgs");
this.parsedArgs = Collections.unmodifiableMap(
Objects.requireNonNull(parsedArgs, "parsedArgs"));
}
public CommandSender getSender() {
return sender;
}
public String getLabel() {
return label;
}
public String[] getRawArgs() {
return rawArgs;
}
public boolean isPlayer() {
return sender instanceof Player;
}
public Optional<Player> getPlayer() {
return sender instanceof Player ? Optional.of((Player) sender) : Optional.empty();
}
public Player requirePlayer() {
if (!(sender instanceof Player player)) {
throw new CommandException("This command can only be used by a player.");
}
return player;
}
@SuppressWarnings("unchecked")
public <T> T get(String name) {
Objects.requireNonNull(name, "name");
Object value = parsedArgs.get(name);
if (value == null) {
throw new CommandException("Missing argument: " + name);
}
return (T) value;
}
@SuppressWarnings("unchecked")
public <T> Optional<T> getOptional(String name) {
return Optional.ofNullable((T) parsedArgs.get(name));
}
public boolean has(String name) {
return parsedArgs.containsKey(name);
}
public void reply(String message) {
sender.sendMessage(message);
}
}
@@ -0,0 +1,12 @@
package fr.luc.crcore.command;
public class CommandException extends RuntimeException {
public CommandException(String message) {
super(message);
}
public CommandException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,60 @@
package fr.luc.crcore.command;
public final class CommandResult {
public enum Type {
SUCCESS,
FAILURE,
INVALID_USAGE,
NO_PERMISSION,
PLAYER_ONLY
}
private final Type type;
private final String message;
private CommandResult(Type type, String message) {
this.type = type;
this.message = message;
}
public Type getType() {
return type;
}
public String getMessage() {
return message;
}
public boolean isSuccess() {
return type == Type.SUCCESS;
}
public static CommandResult success() {
return new CommandResult(Type.SUCCESS, null);
}
public static CommandResult success(String message) {
return new CommandResult(Type.SUCCESS, message);
}
public static CommandResult failure(String message) {
return new CommandResult(Type.FAILURE, message);
}
public static CommandResult invalidUsage() {
return new CommandResult(Type.INVALID_USAGE, null);
}
public static CommandResult invalidUsage(String message) {
return new CommandResult(Type.INVALID_USAGE, message);
}
public static CommandResult noPermission() {
return new CommandResult(Type.NO_PERMISSION, null);
}
public static CommandResult playerOnly() {
return new CommandResult(Type.PLAYER_ONLY, null);
}
}
@@ -0,0 +1,8 @@
package fr.luc.crcore.command;
public abstract class SubCommand extends AbstractCommand {
protected SubCommand(String name, String... aliases) {
super(name, aliases);
}
}
@@ -0,0 +1,30 @@
package fr.luc.crcore.common;
import java.util.Objects;
import java.util.UUID;
public abstract class AbstractEntity implements Identifiable {
private final UUID id;
protected AbstractEntity(UUID id) {
this.id = Objects.requireNonNull(id, "id");
}
@Override
public final UUID getId() {
return id;
}
@Override
public final boolean equals(Object other) {
if (this == other) return true;
if (other == null || getClass() != other.getClass()) return false;
return id.equals(((AbstractEntity) other).id);
}
@Override
public final int hashCode() {
return id.hashCode();
}
}
@@ -0,0 +1,8 @@
package fr.luc.crcore.common;
import java.util.UUID;
public interface Identifiable {
UUID getId();
}
@@ -0,0 +1,6 @@
package fr.luc.crcore.common;
public interface Named {
String getName();
}
@@ -0,0 +1,16 @@
package fr.luc.crcore.common;
import java.util.Collection;
import java.util.Optional;
import java.util.UUID;
public interface Repository<T extends Identifiable> {
T save(T entity);
Optional<T> findById(UUID id);
Collection<T> findAll();
boolean delete(UUID id);
}
@@ -0,0 +1,22 @@
package fr.luc.crcore.common;
import java.util.Map;
public interface ScoreHolder {
int getScore(String scoreName);
boolean hasScore(String scoreName);
Map<String, Integer> getScores();
int getTotalScore();
int addScore(String scoreName, int delta);
int setScore(String scoreName, int value);
boolean resetScore(String scoreName);
void resetAllScores();
}
@@ -0,0 +1,38 @@
package fr.luc.crcore.player;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
public class InMemoryPlayerProfileRepository implements PlayerProfileRepository {
private final Map<UUID, PlayerProfile> profiles = new LinkedHashMap<>();
@Override
public PlayerProfile save(PlayerProfile profile) {
Objects.requireNonNull(profile, "profile");
profiles.put(profile.getId(), profile);
return profile;
}
@Override
public Optional<PlayerProfile> findById(UUID id) {
Objects.requireNonNull(id, "id");
return Optional.ofNullable(profiles.get(id));
}
@Override
public Collection<PlayerProfile> findAll() {
return Collections.unmodifiableCollection(profiles.values());
}
@Override
public boolean delete(UUID id) {
Objects.requireNonNull(id, "id");
return profiles.remove(id) != null;
}
}
@@ -0,0 +1,12 @@
package fr.luc.crcore.player;
public class PlayerException extends RuntimeException {
public PlayerException(String message) {
super(message);
}
public PlayerException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,72 @@
package fr.luc.crcore.player;
import fr.luc.crcore.common.AbstractEntity;
import fr.luc.crcore.common.ScoreHolder;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
public class PlayerProfile extends AbstractEntity implements ScoreHolder {
private final Map<String, Integer> scores;
public PlayerProfile(UUID playerId) {
super(playerId);
this.scores = new HashMap<>();
}
public UUID getPlayerId() {
return getId();
}
@Override
public int getScore(String scoreName) {
Objects.requireNonNull(scoreName, "scoreName");
return scores.getOrDefault(scoreName, 0);
}
@Override
public boolean hasScore(String scoreName) {
Objects.requireNonNull(scoreName, "scoreName");
return scores.containsKey(scoreName);
}
@Override
public Map<String, Integer> getScores() {
return Collections.unmodifiableMap(scores);
}
@Override
public int getTotalScore() {
return scores.values().stream().mapToInt(Integer::intValue).sum();
}
@Override
public int addScore(String scoreName, int delta) {
Objects.requireNonNull(scoreName, "scoreName");
int newValue = getScore(scoreName) + delta;
scores.put(scoreName, newValue);
return newValue;
}
@Override
public int setScore(String scoreName, int value) {
Objects.requireNonNull(scoreName, "scoreName");
scores.put(scoreName, value);
return value;
}
@Override
public boolean resetScore(String scoreName) {
Objects.requireNonNull(scoreName, "scoreName");
return scores.remove(scoreName) != null;
}
@Override
public void resetAllScores() {
scores.clear();
}
}
@@ -0,0 +1,8 @@
package fr.luc.crcore.player;
public class PlayerProfileNotFoundException extends PlayerException {
public PlayerProfileNotFoundException(String message) {
super(message);
}
}
@@ -0,0 +1,6 @@
package fr.luc.crcore.player;
import fr.luc.crcore.common.Repository;
public interface PlayerProfileRepository extends Repository<PlayerProfile> {
}
@@ -0,0 +1,44 @@
package fr.luc.crcore.player;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
public interface PlayerProfileService {
PlayerProfile getOrCreateProfile(UUID playerId);
Optional<PlayerProfile> getProfile(UUID playerId);
boolean deleteProfile(UUID playerId);
Collection<PlayerProfile> getAllProfiles();
// ---- Scores ----
int addScore(UUID playerId, String scoreName, int delta);
int setScore(UUID playerId, String scoreName, int value);
int getScore(UUID playerId, String scoreName);
boolean resetScore(UUID playerId, String scoreName);
void resetAllScores(UUID playerId);
// ---- Rankings ----
List<PlayerRanking> getRankingByScore(String scoreName);
List<PlayerRanking> getGlobalRanking();
default List<PlayerRanking> getTopRankingByScore(String scoreName, int limit) {
return getRankingByScore(scoreName).stream().limit(limit).collect(Collectors.toList());
}
default List<PlayerRanking> getTopGlobalRanking(int limit) {
return getGlobalRanking().stream().limit(limit).collect(Collectors.toList());
}
}
@@ -0,0 +1,169 @@
package fr.luc.crcore.player;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.function.ToIntFunction;
public class PlayerProfileServiceImpl implements PlayerProfileService {
private final PlayerProfileRepository repository;
public PlayerProfileServiceImpl(PlayerProfileRepository repository) {
this.repository = Objects.requireNonNull(repository, "repository");
}
protected PlayerProfileRepository getRepository() {
return repository;
}
// ---- Lifecycle ----
@Override
public PlayerProfile getOrCreateProfile(UUID playerId) {
Objects.requireNonNull(playerId, "playerId");
Optional<PlayerProfile> existing = repository.findById(playerId);
if (existing.isPresent()) return existing.get();
PlayerProfile profile = newProfile(playerId);
PlayerProfile saved = repository.save(profile);
onProfileCreated(saved);
return saved;
}
@Override
public Optional<PlayerProfile> getProfile(UUID playerId) {
return repository.findById(playerId);
}
@Override
public boolean deleteProfile(UUID playerId) {
Objects.requireNonNull(playerId, "playerId");
Optional<PlayerProfile> profileOpt = repository.findById(playerId);
if (profileOpt.isEmpty()) return false;
boolean deleted = repository.delete(playerId);
if (deleted) onProfileDeleted(profileOpt.get());
return deleted;
}
@Override
public Collection<PlayerProfile> getAllProfiles() {
return repository.findAll();
}
// ---- Scores ----
@Override
public int addScore(UUID playerId, String scoreName, int delta) {
Objects.requireNonNull(scoreName, "scoreName");
PlayerProfile profile = getOrCreateProfile(playerId);
int oldValue = profile.getScore(scoreName);
int newValue = profile.addScore(scoreName, delta);
repository.save(profile);
if (oldValue != newValue) {
onScoreChanged(profile, scoreName, oldValue, newValue);
}
return newValue;
}
@Override
public int setScore(UUID playerId, String scoreName, int value) {
Objects.requireNonNull(scoreName, "scoreName");
PlayerProfile profile = getOrCreateProfile(playerId);
int oldValue = profile.getScore(scoreName);
profile.setScore(scoreName, value);
repository.save(profile);
if (oldValue != value) {
onScoreChanged(profile, scoreName, oldValue, value);
}
return value;
}
@Override
public int getScore(UUID playerId, String scoreName) {
Objects.requireNonNull(scoreName, "scoreName");
return repository.findById(playerId)
.map(profile -> profile.getScore(scoreName))
.orElse(0);
}
@Override
public boolean resetScore(UUID playerId, String scoreName) {
Objects.requireNonNull(scoreName, "scoreName");
Optional<PlayerProfile> profileOpt = repository.findById(playerId);
if (profileOpt.isEmpty()) return false;
PlayerProfile profile = profileOpt.get();
int oldValue = profile.getScore(scoreName);
boolean removed = profile.resetScore(scoreName);
if (removed) {
repository.save(profile);
onScoreChanged(profile, scoreName, oldValue, 0);
}
return removed;
}
@Override
public void resetAllScores(UUID playerId) {
Optional<PlayerProfile> profileOpt = repository.findById(playerId);
if (profileOpt.isEmpty()) return;
PlayerProfile profile = profileOpt.get();
Map<String, Integer> snapshot = new LinkedHashMap<>(profile.getScores());
if (snapshot.isEmpty()) return;
profile.resetAllScores();
repository.save(profile);
snapshot.forEach((scoreName, oldValue) ->
onScoreChanged(profile, scoreName, oldValue, 0));
}
// ---- Rankings ----
@Override
public List<PlayerRanking> getRankingByScore(String scoreName) {
Objects.requireNonNull(scoreName, "scoreName");
return rank(profile -> profile.getScore(scoreName));
}
@Override
public List<PlayerRanking> getGlobalRanking() {
return rank(PlayerProfile::getTotalScore);
}
protected List<PlayerRanking> rank(ToIntFunction<PlayerProfile> scoreFn) {
Collection<PlayerProfile> all = repository.findAll();
List<PlayerProfile> sorted = new ArrayList<>(all);
sorted.sort(Comparator
.comparingInt(scoreFn).reversed()
.thenComparing(PlayerProfile::getId));
List<PlayerRanking> result = new ArrayList<>(sorted.size());
int currentRank = 1;
for (PlayerProfile profile : sorted) {
result.add(newRanking(currentRank++, profile, scoreFn.applyAsInt(profile)));
}
return result;
}
// ---- Override hooks ----
protected PlayerProfile newProfile(UUID playerId) {
return new PlayerProfile(playerId);
}
protected PlayerRanking newRanking(int rank, PlayerProfile profile, int score) {
return new PlayerRanking(rank, profile, score);
}
protected void onProfileCreated(PlayerProfile profile) {
}
protected void onProfileDeleted(PlayerProfile profile) {
}
protected void onScoreChanged(PlayerProfile profile, String scoreName,
int oldValue, int newValue) {
}
}
@@ -0,0 +1,13 @@
package fr.luc.crcore.player;
import java.util.Objects;
public record PlayerRanking(int rank, PlayerProfile profile, int score) {
public PlayerRanking {
Objects.requireNonNull(profile, "profile");
if (rank < 1) {
throw new IllegalArgumentException("rank must be >= 1, got " + rank);
}
}
}
@@ -0,0 +1,62 @@
package fr.luc.crcore.team;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
public class InMemoryTeamRepository implements TeamRepository {
private final Map<UUID, Team> teams = new LinkedHashMap<>();
@Override
public Team save(Team team) {
Objects.requireNonNull(team, "team");
teams.put(team.getId(), team);
return team;
}
@Override
public Optional<Team> findById(UUID id) {
Objects.requireNonNull(id, "id");
return Optional.ofNullable(teams.get(id));
}
@Override
public Collection<Team> findAll() {
return Collections.unmodifiableCollection(teams.values());
}
@Override
public boolean delete(UUID id) {
Objects.requireNonNull(id, "id");
return teams.remove(id) != null;
}
@Override
public Optional<Team> findByName(String name) {
Objects.requireNonNull(name, "name");
return teams.values().stream()
.filter(team -> team.getName().equalsIgnoreCase(name))
.findFirst();
}
@Override
public Optional<Team> findByTag(String tag) {
Objects.requireNonNull(tag, "tag");
return teams.values().stream()
.filter(team -> team.getTag().equalsIgnoreCase(tag))
.findFirst();
}
@Override
public Optional<Team> findByMember(UUID playerId) {
Objects.requireNonNull(playerId, "playerId");
return teams.values().stream()
.filter(team -> team.hasMember(playerId))
.findFirst();
}
}
+198
View File
@@ -0,0 +1,198 @@
package fr.luc.crcore.team;
import fr.luc.crcore.common.AbstractEntity;
import fr.luc.crcore.common.Named;
import fr.luc.crcore.common.ScoreHolder;
import org.bukkit.Location;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
public class Team extends AbstractEntity implements Named, ScoreHolder {
private final String name;
private final String tag;
private final TeamColor color;
private final Set<TeamMember> members;
private final Map<String, Integer> scores;
private UUID leaderId;
private TeamVisibility visibility;
private Location spawnPoint;
public Team(UUID id, String name, String tag, TeamColor color, UUID leaderId) {
this(id, name, tag, color, leaderId, TeamVisibility.PRIVATE);
}
public Team(UUID id, String name, String tag, TeamColor color, UUID leaderId,
TeamVisibility visibility) {
super(id);
this.name = Objects.requireNonNull(name, "name");
this.tag = Objects.requireNonNull(tag, "tag");
this.color = Objects.requireNonNull(color, "color");
this.leaderId = Objects.requireNonNull(leaderId, "leaderId");
this.visibility = Objects.requireNonNull(visibility, "visibility");
this.members = new HashSet<>();
this.scores = new HashMap<>();
this.members.add(newMember(leaderId, TeamRole.LEADER));
}
/** Override to instantiate a custom TeamMember subclass. */
protected TeamMember newMember(UUID playerId, TeamRole role) {
return new TeamMember(playerId, role);
}
@Override
public String getName() {
return name;
}
public String getTag() {
return tag;
}
public TeamColor getColor() {
return color;
}
public UUID getLeaderId() {
return leaderId;
}
public TeamVisibility getVisibility() {
return visibility;
}
public void setVisibility(TeamVisibility visibility) {
this.visibility = Objects.requireNonNull(visibility, "visibility");
}
public boolean isPublic() {
return visibility.isPublic();
}
public TeamMember getLeader() {
return getMember(leaderId).orElseThrow(
() -> new IllegalStateException("Team has no leader: " + getId()));
}
public Set<TeamMember> getMembers() {
return Collections.unmodifiableSet(members);
}
public Optional<TeamMember> getMember(UUID playerId) {
Objects.requireNonNull(playerId, "playerId");
return members.stream()
.filter(member -> member.getPlayerId().equals(playerId))
.findFirst();
}
public boolean hasMember(UUID playerId) {
return getMember(playerId).isPresent();
}
public int size() {
return members.size();
}
public TeamMember addMember(UUID playerId) {
Objects.requireNonNull(playerId, "playerId");
Optional<TeamMember> existing = getMember(playerId);
if (existing.isPresent()) {
return existing.get();
}
TeamMember member = newMember(playerId, TeamRole.MEMBER);
members.add(member);
return member;
}
public boolean removeMember(UUID playerId) {
Objects.requireNonNull(playerId, "playerId");
if (playerId.equals(leaderId)) {
throw new IllegalStateException(
"Cannot remove the leader; transfer leadership first.");
}
return members.removeIf(member -> member.getPlayerId().equals(playerId));
}
public void transferLeadership(UUID newLeaderId) {
Objects.requireNonNull(newLeaderId, "newLeaderId");
if (newLeaderId.equals(leaderId)) {
return;
}
TeamMember newLeader = getMember(newLeaderId).orElseThrow(
() -> new IllegalArgumentException(
"New leader must already be a member of the team."));
TeamMember oldLeader = getLeader();
members.remove(oldLeader);
members.remove(newLeader);
members.add(oldLeader.withRole(TeamRole.MEMBER));
members.add(newLeader.withRole(TeamRole.LEADER));
this.leaderId = newLeaderId;
}
// ---- Scores ----
public int getScore(String scoreName) {
Objects.requireNonNull(scoreName, "scoreName");
return scores.getOrDefault(scoreName, 0);
}
public boolean hasScore(String scoreName) {
Objects.requireNonNull(scoreName, "scoreName");
return scores.containsKey(scoreName);
}
public Map<String, Integer> getScores() {
return Collections.unmodifiableMap(scores);
}
public int getTotalScore() {
return scores.values().stream().mapToInt(Integer::intValue).sum();
}
public int addScore(String scoreName, int delta) {
Objects.requireNonNull(scoreName, "scoreName");
int newValue = getScore(scoreName) + delta;
scores.put(scoreName, newValue);
return newValue;
}
public int setScore(String scoreName, int value) {
Objects.requireNonNull(scoreName, "scoreName");
scores.put(scoreName, value);
return value;
}
public boolean resetScore(String scoreName) {
Objects.requireNonNull(scoreName, "scoreName");
return scores.remove(scoreName) != null;
}
public void resetAllScores() {
scores.clear();
}
// ---- Spawn point ----
public Optional<Location> getSpawnPoint() {
return spawnPoint == null ? Optional.empty() : Optional.of(spawnPoint.clone());
}
public boolean hasSpawnPoint() {
return spawnPoint != null;
}
public void setSpawnPoint(Location location) {
this.spawnPoint = location == null ? null : location.clone();
}
public void clearSpawnPoint() {
this.spawnPoint = null;
}
}
@@ -0,0 +1,8 @@
package fr.luc.crcore.team;
public class TeamAccessException extends TeamException {
public TeamAccessException(String message) {
super(message);
}
}
@@ -0,0 +1,8 @@
package fr.luc.crcore.team;
public class TeamAlreadyExistsException extends TeamException {
public TeamAlreadyExistsException(String message) {
super(message);
}
}
@@ -0,0 +1,46 @@
package fr.luc.crcore.team;
import org.bukkit.ChatColor;
import org.bukkit.DyeColor;
public enum TeamColor {
RED(ChatColor.RED, DyeColor.RED, "Red"),
BLUE(ChatColor.BLUE, DyeColor.BLUE, "Blue"),
GREEN(ChatColor.GREEN, DyeColor.LIME, "Green"),
YELLOW(ChatColor.YELLOW, DyeColor.YELLOW, "Yellow"),
AQUA(ChatColor.AQUA, DyeColor.LIGHT_BLUE, "Aqua"),
LIGHT_PURPLE(ChatColor.LIGHT_PURPLE, DyeColor.MAGENTA, "Pink"),
GOLD(ChatColor.GOLD, DyeColor.ORANGE, "Gold"),
WHITE(ChatColor.WHITE, DyeColor.WHITE, "White"),
BLACK(ChatColor.BLACK, DyeColor.BLACK, "Black"),
DARK_BLUE(ChatColor.DARK_BLUE, DyeColor.BLUE, "Dark Blue"),
DARK_GREEN(ChatColor.DARK_GREEN, DyeColor.GREEN, "Dark Green"),
DARK_AQUA(ChatColor.DARK_AQUA, DyeColor.CYAN, "Dark Aqua"),
DARK_RED(ChatColor.DARK_RED, DyeColor.RED, "Dark Red"),
DARK_PURPLE(ChatColor.DARK_PURPLE, DyeColor.PURPLE, "Purple"),
DARK_GRAY(ChatColor.DARK_GRAY, DyeColor.GRAY, "Dark Gray"),
GRAY(ChatColor.GRAY, DyeColor.LIGHT_GRAY, "Gray");
private final ChatColor chatColor;
private final DyeColor dyeColor;
private final String displayName;
TeamColor(ChatColor chatColor, DyeColor dyeColor, String displayName) {
this.chatColor = chatColor;
this.dyeColor = dyeColor;
this.displayName = displayName;
}
public ChatColor getChatColor() {
return chatColor;
}
public DyeColor getDyeColor() {
return dyeColor;
}
public String getDisplayName() {
return displayName;
}
}
@@ -0,0 +1,12 @@
package fr.luc.crcore.team;
public class TeamException extends RuntimeException {
public TeamException(String message) {
super(message);
}
public TeamException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,47 @@
package fr.luc.crcore.team;
import fr.luc.crcore.common.AbstractEntity;
import java.time.Instant;
import java.util.Objects;
import java.util.UUID;
public class TeamMember extends AbstractEntity {
private final TeamRole role;
private final Instant joinedAt;
public TeamMember(UUID playerId, TeamRole role) {
this(playerId, role, Instant.now());
}
public TeamMember(UUID playerId, TeamRole role, Instant joinedAt) {
super(playerId);
this.role = Objects.requireNonNull(role, "role");
this.joinedAt = Objects.requireNonNull(joinedAt, "joinedAt");
}
public UUID getPlayerId() {
return getId();
}
public TeamRole getRole() {
return role;
}
public Instant getJoinedAt() {
return joinedAt;
}
public boolean isLeader() {
return role.isLeader();
}
public TeamMember withRole(TeamRole newRole) {
Objects.requireNonNull(newRole, "newRole");
if (newRole == this.role) {
return this;
}
return new TeamMember(getPlayerId(), newRole, joinedAt);
}
}
@@ -0,0 +1,8 @@
package fr.luc.crcore.team;
public class TeamNotFoundException extends TeamException {
public TeamNotFoundException(String message) {
super(message);
}
}
@@ -0,0 +1,13 @@
package fr.luc.crcore.team;
import java.util.Objects;
public record TeamRanking(int rank, Team team, int score) {
public TeamRanking {
Objects.requireNonNull(team, "team");
if (rank < 1) {
throw new IllegalArgumentException("rank must be >= 1, got " + rank);
}
}
}
@@ -0,0 +1,15 @@
package fr.luc.crcore.team;
import fr.luc.crcore.common.Repository;
import java.util.Optional;
import java.util.UUID;
public interface TeamRepository extends Repository<Team> {
Optional<Team> findByName(String name);
Optional<Team> findByTag(String tag);
Optional<Team> findByMember(UUID playerId);
}
@@ -0,0 +1,11 @@
package fr.luc.crcore.team;
public enum TeamRole {
LEADER,
MEMBER;
public boolean isLeader() {
return this == LEADER;
}
}
@@ -0,0 +1,79 @@
package fr.luc.crcore.team;
import org.bukkit.Location;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
public interface TeamService {
// ---- Lifecycle ----
Team createTeam(String name, String tag, TeamColor color, UUID leaderId);
Team createTeam(String name, String tag, TeamColor color, UUID leaderId,
TeamVisibility visibility);
boolean dissolveTeam(UUID teamId);
// ---- Membership ----
boolean addMember(UUID teamId, UUID playerId);
boolean removeMember(UUID teamId, UUID playerId);
boolean joinTeam(UUID teamId, UUID playerId);
boolean transferLeadership(UUID teamId, UUID newLeaderId);
void setVisibility(UUID teamId, TeamVisibility visibility);
// ---- Scores ----
int addScore(UUID teamId, String scoreName, int delta);
int setScore(UUID teamId, String scoreName, int value);
int getScore(UUID teamId, String scoreName);
boolean resetScore(UUID teamId, String scoreName);
void resetAllScores(UUID teamId);
// ---- Rankings ----
List<TeamRanking> getRankingByScore(String scoreName);
List<TeamRanking> getGlobalRanking();
default List<TeamRanking> getTopRankingByScore(String scoreName, int limit) {
return getRankingByScore(scoreName).stream().limit(limit).collect(Collectors.toList());
}
default List<TeamRanking> getTopGlobalRanking(int limit) {
return getGlobalRanking().stream().limit(limit).collect(Collectors.toList());
}
// ---- Spawn point ----
void setSpawnPoint(UUID teamId, Location location);
void clearSpawnPoint(UUID teamId);
Optional<Location> getSpawnPoint(UUID teamId);
// ---- Queries ----
Optional<Team> getTeam(UUID teamId);
Optional<Team> getTeamByName(String name);
Optional<Team> getTeamByTag(String tag);
Optional<Team> getTeamOfPlayer(UUID playerId);
Collection<Team> getAllTeams();
}
@@ -0,0 +1,337 @@
package fr.luc.crcore.team;
import org.bukkit.Location;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.function.ToIntFunction;
public class TeamServiceImpl implements TeamService {
private final TeamRepository repository;
public TeamServiceImpl(TeamRepository repository) {
this.repository = Objects.requireNonNull(repository, "repository");
}
protected TeamRepository getRepository() {
return repository;
}
// ---- Lifecycle ----
@Override
public Team createTeam(String name, String tag, TeamColor color, UUID leaderId) {
return createTeam(name, tag, color, leaderId, TeamVisibility.PRIVATE);
}
@Override
public Team createTeam(String name, String tag, TeamColor color, UUID leaderId,
TeamVisibility visibility) {
validateName(name);
validateTag(tag);
validateLeader(leaderId);
Objects.requireNonNull(visibility, "visibility");
Team team = newTeam(UUID.randomUUID(), name, tag, color, leaderId, visibility);
onBeforeSave(team);
Team saved = repository.save(team);
onAfterCreate(saved);
return saved;
}
@Override
public boolean dissolveTeam(UUID teamId) {
Objects.requireNonNull(teamId, "teamId");
Optional<Team> teamOpt = repository.findById(teamId);
if (teamOpt.isEmpty()) return false;
Team team = teamOpt.get();
onBeforeDissolve(team);
boolean deleted = repository.delete(teamId);
if (deleted) onAfterDissolve(team);
return deleted;
}
// ---- Membership ----
@Override
public boolean addMember(UUID teamId, UUID playerId) {
Objects.requireNonNull(playerId, "playerId");
Team team = requireTeam(teamId);
if (repository.findByMember(playerId).isPresent()) {
return false;
}
TeamMember member = team.addMember(playerId);
repository.save(team);
onMemberAdded(team, member);
return true;
}
@Override
public boolean removeMember(UUID teamId, UUID playerId) {
Objects.requireNonNull(playerId, "playerId");
Team team = requireTeam(teamId);
boolean removed = team.removeMember(playerId);
if (removed) {
repository.save(team);
onMemberRemoved(team, playerId);
}
return removed;
}
@Override
public boolean joinTeam(UUID teamId, UUID playerId) {
Objects.requireNonNull(playerId, "playerId");
Team team = requireTeam(teamId);
validateJoinable(team, playerId);
TeamMember member = team.addMember(playerId);
repository.save(team);
onMemberAdded(team, member);
onPlayerJoined(team, member);
return true;
}
@Override
public boolean transferLeadership(UUID teamId, UUID newLeaderId) {
Objects.requireNonNull(newLeaderId, "newLeaderId");
Team team = requireTeam(teamId);
UUID oldLeaderId = team.getLeaderId();
team.transferLeadership(newLeaderId);
repository.save(team);
onLeadershipTransferred(team, oldLeaderId, newLeaderId);
return true;
}
@Override
public void setVisibility(UUID teamId, TeamVisibility visibility) {
Objects.requireNonNull(visibility, "visibility");
Team team = requireTeam(teamId);
TeamVisibility old = team.getVisibility();
if (old == visibility) return;
team.setVisibility(visibility);
repository.save(team);
onVisibilityChanged(team, old, visibility);
}
// ---- Scores ----
@Override
public int addScore(UUID teamId, String scoreName, int delta) {
Objects.requireNonNull(scoreName, "scoreName");
Team team = requireTeam(teamId);
int oldValue = team.getScore(scoreName);
int newValue = team.addScore(scoreName, delta);
repository.save(team);
if (oldValue != newValue) {
onScoreChanged(team, scoreName, oldValue, newValue);
}
return newValue;
}
@Override
public int setScore(UUID teamId, String scoreName, int value) {
Objects.requireNonNull(scoreName, "scoreName");
Team team = requireTeam(teamId);
int oldValue = team.getScore(scoreName);
team.setScore(scoreName, value);
repository.save(team);
if (oldValue != value) {
onScoreChanged(team, scoreName, oldValue, value);
}
return value;
}
@Override
public int getScore(UUID teamId, String scoreName) {
return requireTeam(teamId).getScore(scoreName);
}
@Override
public boolean resetScore(UUID teamId, String scoreName) {
Objects.requireNonNull(scoreName, "scoreName");
Team team = requireTeam(teamId);
int oldValue = team.getScore(scoreName);
boolean removed = team.resetScore(scoreName);
if (removed) {
repository.save(team);
onScoreChanged(team, scoreName, oldValue, 0);
}
return removed;
}
@Override
public void resetAllScores(UUID teamId) {
Team team = requireTeam(teamId);
Map<String, Integer> snapshot = new LinkedHashMap<>(team.getScores());
if (snapshot.isEmpty()) return;
team.resetAllScores();
repository.save(team);
snapshot.forEach((scoreName, oldValue) ->
onScoreChanged(team, scoreName, oldValue, 0));
}
// ---- Rankings ----
@Override
public List<TeamRanking> getRankingByScore(String scoreName) {
Objects.requireNonNull(scoreName, "scoreName");
return rank(team -> team.getScore(scoreName));
}
@Override
public List<TeamRanking> getGlobalRanking() {
return rank(Team::getTotalScore);
}
protected List<TeamRanking> rank(ToIntFunction<Team> scoreFn) {
Collection<Team> all = repository.findAll();
List<Team> sorted = new ArrayList<>(all);
sorted.sort(Comparator
.comparingInt(scoreFn).reversed()
.thenComparing(Team::getName, String.CASE_INSENSITIVE_ORDER));
List<TeamRanking> result = new ArrayList<>(sorted.size());
int currentRank = 1;
for (Team team : sorted) {
result.add(newRanking(currentRank++, team, scoreFn.applyAsInt(team)));
}
return result;
}
protected TeamRanking newRanking(int rank, Team team, int score) {
return new TeamRanking(rank, team, score);
}
// ---- Spawn point ----
@Override
public void setSpawnPoint(UUID teamId, Location location) {
Team team = requireTeam(teamId);
Location old = team.getSpawnPoint().orElse(null);
team.setSpawnPoint(location);
repository.save(team);
onSpawnPointChanged(team, old, location);
}
@Override
public void clearSpawnPoint(UUID teamId) {
setSpawnPoint(teamId, null);
}
@Override
public Optional<Location> getSpawnPoint(UUID teamId) {
return requireTeam(teamId).getSpawnPoint();
}
// ---- Queries ----
@Override
public Optional<Team> getTeam(UUID teamId) {
return repository.findById(teamId);
}
@Override
public Optional<Team> getTeamByName(String name) {
return repository.findByName(name);
}
@Override
public Optional<Team> getTeamByTag(String tag) {
return repository.findByTag(tag);
}
@Override
public Optional<Team> getTeamOfPlayer(UUID playerId) {
return repository.findByMember(playerId);
}
@Override
public Collection<Team> getAllTeams() {
return repository.findAll();
}
// ---- Override hooks ----
protected Team newTeam(UUID id, String name, String tag, TeamColor color, UUID leaderId,
TeamVisibility visibility) {
return new Team(id, name, tag, color, leaderId, visibility);
}
protected void validateName(String name) {
Objects.requireNonNull(name, "name");
repository.findByName(name).ifPresent(existing -> {
throw new TeamAlreadyExistsException("Team name already in use: " + name);
});
}
protected void validateTag(String tag) {
Objects.requireNonNull(tag, "tag");
repository.findByTag(tag).ifPresent(existing -> {
throw new TeamAlreadyExistsException("Team tag already in use: " + tag);
});
}
protected void validateLeader(UUID leaderId) {
Objects.requireNonNull(leaderId, "leaderId");
repository.findByMember(leaderId).ifPresent(existing -> {
throw new TeamAlreadyExistsException(
"Player already belongs to team: " + existing.getName());
});
}
protected void validateJoinable(Team team, UUID playerId) {
if (!team.isPublic()) {
throw new TeamAccessException(
"Team " + team.getName() + " is private; ask the leader for an invite.");
}
repository.findByMember(playerId).ifPresent(existing -> {
throw new TeamAccessException(
"You already belong to team: " + existing.getName());
});
}
protected void onBeforeSave(Team team) {
}
protected void onAfterCreate(Team team) {
}
protected void onBeforeDissolve(Team team) {
}
protected void onAfterDissolve(Team team) {
}
protected void onMemberAdded(Team team, TeamMember member) {
}
protected void onMemberRemoved(Team team, UUID playerId) {
}
protected void onPlayerJoined(Team team, TeamMember member) {
}
protected void onLeadershipTransferred(Team team, UUID oldLeaderId, UUID newLeaderId) {
}
protected void onVisibilityChanged(Team team, TeamVisibility oldValue, TeamVisibility newValue) {
}
protected void onScoreChanged(Team team, String scoreName, int oldValue, int newValue) {
}
protected void onSpawnPointChanged(Team team, Location oldLocation, Location newLocation) {
}
protected Team requireTeam(UUID teamId) {
Objects.requireNonNull(teamId, "teamId");
return repository.findById(teamId).orElseThrow(
() -> new TeamNotFoundException("No team with id: " + teamId));
}
}
@@ -0,0 +1,15 @@
package fr.luc.crcore.team;
public enum TeamVisibility {
PUBLIC,
PRIVATE;
public boolean isPublic() {
return this == PUBLIC;
}
public boolean isPrivate() {
return this == PRIVATE;
}
}