• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

excaliburjs / Excalibur / 20112442888

10 Dec 2025 08:32PM UTC coverage: 86.689% (-1.9%) from 88.636%
20112442888

Pull #3617

github

web-flow
Merge 05feefa61 into 548f5e4e7
Pull Request #3617: [feat] extend the components class to add json/serialize/deserialize to component properties

5316 of 7546 branches covered (70.45%)

1 of 374 new or added lines in 6 files covered. (0.27%)

200 existing lines in 5 files now uncovered.

14719 of 16979 relevant lines covered (86.69%)

24145.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.47
/src/engine/Util/Serializer.ts
1
// ============================================================================
2
// ExcaliburJS Serialization System - Centralized Architecture
3
// ============================================================================
4

5
import { Entity } from '../EntityComponentSystem/Entity';
6
import { Actor } from '../Actor';
7
import { Component, ComponentCtor } from '../EntityComponentSystem/Component';
8
import { MotionComponent, TransformComponent } from '../EntityComponentSystem';
9
import { PointerComponent } from '../Input/PointerComponent';
10
import { GraphicsComponent } from '../Graphics/GraphicsComponent';
11
import { ActionsComponent } from '../Actions/ActionsComponent';
12
import { BodyComponent } from '../Collision/BodyComponent';
13
import { ColliderComponent } from '../Collision/ColliderComponent';
14
import { Graphic } from '../Graphics';
15

16
// ============================================================================
17
// Core Serialization Interfaces
18
// ============================================================================
19

20
/**
21
 * Base interface for all serialized data
22
 */
23
export interface SerializedData {
24
  type: string;
25
  version?: string;
26
}
27

28
/**
29
 * Entity serialization format
30
 */
31
export interface EntityData extends SerializedData {
32
  type: 'Entity' | 'Actor';
33
  name: string;
34
  tags: string[];
35
  components: ComponentData[];
36
  children: EntityData[];
37
  customInstance?: string;
38
}
39

40
/**
41
 * Component serialization format
42
 */
43
export interface ComponentData extends SerializedData {
44
  type: string;
45
  [key: string]: any;
46
}
47

48
// ============================================================================
49
// Serialization System - Central Manager
50
// ============================================================================
51

52
/**
53
 * Central serialization system for ExcaliburJS
54
 * Handles all serialization/deserialization with extensible registry
55
 */
