- Add hybrid deployment modes: local_dev (MVP) and production_pwa (optional) - Integrate WarFactory engine reuse with hot-reload 0.4ms - Define multi-target compilation strategy (DLL/SO/WASM) - Detail both deployment modes with cost analysis - Add progressive roadmap: Phase 1 (local), Phase 2 (POC WASM), Phase 3 (cloud) - Budget clarified: $10-20/mois (local) vs $13-25/mois (cloud) - Document open questions for technical validation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
13 KiB
Message Communication System
Vue d'ensemble
Le système de communication inter-modules utilise des classes de messages typées au lieu de JSON brut pour garantir la fiabilité et la maintenabilité.
Concept fondamental
Une classe = un type de message Une instance = un message individuel
Exemple: La classe TankMovedMessage définit le type "mouvement de tank". Chaque fois qu'un tank bouge, on crée une nouvelle instance de cette classe.
Pourquoi des classes?
Problème avec JSON brut
- Erreurs découvertes uniquement au runtime
- Pas de validation automatique
- Typos dans les clés non détectées
- Difficile à maintenir et refactoriser
Solution avec classes
- Validation au compile-time
- Contrat clair entre modules
- IDE autocomplétion et refactoring
- Hot-reload friendly (petites classes isolées)
- Évolutif (ajout de champs sans casser l'existant)
Architecture
IMessage - Interface de base
Interface pure définissant le contrat minimal pour tous les messages:
- Type identification: Chaque message déclare son type via enum
- Serialization: Conversion vers JSON pour transport via IIO
- Deserialization: Reconstruction depuis JSON reçu
AMessage - Classe abstraite avec métadata
Classe abstraite obligatoire fournissant l'implémentation partielle commune à tous les messages.
Métadata immutables (enforced):
timestamp- Horodatage de création (const)sender- Module émetteur (const)messageId- ID unique pour tracking et reassemblage fragments (const)partId- ID de fragment pour messages multi-parts (const)
Caractéristiques:
- Constructeur protégé force passage par classes enfants
- Métadata initialisées automatiquement à la construction
- Impossible de modifier métadata après création
- Tous messages garantis d'avoir timestamp/sender/messageId
MessageType - Enum central
Enum listant tous les types de messages du système:
TANK_MOVED,TANK_FIRED,TANK_DESTROYEDPRICE_UPDATED,TRADE_EXECUTEDITEM_PRODUCED,BELT_CONGESTION- etc.
Pourquoi enum plutôt que strings?
- Performance (comparaison d'entiers)
- Type safety (typos détectées au compile)
- Centralisation (tous les types visibles)
Messages concrets
Chaque type de message est une classe dédiée héritant de AMessage:
TankMovedMessage: Position, vitesse, ID tank PriceUpdatedMessage: Item, ancien prix, nouveau prix ItemProducedMessage: Item type, quantité, factory ID
Chaque classe:
- Hérite obligatoirement de
AMessage - Appelle constructeur
AMessage(senderModule) - Stocke ses données spécifiques
- Valide à la construction
- Sérialise/désérialise son propre format
Flow de communication
Publication
Exemple complet:
void TankModule::process(const json& input) {
// 1. Calculer nouvelle position
Vector2 newPos = calculatePosition();
float currentSpeed = getSpeed();
// 2. Créer message (validation automatique à la construction)
TankMovedMessage msg(newPos, currentSpeed, tankId);
// 3. Sérialiser en JSON
json serialized = msg.serialize();
// 4. Publier via IIO
io->publish("tank:" + std::to_string(tankId), serialized);
}
Étapes:
- Module crée instance de message (validation à la construction)
- Message sérialisé en JSON via
serialize() - Publié via
IIO::publish(topic, json) - IIO route vers subscribers du topic
Réception
Méthode 1: Type-safe template helper (recommandé)
// Clean syntax avec type safety automatique
auto tankMsg = io->pullMessageAs<TankMovedMessage>();
if (tankMsg) { // nullptr si type mismatch ou queue vide
Vector2 pos = tankMsg->getPosition();
float speed = tankMsg->getSpeed();
}
Méthode 2: Manuelle (si besoin de flexibilité)
// Récupère message brut
Message rawMsg = io->pullMessage();
// Désérialise vers base
std::unique_ptr<IMessage> baseMsg = IMessage::deserialize(rawMsg.data);
// Cast vers type concret
if (TankMovedMessage* tankMsg = dynamic_cast<TankMovedMessage*>(baseMsg.get())) {
// Accès type-safe
}
Template helper implementation:
class IIO {
// Template helper inline (zero overhead)
template<typename T>
std::unique_ptr<T> pullMessageAs() {
if (!hasMessages()) return nullptr;
Message raw = pullMessage();
std::unique_ptr<IMessage> base = IMessage::deserialize(raw.data);
T* casted = dynamic_cast<T*>(base.get());
if (!casted) return nullptr; // Type mismatch
base.release();
return std::unique_ptr<T>(casted);
}
};
Performance:
- Template inlined = zero function call overhead
dynamic_cast= ~5-10ns (négligeable vs JSON parsing)- Bottleneck réel = JSON serialization, pas le cast
Désérialisation centralisée
Factory pattern avec routing par type:
// IMessage base class
std::unique_ptr<IMessage> IMessage::deserialize(const json& data) {
// Extract type from JSON
int typeInt = data.value("type", -1);
MessageType type = static_cast<MessageType>(typeInt);
// Route to concrete deserializer
switch (type) {
case MessageType::TANK_MOVED:
return TankMovedMessage::deserialize(data);
case MessageType::PRICE_UPDATED:
return PriceUpdatedMessage::deserialize(data);
case MessageType::ITEM_PRODUCED:
return ItemProducedMessage::deserialize(data);
// Add new messages here
default:
return nullptr; // Unknown type
}
}
Chaque message implémente sa propre désérialisation:
class TankMovedMessage : public AMessage {
static std::unique_ptr<TankMovedMessage> deserialize(const json& data) {
try {
Vector2 pos{data["position"]["x"], data["position"]["y"]};
float speed = data["speed"];
int tankId = data["tankId"];
return std::make_unique<TankMovedMessage>(pos, speed, tankId);
} catch (const json::exception& e) {
return nullptr; // Malformed JSON
}
}
};
Gestion des erreurs:
- JSON malformé → retourne
nullptr - Type inconnu → retourne
nullptr - Validation échoue → exception à la construction
- Modules doivent vérifier
if (msg != nullptr)
Organisation du code
Location des messages
modules/shared/messages/
├── IMessage.h # Interface pure
├── AMessage.h # Classe abstraite avec métadata
├── MessageType.h # Enum des types
├── TankMovedMessage.h
├── PriceUpdatedMessage.h
└── ...
Rationale:
- Single source of truth (pas de duplication)
- Contrat partagé entre émetteur et récepteur
- Facile à trouver et maintenir
Validation
À la construction: Invariants validés immédiatement
- Vitesse négative → exception
- ID invalide → exception
- Fail fast pour détecter bugs tôt
À la désérialisation: Données réseau/externes validées
- JSON malformé → retourne nullptr
- Champs manquants → retourne nullptr
- Protège contre données corrompues
Décisions de design finalisées
Versioning - Breaking changes assumées
Décision: Types versionnés avec breaking changes strictes
Changement de format = nouveau type + nouvelle classe:
TANK_MOVED_V1→TANK_MOVED_V2= types différents- Pas de backward compatibility
- Pas de forward compatibility
- Migration forcée de tous les modules
Rationale:
- Zéro ambiguïté sur le format attendu
- Pas de logique conditionnelle complexe
- Hot-reload force mise à jour synchronisée
- Détection immédiate des incompatibilités
- Code simple et clair
Conséquence:
- Format change → tous modules doivent migrer
- Ancien code ne compile plus → migration manuelle obligatoire
- Breaking changes explicites et visibles
Message inheritance - AMessage obligatoire
Décision: Classe abstraite AMessage avec métadata immutables enforced
Architecture:
IMessage (interface pure)
↓
AMessage (classe abstraite - métadata immutables)
↓
TankMovedMessage, PriceUpdatedMessage... (classes concrètes)
Enforcement:
- Constructeur
AMessageprotégé → impossible de créer message sans passer par enfant - Métadata const → immutables après construction
- Tous messages garantis d'avoir timestamp/sender/messageId
- Format uniforme pour tous messages du système
Rationale:
- Pas de duplication (métadata dans AMessage)
- Impossible d'oublier métadata
- Contrat strict et enforced au compile-time
- Simplicité pour messages concrets (métadata automatique)
Décisions de design finalisées (suite)
Size limits - Fragmentation automatique par IO
Décision: Pas de limite au niveau message, fragmentation automatique par couche transport
Architecture:
- Modules: Publient/reçoivent messages complets (transparence totale)
- IIO: Gère fragmentation/défragmentation automatiquement
- Transport adaptatif: Fragmentation si nécessaire selon type IO
Fragmentation par type:
- IntraIO: Pas de fragmentation (copie mémoire directe)
- LocalIO: Fragmentation si > seuil (ex: 64KB chunks)
- NetworkIO: Fragmentation si > MTU (~1500 bytes)
Mécanisme:
- Module publie message → sérialisé en JSON
- IO détecte si taille > seuil transport
- Si oui: découpe en fragments avec
messageId+partId - Transport des fragments
- IO destination reassemble via
messageId(collect touspartId) - Module reçoit message complet via
pullMessage()
Robustesse:
- Packet loss: Timeout si fragments incomplets (ex: 30s)
- Ordering:
partIdgarantit ordre de reassemblage - Monitoring: Log warning si message > 100KB (design smell probable)
Conséquence:
- Messages peuvent être arbitrairement gros
- Complexité cachée dans couche IO
- Performance optimisée par type de transport
Async handling - Délégation au module
Décision: Pas de gestion async spéciale au niveau messages
Rationale:
- Messages = transport de données uniquement
ITaskSchedulerexiste déjà pour opérations coûteuses- Module décide si déléguer ou traiter directement
- Keep messages simple et stupid
Responsabilité:
- Message: Transport de données (aucune intelligence)
- Module: Décide de déléguer opérations coûteuses au scheduler
- ITaskScheduler: Gère threading et async
Exemple:
// Module reçoit message déclenchant opération coûteuse
if (PathfindingRequestMessage* req = dynamic_cast<...>) {
// Module délègue au TaskScheduler
scheduler->scheduleTask(PATHFINDING_TASK, req->serialize());
// Continue traitement autres messages
}
// Plus tard, récupère résultat
if (scheduler->hasCompletedTasks() > 0) {
TaskResult result = scheduler->getCompletedTask();
// Publie résultat via message
}
Message ordering - Pas de garantie + Messages remplaçables
Décision: Pas de garantie d'ordre, modules gèrent via timestamp si critique
Approche:
- Pas de FIFO enforced: Messages peuvent arriver dans ordre arbitraire
- Messages remplaçables par défaut:
SubscriptionConfig.replaceable = truepour majorité - Timestamp disponible:
msg->getTimestamp()permet tri manuel si nécessaire - Utilité questionnable: Architecture modulaire rend ordre moins critique
Rationale:
- Performance maximale (pas de garanties coûteuses)
- Messages remplaçables optimisent bandwidth (garde juste dernier)
- Timestamp dans
AMessagepermet tri si vraiment nécessaire - Architecture découple les dépendances temporelles
Exemples d'usage:
// Prix - remplaçable (dernière valeur suffit)
io->subscribeLowFreq("economy:*", {.replaceable = true});
// Tank position - remplaçable (position actuelle suffit)
io->subscribe("tank:*", {.replaceable = true});
// Events critiques - non remplaçable + tri si nécessaire
io->subscribe("combat:*", {.replaceable = false});
// Si ordre vraiment critique (rare)
std::vector<Message> messages = collectAllMessages();
std::sort(messages.begin(), messages.end(),
[](const Message& a, const Message& b) {
return a.getTimestamp() < b.getTimestamp();
});
Conséquence:
- Modules ne doivent pas assumer ordre de réception
- Messages remplaçables = dernière valeur seulement
- Tri par timestamp disponible mais rarement nécessaire
Questions ouvertes
Aucune question ouverte restante. Toutes les décisions de design ont été finalisées.
Statut d'implémentation
Phase actuelle: Design et documentation
Prochaines étapes:
- Créer interface
IMessage.h - Créer classe abstraite
AMessage.havec métadata immutables - Créer enum
MessageType.h - Implémenter 5-10 messages d'exemple
- Tester avec modules existants
- Itérer selon usage réel
Références
src/core/include/warfactory/IIO.h- Couche pub/subdocs/01-architecture/architecture-technique.md- Patterns de communicationdocs/03-implementation/CLAUDE-HOT-RELOAD-GUIDE.md- Impact sur design messages