alles fuer den Polizei-Tab im Handy (auch fuer Nicht-Cops: // eigene Heat-Anzeige "wie heiss bin ich gerade?"). if ($action === 'cop_status') { $u = require_user(); $uid = (int)$u['ID']; $persona = persona_id($uid); if (!$persona) fail("Keine Persona."); cop_migrate(); license_migrate(); $carId = current_car_id($persona); $duty = cop_duty_row($persona); $hasLic = (bool)db_one("SELECT 1 FROM rp_business_license WHERE persona_id=? AND license_type='cop' AND property_id=0 LIMIT 1", 'i', [$persona]); // Registrierung ist BESITZER-gebunden (car_id + persona_id): nach einem // Autoverkauf zaehlt die alte Registrierung fuer den Kaeufer nicht. $carReg = $carId ? (bool)db_one("SELECT 1 FROM rp_cop_car WHERE car_id=? AND persona_id=?", 'ii', [(int)$carId, $persona]) : false; $bounty = bounty_get($persona); $hunted = (bool)db_one("SELECT 1 FROM rp_pursuit WHERE target_persona_id=? AND status='active' AND updated_at > NOW() - INTERVAL 120 SECOND LIMIT 1", 'i', [$persona]); $myRank = cop_rank($persona); done(["ok"=>true, "has_license"=>$hasLic, "license_price"=>(int)($CONFIG['licenses']['cop'] ?? 1000), "car_registered"=>$carReg, "register_fee"=>(int)cop_cfg('register_fee', 2500), "on_duty"=>(bool)$duty, "duty_car_id"=>$duty ? (int)$duty['car_id'] : 0, "bounty"=>$bounty, "heat"=>heat_level($bounty), "pursued"=>$hunted, "rank"=>$myRank, "rank_name"=>cop_rank_name($myRank), "is_chief"=>cop_is_chief($persona), "at_poi"=>near_poi(cop_cfg('require_poi','polizei_revier'))]); } // cop_car_register -> aktuelles Auto am Revier als Einsatzfahrzeug eintragen. if ($action === 'cop_car_register') { $u = require_user(); $uid = (int)$u['ID']; $persona = persona_id($uid); if (!$persona) fail("Keine Persona."); cop_migrate(); // Solange das Revier noch Platzhalter-Koordinaten (0/0/0) hat, bleibt der // Cop-Job ZU (fail-closed) -- sonst waere das Orts-Gate wirkungslos. $poiCop = poi_by_id(cop_cfg('require_poi','polizei_revier')); if ($poiCop && (float)$poiCop['x'] == 0.0 && (float)$poiCop['y'] == 0.0 && (float)$poiCop['z'] == 0.0) fail("Das Polizeirevier hat noch keine Koordinaten (Config) -- Cop-Job noch nicht freigeschaltet."); if (!near_poi(cop_cfg('require_poi','polizei_revier'))) fail("Du musst am Polizeirevier sein."); $carId = current_car_id($persona); if (!$carId) fail("Kein Fahrzeug gefunden."); if (db_one("SELECT 1 FROM rp_cop_car WHERE car_id=? AND persona_id=?", 'ii', [(int)$carId, $persona])) fail("Dieses Fahrzeug ist bereits als Einsatzfahrzeug registriert."); // Whitelist (leer = alle Modelle erlaubt): car.name-Slug-Teilstring wie beim Kofferraum. $wl = (array)cop_cfg('car_whitelist', []); if ($wl) { $cn = db_one("SELECT name FROM car WHERE id=?", 'i', [(int)$carId]); $slug = strtolower((string)($cn['name'] ?? '')); $okCar = false; foreach ($wl as $w) if ($w !== '' && strpos($slug, strtolower($w)) !== false) { $okCar = true; break; } if (!$okCar) fail("Dieses Modell ist nicht als Einsatzfahrzeug zugelassen."); } $fee = (int)cop_cfg('register_fee', 2500); $pay = poi_charge($uid, $persona, $fee, 'Einsatzfahrzeug-Registrierung', inp('payment'), 'cop'); if (!$pay['ok']) fail($pay['error']); // Upsert: hat ein VORBESITZER das Auto mal registriert, ueberschreibt der // neue Besitzer die alte Zeile mit seiner eigenen Registrierung. db_exec("INSERT INTO rp_cop_car (car_id, persona_id, registered_at) VALUES (?,?,NOW()) ON DUPLICATE KEY UPDATE persona_id=VALUES(persona_id), registered_at=NOW()", 'ii', [(int)$carId, $persona]); done(["ok"=>true, "message"=>"Fahrzeug als Einsatzfahrzeug registriert.", "fee"=>$fee]); } // cop_duty_start -> Dienst antreten (am Revier, mit Lizenz, im registrierten // Auto, ohne offenes Kopfgeld -- ein gesuchter Cop waere ja absurd). if ($action === 'cop_duty_start') { $u = require_user(); $uid = (int)$u['ID']; $persona = persona_id($uid); if (!$persona) fail("Keine Persona."); cop_migrate(); license_migrate(); // fail-closed bei Platzhalter-Koordinaten (s. cop_car_register). $poiCop = poi_by_id(cop_cfg('require_poi','polizei_revier')); if ($poiCop && (float)$poiCop['x'] == 0.0 && (float)$poiCop['y'] == 0.0 && (float)$poiCop['z'] == 0.0) fail("Das Polizeirevier hat noch keine Koordinaten (Config) -- Cop-Job noch nicht freigeschaltet."); if (!near_poi(cop_cfg('require_poi','polizei_revier'))) fail("Du musst am Polizeirevier sein."); if (!db_one("SELECT 1 FROM rp_business_license WHERE persona_id=? AND license_type='cop' AND property_id=0 LIMIT 1", 'i', [$persona])) fail("Du brauchst eine Cop-Lizenz.", ["code"=>"no_license", "license_type"=>"cop", "price"=>$CONFIG['licenses']['cop'] ?? 1000]); $carId = current_car_id($persona); if (!$carId) fail("Kein Fahrzeug gefunden."); if (!db_one("SELECT 1 FROM rp_cop_car WHERE car_id=? AND persona_id=?", 'ii', [(int)$carId, $persona])) fail("Dein aktuelles Auto ist kein registriertes Einsatzfahrzeug."); if (bounty_get($persona) > 0) fail("Mit offenem Kopfgeld kein Dienstantritt. Warte den Verfall ab oder stell dich (lass dich busten)."); // Sold-Fenster mit der aktuellen Position initialisieren (Referenz fuer die // Bewegungs-Pruefung; am Revier -> von dort wird die Bewegung gemessen). $pxs=(float)inp('px'); $pys=(float)inp('py'); $pzs=(float)inp('pz'); db_exec("INSERT INTO rp_cop_duty (persona_id, car_id, started_at, last_pay_at, px_pay, py_pay, pz_pay) VALUES (?,?,NOW(),NOW(),?,?,?) ON DUPLICATE KEY UPDATE car_id=VALUES(car_id), started_at=NOW(), last_pay_at=NOW(), px_pay=VALUES(px_pay), py_pay=VALUES(py_pay), pz_pay=VALUES(pz_pay)", 'iiddd', [$persona, (int)$carId, $pxs, $pys, $pzs]); done(["ok"=>true, "message"=>"Dienst angetreten. Fahndungs-Radar aktiv. Sold laeuft (nur beim Fahren)."]); } // cop_duty_stop -> Dienst beenden (ueberall moeglich; Verfolgungen enden). if ($action === 'cop_duty_stop') { $u = require_user(); $uid = (int)$u['ID']; $persona = persona_id($uid); if (!$persona) fail("Keine Persona."); cop_migrate(); db_exec("DELETE FROM rp_cop_duty WHERE persona_id=?", 'i', [$persona]); db_exec("UPDATE rp_pursuit SET status='aborted', updated_at=NOW() WHERE cop_persona_id=? AND status='active'", 'i', [$persona]); done(["ok"=>true, "message"=>"Dienst beendet."]); } // cop_set_rank -> Rang eines Officers setzen. NUR der Chef (cop_chief_persona_id) // darf das, direkt im F9-Polizei-Terminal (Personalverwaltung). if ($action === 'cop_set_rank') { $u = require_user(); $uid = (int)$u['ID']; $persona = persona_id($uid); if (!$persona) fail("Keine Persona."); cop_migrate(); license_migrate(); if (!cop_is_chief($persona)) fail("Nur der Polizeichef kann Raenge vergeben."); $tgt = (int)inp('target'); $rank = (int)inp('rank'); if ($tgt <= 0) fail("Kein Ziel angegeben."); if ($tgt === (int)$persona) fail("Deinen eigenen (Chef-)Rang kannst du nicht aendern."); if (cop_is_chief($tgt)) fail("Der Chef-Rang ist reserviert."); $chiefRank = (int)cop_cfg('cop_chief_rank', 11); if ($rank < 1 || $rank > $chiefRank - 1) fail("Rang muss zwischen 1 und ".($chiefRank - 1)." liegen."); if (!db_one("SELECT 1 FROM rp_business_license WHERE persona_id=? AND license_type='cop' AND property_id=0 LIMIT 1", 'i', [$tgt])) fail("Das Ziel hat keine Cop-Lizenz."); db_exec("INSERT INTO rp_cop_rank (persona_id, rank, assigned_by, updated_at) VALUES (?,?,?,NOW()) ON DUPLICATE KEY UPDATE rank=VALUES(rank), assigned_by=VALUES(assigned_by), updated_at=NOW()", 'iii', [$tgt, $rank, (int)$persona]); $tn = db_one("SELECT name FROM persona WHERE id=?", 'i', [$tgt]); done(["ok"=>true, "message"=>(($tn['name'] ?? 'Officer'))." ist jetzt ".cop_rank_name($rank)."."]); } // cop_radar -> Gesuchten-Liste fuer Cops im Dienst. Sichtbarkeit haengt an // der Heat-Stufe des Gesuchten (radar_dist): kleine Fische nur in der Naehe, // Stufe 5 auf der ganzen Karte. Liefert auch den Stand der eigenen Verfolgung // (Bust-Fortschritt) fuer die HUD-Anzeige. if ($action === 'cop_radar') { $u = require_user(); $uid = (int)$u['ID']; $persona = persona_id($uid); if (!$persona) fail("Keine Persona."); cop_migrate(); if (!cop_duty_row($persona)) fail("Du bist nicht im Dienst."); // Ohne gueltige eigene Position kein Radar (sonst rechnen alle Distanzen // vom Kartenursprung 0/0/0 -- Menue/Ladebildschirm). if (inp('px') === '' || inp('py') === '' || inp('pz') === '' || ((float)inp('px') == 0.0 && (float)inp('py') == 0.0 && (float)inp('pz') == 0.0)) fail("Keine gueltige Position (Menue/Ladebildschirm?)."); $px = (float)inp('px'); $py = (float)inp('py'); $pz = (float)inp('pz'); $radar = (array)cop_cfg('radar_dist', [1=>1500,2=>2500,3=>4000,4=>6000,5=>999999]); // Nur Gesuchte mit frischer Welt-Position und ausserhalb laufender Rennen // (gleiche Regeln wie die Verfolgungs-Engine im Heartbeat). $cand = db_all("SELECT h.persona_id, h.bounty, p.px, p.py, p.pz, pe.name FROM rp_heat h JOIN rp_pos p ON p.persona_id = h.persona_id AND p.ts > NOW() - INTERVAL 30 SECOND JOIN rp_online o ON o.persona_id = h.persona_id AND o.last_seen > NOW() - INTERVAL 60 SECOND JOIN persona pe ON pe.id = h.persona_id WHERE h.bounty > 0 AND h.persona_id <> ? AND NOT EXISTS (SELECT 1 FROM event_data ed WHERE ed.personaId = h.persona_id AND ed.ID > p.race_base_id AND ((ed.fractionCompleted < 1 AND ed.finishReason = 0 AND (ed.serverTimeEnded IS NULL OR ed.serverTimeEnded = 0) AND ed.serverTimeStarted > ".((time()-3600)*1000).") OR ed.serverTimeEnded > ".((time()-60)*1000)."))", 'i', [$persona]); $out = []; foreach ($cand as $c) { $lv = heat_level((int)$c['bounty']); $d = sqrt(pow($px-(float)$c['px'],2) + pow($py-(float)$c['py'],2) + pow($pz-(float)$c['pz'],2)); if ($d > (float)($radar[$lv] ?? 0)) continue; // ausserhalb der Radar-Reichweite $out[] = ["persona_id"=>(int)$c['persona_id'], "name"=>$c['name'], "heat"=>$lv, "bounty"=>(int)$c['bounty'], "dist"=>(int)round($d), "px"=>(float)$c['px'], "py"=>(float)$c['py'], "pz"=>(float)$c['pz']]; } usort($out, function($a,$b){ return $a['dist'] <=> $b['dist']; }); $out = array_slice($out, 0, 20); $pu = db_one("SELECT p.*, pe.name AS target_name FROM rp_pursuit p JOIN persona pe ON pe.id = p.target_persona_id WHERE p.cop_persona_id=? AND p.status='active' ORDER BY p.id DESC LIMIT 1", 'i', [$persona]); $puOut = null; if ($pu) $puOut = ["target_name"=>$pu['target_name'], "near_seconds"=>(int)$pu['near_seconds'], "bust_seconds"=>(int)cop_cfg('bust_seconds',15), "far_seconds"=>(int)$pu['far_seconds'], "escape_seconds"=>(int)cop_cfg('escape_seconds',45)]; done(["ok"=>true, "wanted"=>$out, "pursuit"=>$puOut, "bust_dist"=>(int)cop_cfg('bust_dist',80), "start_dist"=>(int)cop_cfg('pursuit_start_dist',250)]); } // cop_ping -> schneller Gesuchten-seitiger Fahndungs-Status fuer HUD + BUSTED-Kino. // Der Client pollt das ~1.5s waehrend Freeroam. Sehr leicht (2 kleine Queries). if ($action === 'cop_ping') { $u = require_user(); $uid = (int)$u['ID']; $persona = persona_id($uid); if (!$persona) done(["ok"=>true]); cop_migrate(); $out = ["ok"=>true]; // 1) Werde ich gerade aktiv verfolgt? (frische Verfolgung < 60s) $pu = db_one("SELECT rp.near_seconds, c.name AS cop, h.bounty FROM rp_pursuit rp LEFT JOIN persona c ON c.id = rp.cop_persona_id LEFT JOIN rp_heat h ON h.persona_id = rp.target_persona_id WHERE rp.target_persona_id = ? AND rp.status = 'active' AND rp.updated_at > NOW() - INTERVAL 60 SECOND ORDER BY rp.updated_at DESC LIMIT 1", 'i', [$persona]); if ($pu) $out['chase'] = [ "near" => (int)$pu['near_seconds'], "need" => (int)cop_cfg('bust_seconds', 15), "cop" => (string)($pu['cop'] ?? 'STREIFE'), "bounty" => (int)($pu['bounty'] ?? 0), "role" => "target", ]; // 1b) Cop-Seite: verfolge ICH gerade jemanden aktiv? -> derselbe Live-Meter // fuer den Jaeger (fuellt sich, waehrend er dranbleibt). "cop" = Ziel-Name. if (!isset($out['chase'])) { $cpu = db_one("SELECT rp.near_seconds, t.name AS tgt, h.bounty FROM rp_pursuit rp LEFT JOIN persona t ON t.id = rp.target_persona_id LEFT JOIN rp_heat h ON h.persona_id = rp.target_persona_id WHERE rp.cop_persona_id = ? AND rp.status = 'active' AND rp.updated_at > NOW() - INTERVAL 60 SECOND ORDER BY rp.updated_at DESC LIMIT 1", 'i', [$persona]); if ($cpu) $out['chase'] = [ "near" => (int)$cpu['near_seconds'], "need" => (int)cop_cfg('bust_seconds', 15), "cop" => (string)($cpu['tgt'] ?? 'GESUCHTER'), "bounty" => (int)($cpu['bounty'] ?? 0), "role" => "cop", ]; } // 2) Frischer Bust, den das Kino noch nicht gezeigt hat -> einmalig ausliefern. $bu = db_one("SELECT rp.id, rp.fine_paid, c.name AS cop FROM rp_pursuit rp LEFT JOIN persona c ON c.id = rp.cop_persona_id WHERE rp.target_persona_id = ? AND rp.status = 'busted' AND rp.cine_shown = 0 AND rp.updated_at > NOW() - INTERVAL 120 SECOND ORDER BY rp.updated_at DESC LIMIT 1", 'i', [$persona]); if ($bu) { db_exec("UPDATE rp_pursuit SET cine_shown = 1 WHERE id = ?", 'i', [(int)$bu['id']]); $out['busted'] = ["cop" => (string)($bu['cop'] ?? 'STREIFE'), "fine" => (int)$bu['fine_paid']]; } done($out); } }