39514-vm/db/sccharacters.php
Flatlogic Bot 2de33ba813 V1.2.3
2026-04-16 00:23:39 +00:00

659 lines
23 KiB
PHP

<?php
require_once __DIR__ . '/config.php';
function sccharacters_column_exists(PDO $db, string $table, string $column): bool
{
$stmt = $db->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);
}