feat: SQLite persistence, default /core commands, Bukkit events, bootstrap

CRCore bootstrap class: one-line setup for game plugins (new CRCore(this).enable()).
Wires SQLite, services with event firing, and the /core command tree.

SQLite layer (fr.luc.crcore.database): Database wrapper exposing execute/update/
queryOne/query plus a fluent TableBuilder. ColumnType enum, RowMapper interface,
DatabaseException. Game plugins create their own tables in 2 lines via
db.table("foo").ifNotExists().column(...).create().

Repositories: SqliteTeamRepository and SqlitePlayerProfileRepository extend their
InMemory counterparts (write-through cache). 5 internal tables prefixed crcore_.

Command framework refactored for nested sub-commands: subcommand storage moved
from BaseCommand to AbstractCommand, recursive dispatch() and tabComplete(),
replaceSubCommand() for plugin overrides.

Default /core team commands (13 leaf sub-commands): create, delete, add, remove,
join, leave, info, list, transfer, visibility, score, top, setspawn. Each in its
own class under fr.luc.crcore.command.builtin.team, fully substitutable.

Bukkit events: 9 team events (Create/Dissolve/MemberAdd/MemberRemove/PlayerJoin/
LeadershipTransfer/VisibilityChange/ScoreChange/SpawnPointChange) + 3 player
events (ProfileCreate/Delete/ScoreChange). All post-only, non-cancellable.
BukkitEventFiringTeamServiceImpl and BukkitEventFiringPlayerProfileServiceImpl
override the on* hooks to call Bukkit.getPluginManager().callEvent.

