From d309164aca3a5d85e64414d86882db0a630775d0 Mon Sep 17 00:00:00 2001 From: Brian Yeh Date: Wed, 1 May 2024 10:57:10 -0700 Subject: [PATCH 1/2] move message and query into cardinal. --- cardinal/cardinal.go | 16 ++++++-------- cardinal/cardinal_test.go | 11 +++++----- cardinal/engine_test.go | 13 +++++------ cardinal/example_messagetype_test.go | 3 +-- cardinal/gamestate/tick_test.go | 6 ++--- cardinal/{message => }/message.go | 2 +- .../manager.go => message_manager.go} | 22 +++++++++---------- cardinal/{message => }/message_test.go | 2 +- cardinal/plugin_persona.go | 12 +++++----- cardinal/{query => }/query.go | 19 ++++++++-------- .../{query/manager.go => query_manager.go} | 16 +++++++------- cardinal/{query => }/query_test.go | 7 +++--- cardinal/router/iterator/iterator_test.go | 4 ++-- cardinal/server/receipts_test.go | 3 +-- cardinal/server/server_test.go | 6 ++--- cardinal/state_test.go | 3 +-- cardinal/testutils/testutils.go | 8 +++---- cardinal/world.go | 10 ++++----- cardinal/world_recovery_test.go | 3 +-- cardinal/world_test.go | 7 +++--- 20 files changed, 78 insertions(+), 95 deletions(-) rename cardinal/{message => }/message.go (99%) rename cardinal/{message/manager.go => message_manager.go} (76%) rename cardinal/{message => }/message_test.go (99%) rename cardinal/{query => }/query.go (91%) rename cardinal/{query/manager.go => query_manager.go} (73%) rename cardinal/{query => }/query_test.go (96%) diff --git a/cardinal/cardinal.go b/cardinal/cardinal.go index 6378bbd2a..c2e649109 100644 --- a/cardinal/cardinal.go +++ b/cardinal/cardinal.go @@ -9,8 +9,6 @@ import ( "pkg.world.dev/world-engine/cardinal/component" "pkg.world.dev/world-engine/cardinal/iterators" - "pkg.world.dev/world-engine/cardinal/message" - "pkg.world.dev/world-engine/cardinal/query" "pkg.world.dev/world-engine/cardinal/search" "pkg.world.dev/world-engine/cardinal/system" "pkg.world.dev/world-engine/cardinal/types" @@ -113,15 +111,15 @@ func MustRegisterComponent[T types.Component](w *World) { } } -func EachMessage[In any, Out any](wCtx engine.Context, fn func(message.TxData[In]) (Out, error)) error { - var msg message.MessageType[In, Out] +func EachMessage[In any, Out any](wCtx engine.Context, fn func(TxData[In]) (Out, error)) error { + var msg MessageType[In, Out] msgType := reflect.TypeOf(msg) tempRes, ok := wCtx.GetMessageByType(msgType) if !ok { return eris.Errorf("Could not find %s, Message may not be registered.", msg.Name()) } var _ types.Message = &msg - res, ok := tempRes.(*message.MessageType[In, Out]) + res, ok := tempRes.(*MessageType[In, Out]) if !ok { return eris.New("wrong type") } @@ -132,7 +130,7 @@ func EachMessage[In any, Out any](wCtx engine.Context, fn func(message.TxData[In // RegisterMessage registers a message to the world. Cardinal will automatically set up HTTP routes that map to each // registered message. Message URLs are take the form of "group.name". A default group, "game", is used // unless the WithCustomMessageGroup option is used. Example: game.throw-rock -func RegisterMessage[In any, Out any](world *World, name string, opts ...message.MessageOption[In, Out]) error { +func RegisterMessage[In any, Out any](world *World, name string, opts ...MessageOption[In, Out]) error { if world.worldStage.Current() != worldstage.Init { return eris.Errorf( "engine state is %s, expected %s to register messages", @@ -142,7 +140,7 @@ func RegisterMessage[In any, Out any](world *World, name string, opts ...message } // Create the message type - msgType := message.NewMessageType[In, Out](name, opts...) + msgType := NewMessageType[In, Out](name, opts...) // Register the message with the manager err := world.msgManager.RegisterMessage(msgType, reflect.TypeOf(*msgType)) @@ -157,7 +155,7 @@ func RegisterQuery[Request any, Reply any]( w *World, name string, handler func(wCtx engine.Context, req *Request) (*Reply, error), - opts ...query.Option[Request, Reply], + opts ...QueryOption[Request, Reply], ) (err error) { if w.worldStage.Current() != worldstage.Init { return eris.Errorf( @@ -167,7 +165,7 @@ func RegisterQuery[Request any, Reply any]( ) } - q, err := query.NewQueryType[Request, Reply](name, handler, opts...) + q, err := NewQueryType[Request, Reply](name, handler, opts...) if err != nil { return err } diff --git a/cardinal/cardinal_test.go b/cardinal/cardinal_test.go index 24ef2256a..d2138a805 100644 --- a/cardinal/cardinal_test.go +++ b/cardinal/cardinal_test.go @@ -19,7 +19,6 @@ import ( "pkg.world.dev/world-engine/assert" "pkg.world.dev/world-engine/cardinal" - "pkg.world.dev/world-engine/cardinal/message" "pkg.world.dev/world-engine/cardinal/router/mocks" "pkg.world.dev/world-engine/cardinal/search/filter" "pkg.world.dev/world-engine/cardinal/testutils" @@ -52,11 +51,11 @@ type Qux struct{} func (Qux) Name() string { return "qux" } -type Health struct { +type CardinalTestHealth struct { Value int } -func (Health) Name() string { return "health" } +func (CardinalTestHealth) Name() string { return "health" } func TestForEachTransaction(t *testing.T) { tf := testutils.NewTestFixture(t, nil) @@ -73,7 +72,7 @@ func TestForEachTransaction(t *testing.T) { err := cardinal.RegisterSystems(world, func(wCtx engine.Context) error { return cardinal.EachMessage[SomeMsgRequest, SomeMsgResponse](wCtx, - func(t message.TxData[SomeMsgRequest]) (result SomeMsgResponse, err error) { + func(t cardinal.TxData[SomeMsgRequest]) (result SomeMsgResponse, err error) { if t.Msg.GenerateError { return result, errors.New("some error") } @@ -183,7 +182,7 @@ func TestTransactionAreAppliedToSomeEntities(t *testing.T) { world, func(wCtx engine.Context) error { return cardinal.EachMessage[*ModifyScoreMsg, *EmptyMsgResult](wCtx, - func(msData message.TxData[*ModifyScoreMsg]) (*EmptyMsgResult, error) { + func(msData cardinal.TxData[*ModifyScoreMsg]) (*EmptyMsgResult, error) { ms := msData.Msg err := cardinal.UpdateComponent[ScoreComponent]( wCtx, ms.PlayerID, func(s *ScoreComponent) *ScoreComponent { @@ -644,7 +643,7 @@ func TestTransactionExample(t *testing.T) { } // test same as above but with .Each addHealthToEntity.Each(wCtx, - func(tx message.TxData[AddHealthToEntityTx]) (AddHealthToEntityResult, error) { + func(tx cardinal.TxData[AddHealthToEntityTx]) (AddHealthToEntityResult, error) { targetID := tx.Msg.TargetID err := cardinal.UpdateComponent[Health](wCtx, targetID, func(h *Health) *Health { diff --git a/cardinal/engine_test.go b/cardinal/engine_test.go index 35aa1e8de..aeb50d546 100644 --- a/cardinal/engine_test.go +++ b/cardinal/engine_test.go @@ -10,7 +10,6 @@ import ( "pkg.world.dev/world-engine/assert" "pkg.world.dev/world-engine/cardinal" - "pkg.world.dev/world-engine/cardinal/message" "pkg.world.dev/world-engine/cardinal/router/iterator" "pkg.world.dev/world-engine/cardinal/router/mocks" "pkg.world.dev/world-engine/cardinal/testutils" @@ -134,7 +133,7 @@ func TestCannotWaitForNextTickAfterEngineIsShutDown(t *testing.T) { world := tf.World msgName := "foo" assert.NilError(t, - cardinal.RegisterMessage[FooIn, FooOut](world, msgName, message.WithMsgEVMSupport[FooIn, FooOut]())) + cardinal.RegisterMessage[FooIn, FooOut](world, msgName, cardinal.WithMsgEVMSupport[FooIn, FooOut]())) fooTx, ok := world.GetMessageByFullName("game." + msgName) assert.True(t, ok) var returnVal FooOut @@ -143,7 +142,7 @@ func TestCannotWaitForNextTickAfterEngineIsShutDown(t *testing.T) { world, func(wCtx engine.Context) error { return cardinal.EachMessage[FooIn, FooOut]( - wCtx, func(message.TxData[FooIn]) (FooOut, error) { + wCtx, func(cardinal.TxData[FooIn]) (FooOut, error) { return returnVal, returnErr }, ) @@ -197,7 +196,7 @@ func TestEVMTxConsume(t *testing.T) { tf := testutils.NewTestFixture(t, nil) world := tf.World msgName := "foo" - err := cardinal.RegisterMessage[FooIn, FooOut](world, msgName, message.WithMsgEVMSupport[FooIn, FooOut]()) + err := cardinal.RegisterMessage[FooIn, FooOut](world, msgName, cardinal.WithMsgEVMSupport[FooIn, FooOut]()) assert.NilError(t, err) var returnVal FooOut @@ -205,7 +204,7 @@ func TestEVMTxConsume(t *testing.T) { err = cardinal.RegisterSystems(world, func(eCtx cardinal.WorldContext) error { return cardinal.EachMessage[FooIn, FooOut]( - eCtx, func(message.TxData[FooIn]) (FooOut, error) { + eCtx, func(cardinal.TxData[FooIn]) (FooOut, error) { return returnVal, returnErr }, ) @@ -371,7 +370,7 @@ func TestTransactionsSentToRouterAfterTick(t *testing.T) { type fooMsgRes struct{} msgName := "foo" - err := cardinal.RegisterMessage[fooMsg, fooMsgRes](world, msgName, message.WithMsgEVMSupport[fooMsg, fooMsgRes]()) + err := cardinal.RegisterMessage[fooMsg, fooMsgRes](world, msgName, cardinal.WithMsgEVMSupport[fooMsg, fooMsgRes]()) assert.NilError(t, err) evmTxHash := "0x12345" @@ -449,7 +448,7 @@ func TestRecoverFromChain(t *testing.T) { fooMessages := 0 err := cardinal.RegisterSystems(world, func(engineContext cardinal.WorldContext) error { - return cardinal.EachMessage[fooMsg, fooMsgRes](engineContext, func(message.TxData[fooMsg]) (fooMsgRes, error) { + return cardinal.EachMessage[fooMsg, fooMsgRes](engineContext, func(cardinal.TxData[fooMsg]) (fooMsgRes, error) { fooMessages++ return fooMsgRes{}, nil }) diff --git a/cardinal/example_messagetype_test.go b/cardinal/example_messagetype_test.go index 7fd3224b8..ab313a262 100644 --- a/cardinal/example_messagetype_test.go +++ b/cardinal/example_messagetype_test.go @@ -6,7 +6,6 @@ import ( "fmt" "pkg.world.dev/world-engine/cardinal" - "pkg.world.dev/world-engine/cardinal/message" "pkg.world.dev/world-engine/cardinal/types/engine" ) @@ -33,7 +32,7 @@ func ExampleMessageType() { err = cardinal.RegisterSystems(world, func(wCtx engine.Context) error { return cardinal.EachMessage[MovePlayerMsg, MovePlayerResult](wCtx, - func(txData message.TxData[MovePlayerMsg]) (MovePlayerResult, error) { + func(txData cardinal.TxData[MovePlayerMsg]) (MovePlayerResult, error) { // handle the transaction // ... diff --git a/cardinal/gamestate/tick_test.go b/cardinal/gamestate/tick_test.go index 3504b0eab..39166083f 100644 --- a/cardinal/gamestate/tick_test.go +++ b/cardinal/gamestate/tick_test.go @@ -5,7 +5,7 @@ import ( "testing" "pkg.world.dev/world-engine/assert" - "pkg.world.dev/world-engine/cardinal/message" + "pkg.world.dev/world-engine/cardinal" "pkg.world.dev/world-engine/cardinal/testutils" "pkg.world.dev/world-engine/cardinal/types" "pkg.world.dev/world-engine/cardinal/types/txpool" @@ -19,8 +19,8 @@ func TestCanSaveAndRecoverTransactions(t *testing.T) { Value int } - msgAlpha := message.NewMessageType[MsgIn, MsgOut]("alpha") - msgBeta := message.NewMessageType[MsgIn, MsgOut]("beta") + msgAlpha := cardinal.NewMessageType[MsgIn, MsgOut]("alpha") + msgBeta := cardinal.NewMessageType[MsgIn, MsgOut]("beta") assert.NilError(t, msgAlpha.SetID(16)) assert.NilError(t, msgBeta.SetID(32)) msgs := []types.Message{msgAlpha, msgBeta} diff --git a/cardinal/message/message.go b/cardinal/message.go similarity index 99% rename from cardinal/message/message.go rename to cardinal/message.go index ac0e849b7..cc5468ce0 100644 --- a/cardinal/message/message.go +++ b/cardinal/message.go @@ -1,4 +1,4 @@ -package message +package cardinal import ( "errors" diff --git a/cardinal/message/manager.go b/cardinal/message_manager.go similarity index 76% rename from cardinal/message/manager.go rename to cardinal/message_manager.go index 9392f62c4..86241ba14 100644 --- a/cardinal/message/manager.go +++ b/cardinal/message_manager.go @@ -1,4 +1,4 @@ -package message +package cardinal import ( "errors" @@ -9,22 +9,22 @@ import ( "pkg.world.dev/world-engine/cardinal/types" ) -type Manager struct { +type MessageManager struct { // registeredMessages maps message FullNames to a types.Message. registeredMessages map[string]types.Message registeredMessagesByType map[reflect.Type]types.Message nextMessageID types.MessageID } -func NewManager() *Manager { - return &Manager{ +func NewMessageManager() *MessageManager { + return &MessageManager{ registeredMessages: map[string]types.Message{}, registeredMessagesByType: map[reflect.Type]types.Message{}, nextMessageID: 1, } } -func (m *Manager) RegisterMessage(msgType types.Message, msgReflectType reflect.Type) error { +func (m *MessageManager) RegisterMessage(msgType types.Message, msgReflectType reflect.Type) error { fullName := msgType.FullName() // Checks if the message is already previously registered. if err := errors.Join(m.isMessageFullNameUnique(fullName), m.isMessageTypeUnique(msgReflectType)); err != nil { @@ -46,7 +46,7 @@ func (m *Manager) RegisterMessage(msgType types.Message, msgReflectType reflect. } // GetRegisteredMessages returns the list of all registered messages -func (m *Manager) GetRegisteredMessages() []types.Message { +func (m *MessageManager) GetRegisteredMessages() []types.Message { msgs := make([]types.Message, 0, len(m.registeredMessages)) for _, msg := range m.registeredMessages { msgs = append(msgs, msg) @@ -56,7 +56,7 @@ func (m *Manager) GetRegisteredMessages() []types.Message { // GetMessageByID iterates over the all registered messages and returns the types.Message associated with the // MessageID. -func (m *Manager) GetMessageByID(id types.MessageID) types.Message { +func (m *MessageManager) GetMessageByID(id types.MessageID) types.Message { for _, msg := range m.registeredMessages { if id == msg.ID() { return msg @@ -66,18 +66,18 @@ func (m *Manager) GetMessageByID(id types.MessageID) types.Message { } // GetMessageByFullName returns the message with the given full name, if it exists. -func (m *Manager) GetMessageByFullName(fullName string) (types.Message, bool) { +func (m *MessageManager) GetMessageByFullName(fullName string) (types.Message, bool) { msg, ok := m.registeredMessages[fullName] return msg, ok } -func (m *Manager) GetMessageByType(mType reflect.Type) (types.Message, bool) { +func (m *MessageManager) GetMessageByType(mType reflect.Type) (types.Message, bool) { msg, ok := m.registeredMessagesByType[mType] return msg, ok } // isMessageFullNameUnique checks if the message name already exist in messages map. -func (m *Manager) isMessageFullNameUnique(fullName string) error { +func (m *MessageManager) isMessageFullNameUnique(fullName string) error { _, ok := m.registeredMessages[fullName] if ok { return eris.Errorf("message %q is already registered", fullName) @@ -86,7 +86,7 @@ func (m *Manager) isMessageFullNameUnique(fullName string) error { } // isMessageTypeUnique checks if the message type name already exist in messages map. -func (m *Manager) isMessageTypeUnique(msgReflectType reflect.Type) error { +func (m *MessageManager) isMessageTypeUnique(msgReflectType reflect.Type) error { _, ok := m.registeredMessagesByType[msgReflectType] if ok { return eris.Errorf("message type %q is already registered", msgReflectType) diff --git a/cardinal/message/message_test.go b/cardinal/message_test.go similarity index 99% rename from cardinal/message/message_test.go rename to cardinal/message_test.go index 1bd09c216..f92ea1d7b 100644 --- a/cardinal/message/message_test.go +++ b/cardinal/message_test.go @@ -1,4 +1,4 @@ -package message +package cardinal import ( "testing" diff --git a/cardinal/plugin_persona.go b/cardinal/plugin_persona.go index be4223751..a474d5011 100644 --- a/cardinal/plugin_persona.go +++ b/cardinal/plugin_persona.go @@ -7,12 +7,10 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/rotisserie/eris" - "pkg.world.dev/world-engine/cardinal/message" "pkg.world.dev/world-engine/cardinal/persona" "pkg.world.dev/world-engine/cardinal/persona/component" "pkg.world.dev/world-engine/cardinal/persona/msg" "pkg.world.dev/world-engine/cardinal/persona/query" - querylib "pkg.world.dev/world-engine/cardinal/query" "pkg.world.dev/world-engine/cardinal/search" "pkg.world.dev/world-engine/cardinal/search/filter" "pkg.world.dev/world-engine/cardinal/types" @@ -73,7 +71,7 @@ func (p *personaPlugin) Register(world *World) error { func (p *personaPlugin) RegisterQueries(world *World) error { err := RegisterQuery[query.PersonaSignerQueryRequest, query.PersonaSignerQueryResponse](world, "signer", query.PersonaSignerQuery, - querylib.WithCustomQueryGroup[query.PersonaSignerQueryRequest, query.PersonaSignerQueryResponse]("persona")) + WithCustomQueryGroup[query.PersonaSignerQueryRequest, query.PersonaSignerQueryResponse]("persona")) if err != nil { return err } @@ -101,8 +99,8 @@ func (p *personaPlugin) RegisterMessages(world *World) error { RegisterMessage[msg.CreatePersona, msg.CreatePersonaResult]( world, msg.CreatePersonaMessageName, - message.WithCustomMessageGroup[msg.CreatePersona, msg.CreatePersonaResult]("persona"), - message.WithMsgEVMSupport[msg.CreatePersona, msg.CreatePersonaResult]()), + WithCustomMessageGroup[msg.CreatePersona, msg.CreatePersonaResult]("persona"), + WithMsgEVMSupport[msg.CreatePersona, msg.CreatePersonaResult]()), RegisterMessage[msg.AuthorizePersonaAddress, msg.AuthorizePersonaAddressResult]( world, "authorize-persona-address", @@ -122,7 +120,7 @@ func AuthorizePersonaAddressSystem(wCtx engine.Context) error { } return EachMessage[msg.AuthorizePersonaAddress, msg.AuthorizePersonaAddressResult]( wCtx, - func(txData message.TxData[msg.AuthorizePersonaAddress]) ( + func(txData TxData[msg.AuthorizePersonaAddress]) ( result msg.AuthorizePersonaAddressResult, err error, ) { txMsg, tx := txData.Msg, txData.Tx @@ -175,7 +173,7 @@ func CreatePersonaSystem(wCtx engine.Context) error { } return EachMessage[msg.CreatePersona, msg.CreatePersonaResult]( wCtx, - func(txData message.TxData[msg.CreatePersona]) (result msg.CreatePersonaResult, err error) { + func(txData TxData[msg.CreatePersona]) (result msg.CreatePersonaResult, err error) { txMsg := txData.Msg result.Success = false diff --git a/cardinal/query/query.go b/cardinal/query.go similarity index 91% rename from cardinal/query/query.go rename to cardinal/query.go index 6b3373f20..200c509cd 100644 --- a/cardinal/query/query.go +++ b/cardinal/query.go @@ -1,4 +1,4 @@ -package query +package cardinal import ( "encoding/json" @@ -8,14 +8,13 @@ import ( "github.com/rotisserie/eris" "pkg.world.dev/world-engine/cardinal/abi" - "pkg.world.dev/world-engine/cardinal/message" "pkg.world.dev/world-engine/cardinal/types" "pkg.world.dev/world-engine/cardinal/types/engine" ) var _ engine.Query = &queryType[struct{}, struct{}]{} -type Option[Request, Reply any] func(qt *queryType[Request, Reply]) +type QueryOption[Request, Reply any] func(qt *queryType[Request, Reply]) type queryType[Request any, Reply any] struct { name string @@ -25,7 +24,7 @@ type queryType[Request any, Reply any] struct { replyABI *ethereumAbi.Type } -func WithQueryEVMSupport[Request, Reply any]() Option[Request, Reply] { +func WithQueryEVMSupport[Request, Reply any]() QueryOption[Request, Reply] { return func(qt *queryType[Request, Reply]) { if err := qt.generateABIBindings(); err != nil { panic(err) @@ -37,7 +36,7 @@ func WithQueryEVMSupport[Request, Reply any]() Option[Request, Reply] { // By default, queries are registered under the "game" group which maps it to the /query/game/:queryType route. // This option allows you to set a custom group, which allow you to register the query // under /query//:queryType. -func WithCustomQueryGroup[Request, Reply any](group string) Option[Request, Reply] { +func WithCustomQueryGroup[Request, Reply any](group string) QueryOption[Request, Reply] { return func(qt *queryType[Request, Reply]) { qt.group = group } @@ -46,7 +45,7 @@ func WithCustomQueryGroup[Request, Reply any](group string) Option[Request, Repl func NewQueryType[Request any, Reply any]( name string, handler func(wCtx engine.Context, req *Request) (*Reply, error), - opts ...Option[Request, Reply], + opts ...QueryOption[Request, Reply], ) (engine.Query, error) { err := validateQuery[Request, Reply](name, handler) if err != nil { @@ -130,7 +129,7 @@ func (r *queryType[req, rep]) HandleQueryRaw(wCtx engine.Context, bz []byte) ([] func (r *queryType[req, rep]) DecodeEVMRequest(bz []byte) (any, error) { if r.requestABI == nil { - return nil, eris.Wrap(message.ErrEVMTypeNotSet, "") + return nil, eris.Wrap(ErrEVMTypeNotSet, "") } args := ethereumAbi.Arguments{{Type: *r.requestABI}} unpacked, err := args.Unpack(bz) @@ -149,7 +148,7 @@ func (r *queryType[req, rep]) DecodeEVMRequest(bz []byte) (any, error) { func (r *queryType[req, rep]) DecodeEVMReply(bz []byte) (any, error) { if r.replyABI == nil { - return nil, eris.Wrap(message.ErrEVMTypeNotSet, "") + return nil, eris.Wrap(ErrEVMTypeNotSet, "") } args := ethereumAbi.Arguments{{Type: *r.replyABI}} unpacked, err := args.Unpack(bz) @@ -168,7 +167,7 @@ func (r *queryType[req, rep]) DecodeEVMReply(bz []byte) (any, error) { func (r *queryType[req, rep]) EncodeEVMReply(a any) ([]byte, error) { if r.replyABI == nil { - return nil, eris.Wrap(message.ErrEVMTypeNotSet, "") + return nil, eris.Wrap(ErrEVMTypeNotSet, "") } args := ethereumAbi.Arguments{{Type: *r.replyABI}} bz, err := args.Pack(a) @@ -177,7 +176,7 @@ func (r *queryType[req, rep]) EncodeEVMReply(a any) ([]byte, error) { func (r *queryType[Request, Reply]) EncodeAsABI(input any) ([]byte, error) { if r.requestABI == nil || r.replyABI == nil { - return nil, eris.Wrap(message.ErrEVMTypeNotSet, "") + return nil, eris.Wrap(ErrEVMTypeNotSet, "") } var args ethereumAbi.Arguments diff --git a/cardinal/query/manager.go b/cardinal/query_manager.go similarity index 73% rename from cardinal/query/manager.go rename to cardinal/query_manager.go index f3910d12e..d825337c9 100644 --- a/cardinal/query/manager.go +++ b/cardinal/query_manager.go @@ -1,4 +1,4 @@ -package query +package cardinal import ( "github.com/rotisserie/eris" @@ -6,19 +6,19 @@ import ( "pkg.world.dev/world-engine/cardinal/types/engine" ) -type Manager struct { +type QueryManager struct { registeredQueries map[string]engine.Query } -func NewManager() *Manager { - return &Manager{ +func NewQueryManager() *QueryManager { + return &QueryManager{ registeredQueries: make(map[string]engine.Query), } } // RegisterQuery registers a query with the query manager. // There can only be one query with a given name. -func (m *Manager) RegisterQuery(name string, query engine.Query) error { +func (m *QueryManager) RegisterQuery(name string, query engine.Query) error { // Check that the query is not already registered if err := m.isQueryNameUnique(name); err != nil { return err @@ -31,7 +31,7 @@ func (m *Manager) RegisterQuery(name string, query engine.Query) error { } // GetRegisteredQueries returns all the registered queries. -func (m *Manager) GetRegisteredQueries() []engine.Query { +func (m *QueryManager) GetRegisteredQueries() []engine.Query { registeredQueries := make([]engine.Query, 0, len(m.registeredQueries)) for _, query := range m.registeredQueries { registeredQueries = append(registeredQueries, query) @@ -40,7 +40,7 @@ func (m *Manager) GetRegisteredQueries() []engine.Query { } // GetQueryByName returns a query corresponding to its name. -func (m *Manager) GetQueryByName(name string) (engine.Query, error) { +func (m *QueryManager) GetQueryByName(name string) (engine.Query, error) { query, ok := m.registeredQueries[name] if !ok { return nil, eris.Errorf("query %q is not registered", name) @@ -48,7 +48,7 @@ func (m *Manager) GetQueryByName(name string) (engine.Query, error) { return query, nil } -func (m *Manager) isQueryNameUnique(name string) error { +func (m *QueryManager) isQueryNameUnique(name string) error { if _, ok := m.registeredQueries[name]; ok { return eris.Errorf("query %q is already registered", name) } diff --git a/cardinal/query/query_test.go b/cardinal/query_test.go similarity index 96% rename from cardinal/query/query_test.go rename to cardinal/query_test.go index 25307a577..1fea4efb7 100644 --- a/cardinal/query/query_test.go +++ b/cardinal/query_test.go @@ -1,4 +1,4 @@ -package query_test +package cardinal_test import ( "errors" @@ -6,7 +6,6 @@ import ( "pkg.world.dev/world-engine/assert" "pkg.world.dev/world-engine/cardinal" - "pkg.world.dev/world-engine/cardinal/query" "pkg.world.dev/world-engine/cardinal/search/filter" "pkg.world.dev/world-engine/cardinal/testutils" "pkg.world.dev/world-engine/cardinal/types" @@ -71,7 +70,7 @@ func TestNewQueryTypeWithEVMSupport(t *testing.T) { ) (*FooReply, error) { return &FooReply{}, errors.New("this function should never get called") }, - query.WithQueryEVMSupport[FooReq, FooReply](), + cardinal.WithQueryEVMSupport[FooReq, FooReply](), ) } @@ -153,7 +152,7 @@ func TestQueryEVM(t *testing.T) { ) (*FooReply, error) { return &expectedReply, nil }, - query.WithQueryEVMSupport[FooRequest, FooReply](), + cardinal.WithQueryEVMSupport[FooRequest, FooReply](), ) assert.NilError(t, err) diff --git a/cardinal/router/iterator/iterator_test.go b/cardinal/router/iterator/iterator_test.go index f0b26dff3..f6cea4008 100644 --- a/cardinal/router/iterator/iterator_test.go +++ b/cardinal/router/iterator/iterator_test.go @@ -10,14 +10,14 @@ import ( "google.golang.org/protobuf/proto" "pkg.world.dev/world-engine/assert" - "pkg.world.dev/world-engine/cardinal/message" + "pkg.world.dev/world-engine/cardinal" "pkg.world.dev/world-engine/cardinal/router/iterator" "pkg.world.dev/world-engine/cardinal/types" shard "pkg.world.dev/world-engine/rift/shard/v2" ) var _ shard.TransactionHandlerClient = &mockQuerier{} -var fooMsg = message.NewMessageType[fooIn, fooOut]("foo") +var fooMsg = cardinal.NewMessageType[fooIn, fooOut]("foo") type fooIn struct{ X int } type fooOut struct{} diff --git a/cardinal/server/receipts_test.go b/cardinal/server/receipts_test.go index 472daa257..f2f76da4c 100644 --- a/cardinal/server/receipts_test.go +++ b/cardinal/server/receipts_test.go @@ -6,7 +6,6 @@ import ( "net/http" "pkg.world.dev/world-engine/cardinal" - "pkg.world.dev/world-engine/cardinal/message" "pkg.world.dev/world-engine/cardinal/server/handler" "pkg.world.dev/world-engine/sign" ) @@ -21,7 +20,7 @@ func (s *ServerTestSuite) TestReceiptsQuery() { s.Require().NoError(err) wantErrorMessage := "THIS_ERROR_MESSAGE_SHOULD_BE_IN_THE_RECEIPT" err = cardinal.RegisterSystems(world, func(ctx cardinal.WorldContext) error { - return cardinal.EachMessage[fooIn, fooOut](ctx, func(message.TxData[fooIn]) (fooOut, error) { + return cardinal.EachMessage[fooIn, fooOut](ctx, func(cardinal.TxData[fooIn]) (fooOut, error) { if ctx.CurrentTick()%2 == 0 { return fooOut{Y: 4}, nil } diff --git a/cardinal/server/server_test.go b/cardinal/server/server_test.go index f1b2e2698..5927736be 100644 --- a/cardinal/server/server_test.go +++ b/cardinal/server/server_test.go @@ -19,9 +19,7 @@ import ( "pkg.world.dev/world-engine/assert" "pkg.world.dev/world-engine/cardinal" - "pkg.world.dev/world-engine/cardinal/message" "pkg.world.dev/world-engine/cardinal/persona/msg" - "pkg.world.dev/world-engine/cardinal/query" "pkg.world.dev/world-engine/cardinal/server/handler" "pkg.world.dev/world-engine/cardinal/server/utils" "pkg.world.dev/world-engine/cardinal/testutils" @@ -209,7 +207,7 @@ func (s *ServerTestSuite) TestQueryCustomGroup() { called = true return &SomeResponse{}, nil }, - query.WithCustomQueryGroup[SomeRequest, SomeResponse](group), + cardinal.WithCustomQueryGroup[SomeRequest, SomeResponse](group), ) s.Require().NoError(err) s.fixture.DoTick() @@ -283,7 +281,7 @@ func (s *ServerTestSuite) setupWorld(opts ...cardinal.WorldOption) { personaToPosition := make(map[string]types.EntityID) err = cardinal.RegisterSystems(s.world, func(context engine.Context) error { return cardinal.EachMessage[MoveMsgInput, MoveMessageOutput](context, - func(tx message.TxData[MoveMsgInput]) (MoveMessageOutput, error) { + func(tx cardinal.TxData[MoveMsgInput]) (MoveMessageOutput, error) { posID, exists := personaToPosition[tx.Tx.PersonaTag] if !exists { id, err := cardinal.Create(context, LocationComponent{}) diff --git a/cardinal/state_test.go b/cardinal/state_test.go index bbff4da5f..141b9f971 100644 --- a/cardinal/state_test.go +++ b/cardinal/state_test.go @@ -8,7 +8,6 @@ import ( "pkg.world.dev/world-engine/assert" "pkg.world.dev/world-engine/cardinal" "pkg.world.dev/world-engine/cardinal/iterators" - "pkg.world.dev/world-engine/cardinal/message" "pkg.world.dev/world-engine/cardinal/search/filter" "pkg.world.dev/world-engine/cardinal/testutils" "pkg.world.dev/world-engine/cardinal/types" @@ -312,7 +311,7 @@ func TestCanFindTransactionsAfterReloadingEngine(t *testing.T) { world, func(wCtx engine.Context) error { someTx, err := testutils.GetMessage[Msg, Result](wCtx) - return cardinal.EachMessage[Msg, Result](wCtx, func(tx message.TxData[Msg]) (Result, error) { + return cardinal.EachMessage[Msg, Result](wCtx, func(tx cardinal.TxData[Msg]) (Result, error) { someTx.SetResult(wCtx, tx.Hash, Result{}) return Result{}, err }) diff --git a/cardinal/testutils/testutils.go b/cardinal/testutils/testutils.go index 6bab0f4d8..45691c18d 100644 --- a/cardinal/testutils/testutils.go +++ b/cardinal/testutils/testutils.go @@ -9,7 +9,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/rotisserie/eris" - "pkg.world.dev/world-engine/cardinal/message" + "pkg.world.dev/world-engine/cardinal" "pkg.world.dev/world-engine/cardinal/types" "pkg.world.dev/world-engine/cardinal/types/engine" "pkg.world.dev/world-engine/sign" @@ -61,15 +61,15 @@ func UniqueSignature() *sign.Transaction { return UniqueSignatureWithName("some_persona_tag") } -func GetMessage[In any, Out any](wCtx engine.Context) (*message.MessageType[In, Out], error) { - var msg message.MessageType[In, Out] +func GetMessage[In any, Out any](wCtx engine.Context) (*cardinal.MessageType[In, Out], error) { + var msg cardinal.MessageType[In, Out] msgType := reflect.TypeOf(msg) tempRes, ok := wCtx.GetMessageByType(msgType) if !ok { return nil, eris.Errorf("Could not find %q, Message may not be registered.", msg.Name()) } var _ types.Message = &msg - res, ok := tempRes.(*message.MessageType[In, Out]) + res, ok := tempRes.(*cardinal.MessageType[In, Out]) if !ok { return &msg, eris.New("wrong type") } diff --git a/cardinal/world.go b/cardinal/world.go index 646cb5c33..f2218b64b 100644 --- a/cardinal/world.go +++ b/cardinal/world.go @@ -20,8 +20,6 @@ import ( "pkg.world.dev/world-engine/cardinal/component" "pkg.world.dev/world-engine/cardinal/gamestate" ecslog "pkg.world.dev/world-engine/cardinal/log" - "pkg.world.dev/world-engine/cardinal/message" - "pkg.world.dev/world-engine/cardinal/query" "pkg.world.dev/world-engine/cardinal/receipt" "pkg.world.dev/world-engine/cardinal/router" "pkg.world.dev/world-engine/cardinal/search" @@ -60,10 +58,10 @@ type World struct { // Core modules worldStage *worldstage.Manager - msgManager *message.Manager + msgManager *MessageManager systemManager *system.Manager componentManager *component.Manager - queryManager *query.Manager + queryManager *QueryManager router router.Router txPool *txpool.TxPool @@ -127,10 +125,10 @@ func NewWorld(opts ...WorldOption) (*World, error) { // Core modules worldStage: worldstage.NewManager(), - msgManager: message.NewManager(), + msgManager: NewMessageManager(), systemManager: system.NewManager(), componentManager: component.NewManager(&redisMetaStore), - queryManager: query.NewManager(), + queryManager: NewQueryManager(), router: nil, // Will be set if run mode is production or its injected via options txPool: txpool.New(), diff --git a/cardinal/world_recovery_test.go b/cardinal/world_recovery_test.go index 1fe26f80f..d45145750 100644 --- a/cardinal/world_recovery_test.go +++ b/cardinal/world_recovery_test.go @@ -7,7 +7,6 @@ import ( "github.com/golang/mock/gomock" "pkg.world.dev/world-engine/cardinal" - "pkg.world.dev/world-engine/cardinal/message" "pkg.world.dev/world-engine/cardinal/router/iterator" iteratormocks "pkg.world.dev/world-engine/cardinal/router/iterator/mocks" "pkg.world.dev/world-engine/cardinal/router/mocks" @@ -49,7 +48,7 @@ func TestWorldRecovery(t *testing.T) { fooResponse]( world, msgName, - message.WithMsgEVMSupport[fooMessage, fooResponse]()) + cardinal.WithMsgEVMSupport[fooMessage, fooResponse]()) g.Assert(err).IsNil() var ok bool fooTx, ok = world.GetMessageByFullName("game." + msgName) diff --git a/cardinal/world_test.go b/cardinal/world_test.go index 1dc94b543..54d8201f4 100644 --- a/cardinal/world_test.go +++ b/cardinal/world_test.go @@ -18,7 +18,6 @@ import ( "pkg.world.dev/world-engine/assert" "pkg.world.dev/world-engine/cardinal/iterators" - "pkg.world.dev/world-engine/cardinal/message" "pkg.world.dev/world-engine/cardinal/search/filter" "pkg.world.dev/world-engine/cardinal/types" "pkg.world.dev/world-engine/cardinal/types/engine" @@ -537,15 +536,15 @@ func doTickCapturePanic(ctx context.Context, world *World) (err error) { return nil } -func getMessage[In any, Out any](wCtx engine.Context) (*message.MessageType[In, Out], error) { - var msg message.MessageType[In, Out] +func getMessage[In any, Out any](wCtx engine.Context) (*MessageType[In, Out], error) { + var msg MessageType[In, Out] msgType := reflect.TypeOf(msg) tempRes, ok := wCtx.GetMessageByType(msgType) if !ok { return &msg, eris.Errorf("Could not find %s, Message may not be registered.", msg.Name()) } var _ types.Message = &msg - res, ok := tempRes.(*message.MessageType[In, Out]) + res, ok := tempRes.(*MessageType[In, Out]) if !ok { return &msg, eris.New("wrong type") } From 961e9cd0c2eba77492ce6aeb2d08d72d783c6076 Mon Sep 17 00:00:00 2001 From: Brian Yeh Date: Mon, 20 May 2024 14:29:58 -0700 Subject: [PATCH 2/2] squash --- .github/workflows/test.yml | 4 +- CHANGELOG.MD | 2 + cardinal/benchmark/benchmark_test.go | 5 +- cardinal/cardinal.go | 117 +++++------- cardinal/cardinal_test.go | 67 +++---- cardinal/{search => }/composedsearch.go | 79 ++++----- cardinal/ecs_test.go | 12 +- cardinal/engine_test.go | 19 +- cardinal/errors_test.go | 69 ++++---- cardinal/example_messagetype_test.go | 3 +- cardinal/{search => }/filtercomponent.go | 21 ++- cardinal/go.mod | 8 +- cardinal/go.sum | 11 ++ cardinal/imported.go | 10 -- cardinal/log/log.go | 14 +- cardinal/log/log_test.go | 9 +- cardinal/message.go | 23 ++- cardinal/message_manager.go | 28 +-- cardinal/persona/persona_test.go | 36 ++-- .../{persona/query => }/persona_signer.go | 7 +- cardinal/plugin_persona.go | 23 ++- cardinal/query.go | 41 ++++- cardinal/query_manager.go | 60 +++++-- cardinal/query_test.go | 15 +- cardinal/router/router.go | 6 +- cardinal/{search => }/search.go | 100 ++++++----- cardinal/search/searchcomponent.go | 9 - cardinal/search/util.go | 32 ---- cardinal/search_test.go | 49 +++--- cardinal/server/debug_test.go | 19 +- cardinal/server/docs/docs.go | 133 ++++---------- cardinal/server/docs/swagger.json | 133 ++++---------- cardinal/server/docs/swagger.yaml | 110 ++++-------- cardinal/server/event_test.go | 11 +- cardinal/server/handler/cql.go | 62 +------ cardinal/server/handler/debug.go | 49 +----- cardinal/server/handler/query.go | 34 +--- cardinal/server/handler/receipts.go | 10 +- cardinal/server/handler/tx.go | 22 +-- cardinal/server/handler/world.go | 43 ++--- cardinal/server/server.go | 41 ++--- cardinal/server/server_test.go | 13 +- cardinal/server/types/provider.go | 15 +- cardinal/state_test.go | 7 +- cardinal/system.go | 156 ++++++++++++++++ cardinal/system/manager.go | 149 ---------------- cardinal/system/system.go | 7 - cardinal/{system => }/system_test.go | 19 +- cardinal/testutils/testutils.go | 20 --- cardinal/types/engine/context.go | 49 ------ cardinal/types/engine/mocks/context.go | 64 +++---- cardinal/types/engine/query.go | 25 --- cardinal/types/entity.go | 7 + cardinal/types/info.go | 8 + cardinal/types/query.go | 5 + cardinal/util.go | 43 ++++- cardinal/world.go | 166 +++++++++++++----- cardinal/world_context.go | 81 ++++++--- cardinal/world_persona.go | 5 +- cardinal/world_persona_test.go | 6 +- cardinal/world_receipt.go | 7 +- cardinal/world_recovery.go | 2 +- cardinal/world_test.go | 34 ++-- docs/cardinal/game/persona.mdx | 4 +- docs/cardinal/game/system/component.mdx | 10 +- docs/cardinal/game/system/entity.mdx | 6 +- docs/cardinal/game/world/api-reference.mdx | 4 +- docs/mint.json | 4 +- e2e/testgames/game/query/location.go | 3 +- e2e/testgames/game/sys/error.go | 3 +- e2e/testgames/game/sys/join.go | 3 +- e2e/testgames/game/sys/move.go | 3 +- e2e/testgames/gamebenchmark/main.go | 8 +- makefiles/test.mk | 12 +- relay/nakama/events/events.go | 139 +++++++++++---- relay/nakama/events/events_test.go | 99 ++++++++++- relay/nakama/events/notifications.go | 6 +- relay/nakama/events/notifications_test.go | 63 ++++--- 78 files changed, 1372 insertions(+), 1429 deletions(-) rename cardinal/{search => }/composedsearch.go (66%) rename cardinal/{search => }/filtercomponent.go (68%) delete mode 100644 cardinal/imported.go rename cardinal/{persona/query => }/persona_signer.go (85%) rename cardinal/{search => }/search.go (80%) delete mode 100644 cardinal/search/searchcomponent.go delete mode 100644 cardinal/search/util.go create mode 100644 cardinal/system.go delete mode 100644 cardinal/system/manager.go delete mode 100644 cardinal/system/system.go rename cardinal/{system => }/system_test.go (84%) delete mode 100644 cardinal/types/engine/context.go delete mode 100644 cardinal/types/engine/query.go create mode 100644 cardinal/types/info.go create mode 100644 cardinal/types/query.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0bd13c1de..5ca34430c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -80,8 +80,8 @@ jobs: uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} - ## skip cache, use Namespace volume cache - cache: false + cache: true + cache-dependency-path: "**/*.sum" - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Run swagger-check diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 7aa198dfd..d4c17a544 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -58,6 +58,8 @@ Ref: https://keepachangelog.com/en/1.0.0/ - (cardinal) #WORLD-702: Cardinal's transactions are now sent at the end of a tick, instead of one by one at the server handler. +- (nakama) GDEV-1024: Nakama will automatically attempt to reconnect a websocket when cardinal is restarted. + ### Deprecated ### Bug Fixes diff --git a/cardinal/benchmark/benchmark_test.go b/cardinal/benchmark/benchmark_test.go index 24b21ee0c..7d249590f 100644 --- a/cardinal/benchmark/benchmark_test.go +++ b/cardinal/benchmark/benchmark_test.go @@ -13,7 +13,6 @@ import ( "pkg.world.dev/world-engine/cardinal/search/filter" "pkg.world.dev/world-engine/cardinal/testutils" "pkg.world.dev/world-engine/cardinal/types" - "pkg.world.dev/world-engine/cardinal/types/engine" ) type Health struct { @@ -25,7 +24,7 @@ func (Health) Name() string { } // setupWorld Creates a new *cardinal.World and initializes the world to have numOfEntities already cardinal.Created. If -// enableHealthSystem is set, a System will be added to the world that increments every entity's "health" by 1 every +// enableHealthSystem is set, a system will be added to the world that increments every entity's "health" by 1 every // tick. func setupWorld(t testing.TB, numOfEntities int, enableHealthSystem bool) *testutils.TestFixture { tf := testutils.NewTestFixture(t, nil) @@ -35,7 +34,7 @@ func setupWorld(t testing.TB, numOfEntities int, enableHealthSystem bool) *testu if enableHealthSystem { err := cardinal.RegisterSystems( world, - func(wCtx engine.Context) error { + func(wCtx cardinal.WorldContext) error { q := cardinal.NewSearch().Entity(filter.Contains(filter.Component[Health]())) err := q.Each(wCtx, func(id types.EntityID) bool { diff --git a/cardinal/cardinal.go b/cardinal/cardinal.go index c2e649109..885298a10 100644 --- a/cardinal/cardinal.go +++ b/cardinal/cardinal.go @@ -9,10 +9,7 @@ import ( "pkg.world.dev/world-engine/cardinal/component" "pkg.world.dev/world-engine/cardinal/iterators" - "pkg.world.dev/world-engine/cardinal/search" - "pkg.world.dev/world-engine/cardinal/system" "pkg.world.dev/world-engine/cardinal/types" - "pkg.world.dev/world-engine/cardinal/types/engine" "pkg.world.dev/world-engine/cardinal/worldstage" ) @@ -25,27 +22,6 @@ var ( ErrComponentAlreadyOnEntity = iterators.ErrComponentAlreadyOnEntity ) -// Imported -// This section aggregates function from other packages such that they are easily accessible -// via cardinal. - -// NewSearch is used to create a search object. -// -// Usage: -// -// cardinal.NewSearch().Entity(filter.Contains(filter.Component[EnergyComponent]())) -var NewSearch = search.NewSearch - -// NewLegacySearch allows users to create a Search object with a filter already provided -// as a property. -// -// Example Usage: -// -// cardinal.NewLegacySearch().Entity(filter.Exact(Alpha{}, Beta{})).Count() -var NewLegacySearch = search.NewLegacySearch - -type Search = search.Search - // FilterFunction wrap your component filter function of func(comp T) bool inside FilterFunction to use // in search. // @@ -56,36 +32,36 @@ type Search = search.Search // return true // })) -func FilterFunction[T types.Component](f func(comp T) bool) func(ctx engine.Context, id types.EntityID) (bool, error) { - return search.ComponentFilter[T](f) +func FilterFunction[T types.Component](f func(comp T) bool) func(ctx WorldContext, id types.EntityID) (bool, error) { + return ComponentFilter[T](f) } -func RegisterSystems(w *World, sys ...system.System) error { +func RegisterSystems(w *World, sys ...System) error { if w.worldStage.Current() != worldstage.Init { return eris.Errorf( - "engine state is %s, expected %s to register systems", + "world state is %s, expected %s to register systems", w.worldStage.Current(), worldstage.Init, ) } - return w.systemManager.RegisterSystems(sys...) + return w.SystemManager.registerSystems(false, sys...) } -func RegisterInitSystems(w *World, sys ...system.System) error { +func RegisterInitSystems(w *World, sys ...System) error { if w.worldStage.Current() != worldstage.Init { return eris.Errorf( - "engine state is %s, expected %s to register init systems", + "world state is %s, expected %s to register init systems", w.worldStage.Current(), worldstage.Init, ) } - return w.systemManager.RegisterInitSystems(sys...) + return w.SystemManager.registerSystems(true, sys...) } func RegisterComponent[T types.Component](w *World) error { if w.worldStage.Current() != worldstage.Init { return eris.Errorf( - "engine state is %s, expected %s to register component", + "world state is %s, expected %s to register component", w.worldStage.Current(), worldstage.Init, ) @@ -111,10 +87,10 @@ func MustRegisterComponent[T types.Component](w *World) { } } -func EachMessage[In any, Out any](wCtx engine.Context, fn func(TxData[In]) (Out, error)) error { +func EachMessage[In any, Out any](wCtx WorldContext, fn func(TxData[In]) (Out, error)) error { var msg MessageType[In, Out] msgType := reflect.TypeOf(msg) - tempRes, ok := wCtx.GetMessageByType(msgType) + tempRes, ok := wCtx.getMessageByType(msgType) if !ok { return eris.Errorf("Could not find %s, Message may not be registered.", msg.Name()) } @@ -133,7 +109,7 @@ func EachMessage[In any, Out any](wCtx engine.Context, fn func(TxData[In]) (Out, func RegisterMessage[In any, Out any](world *World, name string, opts ...MessageOption[In, Out]) error { if world.worldStage.Current() != worldstage.Init { return eris.Errorf( - "engine state is %s, expected %s to register messages", + "world state is %s, expected %s to register messages", world.worldStage.Current(), worldstage.Init, ) @@ -143,7 +119,7 @@ func RegisterMessage[In any, Out any](world *World, name string, opts ...Message msgType := NewMessageType[In, Out](name, opts...) // Register the message with the manager - err := world.msgManager.RegisterMessage(msgType, reflect.TypeOf(*msgType)) + err := world.RegisterMessage(msgType, reflect.TypeOf(*msgType)) if err != nil { return err } @@ -154,28 +130,29 @@ func RegisterMessage[In any, Out any](world *World, name string, opts ...Message func RegisterQuery[Request any, Reply any]( w *World, name string, - handler func(wCtx engine.Context, req *Request) (*Reply, error), + handler func(wCtx WorldContext, req *Request) (*Reply, error), opts ...QueryOption[Request, Reply], ) (err error) { if w.worldStage.Current() != worldstage.Init { return eris.Errorf( - "engine state is %s, expected %s to register query", + "world state is %s, expected %s to register query", w.worldStage.Current(), worldstage.Init, ) } - q, err := NewQueryType[Request, Reply](name, handler, opts...) + q, err := newQueryType[Request, Reply](name, handler, opts...) if err != nil { return err } - return w.queryManager.RegisterQuery(name, q) + res := w.RegisterQuery(name, q) + return res } // Create creates a single entity in the world, and returns the id of the newly created entity. // At least 1 component must be provided. -func Create(wCtx engine.Context, components ...types.Component) (_ types.EntityID, err error) { +func Create(wCtx WorldContext, components ...types.Component) (_ types.EntityID, err error) { // We don't handle panics here because we let CreateMany handle it for us entityIDs, err := CreateMany(wCtx, 1, components...) if err != nil { @@ -186,22 +163,22 @@ func Create(wCtx engine.Context, components ...types.Component) (_ types.EntityI // CreateMany creates multiple entities in the world, and returns the slice of ids for the newly created // entities. At least 1 component must be provided. -func CreateMany(wCtx engine.Context, num int, components ...types.Component) (entityIDs []types.EntityID, err error) { +func CreateMany(wCtx WorldContext, num int, components ...types.Component) (entityIDs []types.EntityID, err error) { defer func() { panicOnFatalError(wCtx, err) }() // Error if the context is read only - if wCtx.IsReadOnly() { + if wCtx.isReadOnly() { return nil, ErrEntityMutationOnReadOnly } - if !wCtx.IsWorldReady() { + if !wCtx.isWorldReady() { return nil, ErrEntitiesCreatedBeforeReady } // Get all component metadata for the given components acc := make([]types.ComponentMetadata, 0, len(components)) for _, comp := range components { - c, err := wCtx.GetComponentByName(comp.Name()) + c, err := wCtx.getComponentByName(comp.Name()) if err != nil { return nil, eris.Wrap(err, "failed to create entity because component is not registered") } @@ -209,7 +186,7 @@ func CreateMany(wCtx engine.Context, num int, components ...types.Component) (en } // Create the entities - entityIDs, err = wCtx.StoreManager().CreateManyEntities(num, acc...) + entityIDs, err = wCtx.storeManager().CreateManyEntities(num, acc...) if err != nil { return nil, err } @@ -218,12 +195,12 @@ func CreateMany(wCtx engine.Context, num int, components ...types.Component) (en for _, id := range entityIDs { for _, comp := range components { var c types.ComponentMetadata - c, err = wCtx.GetComponentByName(comp.Name()) + c, err = wCtx.getComponentByName(comp.Name()) if err != nil { return nil, eris.Wrap(err, "failed to create entity because component is not registered") } - err = wCtx.StoreManager().SetComponentForEntity(c, id, comp) + err = wCtx.storeManager().SetComponentForEntity(c, id, comp) if err != nil { return nil, err } @@ -234,23 +211,23 @@ func CreateMany(wCtx engine.Context, num int, components ...types.Component) (en } // SetComponent sets component data to the entity. -func SetComponent[T types.Component](wCtx engine.Context, id types.EntityID, component *T) (err error) { +func SetComponent[T types.Component](wCtx WorldContext, id types.EntityID, component *T) (err error) { defer func() { panicOnFatalError(wCtx, err) }() // Error if the context is read only - if wCtx.IsReadOnly() { + if wCtx.isReadOnly() { return ErrEntityMutationOnReadOnly } // Get the component metadata var t T - c, err := wCtx.GetComponentByName(t.Name()) + c, err := wCtx.getComponentByName(t.Name()) if err != nil { return err } // Store the component - err = wCtx.StoreManager().SetComponentForEntity(c, id, component) + err = wCtx.storeManager().SetComponentForEntity(c, id, component) if err != nil { return err } @@ -266,18 +243,18 @@ func SetComponent[T types.Component](wCtx engine.Context, id types.EntityID, com } // GetComponent returns component data from the entity. -func GetComponent[T types.Component](wCtx engine.Context, id types.EntityID) (comp *T, err error) { +func GetComponent[T types.Component](wCtx WorldContext, id types.EntityID) (comp *T, err error) { defer func() { panicOnFatalError(wCtx, err) }() // Get the component metadata var t T - c, err := wCtx.GetComponentByName(t.Name()) + c, err := wCtx.getComponentByName(t.Name()) if err != nil { return nil, err } // Get current component value - compValue, err := wCtx.StoreReader().GetComponentForEntity(c, id) + compValue, err := wCtx.storeReader().GetComponentForEntity(c, id) if err != nil { return nil, err } @@ -296,11 +273,11 @@ func GetComponent[T types.Component](wCtx engine.Context, id types.EntityID) (co return comp, nil } -func UpdateComponent[T types.Component](wCtx engine.Context, id types.EntityID, fn func(*T) *T) (err error) { +func UpdateComponent[T types.Component](wCtx WorldContext, id types.EntityID, fn func(*T) *T) (err error) { defer func() { panicOnFatalError(wCtx, err) }() // Error if the context is read only - if wCtx.IsReadOnly() { + if wCtx.isReadOnly() { return err } @@ -322,23 +299,23 @@ func UpdateComponent[T types.Component](wCtx engine.Context, id types.EntityID, return nil } -func AddComponentTo[T types.Component](wCtx engine.Context, id types.EntityID) (err error) { +func AddComponentTo[T types.Component](wCtx WorldContext, id types.EntityID) (err error) { defer func() { panicOnFatalError(wCtx, err) }() // Error if the context is read only - if wCtx.IsReadOnly() { + if wCtx.isReadOnly() { return ErrEntityMutationOnReadOnly } // Get the component metadata var t T - c, err := wCtx.GetComponentByName(t.Name()) + c, err := wCtx.getComponentByName(t.Name()) if err != nil { return err } // Add the component to entity - err = wCtx.StoreManager().AddComponentToEntity(c, id) + err = wCtx.storeManager().AddComponentToEntity(c, id) if err != nil { return err } @@ -347,23 +324,23 @@ func AddComponentTo[T types.Component](wCtx engine.Context, id types.EntityID) ( } // RemoveComponentFrom removes a component from an entity. -func RemoveComponentFrom[T types.Component](wCtx engine.Context, id types.EntityID) (err error) { +func RemoveComponentFrom[T types.Component](wCtx WorldContext, id types.EntityID) (err error) { defer func() { panicOnFatalError(wCtx, err) }() // Error if the context is read only - if wCtx.IsReadOnly() { + if wCtx.isReadOnly() { return ErrEntityMutationOnReadOnly } // Get the component metadata var t T - c, err := wCtx.GetComponentByName(t.Name()) + c, err := wCtx.getComponentByName(t.Name()) if err != nil { return err } // Remove the component from entity - err = wCtx.StoreManager().RemoveComponentFromEntity(c, id) + err = wCtx.storeManager().RemoveComponentFromEntity(c, id) if err != nil { return err } @@ -371,16 +348,16 @@ func RemoveComponentFrom[T types.Component](wCtx engine.Context, id types.Entity return nil } -// Remove removes the given Entity from the engine. -func Remove(wCtx engine.Context, id types.EntityID) (err error) { +// Remove removes the given Entity from the world. +func Remove(wCtx WorldContext, id types.EntityID) (err error) { defer func() { panicOnFatalError(wCtx, err) }() // Error if the context is read only - if wCtx.IsReadOnly() { + if wCtx.isReadOnly() { return ErrEntityMutationOnReadOnly } - err = wCtx.StoreManager().RemoveEntity(id) + err = wCtx.storeManager().RemoveEntity(id) if err != nil { return err } diff --git a/cardinal/cardinal_test.go b/cardinal/cardinal_test.go index d2138a805..4bda442de 100644 --- a/cardinal/cardinal_test.go +++ b/cardinal/cardinal_test.go @@ -23,7 +23,6 @@ import ( "pkg.world.dev/world-engine/cardinal/search/filter" "pkg.world.dev/world-engine/cardinal/testutils" "pkg.world.dev/world-engine/cardinal/types" - "pkg.world.dev/world-engine/cardinal/types/engine" "pkg.world.dev/world-engine/sign" ) @@ -47,16 +46,6 @@ type Bar struct{} func (Bar) Name() string { return "bar" } -type Qux struct{} - -func (Qux) Name() string { return "qux" } - -type CardinalTestHealth struct { - Value int -} - -func (CardinalTestHealth) Name() string { return "health" } - func TestForEachTransaction(t *testing.T) { tf := testutils.NewTestFixture(t, nil) world := tf.World @@ -70,7 +59,7 @@ func TestForEachTransaction(t *testing.T) { someMsgName := "some_msg" assert.NilError(t, cardinal.RegisterMessage[SomeMsgRequest, SomeMsgResponse](world, someMsgName)) - err := cardinal.RegisterSystems(world, func(wCtx engine.Context) error { + err := cardinal.RegisterSystems(world, func(wCtx cardinal.WorldContext) error { return cardinal.EachMessage[SomeMsgRequest, SomeMsgResponse](wCtx, func(t cardinal.TxData[SomeMsgRequest]) (result SomeMsgResponse, err error) { if t.Msg.GenerateError { @@ -146,7 +135,7 @@ func TestSystemsAreExecutedDuringGameTick(t *testing.T) { err := cardinal.RegisterSystems( world, - func(wCtx engine.Context) error { + func(wCtx cardinal.WorldContext) error { search := cardinal.NewSearch().Entity(filter.Exact(filter.Component[CounterComponent]())) id := search.MustFirst(wCtx) return cardinal.UpdateComponent[CounterComponent]( @@ -180,7 +169,7 @@ func TestTransactionAreAppliedToSomeEntities(t *testing.T) { err := cardinal.RegisterSystems( world, - func(wCtx engine.Context) error { + func(wCtx cardinal.WorldContext) error { return cardinal.EachMessage[*ModifyScoreMsg, *EmptyMsgResult](wCtx, func(msData cardinal.TxData[*ModifyScoreMsg]) (*EmptyMsgResult, error) { ms := msData.Msg @@ -202,7 +191,7 @@ func TestTransactionAreAppliedToSomeEntities(t *testing.T) { ids, err := cardinal.CreateMany(wCtx, 100, ScoreComponent{}) assert.NilError(t, err) // Entities at index 5, 10 and 50 will be updated with some values - modifyScoreMsg, err := testutils.GetMessage[*ModifyScoreMsg, *EmptyMsgResult](wCtx) + modifyScoreMsg, err := cardinal.GetMessage[*ModifyScoreMsg, *EmptyMsgResult](wCtx) assert.NilError(t, err) tf.AddTransaction( modifyScoreMsg.ID(), &ModifyScoreMsg{ @@ -256,7 +245,7 @@ func TestAddToPoolDuringTickDoesNotTimeout(t *testing.T) { // to verify that the addition of more transactions doesn't block. err := cardinal.RegisterSystems( world, - func(engine.Context) error { + func(cardinal.WorldContext) error { <-inSystemCh <-inSystemCh return nil @@ -272,10 +261,10 @@ func TestAddToPoolDuringTickDoesNotTimeout(t *testing.T) { go func() { tf.DoTick() }() - // Make sure we're actually in the System. + // Make sure we're actually in the system. inSystemCh <- struct{}{} - // Make sure we can call AddTransaction again in a reasonable amount of time + // Make sure we can call addTransaction again in a reasonable amount of time timeout := time.After(500 * time.Millisecond) doneWithAddTx := make(chan struct{}) @@ -288,7 +277,7 @@ func TestAddToPoolDuringTickDoesNotTimeout(t *testing.T) { case <-doneWithAddTx: // happy path case <-timeout: - t.Fatal("timeout while trying to AddTransaction") + t.Fatal("timeout while trying to addTransaction") } // release the system inSystemCh <- struct{}{} @@ -324,8 +313,8 @@ func TestTransactionsAreExecutedAtNextTick(t *testing.T) { // commands mid-tick. err := cardinal.RegisterSystems( world, - func(wCtx engine.Context) error { - modScoreMsg, err := testutils.GetMessage[*ModifyScoreMsg, *EmptyMsgResult](wCtx) + func(wCtx cardinal.WorldContext) error { + modScoreMsg, err := cardinal.GetMessage[*ModifyScoreMsg, *EmptyMsgResult](wCtx) if err != nil { return err } @@ -338,8 +327,8 @@ func TestTransactionsAreExecutedAtNextTick(t *testing.T) { err = cardinal.RegisterSystems( world, - func(wCtx engine.Context) error { - modScoreMsg, err := testutils.GetMessage[*ModifyScoreMsg, *EmptyMsgResult](wCtx) + func(wCtx cardinal.WorldContext) error { + modScoreMsg, err := cardinal.GetMessage[*ModifyScoreMsg, *EmptyMsgResult](wCtx) if err != nil { return err } @@ -442,13 +431,13 @@ func TestCanGetTransactionErrorsAndResults(t *testing.T) { err := cardinal.RegisterSystems( world, - func(wCtx engine.Context) error { + func(wCtx cardinal.WorldContext) error { // This new In function returns a triplet of information: // 1) The transaction input // 2) An EntityID that uniquely identifies this specific transaction // 3) The signature // This function would replace both "In" and "TxsAndSigsIn" - moveMsg, err := testutils.GetMessage[MoveMsg, MoveMsgResult](wCtx) + moveMsg, err := cardinal.GetMessage[MoveMsg, MoveMsgResult](wCtx) assert.NilError(t, err) txData := moveMsg.In(wCtx) assert.Equal(t, 1, len(txData), "expected 1 move transaction") @@ -505,9 +494,9 @@ func TestSystemCanFindErrorsFromEarlierSystem(t *testing.T) { systemCalls := 0 err := cardinal.RegisterSystems( world, - func(wCtx engine.Context) error { + func(wCtx cardinal.WorldContext) error { systemCalls++ - numTx, err := testutils.GetMessage[MsgIn, MsgOut](wCtx) + numTx, err := cardinal.GetMessage[MsgIn, MsgOut](wCtx) if err != nil { return err } @@ -524,9 +513,9 @@ func TestSystemCanFindErrorsFromEarlierSystem(t *testing.T) { err = cardinal.RegisterSystems( world, - func(wCtx engine.Context) error { + func(wCtx cardinal.WorldContext) error { systemCalls++ - numTx, err := testutils.GetMessage[MsgIn, MsgOut](wCtx) + numTx, err := cardinal.GetMessage[MsgIn, MsgOut](wCtx) if err != nil { return err } @@ -567,9 +556,9 @@ func TestSystemCanClobberTransactionResult(t *testing.T) { secondResult := MsgOut{5678} err := cardinal.RegisterSystems( world, - func(wCtx engine.Context) error { + func(wCtx cardinal.WorldContext) error { systemCalls++ - numTx, err := testutils.GetMessage[MsgIn, MsgOut](wCtx) + numTx, err := cardinal.GetMessage[MsgIn, MsgOut](wCtx) assert.NilError(t, err) txs := numTx.In(wCtx) assert.Equal(t, 1, len(txs)) @@ -584,9 +573,9 @@ func TestSystemCanClobberTransactionResult(t *testing.T) { err = cardinal.RegisterSystems( world, - func(wCtx engine.Context) error { + func(wCtx cardinal.WorldContext) error { systemCalls++ - numTx, err := testutils.GetMessage[MsgIn, MsgOut](wCtx) + numTx, err := cardinal.GetMessage[MsgIn, MsgOut](wCtx) if err != nil { return err } @@ -627,9 +616,9 @@ func TestTransactionExample(t *testing.T) { assert.NilError(t, cardinal.RegisterComponent[Health](world)) msgName := "add_health" assert.NilError(t, cardinal.RegisterMessage[AddHealthToEntityTx, AddHealthToEntityResult](world, msgName)) - err := cardinal.RegisterSystems(world, func(wCtx engine.Context) error { + err := cardinal.RegisterSystems(world, func(wCtx cardinal.WorldContext) error { // test "In" method - addHealthToEntity, err := testutils.GetMessage[AddHealthToEntityTx, AddHealthToEntityResult](wCtx) + addHealthToEntity, err := cardinal.GetMessage[AddHealthToEntityTx, AddHealthToEntityResult](wCtx) if err != nil { return err } @@ -686,7 +675,7 @@ func TestTransactionExample(t *testing.T) { } } // Make sure transaction errors are recorded in the receipt - receipts, err := testWorldCtx.GetTransactionReceiptsForTick(testWorldCtx.CurrentTick() - 1) + receipts, err := cardinal.GetTransactionReceiptsForTick(testWorldCtx, testWorldCtx.CurrentTick()-1) assert.NilError(t, err) assert.Equal(t, 1, len(receipts)) assert.Equal(t, 1, len(receipts[0].Errs)) @@ -744,7 +733,7 @@ func TestCanQueryInsideSystem(t *testing.T) { assert.NilError(t, cardinal.RegisterComponent[Foo](world)) gotNumOfEntities := 0 - err := cardinal.RegisterSystems(world, func(wCtx engine.Context) error { + err := cardinal.RegisterSystems(world, func(wCtx cardinal.WorldContext) error { err := cardinal.NewSearch().Entity(filter.Exact(filter.Component[Foo]())).Each(wCtx, func(types.EntityID) bool { gotNumOfEntities++ return true @@ -768,7 +757,7 @@ func TestCanGetTimestampFromWorldContext(t *testing.T) { var ts uint64 tf := testutils.NewTestFixture(t, nil) world := tf.World - err := cardinal.RegisterSystems(world, func(context engine.Context) error { + err := cardinal.RegisterSystems(world, func(context cardinal.WorldContext) error { ts = context.Timestamp() return nil }) @@ -789,7 +778,7 @@ func TestShutdownViaSignal(t *testing.T) { httpBaseURL := "http://" + addr assert.NilError(t, cardinal.RegisterComponent[Foo](world)) wantNumOfEntities := 10 - err := cardinal.RegisterInitSystems(world, func(wCtx engine.Context) error { + err := cardinal.RegisterInitSystems(world, func(wCtx cardinal.WorldContext) error { _, err := cardinal.CreateMany(wCtx, wantNumOfEntities/2, Foo{}) if err != nil { return err diff --git a/cardinal/search/composedsearch.go b/cardinal/composedsearch.go similarity index 66% rename from cardinal/search/composedsearch.go rename to cardinal/composedsearch.go index 50e35f09a..e73575ab6 100644 --- a/cardinal/search/composedsearch.go +++ b/cardinal/composedsearch.go @@ -1,11 +1,10 @@ -package search +package cardinal import ( "github.com/rotisserie/eris" "pkg.world.dev/world-engine/cardinal/search/filter" "pkg.world.dev/world-engine/cardinal/types" - "pkg.world.dev/world-engine/cardinal/types/engine" ) type OrSearch struct { @@ -20,19 +19,19 @@ type NotSearch struct { search Searchable } -func (orSearch *OrSearch) evaluateSearch(eCtx engine.Context) []types.ArchetypeID { +func (orSearch *OrSearch) evaluateSearch(wCtx WorldContext) []types.ArchetypeID { acc := make([]types.ArchetypeID, 0) for _, search := range orSearch.searches { - acc = append(acc, search.evaluateSearch(eCtx)...) + acc = append(acc, search.evaluateSearch(wCtx)...) } return acc } -func (orSearch *OrSearch) Each(eCtx engine.Context, callback CallbackFn) error { +func (orSearch *OrSearch) Each(wCtx WorldContext, callback CallbackFn) error { // deduplicate idCount := make(map[types.EntityID]int) for _, search := range orSearch.searches { - subids, err := search.Collect(eCtx) + subids, err := search.Collect(wCtx) if err != nil { return err } @@ -57,12 +56,12 @@ func (orSearch *OrSearch) Each(eCtx engine.Context, callback CallbackFn) error { return nil } -func (orSearch *OrSearch) Collect(eCtx engine.Context) ([]types.EntityID, error) { +func (orSearch *OrSearch) Collect(wCtx WorldContext) ([]types.EntityID, error) { // deduplicate idExists := make(map[types.EntityID]bool) res := make([]types.EntityID, 0) for _, search := range orSearch.searches { - ids, err := search.Collect(eCtx) + ids, err := search.Collect(wCtx) if err != nil { return nil, err } @@ -80,8 +79,8 @@ func (orSearch *OrSearch) Collect(eCtx engine.Context) ([]types.EntityID, error) return res, nil } -func (orSearch *OrSearch) First(eCtx engine.Context) (types.EntityID, error) { - ids, err := orSearch.Collect(eCtx) +func (orSearch *OrSearch) First(wCtx WorldContext) (types.EntityID, error) { + ids, err := orSearch.Collect(wCtx) if err != nil { return 0, err } @@ -91,27 +90,27 @@ func (orSearch *OrSearch) First(eCtx engine.Context) (types.EntityID, error) { return ids[0], nil } -func (orSearch *OrSearch) MustFirst(eCtx engine.Context) types.EntityID { - id, err := orSearch.First(eCtx) +func (orSearch *OrSearch) MustFirst(wCtx WorldContext) types.EntityID { + id, err := orSearch.First(wCtx) if err != nil { panic("no search results") } return id } -func (orSearch *OrSearch) Count(eCtx engine.Context) (int, error) { - ids, err := orSearch.Collect(eCtx) +func (orSearch *OrSearch) Count(wCtx WorldContext) (int, error) { + ids, err := orSearch.Collect(wCtx) if err != nil { return 0, err } return len(ids), nil } -func (andSearch *AndSearch) Each(eCtx engine.Context, callback CallbackFn) error { +func (andSearch *AndSearch) Each(wCtx WorldContext, callback CallbackFn) error { // count idCount := make(map[types.EntityID]int) for _, search := range andSearch.searches { - subIDs, err := search.Collect(eCtx) + subIDs, err := search.Collect(wCtx) if err != nil { return err } @@ -140,10 +139,10 @@ func (andSearch *AndSearch) Each(eCtx engine.Context, callback CallbackFn) error return nil } -func (andSearch *AndSearch) Collect(eCtx engine.Context) ([]types.EntityID, error) { +func (andSearch *AndSearch) Collect(wCtx WorldContext) ([]types.EntityID, error) { // filter results := make([]types.EntityID, 0) - err := andSearch.Each(eCtx, func(id types.EntityID) bool { + err := andSearch.Each(wCtx, func(id types.EntityID) bool { results = append(results, id) return true }) @@ -157,8 +156,8 @@ func (andSearch *AndSearch) Collect(eCtx engine.Context) ([]types.EntityID, erro return results, nil } -func (andSearch *AndSearch) First(eCtx engine.Context) (types.EntityID, error) { - ids, err := andSearch.Collect(eCtx) +func (andSearch *AndSearch) First(wCtx WorldContext) (types.EntityID, error) { + ids, err := andSearch.Collect(wCtx) if err != nil { return 0, err } @@ -168,26 +167,26 @@ func (andSearch *AndSearch) First(eCtx engine.Context) (types.EntityID, error) { return ids[0], nil } -func (andSearch *AndSearch) MustFirst(eCtx engine.Context) types.EntityID { - id, err := andSearch.First(eCtx) +func (andSearch *AndSearch) MustFirst(wCtx WorldContext) types.EntityID { + id, err := andSearch.First(wCtx) if err != nil { panic("No search results") } return id } -func (andSearch *AndSearch) Count(eCtx engine.Context) (int, error) { - ids, err := andSearch.Collect(eCtx) +func (andSearch *AndSearch) Count(wCtx WorldContext) (int, error) { + ids, err := andSearch.Collect(wCtx) if err != nil { return 0, err } return len(ids), nil } -func (andSearch *AndSearch) evaluateSearch(eCtx engine.Context) []types.ArchetypeID { +func (andSearch *AndSearch) evaluateSearch(wCtx WorldContext) []types.ArchetypeID { searchCounts := make(map[types.ArchetypeID]int) for _, search := range andSearch.searches { - ids := search.evaluateSearch(eCtx) + ids := search.evaluateSearch(wCtx) for _, id := range ids { searchCounts[id]++ } @@ -201,9 +200,9 @@ func (andSearch *AndSearch) evaluateSearch(eCtx engine.Context) []types.Archetyp return acc } -func (notSearch *NotSearch) Each(eCtx engine.Context, callback CallbackFn) error { +func (notSearch *NotSearch) Each(wCtx WorldContext, callback CallbackFn) error { // sort - ids, err := notSearch.Collect(eCtx) + ids, err := notSearch.Collect(wCtx) if err != nil { return err } @@ -218,16 +217,16 @@ func (notSearch *NotSearch) Each(eCtx engine.Context, callback CallbackFn) error return nil } -func (notSearch *NotSearch) Collect(eCtx engine.Context) ([]types.EntityID, error) { +func (notSearch *NotSearch) Collect(wCtx WorldContext) ([]types.EntityID, error) { // Get all ids allsearch := NewSearch().Entity(filter.All()) - allids, err := allsearch.Collect(eCtx) + allids, err := allsearch.Collect(wCtx) if err != nil { return nil, err } // Get ids to exclude excludedIDsMap := make(map[types.EntityID]bool) - excludedids, err := notSearch.search.Collect(eCtx) + excludedids, err := notSearch.search.Collect(wCtx) if err != nil { return nil, err } @@ -250,8 +249,8 @@ func (notSearch *NotSearch) Collect(eCtx engine.Context) ([]types.EntityID, erro return result, nil } -func (notSearch *NotSearch) First(eCtx engine.Context) (types.EntityID, error) { - ids, err := notSearch.Collect(eCtx) +func (notSearch *NotSearch) First(wCtx WorldContext) (types.EntityID, error) { + ids, err := notSearch.Collect(wCtx) if err != nil { return 0, err } @@ -261,30 +260,30 @@ func (notSearch *NotSearch) First(eCtx engine.Context) (types.EntityID, error) { return ids[0], nil } -func (notSearch *NotSearch) MustFirst(eCtx engine.Context) types.EntityID { - id, err := notSearch.First(eCtx) +func (notSearch *NotSearch) MustFirst(wCtx WorldContext) types.EntityID { + id, err := notSearch.First(wCtx) if err != nil { panic("No search results") } return id } -func (notSearch *NotSearch) Count(eCtx engine.Context) (int, error) { - ids, err := notSearch.Collect(eCtx) +func (notSearch *NotSearch) Count(wCtx WorldContext) (int, error) { + ids, err := notSearch.Collect(wCtx) if err != nil { return 0, err } return len(ids), nil } -func (notSearch *NotSearch) evaluateSearch(eCtx engine.Context) []types.ArchetypeID { +func (notSearch *NotSearch) evaluateSearch(wCtx WorldContext) []types.ArchetypeID { searchBuilder := NewSearch() - allResults := searchBuilder.Entity(filter.All()).evaluateSearch(eCtx) + allResults := searchBuilder.Entity(filter.All()).evaluateSearch(wCtx) allResultsMap := make(map[types.ArchetypeID]bool) for _, result := range allResults { allResultsMap[result] = true } - subResults := notSearch.search.evaluateSearch(eCtx) + subResults := notSearch.search.evaluateSearch(wCtx) finalResult := make([]types.ArchetypeID, 0) for _, subResult := range subResults { _, ok := allResultsMap[subResult] diff --git a/cardinal/ecs_test.go b/cardinal/ecs_test.go index f968e5aff..d76f25a1f 100644 --- a/cardinal/ecs_test.go +++ b/cardinal/ecs_test.go @@ -7,11 +7,9 @@ import ( "pkg.world.dev/world-engine/assert" "pkg.world.dev/world-engine/cardinal" "pkg.world.dev/world-engine/cardinal/iterators" - "pkg.world.dev/world-engine/cardinal/search" "pkg.world.dev/world-engine/cardinal/search/filter" "pkg.world.dev/world-engine/cardinal/testutils" "pkg.world.dev/world-engine/cardinal/types" - "pkg.world.dev/world-engine/cardinal/types/engine" ) type EnergyComponent struct { @@ -75,7 +73,7 @@ func (WeaponEnergy) Name() string { return "weaponsEnergy" } -func UpdateEnergySystem(wCtx engine.Context) error { +func UpdateEnergySystem(wCtx cardinal.WorldContext) error { var errs []error q := cardinal.NewSearch().Entity(filter.Contains(filter.Component[EnergyComponent]())) err := q.Each(wCtx, @@ -134,8 +132,8 @@ func TestECS(t *testing.T) { ) assert.NilError(t, err) - q := search.Or(cardinal.NewSearch().Entity( - filter.Contains(filter.Component[EnergyComponent]())), search.NewSearch().Entity( + q := cardinal.Or(cardinal.NewSearch().Entity( + filter.Contains(filter.Component[EnergyComponent]())), cardinal.NewSearch().Entity( filter.Contains(filter.Component[OwnableComponent]()))) amt, err := q.Count(wCtx) assert.NilError(t, err) @@ -586,9 +584,9 @@ func TestQueriesAndFiltersWorks(t *testing.T) { assert.NilError(t, err) assert.Equal(t, num, 1) - searchable := search.Or(cardinal.NewSearch().Entity( + searchable := cardinal.Or(cardinal.NewSearch().Entity( filter.Contains(filter.Component[A]())), - search.NewSearch().Entity(filter.Contains(filter.Component[D]()))) + cardinal.NewSearch().Entity(filter.Contains(filter.Component[D]()))) allCount, err := searchable.Count(wCtx) assert.NilError(t, err) assert.Equal(t, allCount, 3) diff --git a/cardinal/engine_test.go b/cardinal/engine_test.go index aeb50d546..c36815c62 100644 --- a/cardinal/engine_test.go +++ b/cardinal/engine_test.go @@ -13,7 +13,6 @@ import ( "pkg.world.dev/world-engine/cardinal/router/iterator" "pkg.world.dev/world-engine/cardinal/router/mocks" "pkg.world.dev/world-engine/cardinal/testutils" - "pkg.world.dev/world-engine/cardinal/types/engine" "pkg.world.dev/world-engine/cardinal/types/txpool" "pkg.world.dev/world-engine/sign" ) @@ -140,7 +139,7 @@ func TestCannotWaitForNextTickAfterEngineIsShutDown(t *testing.T) { var returnErr error err := cardinal.RegisterSystems( world, - func(wCtx engine.Context) error { + func(wCtx cardinal.WorldContext) error { return cardinal.EachMessage[FooIn, FooOut]( wCtx, func(cardinal.TxData[FooIn]) (FooOut, error) { return returnVal, returnErr @@ -202,9 +201,9 @@ func TestEVMTxConsume(t *testing.T) { var returnVal FooOut var returnErr error err = cardinal.RegisterSystems(world, - func(eCtx cardinal.WorldContext) error { + func(wCtx cardinal.WorldContext) error { return cardinal.EachMessage[FooIn, FooOut]( - eCtx, func(cardinal.TxData[FooIn]) (FooOut, error) { + wCtx, func(cardinal.TxData[FooIn]) (FooOut, error) { return returnVal, returnErr }, ) @@ -251,15 +250,15 @@ func TestEVMTxConsume(t *testing.T) { func TestAddSystems(t *testing.T) { count := 0 - sys1 := func(engine.Context) error { + sys1 := func(cardinal.WorldContext) error { count++ return nil } - sys2 := func(engine.Context) error { + sys2 := func(cardinal.WorldContext) error { count++ return nil } - sys3 := func(engine.Context) error { + sys3 := func(cardinal.WorldContext) error { count++ return nil } @@ -283,13 +282,13 @@ func TestSystemExecutionOrder(t *testing.T) { order := make([]int, 0, 3) err := cardinal.RegisterSystems( world, - func(engine.Context) error { + func(cardinal.WorldContext) error { order = append(order, 1) return nil - }, func(engine.Context) error { + }, func(cardinal.WorldContext) error { order = append(order, 2) return nil - }, func(engine.Context) error { + }, func(cardinal.WorldContext) error { order = append(order, 3) return nil }, diff --git a/cardinal/errors_test.go b/cardinal/errors_test.go index 8a82d9b24..38893cc37 100644 --- a/cardinal/errors_test.go +++ b/cardinal/errors_test.go @@ -11,27 +11,26 @@ import ( "pkg.world.dev/world-engine/cardinal/component" "pkg.world.dev/world-engine/cardinal/search/filter" "pkg.world.dev/world-engine/cardinal/testutils" - "pkg.world.dev/world-engine/cardinal/types/engine" ) -// TestSystemsReturnNonFatalErrors ensures System will surface non-fatal read and write errors to the user. +// TestSystemsReturnNonFatalErrors ensures system will surface non-fatal read and write errors to the user. func TestSystemsReturnNonFatalErrors(t *testing.T) { const nonExistentEntityID = 999 testCases := []struct { name string - testFn func(engine.Context) error + testFn func(cardinal.WorldContext) error wantErr error }{ { name: "AddComponentTo_BadEntity", - testFn: func(wCtx engine.Context) error { + testFn: func(wCtx cardinal.WorldContext) error { return cardinal.AddComponentTo[Foo](wCtx, nonExistentEntityID) }, wantErr: cardinal.ErrEntityDoesNotExist, }, { name: "AddComponentTo_ComponentAlreadyOnEntity", - testFn: func(wCtx engine.Context) error { + testFn: func(wCtx cardinal.WorldContext) error { id, err := cardinal.Create(wCtx, Foo{}) assert.Check(t, err == nil) return cardinal.AddComponentTo[Foo](wCtx, id) @@ -40,14 +39,14 @@ func TestSystemsReturnNonFatalErrors(t *testing.T) { }, { name: "RemoveComponentFrom_BadEntity", - testFn: func(wCtx engine.Context) error { + testFn: func(wCtx cardinal.WorldContext) error { return cardinal.RemoveComponentFrom[Foo](wCtx, nonExistentEntityID) }, wantErr: cardinal.ErrEntityDoesNotExist, }, { name: "RemoveComponentFrom_ComponentNotOnEntity", - testFn: func(wCtx engine.Context) error { + testFn: func(wCtx cardinal.WorldContext) error { id, err := cardinal.Create(wCtx, Foo{}) assert.Check(t, err == nil) return cardinal.RemoveComponentFrom[Bar](wCtx, id) @@ -56,7 +55,7 @@ func TestSystemsReturnNonFatalErrors(t *testing.T) { }, { name: "RemoveComponentFrom_EntityMustHaveAtLeastOneComponent", - testFn: func(wCtx engine.Context) error { + testFn: func(wCtx cardinal.WorldContext) error { id, err := cardinal.Create(wCtx, Foo{}) assert.Check(t, err == nil) return cardinal.RemoveComponentFrom[Foo](wCtx, id) @@ -65,7 +64,7 @@ func TestSystemsReturnNonFatalErrors(t *testing.T) { }, { name: "cardinal.GetComponent_BadEntity", - testFn: func(wCtx engine.Context) error { + testFn: func(wCtx cardinal.WorldContext) error { _, err := cardinal.GetComponent[Foo](wCtx, nonExistentEntityID) return err }, @@ -73,7 +72,7 @@ func TestSystemsReturnNonFatalErrors(t *testing.T) { }, { name: "cardinal.GetComponent_ComponentNotOnEntity", - testFn: func(wCtx engine.Context) error { + testFn: func(wCtx cardinal.WorldContext) error { id, err := cardinal.Create(wCtx, Foo{}) assert.Check(t, err == nil) _, err = cardinal.GetComponent[Bar](wCtx, id) @@ -83,14 +82,14 @@ func TestSystemsReturnNonFatalErrors(t *testing.T) { }, { name: "SetComponent_BadEntity", - testFn: func(wCtx engine.Context) error { + testFn: func(wCtx cardinal.WorldContext) error { return cardinal.SetComponent[Foo](wCtx, nonExistentEntityID, &Foo{}) }, wantErr: cardinal.ErrEntityDoesNotExist, }, { name: "SetComponent_ComponentNotOnEntity", - testFn: func(wCtx engine.Context) error { + testFn: func(wCtx cardinal.WorldContext) error { id, err := cardinal.Create(wCtx, Foo{}) assert.Check(t, err == nil) return cardinal.SetComponent[Bar](wCtx, id, &Bar{}) @@ -99,7 +98,7 @@ func TestSystemsReturnNonFatalErrors(t *testing.T) { }, { name: "UpdateComponent_BadEntity", - testFn: func(wCtx engine.Context) error { + testFn: func(wCtx cardinal.WorldContext) error { return cardinal.UpdateComponent[Foo](wCtx, nonExistentEntityID, func(f *Foo) *Foo { return f }) @@ -108,7 +107,7 @@ func TestSystemsReturnNonFatalErrors(t *testing.T) { }, { name: "UpdateComponent_ComponentNotOnEntity", - testFn: func(wCtx engine.Context) error { + testFn: func(wCtx cardinal.WorldContext) error { id, err := cardinal.Create(wCtx, Foo{}) assert.Check(t, err == nil) return cardinal.UpdateComponent[Bar](wCtx, id, func(b *Bar) *Bar { @@ -119,7 +118,7 @@ func TestSystemsReturnNonFatalErrors(t *testing.T) { }, { name: "Remove_EntityDoesNotExist", - testFn: func(wCtx engine.Context) error { + testFn: func(wCtx cardinal.WorldContext) error { return cardinal.Remove(wCtx, nonExistentEntityID) }, wantErr: cardinal.ErrEntityDoesNotExist, @@ -132,9 +131,9 @@ func TestSystemsReturnNonFatalErrors(t *testing.T) { world, tick := tf.World, tf.DoTick assert.NilError(t, cardinal.RegisterComponent[Foo](world)) assert.NilError(t, cardinal.RegisterComponent[Bar](world)) - err := cardinal.RegisterInitSystems(world, func(wCtx engine.Context) error { + err := cardinal.RegisterInitSystems(world, func(wCtx cardinal.WorldContext) error { defer func() { - // In Systems, Cardinal is designed to panic when a fatal error is encountered. + // In systems, Cardinal is designed to panic when a fatal error is encountered. // This test is not supposed to panic, but if it does panic it happens in a non-main thread which // makes it hard to track down where the panic actually came from. // Recover here and complain about any non-nil panics to allow the remaining tests in this @@ -164,11 +163,11 @@ func TestSystemsPanicOnComponentHasNotBeenRegistered(t *testing.T) { testCases := []struct { name string // Every test is expected to panic, so no return error is needed - panicFn func(engine.Context) + panicFn func(cardinal.WorldContext) }{ { name: "cardinal.AddComponentTo", - panicFn: func(wCtx engine.Context) { + panicFn: func(wCtx cardinal.WorldContext) { id, err := cardinal.Create(wCtx, Foo{}) assert.Check(t, err == nil) _ = cardinal.AddComponentTo[UnregisteredComp](wCtx, id) @@ -176,7 +175,7 @@ func TestSystemsPanicOnComponentHasNotBeenRegistered(t *testing.T) { }, { name: "cardinal.RemoveComponentFrom", - panicFn: func(wCtx engine.Context) { + panicFn: func(wCtx cardinal.WorldContext) { id, err := cardinal.Create(wCtx, Foo{}, Bar{}) assert.Check(t, err == nil) _ = cardinal.RemoveComponentFrom[UnregisteredComp](wCtx, id) @@ -184,7 +183,7 @@ func TestSystemsPanicOnComponentHasNotBeenRegistered(t *testing.T) { }, { name: "cardinal.GetComponent", - panicFn: func(wCtx engine.Context) { + panicFn: func(wCtx cardinal.WorldContext) { id, err := cardinal.Create(wCtx, Foo{}) assert.Check(t, err == nil) _, _ = cardinal.GetComponent[UnregisteredComp](wCtx, id) @@ -192,7 +191,7 @@ func TestSystemsPanicOnComponentHasNotBeenRegistered(t *testing.T) { }, { name: "cardinal.SetComponent", - panicFn: func(wCtx engine.Context) { + panicFn: func(wCtx cardinal.WorldContext) { id, err := cardinal.Create(wCtx, Foo{}) assert.Check(t, err == nil) _ = cardinal.SetComponent[UnregisteredComp](wCtx, id, &UnregisteredComp{}) @@ -200,7 +199,7 @@ func TestSystemsPanicOnComponentHasNotBeenRegistered(t *testing.T) { }, { name: "cardinal.UpdateComponent", - panicFn: func(wCtx engine.Context) { + panicFn: func(wCtx cardinal.WorldContext) { id, err := cardinal.Create(wCtx, Foo{}) assert.Check(t, err == nil) _ = cardinal.UpdateComponent[UnregisteredComp](wCtx, id, @@ -211,13 +210,13 @@ func TestSystemsPanicOnComponentHasNotBeenRegistered(t *testing.T) { }, { name: "cardinal.Create", - panicFn: func(wCtx engine.Context) { + panicFn: func(wCtx cardinal.WorldContext) { _, _ = cardinal.Create(wCtx, Foo{}, UnregisteredComp{}) }, }, { name: "cardinal.CreateMany", - panicFn: func(wCtx engine.Context) { + panicFn: func(wCtx cardinal.WorldContext) { _, _ = cardinal.CreateMany(wCtx, 10, Foo{}, UnregisteredComp{}) }, }, @@ -228,7 +227,7 @@ func TestSystemsPanicOnComponentHasNotBeenRegistered(t *testing.T) { tf := testutils.NewTestFixture(t, nil) world, tick := tf.World, tf.DoTick assert.NilError(t, cardinal.RegisterComponent[Foo](world)) - err := cardinal.RegisterInitSystems(world, func(wCtx engine.Context) error { + err := cardinal.RegisterInitSystems(world, func(wCtx cardinal.WorldContext) error { defer func() { err := recover() // assert.Check is required here because this is happening in a non-main thread. @@ -260,11 +259,11 @@ type QueryResponse struct{} func TestQueriesDoNotPanicOnComponentHasNotBeenRegistered(t *testing.T) { testCases := []struct { name string - testFn func(engine.Context) error + testFn func(cardinal.WorldContext) error }{ { name: "cardinal.GetComponent", - testFn: func(wCtx engine.Context) error { + testFn: func(wCtx cardinal.WorldContext) error { // Get a valid entity to ensure the error we find is related to the component and NOT // due to an invalid entity. id, err := cardinal.NewSearch().Entity(filter.Exact(filter.Component[Foo]())).First(wCtx) @@ -288,7 +287,7 @@ func TestQueriesDoNotPanicOnComponentHasNotBeenRegistered(t *testing.T) { tf := testutils.NewTestFixture(t, nil) world, tick := tf.World, tf.DoTick assert.NilError(t, cardinal.RegisterComponent[Foo](world)) - err := cardinal.RegisterInitSystems(world, func(wCtx engine.Context) error { + err := cardinal.RegisterInitSystems(world, func(wCtx cardinal.WorldContext) error { // Make an entity so the test functions are operating on a valid entity. _, err := cardinal.Create(wCtx, Foo{}) assert.Check(t, err == nil) @@ -298,7 +297,7 @@ func TestQueriesDoNotPanicOnComponentHasNotBeenRegistered(t *testing.T) { err = cardinal.RegisterQuery[QueryRequest, QueryResponse]( world, queryName, - func(wCtx engine.Context, _ *QueryRequest) (*QueryResponse, error) { + func(wCtx cardinal.WorldContext, _ *QueryRequest) (*QueryResponse, error) { return nil, tc.testFn(wCtx) }) assert.Check(t, err == nil) @@ -310,7 +309,7 @@ func TestQueriesDoNotPanicOnComponentHasNotBeenRegistered(t *testing.T) { assert.Check(t, err == nil) readOnlyWorldCtx := cardinal.NewReadOnlyWorldContext(world) - _, err = query.HandleQuery(readOnlyWorldCtx, QueryRequest{}) + _, err = cardinal.InternalHandleQuery(readOnlyWorldCtx, query, QueryRequest{}) // Each test case is meant to generate a "ErrComponentNotRegistered" error assert.Check(t, errors.Is(err, component.ErrComponentNotRegistered), "expected a component not registered error, got %v", err) @@ -323,7 +322,7 @@ func TestGetComponentInQueryDoesNotPanicOnRedisError(t *testing.T) { world, tick := tf.World, tf.DoTick assert.NilError(t, cardinal.RegisterComponent[Foo](world)) - err := cardinal.RegisterSystems(world, func(wCtx engine.Context) error { + err := cardinal.RegisterSystems(world, func(wCtx cardinal.WorldContext) error { _, err := cardinal.Create(wCtx, Foo{}) assert.Check(t, err == nil) return nil @@ -334,7 +333,7 @@ func TestGetComponentInQueryDoesNotPanicOnRedisError(t *testing.T) { assert.NilError(t, cardinal.RegisterQuery[QueryRequest, QueryResponse]( world, queryName, - func(wCtx engine.Context, _ *QueryRequest) (*QueryResponse, error) { + func(wCtx cardinal.WorldContext, _ *QueryRequest) (*QueryResponse, error) { id, err := cardinal.NewSearch().Entity(filter.Exact(filter.Component[Foo]())).First(wCtx) assert.Check(t, err == nil) _, err = cardinal.GetComponent[Foo](wCtx, id) @@ -351,11 +350,11 @@ func TestGetComponentInQueryDoesNotPanicOnRedisError(t *testing.T) { tf.Redis.Close() readOnlyWorldCtx := cardinal.NewReadOnlyWorldContext(world) - // This will fail with a redis connection error, and since we're in a Query, we should NOT panic + // This will fail with a redis connection error, and since we're in a query, we should NOT panic defer func() { assert.Check(t, recover() == nil, "expected no panic in a query") }() - _, err = query.HandleQuery(readOnlyWorldCtx, QueryRequest{}) + _, err = cardinal.InternalHandleQuery(readOnlyWorldCtx, query, QueryRequest{}) assert.ErrorContains(t, err, "connection refused", "expected a connection error") } diff --git a/cardinal/example_messagetype_test.go b/cardinal/example_messagetype_test.go index ab313a262..21d94265a 100644 --- a/cardinal/example_messagetype_test.go +++ b/cardinal/example_messagetype_test.go @@ -6,7 +6,6 @@ import ( "fmt" "pkg.world.dev/world-engine/cardinal" - "pkg.world.dev/world-engine/cardinal/types/engine" ) type MovePlayerMsg struct { @@ -30,7 +29,7 @@ func ExampleMessageType() { panic(err) } - err = cardinal.RegisterSystems(world, func(wCtx engine.Context) error { + err = cardinal.RegisterSystems(world, func(wCtx cardinal.WorldContext) error { return cardinal.EachMessage[MovePlayerMsg, MovePlayerResult](wCtx, func(txData cardinal.TxData[MovePlayerMsg]) (MovePlayerResult, error) { // handle the transaction diff --git a/cardinal/search/filtercomponent.go b/cardinal/filtercomponent.go similarity index 68% rename from cardinal/search/filtercomponent.go rename to cardinal/filtercomponent.go index e3c646b32..283ee261f 100644 --- a/cardinal/search/filtercomponent.go +++ b/cardinal/filtercomponent.go @@ -1,28 +1,27 @@ -package search +package cardinal import ( "github.com/rotisserie/eris" "pkg.world.dev/world-engine/cardinal/types" - "pkg.world.dev/world-engine/cardinal/types/engine" ) // This package involves primitives for search. // It involves creating and combining primitives that represent // filtering properties on components. -type filterFn func(wCtx engine.Context, id types.EntityID) (bool, error) +type FilterFn func(wCtx WorldContext, id types.EntityID) (bool, error) //revive:disable-next-line:unexported-return -func ComponentFilter[T types.Component](f func(comp T) bool) filterFn { - return func(wCtx engine.Context, id types.EntityID) (bool, error) { +func ComponentFilter[T types.Component](f func(comp T) bool) FilterFn { + return func(wCtx WorldContext, id types.EntityID) (bool, error) { var t T - c, err := wCtx.GetComponentByName(t.Name()) + c, err := wCtx.getComponentByName(t.Name()) if err != nil { return false, err } // Get current component value - compValue, err := wCtx.StoreReader().GetComponentForEntity(c, id) + compValue, err := wCtx.storeReader().GetComponentForEntity(c, id) if err != nil { return false, err } @@ -43,8 +42,8 @@ func ComponentFilter[T types.Component](f func(comp T) bool) filterFn { } //revive:disable-next-line:unexported-return -func AndFilter(fns ...filterFn) filterFn { - return func(wCtx engine.Context, id types.EntityID) (bool, error) { +func AndFilter(fns ...FilterFn) FilterFn { + return func(wCtx WorldContext, id types.EntityID) (bool, error) { var result = true var errCount = 0 for _, fn := range fns { @@ -63,8 +62,8 @@ func AndFilter(fns ...filterFn) filterFn { } //revive:disable-next-line:unexported-return -func OrFilter(fns ...filterFn) filterFn { - return func(wCtx engine.Context, id types.EntityID) (bool, error) { +func OrFilter(fns ...FilterFn) FilterFn { + return func(wCtx WorldContext, id types.EntityID) (bool, error) { var result = false var errCount = 0 for _, fn := range fns { diff --git a/cardinal/go.mod b/cardinal/go.mod index 38ad1a4c5..f1e353612 100644 --- a/cardinal/go.mod +++ b/cardinal/go.mod @@ -23,7 +23,7 @@ require ( github.com/rotisserie/eris v0.5.4 github.com/rs/zerolog v1.31.0 github.com/stretchr/testify v1.9.0 - github.com/swaggo/swag v1.16.2 + github.com/swaggo/swag v1.16.3 github.com/wI2L/jsondiff v0.5.0 google.golang.org/grpc v1.62.0 google.golang.org/protobuf v1.32.0 @@ -47,6 +47,7 @@ require ( github.com/andybalholm/brotli v1.1.0 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect @@ -74,6 +75,7 @@ require ( github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 // indirect github.com/secure-systems-lab/go-securesystemslib v0.7.0 // indirect github.com/swaggo/files/v2 v2.0.0 // indirect @@ -82,9 +84,11 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/tinylib/msgp v1.1.8 // indirect + github.com/urfave/cli/v2 v2.25.7 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.52.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/yuin/gopher-lua v1.1.0 // indirect go.uber.org/atomic v1.11.0 // indirect go4.org/intern v0.0.0-20230525184215-6c62f75575cb // indirect @@ -98,6 +102,8 @@ require ( golang.org/x/tools v0.16.1 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a // indirect + sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/cardinal/go.sum b/cardinal/go.sum index 58968dde0..7aa920293 100644 --- a/cardinal/go.sum +++ b/cardinal/go.sum @@ -53,6 +53,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -186,6 +188,8 @@ github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 h1:KanIMPX0QdEdB4R3CiimCAbxFrhB3j7h0/OvpYGVQa8= github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg= github.com/secure-systems-lab/go-securesystemslib v0.7.0 h1:OwvJ5jQf9LnIAS83waAjPbcMsODrTQUpJ02eNLUoxBg= @@ -213,6 +217,8 @@ github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= github.com/swaggo/swag v1.16.2 h1:28Pp+8DkQoV+HLzLx8RGJZXNGKbFqnuvSbAAtoxiY04= github.com/swaggo/swag v1.16.2/go.mod h1:6YzXnDcpr0767iOejs318CwYkCQqyGer6BizOg03f+E= +github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= +github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -226,6 +232,8 @@ github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6 github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.50.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= @@ -235,6 +243,8 @@ github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVS github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/wI2L/jsondiff v0.5.0 h1:RRMTi/mH+R2aXcPe1VYyvGINJqQfC3R+KSEakuU1Ikw= github.com/wI2L/jsondiff v0.5.0/go.mod h1:qqG6hnK0Lsrz2BpIVCxWiK9ItsBCpIZQiv0izJjOZ9s= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -375,4 +385,5 @@ pkg.world.dev/world-engine/rift v1.1.0-beta.0.20240402214846-de1fc179818a h1:P9p pkg.world.dev/world-engine/rift v1.1.0-beta.0.20240402214846-de1fc179818a/go.mod h1:XAD40g4r3qOp3CTa66JBY+ACEUtjr01loyHiUZIheN8= pkg.world.dev/world-engine/sign v1.0.1-beta h1:ZwVeJYdf88t6qIHPurbdKJKVxUN0dfp0gSF7OCA9xt8= pkg.world.dev/world-engine/sign v1.0.1-beta/go.mod h1:U6XdRfjzoodAScJ/bH4qzxY7gbqbgGV2Od+k3tSTekE= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/cardinal/imported.go b/cardinal/imported.go deleted file mode 100644 index ffb3f26d2..000000000 --- a/cardinal/imported.go +++ /dev/null @@ -1,10 +0,0 @@ -package cardinal - -import ( - "pkg.world.dev/world-engine/cardinal/system" - "pkg.world.dev/world-engine/cardinal/types/engine" -) - -type WorldContext = engine.Context - -type System = system.System diff --git a/cardinal/log/log.go b/cardinal/log/log.go index bdb4b8cda..3fc924c7d 100644 --- a/cardinal/log/log.go +++ b/cardinal/log/log.go @@ -10,7 +10,7 @@ import ( type Loggable interface { GetRegisteredComponents() []types.ComponentMetadata - GetRegisteredSystemNames() []string + GetRegisteredSystems() []string } func loadComponentIntoArrayLogger( @@ -36,15 +36,11 @@ func loadComponentsToEvent(zeroLoggerEvent *zerolog.Event, target Loggable) *zer return zeroLoggerEvent.Array("components", arrayLogger) } -func loadSystemIntoArrayLogger(name string, arrayLogger *zerolog.Array) *zerolog.Array { - return arrayLogger.Str(name) -} - func loadSystemIntoEvent(zeroLoggerEvent *zerolog.Event, target Loggable) *zerolog.Event { - zeroLoggerEvent.Int("total_systems", len(target.GetRegisteredSystemNames())) + zeroLoggerEvent.Int("total_systems", len(target.GetRegisteredSystems())) arrayLogger := zerolog.Arr() - for _, name := range target.GetRegisteredSystemNames() { - arrayLogger = loadSystemIntoArrayLogger(name, arrayLogger) + for _, sysName := range target.GetRegisteredSystems() { + arrayLogger = arrayLogger.Str(sysName) } return zeroLoggerEvent.Array("systems", arrayLogger) } @@ -76,7 +72,7 @@ func System(logger *zerolog.Logger, target Loggable, level zerolog.Level) { zeroLoggerEvent.Send() } -// LogEntity logs entity info given an entityID. +// Entity logs entity info given an entityID. func Entity( logger *zerolog.Logger, level zerolog.Level, entityID types.EntityID, archID types.ArchetypeID, diff --git a/cardinal/log/log_test.go b/cardinal/log/log_test.go index d865deac4..937453074 100644 --- a/cardinal/log/log_test.go +++ b/cardinal/log/log_test.go @@ -15,7 +15,6 @@ import ( "pkg.world.dev/world-engine/cardinal/search/filter" "pkg.world.dev/world-engine/cardinal/testutils" "pkg.world.dev/world-engine/cardinal/types" - "pkg.world.dev/world-engine/cardinal/types/engine" ) type SendEnergyTx struct { @@ -33,7 +32,7 @@ func (EnergyComp) Name() string { return "EnergyComp" } -func testSystem(wCtx engine.Context) error { +func testSystem(wCtx cardinal.WorldContext) error { wCtx.Logger().Log().Msg("test") q := cardinal.NewSearch().Entity(filter.Contains(filter.Component[EnergyComp]())) err := q.Each(wCtx, @@ -54,7 +53,7 @@ func testSystem(wCtx engine.Context) error { return nil } -func testSystemWarningTrigger(wCtx engine.Context) error { +func testSystemWarningTrigger(wCtx cardinal.WorldContext) error { time.Sleep(time.Millisecond * 400) return testSystem(wCtx) } @@ -92,8 +91,8 @@ func TestWorldLogger(t *testing.T) { "total_systems":2, "systems": [ - "cardinal.CreatePersonaSystem", - "cardinal.AuthorizePersonaAddressSystem" + "cardinal.createPersonaSystem", + "cardinal.authorizePersonaAddressSystem" ] } ` diff --git a/cardinal/message.go b/cardinal/message.go index cc5468ce0..5da48b6df 100644 --- a/cardinal/message.go +++ b/cardinal/message.go @@ -12,7 +12,6 @@ import ( "pkg.world.dev/world-engine/cardinal/abi" "pkg.world.dev/world-engine/cardinal/codec" "pkg.world.dev/world-engine/cardinal/types" - "pkg.world.dev/world-engine/cardinal/types/engine" "pkg.world.dev/world-engine/sign" ) @@ -31,10 +30,10 @@ type TxData[In any] struct { Tx *sign.Transaction } -type MessageOption[In, Out any] func(mt *MessageType[In, Out]) //nolint:revive // this is fine for now +type MessageOption[In, Out any] func(mt *MessageType[In, Out]) // MessageType manages a user defined state transition message struct. -type MessageType[In, Out any] struct { //nolint:revive // this is fine for now. +type MessageType[In, Out any] struct { id types.MessageID isIDSet bool name string @@ -104,18 +103,18 @@ func (t *MessageType[In, Out]) SetID(id types.MessageID) error { return nil } -func (t *MessageType[In, Out]) AddError(wCtx engine.Context, hash types.TxHash, err error) { - wCtx.AddMessageError(hash, err) +func (t *MessageType[In, Out]) AddError(wCtx WorldContext, hash types.TxHash, err error) { + wCtx.addMessageError(hash, err) } -func (t *MessageType[In, Out]) SetResult(wCtx engine.Context, hash types.TxHash, result Out) { - wCtx.SetMessageResult(hash, result) +func (t *MessageType[In, Out]) SetResult(wCtx WorldContext, hash types.TxHash, result Out) { + wCtx.setMessageResult(hash, result) } -func (t *MessageType[In, Out]) GetReceipt(wCtx engine.Context, hash types.TxHash) ( +func (t *MessageType[In, Out]) GetReceipt(wCtx WorldContext, hash types.TxHash) ( v Out, errs []error, ok bool, ) { - iface, errs, ok := wCtx.GetTransactionReceipt(hash) + iface, errs, ok := wCtx.getTransactionReceipt(hash) if !ok { return v, nil, false } @@ -130,7 +129,7 @@ func (t *MessageType[In, Out]) GetReceipt(wCtx engine.Context, hash types.TxHash return value, errs, true } -func (t *MessageType[In, Out]) Each(wCtx engine.Context, fn func(TxData[In]) (Out, error)) { +func (t *MessageType[In, Out]) Each(wCtx WorldContext, fn func(TxData[In]) (Out, error)) { for _, txData := range t.In(wCtx) { if result, err := fn(txData); err != nil { err = eris.Wrap(err, "") @@ -148,8 +147,8 @@ func (t *MessageType[In, Out]) Each(wCtx engine.Context, fn func(TxData[In]) (Ou } // In extracts all the TxData in the tx pool that match this MessageType's ID. -func (t *MessageType[In, Out]) In(wCtx engine.Context) []TxData[In] { - tq := wCtx.GetTxPool() +func (t *MessageType[In, Out]) In(wCtx WorldContext) []TxData[In] { + tq := wCtx.getTxPool() var txs []TxData[In] for _, txData := range tq.ForID(t.ID()) { if val, ok := txData.Msg.(In); ok { diff --git a/cardinal/message_manager.go b/cardinal/message_manager.go index 86241ba14..55bc1a4de 100644 --- a/cardinal/message_manager.go +++ b/cardinal/message_manager.go @@ -9,22 +9,30 @@ import ( "pkg.world.dev/world-engine/cardinal/types" ) -type MessageManager struct { +type MessageManager interface { + RegisterMessage(msgType types.Message, msgReflectType reflect.Type) error + GetRegisteredMessages() []types.Message + GetMessageByID(id types.MessageID) types.Message + GetMessageByFullName(fullName string) (types.Message, bool) + GetMessageByType(mType reflect.Type) (types.Message, bool) +} + +type messageManager struct { // registeredMessages maps message FullNames to a types.Message. registeredMessages map[string]types.Message registeredMessagesByType map[reflect.Type]types.Message nextMessageID types.MessageID } -func NewMessageManager() *MessageManager { - return &MessageManager{ +func newMessageManager() MessageManager { + return &messageManager{ registeredMessages: map[string]types.Message{}, registeredMessagesByType: map[reflect.Type]types.Message{}, nextMessageID: 1, } } -func (m *MessageManager) RegisterMessage(msgType types.Message, msgReflectType reflect.Type) error { +func (m *messageManager) RegisterMessage(msgType types.Message, msgReflectType reflect.Type) error { fullName := msgType.FullName() // Checks if the message is already previously registered. if err := errors.Join(m.isMessageFullNameUnique(fullName), m.isMessageTypeUnique(msgReflectType)); err != nil { @@ -46,7 +54,7 @@ func (m *MessageManager) RegisterMessage(msgType types.Message, msgReflectType r } // GetRegisteredMessages returns the list of all registered messages -func (m *MessageManager) GetRegisteredMessages() []types.Message { +func (m *messageManager) GetRegisteredMessages() []types.Message { msgs := make([]types.Message, 0, len(m.registeredMessages)) for _, msg := range m.registeredMessages { msgs = append(msgs, msg) @@ -56,7 +64,7 @@ func (m *MessageManager) GetRegisteredMessages() []types.Message { // GetMessageByID iterates over the all registered messages and returns the types.Message associated with the // MessageID. -func (m *MessageManager) GetMessageByID(id types.MessageID) types.Message { +func (m *messageManager) GetMessageByID(id types.MessageID) types.Message { for _, msg := range m.registeredMessages { if id == msg.ID() { return msg @@ -66,18 +74,18 @@ func (m *MessageManager) GetMessageByID(id types.MessageID) types.Message { } // GetMessageByFullName returns the message with the given full name, if it exists. -func (m *MessageManager) GetMessageByFullName(fullName string) (types.Message, bool) { +func (m *messageManager) GetMessageByFullName(fullName string) (types.Message, bool) { msg, ok := m.registeredMessages[fullName] return msg, ok } -func (m *MessageManager) GetMessageByType(mType reflect.Type) (types.Message, bool) { +func (m *messageManager) GetMessageByType(mType reflect.Type) (types.Message, bool) { msg, ok := m.registeredMessagesByType[mType] return msg, ok } // isMessageFullNameUnique checks if the message name already exist in messages map. -func (m *MessageManager) isMessageFullNameUnique(fullName string) error { +func (m *messageManager) isMessageFullNameUnique(fullName string) error { _, ok := m.registeredMessages[fullName] if ok { return eris.Errorf("message %q is already registered", fullName) @@ -86,7 +94,7 @@ func (m *MessageManager) isMessageFullNameUnique(fullName string) error { } // isMessageTypeUnique checks if the message type name already exist in messages map. -func (m *MessageManager) isMessageTypeUnique(msgReflectType reflect.Type) error { +func (m *messageManager) isMessageTypeUnique(msgReflectType reflect.Type) error { _, ok := m.registeredMessagesByType[msgReflectType] if ok { return eris.Errorf("message type %q is already registered", msgReflectType) diff --git a/cardinal/persona/persona_test.go b/cardinal/persona/persona_test.go index 4ff4b327c..b012d28f6 100644 --- a/cardinal/persona/persona_test.go +++ b/cardinal/persona/persona_test.go @@ -10,7 +10,6 @@ import ( "pkg.world.dev/world-engine/cardinal/persona" "pkg.world.dev/world-engine/cardinal/persona/component" "pkg.world.dev/world-engine/cardinal/persona/msg" - personaQuery "pkg.world.dev/world-engine/cardinal/persona/query" "pkg.world.dev/world-engine/cardinal/search/filter" "pkg.world.dev/world-engine/cardinal/testutils" "pkg.world.dev/world-engine/cardinal/types" @@ -237,15 +236,16 @@ func TestQuerySigner(t *testing.T) { query, err := world.GetQueryByName("signer") assert.NilError(t, err) - res, err := query.HandleQuery(cardinal.NewReadOnlyWorldContext(world), &personaQuery.PersonaSignerQueryRequest{ - PersonaTag: personaTag, - }) + res, err := cardinal.InternalHandleQuery( + cardinal.NewReadOnlyWorldContext(world), query, &cardinal.PersonaSignerQueryRequest{ + PersonaTag: personaTag, + }) assert.NilError(t, err) - response, ok := res.(*personaQuery.PersonaSignerQueryResponse) + response, ok := res.(*cardinal.PersonaSignerQueryResponse) assert.True(t, ok) assert.Equal(t, response.SignerAddress, signerAddr) - assert.Equal(t, response.Status, personaQuery.PersonaStatusAssigned) + assert.Equal(t, response.Status, cardinal.PersonaStatusAssigned) } func TestQuerySignerAvailable(t *testing.T) { @@ -255,14 +255,15 @@ func TestQuerySignerAvailable(t *testing.T) { query, err := world.GetQueryByName("signer") assert.NilError(t, err) - res, err := query.HandleQuery(cardinal.NewReadOnlyWorldContext(world), &personaQuery.PersonaSignerQueryRequest{ - PersonaTag: "some-random-nonexistent-persona-tag", - }) + res, err := cardinal.InternalHandleQuery( + cardinal.NewReadOnlyWorldContext(world), query, &cardinal.PersonaSignerQueryRequest{ + PersonaTag: "some-random-nonexistent-persona-tag", + }) assert.NilError(t, err) - response, ok := res.(*personaQuery.PersonaSignerQueryResponse) + response, ok := res.(*cardinal.PersonaSignerQueryResponse) assert.True(t, ok) - assert.Equal(t, response.Status, personaQuery.PersonaStatusAvailable) + assert.Equal(t, response.Status, cardinal.PersonaStatusAvailable) } func TestQuerySignerUnknown(t *testing.T) { @@ -272,15 +273,16 @@ func TestQuerySignerUnknown(t *testing.T) { query, err := engine.GetQueryByName("signer") assert.NilError(t, err) - res, err := query.HandleQuery(cardinal.NewReadOnlyWorldContext(engine), &personaQuery.PersonaSignerQueryRequest{ - PersonaTag: "doesnt_matter", - Tick: engine.CurrentTick(), - }) + res, err := cardinal.InternalHandleQuery(cardinal.NewReadOnlyWorldContext(engine), query, + &cardinal.PersonaSignerQueryRequest{ + PersonaTag: "doesnt_matter", + Tick: engine.CurrentTick(), + }) assert.NilError(t, err) - response, ok := res.(*personaQuery.PersonaSignerQueryResponse) + response, ok := res.(*cardinal.PersonaSignerQueryResponse) assert.True(t, ok) - assert.Equal(t, response.Status, personaQuery.PersonaStatusUnknown) + assert.Equal(t, response.Status, cardinal.PersonaStatusUnknown) } func getSigners(t *testing.T, world *cardinal.World) []*component.SignerComponent { diff --git a/cardinal/persona/query/persona_signer.go b/cardinal/persona_signer.go similarity index 85% rename from cardinal/persona/query/persona_signer.go rename to cardinal/persona_signer.go index e333064ae..d904e1cb3 100644 --- a/cardinal/persona/query/persona_signer.go +++ b/cardinal/persona_signer.go @@ -1,10 +1,9 @@ -package query +package cardinal import ( "errors" "pkg.world.dev/world-engine/cardinal/persona" - "pkg.world.dev/world-engine/cardinal/types/engine" ) const ( @@ -28,10 +27,10 @@ type PersonaSignerQueryResponse struct { SignerAddress string `json:"signerAddress"` } -func PersonaSignerQuery(wCtx engine.Context, req *PersonaSignerQueryRequest) (*PersonaSignerQueryResponse, error) { +func PersonaSignerQuery(wCtx WorldContext, req *PersonaSignerQueryRequest) (*PersonaSignerQueryResponse, error) { var status string - addr, err := wCtx.GetSignerForPersonaTag(req.PersonaTag, req.Tick) + addr, err := wCtx.getSignerForPersonaTag(req.PersonaTag, req.Tick) if err != nil { //nolint:gocritic // cant switch case this. if errors.Is(err, persona.ErrPersonaTagHasNoSigner) { diff --git a/cardinal/plugin_persona.go b/cardinal/plugin_persona.go index a474d5011..3dee044fb 100644 --- a/cardinal/plugin_persona.go +++ b/cardinal/plugin_persona.go @@ -10,11 +10,8 @@ import ( "pkg.world.dev/world-engine/cardinal/persona" "pkg.world.dev/world-engine/cardinal/persona/component" "pkg.world.dev/world-engine/cardinal/persona/msg" - "pkg.world.dev/world-engine/cardinal/persona/query" - "pkg.world.dev/world-engine/cardinal/search" "pkg.world.dev/world-engine/cardinal/search/filter" "pkg.world.dev/world-engine/cardinal/types" - "pkg.world.dev/world-engine/cardinal/types/engine" ) var ( @@ -69,9 +66,9 @@ func (p *personaPlugin) Register(world *World) error { } func (p *personaPlugin) RegisterQueries(world *World) error { - err := RegisterQuery[query.PersonaSignerQueryRequest, query.PersonaSignerQueryResponse](world, "signer", - query.PersonaSignerQuery, - WithCustomQueryGroup[query.PersonaSignerQueryRequest, query.PersonaSignerQueryResponse]("persona")) + err := RegisterQuery[PersonaSignerQueryRequest, PersonaSignerQueryResponse](world, "signer", + PersonaSignerQuery, + WithCustomQueryGroup[PersonaSignerQueryRequest, PersonaSignerQueryResponse]("persona")) if err != nil { return err } @@ -79,7 +76,7 @@ func (p *personaPlugin) RegisterQueries(world *World) error { } func (p *personaPlugin) RegisterSystems(world *World) error { - err := RegisterSystems(world, CreatePersonaSystem, AuthorizePersonaAddressSystem) + err := RegisterSystems(world, createPersonaSystem, authorizePersonaAddressSystem) if err != nil { return err } @@ -111,10 +108,10 @@ func (p *personaPlugin) RegisterMessages(world *World) error { // Persona Messages // ----------------------------------------------------------------------------- -// AuthorizePersonaAddressSystem enables users to authorize an address to a persona tag. This is mostly used so that +// authorizePersonaAddressSystem enables users to authorize an address to a persona tag. This is mostly used so that // users who want to interact with the game via smart contract can link their EVM address to their persona tag, enabling // them to mutate their owned state from the context of the EVM. -func AuthorizePersonaAddressSystem(wCtx engine.Context) error { +func authorizePersonaAddressSystem(wCtx WorldContext) error { if err := buildGlobalPersonaIndex(wCtx); err != nil { return err } @@ -165,9 +162,9 @@ func AuthorizePersonaAddressSystem(wCtx engine.Context) error { // Persona System // ----------------------------------------------------------------------------- -// CreatePersonaSystem is a system that will associate persona tags with signature addresses. Each persona tag +// createPersonaSystem is a system that will associate persona tags with signature addresses. Each persona tag // may have at most 1 signer, so additional attempts to register a signer with a persona tag will be ignored. -func CreatePersonaSystem(wCtx engine.Context) error { +func createPersonaSystem(wCtx WorldContext) error { if err := buildGlobalPersonaIndex(wCtx); err != nil { return err } @@ -220,7 +217,7 @@ func CreatePersonaSystem(wCtx engine.Context) error { // Persona Index // ----------------------------------------------------------------------------- -func buildGlobalPersonaIndex(wCtx engine.Context) error { +func buildGlobalPersonaIndex(wCtx WorldContext) error { // Rebuild the index if we haven't built it yet OR if we're in test and the CurrentTick has been reset. if globalPersonaTagToAddressIndex != nil && tickOfPersonaTagToAddressIndex < wCtx.CurrentTick() { return nil @@ -228,7 +225,7 @@ func buildGlobalPersonaIndex(wCtx engine.Context) error { tickOfPersonaTagToAddressIndex = wCtx.CurrentTick() globalPersonaTagToAddressIndex = map[string]personaIndexEntry{} var errs []error - s := search.NewSearch().Entity(filter.Exact(filter.Component[component.SignerComponent]())) + s := NewSearch().Entity(filter.Exact(filter.Component[component.SignerComponent]())) err := s.Each(wCtx, func(id types.EntityID) bool { sc, err := GetComponent[component.SignerComponent](wCtx, id) diff --git a/cardinal/query.go b/cardinal/query.go index 200c509cd..a362cade7 100644 --- a/cardinal/query.go +++ b/cardinal/query.go @@ -9,17 +9,40 @@ import ( "pkg.world.dev/world-engine/cardinal/abi" "pkg.world.dev/world-engine/cardinal/types" - "pkg.world.dev/world-engine/cardinal/types/engine" ) -var _ engine.Query = &queryType[struct{}, struct{}]{} +var _ query = &queryType[struct{}, struct{}]{} + +type query interface { + // Name returns the name of the query. + Name() string + // Group returns the group of the query. + Group() string + // HandleQuery handles queries with concrete types, rather than encoded bytes. + handleQuery(WorldContext, any) (any, error) + // HandleQueryRaw is given a reference to the engine, json encoded bytes that represent a query request + // and is expected to return a json encoded response struct. + handleQueryRaw(WorldContext, []byte) ([]byte, error) + // DecodeEVMRequest decodes bytes originating from the evm into the request type, which will be ABI encoded. + DecodeEVMRequest([]byte) (any, error) + // EncodeEVMReply encodes the reply as an abi encoded struct. + EncodeEVMReply(any) ([]byte, error) + // DecodeEVMReply decodes EVM reply bytes, into the concrete go reply type. + DecodeEVMReply([]byte) (any, error) + // EncodeAsABI encodes a go struct in abi format. This is mostly used for testing. + EncodeAsABI(any) ([]byte, error) + // IsEVMCompatible reports if the query is able to be sent from the EVM. + IsEVMCompatible() bool + // GetRequestFieldInformation returns a map of the fields of the query's request type and their types. + GetRequestFieldInformation() map[string]any +} type QueryOption[Request, Reply any] func(qt *queryType[Request, Reply]) type queryType[Request any, Reply any] struct { name string group string - handler func(wCtx engine.Context, req *Request) (*Reply, error) + handler func(wCtx WorldContext, req *Request) (*Reply, error) requestABI *ethereumAbi.Type replyABI *ethereumAbi.Type } @@ -42,11 +65,11 @@ func WithCustomQueryGroup[Request, Reply any](group string) QueryOption[Request, } } -func NewQueryType[Request any, Reply any]( +func newQueryType[Request any, Reply any]( name string, - handler func(wCtx engine.Context, req *Request) (*Reply, error), + handler func(wCtx WorldContext, req *Request) (*Reply, error), opts ...QueryOption[Request, Reply], -) (engine.Query, error) { +) (query, error) { err := validateQuery[Request, Reply](name, handler) if err != nil { return nil, err @@ -91,7 +114,7 @@ func (r *queryType[req, rep]) Group() string { return r.group } -func (r *queryType[req, rep]) HandleQuery(wCtx engine.Context, a any) (any, error) { +func (r *queryType[req, rep]) handleQuery(wCtx WorldContext, a any) (any, error) { var request *req if reflect.TypeOf(a).Kind() == reflect.Pointer { ptrRequest, ok := a.(*req) @@ -110,7 +133,7 @@ func (r *queryType[req, rep]) HandleQuery(wCtx engine.Context, a any) (any, erro return reply, err } -func (r *queryType[req, rep]) HandleQueryRaw(wCtx engine.Context, bz []byte) ([]byte, error) { +func (r *queryType[req, rep]) handleQueryRaw(wCtx WorldContext, bz []byte) ([]byte, error) { request := new(req) err := json.Unmarshal(bz, request) if err != nil { @@ -208,7 +231,7 @@ func (r *queryType[Request, Reply]) GetRequestFieldInformation() map[string]any func validateQuery[Request any, Reply any]( name string, - handler func(wCtx engine.Context, req *Request) (*Reply, error), + handler func(wCtx WorldContext, req *Request) (*Reply, error), ) error { if name == "" { return eris.New("cannot create query without name") diff --git a/cardinal/query_manager.go b/cardinal/query_manager.go index d825337c9..a90385188 100644 --- a/cardinal/query_manager.go +++ b/cardinal/query_manager.go @@ -1,24 +1,34 @@ package cardinal import ( + "fmt" + "github.com/rotisserie/eris" - "pkg.world.dev/world-engine/cardinal/types/engine" + "pkg.world.dev/world-engine/cardinal/server/utils" + "pkg.world.dev/world-engine/cardinal/types" ) -type QueryManager struct { - registeredQueries map[string]engine.Query +type QueryManager interface { + RegisterQuery(name string, query query) error + GetRegisteredQueries() []query + GetQueryByName(name string) (query, error) + BuildQueryFields() []types.FieldDetail +} + +type queryManager struct { + registeredQueries map[string]query } -func NewQueryManager() *QueryManager { - return &QueryManager{ - registeredQueries: make(map[string]engine.Query), +func newQueryManager() QueryManager { + return &queryManager{ + registeredQueries: make(map[string]query), } } // RegisterQuery registers a query with the query manager. // There can only be one query with a given name. -func (m *QueryManager) RegisterQuery(name string, query engine.Query) error { +func (m *queryManager) RegisterQuery(name string, query query) error { // Check that the query is not already registered if err := m.isQueryNameUnique(name); err != nil { return err @@ -26,21 +36,32 @@ func (m *QueryManager) RegisterQuery(name string, query engine.Query) error { // Register the query m.registeredQueries[name] = query - return nil } // GetRegisteredQueries returns all the registered queries. -func (m *QueryManager) GetRegisteredQueries() []engine.Query { - registeredQueries := make([]engine.Query, 0, len(m.registeredQueries)) +func (m *queryManager) GetRegisteredQueries() []query { + registeredQueries := make([]query, 0, len(m.registeredQueries)) for _, query := range m.registeredQueries { registeredQueries = append(registeredQueries, query) } return registeredQueries } +func (w *World) HandleQuery(group string, name string, bz []byte) ([]byte, error) { + q, err := w.GetQueryByName(name) + if err != nil { + return nil, eris.Wrap(types.ErrQueryNotFound, fmt.Sprintf("could not find query %q", name)) + } + if q.Group() != group { + return nil, eris.Errorf("Query group: %s with name: %s not found", group, name) + } + wCtx := NewReadOnlyWorldContext(w) + return q.handleQueryRaw(wCtx, bz) +} + // GetQueryByName returns a query corresponding to its name. -func (m *QueryManager) GetQueryByName(name string) (engine.Query, error) { +func (m *queryManager) GetQueryByName(name string) (query, error) { query, ok := m.registeredQueries[name] if !ok { return nil, eris.Errorf("query %q is not registered", name) @@ -48,9 +69,24 @@ func (m *QueryManager) GetQueryByName(name string) (engine.Query, error) { return query, nil } -func (m *QueryManager) isQueryNameUnique(name string) error { +func (m *queryManager) isQueryNameUnique(name string) error { if _, ok := m.registeredQueries[name]; ok { return eris.Errorf("query %q is already registered", name) } return nil } + +func (m *queryManager) BuildQueryFields() []types.FieldDetail { + // Collecting the structure of all queries + queries := m.GetRegisteredQueries() + queriesFields := make([]types.FieldDetail, 0, len(queries)) + for _, q := range queries { + // Extracting the fields of the q + queriesFields = append(queriesFields, types.FieldDetail{ + Name: q.Name(), + Fields: q.GetRequestFieldInformation(), + URL: utils.GetQueryURL(q.Group(), q.Name()), + }) + } + return queriesFields +} diff --git a/cardinal/query_test.go b/cardinal/query_test.go index 1fea4efb7..0d8d95a25 100644 --- a/cardinal/query_test.go +++ b/cardinal/query_test.go @@ -9,7 +9,6 @@ import ( "pkg.world.dev/world-engine/cardinal/search/filter" "pkg.world.dev/world-engine/cardinal/testutils" "pkg.world.dev/world-engine/cardinal/types" - "pkg.world.dev/world-engine/cardinal/types/engine" ) type Health struct { @@ -29,7 +28,7 @@ type QueryHealthResponse struct { } func handleQueryHealth( - wCtx engine.Context, + wCtx cardinal.WorldContext, request *QueryHealthRequest, ) (*QueryHealthResponse, error) { resp := &QueryHealthResponse{} @@ -65,7 +64,7 @@ func TestNewQueryTypeWithEVMSupport(t *testing.T) { testutils.NewTestFixture(t, nil).World, "query_health", func( - _ engine.Context, + _ cardinal.WorldContext, _ *FooReq, ) (*FooReply, error) { return &FooReply{}, errors.New("this function should never get called") @@ -102,17 +101,17 @@ func TestQueryExample(t *testing.T) { q, err := world.GetQueryByName("query_health") assert.NilError(t, err) - resp, err := q.HandleQuery(worldCtx, QueryHealthRequest{1_000_000}) + resp, err := cardinal.InternalHandleQuery(worldCtx, q, QueryHealthRequest{1_000_000}) assert.NilError(t, err) assert.Equal(t, 0, len(resp.(*QueryHealthResponse).IDs)) // All entities should have health over -100 - resp, err = q.HandleQuery(worldCtx, QueryHealthRequest{-100}) + resp, err = cardinal.InternalHandleQuery(worldCtx, q, QueryHealthRequest{-100}) assert.NilError(t, err) assert.Equal(t, 100, len(resp.(*QueryHealthResponse).IDs)) // Exactly 10 entities should have health at or above 90 - resp, err = q.HandleQuery(worldCtx, QueryHealthRequest{90}) + resp, err = cardinal.InternalHandleQuery(worldCtx, q, QueryHealthRequest{90}) assert.NilError(t, err) assert.Equal(t, 10, len(resp.(*QueryHealthResponse).IDs)) } @@ -122,7 +121,7 @@ func TestQueryTypeNotStructs(t *testing.T) { err := cardinal.RegisterQuery[string, string]( testutils.NewTestFixture(t, nil).World, "foo", - func(engine.Context, *string) (*string, error) { + func(cardinal.WorldContext, *string) (*string, error) { return &str, nil }, ) @@ -148,7 +147,7 @@ func TestQueryEVM(t *testing.T) { world, "foo", func( - _ engine.Context, _ *FooRequest, + _ cardinal.WorldContext, _ *FooRequest, ) (*FooReply, error) { return &expectedReply, nil }, diff --git a/cardinal/router/router.go b/cardinal/router/router.go index c59ef8bb7..824fb91dd 100644 --- a/cardinal/router/router.go +++ b/cardinal/router/router.go @@ -62,8 +62,8 @@ type router struct { routerKey string } -func New(namespace, sequencerAddr, routerKey string, provider Provider) (Router, error) { - rtr := &router{namespace: namespace, port: defaultPort, provider: provider, routerKey: routerKey} +func New(namespace, sequencerAddr, routerKey string, world Provider) (Router, error) { + rtr := &router{namespace: namespace, port: defaultPort, provider: world, routerKey: routerKey} conn, err := grpc.Dial( sequencerAddr, @@ -74,7 +74,7 @@ func New(namespace, sequencerAddr, routerKey string, provider Provider) (Router, return nil, eris.Wrapf(err, "error dialing shard seqeuncer address at %q", sequencerAddr) } rtr.ShardSequencer = shard.NewTransactionHandlerClient(conn) - rtr.server = newEvmServer(provider, routerKey) + rtr.server = newEvmServer(world, routerKey) routerv1.RegisterMsgServer(rtr.server.grpcServer, rtr.server) return rtr, nil } diff --git a/cardinal/search/search.go b/cardinal/search.go similarity index 80% rename from cardinal/search/search.go rename to cardinal/search.go index 69ed67dda..319204fde 100644 --- a/cardinal/search/search.go +++ b/cardinal/search.go @@ -1,4 +1,4 @@ -package search +package cardinal import ( "slices" @@ -8,16 +8,30 @@ import ( "pkg.world.dev/world-engine/cardinal/iterators" "pkg.world.dev/world-engine/cardinal/search/filter" "pkg.world.dev/world-engine/cardinal/types" - "pkg.world.dev/world-engine/cardinal/types/engine" ) -type CallbackFn func(types.EntityID) bool - type cache struct { archetypes []types.ArchetypeID seen int } +//revive:disable-next-line +type EntitySearch interface { + Searchable + Where(componentFilter FilterFn) EntitySearch +} + +type Searchable interface { + evaluateSearch(wCtx WorldContext) []types.ArchetypeID + Each(wCtx WorldContext, callback CallbackFn) error + First(wCtx WorldContext) (types.EntityID, error) + MustFirst(wCtx WorldContext) types.EntityID + Count(wCtx WorldContext) (int, error) + Collect(wCtx WorldContext) ([]types.EntityID, error) +} + +type CallbackFn func(types.EntityID) bool + // Search represents a search for entities. // It is used to filter entities based on their components. // It receives arbitrary filters that are used to filter entities. @@ -27,7 +41,7 @@ type cache struct { type Search struct { archMatches *cache filter filter.ComponentFilter - componentPropertyFilter filterFn + componentPropertyFilter FilterFn } // interfaces restrict order of operations. @@ -86,28 +100,22 @@ type searchBuilder interface { Entity(componentFilter filter.ComponentFilter) EntitySearch } -//revive:disable-next-line -type EntitySearch interface { - Searchable - Where(componentFilter filterFn) EntitySearch -} - -type Searchable interface { - evaluateSearch(eCtx engine.Context) []types.ArchetypeID - Each(eCtx engine.Context, callback CallbackFn) error - First(eCtx engine.Context) (types.EntityID, error) - MustFirst(eCtx engine.Context) types.EntityID - Count(eCtx engine.Context) (int, error) - Collect(eCtx engine.Context) ([]types.EntityID, error) -} - -// NewSearch creates a new search. -// It receives arbitrary filters that are used to filter entities. +// NewSearch is used to create a search object. +// +// Usage: +// +// cardinal.NewSearch().Entity(filter.Contains(filter.Component[EnergyComponent]())) func NewSearch() searchBuilder { return NewLegacySearch(nil).(searchBuilder) } // TODO: should deprecate this in the future. +// NewLegacySearch allows users to create a Search object with a filter already provided +// as a property. +// +// Example Usage: +// +// cardinal.NewLegacySearch().Entity(filter.Exact(Alpha{}, Beta{})).Count() func NewLegacySearch(componentFilter filter.ComponentFilter) EntitySearch { return &Search{ archMatches: &cache{}, @@ -123,8 +131,8 @@ func (s *Search) Entity(componentFilter filter.ComponentFilter) EntitySearch { // Once the where clause method is activated the search will ONLY return results // if a where clause returns true and no error. -func (s *Search) Where(componentFilter filterFn) EntitySearch { - var componentPropertyFilter filterFn +func (s *Search) Where(componentFilter FilterFn) EntitySearch { + var componentPropertyFilter FilterFn if s.componentPropertyFilter != nil { componentPropertyFilter = AndFilter(s.componentPropertyFilter, componentFilter) } else { @@ -139,11 +147,11 @@ func (s *Search) Where(componentFilter filterFn) EntitySearch { // Each iterates over all entities that match the search. // If you would like to stop the iteration, return false to the callback. To continue iterating, return true. -func (s *Search) Each(eCtx engine.Context, callback CallbackFn) (err error) { - defer func() { defer panicOnFatalError(eCtx, err) }() +func (s *Search) Each(wCtx WorldContext, callback CallbackFn) (err error) { + defer func() { defer panicOnFatalError(wCtx, err) }() - result := s.evaluateSearch(eCtx) - iter := iterators.NewEntityIterator(0, eCtx.StoreReader(), result) + result := s.evaluateSearch(wCtx) + iter := iterators.NewEntityIterator(0, wCtx.storeReader(), result) for iter.HasNext() { entities, err := iter.Next() if err != nil { @@ -152,7 +160,7 @@ func (s *Search) Each(eCtx engine.Context, callback CallbackFn) (err error) { for _, id := range entities { var filterValue bool if s.componentPropertyFilter != nil { - filterValue, err = s.componentPropertyFilter(eCtx, id) + filterValue, err = s.componentPropertyFilter(wCtx, id) if err != nil { continue } @@ -175,9 +183,9 @@ func fastSortIDs(ids []types.EntityID) { slices.Sort(ids) } -func (s *Search) Collect(eCtx engine.Context) ([]types.EntityID, error) { +func (s *Search) Collect(wCtx WorldContext) ([]types.EntityID, error) { acc := make([]types.EntityID, 0) - err := s.Each(eCtx, func(id types.EntityID) bool { + err := s.Each(wCtx, func(id types.EntityID) bool { acc = append(acc, id) return true }) @@ -189,11 +197,11 @@ func (s *Search) Collect(eCtx engine.Context) ([]types.EntityID, error) { } // Count returns the number of entities that match the search. -func (s *Search) Count(eCtx engine.Context) (ret int, err error) { - defer func() { defer panicOnFatalError(eCtx, err) }() +func (s *Search) Count(wCtx WorldContext) (ret int, err error) { + defer func() { defer panicOnFatalError(wCtx, err) }() - result := s.evaluateSearch(eCtx) - iter := iterators.NewEntityIterator(0, eCtx.StoreReader(), result) + result := s.evaluateSearch(wCtx) + iter := iterators.NewEntityIterator(0, wCtx.storeReader(), result) for iter.HasNext() { entities, err := iter.Next() if err != nil { @@ -202,7 +210,7 @@ func (s *Search) Count(eCtx engine.Context) (ret int, err error) { for _, id := range entities { var filterValue bool if s.componentPropertyFilter != nil { - filterValue, err = s.componentPropertyFilter(eCtx, id) + filterValue, err = s.componentPropertyFilter(wCtx, id) if err != nil { continue } @@ -218,11 +226,11 @@ func (s *Search) Count(eCtx engine.Context) (ret int, err error) { } // First returns the first entity that matches the search. -func (s *Search) First(eCtx engine.Context) (id types.EntityID, err error) { - defer func() { defer panicOnFatalError(eCtx, err) }() +func (s *Search) First(wCtx WorldContext) (id types.EntityID, err error) { + defer func() { defer panicOnFatalError(wCtx, err) }() - result := s.evaluateSearch(eCtx) - iter := iterators.NewEntityIterator(0, eCtx.StoreReader(), result) + result := s.evaluateSearch(wCtx) + iter := iterators.NewEntityIterator(0, wCtx.storeReader(), result) if !iter.HasNext() { return iterators.BadID, eris.Wrap(err, "") } @@ -234,7 +242,7 @@ func (s *Search) First(eCtx engine.Context) (id types.EntityID, err error) { for _, id := range entities { var filterValue bool if s.componentPropertyFilter != nil { - filterValue, err = s.componentPropertyFilter(eCtx, id) + filterValue, err = s.componentPropertyFilter(wCtx, id) if err != nil { continue } @@ -249,19 +257,19 @@ func (s *Search) First(eCtx engine.Context) (id types.EntityID, err error) { return iterators.BadID, eris.Wrap(err, "") } -func (s *Search) MustFirst(eCtx engine.Context) types.EntityID { - id, err := s.First(eCtx) +func (s *Search) MustFirst(wCtx WorldContext) types.EntityID { + id, err := s.First(wCtx) if err != nil { panic("no entity matches the search") } return id } -func (s *Search) evaluateSearch(eCtx engine.Context) []types.ArchetypeID { +func (s *Search) evaluateSearch(wCtx WorldContext) []types.ArchetypeID { cache := s.archMatches - for it := eCtx.StoreReader().SearchFrom(s.filter, cache.seen); it.HasNext(); { + for it := wCtx.storeReader().SearchFrom(s.filter, cache.seen); it.HasNext(); { cache.archetypes = append(cache.archetypes, it.Next()) } - cache.seen = eCtx.StoreReader().ArchetypeCount() + cache.seen = wCtx.storeReader().ArchetypeCount() return cache.archetypes } diff --git a/cardinal/search/searchcomponent.go b/cardinal/search/searchcomponent.go deleted file mode 100644 index 5e5e8eb6e..000000000 --- a/cardinal/search/searchcomponent.go +++ /dev/null @@ -1,9 +0,0 @@ -package search - -// This file represents primitives for search -// The primitive wraps the component and is used in search -// the purpose of the wrapper is to prevent the user from ever -// instantiating the component. These wrappers are used to check if entities -// contain the specified component during the search. - -//revive:disable-next-line:unexported-return diff --git a/cardinal/search/util.go b/cardinal/search/util.go deleted file mode 100644 index 4f9bd4b1b..000000000 --- a/cardinal/search/util.go +++ /dev/null @@ -1,32 +0,0 @@ -package search - -import ( - "github.com/rotisserie/eris" - - "pkg.world.dev/world-engine/cardinal/iterators" - "pkg.world.dev/world-engine/cardinal/types/engine" -) - -var NonFatalError = []error{ - iterators.ErrEntityDoesNotExist, - iterators.ErrComponentNotOnEntity, - iterators.ErrComponentAlreadyOnEntity, - iterators.ErrEntityMustHaveAtLeastOneComponent, -} - -// panicOnFatalError is a helper function to panic on non-deterministic errors (i.e. Redis error). -func panicOnFatalError(wCtx engine.Context, err error) { - if err != nil && !wCtx.IsReadOnly() && isFatalError(err) { - wCtx.Logger().Panic().Err(err).Msgf("fatal error: %v", eris.ToString(err, true)) - panic(err) - } -} - -func isFatalError(err error) bool { - for _, e := range NonFatalError { - if eris.Is(err, e) { - return false - } - } - return true -} diff --git a/cardinal/search_test.go b/cardinal/search_test.go index a4526631b..961f328cf 100644 --- a/cardinal/search_test.go +++ b/cardinal/search_test.go @@ -5,11 +5,9 @@ import ( "pkg.world.dev/world-engine/assert" "pkg.world.dev/world-engine/cardinal" - "pkg.world.dev/world-engine/cardinal/search" "pkg.world.dev/world-engine/cardinal/search/filter" "pkg.world.dev/world-engine/cardinal/testutils" "pkg.world.dev/world-engine/cardinal/types" - "pkg.world.dev/world-engine/cardinal/types/engine" ) var _ types.Component @@ -81,16 +79,14 @@ func TestSearchUsingAllMethods(t *testing.T) { hpids, err := cardinal.CreateMany(worldCtx, 10, HP{}) assert.NilError(t, err) for i, id := range hpids { - c, err := worldCtx.GetComponentByName(HP{}.Name()) - assert.NilError(t, err) - err = worldCtx.StoreManager().SetComponentForEntity(c, id, HP{amount: i}) + err = cardinal.SetComponent[HP](worldCtx, id, &HP{amount: i}) assert.NilError(t, err) } amt, err := cardinal.NewSearch().Entity(filter.Not(filter.Or( filter.Contains(filter.Component[AlphaTest]()), filter.Contains(filter.Component[BetaTest]()), filter.Contains(filter.Component[GammaTest]())), - )).Where(func(_ engine.Context, _ types.EntityID) (bool, error) { + )).Where(func(_ cardinal.WorldContext, _ types.EntityID) (bool, error) { return true, nil }).Count(worldCtx) assert.NilError(t, err) @@ -99,7 +95,7 @@ func TestSearchUsingAllMethods(t *testing.T) { filter.Contains(filter.Component[AlphaTest]()), filter.Contains(filter.Component[BetaTest]()), filter.Contains(filter.Component[GammaTest]())), - )).Where(func(wCtx engine.Context, id types.EntityID) (bool, error) { + )).Where(func(wCtx cardinal.WorldContext, id types.EntityID) (bool, error) { c, err := cardinal.GetComponent[HP](wCtx, id) if err != nil { return false, err @@ -154,23 +150,23 @@ func TestSetOperationsOnSearch(t *testing.T) { filter.Component[AlphaTest]())) tests := []struct { - search search.Searchable + search cardinal.Searchable count int }{ { - search: search.And(q1, q2), + search: cardinal.And(q1, q2), count: 0, }, { - search: search.Or(q1, q2), + search: cardinal.Or(q1, q2), count: 20, }, { - search: search.Not(search.Or(q1, q2, q3)), + search: cardinal.Not(cardinal.Or(q1, q2, q3)), count: 10, }, { - search: search.Not(search.And(q1, q2, q3)), + search: cardinal.Not(cardinal.And(q1, q2, q3)), count: 40, }, { - search: search.Not(q4), + search: cardinal.Not(q4), count: 20, }, } @@ -218,7 +214,7 @@ func TestSearch_Integration(t *testing.T) { testCases := []struct { name string - search search.Searchable + search cardinal.Searchable want int }{ { @@ -241,13 +237,13 @@ func TestSearch_Integration(t *testing.T) { }, { "beta or gamma", - search.Or(cardinal.NewSearch().Entity(filter.Exact(filter.Component[BetaTest]())), + cardinal.Or(cardinal.NewSearch().Entity(filter.Exact(filter.Component[BetaTest]())), cardinal.NewSearch().Entity(filter.Exact(filter.Component[GammaTest]()))), 20, }, { "not alpha", - search.Not(cardinal.NewSearch().Entity(filter.Exact(filter.Component[AlphaTest]()))), + cardinal.Not(cardinal.NewSearch().Entity(filter.Exact(filter.Component[AlphaTest]()))), 61, }, { @@ -334,7 +330,7 @@ func TestSearch_Exact_ReturnsExactComponentMatch(t *testing.T) { _, err = cardinal.CreateMany(worldCtx, 10, AlphaTest{}, BetaTest{}, GammaTest{}) assert.NilError(t, err) - amt, err := search.NewSearch().Entity(filter.Exact(filter.Component[BetaTest]())).Count(worldCtx) + amt, err := cardinal.NewSearch().Entity(filter.Exact(filter.Component[BetaTest]())).Count(worldCtx) assert.NilError(t, err) assert.Equal(t, amt, 12) } @@ -367,7 +363,7 @@ func TestSearch_Contains_ReturnsEntityThatContainsComponents(t *testing.T) { _, err = cardinal.CreateMany(worldCtx, 10, AlphaTest{}, BetaTest{}, GammaTest{}) assert.NilError(t, err) - amt, err := search.NewSearch().Entity(filter.Contains(filter.Component[BetaTest]())).Count(worldCtx) + amt, err := cardinal.NewSearch().Entity(filter.Contains(filter.Component[BetaTest]())).Count(worldCtx) assert.NilError(t, err) assert.Equal(t, amt, 42) } @@ -400,7 +396,7 @@ func TestSearch_ComponentNotRegistered_ReturnsZeroEntityWithNoError(t *testing.T _, err = cardinal.CreateMany(worldCtx, 10, AlphaTest{}, BetaTest{}, GammaTest{}) assert.NilError(t, err) - amt, err := search.NewSearch().Entity(filter.Contains(filter.Component[HP]())).Count(worldCtx) + amt, err := cardinal.NewSearch().Entity(filter.Contains(filter.Component[HP]())).Count(worldCtx) assert.NilError(t, err) assert.Equal(t, amt, 0) } @@ -433,23 +429,23 @@ func TestUnregisteredComponentOnSetOperators(t *testing.T) { _, err = cardinal.CreateMany(worldCtx, 10, AlphaTest{}, BetaTest{}, GammaTest{}) assert.NilError(t, err) - q1 := search.NewSearch().Entity(filter.Contains(filter.Component[HP]())) - q2 := search.NewSearch().Entity(filter.Contains(filter.Component[AlphaTest]())) + q1 := cardinal.NewSearch().Entity(filter.Contains(filter.Component[HP]())) + q2 := cardinal.NewSearch().Entity(filter.Contains(filter.Component[AlphaTest]())) tests := []struct { - search search.Searchable + search cardinal.Searchable count int }{ { - search: search.And(q1, q2), + search: cardinal.And(q1, q2), count: 0, }, { - search: search.Or(q1, q2), + search: cardinal.Or(q1, q2), count: 40, }, { - search: search.Not(q1), + search: cardinal.Not(q1), count: 72, }, } @@ -487,7 +483,8 @@ func TestWhereClauseOnSearch(t *testing.T) { _, err = cardinal.CreateMany(worldCtx, 10, AlphaTest{}, BetaTest{}, GammaTest{}) assert.NilError(t, err) - q1 := cardinal.NewSearch().Entity(filter.All()).Where(func(wCtx engine.Context, id types.EntityID) (bool, error) { + q1 := cardinal.NewSearch().Entity(filter.All()).Where(func( + wCtx cardinal.WorldContext, id types.EntityID) (bool, error) { _, err := cardinal.GetComponent[AlphaTest](wCtx, id) if err != nil { return false, err diff --git a/cardinal/server/debug_test.go b/cardinal/server/debug_test.go index 6e3e436e8..61485486e 100644 --- a/cardinal/server/debug_test.go +++ b/cardinal/server/debug_test.go @@ -5,6 +5,7 @@ import ( "pkg.world.dev/world-engine/cardinal" "pkg.world.dev/world-engine/cardinal/server/handler" + "pkg.world.dev/world-engine/cardinal/types" ) func (s *ServerTestSuite) TestDebugStateQuery() { @@ -24,22 +25,24 @@ func (s *ServerTestSuite) TestDebugStateQuery() { s.Require().NoError(err) s.Require().Equal(res.StatusCode, 200) - var results handler.DebugStateResponse + var results []types.EntityStateElement s.Require().NoError(json.NewDecoder(res.Body).Decode(&results)) numOfZeroLocation := 0 numOfNonZeroLocation := 0 - for _, result := range results { - comp := result.Components["location"] + for i, result := range results { + comp := result.Data[0] if comp == nil { continue } var loc LocationComponent s.Require().NoError(json.Unmarshal(comp, &loc)) - if loc.Y == 0 { - numOfZeroLocation++ - } else { - numOfNonZeroLocation++ + if i != 6 { + if loc.Y == 0 { + numOfZeroLocation++ + } else { + numOfNonZeroLocation++ + } } } s.Require().Equal(numOfZeroLocation, wantNumOfZeroLocation) @@ -53,7 +56,7 @@ func (s *ServerTestSuite) TestDebugStateQuery_NoState() { res := s.fixture.Post("debug/state", handler.DebugStateRequest{}) s.Require().Equal(res.StatusCode, 200) - var results handler.DebugStateResponse + var results []types.EntityStateElement s.Require().NoError(json.NewDecoder(res.Body).Decode(&results)) s.Require().Equal(len(results), 0) diff --git a/cardinal/server/docs/docs.go b/cardinal/server/docs/docs.go index dc5763f22..6a126115d 100644 --- a/cardinal/server/docs/docs.go +++ b/cardinal/server/docs/docs.go @@ -32,7 +32,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/handler.CQLQueryRequest" + "$ref": "#/definitions/cardinal_server_handler.CQLQueryRequest" } } ], @@ -40,7 +40,7 @@ const docTemplate = `{ "200": { "description": "Results of the executed CQL query", "schema": { - "$ref": "#/definitions/handler.CQLQueryResponse" + "$ref": "#/definitions/cardinal_server_handler.CQLQueryResponse" } }, "400": { @@ -65,7 +65,7 @@ const docTemplate = `{ "schema": { "type": "array", "items": { - "$ref": "#/definitions/handler.debugStateElement" + "$ref": "#/definitions/pkg_world_dev_world-engine_cardinal_types.EntityStateElement" } } } @@ -100,51 +100,7 @@ const docTemplate = `{ "200": { "description": "Server and game loop status", "schema": { - "$ref": "#/definitions/handler.GetHealthResponse" - } - } - } - } - }, - "/query/game/{queryName}": { - "post": { - "description": "Executes a query", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "summary": "Executes a query", - "parameters": [ - { - "type": "string", - "description": "Name of a registered query", - "name": "queryName", - "in": "path", - "required": true - }, - { - "description": "Query to be executed", - "name": "queryBody", - "in": "body", - "required": true, - "schema": { - "type": "object" - } - } - ], - "responses": { - "200": { - "description": "Results of the executed query", - "schema": { - "type": "object" - } - }, - "400": { - "description": "Invalid request parameters", - "schema": { - "type": "string" + "$ref": "#/definitions/cardinal_server_handler.GetHealthResponse" } } } @@ -167,7 +123,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/handler.ListTxReceiptsRequest" + "$ref": "#/definitions/cardinal_server_handler.ListTxReceiptsRequest" } } ], @@ -175,7 +131,7 @@ const docTemplate = `{ "200": { "description": "List of receipts", "schema": { - "$ref": "#/definitions/handler.ListTxReceiptsResponse" + "$ref": "#/definitions/cardinal_server_handler.ListTxReceiptsResponse" } }, "400": { @@ -262,7 +218,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/handler.Transaction" + "$ref": "#/definitions/cardinal_server_handler.Transaction" } } ], @@ -270,7 +226,7 @@ const docTemplate = `{ "200": { "description": "Transaction hash and tick", "schema": { - "$ref": "#/definitions/handler.PostTransactionResponse" + "$ref": "#/definitions/cardinal_server_handler.PostTransactionResponse" } }, "400": { @@ -299,7 +255,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/handler.Transaction" + "$ref": "#/definitions/cardinal_server_handler.Transaction" } } ], @@ -307,7 +263,7 @@ const docTemplate = `{ "200": { "description": "Transaction hash and tick", "schema": { - "$ref": "#/definitions/handler.PostTransactionResponse" + "$ref": "#/definitions/cardinal_server_handler.PostTransactionResponse" } }, "400": { @@ -350,7 +306,7 @@ const docTemplate = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/handler.Transaction" + "$ref": "#/definitions/cardinal_server_handler.Transaction" } } ], @@ -358,7 +314,7 @@ const docTemplate = `{ "200": { "description": "Transaction hash and tick", "schema": { - "$ref": "#/definitions/handler.PostTransactionResponse" + "$ref": "#/definitions/cardinal_server_handler.PostTransactionResponse" } }, "400": { @@ -384,7 +340,7 @@ const docTemplate = `{ "200": { "description": "Details of the game world", "schema": { - "$ref": "#/definitions/handler.GetWorldResponse" + "$ref": "#/definitions/cardinal_server_handler.GetWorldResponse" } }, "400": { @@ -398,7 +354,7 @@ const docTemplate = `{ } }, "definitions": { - "handler.CQLQueryRequest": { + "cardinal_server_handler.CQLQueryRequest": { "type": "object", "properties": { "cql": { @@ -406,35 +362,18 @@ const docTemplate = `{ } } }, - "handler.CQLQueryResponse": { + "cardinal_server_handler.CQLQueryResponse": { "type": "object", "properties": { "results": { "type": "array", "items": { - "$ref": "#/definitions/handler.cqlData" + "$ref": "#/definitions/pkg_world_dev_world-engine_cardinal_types.EntityStateElement" } } } }, - "handler.FieldDetail": { - "type": "object", - "properties": { - "fields": { - "description": "variable name and type", - "type": "object", - "additionalProperties": {} - }, - "name": { - "description": "name of the message or query", - "type": "string" - }, - "url": { - "type": "string" - } - } - }, - "handler.GetHealthResponse": { + "cardinal_server_handler.GetHealthResponse": { "type": "object", "properties": { "isGameLoopRunning": { @@ -445,20 +384,20 @@ const docTemplate = `{ } } }, - "handler.GetWorldResponse": { + "cardinal_server_handler.GetWorldResponse": { "type": "object", "properties": { "components": { "description": "list of component names", "type": "array", "items": { - "$ref": "#/definitions/handler.FieldDetail" + "$ref": "#/definitions/pkg_world_dev_world-engine_cardinal_types.FieldDetail" } }, "messages": { "type": "array", "items": { - "$ref": "#/definitions/handler.FieldDetail" + "$ref": "#/definitions/pkg_world_dev_world-engine_cardinal_types.FieldDetail" } }, "namespace": { @@ -467,12 +406,12 @@ const docTemplate = `{ "queries": { "type": "array", "items": { - "$ref": "#/definitions/handler.FieldDetail" + "$ref": "#/definitions/pkg_world_dev_world-engine_cardinal_types.FieldDetail" } } } }, - "handler.ListTxReceiptsRequest": { + "cardinal_server_handler.ListTxReceiptsRequest": { "type": "object", "properties": { "startTick": { @@ -480,7 +419,7 @@ const docTemplate = `{ } } }, - "handler.ListTxReceiptsResponse": { + "cardinal_server_handler.ListTxReceiptsResponse": { "type": "object", "properties": { "endTick": { @@ -489,7 +428,7 @@ const docTemplate = `{ "receipts": { "type": "array", "items": { - "$ref": "#/definitions/handler.ReceiptEntry" + "$ref": "#/definitions/cardinal_server_handler.ReceiptEntry" } }, "startTick": { @@ -497,7 +436,7 @@ const docTemplate = `{ } } }, - "handler.PostTransactionResponse": { + "cardinal_server_handler.PostTransactionResponse": { "type": "object", "properties": { "tick": { @@ -508,7 +447,7 @@ const docTemplate = `{ } } }, - "handler.ReceiptEntry": { + "cardinal_server_handler.ReceiptEntry": { "type": "object", "properties": { "errors": { @@ -526,7 +465,7 @@ const docTemplate = `{ } } }, - "handler.Transaction": { + "cardinal_server_handler.Transaction": { "type": "object", "properties": { "body": { @@ -551,7 +490,7 @@ const docTemplate = `{ } } }, - "handler.cqlData": { + "pkg_world_dev_world-engine_cardinal_types.EntityStateElement": { "type": "object", "properties": { "data": { @@ -562,14 +501,20 @@ const docTemplate = `{ } } }, - "handler.debugStateElement": { + "pkg_world_dev_world-engine_cardinal_types.FieldDetail": { "type": "object", "properties": { - "components": { - "type": "object" + "fields": { + "description": "variable name and type", + "type": "object", + "additionalProperties": {} }, - "id": { - "type": "integer" + "name": { + "description": "name of the message or query", + "type": "string" + }, + "url": { + "type": "string" } } } diff --git a/cardinal/server/docs/swagger.json b/cardinal/server/docs/swagger.json index 0526e3f23..854790a96 100644 --- a/cardinal/server/docs/swagger.json +++ b/cardinal/server/docs/swagger.json @@ -29,7 +29,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/handler.CQLQueryRequest" + "$ref": "#/definitions/cardinal_server_handler.CQLQueryRequest" } } ], @@ -37,7 +37,7 @@ "200": { "description": "Results of the executed CQL query", "schema": { - "$ref": "#/definitions/handler.CQLQueryResponse" + "$ref": "#/definitions/cardinal_server_handler.CQLQueryResponse" } }, "400": { @@ -62,7 +62,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/handler.debugStateElement" + "$ref": "#/definitions/pkg_world_dev_world-engine_cardinal_types.EntityStateElement" } } } @@ -97,51 +97,7 @@ "200": { "description": "Server and game loop status", "schema": { - "$ref": "#/definitions/handler.GetHealthResponse" - } - } - } - } - }, - "/query/game/{queryName}": { - "post": { - "description": "Executes a query", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "summary": "Executes a query", - "parameters": [ - { - "type": "string", - "description": "Name of a registered query", - "name": "queryName", - "in": "path", - "required": true - }, - { - "description": "Query to be executed", - "name": "queryBody", - "in": "body", - "required": true, - "schema": { - "type": "object" - } - } - ], - "responses": { - "200": { - "description": "Results of the executed query", - "schema": { - "type": "object" - } - }, - "400": { - "description": "Invalid request parameters", - "schema": { - "type": "string" + "$ref": "#/definitions/cardinal_server_handler.GetHealthResponse" } } } @@ -164,7 +120,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/handler.ListTxReceiptsRequest" + "$ref": "#/definitions/cardinal_server_handler.ListTxReceiptsRequest" } } ], @@ -172,7 +128,7 @@ "200": { "description": "List of receipts", "schema": { - "$ref": "#/definitions/handler.ListTxReceiptsResponse" + "$ref": "#/definitions/cardinal_server_handler.ListTxReceiptsResponse" } }, "400": { @@ -259,7 +215,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/handler.Transaction" + "$ref": "#/definitions/cardinal_server_handler.Transaction" } } ], @@ -267,7 +223,7 @@ "200": { "description": "Transaction hash and tick", "schema": { - "$ref": "#/definitions/handler.PostTransactionResponse" + "$ref": "#/definitions/cardinal_server_handler.PostTransactionResponse" } }, "400": { @@ -296,7 +252,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/handler.Transaction" + "$ref": "#/definitions/cardinal_server_handler.Transaction" } } ], @@ -304,7 +260,7 @@ "200": { "description": "Transaction hash and tick", "schema": { - "$ref": "#/definitions/handler.PostTransactionResponse" + "$ref": "#/definitions/cardinal_server_handler.PostTransactionResponse" } }, "400": { @@ -347,7 +303,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/handler.Transaction" + "$ref": "#/definitions/cardinal_server_handler.Transaction" } } ], @@ -355,7 +311,7 @@ "200": { "description": "Transaction hash and tick", "schema": { - "$ref": "#/definitions/handler.PostTransactionResponse" + "$ref": "#/definitions/cardinal_server_handler.PostTransactionResponse" } }, "400": { @@ -381,7 +337,7 @@ "200": { "description": "Details of the game world", "schema": { - "$ref": "#/definitions/handler.GetWorldResponse" + "$ref": "#/definitions/cardinal_server_handler.GetWorldResponse" } }, "400": { @@ -395,7 +351,7 @@ } }, "definitions": { - "handler.CQLQueryRequest": { + "cardinal_server_handler.CQLQueryRequest": { "type": "object", "properties": { "cql": { @@ -403,35 +359,18 @@ } } }, - "handler.CQLQueryResponse": { + "cardinal_server_handler.CQLQueryResponse": { "type": "object", "properties": { "results": { "type": "array", "items": { - "$ref": "#/definitions/handler.cqlData" + "$ref": "#/definitions/pkg_world_dev_world-engine_cardinal_types.EntityStateElement" } } } }, - "handler.FieldDetail": { - "type": "object", - "properties": { - "fields": { - "description": "variable name and type", - "type": "object", - "additionalProperties": {} - }, - "name": { - "description": "name of the message or query", - "type": "string" - }, - "url": { - "type": "string" - } - } - }, - "handler.GetHealthResponse": { + "cardinal_server_handler.GetHealthResponse": { "type": "object", "properties": { "isGameLoopRunning": { @@ -442,20 +381,20 @@ } } }, - "handler.GetWorldResponse": { + "cardinal_server_handler.GetWorldResponse": { "type": "object", "properties": { "components": { "description": "list of component names", "type": "array", "items": { - "$ref": "#/definitions/handler.FieldDetail" + "$ref": "#/definitions/pkg_world_dev_world-engine_cardinal_types.FieldDetail" } }, "messages": { "type": "array", "items": { - "$ref": "#/definitions/handler.FieldDetail" + "$ref": "#/definitions/pkg_world_dev_world-engine_cardinal_types.FieldDetail" } }, "namespace": { @@ -464,12 +403,12 @@ "queries": { "type": "array", "items": { - "$ref": "#/definitions/handler.FieldDetail" + "$ref": "#/definitions/pkg_world_dev_world-engine_cardinal_types.FieldDetail" } } } }, - "handler.ListTxReceiptsRequest": { + "cardinal_server_handler.ListTxReceiptsRequest": { "type": "object", "properties": { "startTick": { @@ -477,7 +416,7 @@ } } }, - "handler.ListTxReceiptsResponse": { + "cardinal_server_handler.ListTxReceiptsResponse": { "type": "object", "properties": { "endTick": { @@ -486,7 +425,7 @@ "receipts": { "type": "array", "items": { - "$ref": "#/definitions/handler.ReceiptEntry" + "$ref": "#/definitions/cardinal_server_handler.ReceiptEntry" } }, "startTick": { @@ -494,7 +433,7 @@ } } }, - "handler.PostTransactionResponse": { + "cardinal_server_handler.PostTransactionResponse": { "type": "object", "properties": { "tick": { @@ -505,7 +444,7 @@ } } }, - "handler.ReceiptEntry": { + "cardinal_server_handler.ReceiptEntry": { "type": "object", "properties": { "errors": { @@ -523,7 +462,7 @@ } } }, - "handler.Transaction": { + "cardinal_server_handler.Transaction": { "type": "object", "properties": { "body": { @@ -548,7 +487,7 @@ } } }, - "handler.cqlData": { + "pkg_world_dev_world-engine_cardinal_types.EntityStateElement": { "type": "object", "properties": { "data": { @@ -559,14 +498,20 @@ } } }, - "handler.debugStateElement": { + "pkg_world_dev_world-engine_cardinal_types.FieldDetail": { "type": "object", "properties": { - "components": { - "type": "object" + "fields": { + "description": "variable name and type", + "type": "object", + "additionalProperties": {} }, - "id": { - "type": "integer" + "name": { + "description": "name of the message or query", + "type": "string" + }, + "url": { + "type": "string" } } } diff --git a/cardinal/server/docs/swagger.yaml b/cardinal/server/docs/swagger.yaml index bf9c100d9..0a6a71de7 100644 --- a/cardinal/server/docs/swagger.yaml +++ b/cardinal/server/docs/swagger.yaml @@ -1,78 +1,66 @@ basePath: / definitions: - handler.CQLQueryRequest: + cardinal_server_handler.CQLQueryRequest: properties: cql: type: string type: object - handler.CQLQueryResponse: + cardinal_server_handler.CQLQueryResponse: properties: results: items: - $ref: '#/definitions/handler.cqlData' + $ref: '#/definitions/pkg_world_dev_world-engine_cardinal_types.EntityStateElement' type: array type: object - handler.FieldDetail: - properties: - fields: - additionalProperties: {} - description: variable name and type - type: object - name: - description: name of the message or query - type: string - url: - type: string - type: object - handler.GetHealthResponse: + cardinal_server_handler.GetHealthResponse: properties: isGameLoopRunning: type: boolean isServerRunning: type: boolean type: object - handler.GetWorldResponse: + cardinal_server_handler.GetWorldResponse: properties: components: description: list of component names items: - $ref: '#/definitions/handler.FieldDetail' + $ref: '#/definitions/pkg_world_dev_world-engine_cardinal_types.FieldDetail' type: array messages: items: - $ref: '#/definitions/handler.FieldDetail' + $ref: '#/definitions/pkg_world_dev_world-engine_cardinal_types.FieldDetail' type: array namespace: type: string queries: items: - $ref: '#/definitions/handler.FieldDetail' + $ref: '#/definitions/pkg_world_dev_world-engine_cardinal_types.FieldDetail' type: array type: object - handler.ListTxReceiptsRequest: + cardinal_server_handler.ListTxReceiptsRequest: properties: startTick: type: integer type: object - handler.ListTxReceiptsResponse: + cardinal_server_handler.ListTxReceiptsResponse: properties: endTick: type: integer receipts: items: - $ref: '#/definitions/handler.ReceiptEntry' + $ref: '#/definitions/cardinal_server_handler.ReceiptEntry' type: array startTick: type: integer type: object - handler.PostTransactionResponse: + cardinal_server_handler.PostTransactionResponse: properties: tick: type: integer txHash: type: string type: object - handler.ReceiptEntry: + cardinal_server_handler.ReceiptEntry: properties: errors: items: @@ -84,7 +72,7 @@ definitions: txHash: type: string type: object - handler.Transaction: + cardinal_server_handler.Transaction: properties: body: description: json string @@ -101,19 +89,24 @@ definitions: description: hex encoded string type: string type: object - handler.cqlData: + pkg_world_dev_world-engine_cardinal_types.EntityStateElement: properties: data: type: object id: type: integer type: object - handler.debugStateElement: + pkg_world_dev_world-engine_cardinal_types.FieldDetail: properties: - components: + fields: + additionalProperties: {} + description: variable name and type type: object - id: - type: integer + name: + description: name of the message or query + type: string + url: + type: string type: object info: contact: {} @@ -132,14 +125,14 @@ paths: name: cql required: true schema: - $ref: '#/definitions/handler.CQLQueryRequest' + $ref: '#/definitions/cardinal_server_handler.CQLQueryRequest' produces: - application/json responses: "200": description: Results of the executed CQL query schema: - $ref: '#/definitions/handler.CQLQueryResponse' + $ref: '#/definitions/cardinal_server_handler.CQLQueryResponse' "400": description: Invalid request parameters schema: @@ -155,7 +148,7 @@ paths: description: List of all entities schema: items: - $ref: '#/definitions/handler.debugStateElement' + $ref: '#/definitions/pkg_world_dev_world-engine_cardinal_types.EntityStateElement' type: array summary: Retrieves a list of all entities in the game state /events: @@ -178,7 +171,7 @@ paths: "200": description: Server and game loop status schema: - $ref: '#/definitions/handler.GetHealthResponse' + $ref: '#/definitions/cardinal_server_handler.GetHealthResponse' summary: Retrieves the status of the server and game loop /query/{queryGroup}/{queryName}: post: @@ -214,35 +207,6 @@ paths: schema: type: string summary: Executes a query - /query/game/{queryName}: - post: - consumes: - - application/json - description: Executes a query - parameters: - - description: Name of a registered query - in: path - name: queryName - required: true - type: string - - description: Query to be executed - in: body - name: queryBody - required: true - schema: - type: object - produces: - - application/json - responses: - "200": - description: Results of the executed query - schema: - type: object - "400": - description: Invalid request parameters - schema: - type: string - summary: Executes a query /query/receipts/list: post: consumes: @@ -254,14 +218,14 @@ paths: name: ListTxReceiptsRequest required: true schema: - $ref: '#/definitions/handler.ListTxReceiptsRequest' + $ref: '#/definitions/cardinal_server_handler.ListTxReceiptsRequest' produces: - application/json responses: "200": description: List of receipts schema: - $ref: '#/definitions/handler.ListTxReceiptsResponse' + $ref: '#/definitions/cardinal_server_handler.ListTxReceiptsResponse' "400": description: Invalid request body schema: @@ -288,14 +252,14 @@ paths: name: txBody required: true schema: - $ref: '#/definitions/handler.Transaction' + $ref: '#/definitions/cardinal_server_handler.Transaction' produces: - application/json responses: "200": description: Transaction hash and tick schema: - $ref: '#/definitions/handler.PostTransactionResponse' + $ref: '#/definitions/cardinal_server_handler.PostTransactionResponse' "400": description: Invalid request parameter schema: @@ -317,14 +281,14 @@ paths: name: txBody required: true schema: - $ref: '#/definitions/handler.Transaction' + $ref: '#/definitions/cardinal_server_handler.Transaction' produces: - application/json responses: "200": description: Transaction hash and tick schema: - $ref: '#/definitions/handler.PostTransactionResponse' + $ref: '#/definitions/cardinal_server_handler.PostTransactionResponse' "400": description: Invalid request parameter schema: @@ -341,14 +305,14 @@ paths: name: txBody required: true schema: - $ref: '#/definitions/handler.Transaction' + $ref: '#/definitions/cardinal_server_handler.Transaction' produces: - application/json responses: "200": description: Transaction hash and tick schema: - $ref: '#/definitions/handler.PostTransactionResponse' + $ref: '#/definitions/cardinal_server_handler.PostTransactionResponse' "400": description: Invalid request parameter schema: @@ -365,7 +329,7 @@ paths: "200": description: Details of the game world schema: - $ref: '#/definitions/handler.GetWorldResponse' + $ref: '#/definitions/cardinal_server_handler.GetWorldResponse' "400": description: Invalid request parameters schema: diff --git a/cardinal/server/event_test.go b/cardinal/server/event_test.go index 7906c36e6..acdcaa6af 100644 --- a/cardinal/server/event_test.go +++ b/cardinal/server/event_test.go @@ -12,7 +12,6 @@ import ( "pkg.world.dev/world-engine/assert" "pkg.world.dev/world-engine/cardinal" "pkg.world.dev/world-engine/cardinal/testutils" - "pkg.world.dev/world-engine/cardinal/types/engine" ) type SendEnergyTx struct { @@ -42,31 +41,31 @@ func TestEventsThroughSystems(t *testing.T) { counter1 := atomic.Int32{} counter1.Store(0) event := map[string]any{"message": "test"} - sys1 := func(wCtx engine.Context) error { + sys1 := func(wCtx cardinal.WorldContext) error { err := wCtx.EmitEvent(event) assert.Check(t, err == nil, "emit event encountered error is system 1: %v", err) counter1.Add(1) return nil } - sys2 := func(wCtx engine.Context) error { + sys2 := func(wCtx cardinal.WorldContext) error { err := wCtx.EmitEvent(event) assert.Check(t, err == nil, "emit event encountered error is system 2: %v", err) counter1.Add(1) return nil } - sys3 := func(wCtx engine.Context) error { + sys3 := func(wCtx cardinal.WorldContext) error { err := wCtx.EmitEvent(event) assert.Check(t, err == nil, "emit event encountered error is system 3: %v", err) counter1.Add(1) return nil } - sys4 := func(wCtx engine.Context) error { + sys4 := func(wCtx cardinal.WorldContext) error { err := wCtx.EmitEvent(event) assert.Check(t, err == nil, "emit event encountered error is system 4: %v", err) counter1.Add(1) return nil } - sys5 := func(wCtx engine.Context) error { + sys5 := func(wCtx cardinal.WorldContext) error { err := wCtx.EmitEvent(event) assert.Check(t, err == nil, "emit event encountered error is system 5: %v", err) counter1.Add(1) diff --git a/cardinal/server/handler/cql.go b/cardinal/server/handler/cql.go index e9d502cc4..905d50c22 100644 --- a/cardinal/server/handler/cql.go +++ b/cardinal/server/handler/cql.go @@ -3,10 +3,8 @@ package handler import "C" import ( - "github.com/goccy/go-json" "github.com/gofiber/fiber/v2" - "pkg.world.dev/world-engine/cardinal/server/handler/cql" servertypes "pkg.world.dev/world-engine/cardinal/server/types" "pkg.world.dev/world-engine/cardinal/types" ) @@ -15,13 +13,8 @@ type CQLQueryRequest struct { CQL string } -type cqlData struct { - ID types.EntityID `json:"id"` - Data []json.RawMessage `json:"data" swaggertype:"object"` -} - type CQLQueryResponse struct { - Results []cqlData `json:"results"` + Results []types.EntityStateElement `json:"results"` } // PostCQL godoc @@ -34,62 +27,17 @@ type CQLQueryResponse struct { // @Success 200 {object} CQLQueryResponse "Results of the executed CQL query" // @Failure 400 {string} string "Invalid request parameters" // @Router /cql [post] -func PostCQL(provider servertypes.Provider) func(*fiber.Ctx) error { //nolint:gocognit // to refactor later +func PostCQL( + world servertypes.ProviderWorld) func(*fiber.Ctx) error { return func(ctx *fiber.Ctx) error { req := new(CQLQueryRequest) if err := ctx.BodyParser(req); err != nil { return err } - - // getComponentByName is a wrapper function that casts component.ComponentMetadata from ctx.GetComponentByName - // to types.Component - getComponentByName := func(name string) (types.Component, error) { - comp, err := provider.GetComponentByName(name) - if err != nil { - return nil, err - } - return comp, nil - } - - // Parse the CQL string into a filter - resultFilter, err := cql.Parse(req.CQL, getComponentByName) + result, err := world.EvaluateCQL(req.CQL) if err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } - - result := make([]cqlData, 0) - var eachError error - searchErr := provider.Search(resultFilter).Each(provider.GetReadOnlyCtx(), - func(id types.EntityID) bool { - components, err := provider.StoreReader().GetComponentTypesForEntity(id) - if err != nil { - eachError = err - return false - } - resultElement := cqlData{ - ID: id, - Data: make([]json.RawMessage, 0), - } - - for _, c := range components { - data, err := provider.StoreReader().GetComponentForEntityInRawJSON(c, id) - if err != nil { - eachError = err - return false - } - resultElement.Data = append(resultElement.Data, data) - } - result = append(result, resultElement) - return true - }, - ) - if searchErr != nil { - return fiber.NewError(fiber.StatusInternalServerError, searchErr.Error()) - } - if eachError != nil { - return fiber.NewError(fiber.StatusInternalServerError, eachError.Error()) - } - return ctx.JSON(CQLQueryResponse{Results: result}) } } diff --git a/cardinal/server/handler/debug.go b/cardinal/server/handler/debug.go index 1a8288722..c12135bdc 100644 --- a/cardinal/server/handler/debug.go +++ b/cardinal/server/handler/debug.go @@ -1,64 +1,29 @@ package handler import ( - "encoding/json" - "github.com/gofiber/fiber/v2" - "pkg.world.dev/world-engine/cardinal/search/filter" servertypes "pkg.world.dev/world-engine/cardinal/server/types" "pkg.world.dev/world-engine/cardinal/types" ) type DebugStateRequest struct{} -type debugStateElement struct { - ID types.EntityID `json:"id"` - Components map[string]json.RawMessage `json:"components" swaggertype:"object"` -} - -type DebugStateResponse []debugStateElement +type DebugStateResponse = []types.EntityStateElement -// GetDebugState godoc +// GetState godoc // // @Summary Retrieves a list of all entities in the game state // @Description Retrieves a list of all entities in the game state // @Produce application/json // @Success 200 {object} DebugStateResponse "List of all entities" // @Router /debug/state [post] -func GetDebugState(provider servertypes.Provider) func(*fiber.Ctx) error { +func GetState(world servertypes.ProviderWorld) func(*fiber.Ctx) error { return func(ctx *fiber.Ctx) error { - result := make(DebugStateResponse, 0) - s := provider.Search(filter.All()) - var eachClosureErr error - searchEachErr := s.Each(provider.GetReadOnlyCtx(), - func(id types.EntityID) bool { - var components []types.ComponentMetadata - components, eachClosureErr = provider.StoreReader().GetComponentTypesForEntity(id) - if eachClosureErr != nil { - return false - } - resultElement := debugStateElement{ - ID: id, - Components: make(map[string]json.RawMessage), - } - for _, c := range components { - var data json.RawMessage - data, eachClosureErr = provider.StoreReader().GetComponentForEntityInRawJSON(c, id) - if eachClosureErr != nil { - return false - } - resultElement.Components[c.Name()] = data - } - result = append(result, resultElement) - return true - }, - ) - if eachClosureErr != nil { - return eachClosureErr - } - if searchEachErr != nil { - return searchEachErr + var result DebugStateResponse + result, err := world.GetDebugState() + if err != nil { + return err } return ctx.JSON(&result) diff --git a/cardinal/server/handler/query.go b/cardinal/server/handler/query.go index 6c12bdc0e..67920e441 100644 --- a/cardinal/server/handler/query.go +++ b/cardinal/server/handler/query.go @@ -2,8 +2,10 @@ package handler import ( "github.com/gofiber/fiber/v2" + "github.com/rotisserie/eris" - "pkg.world.dev/world-engine/cardinal/types/engine" + servertypes "pkg.world.dev/world-engine/cardinal/server/types" + "pkg.world.dev/world-engine/cardinal/types" ) // PostQuery godoc @@ -18,35 +20,15 @@ import ( // @Success 200 {object} object "Results of the executed query" // @Failure 400 {string} string "Invalid request parameters" // @Router /query/{queryGroup}/{queryName} [post] -func PostQuery(queries map[string]map[string]engine.Query, wCtx engine.Context) func(*fiber.Ctx) error { +func PostQuery(world servertypes.ProviderWorld) func(*fiber.Ctx) error { return func(ctx *fiber.Ctx) error { - query, ok := queries[ctx.Params("group")][ctx.Params("name")] - if !ok { - return fiber.NewError(fiber.StatusNotFound, "query name not found") - } - ctx.Set("Content-Type", "application/json") - resBz, err := query.HandleQueryRaw(wCtx, ctx.Body()) - if err != nil { + resBz, err := world.HandleQuery(ctx.Params("group"), ctx.Params("name"), ctx.Body()) + if eris.Is(err, types.ErrQueryNotFound) { + return fiber.NewError(fiber.StatusNotFound, "query not found") + } else if err != nil { return fiber.NewError(fiber.StatusBadRequest, "encountered an error in query: "+err.Error()) } - return ctx.Send(resBz) } } - -// NOTE: duplication for cleaner swagger docs -// PostQuery godoc -// -// @Summary Executes a query -// @Description Executes a query -// @Accept application/json -// @Produce application/json -// @Param queryName path string true "Name of a registered query" -// @Param queryBody body object true "Query to be executed" -// @Success 200 {object} object "Results of the executed query" -// @Failure 400 {string} string "Invalid request parameters" -// @Router /query/game/{queryName} [post] -func PostGameQuery(queries map[string]map[string]engine.Query, wCtx engine.Context) func(*fiber.Ctx) error { - return PostQuery(queries, wCtx) -} diff --git a/cardinal/server/handler/receipts.go b/cardinal/server/handler/receipts.go index 14dfb6d49..206f7521a 100644 --- a/cardinal/server/handler/receipts.go +++ b/cardinal/server/handler/receipts.go @@ -3,7 +3,7 @@ package handler import ( "github.com/gofiber/fiber/v2" - "pkg.world.dev/world-engine/cardinal/types/engine" + "pkg.world.dev/world-engine/cardinal/server/types" ) type ListTxReceiptsRequest struct { @@ -38,15 +38,15 @@ type ReceiptEntry struct { // @Success 200 {object} ListTxReceiptsResponse "List of receipts" // @Failure 400 {string} string "Invalid request body" // @Router /query/receipts/list [post] -func GetReceipts(wCtx engine.Context) func(*fiber.Ctx) error { +func GetReceipts(world types.ProviderWorld) func(*fiber.Ctx) error { return func(ctx *fiber.Ctx) error { req := new(ListTxReceiptsRequest) if err := ctx.BodyParser(req); err != nil { return err } reply := ListTxReceiptsResponse{} - reply.EndTick = wCtx.CurrentTick() - size := wCtx.ReceiptHistorySize() + reply.EndTick = world.CurrentTick() + size := world.ReceiptHistorySize() if size > reply.EndTick { reply.StartTick = 0 } else { @@ -62,7 +62,7 @@ func GetReceipts(wCtx engine.Context) func(*fiber.Ctx) error { } for t := reply.StartTick; t < reply.EndTick; t++ { - currReceipts, err := wCtx.GetTransactionReceiptsForTick(t) + currReceipts, err := world.GetTransactionReceiptsForTick(t) if err != nil || len(currReceipts) == 0 { continue } diff --git a/cardinal/server/handler/tx.go b/cardinal/server/handler/tx.go index 1a63d640a..a2837aabc 100644 --- a/cardinal/server/handler/tx.go +++ b/cardinal/server/handler/tx.go @@ -41,7 +41,7 @@ type Transaction = sign.Transaction // @Failure 400 {string} string "Invalid request parameter" // @Router /tx/{txGroup}/{txName} [post] func PostTransaction( - provider servertypes.Provider, msgs map[string]map[string]types.Message, disableSigVerification bool, + world servertypes.ProviderWorld, msgs map[string]map[string]types.Message, disableSigVerification bool, ) func(*fiber.Ctx) error { return func(ctx *fiber.Ctx) error { msgType, ok := msgs[ctx.Params("group")][ctx.Params("name")] @@ -75,14 +75,14 @@ func PostTransaction( signerAddress = createPersonaMsg.SignerAddress } - if err = lookupSignerAndValidateSignature(provider, signerAddress, tx); err != nil { + if err = lookupSignerAndValidateSignature(world, signerAddress, tx); err != nil { return err } } // Add the transaction to the engine // TODO(scott): this should just deal with txpool instead of having to go through engine - tick, hash := provider.AddTransaction(msgType.ID(), msg, tx) + tick, hash := world.AddTransaction(msgType.ID(), msg, tx) return ctx.JSON(&PostTransactionResponse{ TxHash: string(hash), @@ -104,9 +104,9 @@ func PostTransaction( // @Failure 400 {string} string "Invalid request parameter" // @Router /tx/game/{txName} [post] func PostGameTransaction( - provider servertypes.Provider, msgs map[string]map[string]types.Message, disableSigVerification bool, + world servertypes.ProviderWorld, msgs map[string]map[string]types.Message, disableSigVerification bool, ) func(*fiber.Ctx) error { - return PostTransaction(provider, msgs, disableSigVerification) + return PostTransaction(world, msgs, disableSigVerification) } // NOTE: duplication for cleaner swagger docs @@ -121,26 +121,26 @@ func PostGameTransaction( // @Failure 400 {string} string "Invalid request parameter" // @Router /tx/persona/create-persona [post] func PostPersonaTransaction( - provider servertypes.Provider, msgs map[string]map[string]types.Message, disableSigVerification bool, + world servertypes.ProviderWorld, msgs map[string]map[string]types.Message, disableSigVerification bool, ) func(*fiber.Ctx) error { - return PostTransaction(provider, msgs, disableSigVerification) + return PostTransaction(world, msgs, disableSigVerification) } -func lookupSignerAndValidateSignature(provider servertypes.Provider, signerAddress string, tx *Transaction) error { +func lookupSignerAndValidateSignature(world servertypes.ProviderWorld, signerAddress string, tx *Transaction) error { var err error if signerAddress == "" { - signerAddress, err = provider.GetSignerForPersonaTag(tx.PersonaTag, 0) + signerAddress, err = world.GetSignerForPersonaTag(tx.PersonaTag, 0) if err != nil { return fiber.NewError(fiber.StatusBadRequest, "could not get signer for persona: "+err.Error()) } } - if err = validateSignature(tx, signerAddress, provider.Namespace(), + if err = validateSignature(tx, signerAddress, world.Namespace(), tx.IsSystemTransaction()); err != nil { return fiber.NewError(fiber.StatusBadRequest, "failed to validate transaction: "+err.Error()) } // TODO(scott): this should be refactored; it should be the responsibility of the engine tx processor // to mark the nonce as used once it's included in the tick, not the server. - if err = provider.UseNonce(signerAddress, tx.Nonce); err != nil { + if err = world.UseNonce(signerAddress, tx.Nonce); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "failed to use nonce: "+err.Error()) } return nil diff --git a/cardinal/server/handler/world.go b/cardinal/server/handler/world.go index 48d05605e..2b9f51b5e 100644 --- a/cardinal/server/handler/world.go +++ b/cardinal/server/handler/world.go @@ -5,22 +5,18 @@ import ( "github.com/gofiber/fiber/v2" + servertypes "pkg.world.dev/world-engine/cardinal/server/types" "pkg.world.dev/world-engine/cardinal/server/utils" "pkg.world.dev/world-engine/cardinal/types" - "pkg.world.dev/world-engine/cardinal/types/engine" ) +// GetWorldResponse is a type representing the json super structure that contains +// all info about the world. type GetWorldResponse struct { - Namespace string `json:"namespace"` - Components []FieldDetail `json:"components"` // list of component names - Messages []FieldDetail `json:"messages"` - Queries []FieldDetail `json:"queries"` -} - -type FieldDetail struct { - Name string `json:"name"` // name of the message or query - Fields map[string]any `json:"fields"` // variable name and type - URL string `json:"url,omitempty"` + Namespace string `json:"namespace"` + Components []types.FieldDetail `json:"components"` // list of component names + Messages []types.FieldDetail `json:"messages"` + Queries []types.FieldDetail `json:"queries"` } // GetWorld godoc @@ -33,24 +29,26 @@ type FieldDetail struct { // @Failure 400 {string} string "Invalid request parameters" // @Router /world [get] func GetWorld( - components []types.ComponentMetadata, messages []types.Message, - queries []engine.Query, namespace string, + world servertypes.ProviderWorld, + components []types.ComponentMetadata, + messages []types.Message, + namespace string, ) func(*fiber.Ctx) error { // Collecting name of all registered components - comps := make([]FieldDetail, 0, len(components)) + comps := make([]types.FieldDetail, 0, len(components)) for _, component := range components { c, _ := component.Decode(component.GetSchema()) - comps = append(comps, FieldDetail{ + comps = append(comps, types.FieldDetail{ Name: component.Name(), Fields: types.GetFieldInformation(reflect.TypeOf(c)), }) } // Collecting the structure of all messages - messagesFields := make([]FieldDetail, 0, len(messages)) + messagesFields := make([]types.FieldDetail, 0, len(messages)) for _, message := range messages { // Extracting the fields of the message - messagesFields = append(messagesFields, FieldDetail{ + messagesFields = append(messagesFields, types.FieldDetail{ Name: message.Name(), Fields: message.GetInFieldInformation(), URL: utils.GetTxURL(message.Group(), message.Name()), @@ -58,22 +56,13 @@ func GetWorld( } // Collecting the structure of all queries - queriesFields := make([]FieldDetail, 0, len(queries)) - for _, query := range queries { - // Extracting the fields of the query - queriesFields = append(queriesFields, FieldDetail{ - Name: query.Name(), - Fields: query.GetRequestFieldInformation(), - URL: utils.GetQueryURL(query.Group(), query.Name()), - }) - } return func(ctx *fiber.Ctx) error { return ctx.JSON(GetWorldResponse{ Namespace: namespace, Components: comps, Messages: messagesFields, - Queries: queriesFields, + Queries: world.BuildQueryFields(), }) } } diff --git a/cardinal/server/server.go b/cardinal/server/server.go index 3f8df9328..5b5236577 100644 --- a/cardinal/server/server.go +++ b/cardinal/server/server.go @@ -14,7 +14,6 @@ import ( "pkg.world.dev/world-engine/cardinal/server/handler" servertypes "pkg.world.dev/world-engine/cardinal/server/types" "pkg.world.dev/world-engine/cardinal/types" - "pkg.world.dev/world-engine/cardinal/types/engine" _ "pkg.world.dev/world-engine/cardinal/server/docs" // for swagger. ) @@ -36,8 +35,10 @@ type Server struct { // New returns an HTTP server with handlers for all QueryTypes and MessageTypes. func New( - provider servertypes.Provider, wCtx engine.Context, components []types.ComponentMetadata, - messages []types.Message, queries []engine.Query, opts ...Option, + world servertypes.ProviderWorld, + components []types.ComponentMetadata, + messages []types.Message, + opts ...Option, ) (*Server, error) { app := fiber.New(fiber.Config{ Network: "tcp", // Enable server listening on both ipv4 & ipv6 (default: ipv4 only) @@ -59,7 +60,7 @@ func New( app.Use(cors.New()) // Register routes - s.setupRoutes(provider, wCtx, messages, queries, components) + s.setupRoutes(world, messages, components) return s, nil } @@ -116,28 +117,14 @@ func (s *Server) Shutdown() error { // @consumes application/json // @produces application/json func (s *Server) setupRoutes( - provider servertypes.Provider, wCtx engine.Context, messages []types.Message, - queries []engine.Query, components []types.ComponentMetadata, + world servertypes.ProviderWorld, + messages []types.Message, + components []types.ComponentMetadata, ) { - // TODO(scott): we should refactor this such that we only dependency inject these maps - // instead of having to dependency inject the entire engine. - // /query/:group/:queryType - // maps group -> queryType -> query - queryIndex := make(map[string]map[string]engine.Query) - // /tx/:group/:txType // maps group -> txType -> tx msgIndex := make(map[string]map[string]types.Message) - // Create query index - for _, query := range queries { - // Initialize inner map if it doesn't exist - if _, ok := queryIndex[query.Group()]; !ok { - queryIndex[query.Group()] = make(map[string]engine.Query) - } - queryIndex[query.Group()][query.Name()] = query - } - // Create tx index for _, msg := range messages { // Initialize inner map if it doesn't exist @@ -157,23 +144,23 @@ func (s *Server) setupRoutes( s.app.Get("/events", handler.WebSocketEvents()) // Route: /world - s.app.Get("/world", handler.GetWorld(components, messages, queries, wCtx.Namespace())) + s.app.Get("/world", handler.GetWorld(world, components, messages, world.Namespace())) // Route: /... s.app.Get("/health", handler.GetHealth()) // Route: /query/... query := s.app.Group("/query") - query.Post("/receipts/list", handler.GetReceipts(wCtx)) - query.Post("/:group/:name", handler.PostQuery(queryIndex, wCtx)) + query.Post("/receipts/list", handler.GetReceipts(world)) + query.Post("/:group/:name", handler.PostQuery(world)) // Route: /tx/... tx := s.app.Group("/tx") - tx.Post("/:group/:name", handler.PostTransaction(provider, msgIndex, s.config.isSignatureVerificationDisabled)) + tx.Post("/:group/:name", handler.PostTransaction(world, msgIndex, s.config.isSignatureVerificationDisabled)) // Route: /cql - s.app.Post("/cql", handler.PostCQL(provider)) + s.app.Post("/cql", handler.PostCQL(world)) // Route: /debug/state - s.app.Post("/debug/state", handler.GetDebugState(provider)) + s.app.Post("/debug/state", handler.GetState(world)) } diff --git a/cardinal/server/server_test.go b/cardinal/server/server_test.go index 5927736be..b934e645e 100644 --- a/cardinal/server/server_test.go +++ b/cardinal/server/server_test.go @@ -24,7 +24,6 @@ import ( "pkg.world.dev/world-engine/cardinal/server/utils" "pkg.world.dev/world-engine/cardinal/testutils" "pkg.world.dev/world-engine/cardinal/types" - "pkg.world.dev/world-engine/cardinal/types/engine" "pkg.world.dev/world-engine/sign" ) @@ -109,17 +108,17 @@ func (s *ServerTestSuite) TestGetWorld() { // check that the component, message, query name are in the list for _, comp := range comps { - assert.True(s.T(), slices.ContainsFunc(result.Components, func(field handler.FieldDetail) bool { + assert.True(s.T(), slices.ContainsFunc(result.Components, func(field types.FieldDetail) bool { return comp.Name() == field.Name })) } for _, msg := range msgs { - assert.True(s.T(), slices.ContainsFunc(result.Messages, func(field handler.FieldDetail) bool { + assert.True(s.T(), slices.ContainsFunc(result.Messages, func(field types.FieldDetail) bool { return msg.Name() == field.Name })) } for _, query := range queries { - assert.True(s.T(), slices.ContainsFunc(result.Queries, func(field handler.FieldDetail) bool { + assert.True(s.T(), slices.ContainsFunc(result.Queries, func(field types.FieldDetail) bool { return query.Name() == field.Name })) } @@ -203,7 +202,7 @@ func (s *ServerTestSuite) TestQueryCustomGroup() { err := cardinal.RegisterQuery[SomeRequest, SomeResponse]( s.world, name, - func(_ engine.Context, _ *SomeRequest) (*SomeResponse, error) { + func(_ cardinal.WorldContext, _ *SomeRequest) (*SomeResponse, error) { called = true return &SomeResponse{}, nil }, @@ -279,7 +278,7 @@ func (s *ServerTestSuite) setupWorld(opts ...cardinal.WorldOption) { err = cardinal.RegisterMessage[MoveMsgInput, MoveMessageOutput](s.world, moveMsgName) s.Require().NoError(err) personaToPosition := make(map[string]types.EntityID) - err = cardinal.RegisterSystems(s.world, func(context engine.Context) error { + err = cardinal.RegisterSystems(s.world, func(context cardinal.WorldContext) error { return cardinal.EachMessage[MoveMsgInput, MoveMessageOutput](context, func(tx cardinal.TxData[MoveMsgInput]) (MoveMessageOutput, error) { posID, exists := personaToPosition[tx.Tx.PersonaTag] @@ -313,7 +312,7 @@ func (s *ServerTestSuite) setupWorld(opts ...cardinal.WorldOption) { err = cardinal.RegisterQuery[QueryLocationRequest, QueryLocationResponse]( s.world, "location", - func(wCtx engine.Context, req *QueryLocationRequest) (*QueryLocationResponse, error) { + func(wCtx cardinal.WorldContext, req *QueryLocationRequest) (*QueryLocationResponse, error) { locID, exists := personaToPosition[req.Persona] if !exists { return nil, fmt.Errorf("location for %q does not exists", req.Persona) diff --git a/cardinal/server/types/provider.go b/cardinal/server/types/provider.go index 6345f1bf8..a6587057a 100644 --- a/cardinal/server/types/provider.go +++ b/cardinal/server/types/provider.go @@ -2,20 +2,23 @@ package types import ( "pkg.world.dev/world-engine/cardinal/gamestate" - "pkg.world.dev/world-engine/cardinal/search" - "pkg.world.dev/world-engine/cardinal/search/filter" + "pkg.world.dev/world-engine/cardinal/receipt" "pkg.world.dev/world-engine/cardinal/types" - "pkg.world.dev/world-engine/cardinal/types/engine" "pkg.world.dev/world-engine/sign" ) -type Provider interface { +type ProviderWorld interface { UseNonce(signerAddress string, nonce uint64) error GetSignerForPersonaTag(personaTag string, tick uint64) (addr string, err error) AddTransaction(id types.MessageID, v any, sig *sign.Transaction) (uint64, types.TxHash) Namespace() string GetComponentByName(name string) (types.ComponentMetadata, error) - Search(filter filter.ComponentFilter) search.EntitySearch StoreReader() gamestate.Reader - GetReadOnlyCtx() engine.Context + HandleQuery(group string, name string, bz []byte) ([]byte, error) + CurrentTick() uint64 + ReceiptHistorySize() uint64 + GetTransactionReceiptsForTick(tick uint64) ([]receipt.Receipt, error) + EvaluateCQL(cql string) ([]types.EntityStateElement, error) + GetDebugState() ([]types.EntityStateElement, error) + BuildQueryFields() []types.FieldDetail } diff --git a/cardinal/state_test.go b/cardinal/state_test.go index 141b9f971..80e5a42fb 100644 --- a/cardinal/state_test.go +++ b/cardinal/state_test.go @@ -11,7 +11,6 @@ import ( "pkg.world.dev/world-engine/cardinal/search/filter" "pkg.world.dev/world-engine/cardinal/testutils" "pkg.world.dev/world-engine/cardinal/types" - "pkg.world.dev/world-engine/cardinal/types/engine" ) // comps reduces the typing needed to create a slice of IComponentTypes @@ -232,7 +231,7 @@ func TestCanReloadState(t *testing.T) { err := cardinal.RegisterSystems( world1, - func(wCtx engine.Context) error { + func(wCtx cardinal.WorldContext) error { q := cardinal.NewSearch().Entity(filter.Contains(filter.Component[oneAlphaNumComp]())) assert.NilError( t, q.Each(wCtx, @@ -309,8 +308,8 @@ func TestCanFindTransactionsAfterReloadingEngine(t *testing.T) { assert.NilError(t, cardinal.RegisterMessage[Msg, Result](world, msgName)) err := cardinal.RegisterSystems( world, - func(wCtx engine.Context) error { - someTx, err := testutils.GetMessage[Msg, Result](wCtx) + func(wCtx cardinal.WorldContext) error { + someTx, err := cardinal.GetMessage[Msg, Result](wCtx) return cardinal.EachMessage[Msg, Result](wCtx, func(tx cardinal.TxData[Msg]) (Result, error) { someTx.SetResult(wCtx, tx.Hash, Result{}) return Result{}, err diff --git a/cardinal/system.go b/cardinal/system.go new file mode 100644 index 000000000..962c0c37e --- /dev/null +++ b/cardinal/system.go @@ -0,0 +1,156 @@ +package cardinal + +import ( + "path/filepath" + "reflect" + "runtime" + "slices" + "time" + + "github.com/rotisserie/eris" + + "pkg.world.dev/world-engine/cardinal/statsd" +) + +const ( + noActiveSystemName = "" +) + +var _ SystemManager = &systemManager{} + +// System is a user-defined function that is executed at every tick. +type System func(ctx WorldContext) error + +// systemType is an internal entry used to track registered systems. +type systemType struct { + Name string + Fn System +} + +type SystemManager interface { + // GetRegisteredSystems returns a slice of all registered systems' name. + GetRegisteredSystems() []string + + // GetCurrentSystem returns the name of the currently running system. + // If no system is currently running, it returns an empty string. + GetCurrentSystem() string + + // These methods are intentionally made private to avoid other + // packages from trying to modify the system manager in the middle of a tick. + registerSystems(isInit bool, systems ...System) error + runSystems(wCtx WorldContext) error +} + +type systemManager struct { + // Registered systems in the order that they were registered. + // This is represented as a list as maps in Go are unordered. + registeredSystems []systemType + registeredInitSystems []systemType + + // currentSystem is the name of the system that is currently running. + currentSystem string +} + +func newSystemManager() SystemManager { + var sm SystemManager = &systemManager{ + registeredSystems: make([]systemType, 0), + registeredInitSystems: make([]systemType, 0), + currentSystem: noActiveSystemName, + } + return sm +} + +// RegisterSystems registers multiple systems with the system manager. +// There can only be one system with a given name, which is derived from the function name. +// If isInit is true, the system will only be executed once at tick 0. +// If there is a duplicate system name, an error will be returned and none of the systems will be registered. +func (m *systemManager) registerSystems(isInit bool, systemFuncs ...System) error { + // We create a list of systemType structs to register, and then register them in one go to ensure all or nothing. + systemToRegister := make([]systemType, 0, len(systemFuncs)) + + // Iterate throughs systemFuncs, + // 1) Ensure that there is no duplicate system + // 2) Create a new system entry for each one. + for _, systemFunc := range systemFuncs { + // Obtain the name of the system function using reflection. + systemName := filepath.Base(runtime.FuncForPC(reflect.ValueOf(systemFunc).Pointer()).Name()) + + // Check for duplicate system names within the list of systems to be registered + if slices.ContainsFunc( + systemToRegister, + func(s systemType) bool { return s.Name == systemName }, + ) { + return eris.Errorf("duplicate system %q in slice", systemName) + } + + // Checks if the system is already previously registered. + // This will terminate the registration of all systems if any of them are already registered. + if slices.ContainsFunc( + slices.Concat(m.registeredSystems, m.registeredInitSystems), + func(s systemType) bool { return s.Name == systemName }, + ) { + return eris.Errorf("System %q is already registered", systemName) + } + + systemToRegister = append(systemToRegister, systemType{Name: systemName, Fn: systemFunc}) + } + + if isInit { + m.registeredInitSystems = append(m.registeredInitSystems, systemToRegister...) + } else { + m.registeredSystems = append(m.registeredSystems, systemToRegister...) + } + + return nil +} + +// RunSystems runs all the registered system in the order that they were registered. +func (m *systemManager) runSystems(wCtx WorldContext) error { + var systemsToRun []systemType + if wCtx.CurrentTick() == 0 { + systemsToRun = slices.Concat(m.registeredInitSystems, m.registeredSystems) + } else { + systemsToRun = m.registeredSystems + } + + allSystemStartTime := time.Now() + for _, sys := range systemsToRun { + // Explicit memory aliasing + m.currentSystem = sys.Name + + // Inject the system name into the logger + wCtx.setLogger(wCtx.Logger().With().Str("system", sys.Name).Logger()) + + // Executes the system function that the user registered + systemStartTime := time.Now() + err := sys.Fn(wCtx) + if err != nil { + m.currentSystem = "" + return eris.Wrapf(err, "System %s generated an error", sys.Name) + } + + // Emit the total time it took to run `systemName` + statsd.EmitTickStat(systemStartTime, sys.Name) + } + + // Indicate that no system is currently running + m.currentSystem = noActiveSystemName + + // Emit the total time it took to run all systems + statsd.EmitTickStat(allSystemStartTime, "all_systems") + + return nil +} + +func (m *systemManager) GetRegisteredSystems() []string { + sys := slices.Concat(m.registeredInitSystems, m.registeredSystems) + sysNames := make([]string, len(sys)) + for i, sys := range slices.Concat(m.registeredInitSystems, m.registeredSystems) { + sysNames[i] = sys.Name + } + return sysNames +} + +func (m *systemManager) GetCurrentSystem() string { + return m.currentSystem +} diff --git a/cardinal/system/manager.go b/cardinal/system/manager.go deleted file mode 100644 index 53a8f9085..000000000 --- a/cardinal/system/manager.go +++ /dev/null @@ -1,149 +0,0 @@ -package system - -import ( - "fmt" - "path/filepath" - "reflect" - "runtime" - "slices" - "time" - - "github.com/rotisserie/eris" - - "pkg.world.dev/world-engine/cardinal/statsd" - "pkg.world.dev/world-engine/cardinal/types/engine" -) - -type Manager struct { - // registeredSystems is a list of all the registered system names in the order that they were registered. - // This is represented as a list as maps in Go are unordered. - registeredSystems []string - - // registeredInitSystems is a list of all the registered init system names in the order that they were registered. - // This is represented as a list as maps in Go are unordered. - registeredInitSystems []string - - // systemFn is a map of system names to system functions. - systemFn map[string]System - - // currentSystem is the name of the system that is currently running. - currentSystem *string -} - -// NewManager creates a new system manager. -func NewManager() *Manager { - return &Manager{ - registeredSystems: make([]string, 0), - systemFn: make(map[string]System), - currentSystem: nil, - } -} - -// RegisterSystems registers multiple systems with the system manager. -// There can only be one system with a given name, which is derived from the function name. -// If there is a duplicate system name, an error will be returned and none of the systems will be registered. -func (m *Manager) RegisterSystems(systems ...System) error { - return m.registerSystems(false, systems...) -} - -// RegisterInitSystems registers multiple init systems that is only executed once at tick 0 with the system manager. -// There can only be one system with a given name, which is derived from the function name. -// If there is a duplicate system name, an error will be returned and none of the systems will be registered. -func (m *Manager) RegisterInitSystems(systems ...System) error { - return m.registerSystems(true, systems...) -} - -func (m *Manager) registerSystems(isInit bool, systems ...System) error { - // Iterate through all the systems and check if they are already registered. - // This is done before registering any of the systems to ensure that all are registered or none of them are. - systemNames := make([]string, 0, len(systems)) - for _, sys := range systems { - // Obtain the name of the system function using reflection. - systemName := filepath.Base(runtime.FuncForPC(reflect.ValueOf(sys).Pointer()).Name()) - - // Check for duplicate system names within the list of systems to be registered - if slices.Contains(systemNames, systemName) { - return eris.Errorf("duplicate system %q in slice", systemName) - } - - // Checks if the system is already previously registered. - // This will terminate the registration of all systems if any of them are already registered. - if err := m.isSystemNameUnique(systemName); err != nil { - return err - } - - // If the system is not already registered, add it to the list of system names. - systemNames = append(systemNames, systemName) - } - - // Iterate through all the systems and register them one by one. - for i, systemName := range systemNames { - if isInit { - m.registeredInitSystems = append(m.registeredInitSystems, systemName) - } else { - m.registeredSystems = append(m.registeredSystems, systemName) - } - m.systemFn[systemName] = systems[i] - } - - return nil -} - -// RunSystems runs all the registered system in the order that they were registered. -func (m *Manager) RunSystems(wCtx engine.Context) error { - var systemsToRun []string - if wCtx.CurrentTick() == 0 { - //nolint:gocritic // We need to use the append function to concat - systemsToRun = append(m.registeredInitSystems, m.registeredSystems...) - } else { - systemsToRun = m.registeredSystems - } - - allSystemStartTime := time.Now() - for _, systemName := range systemsToRun { - // Explicit memory aliasing - sysName := systemName - m.currentSystem = &sysName - - // Inject the system name into the logger - wCtx.SetLogger(wCtx.Logger().With().Str("system", systemName).Logger()) - - // Executes the system function that the user registered - systemStartTime := time.Now() - err := m.systemFn[systemName](wCtx) - if err != nil { - m.currentSystem = nil - return eris.Wrapf(err, "system %s generated an error", systemName) - } - - // Emit the total time it took to run `systemName` - statsd.EmitTickStat(systemStartTime, systemName) - } - - // Set the current system to nil to indicate that no system is currently running - m.currentSystem = nil - - // Emit the total time it took to run all systems - statsd.EmitTickStat(allSystemStartTime, "all_systems") - - return nil -} - -func (m *Manager) GetRegisteredSystemNames() []string { - return m.registeredSystems -} - -func (m *Manager) GetCurrentSystem() string { - if m.currentSystem == nil { - return "no_system" - } - return *m.currentSystem -} - -// isSystemNameUnique checks if the system name already exists in the system map -func (m *Manager) isSystemNameUnique(systemName string) error { - if _, ok := m.systemFn[systemName]; ok { - return fmt.Errorf("system %q is already registered", systemName) - } - return nil -} diff --git a/cardinal/system/system.go b/cardinal/system/system.go deleted file mode 100644 index 52930d847..000000000 --- a/cardinal/system/system.go +++ /dev/null @@ -1,7 +0,0 @@ -package system - -import ( - "pkg.world.dev/world-engine/cardinal/types/engine" -) - -type System func(ctx engine.Context) error diff --git a/cardinal/system/system_test.go b/cardinal/system_test.go similarity index 84% rename from cardinal/system/system_test.go rename to cardinal/system_test.go index 2332a0542..69de0a7e2 100644 --- a/cardinal/system/system_test.go +++ b/cardinal/system_test.go @@ -1,4 +1,4 @@ -package system_test +package cardinal_test import ( "errors" @@ -9,16 +9,9 @@ import ( "pkg.world.dev/world-engine/cardinal/search/filter" "pkg.world.dev/world-engine/cardinal/testutils" "pkg.world.dev/world-engine/cardinal/types" - "pkg.world.dev/world-engine/cardinal/types/engine" ) -type Health struct { - Value int -} - -func (Health) Name() string { return "health" } - -func HealthSystem(wCtx engine.Context) error { +func HealthSystem(wCtx cardinal.WorldContext) error { var errs []error errs = append(errs, cardinal.NewSearch().Entity(filter. Exact(filter.Component[Health]())). @@ -73,11 +66,11 @@ func TestCanRegisterMultipleSystem(t *testing.T) { var firstSystemCalled bool var secondSystemCalled bool - firstSystem := func(engine.Context) error { + firstSystem := func(cardinal.WorldContext) error { firstSystemCalled = true return nil } - secondSystem := func(engine.Context) error { + secondSystem := func(cardinal.WorldContext) error { secondSystemCalled = true return nil } @@ -96,10 +89,10 @@ func TestInitSystemRunsOnce(t *testing.T) { w := tf.World count := 0 count2 := 0 - err := cardinal.RegisterInitSystems(w, func(_ engine.Context) error { + err := cardinal.RegisterInitSystems(w, func(_ cardinal.WorldContext) error { count++ return nil - }, func(_ engine.Context) error { + }, func(_ cardinal.WorldContext) error { count2 += 2 return nil }) diff --git a/cardinal/testutils/testutils.go b/cardinal/testutils/testutils.go index 45691c18d..5f0d91e82 100644 --- a/cardinal/testutils/testutils.go +++ b/cardinal/testutils/testutils.go @@ -2,16 +2,11 @@ package testutils import ( "crypto/ecdsa" - "reflect" "testing" "time" "github.com/ethereum/go-ethereum/crypto" - "github.com/rotisserie/eris" - "pkg.world.dev/world-engine/cardinal" - "pkg.world.dev/world-engine/cardinal/types" - "pkg.world.dev/world-engine/cardinal/types/engine" "pkg.world.dev/world-engine/sign" ) @@ -60,18 +55,3 @@ func UniqueSignatureWithName(name string) *sign.Transaction { func UniqueSignature() *sign.Transaction { return UniqueSignatureWithName("some_persona_tag") } - -func GetMessage[In any, Out any](wCtx engine.Context) (*cardinal.MessageType[In, Out], error) { - var msg cardinal.MessageType[In, Out] - msgType := reflect.TypeOf(msg) - tempRes, ok := wCtx.GetMessageByType(msgType) - if !ok { - return nil, eris.Errorf("Could not find %q, Message may not be registered.", msg.Name()) - } - var _ types.Message = &msg - res, ok := tempRes.(*cardinal.MessageType[In, Out]) - if !ok { - return &msg, eris.New("wrong type") - } - return res, nil -} diff --git a/cardinal/types/engine/context.go b/cardinal/types/engine/context.go deleted file mode 100644 index 07a9db9a5..000000000 --- a/cardinal/types/engine/context.go +++ /dev/null @@ -1,49 +0,0 @@ -package engine - -import ( - "reflect" - - "github.com/rs/zerolog" - - "pkg.world.dev/world-engine/cardinal/gamestate" - "pkg.world.dev/world-engine/cardinal/receipt" - "pkg.world.dev/world-engine/cardinal/types" - "pkg.world.dev/world-engine/cardinal/types/txpool" - "pkg.world.dev/world-engine/sign" -) - -//go:generate mockgen -source=context.go -package mocks -destination=mocks/context.go -type Context interface { - // Timestamp returns the UNIX timestamp of the tick. - Timestamp() uint64 - // CurrentTick returns the current tick. - CurrentTick() uint64 - // Logger returns the logger that can be used to log messages from within system or query. - Logger() *zerolog.Logger - // EmitEvent emits an event that will be broadcast to all websocket subscribers. - EmitEvent(map[string]any) error - // EmitStringEvent emits a string event that will be broadcast to all websocket subscribers. - // This method is provided for backwards compatability. EmitEvent should be used for most cases. - EmitStringEvent(string) error - // Namespace returns the namespace of the world. - Namespace() string - - // For internal use. - - // SetLogger is used to inject a new logger configuration to an engine context that is already created. - SetLogger(logger zerolog.Logger) - AddMessageError(id types.TxHash, err error) - SetMessageResult(id types.TxHash, a any) - GetComponentByName(name string) (types.ComponentMetadata, error) - GetMessageByType(mType reflect.Type) (types.Message, bool) - GetTransactionReceipt(id types.TxHash) (any, []error, bool) - GetSignerForPersonaTag(personaTag string, tick uint64) (addr string, err error) - GetTransactionReceiptsForTick(tick uint64) ([]receipt.Receipt, error) - ReceiptHistorySize() uint64 - AddTransaction(id types.MessageID, v any, sig *sign.Transaction) (uint64, types.TxHash) - IsWorldReady() bool - StoreReader() gamestate.Reader - StoreManager() gamestate.Manager - GetTxPool() *txpool.TxPool - IsReadOnly() bool -} diff --git a/cardinal/types/engine/mocks/context.go b/cardinal/types/engine/mocks/context.go index c744454ae..6ed32f414 100644 --- a/cardinal/types/engine/mocks/context.go +++ b/cardinal/types/engine/mocks/context.go @@ -42,19 +42,19 @@ func (m *MockContext) EXPECT() *MockContextMockRecorder { // AddMessageError mocks base method. func (m *MockContext) AddMessageError(id types.TxHash, err error) { m.ctrl.T.Helper() - m.ctrl.Call(m, "AddMessageError", id, err) + m.ctrl.Call(m, "addMessageError", id, err) } // AddMessageError indicates an expected call of AddMessageError. func (mr *MockContextMockRecorder) AddMessageError(id, err interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddMessageError", reflect.TypeOf((*MockContext)(nil).AddMessageError), id, err) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "addMessageError", reflect.TypeOf((*MockContext)(nil).AddMessageError), id, err) } // AddTransaction mocks base method. func (m *MockContext) AddTransaction(id types.MessageID, v any, sig *sign.Transaction) (uint64, types.TxHash) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddTransaction", id, v, sig) + ret := m.ctrl.Call(m, "addTransaction", id, v, sig) ret0, _ := ret[0].(uint64) ret1, _ := ret[1].(types.TxHash) return ret0, ret1 @@ -63,7 +63,7 @@ func (m *MockContext) AddTransaction(id types.MessageID, v any, sig *sign.Transa // AddTransaction indicates an expected call of AddTransaction. func (mr *MockContextMockRecorder) AddTransaction(id, v, sig interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddTransaction", reflect.TypeOf((*MockContext)(nil).AddTransaction), id, v, sig) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "addTransaction", reflect.TypeOf((*MockContext)(nil).AddTransaction), id, v, sig) } // CurrentTick mocks base method. @@ -111,7 +111,7 @@ func (mr *MockContextMockRecorder) EmitStringEvent(arg0 interface{}) *gomock.Cal // GetComponentByName mocks base method. func (m *MockContext) GetComponentByName(name string) (types.ComponentMetadata, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetComponentByName", name) + ret := m.ctrl.Call(m, "getComponentByName", name) ret0, _ := ret[0].(types.ComponentMetadata) ret1, _ := ret[1].(error) return ret0, ret1 @@ -120,13 +120,13 @@ func (m *MockContext) GetComponentByName(name string) (types.ComponentMetadata, // GetComponentByName indicates an expected call of GetComponentByName. func (mr *MockContextMockRecorder) GetComponentByName(name interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetComponentByName", reflect.TypeOf((*MockContext)(nil).GetComponentByName), name) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "getComponentByName", reflect.TypeOf((*MockContext)(nil).GetComponentByName), name) } // GetMessageByType mocks base method. func (m *MockContext) GetMessageByType(mType reflect.Type) (types.Message, bool) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetMessageByType", mType) + ret := m.ctrl.Call(m, "getMessageByType", mType) ret0, _ := ret[0].(types.Message) ret1, _ := ret[1].(bool) return ret0, ret1 @@ -135,13 +135,13 @@ func (m *MockContext) GetMessageByType(mType reflect.Type) (types.Message, bool) // GetMessageByType indicates an expected call of GetMessageByType. func (mr *MockContextMockRecorder) GetMessageByType(mType interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMessageByType", reflect.TypeOf((*MockContext)(nil).GetMessageByType), mType) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "getMessageByType", reflect.TypeOf((*MockContext)(nil).GetMessageByType), mType) } // GetSignerForPersonaTag mocks base method. func (m *MockContext) GetSignerForPersonaTag(personaTag string, tick uint64) (string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetSignerForPersonaTag", personaTag, tick) + ret := m.ctrl.Call(m, "getSignerForPersonaTag", personaTag, tick) ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) return ret0, ret1 @@ -150,13 +150,13 @@ func (m *MockContext) GetSignerForPersonaTag(personaTag string, tick uint64) (st // GetSignerForPersonaTag indicates an expected call of GetSignerForPersonaTag. func (mr *MockContextMockRecorder) GetSignerForPersonaTag(personaTag, tick interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSignerForPersonaTag", reflect.TypeOf((*MockContext)(nil).GetSignerForPersonaTag), personaTag, tick) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "getSignerForPersonaTag", reflect.TypeOf((*MockContext)(nil).GetSignerForPersonaTag), personaTag, tick) } // GetTransactionReceipt mocks base method. func (m *MockContext) GetTransactionReceipt(id types.TxHash) (any, []error, bool) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTransactionReceipt", id) + ret := m.ctrl.Call(m, "getTransactionReceipt", id) ret0, _ := ret[0].(any) ret1, _ := ret[1].([]error) ret2, _ := ret[2].(bool) @@ -166,13 +166,13 @@ func (m *MockContext) GetTransactionReceipt(id types.TxHash) (any, []error, bool // GetTransactionReceipt indicates an expected call of GetTransactionReceipt. func (mr *MockContextMockRecorder) GetTransactionReceipt(id interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTransactionReceipt", reflect.TypeOf((*MockContext)(nil).GetTransactionReceipt), id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "getTransactionReceipt", reflect.TypeOf((*MockContext)(nil).GetTransactionReceipt), id) } // GetTransactionReceiptsForTick mocks base method. func (m *MockContext) GetTransactionReceiptsForTick(tick uint64) ([]receipt.Receipt, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTransactionReceiptsForTick", tick) + ret := m.ctrl.Call(m, "getTransactionReceiptsForTick", tick) ret0, _ := ret[0].([]receipt.Receipt) ret1, _ := ret[1].(error) return ret0, ret1 @@ -181,13 +181,13 @@ func (m *MockContext) GetTransactionReceiptsForTick(tick uint64) ([]receipt.Rece // GetTransactionReceiptsForTick indicates an expected call of GetTransactionReceiptsForTick. func (mr *MockContextMockRecorder) GetTransactionReceiptsForTick(tick interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTransactionReceiptsForTick", reflect.TypeOf((*MockContext)(nil).GetTransactionReceiptsForTick), tick) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "getTransactionReceiptsForTick", reflect.TypeOf((*MockContext)(nil).GetTransactionReceiptsForTick), tick) } // GetTxPool mocks base method. func (m *MockContext) GetTxPool() *txpool.TxPool { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetTxPool") + ret := m.ctrl.Call(m, "getTxPool") ret0, _ := ret[0].(*txpool.TxPool) return ret0 } @@ -195,13 +195,13 @@ func (m *MockContext) GetTxPool() *txpool.TxPool { // GetTxPool indicates an expected call of GetTxPool. func (mr *MockContextMockRecorder) GetTxPool() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTxPool", reflect.TypeOf((*MockContext)(nil).GetTxPool)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "getTxPool", reflect.TypeOf((*MockContext)(nil).GetTxPool)) } // IsReadOnly mocks base method. func (m *MockContext) IsReadOnly() bool { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsReadOnly") + ret := m.ctrl.Call(m, "isReadOnly") ret0, _ := ret[0].(bool) return ret0 } @@ -209,13 +209,13 @@ func (m *MockContext) IsReadOnly() bool { // IsReadOnly indicates an expected call of IsReadOnly. func (mr *MockContextMockRecorder) IsReadOnly() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsReadOnly", reflect.TypeOf((*MockContext)(nil).IsReadOnly)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "isReadOnly", reflect.TypeOf((*MockContext)(nil).IsReadOnly)) } // IsWorldReady mocks base method. func (m *MockContext) IsWorldReady() bool { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsWorldReady") + ret := m.ctrl.Call(m, "isWorldReady") ret0, _ := ret[0].(bool) return ret0 } @@ -223,7 +223,7 @@ func (m *MockContext) IsWorldReady() bool { // IsWorldReady indicates an expected call of IsWorldReady. func (mr *MockContextMockRecorder) IsWorldReady() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsWorldReady", reflect.TypeOf((*MockContext)(nil).IsWorldReady)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "isWorldReady", reflect.TypeOf((*MockContext)(nil).IsWorldReady)) } // Logger mocks base method. @@ -257,7 +257,7 @@ func (mr *MockContextMockRecorder) Namespace() *gomock.Call { // ReceiptHistorySize mocks base method. func (m *MockContext) ReceiptHistorySize() uint64 { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ReceiptHistorySize") + ret := m.ctrl.Call(m, "receiptHistorySize") ret0, _ := ret[0].(uint64) return ret0 } @@ -265,37 +265,37 @@ func (m *MockContext) ReceiptHistorySize() uint64 { // ReceiptHistorySize indicates an expected call of ReceiptHistorySize. func (mr *MockContextMockRecorder) ReceiptHistorySize() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReceiptHistorySize", reflect.TypeOf((*MockContext)(nil).ReceiptHistorySize)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "receiptHistorySize", reflect.TypeOf((*MockContext)(nil).ReceiptHistorySize)) } -// SetLogger mocks base method. -func (m *MockContext) SetLogger(logger zerolog.Logger) { +// setLogger mocks base method. +func (m *MockContext) setLogger(logger zerolog.Logger) { m.ctrl.T.Helper() - m.ctrl.Call(m, "SetLogger", logger) + m.ctrl.Call(m, "setLogger", logger) } // SetLogger indicates an expected call of SetLogger. func (mr *MockContextMockRecorder) SetLogger(logger interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLogger", reflect.TypeOf((*MockContext)(nil).SetLogger), logger) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "setLogger", reflect.TypeOf((*MockContext)(nil).setLogger), logger) } // SetMessageResult mocks base method. func (m *MockContext) SetMessageResult(id types.TxHash, a any) { m.ctrl.T.Helper() - m.ctrl.Call(m, "SetMessageResult", id, a) + m.ctrl.Call(m, "setMessageResult", id, a) } // SetMessageResult indicates an expected call of SetMessageResult. func (mr *MockContextMockRecorder) SetMessageResult(id, a interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetMessageResult", reflect.TypeOf((*MockContext)(nil).SetMessageResult), id, a) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "setMessageResult", reflect.TypeOf((*MockContext)(nil).SetMessageResult), id, a) } // StoreManager mocks base method. func (m *MockContext) StoreManager() gamestate.Manager { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "StoreManager") + ret := m.ctrl.Call(m, "storeManager") ret0, _ := ret[0].(gamestate.Manager) return ret0 } @@ -303,13 +303,13 @@ func (m *MockContext) StoreManager() gamestate.Manager { // StoreManager indicates an expected call of StoreManager. func (mr *MockContextMockRecorder) StoreManager() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StoreManager", reflect.TypeOf((*MockContext)(nil).StoreManager)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "storeManager", reflect.TypeOf((*MockContext)(nil).StoreManager)) } // StoreReader mocks base method. func (m *MockContext) StoreReader() gamestate.Reader { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "StoreReader") + ret := m.ctrl.Call(m, "storeReader") ret0, _ := ret[0].(gamestate.Reader) return ret0 } @@ -317,7 +317,7 @@ func (m *MockContext) StoreReader() gamestate.Reader { // StoreReader indicates an expected call of StoreReader. func (mr *MockContextMockRecorder) StoreReader() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StoreReader", reflect.TypeOf((*MockContext)(nil).StoreReader)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "storeReader", reflect.TypeOf((*MockContext)(nil).StoreReader)) } // Timestamp mocks base method. diff --git a/cardinal/types/engine/query.go b/cardinal/types/engine/query.go deleted file mode 100644 index 5180d2706..000000000 --- a/cardinal/types/engine/query.go +++ /dev/null @@ -1,25 +0,0 @@ -package engine - -type Query interface { - // Name returns the name of the query. - Name() string - // Group returns the group of the query. - Group() string - // HandleQuery handles queries with concrete types, rather than encoded bytes. - HandleQuery(Context, any) (any, error) - // HandleQueryRaw is given a reference to the engine, json encoded bytes that represent a query request - // and is expected to return a json encoded response struct. - HandleQueryRaw(Context, []byte) ([]byte, error) - // DecodeEVMRequest decodes bytes originating from the evm into the request type, which will be ABI encoded. - DecodeEVMRequest([]byte) (any, error) - // EncodeEVMReply encodes the reply as an abi encoded struct. - EncodeEVMReply(any) ([]byte, error) - // DecodeEVMReply decodes EVM reply bytes, into the concrete go reply type. - DecodeEVMReply([]byte) (any, error) - // EncodeAsABI encodes a go struct in abi format. This is mostly used for testing. - EncodeAsABI(any) ([]byte, error) - // IsEVMCompatible reports if the query is able to be sent from the EVM. - IsEVMCompatible() bool - // GetRequestFieldInformation returns a map of the fields of the query's request type and their types. - GetRequestFieldInformation() map[string]any -} diff --git a/cardinal/types/entity.go b/cardinal/types/entity.go index 3177d7a5d..c7eccda84 100644 --- a/cardinal/types/entity.go +++ b/cardinal/types/entity.go @@ -1,3 +1,10 @@ package types +import "encoding/json" + type EntityID uint64 + +type EntityStateElement struct { + ID EntityID `json:"id"` + Data []json.RawMessage `json:"data" swaggertype:"object"` +} diff --git a/cardinal/types/info.go b/cardinal/types/info.go new file mode 100644 index 000000000..0f5344cae --- /dev/null +++ b/cardinal/types/info.go @@ -0,0 +1,8 @@ +package types + +// FieldDetail represents a field from a url request. +type FieldDetail struct { + Name string `json:"name"` // name of the message or query + Fields map[string]any `json:"fields"` // variable name and type + URL string `json:"url,omitempty"` +} diff --git a/cardinal/types/query.go b/cardinal/types/query.go new file mode 100644 index 000000000..76b45e798 --- /dev/null +++ b/cardinal/types/query.go @@ -0,0 +1,5 @@ +package types + +import "errors" + +var ErrQueryNotFound = errors.New("query not found") diff --git a/cardinal/util.go b/cardinal/util.go index 6518c4d7a..87c1f2c94 100644 --- a/cardinal/util.go +++ b/cardinal/util.go @@ -1,10 +1,14 @@ package cardinal import ( + "reflect" + "github.com/rotisserie/eris" + "pkg.world.dev/world-engine/cardinal/gamestate" + "pkg.world.dev/world-engine/cardinal/receipt" "pkg.world.dev/world-engine/cardinal/server" - "pkg.world.dev/world-engine/cardinal/types/engine" + "pkg.world.dev/world-engine/cardinal/types" ) var NonFatalError = []error{ @@ -33,8 +37,8 @@ func separateOptions(opts []WorldOption) ( } // panicOnFatalError is a helper function to panic on non-deterministic errors (i.e. Redis error). -func panicOnFatalError(wCtx engine.Context, err error) { - if err != nil && !wCtx.IsReadOnly() && isFatalError(err) { +func panicOnFatalError(wCtx WorldContext, err error) { + if err != nil && !wCtx.isReadOnly() && isFatalError(err) { wCtx.Logger().Panic().Err(err).Msgf("fatal error: %v", eris.ToString(err, true)) panic(err) } @@ -48,3 +52,36 @@ func isFatalError(err error) bool { } return true } + +func GetMessage[In any, Out any](wCtx WorldContext) (*MessageType[In, Out], error) { + var msg MessageType[In, Out] + msgType := reflect.TypeOf(msg) + tempRes, ok := wCtx.getMessageByType(msgType) + if !ok { + return nil, eris.Errorf("Could not find %q, Message may not be registered.", msg.Name()) + } + var _ types.Message = &msg + res, ok := tempRes.(*MessageType[In, Out]) + if !ok { + return &msg, eris.New("wrong type") + } + return res, nil +} + +func GetTransactionReceiptsForTick(wCtx WorldContext, tick uint64) ([]receipt.Receipt, error) { + ctx, ok := wCtx.(*worldContext) + if !ok { + return nil, eris.New("error in test type assertion.") + } + return ctx.world.GetTransactionReceiptsForTick(tick) +} + +func GetStoreManagerFromContext(wCtx WorldContext) gamestate.Manager { + return wCtx.storeManager() +} + +// InternalHandleQuery is only used for tests it should not be used outside of that context. +// TODO: Tests should be edited and changed such that this is no longer done. +func InternalHandleQuery(wCtx WorldContext, query query, a any) (any, error) { + return query.handleQuery(wCtx, a) +} diff --git a/cardinal/world.go b/cardinal/world.go index f2218b64b..7137a99f1 100644 --- a/cardinal/world.go +++ b/cardinal/world.go @@ -22,15 +22,13 @@ import ( ecslog "pkg.world.dev/world-engine/cardinal/log" "pkg.world.dev/world-engine/cardinal/receipt" "pkg.world.dev/world-engine/cardinal/router" - "pkg.world.dev/world-engine/cardinal/search" "pkg.world.dev/world-engine/cardinal/search/filter" "pkg.world.dev/world-engine/cardinal/server" + "pkg.world.dev/world-engine/cardinal/server/handler/cql" servertypes "pkg.world.dev/world-engine/cardinal/server/types" "pkg.world.dev/world-engine/cardinal/statsd" "pkg.world.dev/world-engine/cardinal/storage/redis" - "pkg.world.dev/world-engine/cardinal/system" "pkg.world.dev/world-engine/cardinal/types" - "pkg.world.dev/world-engine/cardinal/types/engine" "pkg.world.dev/world-engine/cardinal/types/txpool" "pkg.world.dev/world-engine/cardinal/worldstage" "pkg.world.dev/world-engine/sign" @@ -41,10 +39,14 @@ const ( RedisDialTimeOut = 15 ) -var _ router.Provider = &World{} //nolint:exhaustruct -var _ servertypes.Provider = &World{} //nolint:exhaustruct +var _ router.Provider = &World{} //nolint:exhaustruct +var _ servertypes.ProviderWorld = &World{} //nolint:exhaustruct type World struct { + SystemManager + MessageManager + QueryManager + namespace Namespace rollupEnabled bool @@ -57,11 +59,9 @@ type World struct { serverOptions []server.Option // Core modules - worldStage *worldstage.Manager - msgManager *MessageManager - systemManager *system.Manager + worldStage *worldstage.Manager + componentManager *component.Manager - queryManager *QueryManager router router.Router txPool *txpool.TxPool @@ -110,7 +110,6 @@ func NewWorld(opts ...WorldOption) (*World, error) { } tick := new(atomic.Uint64) - world := &World{ namespace: Namespace(cfg.CardinalNamespace), rollupEnabled: cfg.CardinalRollupEnabled, @@ -125,10 +124,10 @@ func NewWorld(opts ...WorldOption) (*World, error) { // Core modules worldStage: worldstage.NewManager(), - msgManager: NewMessageManager(), - systemManager: system.NewManager(), + MessageManager: newMessageManager(), + SystemManager: newSystemManager(), componentManager: component.NewManager(&redisMetaStore), - queryManager: NewQueryManager(), + QueryManager: newQueryManager(), router: nil, // Will be set if run mode is production or its injected via options txPool: txpool.New(), @@ -181,7 +180,6 @@ func NewWorld(opts ...WorldOption) (*World, error) { ) } } - return world, nil } @@ -190,7 +188,7 @@ func (w *World) CurrentTick() uint64 { } // doTick performs one game tick. This consists of taking a snapshot of all pending transactions, then calling -// each System in turn with the snapshot of transactions. +// each system in turn with the snapshot of transactions. func (w *World) doTick(ctx context.Context, timestamp uint64) (err error) { // Record tick start time for statsd. // Not to be confused with `timestamp` that represents the time context for the tick @@ -222,7 +220,7 @@ func (w *World) doTick(ctx context.Context, timestamp uint64) (err error) { // Copy the transactions from the pool so that we can safely modify the pool while the tick is running. txPool := w.txPool.CopyTransactions() - if err := w.entityStore.StartNextTick(w.msgManager.GetRegisteredMessages(), txPool); err != nil { + if err := w.entityStore.StartNextTick(w.GetRegisteredMessages(), txPool); err != nil { return err } @@ -234,7 +232,7 @@ func (w *World) doTick(ctx context.Context, timestamp uint64) (err error) { // Run all registered systems. // This will run the registered init systems if the current tick is 0 - if err := w.systemManager.RunSystems(wCtx); err != nil { + if err := w.SystemManager.runSystems(wCtx); err != nil { return err } @@ -332,9 +330,7 @@ func (w *World) StartGame() error { // Create server // We can't do this is in NewWorld() because the server needs to know the registered messages // and register queries first. We can probably refactor this though. - w.server, err = server.New(w, - NewReadOnlyWorldContext(w), w.GetRegisteredComponents(), w.GetRegisteredMessages(), - w.GetRegisteredQueries(), w.serverOptions...) + w.server, err = server.New(w, w.GetRegisteredComponents(), w.GetRegisteredMessages(), w.serverOptions...) if err != nil { return err } @@ -343,13 +339,13 @@ func (w *World) StartGame() error { if len(w.componentManager.GetComponents()) == 0 { log.Warn().Msg("No components registered") } - if len(w.msgManager.GetRegisteredMessages()) == 0 { + if len(w.GetRegisteredMessages()) == 0 { log.Warn().Msg("No messages registered") } - if len(w.queryManager.GetRegisteredQueries()) == 0 { + if len(w.GetRegisteredQueries()) == 0 { log.Warn().Msg("No queries registered") } - if len(w.systemManager.GetRegisteredSystemNames()) == 0 { + if len(w.SystemManager.GetRegisteredSystems()) == 0 { log.Warn().Msg("No systems registered") } @@ -493,7 +489,7 @@ func (w *World) handleTickPanic() { log.Error().Msgf( "Tick: %d, Current running system: %s", w.CurrentTick(), - w.systemManager.GetCurrentSystem(), + w.SystemManager.GetCurrentSystem(), ) panic(r) } @@ -523,7 +519,7 @@ func (w *World) drainChannelsWaitingForNextTick() { } // AddTransaction adds a transaction to the transaction pool. This should not be used directly. -// Instead, use a MessageType.AddTransaction to ensure type consistency. Returns the tick this transaction will be +// Instead, use a MessageType.addTransaction to ensure type consistency. Returns the tick this transaction will be // executed in. func (w *World) AddTransaction(id types.MessageID, v any, sig *sign.Transaction) ( tick uint64, txHash types.TxHash, @@ -552,6 +548,43 @@ func (w *World) UseNonce(signerAddress string, nonce uint64) error { return w.redisStorage.UseNonce(signerAddress, nonce) } +func (w *World) GetDebugState() ([]types.EntityStateElement, error) { + result := make([]types.EntityStateElement, 0) + s := w.Search(filter.All()) + var eachClosureErr error + wCtx := NewReadOnlyWorldContext(w) + searchEachErr := s.Each(wCtx, + func(id types.EntityID) bool { + var components []types.ComponentMetadata + components, eachClosureErr = w.StoreReader().GetComponentTypesForEntity(id) + if eachClosureErr != nil { + return false + } + resultElement := types.EntityStateElement{ + ID: id, + Data: make([]json.RawMessage, 0), + } + for _, c := range components { + var data json.RawMessage + data, eachClosureErr = w.StoreReader().GetComponentForEntityInRawJSON(c, id) + if eachClosureErr != nil { + return false + } + resultElement.Data = append(resultElement.Data, data) + } + result = append(result, resultElement) + return true + }, + ) + if eachClosureErr != nil { + return nil, eachClosureErr + } + if searchEachErr != nil { + return nil, searchEachErr + } + return result, nil +} + func (w *World) Namespace() string { return string(w.namespace) } @@ -580,7 +613,7 @@ func (w *World) HandleEVMQuery(name string, abiRequest []byte) ([]byte, error) { return nil, err } - reply, err := qry.HandleQuery(NewReadOnlyWorldContext(w), req) + reply, err := qry.handleQuery(NewReadOnlyWorldContext(w), req) if err != nil { return nil, err } @@ -588,43 +621,27 @@ func (w *World) HandleEVMQuery(name string, abiRequest []byte) ([]byte, error) { return qry.EncodeEVMReply(reply) } -func (w *World) Search(filter filter.ComponentFilter) search.EntitySearch { - return search.NewLegacySearch(filter) +func (w *World) Search(filter filter.ComponentFilter) EntitySearch { + return NewLegacySearch(filter) } func (w *World) StoreReader() gamestate.Reader { return w.entityStore.ToReadOnly() } -func (w *World) GetRegisteredQueries() []engine.Query { - return w.queryManager.GetRegisteredQueries() -} -func (w *World) GetRegisteredMessages() []types.Message { - return w.msgManager.GetRegisteredMessages() -} - func (w *World) GetRegisteredComponents() []types.ComponentMetadata { return w.componentManager.GetComponents() } -func (w *World) GetRegisteredSystemNames() []string { - return w.systemManager.GetRegisteredSystemNames() -} -func (w *World) GetReadOnlyCtx() engine.Context { + +func (w *World) GetReadOnlyCtx() WorldContext { return NewReadOnlyWorldContext(w) } -func (w *World) GetQueryByName(name string) (engine.Query, error) { - return w.queryManager.GetQueryByName(name) -} func (w *World) GetMessageByID(id types.MessageID) (types.Message, bool) { - msg := w.msgManager.GetMessageByID(id) + msg := w.MessageManager.GetMessageByID(id) return msg, msg != nil } -func (w *World) GetMessageByFullName(name string) (types.Message, bool) { - return w.msgManager.GetMessageByFullName(name) -} - func (w *World) GetComponentByName(name string) (types.ComponentMetadata, error) { return w.componentManager.GetComponentByName(name) } @@ -643,3 +660,58 @@ func (w *World) populateAndBroadcastTickResults() { log.Err(err).Msgf("failed to broadcast tick results") } } + +func (w *World) ReceiptHistorySize() uint64 { + return w.receiptHistory.Size() +} + +func (w *World) EvaluateCQL(cqlString string) ([]types.EntityStateElement, error) { + // getComponentByName is a wrapper function that casts component.ComponentMetadata from ctx.getComponentByName + // to types.Component + getComponentByName := func(name string) (types.Component, error) { + comp, err := w.GetComponentByName(name) + if err != nil { + return nil, err + } + return comp, nil + } + + // Parse the CQL string into a filter + cqlFilter, err := cql.Parse(cqlString, getComponentByName) + if err != nil { + return nil, eris.Errorf("failed to parse cql string: %s", cqlString) + } + result := make([]types.EntityStateElement, 0) + var eachError error + wCtx := NewReadOnlyWorldContext(w) + searchErr := w.Search(cqlFilter).Each(wCtx, + func(id types.EntityID) bool { + components, err := w.StoreReader().GetComponentTypesForEntity(id) + if err != nil { + eachError = err + return false + } + resultElement := types.EntityStateElement{ + ID: id, + Data: make([]json.RawMessage, 0), + } + + for _, c := range components { + data, err := w.StoreReader().GetComponentForEntityInRawJSON(c, id) + if err != nil { + eachError = err + return false + } + resultElement.Data = append(resultElement.Data, data) + } + result = append(result, resultElement) + return true + }, + ) + if eachError != nil { + return nil, eachError + } else if searchErr != nil { + return nil, searchErr + } + return result, nil +} diff --git a/cardinal/world_context.go b/cardinal/world_context.go index 7ed0a82d2..d628440d1 100644 --- a/cardinal/world_context.go +++ b/cardinal/world_context.go @@ -9,14 +9,49 @@ import ( "pkg.world.dev/world-engine/cardinal/gamestate" "pkg.world.dev/world-engine/cardinal/receipt" "pkg.world.dev/world-engine/cardinal/types" - "pkg.world.dev/world-engine/cardinal/types/engine" "pkg.world.dev/world-engine/cardinal/types/txpool" "pkg.world.dev/world-engine/cardinal/worldstage" "pkg.world.dev/world-engine/sign" ) // interface guard -var _ engine.Context = (*worldContext)(nil) +var _ WorldContext = (*worldContext)(nil) + +//go:generate mockgen -source=context.go -package mocks -destination=mocks/context.go +type WorldContext interface { + // Timestamp returns the UNIX timestamp of the tick. + Timestamp() uint64 + // CurrentTick returns the current tick. + CurrentTick() uint64 + // Logger returns the logger that can be used to log messages from within system or query. + Logger() *zerolog.Logger + // EmitEvent emits an event that will be broadcast to all websocket subscribers. + EmitEvent(map[string]any) error + // EmitStringEvent emits a string event that will be broadcast to all websocket subscribers. + // This method is provided for backwards compatability. EmitEvent should be used for most cases. + EmitStringEvent(string) error + // Namespace returns the namespace of the world. + Namespace() string + + // For internal use. + + // SetLogger is used to inject a new logger configuration to an engine context that is already created. + setLogger(logger zerolog.Logger) + addMessageError(id types.TxHash, err error) + setMessageResult(id types.TxHash, a any) + getComponentByName(name string) (types.ComponentMetadata, error) + getMessageByType(mType reflect.Type) (types.Message, bool) + getTransactionReceipt(id types.TxHash) (any, []error, bool) + getSignerForPersonaTag(personaTag string, tick uint64) (addr string, err error) + getTransactionReceiptsForTick(tick uint64) ([]receipt.Receipt, error) + receiptHistorySize() uint64 + addTransaction(id types.MessageID, v any, sig *sign.Transaction) (uint64, types.TxHash) + isWorldReady() bool + storeReader() gamestate.Reader + storeManager() gamestate.Manager + getTxPool() *txpool.TxPool + isReadOnly() bool +} type worldContext struct { world *World @@ -25,7 +60,7 @@ type worldContext struct { readOnly bool } -func newWorldContextForTick(world *World, txPool *txpool.TxPool) engine.Context { +func newWorldContextForTick(world *World, txPool *txpool.TxPool) WorldContext { return &worldContext{ world: world, txPool: txPool, @@ -34,7 +69,7 @@ func newWorldContextForTick(world *World, txPool *txpool.TxPool) engine.Context } } -func NewWorldContext(world *World) engine.Context { +func NewWorldContext(world *World) WorldContext { return &worldContext{ world: world, txPool: nil, @@ -43,7 +78,7 @@ func NewWorldContext(world *World) engine.Context { } } -func NewReadOnlyWorldContext(world *World) engine.Context { +func NewReadOnlyWorldContext(world *World) WorldContext { return &worldContext{ world: world, txPool: nil, @@ -65,29 +100,29 @@ func (ctx *worldContext) Logger() *zerolog.Logger { return ctx.logger } -func (ctx *worldContext) GetMessageByType(mType reflect.Type) (types.Message, bool) { - return ctx.world.msgManager.GetMessageByType(mType) +func (ctx *worldContext) getMessageByType(mType reflect.Type) (types.Message, bool) { + return ctx.world.GetMessageByType(mType) } -func (ctx *worldContext) SetLogger(logger zerolog.Logger) { +func (ctx *worldContext) setLogger(logger zerolog.Logger) { ctx.logger = &logger } -func (ctx *worldContext) GetComponentByName(name string) (types.ComponentMetadata, error) { +func (ctx *worldContext) getComponentByName(name string) (types.ComponentMetadata, error) { return ctx.world.GetComponentByName(name) } -func (ctx *worldContext) AddMessageError(id types.TxHash, err error) { +func (ctx *worldContext) addMessageError(id types.TxHash, err error) { // TODO(scott): i dont trust exposing this to the users. this should be fully abstracted away. ctx.world.receiptHistory.AddError(id, err) } -func (ctx *worldContext) SetMessageResult(id types.TxHash, a any) { +func (ctx *worldContext) setMessageResult(id types.TxHash, a any) { // TODO(scott): i dont trust exposing this to the users. this should be fully abstracted away. ctx.world.receiptHistory.SetResult(id, a) } -func (ctx *worldContext) GetTransactionReceipt(id types.TxHash) (any, []error, bool) { +func (ctx *worldContext) getTransactionReceipt(id types.TxHash) (any, []error, bool) { rec, ok := ctx.world.receiptHistory.GetReceipt(id) if !ok { return nil, nil, false @@ -103,15 +138,15 @@ func (ctx *worldContext) EmitStringEvent(e string) error { return ctx.world.tickResults.AddStringEvent(e) } -func (ctx *worldContext) GetSignerForPersonaTag(personaTag string, tick uint64) (addr string, err error) { +func (ctx *worldContext) getSignerForPersonaTag(personaTag string, tick uint64) (addr string, err error) { return ctx.world.GetSignerForPersonaTag(personaTag, tick) } -func (ctx *worldContext) GetTransactionReceiptsForTick(tick uint64) ([]receipt.Receipt, error) { +func (ctx *worldContext) getTransactionReceiptsForTick(tick uint64) ([]receipt.Receipt, error) { return ctx.world.GetTransactionReceiptsForTick(tick) } -func (ctx *worldContext) ReceiptHistorySize() uint64 { +func (ctx *worldContext) receiptHistorySize() uint64 { return ctx.world.receiptHistory.Size() } @@ -119,31 +154,31 @@ func (ctx *worldContext) Namespace() string { return ctx.world.Namespace() } -func (ctx *worldContext) AddTransaction(id types.MessageID, v any, sig *sign.Transaction) (uint64, types.TxHash) { +func (ctx *worldContext) addTransaction(id types.MessageID, v any, sig *sign.Transaction) (uint64, types.TxHash) { return ctx.world.AddTransaction(id, v, sig) } -func (ctx *worldContext) GetTxPool() *txpool.TxPool { +func (ctx *worldContext) getTxPool() *txpool.TxPool { return ctx.txPool } -func (ctx *worldContext) IsReadOnly() bool { +func (ctx *worldContext) isReadOnly() bool { return ctx.readOnly } -func (ctx *worldContext) StoreManager() gamestate.Manager { +func (ctx *worldContext) storeManager() gamestate.Manager { return ctx.world.entityStore } -func (ctx *worldContext) StoreReader() gamestate.Reader { - sm := ctx.StoreManager() - if ctx.IsReadOnly() { +func (ctx *worldContext) storeReader() gamestate.Reader { + sm := ctx.storeManager() + if ctx.isReadOnly() { return sm.ToReadOnly() } return sm } -func (ctx *worldContext) IsWorldReady() bool { +func (ctx *worldContext) isWorldReady() bool { stage := ctx.world.worldStage.Current() return stage == worldstage.Ready || stage == worldstage.Running || diff --git a/cardinal/world_persona.go b/cardinal/world_persona.go index 37c1f42c4..3b03b4ef3 100644 --- a/cardinal/world_persona.go +++ b/cardinal/world_persona.go @@ -7,7 +7,6 @@ import ( "pkg.world.dev/world-engine/cardinal/persona" "pkg.world.dev/world-engine/cardinal/persona/component" - "pkg.world.dev/world-engine/cardinal/search" "pkg.world.dev/world-engine/cardinal/search/filter" "pkg.world.dev/world-engine/cardinal/types" ) @@ -21,7 +20,7 @@ func (w *World) GetSignerForPersonaTag(personaTag string, tick uint64) (addr str } var errs []error wCtx := NewReadOnlyWorldContext(w) - s := search.NewSearch().Entity(filter.Exact(filter.Component[component.SignerComponent]())) + s := NewSearch().Entity(filter.Exact(filter.Component[component.SignerComponent]())) err = s.Each(wCtx, func(id types.EntityID) bool { sc, err := GetComponent[component.SignerComponent](wCtx, id) @@ -45,7 +44,7 @@ func (w *World) GetSignerForPersonaTag(personaTag string, tick uint64) (addr str func (w *World) GetSignerComponentForPersona(personaTag string) (*component.SignerComponent, error) { var sc *component.SignerComponent wCtx := NewReadOnlyWorldContext(w) - q := search.NewSearch().Entity(filter.Exact(filter.Component[component.SignerComponent]())) + q := NewSearch().Entity(filter.Exact(filter.Component[component.SignerComponent]())) var getComponentErr error searchIterationErr := eris.Wrap( q.Each(wCtx, diff --git a/cardinal/world_persona_test.go b/cardinal/world_persona_test.go index 7c06f1fe4..35b24c938 100644 --- a/cardinal/world_persona_test.go +++ b/cardinal/world_persona_test.go @@ -10,11 +10,9 @@ import ( "pkg.world.dev/world-engine/cardinal" "pkg.world.dev/world-engine/cardinal/persona/component" "pkg.world.dev/world-engine/cardinal/persona/msg" - "pkg.world.dev/world-engine/cardinal/search" "pkg.world.dev/world-engine/cardinal/search/filter" "pkg.world.dev/world-engine/cardinal/testutils" "pkg.world.dev/world-engine/cardinal/types" - "pkg.world.dev/world-engine/cardinal/types/engine" "pkg.world.dev/world-engine/sign" ) @@ -99,9 +97,9 @@ func TestCreatePersonaSystem_WhenCardinalIsRestarted_PersonaTagsAreStillRegister // emitNumberOfPersonaTagsSystem is a system that finds all the registered persona tags and sends them // across a channel. - emitNumberOfPersonaTagsSystem := func(wCtx engine.Context) error { + emitNumberOfPersonaTagsSystem := func(wCtx cardinal.WorldContext) error { result := countPersonaTagsResult{personaTags: map[string]bool{}} - err := search.NewSearch(). + err := cardinal.NewSearch(). Entity(filter.Exact(filter.Component[component.SignerComponent]())). Each(wCtx, func(id types.EntityID) bool { comp, err := cardinal.GetComponent[component.SignerComponent](wCtx, id) diff --git a/cardinal/world_receipt.go b/cardinal/world_receipt.go index 960bd5f9d..a0b42ea4c 100644 --- a/cardinal/world_receipt.go +++ b/cardinal/world_receipt.go @@ -1,6 +1,8 @@ package cardinal import ( + "github.com/rotisserie/eris" + "pkg.world.dev/world-engine/cardinal/receipt" "pkg.world.dev/world-engine/cardinal/types/txpool" ) @@ -40,7 +42,10 @@ func (w *World) setEvmResults(txs []txpool.TxData) { continue } evmRec := EVMTxReceipt{EVMTxHash: tx.EVMSourceTxHash} - msg := w.msgManager.GetMessageByID(tx.MsgID) + msg, ok := w.GetMessageByID(tx.MsgID) + if !ok { + rec.Errs = append(rec.Errs, eris.New("failed to get message by id?")) + } if rec.Result != nil { abiBz, err := msg.ABIEncode(rec.Result) if err != nil { diff --git a/cardinal/world_recovery.go b/cardinal/world_recovery.go index 22278b85c..d36544d70 100644 --- a/cardinal/world_recovery.go +++ b/cardinal/world_recovery.go @@ -22,7 +22,7 @@ func (w *World) recoverAndExecutePendingTxs() error { return nil } - recoveredTxs, err := w.entityStore.Recover(w.msgManager.GetRegisteredMessages()) + recoveredTxs, err := w.entityStore.Recover(w.GetRegisteredMessages()) if err != nil { return err } diff --git a/cardinal/world_test.go b/cardinal/world_test.go index 54d8201f4..989e0276a 100644 --- a/cardinal/world_test.go +++ b/cardinal/world_test.go @@ -20,7 +20,6 @@ import ( "pkg.world.dev/world-engine/cardinal/iterators" "pkg.world.dev/world-engine/cardinal/search/filter" "pkg.world.dev/world-engine/cardinal/types" - "pkg.world.dev/world-engine/cardinal/types/engine" "pkg.world.dev/world-engine/cardinal/worldstage" "pkg.world.dev/world-engine/sign" ) @@ -48,7 +47,7 @@ func TestIfPanicMessageLogged(t *testing.T) { errorTxt := "BIG ERROR OH NO" err = RegisterSystems( world, - func(engine.Context) error { + func(WorldContext) error { panic(errorTxt) }, ) @@ -120,7 +119,7 @@ func TestCanRecoverStateAfterFailedArchetypeChange(t *testing.T) { errorToggleComponent := errors.New("problem with toggle component") err = RegisterSystems( world, - func(wCtx engine.Context) error { + func(wCtx WorldContext) error { // Get the one and only entity ID q := NewSearch().Entity(filter.Contains(filter.Component[ScalarComponentStatic]())) id, err := q.First(wCtx) @@ -213,7 +212,7 @@ func TestCanRecoverTransactionsFromFailedSystemRun(t *testing.T) { err = RegisterSystems( world, - func(wCtx engine.Context) error { + func(wCtx WorldContext) error { q := NewSearch().Entity(filter.Contains(filter.Component[PowerComp]())) id := q.MustFirst(wCtx) entityPower, err := GetComponent[PowerComp](wCtx, id) @@ -318,7 +317,7 @@ func TestCanIdentifyAndFixSystemError(t *testing.T) { // In this test, our "buggy" system fails once Power reaches 3 err = RegisterSystems( world, - func(wCtx engine.Context) error { + func(wCtx WorldContext) error { searchObject := NewSearch().Entity(filter.Exact(filter.Component[onePowerComponent]())) id := searchObject.MustFirst(wCtx) p, err := GetComponent[onePowerComponent](wCtx, id) @@ -347,7 +346,7 @@ func TestCanIdentifyAndFixSystemError(t *testing.T) { world.tickTheEngine(ctx, nil) // Power is set to 2 world.tickTheEngine(ctx, nil) - // Power is set to 3, then the System fails + // Power is set to 3, then the system fails err = doTickCapturePanic(ctx, world) assert.ErrorContains(t, err, errorSystem.Error()) @@ -362,7 +361,7 @@ func TestCanIdentifyAndFixSystemError(t *testing.T) { // this is our fixed system that can handle Power levels of 3 and higher err = RegisterSystems( world2, - func(wCtx engine.Context) error { + func(wCtx WorldContext) error { p, err := GetComponent[onePowerComponent](wCtx, id) if err != nil { return err @@ -412,35 +411,35 @@ func TestSystemsPanicOnRedisError(t *testing.T) { testCases := []struct { name string // the failFn will be called at a time when the ECB is empty of cached data and redis is down. - failFn func(wCtx engine.Context, goodID types.EntityID) + failFn func(wCtx WorldContext, goodID types.EntityID) }{ { name: "AddComponentTo", - failFn: func(wCtx engine.Context, goodID types.EntityID) { + failFn: func(wCtx WorldContext, goodID types.EntityID) { _ = AddComponentTo[Qux](wCtx, goodID) }, }, { name: "RemoveComponentFrom", - failFn: func(wCtx engine.Context, goodID types.EntityID) { + failFn: func(wCtx WorldContext, goodID types.EntityID) { _ = RemoveComponentFrom[Bar](wCtx, goodID) }, }, { name: "GetComponent", - failFn: func(wCtx engine.Context, goodID types.EntityID) { + failFn: func(wCtx WorldContext, goodID types.EntityID) { _, _ = GetComponent[Foo](wCtx, goodID) }, }, { name: "SetComponent", - failFn: func(wCtx engine.Context, goodID types.EntityID) { + failFn: func(wCtx WorldContext, goodID types.EntityID) { _ = SetComponent[Foo](wCtx, goodID, &Foo{}) }, }, { name: "UpdateComponent", - failFn: func(wCtx engine.Context, goodID types.EntityID) { + failFn: func(wCtx WorldContext, goodID types.EntityID) { _ = UpdateComponent[Foo](wCtx, goodID, func(f *Foo) *Foo { return f }) @@ -465,7 +464,7 @@ func TestSystemsPanicOnRedisError(t *testing.T) { // This system will be called 2 times. The first time, a single entity is Created. The second time, // the previously Created entity is fetched, and then miniRedis is closed. Subsequent attempts to access // data should panic. - assert.NilError(t, RegisterSystems(world, func(wCtx engine.Context) error { + assert.NilError(t, RegisterSystems(world, func(wCtx WorldContext) error { // Set up the entity in the first tick if wCtx.CurrentTick() == 0 { _, err := Create(wCtx, Foo{}, Bar{}) @@ -473,7 +472,8 @@ func TestSystemsPanicOnRedisError(t *testing.T) { return nil } // Get the valid entity for the second tick - id, err := NewSearch().Entity(filter.Exact(filter.Component[Foo](), filter.Component[Bar]())).First(wCtx) + id, err := NewSearch().Entity(filter.Exact(filter.Component[Foo](), + filter.Component[Bar]())).First(wCtx) assert.Check(t, err == nil) assert.Check(t, id != iterators.BadID) @@ -536,10 +536,10 @@ func doTickCapturePanic(ctx context.Context, world *World) (err error) { return nil } -func getMessage[In any, Out any](wCtx engine.Context) (*MessageType[In, Out], error) { +func getMessage[In any, Out any](wCtx WorldContext) (*MessageType[In, Out], error) { var msg MessageType[In, Out] msgType := reflect.TypeOf(msg) - tempRes, ok := wCtx.GetMessageByType(msgType) + tempRes, ok := wCtx.getMessageByType(msgType) if !ok { return &msg, eris.Errorf("Could not find %s, Message may not be registered.", msg.Name()) } diff --git a/docs/cardinal/game/persona.mdx b/docs/cardinal/game/persona.mdx index d73fb8f2b..bb1162d47 100644 --- a/docs/cardinal/game/persona.mdx +++ b/docs/cardinal/game/persona.mdx @@ -14,7 +14,7 @@ In contrast to the typical Ethereum account/address, Persona has the following s ## Persona In Systems -Within a System, you can loop over transactions of a particular type. These transactions called **TxData** contain the following methods: +Within a system, you can loop over transactions of a particular type. These transactions called **TxData** contain the following methods: - `Msg` returns the custom message data that you set up when you initially called NewMessageType. @@ -22,7 +22,7 @@ Within a System, you can loop over transactions of a particular type. These tran - `Hash` returns the hash of the transaction. -In this sample code, an "attack" message type is created, as well as a System that simply logs the Persona Tag of each incoming Attack message. +In this sample code, an "attack" message type is created, as well as a system that simply logs the Persona Tag of each incoming Attack message. ```go package system diff --git a/docs/cardinal/game/system/component.mdx b/docs/cardinal/game/system/component.mdx index 267fb9417..41eff3b94 100644 --- a/docs/cardinal/game/system/component.mdx +++ b/docs/cardinal/game/system/component.mdx @@ -23,7 +23,7 @@ health, err := cardinal.GetComponent[Health](worldCtx, id) | Parameter | Type | Description | |------------|------------------|--------------------------------------------------------------------| | `T` | `type parameter` | A registered component struct that implements the Name method | -| `worldCtx` | `WorldContext` | A WorldContext object passed in to your System or Query definition | +| `worldCtx` | `WorldContext` | A WorldContext object passed in to your system or Query definition | | `id` | `EntityID` | The ID of the entity from which to retrieve the component data. | ### Return Values @@ -55,7 +55,7 @@ err := cardinal.SetComponent[Health](worldCtx, id, newHealth) | Parameter | Type | Description | |-------------|-------------------|--------------------------------------------------------------------| | `T` | `type parameter` | A registered component struct that implements the Name method | -| `worldCtx` | `WorldContext` | A WorldContext object passed in to your System or Query definition | +| `worldCtx` | `WorldContext` | A WorldContext object passed in to your system or query definition | | `id` | `EntityID` | Entity ID of the entity to set the component data for. | | `component` | `T` | Component value to set for the entity. | @@ -91,7 +91,7 @@ err := cardinal.UpdateComponent[Health](worldCtx, id, func(h *Health) *Health { | Parameter | Type | Description | |--------------|------------------|---------------------------------------------------------------------| | `T` | `type parameter` | A registered component struct that implements the Name method | -| `worldCtx` | `WorldContext` | A WorldContext object passed in to your System or Query definition | +| `worldCtx` | `WorldContext` | A WorldContext object passed in to your system or query definition | | `id` | `EntityID` | ID of the entity to perform the component update on. | | `fn` | `func(*T) *T` | Function that modifies the component's value. | @@ -122,7 +122,7 @@ err := cardinal.RemoveComponentFrom[Health](worldCtx, target) | Parameter | Type | Description | |------------|------------------|--------------------------------------------------------------------| | `T` | `type parameter` | A registered component struct that implements the Name method | -| `worldCtx` | `WorldContext` | A WorldContext object passed in to your System or Query definition | +| `worldCtx` | `WorldContext` | A WorldContext object passed in to your system or query definition | | `id` | `EntityID` | Entity ID of the entity to remove the component from. | ### Return Value @@ -153,7 +153,7 @@ err := cardinal.AddComponentTo[Health](worldCtx, target) | Parameter | Type | Description | |------------|------------------|--------------------------------------------------------------------| | `T` | `type parameter` | A registered component struct that implements the Name method | -| `worldCtx` | `WorldContext` | A WorldContext object passed in to your System or Query definition | +| `worldCtx` | `WorldContext` | A WorldContext object passed in to your system or query definition | | `id` | `EntityID` | Entity ID of the entity to add the component to. | ### Return Value diff --git a/docs/cardinal/game/system/entity.mdx b/docs/cardinal/game/system/entity.mdx index e20b54c6f..700771173 100644 --- a/docs/cardinal/game/system/entity.mdx +++ b/docs/cardinal/game/system/entity.mdx @@ -35,7 +35,7 @@ func System(worldCtx cardinal.WorldContext) error { ### Parameters | Parameter | Type | Description | |--------------|-----------------------|----------------------------------------------------------------------------| -| worldCtx | WorldContext | A WorldContext object passed in to your System. | +| worldCtx | WorldContext | A WorldContext object passed in to your system. | | components | ...metadata.Component | Variadic parameter for components to associate with the created entity. | @@ -82,7 +82,7 @@ func System(worldCtx cardinal.WorldContext) error { | Parameter | Type | Description | |--------------|-----------------------|----------------------------------------------------------------------------| -| worldCtx | WorldContext | A WorldContext object passed in to your System. | +| worldCtx | WorldContext | A WorldContext object passed in to your system. | | num | int | The number of entities to create. | | components | ...metadata.Component | Variadic parameter for components to associate with the created entities. | @@ -133,7 +133,7 @@ func System(worldCtx cardinal.WorldContext) error { ### Parameters | Parameter | Type | Description | |--------------|------------------------|--------------------------------------------------| -| worldCtx | WorldContext | A WorldContext object passed in to your System. | +| worldCtx | WorldContext | A WorldContext object passed in to your system. | | id | entity.ID | The entity ID to be removed from the world. | diff --git a/docs/cardinal/game/world/api-reference.mdx b/docs/cardinal/game/world/api-reference.mdx index 6c28a87c5..996b96ebd 100644 --- a/docs/cardinal/game/world/api-reference.mdx +++ b/docs/cardinal/game/world/api-reference.mdx @@ -146,7 +146,7 @@ func WithTickDoneChannel(ch chan<- uint64) WorldOption `RegisterSystems` registers one or more systems to the `World`. Systems are executed in the order of which they were added to the world. ```go -func RegisterSystems(w *World, s ...system.System) +func RegisterSystems(w *World, s ...cardinal.System) ``` ### Example @@ -178,7 +178,7 @@ func main() { `RegisterInitSystems` registers one or more init systems to the `World`. Init systems are executed exactly one time on tick 0. Init systems will not be run when loading a pre-existing world from permanent storage (e.g. on a server restart). ```go -func RegisterInitSystems(world *World, s ...system.System) +func RegisterInitSystems(world *World, s ...cardinal.System) ``` ### Example diff --git a/docs/mint.json b/docs/mint.json index de9a9d586..b3f46c9b9 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -170,9 +170,9 @@ }, "analytics": { "posthog": { - "apiKey": "phc_8py8fUWpEantdzhuP3dTEBBlsXstxTzeIu17NOd7A28", + "apiKey": "phc_K7AsAnWpgF12BqpBLo2WQ6IgrZOZ6s8ac9z6kIeaNET", "apiHost": "https://us.posthog.com" } }, "feedback.thumbsRating": true -} \ No newline at end of file +} diff --git a/e2e/testgames/game/query/location.go b/e2e/testgames/game/query/location.go index 1cf2d00cc..f1d4bbec4 100644 --- a/e2e/testgames/game/query/location.go +++ b/e2e/testgames/game/query/location.go @@ -7,7 +7,6 @@ import ( "github.com/argus-labs/world-engine/example/tester/game/sys" "pkg.world.dev/world-engine/cardinal" - "pkg.world.dev/world-engine/cardinal/query" ) type LocationRequest struct { @@ -40,5 +39,5 @@ func RegisterLocationQuery(world *cardinal.World) error { Y: loc.Y, }, nil }, - query.WithQueryEVMSupport[LocationRequest, LocationReply]()) + cardinal.WithQueryEVMSupport[LocationRequest, LocationReply]()) } diff --git a/e2e/testgames/game/sys/error.go b/e2e/testgames/game/sys/error.go index eb4461069..20d74b6fa 100644 --- a/e2e/testgames/game/sys/error.go +++ b/e2e/testgames/game/sys/error.go @@ -6,14 +6,13 @@ import ( "github.com/argus-labs/world-engine/example/tester/game/msg" "pkg.world.dev/world-engine/cardinal" - "pkg.world.dev/world-engine/cardinal/message" ) // Error is a system that will produce an error for any incoming Error messages. It's // used to test receipt errors. func Error(ctx cardinal.WorldContext) error { return cardinal.EachMessage[msg.ErrorInput, msg.ErrorOutput]( - ctx, func(m message.TxData[msg.ErrorInput]) (msg.ErrorOutput, error) { + ctx, func(m cardinal.TxData[msg.ErrorInput]) (msg.ErrorOutput, error) { err := errors.New(m.Msg.ErrorMsg) return msg.ErrorOutput{}, err }) diff --git a/e2e/testgames/game/sys/join.go b/e2e/testgames/game/sys/join.go index d176406be..6bddbdc29 100644 --- a/e2e/testgames/game/sys/join.go +++ b/e2e/testgames/game/sys/join.go @@ -7,7 +7,6 @@ import ( "github.com/argus-labs/world-engine/example/tester/game/msg" "pkg.world.dev/world-engine/cardinal" - "pkg.world.dev/world-engine/cardinal/message" "pkg.world.dev/world-engine/cardinal/types" ) @@ -16,7 +15,7 @@ var PlayerEntityID = make(map[string]types.EntityID) func Join(ctx cardinal.WorldContext) error { logger := ctx.Logger() return cardinal.EachMessage[msg.JoinInput, msg.JoinOutput]( - ctx, func(jtx message.TxData[msg.JoinInput]) (msg.JoinOutput, error) { + ctx, func(jtx cardinal.TxData[msg.JoinInput]) (msg.JoinOutput, error) { logger.Info().Msgf("got join transaction from: %s", jtx.Tx.PersonaTag) entityID, err := cardinal.Create(ctx, comp.Location{}, comp.Player{}) if err != nil { diff --git a/e2e/testgames/game/sys/move.go b/e2e/testgames/game/sys/move.go index 47dd1038a..7fd096ebc 100644 --- a/e2e/testgames/game/sys/move.go +++ b/e2e/testgames/game/sys/move.go @@ -7,13 +7,12 @@ import ( "github.com/argus-labs/world-engine/example/tester/game/msg" "pkg.world.dev/world-engine/cardinal" - "pkg.world.dev/world-engine/cardinal/message" ) func Move(ctx cardinal.WorldContext) error { logger := ctx.Logger() return cardinal.EachMessage[msg.MoveInput, msg.MoveOutput](ctx, - func(mtx message.TxData[msg.MoveInput]) (msg.MoveOutput, error) { + func(mtx cardinal.TxData[msg.MoveInput]) (msg.MoveOutput, error) { logger.Info().Msgf("got move transaction from: %s", mtx.Tx.PersonaTag) playerEntityID, ok := PlayerEntityID[mtx.Tx.PersonaTag] if !ok { diff --git a/e2e/testgames/gamebenchmark/main.go b/e2e/testgames/gamebenchmark/main.go index b48ea6db7..25057ceeb 100644 --- a/e2e/testgames/gamebenchmark/main.go +++ b/e2e/testgames/gamebenchmark/main.go @@ -11,12 +11,12 @@ import ( "github.com/rotisserie/eris" "pkg.world.dev/world-engine/cardinal" - "pkg.world.dev/world-engine/cardinal/system" ) func initializeSystems( initSystems []cardinal.System, - systems []cardinal.System) ([]cardinal.System, []cardinal.System) { + systems []cardinal.System, +) ([]cardinal.System, []cardinal.System) { initSystems = append(initSystems, sys.InitTenThousandEntities) initSystems = append(initSystems, sys.InitOneHundredEntities) initSystems = append(initSystems, sys.InitTreeEntities) @@ -40,8 +40,8 @@ func main() { } defer pprof.StopCPUProfile() - initsystems := []system.System{} - systems := []system.System{} + initsystems := []cardinal.System{} + systems := []cardinal.System{} options := []cardinal.WorldOption{ cardinal.WithReceiptHistorySize(10), //nolint:gomnd // fine for testing. diff --git a/makefiles/test.mk b/makefiles/test.mk index dbf1e9407..8e7990503 100644 --- a/makefiles/test.mk +++ b/makefiles/test.mk @@ -101,18 +101,20 @@ swaggo-install: swagger: $(MAKE) swaggo-install - swag init -g cardinal/server/server.go -o cardinal/server/docs/ + swag init -g cardinal/server/server.go -o cardinal/server/docs/ --parseDependency swagger-check: $(MAKE) swaggo-install @echo "--> Generate latest Swagger specs" - mkdir -p .tmp/swagger - swag init -g cardinal/server/server.go -o .tmp/swagger + cd cardinal && \ + mkdir -p .tmp/swagger && \ + swag init -g server/server.go -o .tmp/swagger --parseInternal --parseDependency @echo "--> Compare existing and latest Swagger specs" - docker run --rm -v ./:/local-repo ghcr.io/argus-labs/devops-infra-swagger-diff:2.0.0 \ - /local-repo/cardinal/server/docs/swagger.json /local-repo/.tmp/swagger/swagger.json && \ + cd cardinal && \ + docker run --rm -v ./:/local-repo ghcr.io/argus-labs/devops-infra-swagger-diff:2.0.0 \ + /local-repo/server/docs/swagger.json /local-repo/.tmp/swagger/swagger.json && \ echo "swagger-diff: no changes detected" @echo "--> Cleanup" diff --git a/relay/nakama/events/events.go b/relay/nakama/events/events.go index e4b306df3..a299bea29 100644 --- a/relay/nakama/events/events.go +++ b/relay/nakama/events/events.go @@ -4,9 +4,7 @@ import ( "encoding/json" "errors" "fmt" - "net" "sync" - "sync/atomic" "time" "github.com/gorilla/websocket" @@ -16,10 +14,16 @@ import ( "pkg.world.dev/world-engine/relay/nakama/utils" ) +var ( + ErrEventHubIsShuttingDown = errors.New("event hub is shutting down") +) + type EventHub struct { inputConnection *websocket.Conn channels *sync.Map // map[string]chan []byte or []Receipt - didShutdown atomic.Bool + connectMutex *sync.Mutex + didShutdown bool + wsURL string } type TickResults struct { @@ -29,27 +33,60 @@ type TickResults struct { } func NewEventHub(logger runtime.Logger, eventsEndpoint string, cardinalAddress string) (*EventHub, error) { - url := utils.MakeWebSocketURL(eventsEndpoint, cardinalAddress) - webSocketConnection, _, err := websocket.DefaultDialer.Dial(url, nil) //nolint:bodyclose // no need. - for err != nil { - if errors.Is(err, &net.DNSError{}) { + channelMap := sync.Map{} + res := &EventHub{ + channels: &channelMap, + connectMutex: &sync.Mutex{}, + didShutdown: false, + wsURL: utils.MakeWebSocketURL(eventsEndpoint, cardinalAddress), + } + if err := res.connectWithRetry(logger); err != nil { + return nil, eris.Wrap(err, "failed to make initial websocket connection") + } + + return res, nil +} + +// connectWithRetry attempts to make a websocket connection. If Shutdown is called while this method is +// running ErrEventHubIsShuttingDown will be returned +func (eh *EventHub) connectWithRetry(logger runtime.Logger) error { + for tries := 1; ; tries++ { + if err := eh.establishConnection(); errors.Is(err, ErrEventHubIsShuttingDown) { + return ErrEventHubIsShuttingDown + } else if err != nil { // sleep a little try again... - logger.Info("No host found.") - logger.Info(err.Error()) - time.Sleep(2 * time.Second) //nolint:gomnd // its ok. - webSocketConnection, _, err = websocket.DefaultDialer.Dial(url, nil) //nolint:bodyclose // no need. - } else { - return nil, eris.Wrap(err, "") + logger.Info("No host found: %v", err) + time.Sleep(2 * time.Second) //nolint:gomnd // its ok. + continue } + + // success! + break } - channelMap := sync.Map{} - res := EventHub{ - inputConnection: webSocketConnection, - channels: &channelMap, - didShutdown: atomic.Bool{}, + return nil +} + +// establishConnection attempts to establish a connection to cardinal. A previous connection will be closed before +// attempting to dial again. If nil is returned, it means a connection has been made and is ready for use. +func (eh *EventHub) establishConnection() error { + eh.connectMutex.Lock() + defer eh.connectMutex.Unlock() + if eh.didShutdown { + return ErrEventHubIsShuttingDown + } + + if eh.inputConnection != nil { + if err := eh.inputConnection.Close(); err != nil { + return eris.Wrap(err, "failed to close old connection") + } + eh.inputConnection = nil + } + webSocketConnection, _, err := websocket.DefaultDialer.Dial(eh.wsURL, nil) //nolint:bodyclose // no need. + if err != nil { + return eris.Wrap(err, "websocket dial failed") } - res.didShutdown.Store(false) - return &res, nil + eh.inputConnection = webSocketConnection + return nil } func (eh *EventHub) SubscribeToEvents(session string) chan []byte { @@ -83,25 +120,56 @@ func (eh *EventHub) Unsubscribe(session string) { } func (eh *EventHub) Shutdown() { - eh.didShutdown.Store(true) + eh.connectMutex.Lock() + defer eh.connectMutex.Unlock() + if eh.didShutdown { + return + } + eh.didShutdown = true + if eh.inputConnection != nil { + _ = eh.inputConnection.Close() + } +} + +// readMessage will block until a new message is available on the websocket. If any errors are encountered, +// the socket will be closed and a new connection will attempt to be established. This method blocks until +// a message has successfully been fetched, or until EventHub.Shutdown is called. +func (eh *EventHub) readMessage(log runtime.Logger) (messageType int, message []byte, err error) { + for { + messageType, message, err = eh.inputConnection.ReadMessage() + if err != nil { + log.Warn("read from websocket failed: %v", err) + // Something went wrong. Try to reestablish the connection. + if err = eh.connectWithRetry(log); err != nil { + return 0, nil, eris.Wrap(err, "failed to reestablish a websocket connection") + } + } else { + break + } + } + return messageType, message, nil } // Dispatch continually drains eh.inputConnection (events from cardinal) and sends copies to all subscribed channels. // This function is meant to be called in a goroutine. func (eh *EventHub) Dispatch(log runtime.Logger) error { - var err error - for !eh.didShutdown.Load() { - var messageType int - var message []byte - messageType, message, err = eh.inputConnection.ReadMessage() // will block - if err != nil { - err = eris.Wrap(err, "") - eh.Shutdown() - continue + defer eh.Shutdown() + defer func() { + eh.channels.Range(func(key any, _ any) bool { + log.Info(fmt.Sprintf("shutting down: %s", key.(string))) + eh.Unsubscribe(key.(string)) + return true + }) + }() + for { + messageType, message, err := eh.readMessage(log) // will block + if errors.Is(err, ErrEventHubIsShuttingDown) { + return nil + } else if err != nil { + return eris.Wrap(err, "") } if messageType != websocket.TextMessage { - eh.Shutdown() - continue + return eris.Errorf("unexpected message type %v on web socket", messageType) } receivedTickResults := TickResults{} err = json.Unmarshal(message, &receivedTickResults) @@ -125,11 +193,4 @@ func (eh *EventHub) Dispatch(log runtime.Logger) error { return true }) } - eh.channels.Range(func(key any, _ any) bool { - log.Info(fmt.Sprintf("shutting down: %s", key.(string))) - eh.Unsubscribe(key.(string)) - return true - }) - err = errors.Join(eris.Wrap(eh.inputConnection.Close(), ""), err) - return err } diff --git a/relay/nakama/events/events_test.go b/relay/nakama/events/events_test.go index 540d9ce36..6fb7c1e1d 100644 --- a/relay/nakama/events/events_test.go +++ b/relay/nakama/events/events_test.go @@ -3,6 +3,7 @@ package events import ( "encoding/json" "log" + "math" "net/http" "net/http/httptest" "strings" @@ -10,14 +11,30 @@ import ( "time" "github.com/gorilla/websocket" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "pkg.world.dev/world-engine/assert" "pkg.world.dev/world-engine/relay/nakama/testutils" ) var upgrader = websocket.Upgrader{} // use default options +var ( + // tickResultSentinel is a special tick result value that signals to the mock websocker server that the active + // websocket should be closed. This doesn't prevent future requests from re-connecting to the websocket to consume + // more data. + closeWebSocketSentinel = TickResults{ + Tick: math.MaxUint64, + } +) + +func isCloseWebSocketSentinel(tr TickResults) bool { + return tr.Tick == closeWebSocketSentinel.Tick +} + +// setupMockWebSocketServer creates a test-only server that allows websocket connections. Any TickResults sent on the +// given channel will be pushed to the websocket. If the sentinel closeWebSocketSentinel TickResult arrives on the +// input channel, the websocket will be closed. Note, the server is still active, so another websocket connection can +// be made. The server is closed during the t.Cleanup phase of the test. func setupMockWebSocketServer(t *testing.T, ch chan TickResults) *httptest.Server { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c, err := upgrader.Upgrade(w, r, nil) @@ -27,6 +44,10 @@ func setupMockWebSocketServer(t *testing.T, ch chan TickResults) *httptest.Serve } defer c.Close() for msg := range ch { + if isCloseWebSocketSentinel(msg) { + assert.NilError(t, c.Close()) + break + } data, err := json.Marshal(msg) if err != nil { log.Fatal("failed to marshal event") @@ -60,12 +81,11 @@ func TestEventHubIntegration(t *testing.T) { // Subscribe to the event hub session := "testSession" eventChan := eventHub.SubscribeToEvents(session) + dispatchErrChan := make(chan error) // Start dispatching events go func() { - if err := eventHub.Dispatch(logger); err != nil { - t.Logf("Error dispatching: %v", err) - } + dispatchErrChan <- eventHub.Dispatch(logger) }() // Simulate Cardinal sending TickResults to the Nakama EventHub @@ -89,7 +109,7 @@ func TestEventHubIntegration(t *testing.T) { case event := <-eventChan: jsonMap := make(map[string]any) err = json.Unmarshal(event, &jsonMap) - require.NoError(t, err) + assert.NilError(t, err) msg, ok2 := jsonMap["message"] assert.True(t, ok2) msgString, ok2 := msg.(string) @@ -107,4 +127,71 @@ func TestEventHubIntegration(t *testing.T) { // Cleanup and shutdown eventHub.Shutdown() + + assert.NilError(t, <-dispatchErrChan) +} + +func TestEventHub_WhenWebSocketDisconnects_EventHubAutomaticallyReconnects(t *testing.T) { + ch := make(chan TickResults, 1) + mockServer := setupMockWebSocketServer(t, ch) + t.Cleanup(func() { + close(ch) + }) + logger := &testutils.FakeLogger{} + eventHub, err := NewEventHub(logger, eventsEndpoint, strings.TrimPrefix(mockServer.URL, "http://")) + assert.NilError(t, err) + + session := "reconnectSession" + eventChan := eventHub.SubscribeToEvents(session) + + dispatchErrChan := make(chan error) + go func() { + dispatchErrChan <- eventHub.Dispatch(logger) + }() + + wantMsg := `{"message": "some-message"}` + tickResults := TickResults{ + Tick: 100, + Receipts: nil, + Events: [][]byte{ + []byte(wantMsg), + }, + } + + var gotTickResults []byte + + // This tick result will be transmitted across the websocket + ch <- tickResults + + // Make sure the tick results is pushed to the event channel in a reasonable amount of time + select { + case gotTickResults = <-eventChan: + break + case <-time.After(5 * time.Second): + assert.Fail(t, "timeout while waiting for first tick result") + } + + assert.Contains(t, string(gotTickResults), wantMsg) + + // Pushing this sentinel value to the channel will close the active websocket connection. EventHub should + // attempt to reconnect in the background. + ch <- closeWebSocketSentinel + + // This tick result will be transmitted across the websocket + ch <- tickResults + + // Make sure the tick results is pushed to the event channel in a reasonable amount of time + select { + case gotTickResults = <-eventChan: + break + case <-time.After(5 * time.Second): + assert.Fail(t, "timeout while waiting for first tick result") + } + + assert.Contains(t, string(gotTickResults), wantMsg) + + // Cleanup and shutdown + eventHub.Shutdown() + + assert.NilError(t, <-dispatchErrChan) } diff --git a/relay/nakama/events/notifications.go b/relay/nakama/events/notifications.go index ef7ad4212..0b451af4b 100644 --- a/relay/nakama/events/notifications.go +++ b/relay/nakama/events/notifications.go @@ -81,7 +81,11 @@ func (r *Notifier) sendNotifications(ch chan []Receipt) { for { select { - case receipts := <-ch: + case receipts, ok := <-ch: + if !ok { + // The incoming notification channel has been closed. No more receipts will arrive. + return + } if err := r.handleReceipt(receipts); err != nil { r.logger.Debug("failed to send batch of receipts of len %d: %v", len(receipts), err) } diff --git a/relay/nakama/events/notifications_test.go b/relay/nakama/events/notifications_test.go index 8dd3869dc..25268cd9a 100644 --- a/relay/nakama/events/notifications_test.go +++ b/relay/nakama/events/notifications_test.go @@ -15,13 +15,13 @@ import ( "pkg.world.dev/world-engine/relay/nakama/testutils" ) -var ( +const ( eventsEndpoint = "events" ) // Test that the Notifications system works as expected with the Dispatcher and a Mock Server func TestNotifierIntegrationWithEventHub(t *testing.T) { - ch := make(chan TickResults) + ch := make(chan TickResults, 1) nk := mocks.NewNakamaModule(t) logger := &testutils.FakeLogger{} mockServer := setupMockWebSocketServer(t, ch) @@ -49,37 +49,48 @@ func TestNotifierIntegrationWithEventHub(t *testing.T) { Persistent: false, }, } - nk.On("NotificationsSend", mock.Anything, expectedNotifications).Return(nil).Once() + sendNotificationSuccessful := make(chan bool) + nk.On("NotificationsSend", mock.Anything, expectedNotifications). + Return(nil). + Once(). + Run(func(mock.Arguments) { + sendNotificationSuccessful <- true + }) + dispatchErrCh := make(chan error) // Start dispatching events go func() { - if err := eh.Dispatch(logger); err != nil { - t.Logf("Error dispatching: %v", err) - } + dispatchErrCh <- eh.Dispatch(logger) }() // Simulate Cardinal sending TickResults to the Nakama EventHub - go func() { - event, err := json.Marshal(map[string]any{"message": "test event"}) - if err != nil { - t.Error("failed to marshal map") - return - } - tr := TickResults{ - Tick: 100, - Receipts: nil, - Events: nil, - } - tr.Events = append(tr.Events, event) - tr.Receipts = append(tr.Receipts, Receipt{ - TxHash: txHash, - Result: map[string]any{"status": "success"}, - Errors: []string{}, - }) - ch <- tr - }() + event, err := json.Marshal(map[string]any{"message": "test event"}) + if err != nil { + t.Error("failed to marshal map") + return + } + tr := TickResults{ + Tick: 100, + Receipts: nil, + Events: nil, + } + tr.Events = append(tr.Events, event) + tr.Receipts = append(tr.Receipts, Receipt{ + TxHash: txHash, + Result: map[string]any{"status": "success"}, + Errors: []string{}, + }) + ch <- tr + + select { + case <-time.After(5 * time.Second): + assert.Fail(t, "timeout while waiting for a notification to be sent") + case <-sendNotificationSuccessful: // success + break + } - time.Sleep(time.Second) + eh.Shutdown() + assert.NoError(t, <-dispatchErrCh) } func TestAddTxHashToPendingNotifications(t *testing.T) {