# API du jeu — ZF MDT

API machine à machine entre le serveur GMod et le site. Aucune session n'est
ouverte : l'authentification se fait par clé partagée.

**Base :** `https://votre-site/api/game`

---

## Authentification

Toute requête doit porter l'en-tête :

```
X-ZF-Key: <game.api_key de config/config.php>
```

Optionnellement, `game.allowed_ips` restreint l'accès à une liste d'adresses.

Une clé absente, fausse, ou une IP non autorisée renvoient toutes le même
`401` — on ne dit pas laquelle des deux coince, inutile d'aider quelqu'un qui
tâtonne.

Si `game.api_key` est vide ou encore à sa valeur d'exemple, l'API répond
`503` : c'est volontaire, un serveur mal configuré ne doit pas être ouvert.

## Format des réponses

Toujours du JSON.

```json
{ "ok": true,  "...": "..." }
{ "ok": false, "error": "Message explicite." }
```

| Code | Signification |
|---|---|
| 200 | Traité |
| 401 | Clé ou IP refusée |
| 404 | Adresse inconnue |
| 422 | Paramètre manquant ou invalide |
| 429 | File d'appels saturée |
| 503 | API désactivée (clé non configurée) |

---

## Sens de circulation

```
GMod  ──POST──>  site     unités, appels, fiches, immatriculations
GMod  <──GET───  site     file d'actions à exécuter en jeu
```

GMod ne peut pas recevoir de requête entrante. Tout ce que le site veut
faire exécuter en jeu passe donc par `/queue`, que le serveur interroge
toutes les quelques secondes.

---

## Endpoints

### `GET /ping`

Vérification de liaison. Appelé au démarrage du serveur GMod.

```json
{ "ok": true, "service": "ZF MDT", "time": 1786000000, "units": 4 }
```

---

### `POST /token`

Émet un jeton de connexion automatique pour ouvrir le terminal en jeu.

```json
{ "steamid64": "76561198000000000", "name": "John Doe" }
```

```json
{
  "ok": true,
  "token": "a1b2c3...",
  "url": "https://mdt.exemple.fr/?token=a1b2c3...",
  "expires": 1786000120
}
```

Le jeton est **à usage unique** et expire en 120 s (`game.token_ttl`). Il
transite dans une URL : on part du principe qu'il peut fuiter, d'où la durée
courte et la consommation immédiate.

La fiche d'état civil du joueur est créée au passage si elle n'existe pas.

---

### `POST /units`

État complet des unités en service. Le jeu est la source de vérité : il
envoie tout, le site remplace.

```json
{
  "units": [
    {
      "steamid64": "76561198000000000",
      "callsign": "47 ADAM",
      "name": "John Doe",
      "dept": "police",
      "rank": "officer",
      "status": "available",
      "pos_x": 1204, "pos_y": -880, "pos_z": 64,
      "health": 100,
      "in_vehicle": true
    }
  ]
}
```

L'adresse est résolue côté site à partir de la position — le jeu n'a rien à
calculer.

Une unité qui cesse d'émettre plus de `game.unit_timeout` secondes (90 par
défaut) est affichée comme périmée, puis purgée.

**Conflit d'indicatif :** si l'indicatif est déjà pris par un autre SteamID,
il est suffixé automatiquement plutôt que refusé — une prise de service ne
doit jamais échouer silencieusement.

---

### `POST /unit/remove`

Fin de service immédiate.

```json
{ "steamid64": "76561198000000000" }
```

L'unité est retirée et libérée de son appel en cours.

---

### `POST /call`

Crée un appel.

```json
{
  "dept": "police",
  "type_code": "SHOTS",
  "priority": 1,
  "pos_x": 1204, "pos_y": -880, "pos_z": 64,
  "caller_name": "APPEL ANONYME",
  "caller_sid": "",
  "caller_phone": "Numero masque",
  "narrative": "Plusieurs detonations entendues.",
  "source": "auto"
}
```

```json
{ "ok": true, "id": 412, "number": "P26-0087" }
```

- `location` peut être omis : l'adresse est calculée depuis la position.
- `priority` peut être omis : la priorité du type d'appel s'applique.
- `source` : `911` | `auto` | `panic` | `manual`.

Au-delà de 80 appels ouverts pour un service, les créations non manuelles
sont refusées (`429`) : mieux vaut refuser un appel que noyer le
répartiteur.

Une priorité 1 ou 2 déclenche une alerte en jeu pour tout le service, via la
file d'actions.

---

### `POST /call/status`

Fait évoluer un appel depuis le jeu.

```json
{ "call_id": 412, "callsign": "47 ADAM", "action": "onscene" }
```

| `action` | Effet |
|---|---|
| `assign` | Affecte l'unité |
| `unassign` | La libère |
| `onscene` | Marque l'arrivée et horodate le délai d'intervention |
| `note` | Ajoute `text` à la main courante |
| `close` | Clôture avec `disposition` (obligatoire) et `note` |

La disposition doit exister au catalogue du service concerné — c'est elle qui
alimente les statistiques.

---

### `POST /citizen`

Crée ou rafraîchit une fiche d'état civil. À appeler à chaque connexion de
joueur.

```json
{ "steamid64": "76561198000000000", "name": "John Doe", "avatar": "https://..." }
```

```json
{ "ok": true, "id": 88, "nysid": "04821973K" }
```

