Compare commits

..

1 Commits

Author SHA1 Message Date
68cea1eb4d V2 : optimisation API (cache, index, vue materialisee)
- BDD (migrations 1.6.0 + 1.7.0, idempotentes) :
  * 4 nouveaux index composites sur reactevents, triggerevents, sessions
  * vue sessiondebriefs reecrite (sous-requetes correlees -> JOINs)
    puis materialisee dans table session_debriefs_cache via procedure
    stockee rebuild_debrief_cache, recalculee uniquement aux changements
  * fix bug double JOIN participates : TargetRole = "IA" pour les hits
    NPC/objets (au lieu d'un role aleatoire de participant)
- API PHP :
  * connexion PDO en singleton + ATTR_PERSISTENT
  * cache disque par session (cache/sess_<id>/) pour debrief, get, userhistory
    avec invalidation ciblee sur events et lifecycle
  * pagination opt-in (limit/offset) sur lists/sessions_for_user
  * helpers ensure_/rebuild_/invalidate_session_debrief_cache_db
  * debug/benchmark.php pour mesurer la latence des 6 endpoints cles
- Endpoints HTTP : signatures inchangees, compat Unreal preservee
- Gain mesure : -58% a -83% sur les 5 endpoints cles

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 16:41:52 +02:00
20 changed files with 757 additions and 45 deletions

View File

@@ -0,0 +1,3 @@
*
!.gitignore
!.htaccess

View File

@@ -0,0 +1,4 @@
Options -Indexes
<FilesMatch "\.json$">
Require all denied
</FilesMatch>

View File

@@ -0,0 +1,109 @@
<?php
// Cache disque pour resultats lourds et immuables (debrief, userhistory, get session).
// Chaque session a son propre sous-dossier cache/sess_<id>/ : 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
}
}
?>

View File

@@ -24,11 +24,11 @@ define("SESSIONDEBRIEFS_VIEW_NAME", "sessiondebriefs");
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, " .
@@ -42,7 +42,9 @@ define ("SESSIONDEBRIEFS_VIEW_QUERY_FROM",
"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");

View File

@@ -8,21 +8,33 @@ class Database
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;
}
}
?>

View File

@@ -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();

View File

@@ -0,0 +1,106 @@
<?php
// Mesure de latence des endpoints lourds de l'API ProserveAPI.
// A lancer AVANT et APRES chaque palier d'optimisation pour avoir des chiffres comparatifs.
// Usage : POST sur ce fichier avec optionnellement userId et sessionId.
// curl -X POST -d "userId=1&sessionId=42" http://localhost/proserve/debug/benchmark.php
// ou ouvrir directement dans le navigateur (lance avec userId=-1 et sessionId=-1, certains endpoints retourneront vide mais la mesure du round-trip reste valide).
include_once '../config/init.php';
$userId = isset($_POST['userId']) ? intval($_POST['userId']) : (isset($_GET['userId']) ? intval($_GET['userId']) : -1);
$sessionId = isset($_POST['sessionId']) ? intval($_POST['sessionId']) : (isset($_GET['sessionId']) ? intval($_GET['sessionId']) : -1);
$iterations = isset($_POST['iterations']) ? max(1, intval($_POST['iterations'])) : (isset($_GET['iterations']) ? max(1, intval($_GET['iterations'])) : 5);
// si pas de sessionId fourni, prendre la plus recente pour avoir des donnees realistes
if ($sessionId <= 0) {
$stmt = $db->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));
?>

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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 : "";
@@ -127,9 +128,15 @@ class StatsObject extends DBTableObject
// Order results from newest to oldest session
$orderConstraint = " ORDER BY SD.id DESC";
// 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);
@@ -141,10 +148,10 @@ 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))
{
@@ -469,6 +476,11 @@ 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 : "";
@@ -692,6 +704,12 @@ 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 : "";

View File

@@ -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

View File

@@ -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);
$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");
print_r(json_encode($stats_arr)); // OK
$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");
?>

View File

@@ -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");
?>

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}
}
?>

View File

@@ -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;

View File

@@ -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;