JavaDoc on all new public classes and key existing ones. docs/, GEMINI.md and
PUML diagrams synced: new sections (built-in commands, events, database,
bootstrap), 4 new diagrams (builtin-commands, events, database, bootstrap-
sequence), and 7 new architecture decisions logged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Antone Barbaud
2026-06-09 10:54:00 +02:00
parent ffc77c4213
commit c1b414f400
63 changed files with 3632 additions and 400 deletions
+62
View File
@@ -0,0 +1,62 @@
@startuml bootstrap-sequence
title CR-Core — Bootstrap from a game plugin (onEnable)
participant "MyGamePlugin\nextends JavaPlugin" as Plugin
participant "CRCore" as Core
participant "Database" as DB
participant "SqliteTeamRepository" as TeamRepo
participant "SqlitePlayerProfileRepository" as PlayerRepo
participant "BukkitEventFiringTeamServiceImpl" as TeamSvc
participant "BukkitEventFiringPlayerProfileServiceImpl" as PlayerSvc
participant "CoreCommand\n(/core)" as Cmd
participant "Bukkit" as Bukkit
Plugin -> Core : new CRCore(this)
activate Core
Plugin -> Core : enable()
Core -> DB : new Database(<dataFolder>/crcore.db)
activate DB
DB -> DB : ensure parent dir + PRAGMA foreign_keys
DB --> Core
deactivate DB
Core -> TeamRepo : new SqliteTeamRepository(db)
activate TeamRepo
TeamRepo -> DB : ensureSchema() — crcore_teams + crcore_team_members + crcore_team_scores
TeamRepo -> DB : loadAll() — SELECT pour ré-hydrater le cache mémoire
TeamRepo --> Core
deactivate TeamRepo
Core -> PlayerRepo : new SqlitePlayerProfileRepository(db)
activate PlayerRepo
PlayerRepo -> DB : ensureSchema() — crcore_player_profiles + crcore_player_scores
PlayerRepo -> DB : loadAll()
PlayerRepo --> Core
deactivate PlayerRepo
Core -> TeamSvc : buildTeamService(teamRepo)
Core -> PlayerSvc : buildPlayerProfileService(playerRepo)
Core -> Cmd : new CoreCommand(teamSvc, playerSvc)
activate Cmd
Cmd -> Cmd : registerDefaults() — TeamGroupSubCommand avec 13 leaf sub-cmds
Cmd --> Core
deactivate Cmd
Core -> Bukkit : plugin.getCommand("core").setExecutor(cmd)
Core -> Bukkit : .setTabCompleter(cmd)
Core --> Plugin : this (chainable)
deactivate Core
note over Plugin
À ce stade :
- /core team create/delete/add/... fonctionnel
- SQLite persiste team + player + leurs scores
- Évènements Bukkit sont tirés sur chaque opération
- Le plugin de jeu peut listen avec @EventHandler
end note
@enduml
@@ -0,0 +1,76 @@
@startuml builtin-commands-diagram
title CR-Core — Default /core team commands
skinparam classAttributeIconSize 0
hide empty members
package "fr.luc.crcore.command" {
abstract class BaseCommand
abstract class SubCommand
}
package "fr.luc.crcore.command.builtin" {
class CoreCommand {
+ CoreCommand(teamSvc, playerSvc)
# registerDefaults(): void
}
CoreCommand --|> BaseCommand
package "fr.luc.crcore.command.builtin.team" {
class TeamGroupSubCommand {
+ TeamGroupSubCommand(service)
# registerDefaults(): void
}
TeamGroupSubCommand --|> SubCommand
class TeamArgumentTypes <<utility>> {
+ {static} teamByName(service): ArgumentType<Team>
}
class TeamCreateSubCommand {
+ execute(ctx): CommandResult
}
class TeamDeleteSubCommand
class TeamAddSubCommand
class TeamRemoveSubCommand
class TeamJoinSubCommand
class TeamLeaveSubCommand
class TeamInfoSubCommand
class TeamListSubCommand
class TeamTransferSubCommand
class TeamVisibilitySubCommand
class TeamScoreSubCommand
class TeamTopSubCommand
class TeamSetSpawnSubCommand
TeamCreateSubCommand --|> SubCommand
TeamDeleteSubCommand --|> SubCommand
TeamAddSubCommand --|> SubCommand
TeamRemoveSubCommand --|> SubCommand
TeamJoinSubCommand --|> SubCommand
TeamLeaveSubCommand --|> SubCommand
TeamInfoSubCommand --|> SubCommand
TeamListSubCommand --|> SubCommand
TeamTransferSubCommand --|> SubCommand
TeamVisibilitySubCommand --|> SubCommand
TeamScoreSubCommand --|> SubCommand
TeamTopSubCommand --|> SubCommand
TeamSetSpawnSubCommand --|> SubCommand
CoreCommand "1" *-- "1" TeamGroupSubCommand : contains
TeamGroupSubCommand "1" *-- "13" SubCommand : contains
}
}
note right of CoreCommand
Le plugin de jeu downstream
remplace une feuille avec :
core.getCoreCommand()
.findSubCommand("team")
.replaceSubCommand("create",
new MyCreate(svc));
end note
@enduml
+41 -49
View File
@@ -1,5 +1,5 @@
@startuml command-class-diagram
title CR-Core — Command framework (class diagram)
title CR-Core — Command framework (class diagram, nested sub-commands)
skinparam classAttributeIconSize 0
hide empty members
@@ -13,7 +13,7 @@ package "fr.luc.crcore.command" {
+ isPlayerOnly(): boolean
+ getDescription(): String
+ execute(ctx: CommandContext): CommandResult
+ tabComplete(sender, argIndex, partial): List<String>
+ tabComplete(sender, args: String[]): List<String>
+ matches(label: String): boolean
}
@@ -25,46 +25,41 @@ package "fr.luc.crcore.command" {
- 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
- subCommandsByName: Map<String, SubCommand>
- subCommandsByAlias: Map<String, SubCommand>
--
# addAlias(...) / permission / playerOnly / description / usage
# argument(name, type) / optionalArgument(name, type)
# addSubCommand(sub: SubCommand): void
--
+ findSubCommand(label): Optional<SubCommand>
+ getSubCommands(): Collection<SubCommand>
+ replaceSubCommand(name, newSub): Optional<SubCommand>
+ hasSubCommands(): boolean
--
+ dispatch(sender, label, args): CommandResult
+ tabComplete(sender, args): List<String>
+ execute(ctx): CommandResult
# listSubCommands(ctx): CommandResult
# checkAccess(sender): boolean
# buildContext(sender, label, rawArgs): CommandContext
}
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
}
abstract class SubCommand
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
+ getSender / isPlayer / getPlayer / requirePlayer
+ get(name): T
+ getOptional(name): Optional<T>
+ has(name): boolean
+ reply(msg): void
@@ -73,15 +68,7 @@ package "fr.luc.crcore.command" {
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
+ {static} success / failure / invalidUsage / noPermission / playerOnly
}
enum "CommandResult.Type" as ResultType {
@@ -99,17 +86,13 @@ package "fr.luc.crcore.command" {
+ 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 ArgumentTypes <<utility>> {
+ STRING / INTEGER / DOUBLE / BOOLEAN / ONLINE_PLAYER
+ enumOf(Class<E>): ArgumentType<E>
+ choice(String...): ArgumentType<String>
}
class ArgumentDef << (P, #BBBBBB) package-private >> {
class ArgumentDef <<package-private>> {
- name: String
- type: ArgumentType<?>
- required: boolean
@@ -118,15 +101,24 @@ package "fr.luc.crcore.command" {
AbstractCommand ..|> Command
BaseCommand --|> AbstractCommand
SubCommand --|> AbstractCommand
BaseCommand "1" o-- "*" SubCommand : subCommands
BaseCommand ..|> "org.bukkit.command.CommandExecutor"
BaseCommand ..|> "org.bukkit.command.TabCompleter"
AbstractCommand "1" o-- "*" SubCommand : sub-commands\n(recursive)
AbstractCommand "1" *-- "*" ArgumentDef : arguments
ArgumentDef --> ArgumentType
CommandResult +-- ResultType
CommandException --|> RuntimeException
BaseCommand ..> CommandContext : creates
AbstractCommand ..> CommandContext : creates
SubCommand ..> CommandResult : returns
AbstractCommand ..> CommandResult : returns
}
note bottom of AbstractCommand
Le routage est récursif :
/core team create → CoreCommand.dispatch("team", ["create",...])
→ TeamGroup.dispatch("create", [...])
→ TeamCreate.execute(ctx)
end note
@enduml
+86
View File
@@ -0,0 +1,86 @@
@startuml database-diagram
title CR-Core — Database (SQLite wrapper)
skinparam classAttributeIconSize 0
hide empty members
package "fr.luc.crcore.database" {
class Database {
- connection: Connection
+ Database(file: File)
+ execute(sql, params...): void
+ update(sql, params...): int
+ queryOne(sql, mapper, params...): Optional<T>
+ query(sql, mapper, params...): List<T>
+ inTransaction(block: Runnable): void
+ table(name: String): TableBuilder
+ tableExists(name: String): boolean
+ getConnection(): Connection
+ close(): void
}
Database ..|> "java.lang.AutoCloseable"
class TableBuilder {
- database: Database
- name: String
- columns: List<ColumnDef>
- ifNotExists: boolean
+ ifNotExists(): TableBuilder
+ column(name, type): ColumnDef
+ create(): void
}
class "TableBuilder.ColumnDef" as ColumnDef {
- name: String
- type: ColumnType
- primaryKey: boolean
- notNull: boolean
- unique: boolean
- defaultValue: String
+ primaryKey(): ColumnDef
+ notNull(): ColumnDef
+ unique(): ColumnDef
+ defaultValue(expr: String): ColumnDef
+ column(name, type): ColumnDef
+ create(): void
}
enum ColumnType {
INTEGER
REAL
TEXT
BLOB
BOOLEAN
UUID
--
+ getSqlType(): String
}
interface "RowMapper<T>" as RowMapper {
+ map(rs: ResultSet): T
}
class DatabaseException
DatabaseException --|> RuntimeException
Database "1" *-- "*" TableBuilder : creates
TableBuilder "1" *-- "*" ColumnDef : contains
ColumnDef --> ColumnType : type
Database ..> RowMapper : uses
Database ..> DatabaseException : throws
}
note right of Database
Repositories SQLite de CR-Core
(SqliteTeamRepository,
SqlitePlayerProfileRepository)
utilisent Database pour
persister state team/player.
Les plugins de jeu utilisent
Database.table(...) pour
créer leurs tables custom.
end note
@enduml
+89
View File
@@ -0,0 +1,89 @@
@startuml events-diagram
title CR-Core — Bukkit events (team + player)
skinparam classAttributeIconSize 0
hide empty members
package "org.bukkit.event" {
abstract class Event
}
package "fr.luc.crcore.team.event" {
abstract class TeamEvent {
- team: Team
+ getTeam(): Team
}
TeamEvent --|> Event
class TeamCreateEvent
class TeamDissolveEvent
class TeamMemberAddEvent {
+ getMember(): TeamMember
}
class TeamMemberRemoveEvent {
+ getPlayerId(): UUID
}
class PlayerJoinTeamEvent {
+ getMember(): TeamMember
}
class TeamLeadershipTransferEvent {
+ getOldLeaderId(): UUID
+ getNewLeaderId(): UUID
}
class TeamVisibilityChangeEvent {
+ getOldVisibility(): TeamVisibility
+ getNewVisibility(): TeamVisibility
}
class TeamScoreChangeEvent {
+ getScoreName(): String
+ getOldValue(): int
+ getNewValue(): int
+ getDelta(): int
}
class TeamSpawnPointChangeEvent {
+ getOldLocation(): Location
+ getNewLocation(): Location
}
TeamCreateEvent --|> TeamEvent
TeamDissolveEvent --|> TeamEvent
TeamMemberAddEvent --|> TeamEvent
TeamMemberRemoveEvent --|> TeamEvent
PlayerJoinTeamEvent --|> TeamEvent
TeamLeadershipTransferEvent --|> TeamEvent
TeamVisibilityChangeEvent --|> TeamEvent
TeamScoreChangeEvent --|> TeamEvent
TeamSpawnPointChangeEvent --|> TeamEvent
}
package "fr.luc.crcore.player.event" {
abstract class PlayerProfileEvent {
- profile: PlayerProfile
+ getProfile(): PlayerProfile
}
PlayerProfileEvent --|> Event
class PlayerProfileCreateEvent
class PlayerProfileDeleteEvent
class PlayerScoreChangeEvent {
+ getScoreName(): String
+ getOldValue(): int
+ getNewValue(): int
+ getDelta(): int
}
PlayerProfileCreateEvent --|> PlayerProfileEvent
PlayerProfileDeleteEvent --|> PlayerProfileEvent
PlayerScoreChangeEvent --|> PlayerProfileEvent
}
note right of TeamEvent
Tous post-events, non-cancellable.
Tirés par les sous-classes
BukkitEventFiring*ServiceImpl
via les hooks on* hérités.
end note
@enduml