diff --git a/go.mod b/go.mod index b53fd4e..c8d68f6 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,6 @@ require ( golang.org/x/term v0.32.0 ) -require golang.org/x/sys v0.33.0 // indirect +require ( + golang.org/x/sys v0.33.0 // indirect +) diff --git a/go.sum b/go.sum index c67480a..26e9566 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= diff --git a/internal/game/ecs/ecs.go b/internal/ecs/ecs.go similarity index 55% rename from internal/game/ecs/ecs.go rename to internal/ecs/ecs.go index e4c5fc5..63816e7 100644 --- a/internal/game/ecs/ecs.go +++ b/internal/ecs/ecs.go @@ -1,10 +1,13 @@ package ecs import ( + "iter" + "maps" "slices" "time" "code.haedhutner.dev/mvv/LastMUD/internal/logging" + "code.haedhutner.dev/mvv/LastMUD/internal/util" "github.com/google/uuid" ) @@ -50,6 +53,24 @@ func (cs *ComponentStorage[T]) ComponentType() ComponentType { return cs.forType } +func (cs *ComponentStorage[T]) Entities() iter.Seq[Entity] { + return maps.Keys(cs.storage) +} + +func (cs *ComponentStorage[T]) Query(query func(comp T) bool) iter.Seq[Entity] { + return func(yield func(Entity) bool) { + for k, v := range cs.storage { + if !query(v) { + continue + } + + if !yield(k) { + return + } + } + } +} + func (cs *ComponentStorage[T]) Set(e Entity, component T) { cs.storage[e] = component } @@ -85,7 +106,7 @@ func (s *System) Priority() int { return s.priority } -func (s *System) DoWork(world *World, delta time.Duration) { +func (s *System) Execute(world *World, delta time.Duration) { err := s.work(world, delta) if err != nil { @@ -94,18 +115,16 @@ func (s *System) DoWork(world *World, delta time.Duration) { } type World struct { - systems []*System - componentsByType map[ComponentType]any - componentsByEntity map[Entity]map[ComponentType]any - resources map[Resource]any + systems []*System + componentsByType map[ComponentType]any + resources map[Resource]any } func CreateWorld() (world *World) { world = &World{ - systems: []*System{}, - componentsByType: map[ComponentType]any{}, - componentsByEntity: map[Entity]map[ComponentType]any{}, // TODO: Can't figure out use-case right now - resources: map[Resource]any{}, + systems: []*System{}, + componentsByType: map[ComponentType]any{}, + resources: map[Resource]any{}, } return @@ -113,17 +132,21 @@ func CreateWorld() (world *World) { func (w *World) Tick(delta time.Duration) { for _, s := range w.systems { - s.DoWork(w, delta) + s.Execute(w, delta) } } func DeleteEntity(world *World, entity Entity) { for _, s := range world.componentsByType { - storage, ok := s.(*ComponentStorage[Component]) + storage := s.(*ComponentStorage[Component]) - if ok { - storage.Delete(entity) - } + storage.Delete(entity) + } +} + +func DeleteEntities(world *World, entities ...Entity) { + for _, e := range entities { + DeleteEntity(world, e) } } @@ -152,19 +175,20 @@ func RemoveResource(world *World, r Resource) { delete(world.resources, r) } -func RegisterComponent[T Component](world *World, compType ComponentType) { +func registerComponent[T Component](world *World, compType ComponentType) { + if _, ok := world.componentsByType[compType]; ok { + return + } + world.componentsByType[compType] = CreateComponentStorage[T](compType) } func SetComponent[T Component](world *World, entity Entity, component T) { + registerComponent[T](world, component.Type()) + compStorage := world.componentsByType[component.Type()].(*ComponentStorage[T]) + compStorage.Set(entity, component) - - // if _, ok := world.componentsByEntity[entity]; !ok { - // world.componentsByEntity[entity] = map[ComponentType]any{} - // } - - // world.componentsByEntity[entity][component.Type()] = component } func GetComponent[T Component](world *World, entity Entity) (component T, exists bool) { @@ -182,7 +206,59 @@ func DeleteComponent[T Component](world *World, entity Entity) { func GetComponentStorage[T Component](world *World) (compStorage *ComponentStorage[T]) { var zero T - return world.componentsByType[zero.Type()].(*ComponentStorage[T]) + compType := zero.Type() + + registerComponent[T](world, compType) + + return world.componentsByType[compType].(*ComponentStorage[T]) +} + +func IterateEntitiesWithComponent[T Component](world *World) iter.Seq[Entity] { + storage := GetComponentStorage[T](world) + + return storage.Entities() +} + +func QueryEntitiesWithComponent[T Component](world *World, query func(comp T) bool) iter.Seq[Entity] { + storage := GetComponentStorage[T](world) + + return storage.Query(query) +} + +func FindEntitiesWithComponents(world *World, componentTypes ...ComponentType) (entities []Entity) { + entities = []Entity{} + + isFirst := true + + for _, compType := range componentTypes { + // If we've gone through at least one component, and we have an empty result already, return it + if !isFirst && len(entities) == 0 { + return + } + + storage, ok := world.componentsByType[compType].(*ComponentStorage[Component]) + + // If we can't find the storage for this component, then it hasn't been used yet. + // Therefore, no entity could have all components requested. Return empty. + if !ok { + return []Entity{} + } + + // For the first component, simply add all entities to the array + if isFirst { + for entity := range storage.Entities() { + entities = append(entities, entity) + } + + isFirst = false + continue + } + + // For later components, intersect + entities = util.IntersectSliceWithIterator(entities, storage.Entities()) + } + + return entities } func RegisterSystem(world *World, s *System) { @@ -194,3 +270,9 @@ func RegisterSystem(world *World, s *System) { }, ) } + +func RegisterSystems(world *World, systems ...*System) { + for _, s := range systems { + RegisterSystem(world, s) + } +} diff --git a/internal/game/ecs/error.go b/internal/ecs/error.go similarity index 100% rename from internal/game/ecs/error.go rename to internal/ecs/error.go diff --git a/internal/game/components/common.go b/internal/game/components/common.go deleted file mode 100644 index cad68a5..0000000 --- a/internal/game/components/common.go +++ /dev/null @@ -1,29 +0,0 @@ -package components - -import "code.haedhutner.dev/mvv/LastMUD/internal/game/ecs" - -const ( - TypeName ecs.ComponentType = iota - TypeDescription - TypePlayerState - TypeInRoom - TypeNeighbors - TypeIsRoom - TypeIsPlayer -) - -type NameComponent struct { - Name string -} - -func (c NameComponent) Type() ecs.ComponentType { - return TypeName -} - -type DescriptionComponent struct { - Description string -} - -func (c DescriptionComponent) Type() ecs.ComponentType { - return TypeDescription -} diff --git a/internal/game/components/player.go b/internal/game/components/player.go deleted file mode 100644 index 93d02e2..0000000 --- a/internal/game/components/player.go +++ /dev/null @@ -1,35 +0,0 @@ -package components - -import "code.haedhutner.dev/mvv/LastMUD/internal/game/ecs" - -type PlayerState = byte - -const ( - PlayerStateJoining PlayerState = iota - PlayerStateLoggingIn - PlayerStateRegistering - PlayerStatePlaying - PlayerStateLeaving -) - -type PlayerStateComponent struct { - State PlayerState -} - -func (c PlayerStateComponent) Type() ecs.ComponentType { - return TypePlayerState -} - -type InRoomComponent struct { - Room ecs.Entity -} - -func (c InRoomComponent) Type() ecs.ComponentType { - return TypeInRoom -} - -type IsPlayerComponent struct{} - -func (c IsPlayerComponent) Type() ecs.ComponentType { - return TypeIsPlayer -} diff --git a/internal/game/components/room.go b/internal/game/components/room.go deleted file mode 100644 index 5cab687..0000000 --- a/internal/game/components/room.go +++ /dev/null @@ -1,18 +0,0 @@ -package components - -import "code.haedhutner.dev/mvv/LastMUD/internal/game/ecs" - -type IsRoomComponent struct { -} - -func (c IsRoomComponent) Type() ecs.ComponentType { - return TypeIsRoom -} - -type NeighborsComponent struct { - North, South, East, West ecs.Entity -} - -func (c NeighborsComponent) Type() ecs.ComponentType { - return TypeNeighbors -} diff --git a/internal/game/data/common.go b/internal/game/data/common.go new file mode 100644 index 0000000..2d8b73f --- /dev/null +++ b/internal/game/data/common.go @@ -0,0 +1,61 @@ +package data + +import ( + "code.haedhutner.dev/mvv/LastMUD/internal/ecs" + "github.com/google/uuid" +) + +const ( + TypeName ecs.ComponentType = iota + TypeDescription + TypePlayerState + TypeInRoom + TypeNeighbors + TypeIsRoom + TypeIsPlayer + TypeCommandString + TypeEntity + TypeEvent + TypeConnectionId + TypeContents +) + +type EntityComponent struct { + Entity ecs.Entity +} + +func (e EntityComponent) Type() ecs.ComponentType { + return TypeEntity +} + +type NameComponent struct { + Name string +} + +func (c NameComponent) Type() ecs.ComponentType { + return TypeName +} + +type DescriptionComponent struct { + Description string +} + +func (c DescriptionComponent) Type() ecs.ComponentType { + return TypeDescription +} + +type CommandStringComponent struct { + Command string +} + +func (cs CommandStringComponent) Type() ecs.ComponentType { + return TypeCommandString +} + +type ConnectionIdComponent struct { + ConnectionId uuid.UUID +} + +func (cid ConnectionIdComponent) Type() ecs.ComponentType { + return TypeConnectionId +} diff --git a/internal/game/data/event.go b/internal/game/data/event.go new file mode 100644 index 0000000..b0ea27b --- /dev/null +++ b/internal/game/data/event.go @@ -0,0 +1,159 @@ +package data + +import ( + "code.haedhutner.dev/mvv/LastMUD/internal/ecs" + "github.com/google/uuid" +) + +type EventType string + +const ( + EventPlayerConnect EventType = "PlayerConnect" + EventPlayerDisconnect = "PlayerDisconnect" + EventPlayerCommand = "PlayerCommand" + EventPlayerSpeak = "PlayerSpeak" +) + +type EventComponent struct { + EventType EventType +} + +func (is EventComponent) Type() ecs.ComponentType { + return TypeEvent +} + +func CreatePlayerConnectEvent(world *ecs.World, connectionId uuid.UUID) { + event := ecs.NewEntity() + + ecs.SetComponent(world, event, EventComponent{EventType: EventPlayerConnect}) + ecs.SetComponent(world, event, ConnectionIdComponent{ConnectionId: connectionId}) +} + +func CreatePlayerDisconnectEvent(world *ecs.World, connectionId uuid.UUID) { + event := ecs.NewEntity() + + ecs.SetComponent(world, event, EventComponent{EventType: EventPlayerDisconnect}) + ecs.SetComponent(world, event, ConnectionIdComponent{ConnectionId: connectionId}) +} + +func CreatePlayerCommandEvent(world *ecs.World, connectionId uuid.UUID, command string) { + event := ecs.NewEntity() + + ecs.SetComponent(world, event, EventComponent{EventType: EventPlayerCommand}) + ecs.SetComponent(world, event, ConnectionIdComponent{ConnectionId: connectionId}) + ecs.SetComponent(world, event, CommandStringComponent{Command: command}) +} + +// type PlayerJoinEvent struct { +// connectionId uuid.UUID +// } + +// func (game *LastMUDGame) CreatePlayerJoinEvent(connId uuid.UUID) *PlayerJoinEvent { +// return &PlayerJoinEvent{ +// connectionId: connId, +// } +// } + +// func (pje *PlayerJoinEvent) Type() event.EventType { +// return PlayerJoin +// } + +// func (pje *PlayerJoinEvent) Handle(game *LastMUDGame, delta time.Duration) { +// p, err := CreatePlayer(game.world.World, pje.connectionId, components.PlayerStateJoining) + +// if err != nil { +// logging.Error("Unabled to create player: ", err) +// } + +// game.enqeueOutput(game.CreateOutput(p.AsUUID(), []byte("Welcome to LastMUD!"))) +// game.enqeueOutput(game.CreateOutput(p.AsUUID(), []byte("Please enter your name:"))) +// } + +// type PlayerLeaveEvent struct { +// connectionId uuid.UUID +// } + +// func (game *LastMUDGame) CreatePlayerLeaveEvent(connId uuid.UUID) *PlayerLeaveEvent { +// return &PlayerLeaveEvent{ +// connectionId: connId, +// } +// } + +// func (ple *PlayerLeaveEvent) Type() event.EventType { +// return PlayerLeave +// } + +// func (ple *PlayerLeaveEvent) Handle(game *LastMUDGame, delta time.Duration) { +// ecs.DeleteEntity(game.world.World, ecs.CreateEntity(ple.connectionId)) +// } + +// type PlayerCommandEvent struct { +// connectionId uuid.UUID +// command *command.CommandContext +// } + +// func (game *LastMUDGame) CreatePlayerCommandEvent(connId uuid.UUID, cmdString string) (event *PlayerCommandEvent, err error) { +// cmdCtx, err := command.CreateCommandContext(game.commandRegistry(), cmdString) + +// if err != nil { +// return nil, err +// } + +// event = &PlayerCommandEvent{ +// connectionId: connId, +// command: cmdCtx, +// } + +// return +// } + +// func (pce *PlayerCommandEvent) Type() event.EventType { +// return PlayerCommand +// } + +// func (pce *PlayerCommandEvent) Handle(game *LastMUDGame, delta time.Duration) { +// if player == nil { +// logging.Error("Unable to handle player command from player with id", pce.connectionId, ": Player does not exist") +// return +// } + +// event := pce.parseCommandIntoEvent(game, player) +// } + +// func (pce *PlayerCommandEvent) parseCommandIntoEvent(game *LastMUDGame, player ecs.Entity) event.Event { +// switch pce.command.Command().Definition().Name() { +// case SayCommand: +// speech, err := pce.command.Command().Parameters()[0].AsString() + +// if err != nil { +// logging.Error("Unable to handle player speech from player with id", pce.connectionId, ": Speech could not be parsed: ", err.Error()) +// return nil +// } + +// return game.CreatePlayerSayEvent(player, speech) +// } + +// return nil +// } + +// type PlayerSayEvent struct { +// player *Player +// speech string +// } + +// func (game *LastMUDGame) CreatePlayerSayEvent(player *Player, speech string) *PlayerSayEvent { +// return &PlayerSayEvent{ +// player: player, +// speech: speech, +// } +// } + +// func (pse *PlayerSayEvent) Type() EventType { +// return PlayerSpeak +// } + +// func (pse *PlayerSayEvent) Handle(game *LastMUDGame, delta time.Duration) { +// for _, p := range pse.player.CurrentRoom().Players() { +// game.enqeueOutput(game.CreateOutput(p.Identity(), []byte(pse.player.id.String()+" in "+pse.player.CurrentRoom().Name+": "+pse.speech))) +// } +// } diff --git a/internal/game/data/output.go b/internal/game/data/output.go new file mode 100644 index 0000000..a54e60d --- /dev/null +++ b/internal/game/data/output.go @@ -0,0 +1,21 @@ +package data + +import ( + "code.haedhutner.dev/mvv/LastMUD/internal/ecs" + "github.com/google/uuid" +) + +type ContentsComponent struct { + Contents []byte +} + +func (cc ContentsComponent) Type() ecs.ComponentType { + return TypeContents +} + +func CreateGameOutput(world *ecs.World, connectionId uuid.UUID, contents []byte) { + gameOutput := ecs.NewEntity() + + ecs.SetComponent(world, gameOutput, ConnectionIdComponent{ConnectionId: connectionId}) + ecs.SetComponent(world, gameOutput, ContentsComponent{Contents: contents}) +} diff --git a/internal/game/data/player.go b/internal/game/data/player.go new file mode 100644 index 0000000..5ba4419 --- /dev/null +++ b/internal/game/data/player.go @@ -0,0 +1,56 @@ +package data + +import ( + "code.haedhutner.dev/mvv/LastMUD/internal/ecs" + "github.com/google/uuid" +) + +type PlayerState = byte + +const ( + PlayerStateJoining PlayerState = iota + PlayerStateLoggingIn + PlayerStateRegistering + PlayerStatePlaying + PlayerStateLeaving +) + +type PlayerStateComponent struct { + State PlayerState +} + +func (c PlayerStateComponent) Type() ecs.ComponentType { + return TypePlayerState +} + +type InRoomComponent struct { + Room ecs.Entity +} + +func (c InRoomComponent) Type() ecs.ComponentType { + return TypeInRoom +} + +type IsPlayerComponent struct{} + +func (c IsPlayerComponent) Type() ecs.ComponentType { + return TypeIsPlayer +} + +func CreatePlayer(world *ecs.World, id uuid.UUID, state PlayerState) (entity ecs.Entity, err error) { + entity = ecs.NewEntity() + + defaultRoom, err := ecs.GetResource[ecs.Entity](world, ResourceDefaultRoom) + + if err != nil { + return + } + + ecs.SetComponent(world, entity, ConnectionIdComponent{ConnectionId: id}) + ecs.SetComponent(world, entity, PlayerStateComponent{State: state}) + ecs.SetComponent(world, entity, NameComponent{Name: id.String()}) + ecs.SetComponent(world, entity, InRoomComponent{Room: defaultRoom}) + ecs.SetComponent(world, entity, IsPlayerComponent{}) + + return +} diff --git a/internal/game/data/room.go b/internal/game/data/room.go new file mode 100644 index 0000000..8309a40 --- /dev/null +++ b/internal/game/data/room.go @@ -0,0 +1,33 @@ +package data + +import "code.haedhutner.dev/mvv/LastMUD/internal/ecs" + +type IsRoomComponent struct { +} + +func (c IsRoomComponent) Type() ecs.ComponentType { + return TypeIsRoom +} + +type NeighborsComponent struct { + North, South, East, West ecs.Entity +} + +func (c NeighborsComponent) Type() ecs.ComponentType { + return TypeNeighbors +} + +func CreateRoom( + world *ecs.World, + name, description string, + north, south, east, west ecs.Entity, +) ecs.Entity { + entity := ecs.NewEntity() + + ecs.SetComponent(world, entity, IsRoomComponent{}) + ecs.SetComponent(world, entity, NameComponent{Name: name}) + ecs.SetComponent(world, entity, DescriptionComponent{Description: description}) + ecs.SetComponent(world, entity, NeighborsComponent{North: north, South: south, East: east, West: west}) + + return entity +} diff --git a/internal/game/world.go b/internal/game/data/world.go similarity index 91% rename from internal/game/world.go rename to internal/game/data/world.go index fb89d42..534d074 100644 --- a/internal/game/world.go +++ b/internal/game/data/world.go @@ -1,8 +1,7 @@ -package game +package data import ( - "code.haedhutner.dev/mvv/LastMUD/internal/game/components" - "code.haedhutner.dev/mvv/LastMUD/internal/game/ecs" + "code.haedhutner.dev/mvv/LastMUD/internal/ecs" ) const ( @@ -70,32 +69,32 @@ func CreateGameWorld() (gw *GameWorld) { ecs.NilEntity(), ) - ecs.SetComponent(gw.World, forest, components.NeighborsComponent{ + ecs.SetComponent(gw.World, forest, NeighborsComponent{ North: cabin, South: graveyard, East: lake, West: chapel, }) - ecs.SetComponent(gw.World, cabin, components.NeighborsComponent{ + ecs.SetComponent(gw.World, cabin, NeighborsComponent{ South: graveyard, West: chapel, East: lake, }) - ecs.SetComponent(gw.World, chapel, components.NeighborsComponent{ + ecs.SetComponent(gw.World, chapel, NeighborsComponent{ North: cabin, South: graveyard, East: forest, }) - ecs.SetComponent(gw.World, lake, components.NeighborsComponent{ + ecs.SetComponent(gw.World, lake, NeighborsComponent{ West: forest, North: cabin, South: graveyard, }) - ecs.SetComponent(gw.World, graveyard, components.NeighborsComponent{ + ecs.SetComponent(gw.World, graveyard, NeighborsComponent{ North: forest, West: chapel, East: lake, diff --git a/internal/game/entities.go b/internal/game/entities.go deleted file mode 100644 index d722b4b..0000000 --- a/internal/game/entities.go +++ /dev/null @@ -1,39 +0,0 @@ -package game - -import ( - "code.haedhutner.dev/mvv/LastMUD/internal/game/components" - "code.haedhutner.dev/mvv/LastMUD/internal/game/ecs" - "github.com/google/uuid" -) - -func CreatePlayer(world *ecs.World, id uuid.UUID, state components.PlayerState) (entity ecs.Entity, err error) { - entity = ecs.CreateEntity(id) - - defaultRoom, err := ecs.GetResource[ecs.Entity](world, ResourceDefaultRoom) - - if err != nil { - return - } - - ecs.SetComponent(world, entity, components.PlayerStateComponent{State: state}) - ecs.SetComponent(world, entity, components.NameComponent{Name: id.String()}) - ecs.SetComponent(world, entity, components.InRoomComponent{Room: defaultRoom}) - ecs.SetComponent(world, entity, components.IsPlayerComponent{}) - - return -} - -func CreateRoom( - world *ecs.World, - name, description string, - north, south, east, west ecs.Entity, -) ecs.Entity { - entity := ecs.NewEntity() - - ecs.SetComponent(world, entity, components.IsRoomComponent{}) - ecs.SetComponent(world, entity, components.NameComponent{Name: name}) - ecs.SetComponent(world, entity, components.DescriptionComponent{Description: description}) - ecs.SetComponent(world, entity, components.NeighborsComponent{North: north, South: south, East: east, West: west}) - - return entity -} diff --git a/internal/game/event.go b/internal/game/event.go deleted file mode 100644 index d7257b2..0000000 --- a/internal/game/event.go +++ /dev/null @@ -1,75 +0,0 @@ -package game - -import ( - "fmt" - "time" - - "code.haedhutner.dev/mvv/LastMUD/internal/logging" -) - -type EventType int - -const ( - PlayerJoin EventType = iota - PlayerCommand - PlayerLeave - - PlayerSpeak -) - -func (et EventType) String() string { - switch et { - case PlayerCommand: - return "PlayerCommand" - case PlayerJoin: - return "PlayerJoin" - case PlayerLeave: - return "PlayerLeave" - case PlayerSpeak: - return "PlayerSpeak" - default: - return "Unknown" - } -} - -type GameEvent interface { - Type() EventType - Handle(game *LastMUDGame, delta time.Duration) -} - -func stringifyEvent(ev GameEvent) string { - return ev.Type().String() + fmt.Sprintf(`%+v`, ev) -} - -type EventBus struct { - events chan GameEvent -} - -func CreateEventBus(capacity int) *EventBus { - return &EventBus{ - events: make(chan GameEvent, capacity), - } -} - -func (eb *EventBus) HasNext() bool { - return len(eb.events) > 0 -} - -func (eb *EventBus) Pop() (event GameEvent) { - select { - case event := <-eb.events: - logging.Debug("Popped event ", stringifyEvent(event)) - return event - default: - return nil - } -} - -func (eb *EventBus) Push(event GameEvent) { - eb.events <- event - logging.Debug("Enqueued event ", stringifyEvent(event)) -} - -func (eb *EventBus) close() { - close(eb.events) -} diff --git a/internal/game/events.go b/internal/game/events.go deleted file mode 100644 index d69db75..0000000 --- a/internal/game/events.go +++ /dev/null @@ -1,125 +0,0 @@ -package game - -import ( - "time" - - "code.haedhutner.dev/mvv/LastMUD/internal/game/command" - "code.haedhutner.dev/mvv/LastMUD/internal/game/components" - "code.haedhutner.dev/mvv/LastMUD/internal/game/ecs" - "code.haedhutner.dev/mvv/LastMUD/internal/logging" - "github.com/google/uuid" -) - -type PlayerJoinEvent struct { - connectionId uuid.UUID -} - -func (game *LastMUDGame) CreatePlayerJoinEvent(connId uuid.UUID) *PlayerJoinEvent { - return &PlayerJoinEvent{ - connectionId: connId, - } -} - -func (pje *PlayerJoinEvent) Type() EventType { - return PlayerJoin -} - -func (pje *PlayerJoinEvent) Handle(game *LastMUDGame, delta time.Duration) { - p, err := CreatePlayer(game.world.World, pje.connectionId, components.PlayerStateJoining) - - if err != nil { - logging.Error("Unabled to create player: ", err) - } - - game.enqeueOutput(game.CreateOutput(p.AsUUID(), []byte("Welcome to LastMUD!"))) - game.enqeueOutput(game.CreateOutput(p.AsUUID(), []byte("Please enter your name:"))) -} - -type PlayerLeaveEvent struct { - connectionId uuid.UUID -} - -func (game *LastMUDGame) CreatePlayerLeaveEvent(connId uuid.UUID) *PlayerLeaveEvent { - return &PlayerLeaveEvent{ - connectionId: connId, - } -} - -func (ple *PlayerLeaveEvent) Type() EventType { - return PlayerLeave -} - -func (ple *PlayerLeaveEvent) Handle(game *LastMUDGame, delta time.Duration) { - ecs.DeleteEntity(game.world.World, ecs.CreateEntity(ple.connectionId)) -} - -type PlayerCommandEvent struct { - connectionId uuid.UUID - command *command.CommandContext -} - -func (game *LastMUDGame) CreatePlayerCommandEvent(connId uuid.UUID, cmdString string) (event *PlayerCommandEvent, err error) { - cmdCtx, err := command.CreateCommandContext(game.commandRegistry(), cmdString) - - if err != nil { - return nil, err - } - - event = &PlayerCommandEvent{ - connectionId: connId, - command: cmdCtx, - } - - return -} - -func (pce *PlayerCommandEvent) Type() EventType { - return PlayerCommand -} - -func (pce *PlayerCommandEvent) Handle(game *LastMUDGame, delta time.Duration) { - if player == nil { - logging.Error("Unable to handle player command from player with id", pce.connectionId, ": Player does not exist") - return - } - - event := pce.parseCommandIntoEvent(game, player) -} - -func (pce *PlayerCommandEvent) parseCommandIntoEvent(game *LastMUDGame, player ecs.Entity) GameEvent { - switch pce.command.Command().Definition().Name() { - case SayCommand: - speech, err := pce.command.Command().Parameters()[0].AsString() - - if err != nil { - logging.Error("Unable to handle player speech from player with id", pce.connectionId, ": Speech could not be parsed: ", err.Error()) - return nil - } - - return game.CreatePlayerSayEvent(player, speech) - } - - return nil -} - -type PlayerSayEvent struct { - player *Player - speech string -} - -func (game *LastMUDGame) CreatePlayerSayEvent(player *Player, speech string) *PlayerSayEvent { - return &PlayerSayEvent{ - player: player, - speech: speech, - } -} - -func (pse *PlayerSayEvent) Type() EventType { - return PlayerSpeak -} - -func (pse *PlayerSayEvent) Handle(game *LastMUDGame, delta time.Duration) { - for _, p := range pse.player.CurrentRoom().Players() { - game.enqeueOutput(game.CreateOutput(p.Identity(), []byte(pse.player.id.String()+" in "+pse.player.CurrentRoom().Name+": "+pse.speech))) - } -} diff --git a/internal/game/game.go b/internal/game/game.go index 7539dfb..1cfa8cf 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -5,7 +5,10 @@ import ( "sync" "time" + "code.haedhutner.dev/mvv/LastMUD/internal/ecs" "code.haedhutner.dev/mvv/LastMUD/internal/game/command" + "code.haedhutner.dev/mvv/LastMUD/internal/game/data" + "code.haedhutner.dev/mvv/LastMUD/internal/game/systems" "code.haedhutner.dev/mvv/LastMUD/internal/logging" "github.com/google/uuid" ) @@ -13,7 +16,6 @@ import ( const TickRate = time.Duration(50 * time.Millisecond) const MaxEnqueuedOutputPerTick = 100 -const MaxEnqueuedGameEventsPerTick = 100 type GameOutput struct { connId uuid.UUID @@ -41,22 +43,21 @@ type LastMUDGame struct { cmdRegistry *command.CommandRegistry - world *GameWorld - - eventBus *EventBus + world *data.GameWorld output chan GameOutput } func CreateGame(ctx context.Context, wg *sync.WaitGroup) (game *LastMUDGame) { game = &LastMUDGame{ - wg: wg, - ctx: ctx, - eventBus: CreateEventBus(MaxEnqueuedGameEventsPerTick), - output: make(chan GameOutput, MaxEnqueuedOutputPerTick), - world: CreateGameWorld(), + wg: wg, + ctx: ctx, + output: make(chan GameOutput, MaxEnqueuedOutputPerTick), + world: data.CreateGameWorld(), } + ecs.RegisterSystems(game.world.World, systems.CreateEventSystems()...) + game.cmdRegistry = game.CreateGameCommandRegistry() wg.Add(1) @@ -65,10 +66,6 @@ func CreateGame(ctx context.Context, wg *sync.WaitGroup) (game *LastMUDGame) { return } -func (game *LastMUDGame) EnqueueEvent(event GameEvent) { - game.eventBus.Push(event) -} - func (game *LastMUDGame) ConsumeNextOutput() *GameOutput { select { case output := <-game.output: @@ -78,6 +75,18 @@ func (game *LastMUDGame) ConsumeNextOutput() *GameOutput { } } +func (game *LastMUDGame) ConnectPlayer(connectionId uuid.UUID) { + data.CreatePlayerConnectEvent(game.world.World, connectionId) +} + +func (game *LastMUDGame) DisconnectPlayer(connectionId uuid.UUID) { + data.CreatePlayerDisconnectEvent(game.world.World, connectionId) +} + +func (game *LastMUDGame) SendPlayerCommand(connectionId uuid.UUID, command string) { + data.CreatePlayerCommandEvent(game.world.World, connectionId, command) +} + func (game *LastMUDGame) commandRegistry() *command.CommandRegistry { return game.cmdRegistry } @@ -108,10 +117,23 @@ func (game *LastMUDGame) start() { } } +func (game *LastMUDGame) consumeOutputs() { + entities := ecs.FindEntitiesWithComponents(game.world.World, data.TypeConnectionId, data.TypeContents) + + for _, entity := range entities { + connId, _ := ecs.GetComponent[data.ConnectionIdComponent](game.world.World, entity) + contents, _ := ecs.GetComponent[data.ContentsComponent](game.world.World, entity) + + game.enqeueOutput(GameOutput{ + connId: connId.ConnectionId, + contents: contents.Contents, + }) + } +} + func (game *LastMUDGame) shutdown() { logging.Info("Stopping LastMUD...") close(game.output) - game.eventBus.close() } func (game *LastMUDGame) shouldStop() bool { @@ -128,13 +150,6 @@ func (game *LastMUDGame) enqeueOutput(output GameOutput) { } func (g *LastMUDGame) tick(delta time.Duration) { - for { - event := g.eventBus.Pop() - - if event == nil { - return - } - - event.Handle(g, delta) - } + g.world.Tick(delta) + g.consumeOutputs() } diff --git a/internal/game/player.go b/internal/game/player.go deleted file mode 100644 index f1aee49..0000000 --- a/internal/game/player.go +++ /dev/null @@ -1,39 +0,0 @@ -package game - -// import "github.com/google/uuid" - -// type Player struct { -// id uuid.UUID - -// state PlayerState - -// currentRoom *Room -// } - -// func CreateJoiningPlayer(identity uuid.UUID) *Player { -// return &Player{ -// id: identity, -// state: PlayerStateJoining, -// currentRoom: nil, -// } -// } - -// func CreatePlayer(identity uuid.UUID, state PlayerState, room *Room) *Player { -// return &Player{ -// id: identity, -// state: state, -// currentRoom: room, -// } -// } - -// func (p *Player) Identity() uuid.UUID { -// return p.id -// } - -// func (p *Player) SetRoom(r *Room) { -// p.currentRoom = r -// } - -// func (p *Player) CurrentRoom() *Room { -// return p.currentRoom -// } diff --git a/internal/game/room.go b/internal/game/room.go deleted file mode 100644 index db4861f..0000000 --- a/internal/game/room.go +++ /dev/null @@ -1,51 +0,0 @@ -package game - -// import "github.com/google/uuid" - -// type RoomPlayer interface { -// Identity() uuid.UUID -// SetRoom(room *Room) -// } - -// type Room struct { -// world *World - -// North *Room -// South *Room -// East *Room -// West *Room - -// Name string -// Description string - -// players map[uuid.UUID]RoomPlayer -// } - -// func CreateRoom(world *World, name, description string) *Room { -// return &Room{ -// world: world, -// Name: name, -// Description: description, -// players: map[uuid.UUID]RoomPlayer{}, -// } -// } - -// func (r *Room) PlayerJoinRoom(player RoomPlayer) (err error) { -// r.players[player.Identity()] = player - -// return -// } - -// func (r *Room) PlayerLeaveRoom(player RoomPlayer) (err error) { -// delete(r.players, player.Identity()) - -// return -// } - -// func (r *Room) Players() map[uuid.UUID]RoomPlayer { -// return r.players -// } - -// func (r *Room) World() *World { -// return r.world -// } diff --git a/internal/game/systems.go b/internal/game/systems.go deleted file mode 100644 index cde26fe..0000000 --- a/internal/game/systems.go +++ /dev/null @@ -1 +0,0 @@ -package game diff --git a/internal/game/systems/event.go b/internal/game/systems/event.go new file mode 100644 index 0000000..6f91551 --- /dev/null +++ b/internal/game/systems/event.go @@ -0,0 +1,70 @@ +package systems + +import ( + "fmt" + "time" + + "code.haedhutner.dev/mvv/LastMUD/internal/ecs" + "code.haedhutner.dev/mvv/LastMUD/internal/game/data" + "code.haedhutner.dev/mvv/LastMUD/internal/logging" +) + +type eventError struct { + err string +} + +func createEventError(v ...any) *eventError { + return &eventError{ + err: fmt.Sprint(v...), + } +} + +func (e *eventError) Error() string { + return e.err +} + +func EventTypeQuery(eventType data.EventType) func(comp data.EventComponent) bool { + return func(comp data.EventComponent) bool { + return comp.EventType == eventType + } +} + +func CreateEventSystems() []*ecs.System { + return []*ecs.System{ + ecs.CreateSystem("PlayerConnectEventHandlerSystem", 0, handlePlayerConnectEvents), + } +} + +func handlePlayerConnectEvents(world *ecs.World, delta time.Duration) (err error) { + events := ecs.QueryEntitiesWithComponent(world, EventTypeQuery(data.EventPlayerConnect)) + processedEvents := []ecs.Entity{} + + for event := range events { + err = handlePlayerConnectEvent(world, event) + + if err != nil { + logging.Error("PlayerConnect Error: ", err) + } + + processedEvents = append(processedEvents, event) + } + + ecs.DeleteEntities(world, processedEvents...) + + return +} + +func handlePlayerConnectEvent(world *ecs.World, entity ecs.Entity) (err error) { + logging.Warn("Player connect") + + connectionId, ok := ecs.GetComponent[data.ConnectionIdComponent](world, entity) + + if !ok { + return createEventError("Event does not contain connectionId") + } + + data.CreatePlayer(world, connectionId.ConnectionId, data.PlayerStateJoining) + data.CreateGameOutput(world, connectionId.ConnectionId, []byte("Welcome to LastMUD!")) + + return +} diff --git a/internal/server/connection.go b/internal/server/connection.go index a2bd550..3860c5f 100644 --- a/internal/server/connection.go +++ b/internal/server/connection.go @@ -48,7 +48,7 @@ func CreateConnection(server *Server, conn *net.TCPConn, ctx context.Context, wg go c.listen() go c.checkAlive() - server.game().EnqueueEvent(server.game().CreatePlayerJoinEvent(c.Id())) + server.game().ConnectPlayer(c.Id()) return } @@ -79,13 +79,7 @@ func (c *Connection) listen() { break } - event, err := c.server.game().CreatePlayerCommandEvent(c.Id(), message) - - if err != nil { - c.Write([]byte(err.Error())) - } else { - c.server.game().EnqueueEvent(event) - } + c.server.game().SendPlayerCommand(c.Id(), message) c.lastSeen = time.Now() } @@ -128,7 +122,7 @@ func (c *Connection) shouldClose() bool { func (c *Connection) closeConnection() { c.conn.Close() - c.server.game().EnqueueEvent(c.server.game().CreatePlayerLeaveEvent(c.Id())) + c.server.game().DisconnectPlayer(c.Id()) logging.Info("Disconnected: ", c.conn.RemoteAddr()) } diff --git a/internal/util/slices.go b/internal/util/slices.go new file mode 100644 index 0000000..5cf56f1 --- /dev/null +++ b/internal/util/slices.go @@ -0,0 +1,57 @@ +package util + +import "iter" + +// Hash-based intersection of slices +func IntersectSlices[T comparable](a []T, b []T) []T { + set := make([]T, 0) + hash := make(map[T]struct{}) + + for _, v := range a { + hash[v] = struct{}{} + } + + for _, v := range b { + if _, ok := hash[v]; ok { + set = append(set, v) + } + } + + return set +} + +// Hash-based intersection of iterators +func IntersectIterators[T comparable](a, b iter.Seq[T]) []T { + set := make([]T, 0) + hash := make(map[T]struct{}) + + for v := range a { + hash[v] = struct{}{} + } + + for v := range b { + if _, ok := hash[v]; ok { + set = append(set, v) + } + } + + return set +} + +// Hash-based intersection of iterator and slice +func IntersectSliceWithIterator[T comparable](a []T, b iter.Seq[T]) []T { + set := make([]T, 0) + hash := make(map[T]struct{}) + + for _, v := range a { + hash[v] = struct{}{} + } + + for v := range b { + if _, ok := hash[v]; ok { + set = append(set, v) + } + } + + return set +}