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

excaliburjs / Excalibur / 20121940506

11 Dec 2025 04:26AM UTC coverage: 87.968% (-0.7%) from 88.636%
20121940506

Pull #3617

github

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

5431 of 7540 branches covered (72.03%)

211 of 366 new or added lines in 6 files covered. (57.65%)

149 existing lines in 5 files now uncovered.

14929 of 16971 relevant lines covered (87.97%)

24160.88 hits per line

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

77.67
/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 {
250✔
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 {
3✔
82
    if (Serializer._initialized) {
53✔
83
      console.warn('Serializer already initialized. Call reset() before re-initializing.');
2✔
84
      return;
2✔
85
    }
86

87
    Serializer.registerBuiltInSerializers();
51✔
88

89
    if (autoRegisterComponents) {
51✔
90
      Serializer.registerCommonComponents();
4✔
91
    }
92
    Serializer._initialized = true;
51✔
93
  }
94

95
  static isInitialized(): boolean {
96
    return Serializer._initialized;
3✔
97
  }
98

99
  // ============================================================================
100
  // Component Registry
101
  // ============================================================================
102

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

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

124
    Serializer.registerComponents(commonComponents);
4✔
125

126
    // console.log(Serializer.getRegisteredComponents());
127
  }
128

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

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

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

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

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

167
  // #endregion compRegistry
168

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

173
  // #region customActorRegistry
174

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

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

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

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

200
  static getCustomActor(typeName: string): typeof Actor | null {
201
    if (Serializer._actorRegistry.has(typeName)) {
2✔
202
      return Serializer._actorRegistry.get(typeName)!;
1✔
203
    }
204
    return null;
1✔
205
  }
206

207
  static unregisterCustomActor(typeName: string): boolean {
208
    if (Serializer._actorRegistry.has(typeName)) {
1!
209
      return Serializer._actorRegistry.delete(typeName);
1✔
210
    }
NEW
211
    return false;
×
212
  }
213

214
  static clearCustomActors(): void {
215
    Serializer._actorRegistry.clear();
1✔
216
  }
217

218
  // #endregion customActorRegistry
219

220
  // ============================================================================
221
  // Graphics Registry
222
  // ============================================================================
223

224
  // #region graphicsRegistry
225

226
  static registerGraphic(id: string, graphic: any): void {
227
    if (Serializer._graphicsRegistry.has(id)) {
11✔
228
      console.warn(`Graphic ${id} is already registered`);
1✔
229
      return;
1✔
230
    }
231
    Serializer._graphicsRegistry.set(id, graphic);
10✔
232
  }
233

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

243
  /**
244
   * Check if a graphic is registered
245
   */
246
  static isGraphicRegistered(id: string): boolean {
247
    return Serializer._graphicsRegistry.has(id);
5✔
248
  }
249

250
  /**
251
   * Get all registered graphic IDs
252
   */
253
  static getRegisteredGraphics(): string[] {
254
    return Array.from(Serializer._graphicsRegistry.keys());
1✔
255
  }
256

257
  /**
258
   * Unregister a graphic
259
   */
260
  static unregisterGraphic(id: string): boolean {
261
    return Serializer._graphicsRegistry.delete(id);
1✔
262
  }
263

264
  /**
265
   * Clear all registered graphics
266
   */
267
  static clearGraphics(): void {
268
    Serializer._graphicsRegistry.clear();
1✔
269
  }
270

271
  // #endregion graphicsRegistry
272

273
  // ============================================================================
274
  // Component Serialization
275
  // ============================================================================
276

277
  // #region componentSerialization
278

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

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

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

318
    let attachGraphic = data.type == 'GraphicsComponent';
3✔
319

320
    const Ctor = Serializer._componentRegistry.get(data.type);
3✔
321
    if (!Ctor) {
3✔
322
      console.warn(`Component type ${data.type} is not registered`);
1✔
323
      return null;
1✔
324
    }
325

326
    try {
2✔
327
      const component = new Ctor();
2✔
328
      if (component.deserialize) {
2!
329
        component.deserialize(data);
2✔
330

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

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

350
  // #endregion componentSerialization
351

352
  // ============================================================================
353
  // Entity Serialization
354
  // ============================================================================
355

356
  // #region entitySerialization
357

358
  static serializeEntity(entity: Entity): EntityData {
359
    const components: ComponentData[] = [];
6✔
360

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

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

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

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

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

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

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

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

416
  // #endregion entitySerialization
417

418
  // ============================================================================
419
  // Actor Serialization
420
  // ============================================================================
421

422
  // #region actorSerialization
423

424
  static serializeActor(actor: Actor): EntityData {
425
    const components: ComponentData[] = [];
3✔
426

427
    // is actor custom actor
428

429
    let customInstance: string | undefined = undefined;
3✔
430

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

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

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

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

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

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

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

485
      // debugger;
486

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

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

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

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

533
  // #endregion actorSerialization
534

535
  // ============================================================================
536
  // JSON Conversion
537
  // ============================================================================
538

539
  // #region JSONConversion
540

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

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

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

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

577
  // #endregion JSONConversion
578

579
  // ============================================================================
580
  // Validation
581
  // ============================================================================
582

583
  // #region validation
584

585
  /**
586
   * Validate entity data structure
587
   */
588
  static validateEntityData(data: any): data is EntityData {
589
    if (!data || typeof data !== 'object') return false;
6✔
590
    if (data.type !== 'Entity') return false;
5✔
591
    if (typeof data.name !== 'string') return false;
4✔
592
    if (!Array.isArray(data.components)) return false;
3✔
593
    if (!Array.isArray(data.children)) return false;
2!
594

595
    // Validate components
596
    for (const comp of data.components) {
2✔
597
      if (!comp.type || typeof comp.type !== 'string') return false;
2✔
598
    }
599

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

605
    return true;
1✔
606
  }
607

608
  // #endregion validation
609

610
  // ============================================================================
611
  // Utilities
612
  // ============================================================================
613

614
  // #region Utilities
615

616
  /**
617
   * Reset all registrations (useful for testing)
618
   */
619

620
  static reset(): void {
621
    Serializer._componentRegistry.clear();
58✔
622
    Serializer._customSerializers.clear();
58✔
623
    Serializer._graphicsRegistry.clear();
58✔
624
    Serializer._actorRegistry.clear();
58✔
625
    Serializer._initialized = false;
58✔
626
  }
627

628
  // ============================================================================
629
  // Custom Type Serializers
630
  // ============================================================================
631

632
  private static registerBuiltInSerializers(): void {
633
    // Vector serializer
634
    Serializer.registerCustomSerializer(
51✔
635
      'Vector',
636
      (vec) => ({ x: vec.x, y: vec.y }),
1✔
637
      (data) => ({ x: data.x, y: data.y })
1✔
638
    );
639

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

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

655
  /**
656
   * Register a custom serializer for a specific type
657
   * Useful for types like Vector, Color, BoundingBox, etc.
658
   */
659
  static registerCustomSerializer(typeName: string, serialize: (obj: any) => any, deserialize: (data: any) => any): void {
660
    Serializer._customSerializers.set(typeName, { serialize, deserialize });
154✔
661
  }
662

663
  // #endregion Utilities
664
}
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