NateTheGreatt/bitECS

Using serialization for saving and loading

luetkemj opened this issue · 5 comments

Serializing a world seems to work but deserializing throws the following error:

Uncaught TypeError: Cannot read properties of undefined (reading 'Symbol(storeBase)')
at bitecs.v0.3.34.js:472

Here's some repro code:

import { createWorld, defineSerializer, defineDeserializer } from "bitecs";

let packet;

export const save = (world) => {
  const serialize = defineSerializer(world);
  packet = serialize(world);
};

export const load = () => {
  let newWorld = createWorld();
  const deserialize = defineDeserializer(newWorld);
  deserialize(newWorld, packet);
  return newWorld;
};

I don't get an error if I use defineSerializer with same world that I serialized but I'm trying to figure out how that's useful. If I have the world already, there's no reason to deserialize.

Not sure if bug or I'm missing something...

seems like a bug! thanks for reporting, i'll work on a fix ASAP

@luetkemj it seems this error indicates that newWorld does not know about a component that world knows about. components are normally automatically registered with worlds when they are either added to an entity or a query with that component is called on a world, but it can also be done explicitly. i created an example which reproduces the error:

import { 
  Types, 
  createWorld,
  defineSerializer,
  defineDeserializer,
  defineComponent,
  registerComponent,
  addEntity,
  addComponent
} from "bitecs"

const save = (world) => {
  const serialize = defineSerializer(world)
  return serialize(world)
}

const load = (world, packet) => {
  const deserialize = defineDeserializer(world)
  deserialize(world, packet)
}

const worldA = createWorld()
const worldB = createWorld()

const C = defineComponent({
  x: Types.f32,
  y: Types.f32,
  z: Types.f32,
})

registerComponent(worldA, C)
// uncomment this line and the error goes away
// registerComponent(worldB, C)

const eidA = addEntity(worldA)

addComponent(worldA, C, eidA)

const packet = save(worldA)

load(worldB, packet)

because serializers and deserializers need to have the same exact config up-front, using worlds creates a bit of an edge case if those worlds don't know about the same exact components. to avoid this error one should explicitly pass in all components to both the serializer and deserializer for now.

however, i will be introducing a new function to a future release which should make this easier: getWorldComponents

i'm now wondering if it's a bad idea to allow a world to be passed into the initial serializer config, and instead if something like this should replace that feature:

const serialize = defineSerializer(getWorldComponents(world))

what do you think?

const serialize = defineSerializer(getWorldComponents(world))

@NateTheGreatt That feels a lot more intuitive to me. If there's a legitimate usecase for passing in the world directly that I'm missing, good documentation should make things clear about when to do what.

Would also love to see some in-build solution for that!

So far I've been using this to keep track of all components (to avoid forgetting to register one):

const definedComponents: ComponentType<any>[] = [];

const defineComponent: typeof defineComponentBitecs = (schema, size) => {
  const component = defineComponentBitecs(schema, size);
 
  definedComponents.push(component);

  return component;
};

// component definitions with custom `defineComponent`
// ...

export const registerAllComponents = (world: IWorld) => {
  registerComponents(world, definedComponents);
};

Thank you Nate for your code, I didn't realise you code register a component against a world, if I register all components then it works!!!

also, in case anybody needs it, it took me a while how to actually save it to a file (in nodejs, I am using electron)

fs.writeFileSync(this.FilePath(saveName), Buffer.from(data));

and to read
this.typedArrayToBuffer(fs.readFileSync(this.FilePath(saveName)));

with
typedArrayToBuffer(array: Uint8Array): ArrayBuffer {
    return array.buffer.slice(array.byteOffset, array.byteLength + array.byteOffset)
}

(I am unsure why it works but it works for me, which is good enough for now!)