diff --git a/cardinal/cardinal_test.go b/cardinal/cardinal_test.go index 81d6a8280..429200f0a 100644 --- a/cardinal/cardinal_test.go +++ b/cardinal/cardinal_test.go @@ -5,26 +5,11 @@ import ( "time" "gotest.tools/v3/assert" + "pkg.world.dev/world-engine/cardinal" "pkg.world.dev/world-engine/cardinal/test_utils" ) -func makeWorldAndTicker(t *testing.T) (world *cardinal.World, doTick func()) { - startTickCh, doneTickCh := make(chan time.Time), make(chan uint64) - world, err := cardinal.NewMockWorld( - cardinal.WithTickChannel(startTickCh), - cardinal.WithTickDoneChannel(doneTickCh)) - t.Cleanup(func() { - world.ShutDown() - }) - assert.NilError(t, err) - - return world, func() { - startTickCh <- time.Now() - <-doneTickCh - } -} - type Foo struct{} func (Foo) Name() string { return "foo" } @@ -32,19 +17,25 @@ func (Foo) Name() string { return "foo" } func TestCanQueryInsideSystem(t *testing.T) { test_utils.SetTestTimeout(t, 10*time.Second) - world, doTick := makeWorldAndTicker(t) + nextTickCh := make(chan time.Time) + tickDoneCh := make(chan uint64) + + world, err := cardinal.NewMockWorld( + cardinal.WithTickChannel(nextTickCh), + cardinal.WithTickDoneChannel(tickDoneCh)) + assert.NilError(t, err) assert.NilError(t, cardinal.RegisterComponent[Foo](world)) wantNumOfEntities := 10 - world.Init(func(worldCtx cardinal.WorldContext) { - _, err := cardinal.CreateMany(worldCtx, wantNumOfEntities, Foo{}) + world.Init(func(wCtx cardinal.WorldContext) { + _, err = cardinal.CreateMany(wCtx, wantNumOfEntities, Foo{}) assert.NilError(t, err) }) gotNumOfEntities := 0 - cardinal.RegisterSystems(world, func(worldCtx cardinal.WorldContext) error { - q, err := worldCtx.NewSearch(cardinal.Exact(Foo{})) + cardinal.RegisterSystems(world, func(wCtx cardinal.WorldContext) error { + q, err := wCtx.NewSearch(cardinal.Exact(Foo{})) assert.NilError(t, err) - err = q.Each(worldCtx, func(cardinal.EntityID) bool { + err = q.Each(wCtx, func(cardinal.EntityID) bool { gotNumOfEntities++ return true }) @@ -57,9 +48,9 @@ func TestCanQueryInsideSystem(t *testing.T) { for !world.IsGameRunning() { time.Sleep(time.Second) //starting game async, must wait until game is running before testing everything. } - doTick() - - err := world.ShutDown() + nextTickCh <- time.Now() + <-tickDoneCh + err = world.ShutDown() assert.Assert(t, err) assert.Equal(t, gotNumOfEntities, wantNumOfEntities) } diff --git a/cardinal/component_test.go b/cardinal/component_test.go deleted file mode 100644 index 8b7b4794e..000000000 --- a/cardinal/component_test.go +++ /dev/null @@ -1,90 +0,0 @@ -package cardinal_test - -import ( - "testing" - - "gotest.tools/v3/assert" - "pkg.world.dev/world-engine/cardinal" - "pkg.world.dev/world-engine/cardinal/test_utils" -) - -type Height struct { - Inches int -} - -func (Height) Name() string { return "height" } - -type Weight struct { - Pounds int -} - -func (Weight) Name() string { return "weight" } - -type Age struct { - Years int -} - -func (Age) Name() string { return "age" } - -func TestComponentExample(t *testing.T) { - world, _ := makeWorldAndTicker(t) - - assert.NilError(t, cardinal.RegisterComponent[Height](world)) - assert.NilError(t, cardinal.RegisterComponent[Weight](world)) - assert.NilError(t, cardinal.RegisterComponent[Age](world)) - - testWorldCtx := test_utils.WorldToWorldContext(world) - startHeight := 72 - startWeight := 200 - startAge := 30 - - peopleIDs, err := cardinal.CreateMany(testWorldCtx, 10, Height{startHeight}, Weight{startWeight}, Age{startAge}) - assert.NilError(t, err) - - targetID := peopleIDs[4] - height, err := cardinal.GetComponent[Height](testWorldCtx, targetID) - assert.NilError(t, err) - assert.Equal(t, startHeight, height.Inches) - - assert.NilError(t, cardinal.RemoveComponentFrom[Age](testWorldCtx, targetID)) - - // Age was removed form exactly 1 entity. - search, err := testWorldCtx.NewSearch(cardinal.Exact(Height{}, Weight{})) - assert.NilError(t, err) - count, err := search.Count(testWorldCtx) - assert.NilError(t, err) - assert.Equal(t, 1, count) - - // The rest of the entities still have the Age field. - search, err = testWorldCtx.NewSearch(cardinal.Contains(Age{})) - assert.NilError(t, err) - count, err = search.Count(testWorldCtx) - assert.NilError(t, err) - assert.Equal(t, len(peopleIDs)-1, count) - - // Age does not exist on the target ID, so this should result in an error - err = cardinal.UpdateComponent[Age](testWorldCtx, targetID, func(a *Age) *Age { - return a - }) - assert.Check(t, err != nil) - - heavyWeight := 999 - err = cardinal.UpdateComponent[Weight](testWorldCtx, targetID, func(w *Weight) *Weight { - w.Pounds = heavyWeight - return w - }) - assert.NilError(t, err) - - // Adding the Age component to the targetID should not change the weight component - assert.NilError(t, cardinal.AddComponentTo[Age](testWorldCtx, targetID)) - - for _, id := range peopleIDs { - weight, err := cardinal.GetComponent[Weight](testWorldCtx, id) - assert.NilError(t, err) - if id == targetID { - assert.Equal(t, heavyWeight, weight.Pounds) - } else { - assert.Equal(t, startWeight, weight.Pounds) - } - } -} diff --git a/cardinal/ecs/mock_world.go b/cardinal/ecs/mock_world.go index fdaed735e..8dd1dba2d 100644 --- a/cardinal/ecs/mock_world.go +++ b/cardinal/ecs/mock_world.go @@ -16,7 +16,7 @@ import ( // NewMockWorld creates an ecs.World that uses a mock redis DB as the storage // layer. This is only suitable for local development. If you are creating an ecs.World for // unit tests, use NewTestWorld. -func NewMockWorld(opts ...Option) (world *World, cleanup func()) { +func NewMockWorld(opts ...Option) *World { // We manually set the start address to make the port deterministic s := miniredis.NewMiniRedis() err := s.StartAddr(":12345") @@ -30,9 +30,7 @@ func NewMockWorld(opts ...Option) (world *World, cleanup func()) { panic(fmt.Errorf("unable to initialize world: %w", err)) } - return w, func() { - s.Close() - } + return w } // NewTestWorld creates an ecs.World suitable for running in tests. Relevant resources diff --git a/cardinal/evm/server.go b/cardinal/evm/server.go index 4b7d13fda..6e015f283 100644 --- a/cardinal/evm/server.go +++ b/cardinal/evm/server.go @@ -39,7 +39,6 @@ type Server interface { routerv1.MsgServer // Serve serves the application in a new go routine. Serve() error - Shutdown() } // txByID maps transaction type ID's to transaction types. @@ -59,8 +58,6 @@ type msgServerImpl struct { // opts creds credentials.TransportCredentials port string - - shutdown func() } // NewServer returns a new EVM connection server. This server is responsible for handling requests originating from @@ -173,16 +170,9 @@ func (s *msgServerImpl) Serve() error { log.Fatal(err) } }() - s.shutdown = server.GracefulStop return nil } -func (s *msgServerImpl) Shutdown() { - if s.shutdown != nil { - s.shutdown() - } -} - const ( CodeSuccess = iota CodeTxFailed diff --git a/cardinal/query_test.go b/cardinal/query_test.go deleted file mode 100644 index b98903c70..000000000 --- a/cardinal/query_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package cardinal_test - -import ( - "testing" - - "gotest.tools/v3/assert" - "pkg.world.dev/world-engine/cardinal" - "pkg.world.dev/world-engine/cardinal/test_utils" -) - -type QueryHealthRequest struct { - Min int -} - -type QueryHealthResponse struct { - IDs []cardinal.EntityID -} - -func handleQueryHealth(worldCtx cardinal.WorldContext, request *QueryHealthRequest) (*QueryHealthResponse, error) { - q, err := worldCtx.NewSearch(cardinal.Exact(Health{})) - if err != nil { - return nil, err - } - resp := &QueryHealthResponse{} - err = q.Each(worldCtx, func(id cardinal.EntityID) bool { - health, err := cardinal.GetComponent[Health](worldCtx, id) - if err != nil { - return true - } - if health.Value < request.Min { - return true - } - resp.IDs = append(resp.IDs, id) - return true - }) - if err != nil { - return nil, err - } - return resp, nil -} - -var queryHealth = cardinal.NewQueryType[*QueryHealthRequest, *QueryHealthResponse]("query_health", handleQueryHealth) - -func TestQueryExample(t *testing.T) { - world, _ := makeWorldAndTicker(t) - assert.NilError(t, cardinal.RegisterComponent[Health](world)) - assert.NilError(t, cardinal.RegisterQueries(world, queryHealth)) - go world.StartGame() - - worldCtx := test_utils.WorldToWorldContext(world) - ids, err := cardinal.CreateMany(worldCtx, 100, Health{}) - assert.NilError(t, err) - // Give each new entity health based on the ever-increasing index - for i, id := range ids { - assert.NilError(t, cardinal.UpdateComponent[Health](worldCtx, id, func(h *Health) *Health { - h.Value = i - return h - })) - } - - // No entities should have health over a million. - resp, err := queryHealth.DoQuery(worldCtx, &QueryHealthRequest{1_000_000}) - assert.NilError(t, err) - assert.Equal(t, 0, len(resp.IDs)) - - // All entities should have health over -100 - resp, err = queryHealth.DoQuery(worldCtx, &QueryHealthRequest{-100}) - assert.NilError(t, err) - assert.Equal(t, 100, len(resp.IDs)) - - // Exactly 10 entities should have health at or above 90 - resp, err = queryHealth.DoQuery(worldCtx, &QueryHealthRequest{90}) - assert.NilError(t, err) - assert.Equal(t, 10, len(resp.IDs)) -} diff --git a/cardinal/query.go b/cardinal/read.go similarity index 87% rename from cardinal/query.go rename to cardinal/read.go index 492353c3a..82d35ddb5 100644 --- a/cardinal/query.go +++ b/cardinal/read.go @@ -42,12 +42,3 @@ func NewQueryTypeWithEVMSupport[Request, Reply any](name string, handler func(Wo func (r *QueryType[Request, Reply]) Convert() ecs.IQuery { return r.impl } - -func (q *QueryType[Request, Reply]) DoQuery(worldCtx WorldContext, req Request) (reply Reply, err error) { - iface, err := q.impl.HandleQuery(worldCtx.getECSWorldContext(), req) - if err != nil { - return reply, err - } - reply = iface.(Reply) - return reply, nil -} diff --git a/cardinal/search_test.go b/cardinal/search_test.go deleted file mode 100644 index b06ef7d69..000000000 --- a/cardinal/search_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package cardinal_test - -import ( - "testing" - - "gotest.tools/v3/assert" - "pkg.world.dev/world-engine/cardinal" - "pkg.world.dev/world-engine/cardinal/test_utils" -) - -type Alpha struct{} - -func (Alpha) Name() string { return "alpha" } - -type Beta struct{} - -func (Beta) Name() string { return "beta" } - -type Gamma struct{} - -func (Gamma) Name() string { return "gamma" } - -func TestSearchExample(t *testing.T) { - world, _ := makeWorldAndTicker(t) - assert.NilError(t, cardinal.RegisterComponent[Alpha](world)) - assert.NilError(t, cardinal.RegisterComponent[Beta](world)) - assert.NilError(t, cardinal.RegisterComponent[Gamma](world)) - - worldCtx := test_utils.WorldToWorldContext(world) - _, err := cardinal.CreateMany(worldCtx, 10, Alpha{}) - assert.NilError(t, err) - _, err = cardinal.CreateMany(worldCtx, 10, Beta{}) - assert.NilError(t, err) - _, err = cardinal.CreateMany(worldCtx, 10, Gamma{}) - assert.NilError(t, err) - _, err = cardinal.CreateMany(worldCtx, 10, Alpha{}, Beta{}) - assert.NilError(t, err) - _, err = cardinal.CreateMany(worldCtx, 10, Alpha{}, Gamma{}) - assert.NilError(t, err) - _, err = cardinal.CreateMany(worldCtx, 10, Beta{}, Gamma{}) - assert.NilError(t, err) - _, err = cardinal.CreateMany(worldCtx, 10, Alpha{}, Beta{}, Gamma{}) - assert.NilError(t, err) - - testCases := []struct { - name string - filter cardinal.Filter - want int - }{ - { - "exactly alpha", - cardinal.Exact(Alpha{}), - 10, - }, - { - "contains alpha", - cardinal.Contains(Alpha{}), - 40, - }, - { - "beta or gamma", - cardinal.Or( - cardinal.Exact(Beta{}), - cardinal.Exact(Gamma{}), - ), - 20, - }, - { - "not alpha", - cardinal.Not(cardinal.Exact(Alpha{})), - 60, - }, - } - for _, tc := range testCases { - msg := "problem with " + tc.name - q, err := worldCtx.NewSearch(tc.filter) - assert.NilError(t, err, msg) - count, err := q.Count(worldCtx) - assert.NilError(t, err, msg) - assert.Equal(t, tc.want, count, msg) - } -} diff --git a/cardinal/system_test.go b/cardinal/system_test.go deleted file mode 100644 index 39642c27d..000000000 --- a/cardinal/system_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package cardinal_test - -import ( - "errors" - "testing" - - "gotest.tools/v3/assert" - "pkg.world.dev/world-engine/cardinal" - "pkg.world.dev/world-engine/cardinal/test_utils" -) - -type Health struct { - Value int -} - -func (Health) Name() string { return "health" } - -func HealthSystem(worldCtx cardinal.WorldContext) error { - q, err := worldCtx.NewSearch(cardinal.Exact(Health{})) - if err != nil { - return err - } - var errs []error - errs = append(errs, q.Each(worldCtx, func(id cardinal.EntityID) bool { - errs = append(errs, cardinal.UpdateComponent[Health](worldCtx, id, func(h *Health) *Health { - h.Value += 1 - return h - })) - return true - })) - if err := errors.Join(errs...); err != nil { - return err - } - return err -} - -func TestSystemExample(t *testing.T) { - world, doTick := makeWorldAndTicker(t) - assert.NilError(t, cardinal.RegisterComponent[Health](world)) - cardinal.RegisterSystems(world, HealthSystem) - - worldCtx := test_utils.WorldToWorldContext(world) - ids, err := cardinal.CreateMany(worldCtx, 100, Health{}) - assert.NilError(t, err) - go world.StartGame() - - // Make sure we have 100 entities all with a health of 0 - for _, id := range ids { - health, err := cardinal.GetComponent[Health](worldCtx, id) - assert.NilError(t, err) - assert.Equal(t, 0, health.Value) - } - - // do 5 ticks - for i := 0; i < 5; i++ { - doTick() - } - - // Health should be 5 for everyone - for _, id := range ids { - health, err := cardinal.GetComponent[Health](worldCtx, id) - assert.NilError(t, err) - assert.Equal(t, 5, health.Value) - } -} diff --git a/cardinal/test_utils/test_utils.go b/cardinal/test_utils/test_utils.go index da4601f68..c30a777c4 100644 --- a/cardinal/test_utils/test_utils.go +++ b/cardinal/test_utils/test_utils.go @@ -2,20 +2,16 @@ package test_utils import ( "bytes" - "crypto/ecdsa" "encoding/json" - "fmt" "net/http" "testing" "time" - "github.com/ethereum/go-ethereum/crypto" "gotest.tools/v3/assert" - "pkg.world.dev/world-engine/cardinal" + "pkg.world.dev/world-engine/cardinal/ecs" "pkg.world.dev/world-engine/cardinal/events" "pkg.world.dev/world-engine/cardinal/server" - "pkg.world.dev/world-engine/sign" ) func MakeTestTransactionHandler(t *testing.T, world *ecs.World, opts ...server.Option) *TestTransactionHandler { @@ -113,54 +109,3 @@ func SetTestTimeout(t *testing.T, timeout time.Duration) { } }() } - -func WorldToWorldContext(world *cardinal.World) cardinal.WorldContext { - return cardinal.TestingWorldToWorldContext(world) -} - -var ( - nonce uint64 - privateKey *ecdsa.PrivateKey -) - -func uniqueSignature() *sign.SignedPayload { - if privateKey == nil { - var err error - privateKey, err = crypto.GenerateKey() - if err != nil { - panic(err) - } - } - nonce++ - // We only verify signatures when hitting the HTTP server, and in tests we're likely just adding transactions - // directly to the World queue. It's OK if the signature does not match the payload. - sig, err := sign.NewSignedPayload(privateKey, "some-persona-tag", "namespace", nonce, `{"some":"data"}`) - if err != nil { - panic(err) - } - return sig -} - -func AddTransactionToWorldByAnyTransaction(world *cardinal.World, cardinalTx cardinal.AnyTransaction, value any) { - worldCtx := WorldToWorldContext(world) - ecsWorld := cardinal.TestingWorldContextToECSWorld(worldCtx) - - txs, err := ecsWorld.ListTransactions() - if err != nil { - panic(err) - } - txID := cardinalTx.Convert().ID() - found := false - for _, tx := range txs { - if tx.ID() == txID { - found = true - break - } - } - if !found { - panic(fmt.Sprintf("cannot find transaction %q in registered transactinos. did you register it?", cardinalTx.Convert().Name())) - } - // uniqueSignature is copied from - sig := uniqueSignature() - _, _ = ecsWorld.AddTransaction(txID, value, sig) -} diff --git a/cardinal/transaction_test.go b/cardinal/transaction_test.go deleted file mode 100644 index 992a10ccb..000000000 --- a/cardinal/transaction_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package cardinal_test - -import ( - "testing" - - "gotest.tools/v3/assert" - "pkg.world.dev/world-engine/cardinal" - "pkg.world.dev/world-engine/cardinal/test_utils" -) - -type AddHealthToEntityTx struct { - TargetID cardinal.EntityID - Amount int -} - -type AddHealthToEntityResult struct{} - -var addHealthToEntity = cardinal.NewTransactionType[AddHealthToEntityTx, AddHealthToEntityResult]("add_health") - -func TestTransactionExample(t *testing.T) { - world, doTick := makeWorldAndTicker(t) - assert.NilError(t, cardinal.RegisterComponent[Health](world)) - assert.NilError(t, cardinal.RegisterTransactions(world, addHealthToEntity)) - cardinal.RegisterSystems(world, func(worldCtx cardinal.WorldContext) error { - for _, tx := range addHealthToEntity.In(worldCtx) { - targetID := tx.Value().TargetID - err := cardinal.UpdateComponent[Health](worldCtx, targetID, func(h *Health) *Health { - h.Value = tx.Value().Amount - return h - }) - assert.Check(t, err == nil) - } - return nil - }) - go world.StartGame() - - testWorldCtx := test_utils.WorldToWorldContext(world) - ids, err := cardinal.CreateMany(testWorldCtx, 10, Health{}) - assert.NilError(t, err) - - // Queue up the transaction. - idToModify := ids[3] - amountToModify := 20 - - test_utils.AddTransactionToWorldByAnyTransaction(world, addHealthToEntity, AddHealthToEntityTx{idToModify, amountToModify}) - - // The health change should be applied during this tick - doTick() - - // Make sure the target entity had its health updated. - for _, id := range ids { - health, err := cardinal.GetComponent[Health](testWorldCtx, id) - assert.NilError(t, err) - if id == idToModify { - assert.Equal(t, amountToModify, health.Value) - } else { - assert.Equal(t, 0, health.Value) - } - } -} diff --git a/cardinal/world.go b/cardinal/world.go index 9141ae7c1..49f4c3aa4 100644 --- a/cardinal/world.go +++ b/cardinal/world.go @@ -30,7 +30,6 @@ type World struct { tickChannel <-chan time.Time tickDoneChannel chan<- uint64 serverOptions []server.Option - cleanup func() } type ( @@ -91,11 +90,9 @@ func NewMockWorld(opts ...WorldOption) (*World, error) { ecsOptions, serverOptions, cardinalOptions := separateOptions(opts) eventHub := events.CreateWebSocketEventHub() ecsOptions = append(ecsOptions, ecs.WithEventHub(eventHub)) - implWorld, mockWorldCleanup := ecs.NewMockWorld(ecsOptions...) world := &World{ - implWorld: implWorld, + implWorld: ecs.NewMockWorld(ecsOptions...), serverOptions: serverOptions, - cleanup: mockWorldCleanup, } world.isGameRunning.Store(false) for _, opt := range cardinalOptions { @@ -207,12 +204,6 @@ func (w *World) IsGameRunning() bool { } func (w *World) ShutDown() error { - if w.cleanup != nil { - w.cleanup() - } - if w.evmServer != nil { - w.evmServer.Shutdown() - } if !w.IsGameRunning() { return errors.New("game is not running") } diff --git a/cardinal/world_context.go b/cardinal/world_context.go index 32740cd9d..712f5bcf2 100644 --- a/cardinal/world_context.go +++ b/cardinal/world_context.go @@ -7,9 +7,9 @@ import ( type WorldContext interface { NewSearch(filter Filter) (*Search, error) + getECSWorldContext() ecs.WorldContext CurrentTick() uint64 Logger() *zerolog.Logger - getECSWorldContext() ecs.WorldContext } type worldContext struct { @@ -35,12 +35,3 @@ func (wCtx *worldContext) NewSearch(filter Filter) (*Search, error) { func (wCtx *worldContext) getECSWorldContext() ecs.WorldContext { return wCtx.implContext } - -func (w *worldContext) TestOnlyGetECSWorld() *ecs.World { - return w.implContext.GetWorld() -} - -func TestOnlyGetECSWorld(worldCtx WorldContext) *ecs.World { - w := worldCtx.(*worldContext) - return w.implContext.GetWorld() -}