Skip to content

Commit d17c345

Browse files
committed
feat(serialize): support encoding entity IDs as SerializedEntityId
- Add encodeEntityId and decodeSerializedId helper functions - Update SerializedEntityId type to support wildcard relations - Refactor World.serialize() and constructor to use encoding helpers - Enable entity IDs to be serialized as names or relation objects
1 parent 7cc0e7c commit d17c345

File tree

1 file changed

+110
-64
lines changed

1 file changed

+110
-64
lines changed

src/world.ts

Lines changed: 110 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,107 @@ import { serializeQueryFilter, type QueryFilter } from "./query-filter";
2424
import type { ComponentTuple, LifecycleHook } from "./types";
2525
import { getOrCreateWithSideEffect } from "./utils";
2626

27+
// -----------------------------------------------------------------------------
28+
// Serialization helpers for IDs
29+
// -----------------------------------------------------------------------------
30+
31+
export type SerializedEntityId = number | string | { component: string; target: number | string | "*" };
32+
33+
/**
34+
* Encode an internal EntityId into a SerializedEntityId for snapshots
35+
*/
36+
function encodeEntityId(id: EntityId<any>): SerializedEntityId {
37+
const detailed = getDetailedIdType(id);
38+
switch (detailed.type) {
39+
case "component": {
40+
const name = getComponentNameById(id as ComponentId);
41+
if (!name) {
42+
// Warn if component doesn't have a name; keep numeric fallback
43+
console.warn(`Component ID ${id} has no registered name, serializing as number`);
44+
}
45+
return name || (id as number);
46+
}
47+
case "entity-relation": {
48+
const componentName = getComponentNameById(detailed.componentId);
49+
if (!componentName) {
50+
console.warn(`Component ID ${detailed.componentId} in relation has no registered name`);
51+
}
52+
return { component: componentName || (detailed.componentId as number).toString(), target: detailed.targetId! };
53+
}
54+
case "component-relation": {
55+
const componentName = getComponentNameById(detailed.componentId);
56+
const targetName = getComponentNameById(detailed.targetId! as ComponentId);
57+
if (!componentName) {
58+
console.warn(`Component ID ${detailed.componentId} in relation has no registered name`);
59+
}
60+
if (!targetName) {
61+
console.warn(`Target component ID ${detailed.targetId} in relation has no registered name`);
62+
}
63+
return {
64+
component: componentName || (detailed.componentId as number).toString(),
65+
target: targetName || (detailed.targetId as number),
66+
};
67+
}
68+
case "wildcard-relation": {
69+
const componentName = getComponentNameById(detailed.componentId);
70+
if (!componentName) {
71+
console.warn(`Component ID ${detailed.componentId} in relation has no registered name`);
72+
}
73+
return { component: componentName || (detailed.componentId as number).toString(), target: "*" };
74+
}
75+
default:
76+
return id as number;
77+
}
78+
}
79+
80+
/**
81+
* Decode a SerializedEntityId back into an internal EntityId
82+
*/
83+
function decodeSerializedId(sid: SerializedEntityId): EntityId<any> {
84+
if (typeof sid === "number") {
85+
return sid as EntityId<any>;
86+
}
87+
if (typeof sid === "string") {
88+
const id = getComponentIdByName(sid);
89+
if (id === undefined) {
90+
const num = parseInt(sid, 10);
91+
if (!isNaN(num)) return num as EntityId<any>;
92+
throw new Error(`Unknown component name in snapshot: ${sid}`);
93+
}
94+
return id;
95+
}
96+
if (typeof sid === "object" && sid !== null && typeof sid.component === "string") {
97+
let compId = getComponentIdByName(sid.component);
98+
if (compId === undefined) {
99+
const num = parseInt(sid.component, 10);
100+
if (!isNaN(num)) compId = num as ComponentId;
101+
}
102+
if (compId === undefined) {
103+
throw new Error(`Unknown component name in snapshot: ${sid.component}`);
104+
}
105+
106+
if (sid.target === "*") {
107+
return relation(compId, "*");
108+
}
109+
110+
let targetId: EntityId<any>;
111+
if (typeof sid.target === "string") {
112+
const tid = getComponentIdByName(sid.target);
113+
if (tid === undefined) {
114+
const num = parseInt(sid.target, 10);
115+
if (!isNaN(num)) targetId = num as EntityId<any>;
116+
else throw new Error(`Unknown target component name in snapshot: ${sid.target}`);
117+
} else {
118+
targetId = tid;
119+
}
120+
} else {
121+
targetId = sid.target as EntityId<any>;
122+
}
123+
return relation(compId, targetId as any);
124+
}
125+
throw new Error(`Invalid ID in snapshot: ${JSON.stringify(sid)}`);
126+
}
127+
27128
/**
28129
* World class for ECS architecture
29130
* Manages entities and components
@@ -81,47 +182,14 @@ export class World {
81182
// Restore entities and their components
82183
if (Array.isArray(snapshot.entities)) {
83184
for (const entry of snapshot.entities) {
84-
const entityId = entry.id as EntityId;
185+
const entityId = decodeSerializedId(entry.id);
85186
const componentsArray: SerializedComponent[] = entry.components || [];
86187

87188
const componentMap = new Map<EntityId<any>, any>();
88189
const componentTypes: EntityId<any>[] = [];
89190

90191
for (const componentEntry of componentsArray) {
91-
const componentTypeRaw = componentEntry.type;
92-
let componentType: EntityId<any>;
93-
94-
if (typeof componentTypeRaw === "number") {
95-
componentType = componentTypeRaw as EntityId<any>;
96-
} else if (typeof componentTypeRaw === "string") {
97-
// Component name lookup
98-
const compId = getComponentIdByName(componentTypeRaw);
99-
if (compId === undefined) {
100-
throw new Error(`Unknown component name in snapshot: ${componentTypeRaw}`);
101-
}
102-
componentType = compId;
103-
} else if (
104-
typeof componentTypeRaw === "object" &&
105-
componentTypeRaw !== null &&
106-
typeof componentTypeRaw.component === "string"
107-
) {
108-
// Component name lookup
109-
const compId = getComponentIdByName(componentTypeRaw.component);
110-
if (compId === undefined) {
111-
throw new Error(`Unknown component name in snapshot: ${componentTypeRaw.component}`);
112-
}
113-
if (typeof componentTypeRaw.target === "string") {
114-
const targetCompId = getComponentIdByName(componentTypeRaw.target);
115-
if (targetCompId === undefined) {
116-
throw new Error(`Unknown target component name in snapshot: ${componentTypeRaw.target}`);
117-
}
118-
componentType = relation(compId, targetCompId);
119-
} else {
120-
componentType = relation(compId, componentTypeRaw.target as EntityId);
121-
}
122-
} else {
123-
throw new Error(`Invalid component type in snapshot: ${JSON.stringify(componentTypeRaw)}`);
124-
}
192+
const componentType = decodeSerializedId(componentEntry.type);
125193
componentMap.set(componentType, componentEntry.value);
126194
componentTypes.push(componentType);
127195
}
@@ -1264,33 +1332,11 @@ export class World {
12641332
const dumpedEntities = archetype.dump();
12651333
for (const { entity, components } of dumpedEntities) {
12661334
entities.push({
1267-
id: entity,
1268-
components: Array.from(components.entries()).map(([rawType, value]) => {
1269-
const detailedType = getDetailedIdType(rawType);
1270-
let type: SerializedComponent["type"] = rawType;
1271-
let componentName;
1272-
switch (detailedType.type) {
1273-
case "component":
1274-
type = getComponentNameById(rawType as ComponentId) || rawType;
1275-
break;
1276-
case "entity-relation":
1277-
componentName = getComponentNameById(detailedType.componentId);
1278-
if (componentName) {
1279-
type = { component: componentName, target: detailedType.targetId! };
1280-
}
1281-
break;
1282-
case "component-relation":
1283-
componentName = getComponentNameById(detailedType.componentId);
1284-
if (componentName) {
1285-
type = {
1286-
component: componentName,
1287-
target: getComponentNameById(detailedType.targetId!) || detailedType.targetId!,
1288-
};
1289-
}
1290-
break;
1291-
}
1292-
return { type, value: value === MISSING_COMPONENT ? undefined : value };
1293-
}),
1335+
id: encodeEntityId(entity),
1336+
components: Array.from(components.entries()).map(([rawType, value]) => ({
1337+
type: encodeEntityId(rawType),
1338+
value: value === MISSING_COMPONENT ? undefined : value,
1339+
})),
12941340
});
12951341
}
12961342
}
@@ -1310,12 +1356,12 @@ export type SerializedWorld = {
13101356
};
13111357

13121358
export type SerializedEntity = {
1313-
id: number;
1359+
id: SerializedEntityId;
13141360
components: SerializedComponent[];
13151361
};
13161362

13171363
export type SerializedComponent = {
1318-
type: number | string | { component: string; target: number | string };
1364+
type: SerializedEntityId;
13191365
value: any;
13201366
};
13211367

0 commit comments

Comments
 (0)