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
+132
View File
@@ -0,0 +1,132 @@
@startuml command-class-diagram
title CR-Core — Command framework (class diagram)
skinparam classAttributeIconSize 0
hide empty members
package "fr.luc.crcore.command" {
interface Command {
+ getName(): String
+ getAliases(): List<String>
+ getPermission(): String
+ isPlayerOnly(): boolean
+ getDescription(): String
+ execute(ctx: CommandContext): CommandResult
+ tabComplete(sender, argIndex, partial): List<String>
+ matches(label: String): boolean
}
abstract class AbstractCommand {
- name: String
- aliases: List<String>
- permission: String
- playerOnly: boolean
- description: String
- usage: String
- arguments: List<ArgumentDef>
# addAlias(...): void
# permission(p): void
# playerOnly(): void
# description(d): void
# usage(u): void
# argument(name, type): void
# optionalArgument(name, type): void
# buildContext(sender, label, subArgs): CommandContext
+ getRequiredArgumentCount(): int
+ getTotalArgumentCount(): int
+ getUsage(): String
}
abstract class BaseCommand {
- subCommandsByName: Map<String, SubCommand>
- subCommandsByAlias: Map<String, SubCommand>
# addSubCommand(sub: SubCommand): void
+ findSubCommand(label: String): Optional<SubCommand>
+ getSubCommands(): Collection<SubCommand>
# execute(ctx): CommandResult
+ onCommand(sender, cmd, label, args): boolean
+ onTabComplete(sender, cmd, alias, args): List<String>
# checkAccess(sender, target): boolean
# handleResult(sender, result): void
}
abstract class SubCommand {
+ {abstract} execute(ctx: CommandContext): CommandResult
}
class CommandContext {
- sender: CommandSender
- label: String
- rawArgs: String[]
- parsedArgs: Map<String, Object>
+ getSender(): CommandSender
+ isPlayer(): boolean
+ getPlayer(): Optional<Player>
+ requirePlayer(): Player
+ get(name: String): T
+ getOptional(name): Optional<T>
+ has(name): boolean
+ reply(msg): void
}
class CommandResult {
- type: Type
- message: String
+ getType(): Type
+ getMessage(): String
+ isSuccess(): boolean
+ {static} success(): CommandResult
+ {static} success(msg): CommandResult
+ {static} failure(msg): CommandResult
+ {static} invalidUsage(): CommandResult
+ {static} noPermission(): CommandResult
+ {static} playerOnly(): CommandResult
}
enum "CommandResult.Type" as ResultType {
SUCCESS
FAILURE
INVALID_USAGE
NO_PERMISSION
PLAYER_ONLY
}
class CommandException
interface "ArgumentType<T>" as ArgumentType {
+ parse(input: String): T
+ suggestions(sender, partial): List<String>
}
class ArgumentTypes << (S, #FFC107) static >> {
+ {static} STRING: ArgumentType<String>
+ {static} INTEGER: ArgumentType<Integer>
+ {static} DOUBLE: ArgumentType<Double>
+ {static} BOOLEAN: ArgumentType<Boolean>
+ {static} ONLINE_PLAYER: ArgumentType<Player>
+ {static} enumOf(type): ArgumentType<E>
+ {static} choice(choices): ArgumentType<String>
}
class ArgumentDef << (P, #BBBBBB) package-private >> {
- name: String
- type: ArgumentType<?>
- required: boolean
}
AbstractCommand ..|> Command
BaseCommand --|> AbstractCommand
SubCommand --|> AbstractCommand
BaseCommand "1" o-- "*" SubCommand : subCommands
AbstractCommand "1" *-- "*" ArgumentDef : arguments
ArgumentDef --> ArgumentType
CommandResult +-- ResultType
CommandException --|> RuntimeException
BaseCommand ..> CommandContext : creates
AbstractCommand ..> CommandContext : creates
SubCommand ..> CommandResult : returns
}
@enduml
+110
View File
@@ -0,0 +1,110 @@
@startuml player-class-diagram
title CR-Core — Player domain (class diagram)
skinparam classAttributeIconSize 0
hide empty members
' === Common abstractions ===
package "fr.luc.crcore.common" {
interface Identifiable {
+ getId(): UUID
}
interface ScoreHolder {
+ getScore(name): int
+ hasScore(name): boolean
+ getScores(): Map<String, Integer>
+ getTotalScore(): int
+ addScore(name, delta): int
+ setScore(name, value): int
+ resetScore(name): boolean
+ resetAllScores(): void
}
abstract class AbstractEntity {
- id: UUID
+ getId(): UUID
}
interface "Repository<T extends Identifiable>" as Repository {
+ save(entity: T): T
+ findById(id): Optional<T>
+ findAll(): Collection<T>
+ delete(id): boolean
}
AbstractEntity ..|> Identifiable
}
' === Player domain ===
package "fr.luc.crcore.player" {
class PlayerProfile {
- scores: Map<String, Integer>
+ PlayerProfile(playerId: UUID)
+ getPlayerId(): UUID
}
class PlayerRanking <<record>> {
+ rank: int
+ profile: PlayerProfile
+ score: int
}
interface PlayerProfileRepository
class InMemoryPlayerProfileRepository {
- profiles: Map<UUID, PlayerProfile>
}
interface PlayerProfileService {
+ getOrCreateProfile(playerId): PlayerProfile
+ getProfile(playerId): Optional<PlayerProfile>
+ deleteProfile(playerId): boolean
+ getAllProfiles(): Collection<PlayerProfile>
--
+ addScore(playerId, name, delta): int
+ setScore(playerId, name, value): int
+ getScore(playerId, name): int
+ resetScore(playerId, name): boolean
+ resetAllScores(playerId): void
--
+ getRankingByScore(name): List<PlayerRanking>
+ getGlobalRanking(): List<PlayerRanking>
+ getTopRankingByScore(name, limit): List<PlayerRanking>
+ getTopGlobalRanking(limit): List<PlayerRanking>
}
class PlayerProfileServiceImpl {
- repository: PlayerProfileRepository
--
# newProfile(playerId): PlayerProfile
# newRanking(rank, profile, score): PlayerRanking
# rank(scoreFn): List<PlayerRanking>
--
# onProfileCreated(profile): void
# onProfileDeleted(profile): void
# onScoreChanged(profile, name, oldV, newV): void
}
class PlayerException
class PlayerProfileNotFoundException
PlayerProfile --|> AbstractEntity
PlayerProfile ..|> ScoreHolder
PlayerProfileRepository --|> Repository
InMemoryPlayerProfileRepository ..|> PlayerProfileRepository
PlayerProfileServiceImpl ..|> PlayerProfileService
PlayerRanking --> PlayerProfile
PlayerProfileServiceImpl o--> PlayerProfileRepository
PlayerProfileService ..> PlayerRanking : produces
PlayerException --|> RuntimeException
PlayerProfileNotFoundException --|> PlayerException
}
@enduml
+239
View File
@@ -0,0 +1,239 @@
@startuml team-class-diagram
title CR-Core — Team domain (class diagram)
skinparam classAttributeIconSize 0
hide empty members
' === Common abstractions ===
package "fr.luc.crcore.common" {
interface Identifiable {
+ getId(): UUID
}
interface Named {
+ getName(): String
}
interface ScoreHolder {
+ getScore(name): int
+ hasScore(name): boolean
+ getScores(): Map<String, Integer>
+ getTotalScore(): int
+ addScore(name, delta): int
+ setScore(name, value): int
+ resetScore(name): boolean
+ resetAllScores(): void
}
abstract class AbstractEntity {
- id: UUID
+ AbstractEntity(id: UUID)
+ getId(): UUID
+ equals(o: Object): boolean
+ hashCode(): int
}
interface "Repository<T extends Identifiable>" as Repository {
+ save(entity: T): T
+ findById(id: UUID): Optional<T>
+ findAll(): Collection<T>
+ delete(id: UUID): boolean
}
AbstractEntity ..|> Identifiable
}
' === Team domain ===
package "fr.luc.crcore.team" {
enum TeamRole {
LEADER
MEMBER
--
+ isLeader(): boolean
}
enum TeamVisibility {
PUBLIC
PRIVATE
--
+ isPublic(): boolean
+ isPrivate(): boolean
}
enum TeamColor {
RED
BLUE
GREEN
YELLOW
AQUA
LIGHT_PURPLE
GOLD
WHITE
BLACK
DARK_BLUE
DARK_GREEN
DARK_AQUA
DARK_RED
DARK_PURPLE
DARK_GRAY
GRAY
--
+ getChatColor(): ChatColor
+ getDyeColor(): DyeColor
+ getDisplayName(): String
}
class TeamMember {
- role: TeamRole
- joinedAt: Instant
+ getPlayerId(): UUID
+ getRole(): TeamRole
+ getJoinedAt(): Instant
+ isLeader(): boolean
+ withRole(role: TeamRole): TeamMember
}
class Team {
- name: String
- tag: String
- color: TeamColor
- leaderId: UUID
- visibility: TeamVisibility
- members: Set<TeamMember>
- scores: Map<String, Integer>
- spawnPoint: Location
--
+ getName(): String
+ getTag(): String
+ getColor(): TeamColor
+ getLeaderId(): UUID
+ getLeader(): TeamMember
+ getVisibility(): TeamVisibility
+ setVisibility(v): void
+ isPublic(): boolean
+ getMembers(): Set<TeamMember>
+ getMember(playerId): Optional<TeamMember>
+ hasMember(playerId): boolean
+ size(): int
+ addMember(playerId): TeamMember
+ removeMember(playerId): boolean
+ transferLeadership(newLeaderId): void
--
+ getScore(name): int
+ hasScore(name): boolean
+ getScores(): Map<String, Integer>
+ getTotalScore(): int
+ addScore(name, delta): int
+ setScore(name, value): int
+ resetScore(name): boolean
+ resetAllScores(): void
--
+ getSpawnPoint(): Optional<Location>
+ hasSpawnPoint(): boolean
+ setSpawnPoint(loc): void
+ clearSpawnPoint(): void
--
# newMember(playerId, role): TeamMember
}
class TeamRanking <<record>> {
+ rank: int
+ team: Team
+ score: int
}
interface TeamRepository {
+ findByName(name: String): Optional<Team>
+ findByTag(tag: String): Optional<Team>
+ findByMember(playerId: UUID): Optional<Team>
}
class InMemoryTeamRepository {
- teams: Map<UUID, Team>
}
interface TeamService {
+ createTeam(name, tag, color, leaderId, [visibility]): Team
+ dissolveTeam(teamId): boolean
+ addMember(teamId, playerId): boolean
+ removeMember(teamId, playerId): boolean
+ joinTeam(teamId, playerId): boolean
+ transferLeadership(teamId, newLeaderId): boolean
+ setVisibility(teamId, visibility): void
--
+ addScore(teamId, name, delta): int
+ setScore(teamId, name, value): int
+ getScore(teamId, name): int
+ resetScore(teamId, name): boolean
+ resetAllScores(teamId): void
--
+ getRankingByScore(name): List<TeamRanking>
+ getGlobalRanking(): List<TeamRanking>
+ getTopRankingByScore(name, limit): List<TeamRanking>
+ getTopGlobalRanking(limit): List<TeamRanking>
--
+ setSpawnPoint(teamId, loc): void
+ clearSpawnPoint(teamId): void
+ getSpawnPoint(teamId): Optional<Location>
--
+ getTeam / getTeamByName / getTeamByTag / getTeamOfPlayer
+ getAllTeams(): Collection<Team>
}
class TeamServiceImpl {
- repository: TeamRepository
--
# newTeam(...): Team
# newRanking(rank, team, score): TeamRanking
# rank(scoreFn): List<TeamRanking>
--
# validateName(name): void
# validateTag(tag): void
# validateLeader(leaderId): void
# validateJoinable(team, playerId): void
--
# onBeforeSave(team): void
# onAfterCreate(team): void
# onBeforeDissolve(team): void
# onAfterDissolve(team): void
# onMemberAdded(team, member): void
# onMemberRemoved(team, playerId): void
# onPlayerJoined(team, member): void
# onLeadershipTransferred(team, oldId, newId): void
# onVisibilityChanged(team, oldV, newV): void
# onScoreChanged(team, name, oldV, newV): void
# onSpawnPointChanged(team, oldLoc, newLoc): void
}
class TeamException
class TeamAlreadyExistsException
class TeamNotFoundException
class TeamAccessException
TeamMember --|> AbstractEntity
Team --|> AbstractEntity
Team ..|> Named
Team ..|> ScoreHolder
TeamRepository --|> Repository
InMemoryTeamRepository ..|> TeamRepository
TeamServiceImpl ..|> TeamService
Team "1" o-- "1..*" TeamMember : members
Team --> TeamColor : color
Team --> TeamVisibility : visibility
TeamMember --> TeamRole : role
TeamRanking --> Team
TeamServiceImpl o--> TeamRepository
TeamService ..> TeamRanking : produces
TeamException --|> RuntimeException
TeamAlreadyExistsException --|> TeamException
TeamNotFoundException --|> TeamException
TeamAccessException --|> TeamException
}
@enduml
+40
View File
@@ -0,0 +1,40 @@
@startuml team-create-activity
title CR-Core — Create Team (activity diagram)
start
:Player runs /team create <name> <tag> <color> [visibility];
if (Name already in use?) then (yes)
:Reply "team name already taken";
stop
else (no)
endif
if (Tag already in use?) then (yes)
:Reply "team tag already taken";
stop
else (no)
endif
if (Player already in a team?) then (yes)
:Reply "you already belong to a team";
stop
else (no)
endif
if (Color valid?) then (no)
:Reply "unknown color";
stop
else (yes)
endif
:Create Team(id = randomUUID, name, tag, color, leaderId = playerId, visibility);
note right: visibility defaults to PRIVATE\nif not specified
:Add player as TeamMember(role = LEADER);
:Persist via TeamRepository.save(team);
:Reply "team created";
stop
@enduml
+69
View File
@@ -0,0 +1,69 @@
@startuml team-create-sequence
title CR-Core — Create Team via command framework (sequence diagram)
actor Player
participant "BaseCommand\n(TeamCommand)" as Base
participant "SubCommand\n(TeamCreateSub)" as Sub
participant "TeamService" as Service
participant "TeamRepository" as Repo
participant "Team" as Team
Player -> Base : /team create <name> <tag> <color>
activate Base
Base -> Base : route args[0]="create" → TeamCreateSub
Base -> Base : check permission + playerOnly
Base -> Sub : buildContext(sender, label, subArgs)
activate Sub
Sub -> Sub : parse name (STRING) / tag (STRING) / color (enumOf TeamColor)
Sub --> Base : CommandContext
deactivate Sub
Base -> Sub : execute(ctx)
activate Sub
Sub -> Service : createTeam(name, tag, color, playerId)
activate Service
Service -> Service : validateName(name)
Service -> Repo : findByName(name)
activate Repo
Repo --> Service : Optional.empty()
deactivate Repo
Service -> Service : validateTag(tag)
Service -> Repo : findByTag(tag)
activate Repo
Repo --> Service : Optional.empty()
deactivate Repo
Service -> Service : validateLeader(playerId)
Service -> Repo : findByMember(playerId)
activate Repo
Repo --> Service : Optional.empty()
deactivate Repo
Service -> Team : newTeam(UUID.randomUUID(), name, tag, color, playerId, PRIVATE)
activate Team
Team -> Team : add newMember(playerId, LEADER)
Team --> Service : team
deactivate Team
Service -> Service : onBeforeSave(team)
Service -> Repo : save(team)
activate Repo
Repo --> Service : team
deactivate Repo
Service -> Service : onAfterCreate(team)
Service --> Sub : team
deactivate Service
Sub --> Base : CommandResult.success("Team created")
deactivate Sub
Base -> Base : handleResult → sender.sendMessage(green text)
Base --> Player : "Team <name> created"
deactivate Base
@enduml
+62
View File
@@ -0,0 +1,62 @@
@startuml team-join-sequence
title CR-Core — Player joins a public team (sequence diagram)
actor Player
participant "BaseCommand\n(TeamCommand)" as Base
participant "SubCommand\n(TeamJoinSub)" as Sub
participant "TeamService" as Service
participant "TeamRepository" as Repo
participant "Team" as Team
Player -> Base : /team join <name>
activate Base
Base -> Base : route args[0]="join" → TeamJoinSub
Base -> Sub : execute(ctx)
activate Sub
Sub -> Service : getTeamByName(name)
activate Service
Service -> Repo : findByName(name)
activate Repo
Repo --> Service : Optional<Team>
deactivate Repo
Service --> Sub : Optional<Team>
deactivate Service
alt team not found
Sub --> Base : CommandResult.failure("Team not found")
else team found
Sub -> Service : joinTeam(team.id, playerId)
activate Service
Service -> Service : validateJoinable(team, playerId)
alt team is PRIVATE
Service --> Sub : throw TeamAccessException
Sub --> Base : CommandResult.failure(ex.message)
else player already in a team
Service --> Sub : throw TeamAccessException
Sub --> Base : CommandResult.failure(ex.message)
else allowed
Service -> Team : addMember(playerId)
activate Team
Team --> Service : member
deactivate Team
Service -> Repo : save(team)
activate Repo
Repo --> Service : team
deactivate Repo
Service -> Service : onMemberAdded(team, member)
Service -> Service : onPlayerJoined(team, member)
Service --> Sub : true
Sub --> Base : CommandResult.success("Joined " + team.name)
end
deactivate Service
end
deactivate Sub
Base -> Base : handleResult → sender.sendMessage
Base --> Player : reply
deactivate Base
@enduml