Events, ECS almost there, systems for events

This commit is contained in:
Miroslav Vasilev 2025-06-26 23:48:54 +03:00
parent b2212a279c
commit a18862a976
23 changed files with 614 additions and 475 deletions

4
go.mod
View file

@ -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
)

2
go.sum
View file

@ -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=

View file

@ -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 {
@ -96,7 +117,6 @@ 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
}
@ -104,7 +124,6 @@ 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{},
}
@ -113,18 +132,22 @@ 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)
}
}
func DeleteEntities(world *World, entities ...Entity) {
for _, e := range entities {
DeleteEntity(world, e)
}
}
func SetResource(world *World, r Resource, val any) {
@ -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)
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View 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
View 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)))
// }
// }

View 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})
}

View 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
}

View 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
}

View file

@ -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,

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)))
}
}

View file

@ -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,9 +43,7 @@ type LastMUDGame struct {
cmdRegistry *command.CommandRegistry
world *GameWorld
eventBus *EventBus
world *data.GameWorld
output chan GameOutput
}
@ -52,11 +52,12 @@ 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(),
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()
}

View file

@ -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
// }

View file

@ -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
// }

View file

@ -1 +0,0 @@
package game

View 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
}

View file

@ -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())
}

57
internal/util/slices.go Normal file
View 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
}