Chief communication dispatcher! Consul Caesar.js noticed that our legion needs better communication organization. Not all messages should reach all cohorts! Time to learn how to organize communication into rooms and namespaces - like different signal channels for different purposes.
Imagine you are a signaller at the legate's headquarters. You have multiple channels at your disposal:
1// services/room-management.service.ts
2import { Injectable, Logger } from '@nestjs/common';
3import { LegionCommandGateway } from '../gateways/legion-command.gateway';
4
5export enum RoomType {
6 COMMAND = 'command',
7 NAVIGATION = 'navigation',
8 BATTLE = 'battle',
9 TRADE = 'trade',
10 EMERGENCY = 'emergency',
11 SOCIAL = 'social',
12 MISSION = 'mission',
13 REGIONAL = 'regional',
14}
15
16export interface RoomConfig {
17 name: string;
18 type: RoomType;
19 maxMembers?: number;
20 requiresPassword?: boolean;
21 minimumRank?: string;
22 allowedLegionTypes?: string[];
23 moderators?: string[];
24 persistent?: boolean;
25 autoJoin?: boolean;
26}
27
28export interface RoomMember {
29 cohortId: string;
30 cohortName: string;
31 centurion: string;
32 joinedAt: Date;
33 role: 'MEMBER' | 'MODERATOR' | 'OWNER';
34 muted?: boolean;
35}
36
37@Injectable()
38export class RoomManagementService {
39 private readonly logger = new Logger(RoomManagementService.name);
40 private rooms = new Map<string, RoomConfig>();
41 private roomMembers = new Map<string, Map<string, RoomMember>>();
42 private roomPasswords = new Map<string, string>();
43 private bannedFromRooms = new Map<string, Set<string>>();
44
45 constructor(private legionGateway: LegionCommandGateway) {
46 this.initializeDefaultRooms();
47 }
48
49 // Creating a new room
50 async createRoom(
51 roomName: string,
52 config: Partial<RoomConfig>,
53 creatorLegionId: string
54 ): Promise<{ success: boolean; roomName?: string; error?: string }> {
55 // Check if room already exists
56 if (this.rooms.has(roomName)) {
57 return { success: false, error: 'Room already exists' };
58 }
59
60 // Check creator permissions
61 const creator = this.legionGateway.getLegionById(creatorLegionId);
62 if (!creator) {
63 return { success: false, error: 'Creator cohort not found' };
64 }
65
66 const roomConfig: RoomConfig = {
67 name: roomName,
68 type: config.type || RoomType.SOCIAL,
69 maxMembers: config.maxMembers || 50,
70 requiresPassword: config.requiresPassword || false,
71 minimumRank: config.minimumRank,
72 allowedLegionTypes: config.allowedLegionTypes,
73 moderators: [creatorLegionId],
74 persistent: config.persistent || false,
75 autoJoin: config.autoJoin || false,
76 };
77
78 // Save room configuration
79 this.rooms.set(roomName, roomConfig);
80 this.roomMembers.set(roomName, new Map());
81
82 // If requires password, save it
83 if (config.requiresPassword && config.password) {
84 this.roomPasswords.set(roomName, config.password);
85 }
86
87 // Automatically add creator
88 await this.addMemberToRoom(roomName, creatorLegionId, 'OWNER');
89
90 this.logger.log(`Created room: ${roomName} (${config.type})`);
91
92 return { success: true, roomName };
93 }
94
95 // Joining a room
96 async joinRoom(
97 roomName: string,
98 cohortId: string,
99 password?: string
100 ): Promise<{ success: boolean; error?: string; roomInfo?: any }> {
101 const room = this.rooms.get(roomName);
102 const cohort = this.legionGateway.getLegionById(cohortId);
103
104 if (!room) {
105 return { success: false, error: 'Room does not exist' };
106 }
107
108 if (!cohort) {
109 return { success: false, error: 'Legion not found' };
110 }
111
112 // Check if cohort is banned
113 if (this.bannedFromRooms.get(roomName)?.has(cohortId)) {
114 return { success: false, error: 'Banned from this room' };
115 }
116
117 // Check password
118 if (room.requiresPassword) {
119 const correctPassword = this.roomPasswords.get(roomName);
120 if (!password || password !== correctPassword) {
121 return { success: false, error: 'Invalid password' };
122 }
123 }
124
125 // Check member limit
126 const currentMembers = this.roomMembers.get(roomName);
127 if (currentMembers && currentMembers.size >= room.maxMembers) {
128 return { success: false, error: 'Room is full' };
129 }
130
131 // Check minimum requirements
132 if (!this.meetsRoomRequirements(cohort, room)) {
133 return { success: false, error: 'Does not meet room requirements' };
134 }
135
136 // Add to room
137 await this.addMemberToRoom(roomName, cohortId, 'MEMBER');
138
139 // Notify other members
140 this.legionGateway.broadcastToRoom(roomName, 'room:member-joined', {
141 cohortName: cohort.name,
142 centurion: cohort.centurion,
143 timestamp: new Date().toISOString(),
144 });
145
146 const roomInfo = this.getRoomInfo(roomName);
147
148 this.logger.log(`${cohort.name} joined room: ${roomName}`);
149
150 return { success: true, roomInfo };
151 }
152
153 // Leaving a room
154 async leaveRoom(
155 roomName: string,
156 cohortId: string
157 ): Promise<{ success: boolean; error?: string }> {
158 const room = this.rooms.get(roomName);
159 const members = this.roomMembers.get(roomName);
160 const cohort = this.legionGateway.getLegionById(cohortId);
161
162 if (!room || !members || !cohort) {
163 return { success: false, error: 'Room or cohort not found' };
164 }
165
166 const member = members.get(cohortId);
167 if (!member) {
168 return { success: false, error: 'Not a member of this room' };
169 }
170
171 // Remove from room
172 members.delete(cohortId);
173
174 // Notify other members
175 this.legionGateway.broadcastToRoom(roomName, 'room:member-left', {
176 cohortName: cohort.name,
177 timestamp: new Date().toISOString(),
178 });
179
180 // If it was the owner, transfer ownership
181 if (member.role === 'OWNER' && members.size > 0) {
182 const newOwner = Array.from(members.values())
183 .find(m => m.role === 'MODERATOR') ||
184 Array.from(members.values())[0];
185
186 newOwner.role = 'OWNER';
187
188 this.legionGateway.broadcastToRoom(roomName, 'room:ownership-transferred', {
189 newOwner: newOwner.cohortName,
190 timestamp: new Date().toISOString(),
191 });
192 }
193
194 // Delete room if empty and not persistent
195 if (members.size === 0 && !room.persistent) {
196 this.deleteRoom(roomName);
197 }
198
199 this.logger.log(`${cohort.name} left room: ${roomName}`);
200
201 return { success: true };
202 }
203
204 // Room search
205 searchRooms(criteria: {
206 type?: RoomType;
207 hasSpace?: boolean;
208 keyword?: string;
209 publicOnly?: boolean;
210 }): RoomConfig[] {
211 return Array.from(this.rooms.values()).filter(room => {
212 if (criteria.type && room.type !== criteria.type) {
213 return false;
214 }
215
216 if (criteria.keyword && !room.name.toLowerCase().includes(criteria.keyword.toLowerCase())) {
217 return false;
218 }
219
220 if (criteria.publicOnly && room.requiresPassword) {
221 return false;
222 }
223
224 if (criteria.hasSpace) {
225 const members = this.roomMembers.get(room.name);
226 if (members && members.size >= room.maxMembers) {
227 return false;
228 }
229 }
230
231 return true;
232 });
233 }
234
235 // Room moderation
236 async moderateRoom(
237 roomName: string,
238 moderatorLegionId: string,
239 action: 'MUTE' | 'UNMUTE' | 'KICK' | 'BAN' | 'PROMOTE' | 'DEMOTE',
240 targetLegionId: string,
241 reason?: string
242 ): Promise<{ success: boolean; error?: string }> {
243 const room = this.rooms.get(roomName);
244 const members = this.roomMembers.get(roomName);
245
246 if (!room || !members) {
247 return { success: false, error: 'Room not found' };
248 }
249
250 const moderator = members.get(moderatorLegionId);
251 const target = members.get(targetLegionId);
252
253 if (!moderator || !target) {
254 return { success: false, error: 'Moderator or target not found' };
255 }
256
257 // Check permissions
258 if (!['MODERATOR', 'OWNER'].includes(moderator.role)) {
259 return { success: false, error: 'Insufficient permissions' };
260 }
261
262 const moderatorLegion = this.legionGateway.getLegionById(moderatorLegionId);
263 const targetLegion = this.legionGateway.getLegionById(targetLegionId);
264
265 switch (action) {
266 case 'MUTE':
267 target.muted = true;
268 break;
269
270 case 'UNMUTE':
271 target.muted = false;
272 break;
273
274 case 'KICK':
275 await this.leaveRoom(roomName, targetLegionId);
276 break;
277
278 case 'BAN':
279 await this.leaveRoom(roomName, targetLegionId);
280 if (!this.bannedFromRooms.has(roomName)) {
281 this.bannedFromRooms.set(roomName, new Set());
282 }
283 this.bannedFromRooms.get(roomName).add(targetLegionId);
284 break;
285
286 case 'PROMOTE':
287 if (moderator.role === 'OWNER') {
288 target.role = 'MODERATOR';
289 }
290 break;
291
292 case 'DEMOTE':
293 if (moderator.role === 'OWNER' && target.role === 'MODERATOR') {
294 target.role = 'MEMBER';
295 }
296 break;
297 }
298
299 // Notify room about moderation action
300 this.legionGateway.broadcastToRoom(roomName, 'room:moderation', {
301 action,
302 moderator: moderatorLegion?.name,
303 target: targetLegion?.name,
304 reason,
305 timestamp: new Date().toISOString(),
306 });
307
308 this.logger.warn(`⚖ Room ${roomName}: ${action} on ${targetLegion?.name} by ${moderatorLegion?.name}`);
309
310 return { success: true };
311 }
312
313 // Helper methods
314 private async addMemberToRoom(
315 roomName: string,
316 cohortId: string,
317 role: 'MEMBER' | 'MODERATOR' | 'OWNER'
318 ) {
319 const cohort = this.legionGateway.getLegionById(cohortId);
320 if (!cohort) return;
321
322 const members = this.roomMembers.get(roomName);
323 if (!members) return;
324
325 const member: RoomMember = {
326 cohortId,
327 cohortName: cohort.name,
328 centurion: cohort.centurion,
329 joinedAt: new Date(),
330 role,
331 muted: false,
332 };
333
334 members.set(cohortId, member);
335
336 // Add to Socket.IO room
337 const socket = this.legionGateway.getSocketById(cohortId);
338 if (socket) {
339 await socket.join(roomName);
340 }
341 }
342
343 private meetsRoomRequirements(cohort: any, room: RoomConfig): boolean {
344 // Check cohort type
345 if (room.allowedLegionTypes && !room.allowedLegionTypes.includes(cohort.type)) {
346 return false;
347 }
348
349 // Check rank (rank checking logic would go here)
350 if (room.minimumRank) {
351 // Rank checking implementation
352 }
353
354 return true;
355 }
356
357 private getRoomInfo(roomName: string): any {
358 const room = this.rooms.get(roomName);
359 const members = this.roomMembers.get(roomName);
360
361 if (!room || !members) return null;
362
363 return {
364 name: roomName,
365 type: room.type,
366 memberCount: members.size,
367 maxMembers: room.maxMembers,
368 requiresPassword: room.requiresPassword,
369 members: Array.from(members.values()).map(m => ({
370 cohortName: m.cohortName,
371 centurion: m.centurion,
372 role: m.role,
373 joinedAt: m.joinedAt,
374 })),
375 };
376 }
377
378 private deleteRoom(roomName: string) {
379 this.rooms.delete(roomName);
380 this.roomMembers.delete(roomName);
381 this.roomPasswords.delete(roomName);
382 this.bannedFromRooms.delete(roomName);
383
384 this.logger.log(`Deleted empty room: ${roomName}`);
385 }
386
387 private initializeDefaultRooms() {
388 const defaultRooms: RoomConfig[] = [
389 {
390 name: 'legion-general',
391 type: RoomType.SOCIAL,
392 persistent: true,
393 autoJoin: true,
394 },
395 {
396 name: 'legate-command',
397 type: RoomType.COMMAND,
398 minimumRank: 'LEGATE',
399 persistent: true,
400 requiresPassword: true,
401 },
402 {
403 name: 'navigation-channel',
404 type: RoomType.NAVIGATION,
405 persistent: true,
406 },
407 {
408 name: 'emergency-response',
409 type: RoomType.EMERGENCY,
410 persistent: true,
411 },
412 {
413 name: 'trading-post',
414 type: RoomType.TRADE,
415 persistent: true,
416 },
417 ];
418
419 defaultRooms.forEach(config => {
420 this.rooms.set(config.name, config);
421 this.roomMembers.set(config.name, new Map());
422 });
423
424 // Set passwords for protected rooms
425 this.roomPasswords.set('legate-command', 'caesar-supreme');
426 }
427
428 // Public API
429 getAllRooms(): RoomConfig[] {
430 return Array.from(this.rooms.values());
431 }
432
433 getRoomMembers(roomName: string): RoomMember[] {
434 const members = this.roomMembers.get(roomName);
435 return members ? Array.from(members.values()) : [];
436 }
437
438 isLegionInRoom(roomName: string, cohortId: string): boolean {
439 return this.roomMembers.get(roomName)?.has(cohortId) || false;
440 }
441}1// services/namespace-management.service.ts
2import { Injectable, Logger } from '@nestjs/common';
3import { Server } from 'socket.io';
4
5export enum NamespaceType {
6 COMMAND = '/command',
7 BATTLE = '/battle',
8 TRADE = '/trade',
9 NAVIGATION = '/navigation',
10 INTELLIGENCE = '/intelligence',
11 DIPLOMACY = '/diplomacy',
12}
13
14@Injectable()
15export class NamespaceManagementService {
16 private readonly logger = new Logger(NamespaceManagementService.name);
17 private namespaces = new Map<string, any>();
18
19 constructor(private server: Server) {
20 this.initializeNamespaces();
21 }
22
23 private initializeNamespaces() {
24 // Command namespace
25 const commandNamespace = this.server.of(NamespaceType.COMMAND);
26 commandNamespace.use(this.createAuthMiddleware('COMMAND'));
27 this.setupCommandNamespace(commandNamespace);
28
29 // Battle namespace
30 const battleNamespace = this.server.of(NamespaceType.BATTLE);
31 battleNamespace.use(this.createAuthMiddleware('BATTLE'));
32 this.setupBattleNamespace(battleNamespace);
33
34 // Trade namespace
35 const tradeNamespace = this.server.of(NamespaceType.TRADE);
36 tradeNamespace.use(this.createAuthMiddleware('TRADE'));
37 this.setupTradeNamespace(tradeNamespace);
38
39 // Navigation namespace
40 const navigationNamespace = this.server.of(NamespaceType.NAVIGATION);
41 this.setupNavigationNamespace(navigationNamespace);
42
43 // Intelligence namespace
44 const intelligenceNamespace = this.server.of(NamespaceType.INTELLIGENCE);
45 intelligenceNamespace.use(this.createAuthMiddleware('INTELLIGENCE'));
46 this.setupIntelligenceNamespace(intelligenceNamespace);
47
48 this.logger.log('All namespaces initialized');
49 }
50
51 private createAuthMiddleware(requiredPermission: string) {
52 return (socket: any, next: any) => {
53 const hasPermission = this.checkNamespacePermission(socket, requiredPermission);
54 if (hasPermission) {
55 next();
56 } else {
57 next(new Error(`No ${requiredPermission} permission`));
58 }
59 };
60 }
61
62 private setupCommandNamespace(namespace: any) {
63 namespace.on('connection', (socket: any) => {
64 this.logger.log(` Command connection: ${socket.id}`);
65
66 socket.on('legion:order', (data: any) => {
67 // Broadcast order to all cohorts
68 namespace.emit('command:received', {
69 order: data.order,
70 from: data.from,
71 priority: data.priority,
72 timestamp: new Date().toISOString(),
73 });
74 });
75
76 socket.on('strategy:update', (data: any) => {
77 // Strategy update
78 socket.broadcast.emit('strategy:changed', data);
79 });
80 });
81 }
82
83 private setupBattleNamespace(namespace: any) {
84 namespace.on('connection', (socket: any) => {
85 this.logger.log(` Battle connection: ${socket.id}`);
86
87 socket.on('battle:join', (data: any) => {
88 socket.join(`battle-${data.battleId}`);
89 socket.to(`battle-${data.battleId}`).emit('combatant:joined', {
90 cohort: data.cohortName,
91 side: data.side,
92 });
93 });
94
95 socket.on('battle:action', (data: any) => {
96 namespace.to(`battle-${data.battleId}`).emit('battle:update', {
97 action: data.action,
98 performer: data.cohortName,
99 target: data.target,
100 result: data.result,
101 timestamp: new Date().toISOString(),
102 });
103 });
104 });
105 }
106
107 private setupTradeNamespace(namespace: any) {
108 namespace.on('connection', (socket: any) => {
109 this.logger.log(` Trade connection: ${socket.id}`);
110
111 socket.on('trade:offer', (data: any) => {
112 // Trade proposal
113 if (data.targetLegion) {
114 socket.to(data.targetLegion).emit('trade:proposal', {
115 from: data.from,
116 offer: data.offer,
117 requestedItems: data.requestedItems,
118 expires: new Date(Date.now() + 300000).toISOString(),
119 });
120 }
121 });
122
123 socket.on('trade:accept', (data: any) => {
124 namespace.emit('trade:completed', {
125 buyer: data.buyer,
126 seller: data.seller,
127 items: data.items,
128 value: data.value,
129 timestamp: new Date().toISOString(),
130 });
131 });
132 });
133 }
134
135 private setupNavigationNamespace(namespace: any) {
136 namespace.on('connection', (socket: any) => {
137 this.logger.log(`🧭 Navigation connection: ${socket.id}`);
138
139 socket.on('navigation:update', (data: any) => {
140 // Position update
141 socket.broadcast.emit('cohort:position', {
142 cohortId: socket.id,
143 position: data.position,
144 course: data.course,
145 speed: data.speed,
146 timestamp: new Date().toISOString(),
147 });
148 });
149
150 socket.on('weather:request', (data: any) => {
151 // Weather information request
152 socket.emit('weather:report', {
153 location: data.location,
154 forecast: this.generateWeatherReport(data.location),
155 timestamp: new Date().toISOString(),
156 });
157 });
158 });
159 }
160
161 private setupIntelligenceNamespace(namespace: any) {
162 namespace.on('connection', (socket: any) => {
163 this.logger.log(`Intelligence connection: ${socket.id}`);
164
165 socket.on('intel:report', (data: any) => {
166 // Intelligence report
167 namespace.emit('intel:update', {
168 type: data.type,
169 location: data.location,
170 details: data.details,
171 reliability: data.reliability,
172 source: 'CLASSIFIED',
173 timestamp: new Date().toISOString(),
174 });
175 });
176
177 socket.on('enemy:spotted', (data: any) => {
178 // Enemy detection
179 namespace.emit('threat:alert', {
180 threatLevel: data.threatLevel,
181 location: data.location,
182 enemyForce: data.enemyForce,
183 recommendedAction: data.recommendedAction,
184 timestamp: new Date().toISOString(),
185 });
186 });
187 });
188 }
189
190 private checkNamespacePermission(socket: any, permission: string): boolean {
191 // Permission checking logic would go here
192 const cohortType = socket.handshake.auth?.cohortType;
193 const rank = socket.handshake.auth?.rank;
194
195 switch (permission) {
196 case 'COMMAND':
197 return ['LEGATE', 'TRIBUNUS', 'CENTURION'].includes(rank);
198 case 'BATTLE':
199 return ['WARCOHORT', 'COHORS', 'LEGIO'].includes(cohortType);
200 case 'TRADE':
201 return true; // Everyone can trade
202 case 'INTELLIGENCE':
203 return ['SPY_COHORT', 'SCOUT'].includes(cohortType) || rank === 'INTELLIGENCE_OFFICER';
204 default:
205 return true;
206 }
207 }
208
209 private generateWeatherReport(location: any) {
210 // Weather report simulation
211 const conditions = ['Sunny', 'Cloudy', 'Stormy', 'Foggy', 'Windy'];
212 return {
213 condition: conditions[Math.floor(Math.random() * conditions.length)],
214 windSpeed: Math.floor(Math.random() * 40) + 5,
215 visibility: Math.floor(Math.random() * 10) + 1,
216 seaState: Math.floor(Math.random() * 6) + 1,
217 };
218 }
219
220 // Public API
221 broadcastToNamespace(namespaceName: string, event: string, data: any) {
222 const namespace = this.server.of(namespaceName);
223 namespace.emit(event, data);
224 }
225
226 getNamespaceConnections(namespaceName: string): number {
227 const namespace = this.server.of(namespaceName);
228 return namespace.sockets.size;
229 }
230}Rooms and namespaces are the foundation of well-organized legion communication. They enable efficient communication segmentation and ensure that the right information reaches the right recipients!