From bcba8363c9a0eae16ba8e5243cbb6469196094ef Mon Sep 17 00:00:00 2001 From: Antone Barbaud Date: Tue, 9 Jun 2026 15:28:48 +0200 Subject: [PATCH] fix: restore visibility arg on /core team create + auto-migrate leader_id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /core team create now takes [visibility] [leader] (both optional, positional in this order). Variants: - /core team create N T C → PRIVATE, leaderless - /core team create N T C PUBLIC → PUBLIC, leaderless - /core team create N T C PRIVATE Alice → PRIVATE, Alice as leader - /core team create N T C PUBLIC Alice → PUBLIC, Alice as leader Auto-migration for pre-existing SQLite databases: SqliteTeamRepository.ensureSchema() now checks pragma_table_info for the leader_id column. If it still has the NOT NULL constraint from an older schema, it recreates the table (in a transaction) with leader_id nullable and copies the rows over. Required because CREATE TABLE IF NOT EXISTS skips the alteration on existing tables, so production databases were crashing with SQLITE_CONSTRAINT_NOTNULL when an admin tried to create a leaderless team. Co-Authored-By: Claude Opus 4.7 --- .../builtin/team/TeamCreateSubCommand.java | 36 ++++++++------- .../luc/crcore/team/SqliteTeamRepository.java | 46 +++++++++++++++++++ 2 files changed, 66 insertions(+), 16 deletions(-) diff --git a/src/main/java/fr/luc/crcore/command/builtin/team/TeamCreateSubCommand.java b/src/main/java/fr/luc/crcore/command/builtin/team/TeamCreateSubCommand.java index 14d20b0..3424e07 100644 --- a/src/main/java/fr/luc/crcore/command/builtin/team/TeamCreateSubCommand.java +++ b/src/main/java/fr/luc/crcore/command/builtin/team/TeamCreateSubCommand.java @@ -8,6 +8,7 @@ import fr.luc.crcore.team.Team; import fr.luc.crcore.team.TeamColor; import fr.luc.crcore.team.TeamException; import fr.luc.crcore.team.TeamService; +import fr.luc.crcore.team.TeamVisibility; import org.bukkit.entity.Player; import java.util.Objects; @@ -15,21 +16,20 @@ import java.util.Optional; import java.util.UUID; /** - * {@code /core team create [leader]} + * {@code /core team create [visibility] [leader]} * - *

Admin uniquement. Crée une équipe en {@link - * fr.luc.crcore.team.TeamVisibility#PRIVATE} par défaut. + *

Admin uniquement. + * + *

Les deux derniers arguments sont optionnels mais positionnels : + * pour passer un {@code leader}, il faut aussi taper la {@code visibility} + * d'abord (PUBLIC ou PRIVATE). Variantes valides : * - *

Le chef est optionnel : *

    - *
  • Sans argument {@code leader} → équipe leaderless. L'admin assignera - * plus tard via {@code /core team setleader}.
  • - *
  • Avec argument {@code leader} (nom d'un joueur connecté) → ce joueur - * devient chef et membre de l'équipe.
  • + *
  • {@code /core team create Wolves WOLF RED} → PRIVATE, leaderless
  • + *
  • {@code /core team create Wolves WOLF RED PUBLIC} → PUBLIC, leaderless
  • + *
  • {@code /core team create Wolves WOLF RED PRIVATE Alice} → Alice chef, PRIVATE
  • + *
  • {@code /core team create Wolves WOLF RED PUBLIC Alice} → Alice chef, PUBLIC
  • *
- * - *

La visibilité (PUBLIC/PRIVATE) se change ensuite via {@code /core team - * visibility} (action du chef). */ public class TeamCreateSubCommand extends SubCommand { @@ -43,6 +43,7 @@ public class TeamCreateSubCommand extends SubCommand { argument("name", ArgumentTypes.STRING); argument("tag", ArgumentTypes.STRING); argument("color", ArgumentTypes.enumOf(TeamColor.class)); + optionalArgument("visibility", ArgumentTypes.enumOf(TeamVisibility.class)); optionalArgument("leader", ArgumentTypes.ONLINE_PLAYER); } @@ -51,15 +52,18 @@ public class TeamCreateSubCommand extends SubCommand { String name = ctx.get("name"); String tag = ctx.get("tag"); TeamColor color = ctx.get("color"); + TeamVisibility visibility = ctx.getOptional("visibility") + .orElse(TeamVisibility.PRIVATE); Optional leaderOpt = ctx.getOptional("leader"); UUID leaderId = leaderOpt.map(Player::getUniqueId).orElse(null); try { - Team team = service.createTeam(name, tag, color, leaderId); - String suffix = leaderOpt.isPresent() - ? " (chef : " + leaderOpt.get().getName() + ")" - : " (sans chef)"; - return CommandResult.success("Équipe " + team.getName() + " [#" + team.getTag() + "] créée" + suffix + "."); + Team team = service.createTeam(name, tag, color, leaderId, visibility); + String chefPart = leaderOpt.isPresent() + ? "chef : " + leaderOpt.get().getName() + : "sans chef"; + return CommandResult.success("Équipe " + team.getName() + " [#" + team.getTag() + + "] créée (" + visibility + ", " + chefPart + ")."); } catch (TeamException ex) { return CommandResult.failure(ex.getMessage()); } diff --git a/src/main/java/fr/luc/crcore/team/SqliteTeamRepository.java b/src/main/java/fr/luc/crcore/team/SqliteTeamRepository.java index c4b280b..491f975 100644 --- a/src/main/java/fr/luc/crcore/team/SqliteTeamRepository.java +++ b/src/main/java/fr/luc/crcore/team/SqliteTeamRepository.java @@ -69,6 +69,52 @@ public class SqliteTeamRepository extends InMemoryTeamRepository { .column("score_name", ColumnType.TEXT).notNull() .column("value", ColumnType.INTEGER).notNull() .create(); + + // Migration : ancien schéma avait leader_id NOT NULL ; on le rend nullable + // pour permettre des équipes leaderless. SQLite ne supporte pas ALTER COLUMN, + // donc on recrée la table en copiant les données. + migrateLeaderIdToNullableIfNeeded(); + } + + /** + * Vérifie si la colonne {@code crcore_teams.leader_id} a une contrainte + * {@code NOT NULL} (ancien schéma) et la migre vers nullable si besoin. + * Idempotent — no-op si la colonne est déjà nullable ou si la table est + * neuve. + */ + private void migrateLeaderIdToNullableIfNeeded() { + boolean hasNotNull = db.queryOne( + "SELECT \"notnull\" FROM pragma_table_info('" + TABLE_TEAMS + "') WHERE name='leader_id'", + rs -> rs.getInt(1) == 1 + ).orElse(false); + if (!hasNotNull) return; + + // Recréation via table temporaire — séquence standard SQLite pour + // changer une contrainte de colonne. + db.inTransaction(() -> { + db.execute("ALTER TABLE " + TABLE_TEAMS + " RENAME TO " + TABLE_TEAMS + "_old"); + db.execute("CREATE TABLE " + TABLE_TEAMS + " (" + + "\"id\" TEXT PRIMARY KEY, " + + "\"name\" TEXT NOT NULL UNIQUE, " + + "\"tag\" TEXT NOT NULL UNIQUE, " + + "\"color\" TEXT NOT NULL, " + + "\"leader_id\" TEXT, " + + "\"visibility\" TEXT NOT NULL, " + + "\"spawn_world\" TEXT, " + + "\"spawn_x\" REAL, " + + "\"spawn_y\" REAL, " + + "\"spawn_z\" REAL, " + + "\"spawn_yaw\" REAL, " + + "\"spawn_pitch\" REAL" + + ")"); + db.execute("INSERT INTO " + TABLE_TEAMS + " " + + "(id, name, tag, color, leader_id, visibility, " + + " spawn_world, spawn_x, spawn_y, spawn_z, spawn_yaw, spawn_pitch) " + + "SELECT id, name, tag, color, leader_id, visibility, " + + " spawn_world, spawn_x, spawn_y, spawn_z, spawn_yaw, spawn_pitch " + + "FROM " + TABLE_TEAMS + "_old"); + db.execute("DROP TABLE " + TABLE_TEAMS + "_old"); + }); } /** Recharge tous les Teams depuis la DB dans le cache mémoire hérité. */