# Infrastructure runtime et utils Unity

## Dossier

`Assets/Src/Utils/` contient l'infrastructure Unity qui ne relève pas directement d'un domaine métier.

<table id="bkmrk-composantr%C3%B4le-contex"><thead><tr><th>Composant</th><th>Rôle</th></tr></thead><tbody><tr><td>`ContextState`, `State`, `Machine`</td><td>Fondations des machines d'état.</td></tr><tr><td>`ScheduledMachine`, `ContextScheduledMachine`, `Scheduler`</td><td>Orchestration d'états/tâches planifiées.</td></tr><tr><td>`Coroutiner`</td><td>Point d'accès aux coroutines pour les classes non-MonoBehaviour.</td></tr><tr><td>`GameEventMessageUtils`</td><td>Événements locaux UI/runtime distincts des messages RTT serveur.</td></tr><tr><td>`GlobalTime`</td><td>Temps serveur/global.</td></tr><tr><td>`SharedConstants`</td><td>GameUid, SimType et constantes transversales.</td></tr><tr><td>`Utils` / `UtilsBase`</td><td>Helpers client communs.</td></tr><tr><td>`Extensions`</td><td>Méthodes d'extension C#.</td></tr><tr><td>`ObjectPool`, `PoolableObject`, `PoolsManager`</td><td>Pooling d'objets Unity.</td></tr><tr><td>`ImageLoader`</td><td>Chargement d'images.</td></tr><tr><td>`SoundsManager`</td><td>Audio commun.</td></tr><tr><td>`TextsManager`</td><td>Localisation; documenté dans la page Textes.</td></tr><tr><td>`GameSkinReloader`</td><td>Rechargement du skin.</td></tr><tr><td>`ExcelUtils`</td><td>Helpers d'export tableur utilisés notamment en By Module/WebGL.</td></tr><tr><td>`Tweener`</td><td>Animation/tween utilitaire.</td></tr><tr><td>`HashInformation*`</td><td>Informations/erreurs liées aux contrôles d'intégrité.</td></tr></tbody></table>

## CommandManager

`CommandManager.cs` est un gestionnaire historique des commandes **GameSparks**. Sa méthode `SendCommand()` refuse l'appel si le backend courant n'est pas `eGameSparks`. La voie BrainCloud moderne passe par `BackEndBrainCloud.SendCommand()`; ne pas confondre les deux systèmes.

## Événements locaux vs RTT

```mermaid
flowchart LR
LOCAL[GameEventMessageUtils] --> UI[UI / transitions locales]
SERVER[BrainCloud RTT] --> DISPATCH[MessagesDispatcher]
DISPATCH --> CTRL[Controllers]
CTRL --> UI

```

Un événement UI local n'est pas une synchronisation serveur. Pour une feature multi-utilisateur, il faut généralement persister côté serveur puis recevoir un message RTT.

## Règle de maintenance

Éviter d'ajouter de grosses règles métier dans `Utils.cs`. Une fonction Accounting, MK1, HR1, etc. doit vivre avec son domaine. Le dossier Utils doit rester transversal.

## Carte réelle de l’infrastructure runtime

<table id="bkmrk-composant-r%C3%B4le-%C3%80-uti"><thead><tr><th>Composant</th><th>Rôle</th><th>À utiliser pour</th></tr></thead><tbody><tr><td>`Game`</td><td>Composition root Unity et accès aux services globaux.</td><td>Services uniques réellement globaux au runtime.</td></tr><tr><td>`Scheduler`</td><td>Ordonnancement déterministe Update/LateUpdate par priorité.</td><td>FSM et callbacks qui doivent respecter un ordre de frame.</td></tr><tr><td>`Coroutiner`</td><td>MonoBehaviour hôte pour coroutines lancées par du code non-MonoBehaviour.</td><td>Backend IEnumerator, délais UI, séquences async Unity.</td></tr><tr><td>`ObjectPool<T>` / `PoolsManager`</td><td>Réutilisation de prefabs/contrôles.</td><td>Rows, pages, items et objets UI fréquemment créés/détruits.</td></tr><tr><td>`GameEventMessageUtils`</td><td>Bus d’événements Doozy local au client.</td><td>Navigation/UI locale.</td></tr><tr><td>`MessagesDispatcher`</td><td>Distribution des messages RTT serveur.</td><td>Synchronisation backend → client.</td></tr><tr><td>`Extensions`</td><td>Extensions génériques Action/UI/collections.</td><td>Helpers sans état métier.</td></tr><tr><td>`UtilsBase`</td><td>Helpers bas niveau stables, temps/UIDs.</td><td>Logique pure sans dépendance UI.</td></tr><tr><td>`Utils`</td><td>Helpers applicatifs historiques très larges.</td><td>Compatibilité/usage existant; éviter d’y ajouter automatiquement toute nouvelle logique.</td></tr><tr><td>`CommandManager`</td><td>Throttling/loading des anciennes requêtes GameSparks.</td><td>**Legacy GameSparks seulement.**</td></tr></tbody></table>

