Repository: mlange-42/arche
Pull request #319 (2024-01-11 00:34:30)

Allow to save / load state in terms of assigned entities
Allows the world to serialize and deserialize the exact state of all entites (ids, generation, alive; but not components!)

This is required to make it possible to serialize (implicit or explicit) entity relations.

TODO:

* [x] Get rid of JSON
* [x] Infer pool and entity list capacity
* [x] Document the new functionality more comprehensively

coveralls: ## Pull Request Test Coverage Report for [Build 7482556208](https://coveralls.io/builds/65003946)


* **0** of **0**   changed or added relevant lines in **0** files are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage decreased (**-0.2%**) to **99.847%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/65003946/badge)](https://coveralls.io/builds/65003946) |
| :-- | --: |
| Change from base [Build 7482536863](https://coveralls.io/builds/65003901): |  -0.2% |
| Covered Lines: | 3928 |
| Relevant Lines: | 3934 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9efe76b..3803ad9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
 ### Highlights
 
 * Arche now supports 256 instead of 128 component types as well as resource types and engine locks (#313)
+* Arche supports full world serialization and deserialization, in conjunction with [arche-serde](https://github.com/mlange-42/arche-serde) (#319)
 
 ### Breaking changes
 
@@ -17,6 +18,8 @@ This change was necessary to get the same performance as before, despite the mor
 
 * Adds functions `ComponentInfo(*World, ID)` and `ResourceType(*World, ResID)` (#315, #318)
 * Adds methods `World.Ids(Entity)` and `Query.Ids()` (#315)
+* Entities support JSON marshalling/unmarshalling (#319)
+* The world's entity state can be extracted and re-established via `World.GetEntityData()` and `World.SetEntityData()` (#319)
 
 ### Other
 
diff --git a/README.md b/README.md
index a50eef5..461a01e 100644
--- a/README.md
+++ b/README.md
@@ -27,6 +27,7 @@
 * Entity relations as first-class feature. See [Architecture](https://github.com/mlange-42/arche/blob/main/ARCHITECTURE.md#entity-relations).
 * No systems. Just queries. Use your own structure (or the [Tools](#tools)).
 * No dependencies. Except for unit tests ([100% coverage](https://coveralls.io/github/mlange-42/arche)).
+* Supports serialization and deserialization of the entire world state via [arche-serde](https://github.com/mlange-42/arche-serde).
 * Probably the fastest Go ECS out there. See the [Benchmarks](#benchmarks).
 
 ## Installation
diff --git a/ecs/entity.go b/ecs/entity.go
index fff9586..fbc343a 100644
--- a/ecs/entity.go
+++ b/ecs/entity.go
@@ -1,6 +1,9 @@
 package ecs
 
-import "reflect"
+import (
+	"encoding/json"
+	"reflect"
+)
 
 // Reflection type of an [Entity].
 var entityType = reflect.TypeOf(Entity{})
@@ -40,6 +43,43 @@ func (e Entity) IsZero() bool {
 	return e.id == 0
 }
 
+// ID of the entity. For serialization purposes only!
+//
+// Do not use this in normal model or game code!
+func (e Entity) ID() uint32 {
+	return uint32(e.id)
+}
+
+// Gen returns the generation of the entity. For serialization purposes only!
+//
+// Do not use this in normal model or game code!
+func (e Entity) Gen() uint32 {
+	return e.gen
+}
+
+// MarshalJSON returns a JSON representation of the entity, for serialization purposes.
+//
+// The JSON representation of an entity is a two-element array of entity ID and generation.
+func (e Entity) MarshalJSON() ([]byte, error) {
+	arr := [2]uint32{uint32(e.id), e.gen}
+	jsonValue, _ := json.Marshal(arr) // Ignore the error, as we can be sure this works.
+	return jsonValue, nil
+}
+
+// UnmarshalJSON into an entity.
+//
+// For serialization purposes only. Do not use this to create entities!
+func (e *Entity) UnmarshalJSON(data []byte) error {
+	arr := [2]uint32{}
+	if err := json.Unmarshal(data, &arr); err != nil {
+		return err
+	}
+	e.id = eid(arr[0])
+	e.gen = arr[1]
+
+	return nil
+}
+
 // entityIndex indicates where an entity is currently stored.
 type entityIndex struct {
 	arch  *archetype // Entity's current archetype
diff --git a/ecs/types.go b/ecs/types.go
index cc47923..1dec0d9 100644
--- a/ecs/types.go
+++ b/ecs/types.go
@@ -33,3 +33,13 @@ type CompInfo struct {
 	Type       reflect.Type
 	IsRelation bool
 }
+
+// EntityData is a dump of the entire entity data of the world.
+//
+// See [World.GetEntityData] and [World.SetEntityData].
+type EntityData struct {
+	Entities  []Entity // Entities in the World's entity pool.
+	Alive     []uint32 // IDs of all alive entities in query iteration order.
+	Next      uint32   // The next free entity of the World's entity pool.
+	Available uint32   // The number of allocated and available entities in the World's entity pool.
+}
diff --git a/ecs/world.go b/ecs/world.go
index 90f0264..3c71091 100644
--- a/ecs/world.go
+++ b/ecs/world.go
@@ -1116,6 +1116,67 @@ func (w *World) Stats() *stats.WorldStats {
 	return &w.stats
 }
 
+// GetEntityData dumps entity information into an [EntityData] object.
+// This dump can be used with [World.SetEntityData] to set the World's entity state.
+//
+// For world serialization with components and resources, see module [github.com/mlange-42/arche-serde].
+func (w *World) GetEntityData() EntityData {
+	alive := []uint32{}
+
+	query := w.Query(All())
+	for query.Next() {
+		alive = append(alive, query.Entity().ID())
+	}
+
+	data := EntityData{
+		Entities:  append([]Entity{}, w.entityPool.entities...),
+		Alive:     alive,
+		Next:      uint32(w.entityPool.next),
+		Available: w.entityPool.available,
+	}
+
+	return data
+}
+
+// SetEntityData resets all entities to the state saved with [World.GetEntityData].
+//
+// Use this only on an empty world! Can be used after [World.Reset].
+//
+// The resulting world will have the same entities (in terms of ID, generation and alive state)
+// as the original world. This is necessary for proper serialization of entity relations.
+// However, the entities will not have any components.
+//
+// Panics if the world has any dead or alive entities.
+//
+// For world serialization with components and resources, see module [github.com/mlange-42/arche-serde].
+func (w *World) SetEntityData(data *EntityData) {
+	w.checkLocked()
+
+	if len(w.entityPool.entities) > 1 || w.entityPool.available > 0 {
+		panic("can set entity data only on a fresh or reset world")
+	}
+
+	capacity := capacity(len(data.Entities), w.config.CapacityIncrement)
+
+	entities := make([]Entity, 0, capacity)
+	entities = append(entities, data.Entities...)
+
+	w.entityPool.entities = entities
+	w.entityPool.next = eid(data.Next)
+	w.entityPool.available = data.Available
+
+	w.entities = make([]entityIndex, len(data.Entities), capacity)
+	w.targetEntities = bitSet{}
+	w.targetEntities.ExtendTo(capacity)
+
+	arch := w.archetypes.Get(0)
+	for _, idx := range data.Alive {
+		entity := w.entityPool.entities[idx]
+		archIdx := arch.Alloc(entity)
+		w.entities[entity.id] = entityIndex{arch: arch, index: archIdx}
+	}
+}
+
 // lock the world and get the lock bit for later unlocking.
 func (w *World) lock() uint8 {
 	return w.locks.Lock()


Test patch:
diff --git a/ecs/entity_test.go b/ecs/entity_test.go
index a5739bd..63b8c37 100644
--- a/ecs/entity_test.go
+++ b/ecs/entity_test.go
@@ -1,6 +1,7 @@
 package ecs
 
 import (
+	"encoding/json"
 	"fmt"
 	"testing"
 
@@ -20,6 +21,32 @@ func TestZeroEntity(t *testing.T) {
 	assert.False(t, Entity{1, 0}.IsZero())
 }
 
+func TestEntityGetters(t *testing.T) {
+	e := newEntityGen(2, 3)
+	assert.Equal(t, e.ID(), uint32(2))
+	assert.Equal(t, e.Gen(), uint32(3))
+}
+
+func TestEntityMarshal(t *testing.T) {
+	e := newEntityGen(2, 3)
+
+	jsonData, err := json.Marshal(&e)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	e2 := Entity{}
+	err = json.Unmarshal(jsonData, &e2)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	assert.Equal(t, e2, e)
+
+	err = e2.UnmarshalJSON([]byte("pft"))
+	assert.NotNil(t, err)
+}
+
 func BenchmarkEntityIsZero(b *testing.B) {
 	e := Entity{}
 
diff --git a/ecs/world_test.go b/ecs/world_test.go
index 93d84e3..5acdde8 100644
--- a/ecs/world_test.go
+++ b/ecs/world_test.go
@@ -1673,6 +1673,72 @@ func TestTypeSizes(t *testing.T) {
 	printTypeSizeName[idMap[uint32]]("idMap")
 }
 
+func TestWorldEntityData(t *testing.T) {
+	w := NewWorld()
+
+	e1 := w.NewEntity()
+	e2 := w.NewEntity()
+	e3 := w.NewEntity()
+	e4 := w.NewEntity()
+
+	w.RemoveEntity(e2)
+	w.RemoveEntity(e3)
+	e5 := w.NewEntity()
+
+	eData := w.GetEntityData()
+	fmt.Println(eData)
+
+	w2 := NewWorld()
+	w2.SetEntityData(&eData)
+
+	assert.True(t, w2.Alive(e1))
+	assert.True(t, w2.Alive(e4))
+	assert.True(t, w2.Alive(e5))
+
+	assert.False(t, w2.Alive(e2))
+	assert.False(t, w2.Alive(e3))
+
+	assert.Equal(t, w.Ids(e1), []ID{})
+
+	query := w2.Query(All())
+	assert.Equal(t, query.Count(), 3)
+	query.Close()
+}
+
+func TestWorldEntityDataEmpty(t *testing.T) {
+	w := NewWorld()
+
+	eData := w.GetEntityData()
+
+	w2 := NewWorld()
+	w2.SetEntityData(&eData)
+
+	e1 := w2.NewEntity()
+	e2 := w2.NewEntity()
+
+	assert.True(t, w2.Alive(e1))
+	assert.True(t, w2.Alive(e2))
+
+	query := w2.Query(All())
+	assert.Equal(t, query.Count(), 2)
+	query.Close()
+}
+
+func TestWorldEntityDataFail(t *testing.T) {
+	w := NewWorld()
+	_ = w.NewEntity()
+
+	eData := w.GetEntityData()
+
+	w2 := NewWorld()
+	e1 := w2.NewEntity()
+	w2.RemoveEntity(e1)
+
+	assert.Panics(t, func() {
+		w2.SetEntityData(&eData)
+	})
+}
+
 func printTypeSize[T any]() {
 	tp := reflect.TypeOf((*T)(nil)).Elem()
 	fmt.Printf("%18s: %5d B\n", tp.Name(), tp.Size())