56
export class Serializer {
248✔
57
  // ============================================================================
58
  // Private Members
59
  // ============================================================================
60

61
  // Component type registry
62
  private static _componentRegistry = new Map<string, ComponentCtor>();
63
  // Graphics registry for reference-based serialization
64
  private static _graphicsRegistry = new Map<string, typeof Graphic>();
65
  // Actor class registry for custom actor types
66
  private static _actorRegistry = new Map<string, typeof Actor>();
67
  // Custom serializers for complex types
68
  private static _customSerializers = new Map<
69
    string,
70
    {
71
      serialize: (obj: any) => any;
72
      deserialize: (data: any) => any;
73
    }
74
  >();
75
  private static _initialized = false;
76

77
  // ============================================================================
78
  // Initialization
79
  // ============================================================================
80

81
  static init(autoRegisterComponents: boolean = true): void {
×
NEW
82
    if (Serializer._initialized) {
×
NEW
83
      console.warn('Serializer already initialized. Call reset() before re-initializing.');
×
NEW
84
      return;
×
85
    }
86

NEW
87
    Serializer.registerBuiltInSerializers();
×
NEW
88
    if (autoRegisterComponents) {
×
NEW
89
      Serializer.registerCommonComponents();
×
90
    }
NEW
91
    Serializer._initialized = true;
×
92
  }
93

94
  // ============================================================================
95
  // Component Registry
96
  // ============================================================================
97

98
  // #region compRegistry
99
  static registerComponent<T extends Component>(ctor: ComponentCtor<T>): void {
NEW
100
    if (!Serializer._initialized) {
×
NEW
101
      console.warn('Serializer not initialized. Call init() before registering components.');
×
NEW
102
      return;
×
103
    }
104

NEW
105
    const typeName = ctor.name;
×
NEW
106
    if (Serializer._componentRegistry.has(typeName)) {
×
NEW
107
      console.warn(`Component ${typeName} is already registered`);
×
NEW
108
      return;
×
109
    }
NEW
110
    Serializer._componentRegistry.set(typeName, ctor);
×
111
  }
112

113
  private static registerCommonComponents(): void {
NEW
114
    const commonComponents: ComponentCtor[] = [
×
115
      TransformComponent,
116
      MotionComponent,
117
      GraphicsComponent,
118
      PointerComponent,
119
      ActionsComponent,
120
      BodyComponent,
121
      ColliderComponent
122
    ];
123

NEW
124
    Serializer.registerComponents(commonComponents);
×
125
  }
126

127
  static registerComponents(ctors: ComponentCtor[]): void {
NEW
128
    if (!Serializer._initialized) {
×
NEW
129
      console.warn('Serializer not initialized. Call init() before registering components.');
×
NEW
130
      return;
×
131
    }
NEW
132
    for (const ctor of ctors) {
×
NEW
133
      Serializer.registerComponent(ctor);
×
134
    }
135
  }
136

137
  /**
138
   * Check if a component type is registered
139
   */
140
  static isComponentRegistered(typeName: string): boolean {
NEW
141
    return Serializer._componentRegistry.has(typeName);
×
142
  }
143

144
  /**
145
   * Get all registered component types
146
   */
147
  static getRegisteredComponents(): string[] {
NEW
148
    return Array.from(Serializer._componentRegistry.keys());
×
149
  }
150

151
  /**
152
   * Unregister a component type
153
   */
154
  static unregisterComponent(typeName: string): boolean {
NEW
155
    if (!Serializer._initialized) {
×
NEW
156
      console.warn('Serializer not initialized. Call init() before registering components.');
×
NEW
157
      return false;
×
158
    }
NEW
159
    return Serializer._componentRegistry.delete(typeName);
×
160
  }
161

162
  /**
163
   * Clear all registered components
164
   */
165
  static clearComponents(): void {
NEW
166
    Serializer._componentRegistry.clear();
×
167
  }
168

169
  // #endregion compRegistry
170

171
  // ============================================================================
172
  // Custom Actor Class Registry
173
  // ============================================================================
174

175
  // #region customActorRegistry
176

177
  static registerCustomActor(ctor: typeof Actor): void {
NEW
178
    const typeName = ctor.name;
×
NEW
179
    if (Serializer._actorRegistry.has(typeName)) {
×
NEW
180
      console.warn(`Custom Actor ${typeName} is already registered`);
×
NEW
181
      return;
×
182
    }
NEW
183
    Serializer._actorRegistry.set(typeName, ctor);
×
184
  }
185

186
  static registerCustomActors(ctors: Array<typeof Actor>): void {
NEW
187
    for (const ctor of ctors) {
×
NEW
188
      Serializer.registerCustomActor(ctor);
×
189
    }
190
  }
191

192
  static isCustomActorRegistered(typeName: string): boolean {
NEW
193
    if (Serializer._actorRegistry.has(typeName)) return true;
×
NEW
194
    return false;
×
195
  }
196

197
  static getRegisteredCustomActors(): string[] {
NEW
198
    if (Serializer._actorRegistry.size > 0) return Array.from(Serializer._actorRegistry.keys());
×
NEW
199
    return [];
×
200
  }
201

202
  static getCustomActor(typeName: string): typeof Actor | null {
NEW
203
    if (Serializer._actorRegistry.has(typeName)) {
×
NEW
204
      return Serializer._actorRegistry.get(typeName)!;
×
205
    }
NEW
206
    return null;
×
207
  }
208

209
  static unregisterCustomActor(typeName: string): boolean {
NEW
210
    if (Serializer._actorRegistry.has(typeName)) {
×
NEW
211
      return Serializer._actorRegistry.delete(typeName);
×
212
    }
NEW
213
    return false;
×
214
  }
215

216
  static clearCustomActors(): void {
NEW
217
    Serializer._actorRegistry.clear();
×
218
  }
219

220
  // #endregion customActorRegistry
221

222
  // ============================================================================
223
  // Graphics Registry
224
  // ============================================================================
225

226
  // #region graphicsRegistry
227

228
  static registerGraphic(id: string, graphic: any): void {
NEW
229
    if (Serializer._graphicsRegistry.has(id)) {
×
NEW
230
      console.warn(`Graphic ${id} is already registered`);
×
NEW
231
      return;
×
232
    }
NEW
233
    Serializer._graphicsRegistry.set(id, graphic);
×
234
  }
235

236
  static registerGraphics(graphics: Record<string, any>): void {
NEW
237
    for (const [id, graphic] of Object.entries(graphics)) {
×
NEW
238
      Serializer.registerGraphic(id, graphic);
×
239
    }
240
  }
241
  static getGraphic(id: string): any | undefined {
NEW
242
    return Serializer._graphicsRegistry.get(id);
×
243
  }
244

245
  /**
246
   * Check if a graphic is registered
247
   */
248
  static isGraphicRegistered(id: string): boolean {
NEW
249
    return Serializer._graphicsRegistry.has(id);
×
250
  }
251

252
  /**
253
   * Get all registered graphic IDs
254
   */
255
  static getRegisteredGraphics(): string[] {
NEW
256
    return Array.from(Serializer._graphicsRegistry.keys());
×
257
  }
258

259
  /**
260
   * Unregister a graphic
261
   */
262
  static unregisterGraphic(id: string): boolean {
NEW
263
    return Serializer._graphicsRegistry.delete(id);
×
264
  }
265

266
  /**
267
   * Clear all registered graphics
268
   */
269
  static clearGraphics(): void {
NEW
270
    Serializer._graphicsRegistry.clear();
×
271
  }
272

273
  // #endregion graphicsRegistry
274

275
  // ============================================================================
276
  // Component Serialization
277
  // ============================================================================
278

279
  // #region componentSerialization
280

281
  /**
282
   * Serialize a component
283
   */
284
  static serializeComponent(component: Component): ComponentData | null {
NEW
285
    if (!Serializer._initialized) {
×
NEW
286
      console.warn('Serializer not initialized. Call init() before registering components.');
×
NEW
287
      return null;
×
288
    }
NEW
289
    if (!component.serialize) {
×
NEW
290
      console.warn(`Component ${component.constructor.name} does not have a serialize method`);
×
NEW
291
      return null;
×
292
    }
293

NEW
294
    try {
×
NEW
295
      const data = component.serialize();
×
296
      // Ensure type field is set
NEW
297
      return {
×
298
        type: component.constructor.name,
299
        ...data
300
      };
301
    } catch (error) {
NEW
302
      console.error(`Error serializing component ${component.constructor.name}:`, error);
×
NEW
303
      return null;
×
304
    }
305
  }
306

307
  /**
308
   * Deserialize a component
309
   */
310
  static deserializeComponent(data: ComponentData): Component | null {
NEW
311
    if (!Serializer._initialized) {
×
NEW
312
      console.warn('Serializer not initialized. Call init() before registering components.');
×
NEW
313
      return null;
×
314
    }
NEW
315
    if (!data.type) {
×
NEW
316
      console.error('Component data missing type field');
×
NEW
317
      return null;
×
318
    }
319

NEW
320
    let attachGraphic = data.type == 'GraphicsComponent';
×
321

NEW
322
    const Ctor = Serializer._componentRegistry.get(data.type);
×
NEW
323
    if (!Ctor) {
×
NEW
324
      console.warn(`Component type ${data.type} is not registered`);
×
NEW
325
      return null;
×
326
    }
327

NEW
328
    try {
×
NEW
329
      const component = new Ctor();
×
NEW
330
      if (component.deserialize) {
×
NEW
331
        component.deserialize(data);
×
332

NEW
333
        if (attachGraphic) {
×
NEW
334
          debugger;
×
NEW
335
          let grph = Serializer.getGraphic((data as any).current).clone();
×
NEW
336
          if (grph) {
×
NEW
337
            (component as GraphicsComponent).use(grph);
×
338
          }
339

NEW
340
          if (data.tint) {
×
NEW
341
            (component as GraphicsComponent).current!.tint = Serializer._customSerializers.get('Color')!.deserialize(data.tint);
×
342
          }
343
        }
344
      }
NEW
345
      return component;
×
346
    } catch (error) {
NEW
347
      console.error(`Error deserializing component ${data.type}:`, error);
×
NEW
348
      return null;
×
349
    }
350
  }
351

352
  // #endregion componentSerialization
353

354
  // ============================================================================
355
  // Entity Serialization
356
  // ============================================================================
357

358
  // #region entitySerialization
359

360
  static serializeEntity(entity: Entity): EntityData {
NEW
361
    const components: ComponentData[] = [];
×
362

363
    // Serialize tags
NEW
364
    const tags: string[] = [];
×
NEW
365
    for (const tag of entity.tags) {
×
NEW
366
      tags.push(tag);
×
367
    }
368

NEW
369
    for (const component of entity.getComponents()) {
×
NEW
370
      const componentData = Serializer.serializeComponent(component);
×
NEW
371
      if (componentData) {
×
NEW
372
        components.push(componentData);
×
373
      }
374
    }
375

NEW
376
    const children: EntityData[] = [];
×
NEW
377
    for (const child of entity.children) {
×
NEW
378
      children.push(Serializer.serializeEntity(child));
×
379
    }
380

NEW
381
    return {
×
382
      type: 'Entity',
383
      name: entity.name,
384
      tags,
385
      components,
386
      children
387
    };
388
  }
389

390
  static deserializeEntity(data: EntityData): Entity | null {
NEW
391
    try {
×
NEW
392
      const entity = new Entity();
×
NEW
393
      entity.name = data.name;
×
394

395
      // Deserialize components
NEW
396
      for (const componentData of data.components) {
×
NEW
397
        const component = Serializer.deserializeComponent(componentData);
×
NEW
398
        if (component) {
×
NEW
399
          entity.addComponent(component);
×
400
        }
401
      }
402

403
      // Recursively deserialize children
NEW
404
      for (const childData of data.children) {
×
NEW
405
        const child = Serializer.deserializeEntity(childData);
×
NEW
406
        if (child) {
×
NEW
407
          entity.addChild(child);
×
408
        }
409
      }
410

NEW
411
      return entity;
×
412
    } catch (error) {
NEW
413
      console.error('Error deserializing entity:', error);
×
NEW
414
      return null;
×
415
    }
416
  }
417

418
  // #endregion entitySerialization
419

420
  // ============================================================================
421
  // Actor Serialization
422
  // ============================================================================
423

424
  // #region actorSerialization
425

426
  static serializeActor(actor: Actor): EntityData {
NEW
427
    const components: ComponentData[] = [];
×
428

429
    // is actor custom actor
430

NEW
431
    let customInstance: string | undefined = undefined;
×
432

NEW
433
    for (const [key, ctor] of Serializer._actorRegistry.entries()) {
×
NEW
434
      if (actor instanceof ctor) {
×
NEW
435
        customInstance = key;
×
NEW
436
        break;
×
437
      }
438
    }
439

440
    // Serialize tags
NEW
441
    const tags: string[] = [];
×
NEW
442
    for (const tag of actor.tags) {
×
NEW
443
      tags.push(tag);
×
444
    }
445

NEW
446
    for (const component of actor.getComponents()) {
×
NEW
447
      const componentData = Serializer.serializeComponent(component);
×
NEW
448
      if (componentData) {
×
NEW
449
        components.push(componentData);
×
450
      }
451
    }
452

NEW
453
    const children: EntityData[] = [];
×
NEW
454
    for (const child of actor.children) {
×
NEW
455
      children.push(Serializer.serializeEntity(child));
×
456
    }
457

NEW
458
    return {
×
459
      type: 'Actor',
460
      name: actor.name,
461
      tags,
462
      components,
463
      children,
464
      customInstance
465
    };
466
  }
467
  static deserializeActor(data: EntityData): Actor | null {
NEW
468
    try {
×
469
      let entity: Actor;
470

NEW
471
      if (data.customInstance) {
×
NEW
472
        const ctor = Serializer._actorRegistry.get(data.customInstance);
×
NEW
473
        if (ctor) {
×
NEW
474
          entity = new ctor() as Actor;
×
475
        }
476
      } else {
NEW
477
        entity = new Actor();
×
478
      }
NEW
479
      entity.name = data.name;
×
480

481
      //remove all existing components
482
      // debugger;
NEW
483
      for (const component of entity.getComponents()) {
×
NEW
484
        entity.removeComponent(component, true);
×
485
      }
486

487
      // debugger;
488

489
      // Deserialize components
NEW
490
      for (const componentData of data.components) {
×
NEW
491
        const component = Serializer.deserializeComponent(componentData);
×
NEW
492
        if (component) {
×
NEW
493
          entity.addComponent(component);
×
494
        }
495
      }
496

497
      // Setup actor references and getters/setters
NEW
498
      if (entity.get(TransformComponent)) {
×
NEW
499
        entity.transform = entity.get(TransformComponent)!;
×
500
      }
NEW
501
      if (entity.get(PointerComponent)) {
×
NEW
502
        entity.pointer = entity.get(PointerComponent)!;
×
503
      }
NEW
504
      if (entity.get(GraphicsComponent)) {
×
NEW
505
        entity.graphics = entity.get(GraphicsComponent)!;
×
506
      }
NEW
507
      if (entity.get(MotionComponent)) {
×
NEW
508
        entity.motion = entity.get(MotionComponent)!;
×
509
      }
NEW
510
      if (entity.get(ActionsComponent)) {
×
NEW
511
        entity.actions = entity.get(ActionsComponent)!;
×
512
      }
NEW
513
      if (entity.get(BodyComponent)) {
×
NEW
514
        entity.body = entity.get(BodyComponent)!;
×
515
      }
NEW
516
      if (entity.get(ColliderComponent)) {
×
NEW
517
        entity.collider = entity.get(ColliderComponent);
×
518
      }
519

520
      // Recursively deserialize children
NEW
521
      for (const childData of data.children) {
×
NEW
522
        const child = Serializer.deserializeEntity(childData);
×
NEW
523
        if (child) {
×
NEW
524
          entity.addChild(child);
×
525
        }
526
      }
527

NEW
528
      return entity;
×
529
    } catch (error) {
NEW
530
      console.error('Error deserializing entity:', error);
×
NEW
531
      return null;
×
532
    }
533
  }
534

535
  // #endregion actorSerialization
536

537
  // ============================================================================
538
  // JSON Conversion
539
  // ============================================================================
540

541
  // #region JSONConversion
542

543
  /**
544
   * Serialize entity to JSON string
545
   */
546
  static entityToJSON(entity: Entity, pretty: boolean = false): string {
×
NEW
547
    const data = Serializer.serializeEntity(entity);
×
NEW
548
    return JSON.stringify(data, null, pretty ? 2 : 0);
×
549
  }
550

551
  /**
552
   * Deserialize entity from JSON string
553
   */
554
  static entityFromJSON(json: string): Entity | null {
NEW
555
    try {
×
NEW
556
      const data = JSON.parse(json);
×
NEW
557
      return Serializer.deserializeEntity(data);
×
558
    } catch (error) {
NEW
559
      console.error('Error parsing entity JSON:', error);
×
NEW
560
      return null;
×
561
    }
562
  }
563

564
  static actorToJSON(actor: Actor, pretty: boolean = false): string {
×
NEW
565
    const data = Serializer.serializeActor(actor);
×
NEW
566
    return JSON.stringify(data, null, pretty ? 2 : 0);
×
567
  }
568

569
  static actorFromJSON(json: string): Actor | null {
NEW
570
    try {
×
NEW
571
      const data = JSON.parse(json);
×
NEW
572
      return Serializer.deserializeActor(data);
×
573
    } catch (error) {
NEW
574
      console.error('Error parsing actor JSON:', error);
×
NEW
575
      return null;
×
576
    }
577
  }
578

579
  // #endregion JSONConversion
580

581
  // ============================================================================
582
  // Validation
583
  // ============================================================================
584

585
  // #region validation
586

587
  /**
588
   * Validate entity data structure
589
   */
590
  static validateEntityData(data: any): data is EntityData {
NEW
591
    if (!data || typeof data !== 'object') return false;
×
NEW
592
    if (data.type !== 'Entity') return false;
×
NEW
593
    if (typeof data.name !== 'string') return false;
×
NEW
594
    if (!Array.isArray(data.components)) return false;
×
NEW
595
    if (!Array.isArray(data.children)) return false;
×
596

597
    // Validate components
NEW
598
    for (const comp of data.components) {
×
NEW
599
      if (!comp.type || typeof comp.type !== 'string') return false;
×
600
    }
601

602
    // Recursively validate children
NEW
603
    for (const child of data.children) {
×
NEW
604
      if (!Serializer.validateEntityData(child)) return false;
×
605
    }
606

NEW
607
    return true;
×
608
  }
609

610
  // #endregion validation
611

612
  // ============================================================================
613
  // Utilities
614
  // ============================================================================
615

616
  // #region Utilities
617

618
  /**
619
   * Reset all registrations (useful for testing)
620
   */
621

622
  static reset(): void {
NEW
623
    Serializer._componentRegistry.clear();
×
NEW
624
    Serializer._customSerializers.clear();
×
NEW
625
    Serializer._graphicsRegistry.clear();
×
NEW
626
    Serializer._actorRegistry.clear();
×
NEW
627
    Serializer._initialized = false;
×
628
  }
629

630
  // ============================================================================
631
  // Custom Type Serializers
632
  // ============================================================================
633

634
  private static registerBuiltInSerializers(): void {
635
    // Vector serializer
NEW
636
    Serializer.registerCustomSerializer(
×
637
      'Vector',
NEW
638
      (vec) => ({ x: vec.x, y: vec.y }),
×
NEW
639
      (data) => ({ x: data.x, y: data.y })
×
640
    );
641

642
    // Color serializer
NEW
643
    Serializer.registerCustomSerializer(
×
644
      'Color',
NEW
645
      (color) => ({ r: color.r, g: color.g, b: color.b, a: color.a }),
×
NEW
646
      (data) => ({ r: data.r, g: data.g, b: data.b, a: data.a })
×
647
    );
648

649
    // BoundingBox serializer
NEW
650
    Serializer.registerCustomSerializer(
×
651
      'BoundingBox',
NEW
652
      (bb) => ({ left: bb.left, top: bb.top, right: bb.right, bottom: bb.bottom }),
×
NEW
653
      (data) => ({ left: data.left, top: data.top, right: data.right, bottom: data.bottom })
×
654
    );
655
  }
656

657
  /**
658
   * Register a custom serializer for a specific type
659
   * Useful for types like Vector, Color, BoundingBox, etc.
660
   */
661
  static registerCustomSerializer(typeName: string, serialize: (obj: any) => any, deserialize: (data: any) => any): void {
NEW
662
    if (!Serializer._initialized) {
×
NEW
663
      console.warn('Serializer not initialized. Call init() before registering components.');
×
NEW
664
      return;
×
665
    }
NEW
666
    Serializer._customSerializers.set(typeName, { serialize, deserialize });
×
667
  }
668

669
  // #endregion Utilities
670
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc