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

excaliburjs / Excalibur / 20140052979

11 Dec 2025 04:26PM UTC coverage: 87.965% (-0.7%) from 88.636%
20140052979

Pull #3617

github

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

5435 of 7543 branches covered (72.05%)

223 of 379 new or added lines in 6 files covered. (58.84%)

1 existing line in 1 file now uncovered.

14940 of 16984 relevant lines covered (87.97%)

24133.72 hits per line

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

78.54
/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 type { 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 type { Graphic } from '../Graphics';
15
import { Logger } from './Log';
16

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

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

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

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

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

53
/**
54
 * Central serialization system for ExcaliburJS
55
 * Handles all serialization/deserialization with extensible registry
56
 */
57
export class Serializer {
250✔
58
  // ============================================================================
59
  // Private Members
60
  // ============================================================================
61
  private static _LOGGER = Logger.getInstance();
62
  // Component type registry
63
  private static _COMPONENTREGISTRY = new Map<string, ComponentCtor>();
64
  // Graphics registry for reference-based serialization
65
  private static _GRAPHICSREGISTRY = new Map<string, typeof Graphic>();
66
  // Actor class registry for custom actor types
67
  private static _ACTORREGISTRY = new Map<string, typeof Actor>();
68
  // Custom serializers for complex types
69
  private static _CUSTOMSERIALIZERS = new Map<
70
    string,
71
    {
72
      serialize: (obj: any) => any;
73
      deserialize: (data: any) => any;
74
    }
75
  >();
76
  private static _INITIALIZED = false;
77

78
  // ============================================================================
79
  // Initialization
80
  // ============================================================================
81

82
  static init(autoRegisterComponents: boolean = true): void {
3✔
83
    if (Serializer._INITIALIZED) {
53✔
84
      Serializer._LOGGER.warn('Serializer already initialized. Call reset() before re-initializing.');
2✔
85
      return;
2✔
86
    }
87

88
    Serializer._REGISTERBUILTINSERIALIZERS();
51✔
89

90
    if (autoRegisterComponents) {
51✔
91
      Serializer._REGISTERCOMMONCOMPONENTS();
4✔
92
    }
93
    Serializer._INITIALIZED = true;
51✔
94
  }
95

96
  static isInitialized(): boolean {
97
    return Serializer._INITIALIZED;
4✔
98
  }
99

100
  // ============================================================================
101
  // Component Registry
102
  // ============================================================================
103

104
  // #region compRegistry
105
  static registerComponent<T extends Component>(ctor: ComponentCtor<T>): void {
106
    const typeName = ctor.name;
58✔
107
    if (Serializer._COMPONENTREGISTRY.has(typeName)) {
58✔
108
      Serializer._LOGGER.warn(`Component ${typeName} is already registered`);
1✔
109
      return;
1✔
110
    }
111
    Serializer._COMPONENTREGISTRY.set(typeName, ctor);
57✔
112
  }
113

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

125
    Serializer.registerComponents(commonComponents);
4✔
126
  }
127

128
  static registerComponents(ctors: ComponentCtor[]): void {
129
    for (const ctor of ctors) {
7✔
130
      Serializer.registerComponent(ctor);
34✔
131
    }
132
  }
133

134
  /**
135
   * Check if a component type is registered
136
   */
137
  static isComponentRegistered(typeName: string): boolean {
138
    return Serializer._COMPONENTREGISTRY.has(typeName);
5✔
139
  }
140

141
  /**
142
   * Get all registered component types
143
   */
144
  static getRegisteredComponents(): string[] {
145
    return Array.from(Serializer._COMPONENTREGISTRY.keys());
4✔
146
  }
147

148
  /**
149
   * Unregister a component type
150
   */
151
  static unregisterComponent(typeName: string): boolean {
152
    if (!Serializer._INITIALIZED) {
2!
NEW
153
      Serializer._LOGGER.warn('Serializer not initialized. Call init() before registering components.');
×
NEW
154
      return false;
×
155
    }
156
    return Serializer._COMPONENTREGISTRY.delete(typeName);
2✔
157
  }
158

159
  /**
160
   * Clear all registered components
161
   */
162
  static clearComponents(): void {
163
    Serializer._COMPONENTREGISTRY.clear();
1✔
164
  }
165

166
  // #endregion compRegistry
167

168
  // ============================================================================
169
  // Custom Actor Class Registry
170
  // ============================================================================
171

172
  // #region customActorRegistry
173

174
  static registerCustomActor(ctor: typeof Actor): void {
175
    const typeName = ctor.name;
10✔
176
    if (Serializer._ACTORREGISTRY.has(typeName)) {
10✔
177
      Serializer._LOGGER.warn(`Custom Actor ${typeName} is already registered`);
1✔
178
      return;
1✔
179
    }
180
    Serializer._ACTORREGISTRY.set(typeName, ctor);
9✔
181
  }
182

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

189
  static isCustomActorRegistered(typeName: string): boolean {
190
    if (Serializer._ACTORREGISTRY.has(typeName)) {
3✔
191
      return true;
1✔
192
    }
193
    return false;
2✔
194
  }
195

196
  static getRegisteredCustomActors(): string[] {
197
    if (Serializer._ACTORREGISTRY.size > 0) {
2✔
198
      return Array.from(Serializer._ACTORREGISTRY.keys());
1✔
199
    }
200
    return [];
1✔
201
  }
202

203
  static getCustomActor(typeName: string): typeof Actor | null {
204
    if (Serializer._ACTORREGISTRY.has(typeName)) {
2✔
205
      return Serializer._ACTORREGISTRY.get(typeName)!;
1✔
206
    }
207
    return null;
1✔
208
  }
209

210
  static unregisterCustomActor(typeName: string): boolean {
211
    if (Serializer._ACTORREGISTRY.has(typeName)) {
1!
212
      return Serializer._ACTORREGISTRY.delete(typeName);
1✔
213
    }
NEW
214
    return false;
×
215
  }
216

217
  static clearCustomActors(): void {
218
    Serializer._ACTORREGISTRY.clear();
1✔
219
  }
220

221
  // #endregion customActorRegistry
222

223
  // ============================================================================
224
  // Graphics Registry
225
  // ============================================================================
226

227
  // #region graphicsRegistry
228

229
  static registerGraphic(id: string, graphic: any): void {
230
    if (Serializer._GRAPHICSREGISTRY.has(id)) {
11✔
231
      Serializer._LOGGER.warn(`Graphic ${id} is already registered`);
1✔
232
      return;
1✔
233
    }
234
    Serializer._GRAPHICSREGISTRY.set(id, graphic);
10✔
235
  }
236

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

246
  /**
247
   * Check if a graphic is registered
248
   */
249
  static isGraphicRegistered(id: string): boolean {
250
    return Serializer._GRAPHICSREGISTRY.has(id);
5✔
251
  }
252

253
  /**
254
   * Get all registered graphic IDs
255
   */
256
  static getRegisteredGraphics(): string[] {
257
    return Array.from(Serializer._GRAPHICSREGISTRY.keys());
1✔
258
  }
259

260
  /**
261
   * Unregister a graphic
262
   */
263
  static unregisterGraphic(id: string): boolean {
264
    return Serializer._GRAPHICSREGISTRY.delete(id);
1✔
265
  }
266

267
  /**
268
   * Clear all registered graphics
269
   */
270
  static clearGraphics(): void {
271
    Serializer._GRAPHICSREGISTRY.clear();
1✔
272
  }
273

274
  // #endregion graphicsRegistry
275

276
  // ============================================================================
277
  // Component Serialization
278
  // ============================================================================
279

280
  // #region componentSerialization
281

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

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

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

321
    const attachGraphic = data.type === 'GraphicsComponent';
3✔
322

323
    const Ctor = Serializer._COMPONENTREGISTRY.get(data.type);
3✔
324
    if (!Ctor) {
3✔
325
      Serializer._LOGGER.warn(`Component type ${data.type} is not registered`);
1✔
326
      return null;
1✔
327
    }
328

329
    try {
2✔
330
      const component = new Ctor();
2✔
331
      if (component.deserialize) {
2!
332
        component.deserialize(data);
2✔
333

334
        if (attachGraphic) {
2!
NEW
335
          const 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
      }
345
      return component;
2✔
346
    } catch (error) {
NEW
347
      Serializer._LOGGER.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 {
361
    const components: ComponentData[] = [];
6✔
362

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

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

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

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

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

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

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

411
      return entity;
4✔
412
    } catch (error) {
NEW
413
      Serializer._LOGGER.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 {
427
    const components: ComponentData[] = [];
3✔
428

429
    // is actor custom actor
430

431
    let customInstance: string | undefined = undefined;
3✔
432

433
    for (const [key, ctor] of Serializer._ACTORREGISTRY.entries()) {
3✔
434
      if (actor instanceof ctor) {
1!
435
        customInstance = key;
1✔
436
        break;
1✔
437
      }
438
    }
439

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

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

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

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

471
      if (data.customInstance) {
3✔
472
        const ctor = Serializer._ACTORREGISTRY.get(data.customInstance);
1✔
473
        if (ctor) {
1!
474
          entity = new ctor() as Actor;
1✔
475
        }
476
      } else {
477
        entity = new Actor();
2✔
478
      }
479
      entity.name = data.name;
3✔
480

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

487
      // debugger;
488

489
      // Deserialize components
490
      for (const componentData of data.components) {
3✔
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
498
      if (entity.get(TransformComponent)) {
3!
NEW
499
        entity.transform = entity.get(TransformComponent)!;
×
500
      }
501
      if (entity.get(PointerComponent)) {
3!
NEW
502
        entity.pointer = entity.get(PointerComponent)!;
×
503
      }
504
      if (entity.get(GraphicsComponent)) {
3!
NEW
505
        entity.graphics = entity.get(GraphicsComponent)!;
×
506
      }
507
      if (entity.get(MotionComponent)) {
3!
NEW
508
        entity.motion = entity.get(MotionComponent)!;
×
509
      }
510
      if (entity.get(ActionsComponent)) {
3!
NEW
511
        entity.actions = entity.get(ActionsComponent)!;
×
512
      }
513
      if (entity.get(BodyComponent)) {
3!
NEW
514
        entity.body = entity.get(BodyComponent)!;
×
515
      }
516
      if (entity.get(ColliderComponent)) {
3!
NEW
517
        entity.collider = entity.get(ColliderComponent);
×
518
      }
519

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

528
      return entity;
3✔
529
    } catch (error) {
NEW
530
      Serializer._LOGGER.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 {
1✔
547
    const data = Serializer.serializeEntity(entity);
2✔
548
    return JSON.stringify(data, null, pretty ? 2 : 0);
2✔
549
  }
550

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

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

569
  static actorFromJSON(json: string): Actor | null {
570
    try {
1✔
571
      const data = JSON.parse(json);
1✔
572
      return Serializer.deserializeActor(data);
1✔
573
    } catch (error) {
NEW
574
      Serializer._LOGGER.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 {
591
    if (!data || typeof data !== 'object') {
6✔
592
      return false;
1✔
593
    }
594
    if (data.type !== 'Entity') {
5✔
595
      return false;
1✔
596
    }
597
    if (typeof data.name !== 'string') {
4✔
598
      return false;
1✔
599
    }
600
    if (!Array.isArray(data.components)) {
3✔
601
      return false;
1✔
602
    }
603
    if (!Array.isArray(data.children)) {
2!
NEW
604
      return false;
×
605
    }
606

607
    // Validate components
608
    for (const comp of data.components) {
2✔
609
      if (!comp.type || typeof comp.type !== 'string') {
2✔
610
        return false;
1✔
611
      }
612
    }
613

614
    // Recursively validate children
615
    for (const child of data.children) {
1✔
NEW
616
      if (!Serializer.validateEntityData(child)) {
×
NEW
617
        return false;
×
618
      }
619
    }
620

621
    return true;
1✔
622
  }
623

624
  // #endregion validation
625

626
  // ============================================================================
627
  // Utilities
628
  // ============================================================================
629

630
  // #region Utilities
631

632
  /**
633
   * Reset all registrations (useful for testing)
634
   */
635

636
  static reset(): void {
637
    Serializer._COMPONENTREGISTRY.clear();
58✔
638
    Serializer._CUSTOMSERIALIZERS.clear();
58✔
639
    Serializer._GRAPHICSREGISTRY.clear();
58✔
640
    Serializer._ACTORREGISTRY.clear();
58✔
641
    Serializer._INITIALIZED = false;
58✔
642
  }
643

644
  // ============================================================================
645
  // Custom Type Serializers
646
  // ============================================================================
647

648
  private static _REGISTERBUILTINSERIALIZERS(): void {
649
    // Vector serializer
650
    Serializer.registerCustomSerializer(
51✔
651
      'Vector',
652
      (vec) => ({ x: vec.x, y: vec.y }),
1✔
653
      (data) => ({ x: data.x, y: data.y })
1✔
654
    );
655

656
    // Color serializer
657
    Serializer.registerCustomSerializer(
51✔
658
      'Color',
659
      (color) => ({ r: color.r, g: color.g, b: color.b, a: color.a }),
1✔
NEW
660
      (data) => ({ r: data.r, g: data.g, b: data.b, a: data.a })
×
661
    );
662

663
    // BoundingBox serializer
664
    Serializer.registerCustomSerializer(
51✔
665
      'BoundingBox',
NEW
666
      (bb) => ({ left: bb.left, top: bb.top, right: bb.right, bottom: bb.bottom }),
×
NEW
667
      (data) => ({ left: data.left, top: data.top, right: data.right, bottom: data.bottom })
×
668
    );
669
  }
670

671
  /**
672
   * Register a custom serializer for a specific type
673
   * Useful for types like Vector, Color, BoundingBox, etc.
674
   */
675
  static registerCustomSerializer(typeName: string, serialize: (obj: any) => any, deserialize: (data: any) => any): void {
676
    Serializer._CUSTOMSERIALIZERS.set(typeName, { serialize, deserialize });
154✔
677
  }
678

679
  static getCustomSerializer(typeName: string): { serialize: (obj: any) => any; deserialize: (data: any) => any } | undefined {
680
    return Serializer._CUSTOMSERIALIZERS.get(typeName);
5✔
681
  }
682

683
  static getRegistry(registry: 'graphics' | 'actors' | 'components'): Map<string, any> {
684
    switch (registry) {
3✔
685
      case 'graphics':
686
        return Serializer._GRAPHICSREGISTRY;
1✔
687
      case 'actors':
688
        return Serializer._ACTORREGISTRY;
1✔
689
      case 'components':
690
        return Serializer._COMPONENTREGISTRY;
1✔
691
    }
692
  }
693

694
  // #endregion Utilities
695
}
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