## Composition dans Game.Awake()

`Game` est le composition root principal. Il initialise notamment :

- `PoolsManager` depuis une référence sérialisée;
- `Scheduler = new Scheduler()`;
- `MessagesDispatcher = new MessagesDispatcher()`;
- `FxMgr`, `CommandManager`, `Toaster`;
- `NotificationManager`;
- `TextsManager`;
- la machine d’état via `ContextScheduledMachine<GameStateDTO>`.

`Game.Update()` appelle `Scheduler.Update()`; `Game.LateUpdate()` appelle `Scheduler.LateUpdate()`. Un service enregistré au Scheduler n’a donc pas besoin de son propre `MonoBehaviour.Update()`.

## Ajouter un service runtime global de A à Z

Ne rendre un service global que s’il doit réellement exister une seule fois pendant toute la session. Un service propre à une simulation, équipe ou popup doit rester dans son controller/contexte.

### 1. Choisir MonoBehaviour ou classe pure

- classe pure : pas besoin de scene lifecycle, inspector ou coroutine;
- `MonoBehaviour` : dépend de références Unity sérialisées, coroutines, callbacks Unity ou scene objects.

### 2. Ajouter la propriété dans Game seulement si elle est globale

```csharp
public static MyService MyService { get; private set; }
```

Pour une classe pure, l’instancier dans `Game.Awake()`. Pour un MonoBehaviour, injecter une référence sérialisée depuis la scène/prefab puis assigner la propriété.

### 3. Définir explicitement son lifecycle

Décider :

- quand il est initialisé;
- s’il dépend de `Backend` ou de `CurrentGameUid`;
- ce qui doit être reset lors d’un changement de simulation;
- ce qui doit être nettoyé dans `OnDestroy()` ou logout.

### 4. Ne pas créer un deuxième singleton caché

Si le service est déjà accessible via `Game`, éviter un second `static Instance` sauf raison Unity spécifique. Les doubles singletons rendent les tests et changements de scène difficiles à tracer.

### 5. Tester relog / changement de jeu

Un service global qui garde une référence au vieux `TeamController`, à une ancienne simulation ou à un prefab détruit produit des bugs fantômes. Tester au minimum : login → simulation A → logout/reload → simulation B.

## Scheduler — ordonnancement par priorité

Les priorités sont exécutées dans cet ordre :

```
BeginFrame
ServerEvent
Fsm
World
Render
Ui
EndFrame
```

Le même ordre existe pour le registry de `LateUpdate`.

### Ajouter un callback planifié

```csharp
Game.Scheduler.Register(
    Scheduler.Priority.Ui,
    UpdateMyControl
);

// nettoyage obligatoire
Game.Scheduler.Unregister(
    Scheduler.Priority.Ui,
    UpdateMyControl
);
```

`Scheduler.Unregister()` sait gérer une désinscription pendant qu’une priorité est en cours d’itération : le callback est placé dans `_toRemove` puis retiré après la boucle. Il faut néanmoins utiliser **exactement le même delegate** lors du unregister.

### ScheduledMachine

`ScheduledMachine.Start()` s’enregistre automatiquement dans la priorité `Fsm` pour Update et LateUpdate. `Stop()` se désenregistre puis déclenche `onStop`. C’est la raison pour laquelle la machine d’état principale n’a pas son propre `MonoBehaviour.Update()`.

## Coroutiner — lancer une coroutine hors MonoBehaviour

`Game.Instance.coroutiner` sert de point d’ancrage lorsque le code appelant n’est pas lui-même un MonoBehaviour :

```csharp
Coroutine c = Game.Instance.coroutiner.StartLocalCoroutine(
    MyRoutine()
);

// si nécessaire
Game.Instance.coroutiner.StopLocalCoroutine(c);
```

Ne pas laisser une coroutine continuer après destruction logique de son owner. Si elle modifie un popup/controller qui peut disparaître, conserver la référence et l’arrêter au teardown.

## ObjectPool et PoolsManager

`ObjectPool<T>` possède deux modes :

- **local pool** — défaut actuel : le template et les instances restent gérés par le pool local;
- **global PoolsManager** — `isLocalPool = false` : le template est enregistré par son `Type` dans `Game.PoolsManager`.

### Créer un objet poolable

```csharp
public class MyRow : PoolableObject
{
    public override void Alloc()
    {
        // remettre l’état nécessaire pour une nouvelle utilisation
    }

    public override void Free()
    {
        // retirer listeners, vider données temporaires, arrêter tweens
    }
}
```

### Utilisation

```csharp
_pool = new ObjectPool<MyRow>(rowTemplate, contentRoot);

MyRow row = _pool.AllocTemplate();
row.SetData(data);

// plus tard
_pool.FreeTemplate(row);
// ou
_pool.FreeAllTemplates();
```

### Règle critique

`Free()` doit remettre l’objet dans un état réutilisable. Un pool masque facilement des bugs de lifecycle : listener ajouté à chaque `Alloc()`, tween encore actif, référence à l’ancien modèle, event global non retiré.

## GameEventMessageUtils vs RTT vs Action locale

<table id="bkmrk-besoin-outil-changer"><thead><tr><th>Besoin</th><th>Outil</th></tr></thead><tbody><tr><td>Changer un panel/navigation dans le même client</td><td>`GameEventMessageUtils.SendEvent()`.</td></tr><tr><td>Prévenir des objets C# directement liés</td><td>`Action`/`event` local.</td></tr><tr><td>Synchroniser un changement venant du serveur</td><td>`MessagesDispatcher` + RTT.</td></tr><tr><td>Communiquer entre deux utilisateurs</td><td>Serveur + persistance + RTT, jamais Doozy local.</td></tr></tbody></table>

`GameEventMessageUtils` contient par exemple les événements de tabs, login/scenario/choose team et quelques événements By Module/MK. Ce bus ne persiste rien et ne traverse pas le réseau.

## Extensions.Fire() et diagnostic des callbacks

Les extensions `Action.Fire()` ne sont pas de simples `?.Invoke()` : elles parcourent la liste des delegates et encadrent chaque appel avec `CrashMessages.CrashMessage.StackPush/StackPop`. Cela permet de conserver plus de contexte lorsqu’un callback plante.

Pour les events existants qui utilisent ce pattern, préférer `.Fire()` afin de conserver ce diagnostic.

## Temps : GlobalTime, UtilsBase et temps serveur

`GlobalTime.UtcNow` retourne actuellement directement `DateTime.UtcNow` converti en epoch ms. L’ancien mécanisme de resynchronisation périodique serveur est commenté.

En parallèle, `Game.RefreshCurrentServerTime()` charge explicitement l’heure via le backend et `Game.Update()` fait avancer `currentServerTime`.

Donc, pour une règle métier sensible au serveur, ne pas supposer que `GlobalTime.UtcNow` est une horloge serveur synchronisée.

## Où placer un nouvel helper

<table id="bkmrk-type-d%E2%80%99helper-emplac"><thead><tr><th>Type d’helper</th><th>Emplacement recommandé</th></tr></thead><tbody><tr><td>Conversion temps/UID pure et partagée</td><td>`UtilsBase`.</td></tr><tr><td>Extension naturelle d’un type C# / Unity</td><td>`Extensions`.</td></tr><tr><td>Helper d’un domaine précis</td><td>Classe/domain utils du domaine, pas `Utils.cs`.</td></tr><tr><td>Helper UI très spécifique</td><td>Contrôle/base UI concerné.</td></tr><tr><td>Helper game-specific</td><td>`GameSpecific/<gameUid>`.</td></tr><tr><td>Compatibilité avec un ancien helper déjà centralisé</td><td>`Utils`, seulement si cela reste cohérent avec l’existant.</td></tr></tbody></table>

## CommandManager : ne pas l’utiliser pour BrainCloud

`CommandManager.SendCommand()` accepte des `GSTypedRequest` et vérifie explicitement que `Game.Backend.BackEndType == eGameSparks`. Il gère l’ancien throttling GameSparks et le loading circle.

Une nouvelle feature BrainCloud doit passer par `BackEndBrainCloud.SendCommand()` / les méthodes de `BackEndBase`, jamais par `CommandManager`.

## Checklist avant d’ajouter une nouvelle infrastructure

- Le besoin est-il vraiment global ?
- Peut-il vivre dans un controller existant ?
- Le lifecycle de cleanup est-il défini ?
- Scheduler plutôt qu’un Update supplémentaire ?
- Coroutine arrêtée si owner détruit ?
- Pool : `Alloc()`/`Free()` symétriques ?
- Event local ou RTT correctement choisi ?
- Helper placé dans le bon domaine plutôt que dans `Utils.cs` ?
- Aucune dépendance GameSparks introduite dans une nouvelle feature BrainCloud ?
- Relog/changement de simulation testé ?

## Debug rapide

<table id="bkmrk-sympt%C3%B4me-zone-probab"><thead><tr><th>Symptôme</th><th>Zone probable</th></tr></thead><tbody><tr><td>Callback exécuté deux fois après navigation</td><td>listener/Scheduler non désinscrit ou pool `Free()` incomplet.</td></tr><tr><td>Objet réutilisé avec anciennes données</td><td>`PoolableObject.Free/Alloc`.</td></tr><tr><td>FSM cesse d’avancer</td><td>`ScheduledMachine` stoppée/désinscrite ou exception dans callback Fsm.</td></tr><tr><td>UI locale ne bouge pas</td><td>Doozy event / Action locale.</td></tr><tr><td>Autre client ne bouge pas</td><td>Ce n’est pas `GameEventMessageUtils`; suivre RTT/persistance.</td></tr><tr><td>Heure différente du serveur</td><td>`GlobalTime` vs `Game.currentServerTime`.</td></tr><tr><td>Exception “Cannot use gamesparks”</td><td>Nouvelle logique branchée par erreur sur `CommandManager`.</td></tr></tbody></table>