Events, ECS almost there, systems for events
This commit is contained in:
parent
b2212a279c
commit
a18862a976
23 changed files with 614 additions and 475 deletions
4
go.mod
4
go.mod
|
@ -7,4 +7,6 @@ require (
|
||||||
golang.org/x/term v0.32.0
|
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
|
||||||
|
)
|
||||||
|
|
2
go.sum
2
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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
package ecs
|
package ecs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"iter"
|
||||||
|
"maps"
|
||||||
"slices"
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.haedhutner.dev/mvv/LastMUD/internal/logging"
|
"code.haedhutner.dev/mvv/LastMUD/internal/logging"
|
||||||
|
"code.haedhutner.dev/mvv/LastMUD/internal/util"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -50,6 +53,24 @@ func (cs *ComponentStorage[T]) ComponentType() ComponentType {
|
||||||
return cs.forType
|
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) {
|
func (cs *ComponentStorage[T]) Set(e Entity, component T) {
|
||||||
cs.storage[e] = component
|
cs.storage[e] = component
|
||||||
}
|
}
|
||||||
|
@ -85,7 +106,7 @@ func (s *System) Priority() int {
|
||||||
return s.priority
|
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)
|
err := s.work(world, delta)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -96,7 +117,6 @@ func (s *System) DoWork(world *World, delta time.Duration) {
|
||||||
type World struct {
|
type World struct {
|
||||||
systems []*System
|
systems []*System
|
||||||
componentsByType map[ComponentType]any
|
componentsByType map[ComponentType]any
|
||||||
componentsByEntity map[Entity]map[ComponentType]any
|
|
||||||
resources map[Resource]any
|
resources map[Resource]any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,7 +124,6 @@ func CreateWorld() (world *World) {
|
||||||
world = &World{
|
world = &World{
|
||||||
systems: []*System{},
|
systems: []*System{},
|
||||||
componentsByType: map[ComponentType]any{},
|
componentsByType: map[ComponentType]any{},
|
||||||
componentsByEntity: map[Entity]map[ComponentType]any{}, // TODO: Can't figure out use-case right now
|
|
||||||
resources: map[Resource]any{},
|
resources: map[Resource]any{},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,17 +132,21 @@ func CreateWorld() (world *World) {
|
||||||
|
|
||||||
func (w *World) Tick(delta time.Duration) {
|
func (w *World) Tick(delta time.Duration) {
|
||||||
for _, s := range w.systems {
|
for _, s := range w.systems {
|
||||||
s.DoWork(w, delta)
|
s.Execute(w, delta)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteEntity(world *World, entity Entity) {
|
func DeleteEntity(world *World, entity Entity) {
|
||||||
for _, s := range world.componentsByType {
|
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)
|
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)
|
world.componentsByType[compType] = CreateComponentStorage[T](compType)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetComponent[T Component](world *World, entity Entity, component T) {
|
func SetComponent[T Component](world *World, entity Entity, component T) {
|
||||||
|
registerComponent[T](world, component.Type())
|
||||||
|
|
||||||
compStorage := world.componentsByType[component.Type()].(*ComponentStorage[T])
|
compStorage := world.componentsByType[component.Type()].(*ComponentStorage[T])
|
||||||
|
|
||||||
compStorage.Set(entity, component)
|
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) {
|
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]) {
|
func GetComponentStorage[T Component](world *World) (compStorage *ComponentStorage[T]) {
|
||||||
var zero 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) {
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
61
internal/game/data/common.go
Normal file
61
internal/game/data/common.go
Normal file
|
@ -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
|
||||||
|
}
|
159
internal/game/data/event.go
Normal file
159
internal/game/data/event.go
Normal file
|
@ -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)))
|
||||||
|
// }
|
||||||
|
// }
|
21
internal/game/data/output.go
Normal file
21
internal/game/data/output.go
Normal file
|
@ -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})
|
||||||
|
}
|
56
internal/game/data/player.go
Normal file
56
internal/game/data/player.go
Normal file
|
@ -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
|
||||||
|
}
|
33
internal/game/data/room.go
Normal file
33
internal/game/data/room.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -1,8 +1,7 @@
|
||||||
package game
|
package data
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"code.haedhutner.dev/mvv/LastMUD/internal/game/components"
|
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
|
||||||
"code.haedhutner.dev/mvv/LastMUD/internal/game/ecs"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -70,32 +69,32 @@ func CreateGameWorld() (gw *GameWorld) {
|
||||||
ecs.NilEntity(),
|
ecs.NilEntity(),
|
||||||
)
|
)
|
||||||
|
|
||||||
ecs.SetComponent(gw.World, forest, components.NeighborsComponent{
|
ecs.SetComponent(gw.World, forest, NeighborsComponent{
|
||||||
North: cabin,
|
North: cabin,
|
||||||
South: graveyard,
|
South: graveyard,
|
||||||
East: lake,
|
East: lake,
|
||||||
West: chapel,
|
West: chapel,
|
||||||
})
|
})
|
||||||
|
|
||||||
ecs.SetComponent(gw.World, cabin, components.NeighborsComponent{
|
ecs.SetComponent(gw.World, cabin, NeighborsComponent{
|
||||||
South: graveyard,
|
South: graveyard,
|
||||||
West: chapel,
|
West: chapel,
|
||||||
East: lake,
|
East: lake,
|
||||||
})
|
})
|
||||||
|
|
||||||
ecs.SetComponent(gw.World, chapel, components.NeighborsComponent{
|
ecs.SetComponent(gw.World, chapel, NeighborsComponent{
|
||||||
North: cabin,
|
North: cabin,
|
||||||
South: graveyard,
|
South: graveyard,
|
||||||
East: forest,
|
East: forest,
|
||||||
})
|
})
|
||||||
|
|
||||||
ecs.SetComponent(gw.World, lake, components.NeighborsComponent{
|
ecs.SetComponent(gw.World, lake, NeighborsComponent{
|
||||||
West: forest,
|
West: forest,
|
||||||
North: cabin,
|
North: cabin,
|
||||||
South: graveyard,
|
South: graveyard,
|
||||||
})
|
})
|
||||||
|
|
||||||
ecs.SetComponent(gw.World, graveyard, components.NeighborsComponent{
|
ecs.SetComponent(gw.World, graveyard, NeighborsComponent{
|
||||||
North: forest,
|
North: forest,
|
||||||
West: chapel,
|
West: chapel,
|
||||||
East: lake,
|
East: lake,
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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)))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,7 +5,10 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
|
||||||
"code.haedhutner.dev/mvv/LastMUD/internal/game/command"
|
"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"
|
"code.haedhutner.dev/mvv/LastMUD/internal/logging"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
@ -13,7 +16,6 @@ import (
|
||||||
const TickRate = time.Duration(50 * time.Millisecond)
|
const TickRate = time.Duration(50 * time.Millisecond)
|
||||||
|
|
||||||
const MaxEnqueuedOutputPerTick = 100
|
const MaxEnqueuedOutputPerTick = 100
|
||||||
const MaxEnqueuedGameEventsPerTick = 100
|
|
||||||
|
|
||||||
type GameOutput struct {
|
type GameOutput struct {
|
||||||
connId uuid.UUID
|
connId uuid.UUID
|
||||||
|
@ -41,9 +43,7 @@ type LastMUDGame struct {
|
||||||
|
|
||||||
cmdRegistry *command.CommandRegistry
|
cmdRegistry *command.CommandRegistry
|
||||||
|
|
||||||
world *GameWorld
|
world *data.GameWorld
|
||||||
|
|
||||||
eventBus *EventBus
|
|
||||||
|
|
||||||
output chan GameOutput
|
output chan GameOutput
|
||||||
}
|
}
|
||||||
|
@ -52,11 +52,12 @@ func CreateGame(ctx context.Context, wg *sync.WaitGroup) (game *LastMUDGame) {
|
||||||
game = &LastMUDGame{
|
game = &LastMUDGame{
|
||||||
wg: wg,
|
wg: wg,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
eventBus: CreateEventBus(MaxEnqueuedGameEventsPerTick),
|
|
||||||
output: make(chan GameOutput, MaxEnqueuedOutputPerTick),
|
output: make(chan GameOutput, MaxEnqueuedOutputPerTick),
|
||||||
world: CreateGameWorld(),
|
world: data.CreateGameWorld(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ecs.RegisterSystems(game.world.World, systems.CreateEventSystems()...)
|
||||||
|
|
||||||
game.cmdRegistry = game.CreateGameCommandRegistry()
|
game.cmdRegistry = game.CreateGameCommandRegistry()
|
||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
|
@ -65,10 +66,6 @@ func CreateGame(ctx context.Context, wg *sync.WaitGroup) (game *LastMUDGame) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (game *LastMUDGame) EnqueueEvent(event GameEvent) {
|
|
||||||
game.eventBus.Push(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (game *LastMUDGame) ConsumeNextOutput() *GameOutput {
|
func (game *LastMUDGame) ConsumeNextOutput() *GameOutput {
|
||||||
select {
|
select {
|
||||||
case output := <-game.output:
|
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 {
|
func (game *LastMUDGame) commandRegistry() *command.CommandRegistry {
|
||||||
return game.cmdRegistry
|
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() {
|
func (game *LastMUDGame) shutdown() {
|
||||||
logging.Info("Stopping LastMUD...")
|
logging.Info("Stopping LastMUD...")
|
||||||
close(game.output)
|
close(game.output)
|
||||||
game.eventBus.close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (game *LastMUDGame) shouldStop() bool {
|
func (game *LastMUDGame) shouldStop() bool {
|
||||||
|
@ -128,13 +150,6 @@ func (game *LastMUDGame) enqeueOutput(output GameOutput) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *LastMUDGame) tick(delta time.Duration) {
|
func (g *LastMUDGame) tick(delta time.Duration) {
|
||||||
for {
|
g.world.Tick(delta)
|
||||||
event := g.eventBus.Pop()
|
g.consumeOutputs()
|
||||||
|
|
||||||
if event == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
event.Handle(g, delta)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
// }
|
|
|
@ -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
|
|
||||||
// }
|
|
|
@ -1 +0,0 @@
|
||||||
package game
|
|
70
internal/game/systems/event.go
Normal file
70
internal/game/systems/event.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -48,7 +48,7 @@ func CreateConnection(server *Server, conn *net.TCPConn, ctx context.Context, wg
|
||||||
go c.listen()
|
go c.listen()
|
||||||
go c.checkAlive()
|
go c.checkAlive()
|
||||||
|
|
||||||
server.game().EnqueueEvent(server.game().CreatePlayerJoinEvent(c.Id()))
|
server.game().ConnectPlayer(c.Id())
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -79,13 +79,7 @@ func (c *Connection) listen() {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
event, err := c.server.game().CreatePlayerCommandEvent(c.Id(), message)
|
c.server.game().SendPlayerCommand(c.Id(), message)
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
c.Write([]byte(err.Error()))
|
|
||||||
} else {
|
|
||||||
c.server.game().EnqueueEvent(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.lastSeen = time.Now()
|
c.lastSeen = time.Now()
|
||||||
}
|
}
|
||||||
|
@ -128,7 +122,7 @@ func (c *Connection) shouldClose() bool {
|
||||||
func (c *Connection) closeConnection() {
|
func (c *Connection) closeConnection() {
|
||||||
c.conn.Close()
|
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())
|
logging.Info("Disconnected: ", c.conn.RemoteAddr())
|
||||||
}
|
}
|
||||||
|
|
57
internal/util/slices.go
Normal file
57
internal/util/slices.go
Normal file
|
@ -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
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue