query("SHOW COLUMNS FROM `{$table}` LIKE " . $db->quote($column)); return (bool) $stmt->fetch(); } function sccharacters_index_exists(PDO $db, string $table, string $index): bool { $stmt = $db->query("SHOW INDEX FROM `{$table}` WHERE Key_name = " . $db->quote($index)); return (bool) $stmt->fetch(); } function sccharacters_foreign_key_exists(PDO $db, string $table, string $constraint): bool { $stmt = $db->prepare( "SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :table_name AND CONSTRAINT_NAME = :constraint_name AND CONSTRAINT_TYPE = 'FOREIGN KEY'" ); $stmt->execute([ 'table_name' => $table, 'constraint_name' => $constraint, ]); return (int) $stmt->fetchColumn() > 0; } function sccharacters_bootstrap(): void { static $bootstrapped = false; if ($bootstrapped) { return; } $db = db(); $db->exec( "CREATE TABLE IF NOT EXISTS tbl_sccharacters ( cl_sccharacter_id INT UNSIGNED NOT NULL AUTO_INCREMENT, cl_sccharacter_owner_auth_id INT UNSIGNED NOT NULL, cl_sccharacter_name VARCHAR(190) NOT NULL, cl_sccharacter_role VARCHAR(190) NOT NULL DEFAULT '', cl_sccharacter_faction VARCHAR(190) NOT NULL DEFAULT '', cl_sccharacter_avatar_url VARCHAR(255) NOT NULL DEFAULT '', cl_sccharacter_description TEXT DEFAULT NULL, cl_sccharacter_notes TEXT DEFAULT NULL, cl_sccharacter_share_token VARCHAR(64) NOT NULL, cl_sccharacter_share_enabled TINYINT(1) NOT NULL DEFAULT 0, cl_sccharacter_is_pinned TINYINT(1) NOT NULL DEFAULT 0, cl_sccharacter_category_order TEXT DEFAULT NULL, cl_sccharacter_created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, cl_sccharacter_updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (cl_sccharacter_id), UNIQUE KEY uq_sccharacter_share_token (cl_sccharacter_share_token), KEY idx_sccharacter_owner (cl_sccharacter_owner_auth_id), KEY idx_sccharacter_name (cl_sccharacter_name), CONSTRAINT fk_sccharacter_owner_auth FOREIGN KEY (cl_sccharacter_owner_auth_id) REFERENCES tbl_auth (cl_auth_id) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" ); if (!sccharacters_column_exists($db, 'tbl_sccharacters', 'cl_sccharacter_owner_auth_id')) { $db->exec( 'ALTER TABLE tbl_sccharacters ADD COLUMN cl_sccharacter_owner_auth_id INT UNSIGNED NULL AFTER cl_sccharacter_id' ); } if (!sccharacters_column_exists($db, 'tbl_sccharacters', 'cl_sccharacter_share_token')) { $db->exec( "ALTER TABLE tbl_sccharacters ADD COLUMN cl_sccharacter_share_token VARCHAR(64) NOT NULL DEFAULT '' AFTER cl_sccharacter_notes" ); } if (!sccharacters_column_exists($db, 'tbl_sccharacters', 'cl_sccharacter_share_enabled')) { $db->exec( 'ALTER TABLE tbl_sccharacters ADD COLUMN cl_sccharacter_share_enabled TINYINT(1) NOT NULL DEFAULT 0 AFTER cl_sccharacter_share_token' ); } if (!sccharacters_column_exists($db, 'tbl_sccharacters', 'cl_sccharacter_is_pinned')) { $db->exec( 'ALTER TABLE tbl_sccharacters ADD COLUMN cl_sccharacter_is_pinned TINYINT(1) NOT NULL DEFAULT 0 AFTER cl_sccharacter_share_enabled' ); } if (!sccharacters_column_exists($db, 'tbl_sccharacters', 'cl_sccharacter_category_order')) { $db->exec( 'ALTER TABLE tbl_sccharacters ADD COLUMN cl_sccharacter_category_order TEXT NULL AFTER cl_sccharacter_is_pinned' ); } if (!sccharacters_index_exists($db, 'tbl_sccharacters', 'idx_sccharacter_owner')) { $db->exec( 'ALTER TABLE tbl_sccharacters ADD INDEX idx_sccharacter_owner (cl_sccharacter_owner_auth_id)' ); } if (!sccharacters_index_exists($db, 'tbl_sccharacters', 'uq_sccharacter_share_token')) { $db->exec( 'ALTER TABLE tbl_sccharacters ADD UNIQUE KEY uq_sccharacter_share_token (cl_sccharacter_share_token)' ); } if (!sccharacters_foreign_key_exists($db, 'tbl_sccharacters', 'fk_sccharacter_owner_auth')) { $db->exec( 'ALTER TABLE tbl_sccharacters ADD CONSTRAINT fk_sccharacter_owner_auth FOREIGN KEY (cl_sccharacter_owner_auth_id) REFERENCES tbl_auth (cl_auth_id) ON DELETE CASCADE ON UPDATE CASCADE' ); } $db->exec( "CREATE TABLE IF NOT EXISTS tbl_sccharacteritems ( cl_sccharacteritem_id INT UNSIGNED NOT NULL AUTO_INCREMENT, cl_sccharacteritem_character_id INT UNSIGNED NOT NULL, cl_sccharacteritem_source ENUM('base', 'custom') NOT NULL DEFAULT 'base', cl_sccharacteritem_scobjs_id INT UNSIGNED DEFAULT NULL, cl_sccharacteritem_scitemcustom_id INT(11) DEFAULT NULL, cl_sccharacteritem_slot VARCHAR(120) NOT NULL DEFAULT '', cl_sccharacteritem_note TEXT DEFAULT NULL, cl_sccharacteritem_sort_order INT UNSIGNED NOT NULL DEFAULT 0, cl_sccharacteritem_created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (cl_sccharacteritem_id), KEY idx_sccharacteritem_character (cl_sccharacteritem_character_id), KEY idx_sccharacteritem_scobjs (cl_sccharacteritem_scobjs_id), KEY idx_sccharacteritem_scitemcustom (cl_sccharacteritem_scitemcustom_id), KEY idx_sccharacteritem_character_sort (cl_sccharacteritem_character_id, cl_sccharacteritem_sort_order, cl_sccharacteritem_id), CONSTRAINT fk_sccharacteritem_character FOREIGN KEY (cl_sccharacteritem_character_id) REFERENCES tbl_sccharacters (cl_sccharacter_id) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT fk_sccharacteritem_scobjs FOREIGN KEY (cl_sccharacteritem_scobjs_id) REFERENCES tbl_scobjs (cl_scobjs_id) ON DELETE SET NULL ON UPDATE CASCADE, CONSTRAINT fk_sccharacteritem_scitemcustom FOREIGN KEY (cl_sccharacteritem_scitemcustom_id) REFERENCES tbl_scitemcustom (cl_scitemcustom_id) ON DELETE SET NULL ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" ); if (!sccharacters_column_exists($db, 'tbl_sccharacteritems', 'cl_sccharacteritem_scobjs_id')) { $db->exec( 'ALTER TABLE tbl_sccharacteritems ADD COLUMN cl_sccharacteritem_scobjs_id INT UNSIGNED NULL AFTER cl_sccharacteritem_source' ); } if (!sccharacters_column_exists($db, 'tbl_sccharacteritems', 'cl_sccharacteritem_scitemcustom_id')) { $db->exec( 'ALTER TABLE tbl_sccharacteritems ADD COLUMN cl_sccharacteritem_scitemcustom_id INT(11) NULL AFTER cl_sccharacteritem_scobjs_id' ); } if (!sccharacters_column_exists($db, 'tbl_sccharacteritems', 'cl_sccharacteritem_sort_order')) { $db->exec( 'ALTER TABLE tbl_sccharacteritems ADD COLUMN cl_sccharacteritem_sort_order INT UNSIGNED NOT NULL DEFAULT 0 AFTER cl_sccharacteritem_note' ); } if (!sccharacters_index_exists($db, 'tbl_sccharacteritems', 'idx_sccharacteritem_character')) { $db->exec( 'ALTER TABLE tbl_sccharacteritems ADD INDEX idx_sccharacteritem_character (cl_sccharacteritem_character_id)' ); } if (!sccharacters_index_exists($db, 'tbl_sccharacteritems', 'idx_sccharacteritem_scobjs')) { $db->exec( 'ALTER TABLE tbl_sccharacteritems ADD INDEX idx_sccharacteritem_scobjs (cl_sccharacteritem_scobjs_id)' ); } if (!sccharacters_index_exists($db, 'tbl_sccharacteritems', 'idx_sccharacteritem_scitemcustom')) { $db->exec( 'ALTER TABLE tbl_sccharacteritems ADD INDEX idx_sccharacteritem_scitemcustom (cl_sccharacteritem_scitemcustom_id)' ); } if (!sccharacters_index_exists($db, 'tbl_sccharacteritems', 'idx_sccharacteritem_character_sort')) { $db->exec( 'ALTER TABLE tbl_sccharacteritems ADD INDEX idx_sccharacteritem_character_sort (cl_sccharacteritem_character_id, cl_sccharacteritem_sort_order, cl_sccharacteritem_id)' ); } if (!sccharacters_foreign_key_exists($db, 'tbl_sccharacteritems', 'fk_sccharacteritem_character')) { $db->exec( 'ALTER TABLE tbl_sccharacteritems ADD CONSTRAINT fk_sccharacteritem_character FOREIGN KEY (cl_sccharacteritem_character_id) REFERENCES tbl_sccharacters (cl_sccharacter_id) ON DELETE CASCADE ON UPDATE CASCADE' ); } if (!sccharacters_foreign_key_exists($db, 'tbl_sccharacteritems', 'fk_sccharacteritem_scobjs')) { $db->exec( 'ALTER TABLE tbl_sccharacteritems ADD CONSTRAINT fk_sccharacteritem_scobjs FOREIGN KEY (cl_sccharacteritem_scobjs_id) REFERENCES tbl_scobjs (cl_scobjs_id) ON DELETE SET NULL ON UPDATE CASCADE' ); } if (!sccharacters_foreign_key_exists($db, 'tbl_sccharacteritems', 'fk_sccharacteritem_scitemcustom')) { $db->exec( 'ALTER TABLE tbl_sccharacteritems ADD CONSTRAINT fk_sccharacteritem_scitemcustom FOREIGN KEY (cl_sccharacteritem_scitemcustom_id) REFERENCES tbl_scitemcustom (cl_scitemcustom_id) ON DELETE SET NULL ON UPDATE CASCADE' ); } $stmt_missing_tokens = $db->query( "SELECT cl_sccharacter_id FROM tbl_sccharacters WHERE cl_sccharacter_share_token IS NULL OR cl_sccharacter_share_token = ''" ); $stmt_update_token = $db->prepare( 'UPDATE tbl_sccharacters SET cl_sccharacter_share_token = :token WHERE cl_sccharacter_id = :id' ); foreach ($stmt_missing_tokens->fetchAll() as $row) { $stmt_update_token->execute([ 'token' => sccharacters_generate_share_token($db), 'id' => (int) $row['cl_sccharacter_id'], ]); } $bootstrapped = true; } function sccharacters_generate_share_token(PDO $db): string { do { $token = bin2hex(random_bytes(16)); $stmt = $db->prepare( 'SELECT cl_sccharacter_id FROM tbl_sccharacters WHERE cl_sccharacter_share_token = :token LIMIT 1' ); $stmt->execute(['token' => $token]); } while ($stmt->fetch()); return $token; } function sccharacters_reindex_character_items(PDO $db, int $character_id): void { if ($character_id <= 0) { return; } $stmt = $db->prepare( 'SELECT cl_sccharacteritem_id FROM tbl_sccharacteritems WHERE cl_sccharacteritem_character_id = :character_id ORDER BY CASE WHEN cl_sccharacteritem_sort_order <= 0 THEN 0 ELSE 1 END, cl_sccharacteritem_sort_order ASC, cl_sccharacteritem_id ASC' ); $stmt->execute(['character_id' => $character_id]); $item_ids = array_map('intval', $stmt->fetchAll(PDO::FETCH_COLUMN)); if ($item_ids === []) { return; } $stmt_update = $db->prepare( 'UPDATE tbl_sccharacteritems SET cl_sccharacteritem_sort_order = :sort_order WHERE cl_sccharacteritem_character_id = :character_id AND cl_sccharacteritem_id = :item_id' ); $db->beginTransaction(); try { $position = 1; foreach ($item_ids as $item_id) { $stmt_update->execute([ 'sort_order' => $position, 'character_id' => $character_id, 'item_id' => $item_id, ]); $position++; } $db->commit(); } catch (Throwable $exception) { if ($db->inTransaction()) { $db->rollBack(); } throw $exception; } } function sccharacters_next_item_sort_order(PDO $db, int $character_id): int { if ($character_id <= 0) { return 1; } sccharacters_reindex_character_items($db, $character_id); $stmt = $db->prepare( 'SELECT COALESCE(MAX(cl_sccharacteritem_sort_order), 0) FROM tbl_sccharacteritems WHERE cl_sccharacteritem_character_id = :character_id' ); $stmt->execute(['character_id' => $character_id]); return ((int) $stmt->fetchColumn()) + 1; } function sccharacters_item_category_options(): array { return [ 'weapon' => 'Armes', 'armor' => 'Armures', 'tools' => 'Outils', 'consumables' => 'Consommables', 'ammunition' => 'Munitions', 'attachments' => 'Accessoires', 'clothing' => 'Vêtements', 'cargo' => 'Cargo / Conteneurs', 'ship' => 'Composants / Véhicule', 'access' => 'Accès / Mobilier', 'misc' => 'Divers', ]; } function sccharacters_default_category_order(): array { return array_keys(sccharacters_item_category_options()); } function sccharacters_normalize_category_order(array $category_order): array { $known_categories = sccharacters_default_category_order(); $known_lookup = array_fill_keys($known_categories, true); $normalized = []; foreach ($category_order as $category_key) { $category_key = trim((string) $category_key); if ($category_key === '' || !isset($known_lookup[$category_key]) || isset($normalized[$category_key])) { continue; } $normalized[$category_key] = $category_key; } foreach ($known_categories as $category_key) { if (!isset($normalized[$category_key])) { $normalized[$category_key] = $category_key; } } return array_values($normalized); } function sccharacters_parse_category_order(?string $raw_value): array { $raw_value = trim((string) $raw_value); if ($raw_value === '') { return sccharacters_default_category_order(); } $decoded = json_decode($raw_value, true); if (!is_array($decoded)) { return sccharacters_default_category_order(); } return sccharacters_normalize_category_order($decoded); } function sccharacters_encode_category_order(array $category_order): string { $encoded = json_encode( sccharacters_normalize_category_order($category_order), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ); return $encoded !== false ? $encoded : json_encode(sccharacters_default_category_order(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); } function sccharacters_character_category_order(array $character_row): array { return sccharacters_parse_category_order((string) ($character_row['cl_sccharacter_category_order'] ?? '')); } function sccharacters_sort_items_by_category_order(array $items_by_category, array $category_order): array { if ($items_by_category === []) { return []; } $sorted = []; foreach (sccharacters_normalize_category_order($category_order) as $category_key) { if (isset($items_by_category[$category_key])) { $sorted[$category_key] = $items_by_category[$category_key]; } } foreach ($items_by_category as $category_key => $category_items) { if (!isset($sorted[$category_key])) { $sorted[$category_key] = $category_items; } } return $sorted; } function sccharacters_save_character_category_order(PDO $db, int $character_id, array $category_order): void { if ($character_id <= 0) { return; } $stmt = $db->prepare( 'UPDATE tbl_sccharacters SET cl_sccharacter_category_order = :category_order WHERE cl_sccharacter_id = :character_id' ); $stmt->execute([ 'category_order' => sccharacters_encode_category_order($category_order), 'character_id' => $character_id, ]); } function sccharacters_item_category_label(string $category): string { $options = sccharacters_item_category_options(); return $options[$category] ?? $options['misc']; } function sccharacters_item_category_slug(string $value): string { $value = trim($value); if ($value == '') { return ''; } if (function_exists('iconv')) { $converted = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value); if ($converted !== false) { $value = $converted; } } $value = strtolower($value); $value = preg_replace('/[^a-z0-9]+/', '_', $value) ?? ''; return trim($value, '_'); } function sccharacters_string_contains_any(string $haystack, array $needles): bool { foreach ($needles as $needle) { if ($needle !== '' && strpos($haystack, $needle) !== false) { return true; } } return false; } function sccharacters_normalize_item_category(?string $value): string { $slug = sccharacters_item_category_slug((string) $value); if ($slug === '' || in_array($slug, ['auto', 'automatic', 'automatique', 'default', 'defaut'], true)) { return ''; } $options = sccharacters_item_category_options(); if (isset($options[$slug])) { return $slug; } $exact_aliases = [ 'arme' => 'weapon', 'armes' => 'weapon', 'weapon' => 'weapon', 'weapons' => 'weapon', 'armure' => 'armor', 'armures' => 'armor', 'armor' => 'armor', 'armors' => 'armor', 'outil' => 'tools', 'outils' => 'tools', 'tool' => 'tools', 'tools' => 'tools', 'consommable' => 'consumables', 'consommables' => 'consumables', 'usable' => 'consumables', 'food' => 'consumables', 'drink' => 'consumables', 'munition' => 'ammunition', 'munitions' => 'ammunition', 'ammo' => 'ammunition', 'ammunition' => 'ammunition', 'accessoire' => 'attachments', 'accessoires' => 'attachments', 'attachment' => 'attachments', 'attachments' => 'attachments', 'vetement' => 'clothing', 'vetements' => 'clothing', 'clothing' => 'clothing', 'clothes' => 'clothing', 'cargo' => 'cargo', 'container' => 'cargo', 'conteneur' => 'cargo', 'conteneurs' => 'cargo', 'composant' => 'ship', 'composants' => 'ship', 'component' => 'ship', 'components' => 'ship', 'vaisseau' => 'ship', 'vaisseaux' => 'ship', 'vehicule' => 'ship', 'vehicules' => 'ship', 'acces' => 'access', 'access' => 'access', 'mobilier' => 'access', 'divers' => 'misc', 'misc' => 'misc', 'autre' => 'misc', 'autres' => 'misc', 'other' => 'misc', ]; if (isset($exact_aliases[$slug])) { return $exact_aliases[$slug]; } $contains_aliases = [ 'weapon' => ['arme', 'weapon', 'pistol', 'rifle', 'shotgun', 'sniper', 'knife', 'blade'], 'armor' => ['armure', 'armor', 'plating'], 'tools' => ['outil', 'tool', 'tractor', 'mining', 'multitool', 'multi_tool', 'gadget'], 'consumables' => ['consommable', 'consumable', 'usable', 'food', 'drink', 'med', 'medical', 'heal'], 'ammunition' => ['munition', 'ammo', 'ammunition', 'magazine', 'grenade', 'rocket'], 'attachments' => ['attachment', 'accessoire', 'scope', 'optic', 'silencer', 'sight', 'barrel'], 'clothing' => ['clothing', 'vetement', 'apparel', 'outfit', 'char_clothing', 'char_head'], 'cargo' => ['cargo', 'container', 'crate', 'box'], 'ship' => ['component', 'composant', 'vaisseau', 'vehicule', 'thruster', 'quantum', 'powerplant', 'cooler', 'radar', 'sensor', 'shield', 'turret', 'missilelauncher', 'docking', 'flightcontroller', 'fueltank', 'fuelintake', 'aimodule'], 'access' => ['door', 'seat', 'access', 'display', 'controlpanel', 'dashboard', 'mobilier'], ]; foreach ($contains_aliases as $category => $needles) { if (sccharacters_string_contains_any($slug, $needles)) { return $category; } } return ''; } function sccharacters_guess_item_category(?string $type, ?string $subtype = null): string { $haystack = strtolower(trim(((string) $type) . ' ' . ((string) $subtype))); $slug = sccharacters_item_category_slug($haystack); if ($slug === '') { return 'misc'; } if (sccharacters_string_contains_any($slug, ['char_clothing', 'char_head', 'clothing', 'apparel', 'outfit', 'hat', 'beard', 'piercing'])) { return 'clothing'; } if (sccharacters_string_contains_any($slug, ['weaponattachment', 'attachment', 'scope', 'optic', 'sight', 'silencer', 'barrel', 'mag_mount'])) { return 'attachments'; } if (sccharacters_string_contains_any($slug, ['missile', 'ammo', 'ammunition', 'magazine', 'rocket'])) { return 'ammunition'; } if (sccharacters_string_contains_any($slug, ['armor', 'armure'])) { return 'armor'; } if (sccharacters_string_contains_any($slug, ['weapon', 'gun', 'rifle', 'pistol', 'shotgun', 'sniper', 'knife', 'blade'])) { return 'weapon'; } if (sccharacters_string_contains_any($slug, ['usable', 'consumable', 'food', 'drink', 'fps_consumable', 'med', 'medical'])) { return 'consumables'; } if (sccharacters_string_contains_any($slug, ['tool', 'outil', 'mining', 'tractor', 'gadget', 'utility'])) { return 'tools'; } if (sccharacters_string_contains_any($slug, ['cargo', 'container', 'crate', 'box'])) { return 'cargo'; } if (sccharacters_string_contains_any($slug, ['door', 'seat', 'access', 'display', 'controlpanel', 'dashboard', 'shopdisplay', 'player'])) { return 'access'; } if (sccharacters_string_contains_any($slug, ['thruster', 'quantum', 'powerplant', 'cooler', 'radar', 'sensor', 'shield', 'turret', 'flightcontroller', 'fueltank', 'fuelintake', 'a_module', 'aimodule', 'docking', 'attachedpart'])) { return 'ship'; } return 'misc'; } function sccharacters_resolve_item_category(?string $storedValue, ?string $type, ?string $subtype = null): string { $normalized = sccharacters_normalize_item_category($storedValue); if ($normalized !== '') { return $normalized; } return sccharacters_guess_item_category($type, $subtype); }