docs: organize diagrams to mirror code layout (util/ + features/)

Move flat docs/diagrams/*.puml into a hierarchy matching the source
package structure:

  docs/diagrams/
  ├── bootstrap-sequence.puml         (cross-cutting)
  ├── events-diagram.puml             (cross-feature)
  ├── util/
  │   ├── command-class-diagram.puml
  │   ├── database-diagram.puml
  │   ├── messages-class-diagram.puml
  │   ├── broadcasts-class-diagram.puml
  │   └── gui-class-diagram.puml
  └── features/
      ├── team/
      │   ├── team-class-diagram.puml
      │   ├── team-config-class-diagram.puml
      │   ├── builtin-commands-diagram.puml
      │   ├── team-create-sequence.puml
      │   ├── team-create-activity.puml
      │   └── team-join-sequence.puml
      ├── player/
      │   └── player-class-diagram.puml
      └── moderation/
          └── moderation-class-diagram.puml

README.md diagram index split into 4 sections (overview, util,
features/team, features/player, features/moderation) for readability;
all links updated. features.md auto-updated by sed for the new paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Antone Barbaud
2026-06-10 13:58:48 +02:00
parent 4efaa5bbde
commit b02e532563
15 changed files with 53 additions and 27 deletions
@@ -0,0 +1,176 @@
@startuml moderation-class-diagram
title CR-Core — Moderation feature (class diagram, skeleton)
skinparam classAttributeIconSize 0
hide empty members
package "fr.luc.crcore.features.moderation" {
class ModerationState <<final>> {
- playerId: UUID
- enteredAt: Instant
- inventoryContents: ItemStack[]
- armorContents: ItemStack[]
- offhandItem: ItemStack
- xpLevel: int
- xpProgress: float
- health: double
- foodLevel: int
- saturation: float
- location: Location
- gameMode: GameMode
- allowFlight / flying / walkSpeed / flySpeed
--
+ ModerationState(player)
+ restoreTo(player): void
}
interface ModerationRepository {
+ findByPlayer(uuid): Optional<ModerationState>
+ exists(uuid): boolean
+ save(state): void
+ delete(uuid): boolean
+ findAll(): Collection<ModerationState>
}
interface ModeratorTool {
+ getKey(): String
+ getSlot(): int ' 0..8
+ buildIcon(): ItemStack
+ onLeftClick(player): void
+ onRightClick(player): void
+ onInteractEntity(player, target): void
}
class ModeratorToolRegistry {
- bySlot: Map<Integer, ModeratorTool>
- byKey: Map<String, ModeratorTool>
+ register(tool): void
+ unregister(key): boolean
+ get(key): Optional<ModeratorTool>
+ getBySlot(slot): Optional<ModeratorTool>
+ all(): Collection<ModeratorTool>
}
interface ModerationService {
+ enter(player): void
+ exit(player): void
+ isInModeration(uuid): boolean
+ getState(uuid): Optional<ModerationState>
+ getActiveModerators(): Set<UUID>
--
+ vanish(player) / unvanish / isVanished / getVanishedPlayers
+ freeze(uuid) / unfreeze / isFrozen / getFrozenPlayers
+ getToolRegistry(): ModeratorToolRegistry
}
package "fr.luc.crcore.features.moderation.impl" {
class InMemoryModerationRepository
InMemoryModerationRepository ..|> ModerationRepository
class ModerationServiceImpl {
# plugin: Plugin
# repository: ModerationRepository
# toolRegistry: ModeratorToolRegistry
# vanished: Set<UUID>
# frozen: Set<UUID>
--
# onAfterEnter(player) ' hook
# onAfterExit(player)
}
ModerationServiceImpl ..|> ModerationService
class BukkitEventFiringModerationServiceImpl
BukkitEventFiringModerationServiceImpl --|> ModerationServiceImpl
class ModerationListener {
+ registerOn(plugin): void
--
@ onInteract ' route → tool.onLeftClick / onRightClick
@ onInteractEntity ' route → tool.onInteractEntity
@ onDrop / onSwap ' lock hotbar
@ onInventoryClick ' cancel sur self-inv
@ onJoin ' re-hide vanished players
@ onQuit ' unfreeze
@ onMove ' cancel si frozen
}
ModerationListener ..|> "org.bukkit.event.Listener"
ModerationListener --> ModerationService
}
package "fr.luc.crcore.features.moderation.tool" {
class ExitTool ' slot 8
class VanishToggleTool ' slot 7
class FreezeTool ' slot 2 (entity right-click)
class InventorySpyTool ' slot 1 (entity right-click)
class TeleportRandomPlayerTool ' slot 0
ExitTool ..|> ModeratorTool
VanishToggleTool ..|> ModeratorTool
FreezeTool ..|> ModeratorTool
InventorySpyTool ..|> ModeratorTool
TeleportRandomPlayerTool ..|> ModeratorTool
}
package "fr.luc.crcore.features.moderation.event" {
abstract class ModerationEvent {
- moderator: Player
+ getModerator(): Player
}
ModerationEvent --|> "org.bukkit.event.Event"
class ModerationEnterEvent
class ModerationExitEvent
ModerationEnterEvent --|> ModerationEvent
ModerationExitEvent --|> ModerationEvent
}
package "fr.luc.crcore.features.moderation.exception" {
class ModerationException
class ModerationAlreadyActiveException
class ModerationNotActiveException
ModerationException --|> RuntimeException
ModerationAlreadyActiveException --|> ModerationException
ModerationNotActiveException --|> ModerationException
}
package "fr.luc.crcore.features.moderation.command" {
class AdminToggleSubCommand {
+ execute(ctx): CommandResult
}
AdminToggleSubCommand ..> ModerationService
}
ModerationService ..> ModerationState : reads/writes
ModerationService o--> ModerationRepository
ModerationService o--> ModeratorToolRegistry
ModeratorToolRegistry o--> "*" ModeratorTool
}
package "fr.luc.crcore" {
class CRCore {
+ moderation(): ModerationService
# buildModerationService(repo, registry): ModerationService
# registerDefaultModeratorTools(registry, mod): void
}
CRCore "1" *-- "0..1" ModerationService : owns (if setupModeration)
CRCore ..> ModerationListener : registers
}
note bottom of ModerationServiceImpl
enter(player) :
1. ModerationState snapshot → repo.save
2. clear inventory, set tools in hotbar
3. gamemode CREATIVE + allowFlight
4. vanish(player)
5. onAfterEnter() → event fired
exit(player) :
1. state.restoreTo(player) ' inv + xp + loc + gm + flight
2. unvanish(player)
3. unfreeze + repo.delete
4. onAfterExit() → event fired
Skeleton : in-memory repository only.
Persistance SQLite à venir.
end note
@enduml
@@ -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.util.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.features.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
@@ -0,0 +1,124 @@
@startuml builtin-commands-diagram
title CR-Core — Default /core team commands (admin / joueur)
skinparam classAttributeIconSize 0
hide empty members
package "fr.luc.crcore.util.command" {
abstract class BaseCommand
abstract class SubCommand
}
package "fr.luc.crcore.builtin" {
class CoreCommand
CoreCommand --|> BaseCommand
package "fr.luc.crcore.features.team.command" {
class TeamGroupSubCommand {
+ TeamGroupSubCommand(service)
}
TeamGroupSubCommand --|> SubCommand
class TeamArgumentTypes <<utility>> {
+ {static} teamByName(service): ArgumentType<Team>
}
' ─── ADMIN commands (permission seule, team par argument) ───
package "admin" <<Rectangle>> {
class TeamCreateSubCommand {
perm: crcore.team.create
args: name, tag, color, [leader]
}
class TeamDeleteSubCommand {
perm: crcore.team.delete
args: <team>
}
class TeamSetLeaderSubCommand {
perm: crcore.team.setleader
args: <team> <player>
}
class TeamScoreSubCommand {
perm: crcore.team.score
args: <team> <name> <add|set> <value>
}
class TeamAddSubCommand {
perm: crcore.team.add
args: <team> <player>
}
class TeamRemoveSubCommand {
perm: crcore.team.remove
args: <team> <player>
}
class TeamTransferSubCommand {
perm: crcore.team.transfer
args: <team> <player>
}
class TeamVisibilitySubCommand {
perm: crcore.team.visibility
args: <team> <vis>
}
class TeamSetSpawnSubCommand {
perm: crcore.team.setspawn
args: <team>
playerOnly
}
}
' ─── PLAYER commands ───
package "player" <<Rectangle>> {
class TeamJoinSubCommand {
perm: crcore.team.join
args: <team>
}
class TeamLeaveSubCommand {
perm: crcore.team.leave
}
class TeamInfoSubCommand {
perm: crcore.team.info
args: [team]
}
class TeamListSubCommand {
perm: crcore.team.list
}
class TeamTopSubCommand {
perm: crcore.team.top
args: [score]
}
}
TeamCreateSubCommand --|> SubCommand
TeamDeleteSubCommand --|> SubCommand
TeamSetLeaderSubCommand --|> SubCommand
TeamScoreSubCommand --|> SubCommand
TeamAddSubCommand --|> SubCommand
TeamRemoveSubCommand --|> SubCommand
TeamTransferSubCommand --|> SubCommand
TeamVisibilitySubCommand --|> SubCommand
TeamSetSpawnSubCommand --|> SubCommand
TeamJoinSubCommand --|> SubCommand
TeamLeaveSubCommand --|> SubCommand
TeamInfoSubCommand --|> SubCommand
TeamListSubCommand --|> SubCommand
TeamTopSubCommand --|> SubCommand
CoreCommand "1" *-- "1" TeamGroupSubCommand : contains
TeamGroupSubCommand "1" *-- "14" SubCommand : contains
}
}
note bottom of TeamGroupSubCommand
Le rôle LEADER reste dans le modèle Team
(utilisable par les game plugins via l'API)
mais n'accorde aucun privilège de commande
dans le set par défaut.
Override d'une feuille :
core.getCoreCommand()
.findSubCommand("team")
.replaceSubCommand("create",
new MyCreate(svc));
end note
@enduml
@@ -0,0 +1,251 @@
@startuml team-class-diagram
title CR-Core — Team domain (class diagram)
skinparam classAttributeIconSize 0
hide empty members
' === Common abstractions ===
package "fr.luc.crcore.util.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.features.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 *(nullable)*
- visibility: TeamVisibility
- members: Set<TeamMember>
- scores: Map<String, Integer>
- spawnPoint: Location
--
+ Team(id, name, tag, color) ' leaderless, PRIVATE
+ Team(id, name, tag, color, visibility) ' leaderless
+ Team(id, name, tag, color, leaderId) ' with leader, PRIVATE
+ Team(id, name, tag, color, leaderId, visibility)
--
+ getName(): String
+ getTag(): String
+ getColor(): TeamColor
+ getLeaderId(): Optional<UUID>
+ getLeader(): Optional<TeamMember>
+ hasLeader(): boolean
+ isLeader(playerId): boolean
+ 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 ' strict: chef→chef
+ setLeader(newLeaderId): void ' permissive: assign
--
+ 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): Team ' leaderless, PRIVATE
+ createTeam(name, tag, color, visibility): Team ' leaderless
+ createTeam(name, tag, color, leaderId): Team
+ 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 ' strict: chef→chef
+ setLeader(teamId, newLeaderId): boolean ' permissive (admin)
+ 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
@@ -0,0 +1,119 @@
@startuml team-config-class-diagram
title CR-Core — Team config (class diagram, cascade per-team → global → default)
skinparam classAttributeIconSize 0
hide empty members
package "fr.luc.crcore.features.team.config" {
class "TeamSetting<T>" as TeamSetting <<final>> {
- key: String
- type: Class<T>
- defaultValue: T
- kind: Kind
- parser: Function<Object,T>
- serializer: Function<T,Object>
--
+ {static} ofBoolean(key, default): TeamSetting<Boolean>
+ {static} ofInt(key, default): TeamSetting<Integer>
+ {static} ofString(key, default): TeamSetting<String>
+ {static} ofEnum(key, default): TeamSetting<E>
--
+ parse(raw): T
+ serialize(value): Object
+ getKey() / getType() / getDefaultValue() / getKind()
}
enum "TeamSetting.Kind" as Kind {
BOOLEAN
INTEGER
STRING
ENUM
}
TeamSetting +-- Kind
class TeamSettings <<utility>> {
+ {static} FRIENDLY_FIRE: TeamSetting<Boolean>
+ {static} PVP_PROTECTION_SECONDS: TeamSetting<Integer>
+ {static} MAX_SIZE: TeamSetting<Integer>
+ {static} MIN_SIZE: TeamSetting<Integer>
+ {static} RESPAWN_AT_TEAM_SPAWN: TeamSetting<Boolean>
+ {static} TEAM_CHAT_ENABLED: TeamSetting<Boolean>
+ {static} SHOW_TAG_ABOVE_HEAD: TeamSetting<Boolean>
+ {static} TEAM_COLOR_IN_NAME: TeamSetting<Boolean>
--
+ {static} register(setting): void
+ {static} get(key): Optional<TeamSetting<?>>
+ {static} all(): Collection<TeamSetting<?>>
}
TeamSettings ..> TeamSetting
interface TeamConfigService {
+ get(team, setting): T
+ getGlobal(setting): T
+ setPerTeam(team, setting, value): void
+ resetPerTeam(team, setting): void
+ setGlobal(setting, value): void
+ reload(): void
+ hasPerTeamOverride(team, setting): boolean
+ getGlobalSnapshot(): Map<TeamSetting<?>, Object>
+ getGlobalFileName(): Optional<String>
}
package "fr.luc.crcore.features.team.config.impl" {
class YamlTeamConfigService {
- plugin: JavaPlugin
- teamRepository: TeamRepository
- userFile: File
- globalValues: Map<String, Object>
--
- ensureUserFile(): void
- rebuildGlobalValues(): void
- persistGlobals(): void
}
YamlTeamConfigService ..|> TeamConfigService
}
TeamConfigService ..> TeamSetting : reads/writes
}
package "fr.luc.crcore.features.team" {
class Team {
- settings: Map<String, Object>
+ getSettings(): Map<String, Object>
}
}
package "fr.luc.crcore.features.team.config.gui" {
abstract class AbstractSettingsGui {
- rebuild() : peint la grille
# {abstract} getCurrentValue(setting): T
# {abstract} onChange(setting, newValue): void
# isOverride(setting): boolean
# renderFooter(): void
}
class GlobalSettingsGui
class TeamSettingsGui
GlobalSettingsGui --|> AbstractSettingsGui
TeamSettingsGui --|> AbstractSettingsGui
AbstractSettingsGui --|> "fr.luc.crcore.util.gui.AbstractInventoryGui"
GlobalSettingsGui --> TeamConfigService
TeamSettingsGui --> TeamConfigService
TeamSettingsGui --> Team
}
YamlTeamConfigService --> "fr.luc.crcore.features.team.TeamRepository" : persists per-team via save()
TeamConfigService ..> Team : reads/writes settings map
note bottom of YamlTeamConfigService
Cascade de résolution :
1. team.getSettings().get(key) ← override per-team (SQLite)
2. globalValues.get(key) ← <plugin>-team-config.yml
3. setting.getDefaultValue() ← constante Java
Le YAML global est ré-écrit à chaque setGlobal() (persistant à crash).
Les per-team sont écrits via teamRepository.save(team).
end note
@enduml
@@ -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
@@ -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
@@ -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