insert_id; } /** * Atomically seat one player in the lowest-numbered open mission that has room * (creating one if none does). Returns the MissionID. Used by the kiosk's * normal registration. */ function pqs_enqueue_player(string $callsign, string $mech, int $merc = 0): int { return pqs_with_queue_lock(function () use ($callsign, $mech, $merc) { $db = pqs_db(); $db->begin_transaction(); try { $mission = pqs_row( "SELECT MissionID FROM pqs_mission WHERE Completed = 0 AND locked = 0 AND (SELECT COUNT(*) FROM pqs_queue WHERE MissionID = pqs_mission.MissionID) < MissionSize ORDER BY MissionID ASC LIMIT 1" ); $missionid = $mission === null ? pqs_create_mission() : (int) $mission['MissionID']; pqs_exec( "INSERT INTO pqs_queue (CallSign, Mech, MissionID, Merc) VALUES (?, ?, ?, ?)", 'ssii', [$callsign, $mech, $missionid, $merc] ); pqs_sync_numplayers($missionid); $db->commit(); return $missionid; } catch (Throwable $e) { $db->rollback(); throw $e; } }); } /** * Atomically remove a player from the queue and resync their mission's * numplayers from actual occupancy. Under the shared lock so it can't race a * concurrent registration/commit. */ function pqs_remove_player(int $playerid): void { pqs_with_queue_lock(function () use ($playerid) { $db = pqs_db(); $db->begin_transaction(); try { $row = pqs_row("SELECT MissionID FROM pqs_queue WHERE ID = ?", 'i', [$playerid]); pqs_exec("DELETE FROM pqs_queue WHERE ID = ?", 'i', [$playerid]); if ($row !== null && $row['MissionID'] !== null) { pqs_sync_numplayers((int) $row['MissionID']); } $db->commit(); } catch (Throwable $e) { $db->rollback(); throw $e; } }); } /** * Atomically add a player to a SPECIFIC mission (staff "new" action). Does not * pick a mission or enforce capacity — staff may intentionally overfill — but * shares the lock so it can't race with kiosk registrations, and keeps * numplayers accurate. */ function pqs_add_player_to_mission(int $missionid, string $callsign, string $mech, int $merc = 0): void { pqs_with_queue_lock(function () use ($missionid, $callsign, $mech, $merc) { $db = pqs_db(); $db->begin_transaction(); try { pqs_exec( "INSERT INTO pqs_queue (CallSign, Mech, MissionID, Merc) VALUES (?, ?, ?, ?)", 'ssii', [$callsign, $mech, $missionid, $merc] ); pqs_sync_numplayers($missionid); $db->commit(); } catch (Throwable $e) { $db->rollback(); throw $e; } }); } /** * Staff "add to multiple missions": place $callsign in the next $count missions * after $startmission, filling existing missions that have room and creating * new ones at the tail as needed. Runs as one atomic unit under the shared lock. */ function pqs_add_player_to_missions(int $startmission, int $count, string $callsign, string $mech, int $merc = 0): void { if ($count < 1) { return; } pqs_with_queue_lock(function () use ($startmission, $count, $callsign, $mech, $merc) { $db = pqs_db(); $db->begin_transaction(); try { $placed = 0; $mid = $startmission + 1; while ($placed < $count) { $maxmission = (int) pqs_val("SELECT MAX(MissionID) FROM pqs_mission"); if ($mid <= $maxmission) { $row = pqs_row( "SELECT MissionSize, (SELECT COUNT(*) FROM pqs_queue WHERE MissionID = ?) AS filled FROM pqs_mission WHERE MissionID = ?", 'ii', [$mid, $mid] ); if ($row !== null && (int) $row['filled'] < (int) $row['MissionSize']) { pqs_exec( "INSERT INTO pqs_queue (CallSign, Mech, MissionID, Merc) VALUES (?, ?, ?, ?)", 'ssii', [$callsign, $mech, $mid, $merc] ); pqs_sync_numplayers($mid); $placed++; } // full or missing: fall through and try the next mission } else { $mid = pqs_create_mission(); pqs_exec( "INSERT INTO pqs_queue (CallSign, Mech, MissionID, Merc) VALUES (?, ?, ?, ?)", 'ssii', [$callsign, $mech, $mid, $merc] ); pqs_sync_numplayers($mid); $placed++; } $mid++; } $db->commit(); } catch (Throwable $e) { $db->rollback(); throw $e; } }); } /** * Atomically reserve a group of $count seats in one mission for a leader: * inserts the leader ($callsign) plus placeholder rows "$callsign-2" .. * "$callsign-$count" that members later claim. Picks the lowest open mission * that can fit the whole group, else creates one. Returns the MissionID. */ function pqs_enqueue_group(string $callsign, string $mech, int $count): int { if ($count < 1) { $count = 1; } return pqs_with_queue_lock(function () use ($callsign, $mech, $count) { $db = pqs_db(); $db->begin_transaction(); try { $mission = pqs_row( "SELECT MissionID FROM pqs_mission WHERE Completed = 0 AND locked = 0 AND (SELECT COUNT(*) FROM pqs_queue WHERE MissionID = pqs_mission.MissionID) + ? <= MissionSize ORDER BY MissionID ASC LIMIT 1", 'i', [$count] ); $missionid = $mission === null ? pqs_create_mission() : (int) $mission['MissionID']; pqs_exec( "INSERT INTO pqs_queue (CallSign, Mech, MissionID) VALUES (?, ?, ?)", 'ssi', [$callsign, $mech, $missionid] ); for ($i = 2; $i <= $count; $i++) { pqs_exec( "INSERT INTO pqs_queue (CallSign, Mech, MissionID) VALUES (?, ?, ?)", 'ssi', ["$callsign-$i", $mech, $missionid] ); } pqs_sync_numplayers($missionid); $db->commit(); return $missionid; } catch (Throwable $e) { $db->rollback(); throw $e; } }); } /** * Atomically claim one unclaimed placeholder slot reserved by leader $who, * replacing it with this member's $callsign/$mech. Returns the MissionID the * member landed in, or null if no free slot remained. Serialized so two members * can't grab the same slot. */ function pqs_claim_group_slot(string $who, string $callsign, string $mech): ?int { return pqs_with_queue_lock(function () use ($who, $callsign, $mech) { $db = pqs_db(); $db->begin_transaction(); try { // Unclaimed placeholders still look like "who-"; a claimed one has // been renamed to the member's real callsign. $slot = pqs_row( "SELECT ID, MissionID FROM pqs_queue WHERE CallSign LIKE ? ORDER BY ID ASC LIMIT 1", 's', ["$who-%"] ); if ($slot === null) { $db->commit(); return null; } pqs_exec( "UPDATE pqs_queue SET CallSign = ?, Mech = ? WHERE ID = ?", 'ssi', [$callsign, $mech, (int) $slot['ID']] ); $db->commit(); return (int) $slot['MissionID']; } catch (Throwable $e) { $db->rollback(); throw $e; } }); }