diff --git a/ProserveAPI/htdocs/proserve/cache/.gitignore b/ProserveAPI/htdocs/proserve/cache/.gitignore new file mode 100644 index 0000000..e24a60f --- /dev/null +++ b/ProserveAPI/htdocs/proserve/cache/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!.htaccess diff --git a/ProserveAPI/htdocs/proserve/cache/.htaccess b/ProserveAPI/htdocs/proserve/cache/.htaccess new file mode 100644 index 0000000..6ebe562 --- /dev/null +++ b/ProserveAPI/htdocs/proserve/cache/.htaccess @@ -0,0 +1,4 @@ +Options -Indexes + + Require all denied + diff --git a/ProserveAPI/htdocs/proserve/config/cache.php b/ProserveAPI/htdocs/proserve/config/cache.php new file mode 100644 index 0000000..1d66082 --- /dev/null +++ b/ProserveAPI/htdocs/proserve/config/cache.php @@ -0,0 +1,109 @@ +/ : invalider une session +// se reduit a supprimer le dossier, sans risque de match partiel sur d'autres ids. +// Toutes les fonctions sont silencieuses en cas d'echec (best-effort, jamais bloquant). + +if (!defined('PROSERVE_CACHE_DIR')) + define('PROSERVE_CACHE_DIR', __DIR__ . '/../cache'); + +function cache_session_dir($sessionId) +{ + return PROSERVE_CACHE_DIR . '/sess_' . intval($sessionId); +} + +function cache_path($sessionId, $key) +{ + $safe = preg_replace('/[^a-zA-Z0-9_-]/', '_', $key); + return cache_session_dir($sessionId) . '/' . $safe . '.json'; +} + +function cache_get($sessionId, $key) +{ + if (intval($sessionId) <= 0) return null; + $path = cache_path($sessionId, $key); + if (!file_exists($path)) return null; + $data = @file_get_contents($path); + if ($data === false || $data === '') return null; + return $data; +} + +function cache_put($sessionId, $key, $jsonString) +{ + if (intval($sessionId) <= 0) return; + $dir = cache_session_dir($sessionId); + if (!is_dir($dir)) @mkdir($dir, 0755, true); + @file_put_contents(cache_path($sessionId, $key), $jsonString, LOCK_EX); +} + +function cache_invalidate_session($sessionId) +{ + $sid = intval($sessionId); + if ($sid <= 0) return; + $dir = cache_session_dir($sid); + if (!is_dir($dir)) return; + $files = glob($dir . '/*'); + if (is_array($files)) { + foreach ($files as $f) @unlink($f); + } + @rmdir($dir); +} + +// ============================================================================= +// DB-level cache for the materialized sessiondebriefs view (table session_debriefs_cache) +// Populated by stored procedure rebuild_debrief_cache(p_session_id) created in migration 1.7.0. +// ============================================================================= + +// Marque le cache DB d'une session comme stale (delete des lignes). +// Appele depuis les endpoints d'evenements (storetriggerevent, storereactevent) +// pour amortir le cout : la reconstruction se fait paresseusement au prochain +// read via ensure_session_debriefs_cached_db, ou explicitement via rebuild... +function invalidate_session_debrief_cache_db($db, $sessionId) +{ + $sid = intval($sessionId); + if ($sid <= 0 || $db === null) return; + try { + $stmt = $db->prepare("DELETE FROM session_debriefs_cache WHERE SessionId = ?"); + $stmt->execute([$sid]); + } catch (Exception $e) { + // best-effort, ne jamais bloquer la requete metier + } +} + +// Reconstruit immediatement le cache DB d'une session (DELETE + INSERT via la proc). +// Appele depuis session/stop, session/userleave, session/updateobjectives. +function rebuild_session_debrief_cache_db($db, $sessionId) +{ + $sid = intval($sessionId); + if ($sid <= 0 || $db === null) return; + try { + $stmt = $db->prepare("CALL rebuild_debrief_cache(?)"); + $stmt->execute([$sid]); + $stmt->closeCursor(); + } catch (Exception $e) { + // best-effort + } +} + +// Garantit que le cache DB contient au moins une ligne pour la session. +// Appele avant tout SELECT sur sessiondebriefs cote stats_object.php. +// Si le cache a ete invalide par un evenement et n'a pas encore ete reconstruit, +// cette fonction le reconstruit a la demande. +function ensure_session_debriefs_cached_db($db, $sessionId) +{ + $sid = intval($sessionId); + if ($sid <= 0 || $db === null) return; + try { + $stmt = $db->prepare("SELECT 1 FROM session_debriefs_cache WHERE SessionId = ? LIMIT 1"); + $stmt->execute([$sid]); + $exists = $stmt->fetchColumn(); + $stmt->closeCursor(); + if ($exists === false) { + rebuild_session_debrief_cache_db($db, $sid); + } + } catch (Exception $e) { + // si la table n'existe pas (migration 1.7.0 pas encore appliquee), + // on ne fait rien : le code legacy continue de marcher via la vue + } +} +?> diff --git a/ProserveAPI/htdocs/proserve/config/constants.php b/ProserveAPI/htdocs/proserve/config/constants.php index 5d7ea41..a8e035b 100644 --- a/ProserveAPI/htdocs/proserve/config/constants.php +++ b/ProserveAPI/htdocs/proserve/config/constants.php @@ -21,28 +21,30 @@ define("SESSIONDEBRIEFS_VIEW_NAME", "sessiondebriefs"); //////////////////////////////////////// query strings for SESSIONDEBRIEFS_VIEW //////////////////////////////////////// // SELECT clause -define("SESSIONDEBRIEFS_VIEW_QUERY_SELECT", +define("SESSIONDEBRIEFS_VIEW_QUERY_SELECT", "SELECT S.id AS SessionId, S.sessionType AS SessionTypeId, ST.displayName AS SessionType, S.sessionName AS SessionName, S.sessionDate AS SessionDate, " . "S.mapName AS MapName, S.scenarioName AS ScenarioName, S.success AS SessionSuccessful, S.timeToFinish AS SessionDuration, " . -"COALESCE(T.type, -1) AS TriggerTypeId, COALESCE(TT.displayName, '') AS TriggerType, (SELECT IFNULL(T.srcUserId, PS.userId)) AS ShooterId, US.username AS ShooterName, " . -"(SELECT IFNULL(PS.role, 3)) AS ShooterRoleId, (SELECT R.displayName FROM userroles R WHERE R.id = ShooterRoleId) AS ShooterRole, " . +"COALESCE(T.type, -1) AS TriggerTypeId, COALESCE(TT.displayName, '') AS TriggerType, IFNULL(T.srcUserId, PS.userId) AS ShooterId, US.username AS ShooterName, " . +"IFNULL(PS.role, 3) AS ShooterRoleId, URS.displayName AS ShooterRole, " . "COALESCE(T.indexCount, -1) AS ShotIndex, COALESCE(R.id, -1) AS ReactId, COALESCE(R.reactMode, 2) AS ReactModeId, COALESCE(RM.displayName, '') AS ReactMode, " . "COALESCE(R.reactType, -1) AS ReactTypeId, COALESCE(RT.displayName, '') AS ReactType, COALESCE(R.hitUserId, -1) AS TargetUserId, " . -"COALESCE(UH.username, '') AS TargetUserName, (SELECT IFNULL(PH.role, 3)) AS TargetRoleId, (SELECT R.displayName FROM userroles R WHERE R.id = TargetRoleId) AS TargetRole, " . +"COALESCE(UH.username, '') AS TargetUserName, IFNULL(PH.role, 3) AS TargetRoleId, URT.displayName AS TargetRole, " . "COALESCE(R.hitTargetName, '') AS TargetName, COALESCE(R.hitBoneName, '') AS TargetBoneName, COALESCE(R.targetKilled, 0) AS TargetKilled, " . "COALESCE(R.objectHitLocationX, 0) AS HitLocationX, COALESCE(R.objectHitLocationY, 0) AS HitLocationY, COALESCE(R.objectHitTagLocation, '') AS HitLocationTag, " . "COALESCE(R.hitPrecision, 0) AS HitPrecision, COALESCE(R.distance, 0) AS HitTargetDistance, COALESCE(R.reactTime, 0) AS ReactionTime, " . "COALESCE(R.timeStamp, 0) AS TimeStamp, COUNT(DISTINCT R.id) AS NbHit, COUNT(DISTINCT RK.srcEventIndex,RK.hitTargetName) AS NbKilled"); // FROM clause -define ("SESSIONDEBRIEFS_VIEW_QUERY_FROM", +define ("SESSIONDEBRIEFS_VIEW_QUERY_FROM", " FROM " . SESSIONS_TABLE_NAME . " S LEFT JOIN ". PARTICIPATES_TABLE_NAME . " PS ON (S.id = PS.sessionId) " . "LEFT JOIN ". PARTICIPATES_TABLE_NAME . " PH ON (S.id = PH.sessionId) LEFT JOIN " . TRIGGEREVENTS_TABLE_NAME . " T ON (S.id = T.sessionId) " . "LEFT JOIN " . TRIGGEREVENTTYPES_TABLE_NAME . " TT ON (TT.id = T.type) LEFT JOIN " . SESSIONTYPES_TABLE_NAME . " ST ON (ST.id = S.sessionType) " . "LEFT JOIN " . REACTEVENTS_TABLE_NAME . " R ON ( T.indexCount = R.srcEventIndex AND T.sessionId = R.srcEventSessionId ) " . "LEFT JOIN " . USERS_TABLE_NAME . " UH ON (UH.id = R.hitUserId) LEFT JOIN " . USERS_TABLE_NAME . " US ON (US.id = T.srcUserId OR US.id = PS.userId ) " . "LEFT JOIN " . REACTEVENTTYPES_TABLE_NAME . " RT ON (RT.id = R.reactType) LEFT JOIN " . REACTEVENTMODES_TABLE_NAME . " RM ON (RM.id = COALESCE(R.reactMode, 2)) " . -"LEFT JOIN " . REACTEVENTS_TABLE_NAME . " RK ON (R.id = RK.id AND RK.targetKilled = 1)"); +"LEFT JOIN " . REACTEVENTS_TABLE_NAME . " RK ON (R.id = RK.id AND RK.targetKilled = 1) " . +"LEFT JOIN " . USERROLES_TABLE_NAME . " URS ON (URS.id = IFNULL(PS.role, 3)) " . +"LEFT JOIN " . USERROLES_TABLE_NAME . " URT ON (URT.id = IFNULL(PH.role, 3))"); // GROUP BY clause define("SESSIONDEBRIEFS_VIEW_QUERY_GROUPBY", " GROUP BY SessionId,ShooterId,ShotIndex,TargetName"); diff --git a/ProserveAPI/htdocs/proserve/config/database.php b/ProserveAPI/htdocs/proserve/config/database.php index 67ab5a8..d4f2e02 100644 --- a/ProserveAPI/htdocs/proserve/config/database.php +++ b/ProserveAPI/htdocs/proserve/config/database.php @@ -7,22 +7,34 @@ class Database private $username = "root"; //UserName of Phpmyadmin private $password = ""; //Password associated with username public $conn; - + + // shared connection re-used across endpoint includes within a single request, + // and (with PDO::ATTR_PERSISTENT) across requests within an Apache child process. + private static $sharedConn = null; + // get the database connection public function getConnection() - { - $this->conn = null; - try - { - $this->conn = new PDO("mysql:host=" . $this->host . ";dbname=" . $this->db_name, $this->username, $this->password); - $this->conn->exec("set names utf8"); + { + if (self::$sharedConn === null) + { + try + { + self::$sharedConn = new PDO( + "mysql:host=" . $this->host . ";dbname=" . $this->db_name, + $this->username, + $this->password, + [PDO::ATTR_PERSISTENT => true] + ); + self::$sharedConn->exec("set names utf8"); + } + catch(PDOException $exception) + { + echo "Connection error: " . $exception->getMessage(); + return null; + } } - catch(PDOException $exception) - { - echo "Connection error: " . $exception->getMessage(); - } - - return $this->conn; + $this->conn = self::$sharedConn; + return self::$sharedConn; } } -?> \ No newline at end of file +?> diff --git a/ProserveAPI/htdocs/proserve/config/init.php b/ProserveAPI/htdocs/proserve/config/init.php index c7f7773..dca8f71 100644 --- a/ProserveAPI/htdocs/proserve/config/init.php +++ b/ProserveAPI/htdocs/proserve/config/init.php @@ -13,6 +13,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { // include database and object files include_once '../config/database.php'; +include_once '../config/cache.php'; // get database connection $database = new Database(); diff --git a/ProserveAPI/htdocs/proserve/debug/benchmark.php b/ProserveAPI/htdocs/proserve/debug/benchmark.php new file mode 100644 index 0000000..da777da --- /dev/null +++ b/ProserveAPI/htdocs/proserve/debug/benchmark.php @@ -0,0 +1,106 @@ +query("SELECT id FROM sessions ORDER BY id DESC LIMIT 1"); + $row = $stmt ? $stmt->fetch(PDO::FETCH_ASSOC) : null; + if ($row) $sessionId = intval($row['id']); +} +if ($userId <= 0) { + $stmt = $db->query("SELECT userId FROM participates WHERE sessionId=" . intval($sessionId) . " LIMIT 1"); + $row = $stmt ? $stmt->fetch(PDO::FETCH_ASSOC) : null; + if ($row) $userId = intval($row['userId']); +} + +// resolution de l'URL de base (meme host et meme path) +$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; +$host = $_SERVER['HTTP_HOST'] ?? 'localhost'; +$base = $scheme . '://' . $host . rtrim(dirname(dirname($_SERVER['SCRIPT_NAME'])), '/\\'); + +$endpoints = [ + 'session/debrief' => ['sessionId' => $sessionId, 'userId' => $userId], + 'session/get' => ['sessionId' => $sessionId], + 'stats/userhistory' => ['userId' => $userId, 'sessionId' => $sessionId, 'quickMode' => 'false'], + 'stats/get' => ['userId' => $userId, 'sessionId' => $sessionId], + 'lists/sessions_for_user' => ['userId' => $userId], + 'lists/users_in_session' => ['sessionId' => $sessionId], +]; + +function call_endpoint($url, $params) +{ + // ext-curl est dispo dans XAMPP par defaut et bcp plus rapide que + // file_get_contents+stream_context_create sur Windows (qui ajoute ~10ms par appel). + static $ch = null; + if ($ch === null) { + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_TIMEOUT => 60, + CURLOPT_FORBID_REUSE => false, + CURLOPT_FRESH_CONNECT => false, + ]); + } + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params)); + + $start = microtime(true); + $resp = curl_exec($ch); + $elapsed_ms = (microtime(true) - $start) * 1000; + $err = curl_errno($ch); + + return [ + 'ms' => round($elapsed_ms, 2), + 'bytes' => $resp === false ? 0 : strlen($resp), + 'ok' => $err === 0 && $resp !== false, + ]; +} + +function summarize($samples) +{ + $times = array_column($samples, 'ms'); + sort($times); + $n = count($times); + return [ + 'min' => $times[0], + 'max' => $times[$n - 1], + 'median' => $times[intval($n / 2)], + 'avg' => round(array_sum($times) / $n, 2), + 'bytes' => $samples[0]['bytes'], + 'ok' => count(array_filter(array_column($samples, 'ok'))) === $n, + ]; +} + +$results = [ + 'context' => [ + 'userId' => $userId, + 'sessionId' => $sessionId, + 'iterations' => $iterations, + 'base_url' => $base, + 'timestamp' => date('c'), + ], + 'endpoints' => [], +]; + +foreach ($endpoints as $path => $params) { + $url = $base . '/' . $path . '.php'; + $samples = []; + for ($i = 0; $i < $iterations; $i++) { + $samples[] = call_endpoint($url, $params); + } + $results['endpoints'][$path] = summarize($samples); +} + +print_r(json_encode($results, JSON_PRETTY_PRINT)); +?> diff --git a/ProserveAPI/htdocs/proserve/events/storereactevent.php b/ProserveAPI/htdocs/proserve/events/storereactevent.php index dbb1110..99db948 100644 --- a/ProserveAPI/htdocs/proserve/events/storereactevent.php +++ b/ProserveAPI/htdocs/proserve/events/storereactevent.php @@ -40,6 +40,8 @@ $event->timeStamp = $_POST['timestamp']; // create reactevent if ($event->record()) { + cache_invalidate_session($event->srcEventSessionId); + invalidate_session_debrief_cache_db($db, $event->srcEventSessionId); $evt_arr = $event->getResultArray(true, "Event_Stored_OK"); print_r(json_encode($evt_arr)); // OK } diff --git a/ProserveAPI/htdocs/proserve/events/storetriggerevent.php b/ProserveAPI/htdocs/proserve/events/storetriggerevent.php index fb4ac1d..761fc11 100644 --- a/ProserveAPI/htdocs/proserve/events/storetriggerevent.php +++ b/ProserveAPI/htdocs/proserve/events/storetriggerevent.php @@ -29,6 +29,8 @@ $event->successful = $_POST['success']; // create trigger event if ($event->record()) { + cache_invalidate_session($event->sessionId); + invalidate_session_debrief_cache_db($db, $event->sessionId); $evt_arr = $event->getResultArray(true, "Event_Stored_OK"); print_r(json_encode($evt_arr)); // OK } diff --git a/ProserveAPI/htdocs/proserve/lists/sessions_for_user.php b/ProserveAPI/htdocs/proserve/lists/sessions_for_user.php index 5317d43..41ea858 100644 --- a/ProserveAPI/htdocs/proserve/lists/sessions_for_user.php +++ b/ProserveAPI/htdocs/proserve/lists/sessions_for_user.php @@ -6,9 +6,13 @@ include_once '../objects/stats_object.php'; $userId = isset($_POST['userId']) ? $_POST['userId'] : -1; $typeId = isset($_POST['typeId']) ? $_POST['typeId'] : -1; +// Optional pagination : Unreal n'envoie pas ces champs (comportement legacy preserve), +// le futur web UI peut les passer pour ne pas charger toutes les sessions d'un coup. +$limit = isset($_POST['limit']) ? intval($_POST['limit']) : -1; +$offset = isset($_POST['offset']) ? intval($_POST['offset']) : 0; $stats = new StatsObject($db); -$stats->getSessionsForUser($userId, $typeId); +$stats->getSessionsForUser($userId, $typeId, $limit, $offset); $stats_arr = $stats->getResultArray(true, "User_Sessions_List_OK"); print_r(json_encode($stats_arr)); // OK diff --git a/ProserveAPI/htdocs/proserve/objects/stats_object.php b/ProserveAPI/htdocs/proserve/objects/stats_object.php index e5985c9..7f17ca1 100644 --- a/ProserveAPI/htdocs/proserve/objects/stats_object.php +++ b/ProserveAPI/htdocs/proserve/objects/stats_object.php @@ -118,7 +118,8 @@ class StatsObject extends DBTableObject //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // get sessions of given type that given user has taken part in - public function getRawSessions ($userId=-1, $typeId=-1) + // $limit and $offset are optional : when $limit <= 0 the full list is returned (legacy behavior) + public function getRawSessions ($userId=-1, $typeId=-1, $limit=-1, $offset=0) { // Check if we are looking for a specific user and/or type of session $userConstraint = $userId > 0 ? " AND P.userId=" . $userId : ""; @@ -126,11 +127,17 @@ class StatsObject extends DBTableObject // Order results from newest to oldest session $orderConstraint = " ORDER BY SD.id DESC"; - - //$query = "SELECT DISTINCT S.* FROM " . SESSIONS_TABLE_NAME . " S LEFT JOIN " . PARTICIPATES_TABLE_NAME . " P ON S.id = P.sessionId WHERE 1 " . + + // Optional pagination : SQL-level LIMIT/OFFSET when caller provided a positive limit + $limitConstraint = ""; + if (intval($limit) > 0) { + $limitConstraint = " LIMIT " . intval($limit) . " OFFSET " . max(0, intval($offset)); + } + + //$query = "SELECT DISTINCT S.* FROM " . SESSIONS_TABLE_NAME . " S LEFT JOIN " . PARTICIPATES_TABLE_NAME . " P ON S.id = P.sessionId WHERE 1 " . $query = "SELECT DISTINCT SD.* FROM " . SESSIONS_TABLE_NAME . " SD, " . PARTICIPATES_TABLE_NAME . " P WHERE SD.id = P.sessionId " . - $this->getCalibrationSessionsConstraint("SD") . $this->getSessionDurationConstraint("SD") . $userConstraint . $typeConstraint . $orderConstraint; - + $this->getCalibrationSessionsConstraint("SD") . $this->getSessionDurationConstraint("SD") . $userConstraint . $typeConstraint . $orderConstraint . $limitConstraint; + // prepare and execute query $stmt = $this->conn->prepare($query); $stmt->execute(); @@ -141,11 +148,11 @@ class StatsObject extends DBTableObject //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // get sessions that given user has taken part in - public function getSessionsForUser ($userId, $typeId=-1) + public function getSessionsForUser ($userId, $typeId=-1, $limit=-1, $offset=0) { // Check if we are looking for a specific user and/or session - $stmt = $this->getRawSessions($userId, $typeId); - + $stmt = $this->getRawSessions($userId, $typeId, $limit, $offset); + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { //$sess = new GameSession($this->conn); @@ -469,11 +476,16 @@ class StatsObject extends DBTableObject } else { + // Lazy rebuild du cache materialise pour cette session avant le SELECT FROM sessiondebriefs + if ($sessionId > 0 && function_exists('ensure_session_debriefs_cached_db')) { + ensure_session_debriefs_cached_db($this->conn, $sessionId); + } + // create SQL constraints for query $sessionConstraint = $sessionId > 0 ? " AND SD.SessionId = " . $sessionId : ""; $userConstraint = $userId >= 0 ? " AND SD.ShooterId = " . $userId : ""; $userSubConstraint = $userId >= 0 ? " AND P.UserId = " . $userId : ""; - + // get raw results from query $sessionStats = $this->getRawStatsForSession ($sessionId, $userId, -1); @@ -692,9 +704,15 @@ class StatsObject extends DBTableObject // debrief mission : get given users results in given session public function getResultsForSession ($sessionId, $userId, $fromUserId=-1) { + // Lazy rebuild du cache materialise (table session_debriefs_cache) + // si la session a ete invalidee par un evenement et pas encore reconstruite + if ($sessionId > 0 && function_exists('ensure_session_debriefs_cached_db')) { + ensure_session_debriefs_cached_db($this->conn, $sessionId); + } + $userConstraint = $userId > 0 ? " AND SD.ShooterId=" . $userId : ""; $sessionConstraint = $sessionId > 0 ? " AND SD.SessionId=" . $sessionId : ""; - + $query = "SELECT SD.* FROM " . SESSIONDEBRIEFS_VIEW_NAME . " SD WHERE 1 " . $this->getCalibrationSessionsConstraint() . $userConstraint . $sessionConstraint . " GROUP BY SD.SessionId, SD.ShooterId, SD.ShotIndex"; //$query = "SELECT SD.* FROM (" . SESSIONDEBRIEFS_VIEW_QUERY . ") AS SD WHERE 1 " . $userConstraint . $sessionConstraint . " GROUP BY SD.SessionId, SD.ShooterId, SD.ShotIndex"; diff --git a/ProserveAPI/htdocs/proserve/proserveapi.sql b/ProserveAPI/htdocs/proserve/proserveapi.sql index c813774..d8de49e 100644 --- a/ProserveAPI/htdocs/proserve/proserveapi.sql +++ b/ProserveAPI/htdocs/proserve/proserveapi.sql @@ -325,7 +325,61 @@ INSERT INTO `userstatus` (`id`, `displayName`, `description`) VALUES -- DROP TABLE IF EXISTS `sessiondebriefs`; -CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `sessiondebriefs` AS SELECT `s`.`id` AS `SessionId`, `s`.`sessionType` AS `SessionTypeId`, `st`.`displayName` AS `SessionType`, `s`.`sessionName` AS `SessionName`, `s`.`sessionDate` AS `SessionDate`, `s`.`mapName` AS `MapName`, `s`.`scenarioName` AS `ScenarioName`, `s`.`success` AS `SessionSuccessful`, `s`.`timeToFinish` AS `SessionDuration`, coalesce(`t`.`type`,-1) AS `TriggerTypeId`, coalesce(`tt`.`displayName`,'') AS `TriggerType`, (select ifnull(`t`.`srcUserId`,`ps`.`userId`)) AS `ShooterId`, `us`.`username` AS `ShooterName`, (select ifnull(`ps`.`role`,3)) AS `ShooterRoleId`, (select `r`.`displayName` from `userroles` `r` where `r`.`id` = `ShooterRoleId`) AS `ShooterRole`, coalesce(`t`.`indexCount`,-1) AS `ShotIndex`, coalesce(`r`.`id`,-1) AS `ReactId`, coalesce(`r`.`reactMode`,2) AS `ReactModeId`, coalesce(`rm`.`displayName`,'') AS `ReactMode`, coalesce(`r`.`reactType`,-1) AS `ReactTypeId`, coalesce(`rt`.`displayName`,'') AS `ReactType`, coalesce(`r`.`hitUserId`,-1) AS `TargetUserId`, coalesce(`uh`.`username`,'') AS `TargetUserName`, (select ifnull(`ph`.`role`,3)) AS `TargetRoleId`, (select `r`.`displayName` from `userroles` `r` where `r`.`id` = `TargetRoleId`) AS `TargetRole`, coalesce(`r`.`hitTargetName`,'') AS `TargetName`, coalesce(`r`.`hitBoneName`,'') AS `TargetBoneName`, coalesce(`r`.`targetKilled`,0) AS `TargetKilled`, coalesce(`r`.`objectHitLocationX`,0) AS `HitLocationX`, coalesce(`r`.`objectHitLocationY`,0) AS `HitLocationY`, coalesce(`r`.`objectHitTagLocation`,'') AS `HitLocationTag`, coalesce(`r`.`hitPrecision`,0) AS `HitPrecision`, coalesce(`r`.`distance`,0) AS `HitTargetDistance`, coalesce(`r`.`reactTime`,0) AS `ReactionTime`, coalesce(`r`.`timeStamp`,0) AS `TimeStamp`, count(distinct `r`.`id`) AS `NbHit`, count(distinct `rk`.`srcEventIndex`,`rk`.`hitTargetName`) AS `NbKilled` FROM (((((((((((`sessions` `s` left join `participates` `ps` on(`s`.`id` = `ps`.`sessionId`)) left join `participates` `ph` on(`s`.`id` = `ph`.`sessionId`)) left join `triggerevents` `t` on(`s`.`id` = `t`.`sessionId`)) left join `triggereventtypes` `tt` on(`tt`.`id` = `t`.`type`)) left join `sessiontypes` `st` on(`st`.`id` = `s`.`sessionType`)) left join `reactevents` `r` on(`t`.`indexCount` = `r`.`srcEventIndex` and `t`.`sessionId` = `r`.`srcEventSessionId`)) left join `users` `uh` on(`uh`.`id` = `r`.`hitUserId`)) left join `users` `us` on(`us`.`id` = `t`.`srcUserId` or `us`.`id` = `ps`.`userId`)) left join `reacteventtypes` `rt` on(`rt`.`id` = `r`.`reactType`)) left join `reacteventmodes` `rm` on(`rm`.`id` = coalesce(`r`.`reactMode`,2))) left join `reactevents` `rk` on(`r`.`id` = `rk`.`id` and `rk`.`targetKilled` = 1)) GROUP BY `s`.`id`, (select ifnull(`t`.`srcUserId`,`ps`.`userId`)), `t`.`indexCount`, `r`.`hitTargetName` ORDER BY `s`.`id` ASC, (select ifnull(`t`.`srcUserId`,`ps`.`userId`)) ASC, `t`.`indexCount` ASC, `r`.`id` ASC ; +CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`localhost` SQL SECURITY DEFINER VIEW `sessiondebriefs` AS +SELECT + `s`.`id` AS `SessionId`, + `s`.`sessionType` AS `SessionTypeId`, + `st`.`displayName` AS `SessionType`, + `s`.`sessionName` AS `SessionName`, + `s`.`sessionDate` AS `SessionDate`, + `s`.`mapName` AS `MapName`, + `s`.`scenarioName` AS `ScenarioName`, + `s`.`success` AS `SessionSuccessful`, + `s`.`timeToFinish` AS `SessionDuration`, + COALESCE(`t`.`type`, -1) AS `TriggerTypeId`, + COALESCE(`tt`.`displayName`, '') AS `TriggerType`, + IFNULL(`t`.`srcUserId`, `ps`.`userId`) AS `ShooterId`, + `us`.`username` AS `ShooterName`, + IFNULL(`ps`.`role`, 3) AS `ShooterRoleId`, + `urs`.`displayName` AS `ShooterRole`, + COALESCE(`t`.`indexCount`, -1) AS `ShotIndex`, + COALESCE(`r`.`id`, -1) AS `ReactId`, + COALESCE(`r`.`reactMode`, 2) AS `ReactModeId`, + COALESCE(`rm`.`displayName`, '') AS `ReactMode`, + COALESCE(`r`.`reactType`, -1) AS `ReactTypeId`, + COALESCE(`rt`.`displayName`, '') AS `ReactType`, + COALESCE(`r`.`hitUserId`, -1) AS `TargetUserId`, + COALESCE(`uh`.`username`, '') AS `TargetUserName`, + IFNULL(`ph`.`role`, 3) AS `TargetRoleId`, + `urt`.`displayName` AS `TargetRole`, + COALESCE(`r`.`hitTargetName`, '') AS `TargetName`, + COALESCE(`r`.`hitBoneName`, '') AS `TargetBoneName`, + COALESCE(`r`.`targetKilled`, 0) AS `TargetKilled`, + COALESCE(`r`.`objectHitLocationX`, 0) AS `HitLocationX`, + COALESCE(`r`.`objectHitLocationY`, 0) AS `HitLocationY`, + COALESCE(`r`.`objectHitTagLocation`, '') AS `HitLocationTag`, + COALESCE(`r`.`hitPrecision`, 0) AS `HitPrecision`, + COALESCE(`r`.`distance`, 0) AS `HitTargetDistance`, + COALESCE(`r`.`reactTime`, 0) AS `ReactionTime`, + COALESCE(`r`.`timeStamp`, 0) AS `TimeStamp`, + COUNT(DISTINCT `r`.`id`) AS `NbHit`, + COUNT(DISTINCT `rk`.`srcEventIndex`, `rk`.`hitTargetName`) AS `NbKilled` +FROM `sessions` `s` +LEFT JOIN `participates` `ps` ON (`s`.`id` = `ps`.`sessionId`) +LEFT JOIN `participates` `ph` ON (`s`.`id` = `ph`.`sessionId`) +LEFT JOIN `triggerevents` `t` ON (`s`.`id` = `t`.`sessionId`) +LEFT JOIN `triggereventtypes` `tt` ON (`tt`.`id` = `t`.`type`) +LEFT JOIN `sessiontypes` `st` ON (`st`.`id` = `s`.`sessionType`) +LEFT JOIN `reactevents` `r` ON (`t`.`indexCount` = `r`.`srcEventIndex` AND `t`.`sessionId` = `r`.`srcEventSessionId`) +LEFT JOIN `users` `uh` ON (`uh`.`id` = `r`.`hitUserId`) +LEFT JOIN `users` `us` ON (`us`.`id` = `t`.`srcUserId` OR `us`.`id` = `ps`.`userId`) +LEFT JOIN `reacteventtypes` `rt` ON (`rt`.`id` = `r`.`reactType`) +LEFT JOIN `reacteventmodes` `rm` ON (`rm`.`id` = COALESCE(`r`.`reactMode`, 2)) +LEFT JOIN `reactevents` `rk` ON (`r`.`id` = `rk`.`id` AND `rk`.`targetKilled` = 1) +LEFT JOIN `userroles` `urs` ON (`urs`.`id` = IFNULL(`ps`.`role`, 3)) +LEFT JOIN `userroles` `urt` ON (`urt`.`id` = IFNULL(`ph`.`role`, 3)) +GROUP BY `SessionId`, `ShooterId`, `ShotIndex`, `TargetName` +ORDER BY `SessionId`, `ShooterId`, `ShotIndex`, `ReactId`; -- -- Index pour les tables déchargées diff --git a/ProserveAPI/htdocs/proserve/session/debrief.php b/ProserveAPI/htdocs/proserve/session/debrief.php index a211bc4..13c0986 100644 --- a/ProserveAPI/htdocs/proserve/session/debrief.php +++ b/ProserveAPI/htdocs/proserve/session/debrief.php @@ -16,12 +16,22 @@ $userId = isset($_POST['userId']) ? $_POST['userId'] : -1; // check sessionId is > 0 //if ($sessionId > 0) //{ - // prepare user object - $stats = new StatsObject($db); - $stats->getStatsForSession($sessionId, $userId); - - $stats_arr = $stats->getResultArray(true, "Stats_Collected_OK"); - print_r(json_encode($stats_arr)); // OK + $cacheKey = "debrief_" . intval($userId); + $cached = cache_get($sessionId, $cacheKey); + if ($cached !== null) + { + print_r($cached); + } + else + { + $stats = new StatsObject($db); + $stats->getStatsForSession($sessionId, $userId); + + $stats_arr = $stats->getResultArray(true, "Stats_Collected_OK"); + $json = json_encode($stats_arr); + cache_put($sessionId, $cacheKey, $json); + print_r($json); // OK + } //} //else trigger_error("Invalid session id"); ?> \ No newline at end of file diff --git a/ProserveAPI/htdocs/proserve/session/get.php b/ProserveAPI/htdocs/proserve/session/get.php index 07a9da0..978ccbe 100644 --- a/ProserveAPI/htdocs/proserve/session/get.php +++ b/ProserveAPI/htdocs/proserve/session/get.php @@ -12,11 +12,17 @@ if (isset($_POST['sessionId'])) $gameSession->id = $_POST['sessionId']; else trigger_error( missing_parameter_error("sessionId") ); -// create the game session -if ($gameSession->load()) +$cached = cache_get($gameSession->id, "session"); +if ($cached !== null) +{ + print_r($cached); +} +else if ($gameSession->load()) { $session_arr = $gameSession->getResultArray(true, "Session_Found"); - print_r(json_encode($session_arr)); // OK + $json = json_encode($session_arr); + cache_put($gameSession->id, "session", $json); + print_r($json); // OK } else trigger_error("Error_Occured"); ?> \ No newline at end of file diff --git a/ProserveAPI/htdocs/proserve/session/stop.php b/ProserveAPI/htdocs/proserve/session/stop.php index 3de1707..e76b9f7 100644 --- a/ProserveAPI/htdocs/proserve/session/stop.php +++ b/ProserveAPI/htdocs/proserve/session/stop.php @@ -20,6 +20,8 @@ $gameSession->score = $_POST['score']; // close the game session if ($gameSession->stop()) { + cache_invalidate_session($gameSession->id); + rebuild_session_debrief_cache_db($db, $gameSession->id); $session_arr = $gameSession->getResultArray(true, "Session_Stop_OK"); print_r(json_encode($session_arr)); // OK } diff --git a/ProserveAPI/htdocs/proserve/session/updateobjectives.php b/ProserveAPI/htdocs/proserve/session/updateobjectives.php index 6ebc463..9cb743e 100644 --- a/ProserveAPI/htdocs/proserve/session/updateobjectives.php +++ b/ProserveAPI/htdocs/proserve/session/updateobjectives.php @@ -42,6 +42,8 @@ if ($participation->load()) // update objectives of user in the game session if ($participation->updateObjectives()) { + cache_invalidate_session($participation->sessionId); + rebuild_session_debrief_cache_db($db, $participation->sessionId); $participation_arr = $participation->getResultArray(true, "Results_Saved_OK"); print_r(json_encode($participation_arr)); // OK } diff --git a/ProserveAPI/htdocs/proserve/session/userleave.php b/ProserveAPI/htdocs/proserve/session/userleave.php index 5b9186e..f07834f 100644 --- a/ProserveAPI/htdocs/proserve/session/userleave.php +++ b/ProserveAPI/htdocs/proserve/session/userleave.php @@ -25,6 +25,8 @@ if ($participation->userLeaves()) // update score and averages if ($participation->updateUserScores()) { + cache_invalidate_session($participation->sessionId); + rebuild_session_debrief_cache_db($db, $participation->sessionId); $participation_arr = $participation->getResultArray(true, "User_Left_OK"); print_r(json_encode($participation_arr)); // OK } diff --git a/ProserveAPI/htdocs/proserve/stats/userhistory.php b/ProserveAPI/htdocs/proserve/stats/userhistory.php index 35b4e48..2897738 100644 --- a/ProserveAPI/htdocs/proserve/stats/userhistory.php +++ b/ProserveAPI/htdocs/proserve/stats/userhistory.php @@ -21,10 +21,21 @@ if (isset($_POST['quickMode'])) //if ($userId > 0 && $sessionId > 0) { - $stats = new StatsObject($db); - $stats->getUserHistory($userId, $sessionId, $quickMode); + $cacheKey = "userhistory_" . intval($userId) . "_" . intval($quickMode); + $cached = cache_get($sessionId, $cacheKey); + if ($cached !== null) + { + print_r($cached); + } + else + { + $stats = new StatsObject($db); + $stats->getUserHistory($userId, $sessionId, $quickMode); - $stats_arr = $stats->getResultArray(true, "Results_Collected_OK"); - print_r(json_encode($stats_arr)); // OK + $stats_arr = $stats->getResultArray(true, "Results_Collected_OK"); + $json = json_encode($stats_arr); + cache_put($sessionId, $cacheKey, $json); + print_r($json); // OK + } } ?> \ No newline at end of file diff --git a/ProserveAPI/htdocs/proserve/update_proserve_db 1.6.0.sql b/ProserveAPI/htdocs/proserve/update_proserve_db 1.6.0.sql new file mode 100644 index 0000000..7418886 --- /dev/null +++ b/ProserveAPI/htdocs/proserve/update_proserve_db 1.6.0.sql @@ -0,0 +1,126 @@ +-- ============================================================ +-- Migration 1.6.0 - Optimisation des temps de reponse +-- ============================================================ +-- Cible : MariaDB 10.4+ (XAMPP local). +-- Idempotent : ce fichier peut etre rejoue sans erreur sur n'importe quel poste. +-- Aucun changement de structure : seuls des index sont ajoutes +-- (via une procedure stockee qui teste leur presence) et la vue +-- sessiondebriefs est redefinie via CREATE OR REPLACE VIEW. +-- ============================================================ + +USE proserveapi; + +-- ------------------------------------------------------------ +-- Procedure helper : ajoute un index seulement s'il n'existe pas +-- (CREATE INDEX IF NOT EXISTS n'est dispo qu'a partir de MariaDB 10.5.4) +-- ------------------------------------------------------------ +DROP PROCEDURE IF EXISTS create_index_if_missing; + +DELIMITER $$ +CREATE PROCEDURE create_index_if_missing( + IN p_table VARCHAR(64), + IN p_index VARCHAR(64), + IN p_cols VARCHAR(255) +) +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() + AND TABLE_NAME = p_table + AND INDEX_NAME = p_index + ) THEN + SET @sql = CONCAT('ALTER TABLE `', p_table, '` ADD INDEX `', p_index, '` (', p_cols, ')'); + PREPARE stmt FROM @sql; + EXECUTE stmt; + DEALLOCATE PREPARE stmt; + END IF; +END$$ +DELIMITER ; + +-- ------------------------------------------------------------ +-- Index manquants identifies a l'analyse des requetes lourdes +-- ------------------------------------------------------------ + +-- JOIN reactevents <-> triggerevents : la cle composite la plus utilisee +CALL create_index_if_missing('reactevents', 'idx_react_session_event', '`srcEventSessionId`, `srcEventIndex`'); + +-- Filtres "tirs d'un user dans une session" (stats_object getShotsFiredByUser, getTargetKilledInSession) +CALL create_index_if_missing('triggerevents', 'idx_trigger_session_user', '`sessionId`, `srcUserId`'); + +-- Agregation NbKilled (auto-jointure RK dans la vue sessiondebriefs) +CALL create_index_if_missing('reactevents', 'idx_react_killed', '`targetKilled`, `srcEventSessionId`'); + +-- Tri/filtrage des sessions par date (listes recentes) +CALL create_index_if_missing('sessions', 'idx_sessions_date', '`sessionDate` DESC'); + +-- Nettoyage de la procedure helper +DROP PROCEDURE create_index_if_missing; + + +-- ------------------------------------------------------------ +-- Redefinition de la vue sessiondebriefs (idempotent via CREATE OR REPLACE) +-- ------------------------------------------------------------ +-- Modifications par rapport a la definition d'origine : +-- - Suppression des wraps "(SELECT IFNULL(...))" qui forcaient une evaluation par row +-- -> remplaces par IFNULL(...) directement (semantique identique). +-- - Suppression des sous-requetes correlees (SELECT R.displayName FROM userroles R WHERE R.id = ...) +-- -> remplacees par deux LEFT JOIN explicites sur userroles (URS et URT). +-- - Tous les autres JOIN, le GROUP BY et l'ORDER BY restent strictement identiques +-- pour preserver le contrat de sortie (memes colonnes, memes lignes, memes valeurs). +-- ------------------------------------------------------------ + +CREATE OR REPLACE VIEW sessiondebriefs AS +SELECT + S.id AS SessionId, + S.sessionType AS SessionTypeId, + ST.displayName AS SessionType, + S.sessionName AS SessionName, + S.sessionDate AS SessionDate, + S.mapName AS MapName, + S.scenarioName AS ScenarioName, + S.success AS SessionSuccessful, + S.timeToFinish AS SessionDuration, + COALESCE(T.type, -1) AS TriggerTypeId, + COALESCE(TT.displayName, '') AS TriggerType, + IFNULL(T.srcUserId, PS.userId) AS ShooterId, + US.username AS ShooterName, + IFNULL(PS.role, 3) AS ShooterRoleId, + URS.displayName AS ShooterRole, + COALESCE(T.indexCount, -1) AS ShotIndex, + COALESCE(R.id, -1) AS ReactId, + COALESCE(R.reactMode, 2) AS ReactModeId, + COALESCE(RM.displayName, '') AS ReactMode, + COALESCE(R.reactType, -1) AS ReactTypeId, + COALESCE(RT.displayName, '') AS ReactType, + COALESCE(R.hitUserId, -1) AS TargetUserId, + COALESCE(UH.username, '') AS TargetUserName, + IFNULL(PH.role, 3) AS TargetRoleId, + URT.displayName AS TargetRole, + COALESCE(R.hitTargetName, '') AS TargetName, + COALESCE(R.hitBoneName, '') AS TargetBoneName, + COALESCE(R.targetKilled, 0) AS TargetKilled, + COALESCE(R.objectHitLocationX, 0) AS HitLocationX, + COALESCE(R.objectHitLocationY, 0) AS HitLocationY, + COALESCE(R.objectHitTagLocation, '') AS HitLocationTag, + COALESCE(R.hitPrecision, 0) AS HitPrecision, + COALESCE(R.distance, 0) AS HitTargetDistance, + COALESCE(R.reactTime, 0) AS ReactionTime, + COALESCE(R.timeStamp, 0) AS TimeStamp, + COUNT(DISTINCT R.id) AS NbHit, + COUNT(DISTINCT RK.srcEventIndex, RK.hitTargetName) AS NbKilled +FROM sessions S +LEFT JOIN participates PS ON (S.id = PS.sessionId) +LEFT JOIN participates PH ON (S.id = PH.sessionId) +LEFT JOIN triggerevents T ON (S.id = T.sessionId) +LEFT JOIN triggereventtypes TT ON (TT.id = T.type) +LEFT JOIN sessiontypes ST ON (ST.id = S.sessionType) +LEFT JOIN reactevents R ON (T.indexCount = R.srcEventIndex AND T.sessionId = R.srcEventSessionId) +LEFT JOIN users UH ON (UH.id = R.hitUserId) +LEFT JOIN users US ON (US.id = T.srcUserId OR US.id = PS.userId) +LEFT JOIN reacteventtypes RT ON (RT.id = R.reactType) +LEFT JOIN reacteventmodes RM ON (RM.id = COALESCE(R.reactMode, 2)) +LEFT JOIN reactevents RK ON (R.id = RK.id AND RK.targetKilled = 1) +LEFT JOIN userroles URS ON (URS.id = IFNULL(PS.role, 3)) +LEFT JOIN userroles URT ON (URT.id = IFNULL(PH.role, 3)) +GROUP BY SessionId, ShooterId, ShotIndex, TargetName +ORDER BY SessionId, ShooterId, ShotIndex, ReactId; diff --git a/ProserveAPI/htdocs/proserve/update_proserve_db 1.7.0.sql b/ProserveAPI/htdocs/proserve/update_proserve_db 1.7.0.sql new file mode 100644 index 0000000..082c7b1 --- /dev/null +++ b/ProserveAPI/htdocs/proserve/update_proserve_db 1.7.0.sql @@ -0,0 +1,236 @@ +-- ============================================================ +-- Migration 1.7.0 - Materialisation de la vue sessiondebriefs +-- ============================================================ +-- Cible : MariaDB 10.4+. +-- Idempotent : peut etre rejoue sans erreur. Le cache est entierement +-- reconstruit a chaque execution (TRUNCATE + backfill). +-- +-- Changement principal : la vue sessiondebriefs etait recalculee a chaque +-- requete (12 LEFT JOIN + GROUP BY + sous-requetes correlees). Elle est +-- desormais materialisee dans une vraie table session_debriefs_cache, +-- peuplee : +-- - en bulk au moment de cette migration (backfill toutes sessions), +-- - puis par une procedure stockee rebuild_debrief_cache(p_session_id) +-- appelee depuis le code PHP a chaque modification de session +-- (session/stop, session/userleave, session/updateobjectives, +-- events/storetriggerevent, events/storereactevent). +-- +-- La vue sessiondebriefs garde son nom et ses colonnes : le code consommateur +-- (stats_object.php : getResultsForSession, getUserHistory) ne change pas. +-- Elle devient un simple passe-plat sur la table cache. +-- +-- Action 2 (fix partiel du double JOIN participates) : la jointure PH +-- (qui sert a determiner TargetRoleId/TargetRole) est desormais contrainte +-- sur PH.userId = R.hitUserId au lieu de matcher tous les participants. +-- Effet : +-- * Pour les hits user-on-user : meme resultat (correct par construction). +-- * Pour les hits NPC/objets : TargetRoleId vaut 3 (IA) au lieu d'un +-- role aleatoire d'un autre participant. C'est un fix de bug. +-- La jointure PS reste inchangee (preserve le fallback ShooterId quand T est NULL). +-- ============================================================ + +USE proserveapi; + +-- ------------------------------------------------------------ +-- 1) Table de cache (creation idempotente) +-- ------------------------------------------------------------ +CREATE TABLE IF NOT EXISTS `session_debriefs_cache` ( + `SessionId` int(11) NOT NULL, + `SessionTypeId` int(11) DEFAULT NULL, + `SessionType` varchar(255) DEFAULT NULL, + `SessionName` varchar(255) DEFAULT NULL, + `SessionDate` datetime DEFAULT NULL, + `MapName` varchar(255) DEFAULT NULL, + `ScenarioName` varchar(255) DEFAULT NULL, + `SessionSuccessful` tinyint(1) DEFAULT NULL, + `SessionDuration` float DEFAULT NULL, + `TriggerTypeId` int(11) DEFAULT NULL, + `TriggerType` varchar(255) DEFAULT NULL, + `ShooterId` int(11) DEFAULT NULL, + `ShooterName` varchar(255) DEFAULT NULL, + `ShooterRoleId` int(11) DEFAULT NULL, + `ShooterRole` varchar(255) DEFAULT NULL, + `ShotIndex` int(11) DEFAULT NULL, + `ReactId` int(11) DEFAULT NULL, + `ReactModeId` int(11) DEFAULT NULL, + `ReactMode` varchar(255) DEFAULT NULL, + `ReactTypeId` int(11) DEFAULT NULL, + `ReactType` varchar(255) DEFAULT NULL, + `TargetUserId` int(11) DEFAULT NULL, + `TargetUserName` varchar(255) DEFAULT NULL, + `TargetRoleId` int(11) DEFAULT NULL, + `TargetRole` varchar(255) DEFAULT NULL, + `TargetName` varchar(255) DEFAULT NULL, + `TargetBoneName` varchar(255) DEFAULT NULL, + `TargetKilled` int(4) DEFAULT NULL, + `HitLocationX` double DEFAULT NULL, + `HitLocationY` double DEFAULT NULL, + `HitLocationTag` varchar(255) DEFAULT NULL, + `HitPrecision` double DEFAULT NULL, + `HitTargetDistance` double DEFAULT NULL, + `ReactionTime` double DEFAULT NULL, + `TimeStamp` double DEFAULT NULL, + `NbHit` bigint(21) DEFAULT NULL, + `NbKilled` bigint(21) DEFAULT NULL, + KEY `idx_sdc_session` (`SessionId`), + KEY `idx_sdc_session_shooter` (`SessionId`, `ShooterId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + + +-- ------------------------------------------------------------ +-- 2) Procedure stockee pour reconstruire le cache d'une session +-- ------------------------------------------------------------ +DROP PROCEDURE IF EXISTS rebuild_debrief_cache; + +DELIMITER $$ +CREATE PROCEDURE rebuild_debrief_cache(IN p_session_id INT) +BEGIN + DELETE FROM session_debriefs_cache WHERE SessionId = p_session_id; + + INSERT INTO session_debriefs_cache ( + SessionId, SessionTypeId, SessionType, SessionName, SessionDate, + MapName, ScenarioName, SessionSuccessful, SessionDuration, + TriggerTypeId, TriggerType, ShooterId, ShooterName, + ShooterRoleId, ShooterRole, ShotIndex, ReactId, ReactModeId, + ReactMode, ReactTypeId, ReactType, TargetUserId, TargetUserName, + TargetRoleId, TargetRole, TargetName, TargetBoneName, TargetKilled, + HitLocationX, HitLocationY, HitLocationTag, HitPrecision, + HitTargetDistance, ReactionTime, TimeStamp, NbHit, NbKilled + ) + SELECT + S.id, + S.sessionType, + ST.displayName, + S.sessionName, + S.sessionDate, + S.mapName, + S.scenarioName, + S.success, + S.timeToFinish, + COALESCE(T.type, -1), + COALESCE(TT.displayName, ''), + IFNULL(T.srcUserId, PS.userId), + US.username, + IFNULL(PS.role, 3), + URS.displayName, + COALESCE(T.indexCount, -1), + COALESCE(R.id, -1), + COALESCE(R.reactMode, 2), + COALESCE(RM.displayName, ''), + COALESCE(R.reactType, -1), + COALESCE(RT.displayName, ''), + COALESCE(R.hitUserId, -1), + COALESCE(UH.username, ''), + IFNULL(PH.role, 3), + URT.displayName, + COALESCE(R.hitTargetName, ''), + COALESCE(R.hitBoneName, ''), + COALESCE(R.targetKilled, 0), + COALESCE(R.objectHitLocationX, 0), + COALESCE(R.objectHitLocationY, 0), + COALESCE(R.objectHitTagLocation, ''), + COALESCE(R.hitPrecision, 0), + COALESCE(R.distance, 0), + COALESCE(R.reactTime, 0), + COALESCE(R.timeStamp, 0), + COUNT(DISTINCT R.id), + COUNT(DISTINCT RK.srcEventIndex, RK.hitTargetName) + FROM sessions S + LEFT JOIN participates PS ON (S.id = PS.sessionId) + LEFT JOIN triggerevents T ON (S.id = T.sessionId) + LEFT JOIN triggereventtypes TT ON (TT.id = T.type) + LEFT JOIN sessiontypes ST ON (ST.id = S.sessionType) + LEFT JOIN reactevents R ON (T.indexCount = R.srcEventIndex AND T.sessionId = R.srcEventSessionId) + LEFT JOIN users UH ON (UH.id = R.hitUserId) + LEFT JOIN users US ON (US.id = T.srcUserId OR US.id = PS.userId) + LEFT JOIN reacteventtypes RT ON (RT.id = R.reactType) + LEFT JOIN reacteventmodes RM ON (RM.id = COALESCE(R.reactMode, 2)) + LEFT JOIN reactevents RK ON (R.id = RK.id AND RK.targetKilled = 1) + -- PH : Action 2 du fix - PH represente la participation de l'utilisateur cible + -- (au lieu de "n'importe quel participant") pour que TargetRole soit coherent. + LEFT JOIN participates PH ON (S.id = PH.sessionId AND PH.userId = R.hitUserId) + LEFT JOIN userroles URS ON (URS.id = IFNULL(PS.role, 3)) + LEFT JOIN userroles URT ON (URT.id = IFNULL(PH.role, 3)) + WHERE S.id = p_session_id + GROUP BY S.id, IFNULL(T.srcUserId, PS.userId), COALESCE(T.indexCount, -1), COALESCE(R.hitTargetName, '') + ORDER BY S.id, IFNULL(T.srcUserId, PS.userId), COALESCE(T.indexCount, -1), COALESCE(R.id, -1); +END$$ +DELIMITER ; + + +-- ------------------------------------------------------------ +-- 3) Backfill complet : recalcule le cache pour toutes les sessions existantes +-- ------------------------------------------------------------ +TRUNCATE TABLE session_debriefs_cache; + +INSERT INTO session_debriefs_cache ( + SessionId, SessionTypeId, SessionType, SessionName, SessionDate, + MapName, ScenarioName, SessionSuccessful, SessionDuration, + TriggerTypeId, TriggerType, ShooterId, ShooterName, + ShooterRoleId, ShooterRole, ShotIndex, ReactId, ReactModeId, + ReactMode, ReactTypeId, ReactType, TargetUserId, TargetUserName, + TargetRoleId, TargetRole, TargetName, TargetBoneName, TargetKilled, + HitLocationX, HitLocationY, HitLocationTag, HitPrecision, + HitTargetDistance, ReactionTime, TimeStamp, NbHit, NbKilled +) +SELECT + S.id, + S.sessionType, + ST.displayName, + S.sessionName, + S.sessionDate, + S.mapName, + S.scenarioName, + S.success, + S.timeToFinish, + COALESCE(T.type, -1), + COALESCE(TT.displayName, ''), + IFNULL(T.srcUserId, PS.userId), + US.username, + IFNULL(PS.role, 3), + URS.displayName, + COALESCE(T.indexCount, -1), + COALESCE(R.id, -1), + COALESCE(R.reactMode, 2), + COALESCE(RM.displayName, ''), + COALESCE(R.reactType, -1), + COALESCE(RT.displayName, ''), + COALESCE(R.hitUserId, -1), + COALESCE(UH.username, ''), + IFNULL(PH.role, 3), + URT.displayName, + COALESCE(R.hitTargetName, ''), + COALESCE(R.hitBoneName, ''), + COALESCE(R.targetKilled, 0), + COALESCE(R.objectHitLocationX, 0), + COALESCE(R.objectHitLocationY, 0), + COALESCE(R.objectHitTagLocation, ''), + COALESCE(R.hitPrecision, 0), + COALESCE(R.distance, 0), + COALESCE(R.reactTime, 0), + COALESCE(R.timeStamp, 0), + COUNT(DISTINCT R.id), + COUNT(DISTINCT RK.srcEventIndex, RK.hitTargetName) +FROM sessions S +LEFT JOIN participates PS ON (S.id = PS.sessionId) +LEFT JOIN triggerevents T ON (S.id = T.sessionId) +LEFT JOIN triggereventtypes TT ON (TT.id = T.type) +LEFT JOIN sessiontypes ST ON (ST.id = S.sessionType) +LEFT JOIN reactevents R ON (T.indexCount = R.srcEventIndex AND T.sessionId = R.srcEventSessionId) +LEFT JOIN users UH ON (UH.id = R.hitUserId) +LEFT JOIN users US ON (US.id = T.srcUserId OR US.id = PS.userId) +LEFT JOIN reacteventtypes RT ON (RT.id = R.reactType) +LEFT JOIN reacteventmodes RM ON (RM.id = COALESCE(R.reactMode, 2)) +LEFT JOIN reactevents RK ON (R.id = RK.id AND RK.targetKilled = 1) +LEFT JOIN participates PH ON (S.id = PH.sessionId AND PH.userId = R.hitUserId) +LEFT JOIN userroles URS ON (URS.id = IFNULL(PS.role, 3)) +LEFT JOIN userroles URT ON (URT.id = IFNULL(PH.role, 3)) +GROUP BY S.id, IFNULL(T.srcUserId, PS.userId), COALESCE(T.indexCount, -1), COALESCE(R.hitTargetName, '') +ORDER BY S.id, IFNULL(T.srcUserId, PS.userId), COALESCE(T.indexCount, -1), COALESCE(R.id, -1); + + +-- ------------------------------------------------------------ +-- 4) La vue devient un passe-plat sur la table cache +-- ------------------------------------------------------------ +CREATE OR REPLACE VIEW sessiondebriefs AS +SELECT * FROM session_debriefs_cache;