L'identité (NYSID, date de naissance, taille, signalement) est **déterministe**
et dérivée du SteamID : purger la base ne fait pas perdre l'identité d'un
joueur, et le jeu comme le site calculent les mêmes valeurs.

Un changement de nom RP est conservé en alias dans les notes de la fiche.

---

### `POST /vehicle`

Immatricule un véhicule.

```json
{
  "owner_steamid64": "76561198000000000",
  "make": "Ford",
  "model": "crown_victoria",
  "color": "Jaune"
}
```

```json
{ "ok": true, "plate": "KTR-4820" }
```

`plate` peut être omis : la plaque est calculée de façon déterministe depuis
le propriétaire et le modèle, donc identique des deux côtés.

---

### `GET /lookup/plate?plate=ABC-1234`

Interrogation de plaque, pour un lecteur embarqué en jeu.

```json
{
  "ok": true, "found": true,
  "plate": "ABC-1234",
  "make": "Ford", "model": "Crown Victoria", "color": "Jaune",
  "reg": 1, "insured": true,
  "flags": ["stolen"],
  "owner": {
    "id": 88, "name": "John Doe", "nysid": "04821973K",
    "licence": 2, "flags": ["armed"]
  },
  "warrants": 1
}
```

`reg` et `licence` : `0` aucun, `1` valide, `2` suspendu, `3` révoqué,
`4` expiré.

---

### `GET /lookup/person?steamid64=765...`

Fiche résumée, pour un contrôle d'identité en jeu.

```json
{
  "ok": true, "found": true,
  "id": 88, "name": "John Doe", "nysid": "04821973K", "dob": "14/03/1988",
  "flags": ["armed", "wanted"],
  "licences": { "driver": 2, "firearm": 3 },
  "summary": { "arrests": 4, "felonies": 1, "fines": 12400, "warrants": 1 },
  "warrants": [ { "type": "arrest", "reason": "..." } ]
}
```

---

### `GET /queue?limit=100`

**Le cœur de la liaison site → jeu.** Récupère les actions à exécuter et les
marque consommées.

```json
{
  "ok": true, "count": 2,
  "actions": [
    { "id": 91, "action": "wanted",
      "payload": { "steamid64": "765...", "wanted": true, "reason": "Vol a main armee" } },
    { "id": 92, "action": "assigned",
      "payload": { "steamid64": "765...", "call_id": 412, "number": "P26-0087",
                   "type": "Coups de feu", "location": "247 W 47th St", "priority": 1 } }
  ]
}
```

Les actions sont marquées consommées **avant** la réponse : en cas de
coupure réseau on préfère perdre une notification que la rejouer en boucle.

| `action` | Ce que le jeu doit faire |
|---|---|
| `sanction` | Prélever `fine`, incarcérer `jail` secondes |
| `wanted` | Poser ou lever le statut recherché |
| `assigned` | Alerter l'unité de son affectation |
| `unit_status` | Appliquer le statut imposé par le répartiteur |
| `broadcast` | Alerte à tout un service |

Une action inconnue est ignorée sans erreur : le site peut en introduire de
nouvelles sans casser un pont plus ancien.

---

### `POST /zone`

Enregistre un lieu nommé capturé en jeu.

```json
{ "name": "Bellevue Hospital Center", "pos_x": 1204, "pos_y": -880, "pos_z": 64, "radius": 500 }
```

```json
{ "ok": true, "id": 7, "grid_address": "247 W 47th St" }
```

`grid_address` renvoie ce que la grille calcule à cet endroit : c'est
l'outil de calage de la grille.

---

## Exemples

### curl

```bash
curl -H "X-ZF-Key: VOTRE_CLE" https://mdt.exemple.fr/api/game/ping
```

```bash
curl -X POST https://mdt.exemple.fr/api/game/call \
  -H "X-ZF-Key: VOTRE_CLE" \
  -H "Content-Type: application/json" \
  -d '{"dept":"fire","type_code":"111","pos_x":1204,"pos_y":-880,"narrative":"Flammes visibles."}'
```

### Lua (GMod)

Le pont expose déjà les helpers :

```lua
ZFBridge.Post( "/api/game/call", {
    dept      = "police",
    type_code = "10-30",
    priority  = 1,
    pos_x     = math.floor( pos.x ),
    pos_y     = math.floor( pos.y ),
    narrative = "Braquage en cours.",
    source    = "auto",
}, function( ok, data )
    if ok then print( "Incident " .. data.number ) end
end )
```

Un coupe-circuit espace automatiquement les tentatives après plusieurs
échecs consécutifs, pour ne pas marteler un site injoignable. Une erreur
applicative (`4xx`) ne le déclenche pas — seules les pannes réseau et les
`5xx` comptent comme une panne de liaison.

---

## Étendre l'API

1. Déclarez la route dans `app/routes.php` avec `->api()`
2. Ajoutez la méthode dans `app/Controllers/Api/GameController.php`
3. Retournez `Response::apiOk([...])` ou `Response::apiError('...', 422)`

Les valeurs entrantes se lisent **toujours** via `$request->str()`,
`->int()`, `->bool()`, `->arr()` ou `->enum()` : elles bornent la longueur,
nettoient les caractères de contrôle et contraignent les listes fermées.
Ne lisez jamais `$_POST` directement.
