From 87f5c2f842a330bf0657c1b3ef91f3f519fac88e Mon Sep 17 00:00:00 2001 From: Miroslav Vasilev Date: Tue, 24 Jun 2025 16:37:26 +0300 Subject: [PATCH] ECS --- cmd/lastmudserver/main.go | 3 ++ internal/game/.DS_Store | Bin 0 -> 6148 bytes internal/game/db/repository.go | 17 +++++++++++ internal/game/ecs/components.go | 31 +++++++++++++++++++++ internal/game/ecs/entity.go | 19 +++++++++++++ internal/game/ecs/systems.go | 1 + internal/game/ecs/world.go | 15 ++++++++++ internal/game/event.go | 4 +-- internal/game/events.go | 18 ++++++++---- internal/game/game.go | 48 +++++++++++++++++--------------- internal/game/player.go | 13 ++++++++- internal/game/traits.go | 1 - internal/server/connection.go | 22 +++++++++------ internal/server/error.go | 15 ---------- 14 files changed, 153 insertions(+), 54 deletions(-) create mode 100644 internal/game/.DS_Store create mode 100644 internal/game/db/repository.go create mode 100644 internal/game/ecs/components.go create mode 100644 internal/game/ecs/entity.go create mode 100644 internal/game/ecs/systems.go create mode 100644 internal/game/ecs/world.go delete mode 100644 internal/game/traits.go delete mode 100644 internal/server/error.go diff --git a/cmd/lastmudserver/main.go b/cmd/lastmudserver/main.go index e904555..3e9abda 100644 --- a/cmd/lastmudserver/main.go +++ b/cmd/lastmudserver/main.go @@ -7,6 +7,7 @@ import ( "os/signal" "sync" "syscall" + "time" "code.haedhutner.dev/mvv/LastMUD/internal/server" @@ -57,5 +58,7 @@ func processInput() { if buf[0] == 'q' { return } + + time.Sleep(50 * time.Millisecond) } } diff --git a/internal/game/.DS_Store b/internal/game/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..a2a629c92adf0ed13667860c529b094301352b9a GIT binary patch literal 6148 zcmeHK%SyvQ6rE|SO({Ya3SADkE!b-DftwKP4;ayfN=;1AV9b;zHH%WnT7Sqd@q4^? zW&#$o7P0rj%(>5*%z?~)S6PI_05L!e5CiMYfH@be z?t0Tet0xACff@#Ie-O|R9fPGtwRJ#;*Jq5k5K%zKw*;av=olT<@_%rK6bxq7^CH9Po)N@v{BNIfw?46HKH)TWK+|2h0JwU7MO60(Q^ zV&I=Kz*}Q)?82hV+4^I7c-9JN_s~!X7FcEH&aN=vU={ ObP-U5P)7{>0s~*RA4zEd literal 0 HcmV?d00001 diff --git a/internal/game/db/repository.go b/internal/game/db/repository.go new file mode 100644 index 0000000..2b11398 --- /dev/null +++ b/internal/game/db/repository.go @@ -0,0 +1,17 @@ +package db + +import "github.com/google/uuid" + +type Identifier = uuid.UUID + +type Entity interface { + Id() Identifier +} + +type Repository[T Entity] interface { + Create(entity T) (rowsAffected int, err error) + Delete(entity T) (rowsAffected int, err error) + Update(entity T) (rowsAffected int, err error) + FetchOne(id Identifier) (entity T, err error) + FetchAll() (entities []T, err error) +} diff --git a/internal/game/ecs/components.go b/internal/game/ecs/components.go new file mode 100644 index 0000000..b98d19f --- /dev/null +++ b/internal/game/ecs/components.go @@ -0,0 +1,31 @@ +package ecs + +type PlayerState = byte + +const ( + PlayerStateJoining PlayerState = iota + PlayerStateLoggingIn + PlayerStateRegistering + PlayerStatePlaying + PlayerStateLeaving +) + +type PlayerStateComponent struct { + State PlayerState +} + +type NameComponent struct { + Name string +} + +type DescriptionComponent struct { + Description string +} + +type InRoomComponent struct { + InRoom Entity +} + +type NeighboringRoomsComponent struct { + North, South, East, West Entity +} diff --git a/internal/game/ecs/entity.go b/internal/game/ecs/entity.go new file mode 100644 index 0000000..f9d8197 --- /dev/null +++ b/internal/game/ecs/entity.go @@ -0,0 +1,19 @@ +package ecs + +import "github.com/google/uuid" + +type Entity uuid.UUID + +type Room struct { + Entity + NameComponent + DescriptionComponent + NeighboringRoomsComponent +} + +type Player struct { + Entity + PlayerStateComponent + NameComponent + InRoomComponent +} diff --git a/internal/game/ecs/systems.go b/internal/game/ecs/systems.go new file mode 100644 index 0000000..7f32d0d --- /dev/null +++ b/internal/game/ecs/systems.go @@ -0,0 +1 @@ +package ecs diff --git a/internal/game/ecs/world.go b/internal/game/ecs/world.go new file mode 100644 index 0000000..7bc3de8 --- /dev/null +++ b/internal/game/ecs/world.go @@ -0,0 +1,15 @@ +package ecs + +type World struct { + Players []*Player + Rooms []*Room + DefaultRoom *Room +} + +func CreateWorld() *World { + world := &World{ + Players: []*Player{}, + Rooms: []*Room{}, + } + +} diff --git a/internal/game/event.go b/internal/game/event.go index a2f8f19..d7257b2 100644 --- a/internal/game/event.go +++ b/internal/game/event.go @@ -45,9 +45,9 @@ type EventBus struct { events chan GameEvent } -func CreateEventBus() *EventBus { +func CreateEventBus(capacity int) *EventBus { return &EventBus{ - events: make(chan GameEvent, 10), + events: make(chan GameEvent, capacity), } } diff --git a/internal/game/events.go b/internal/game/events.go index b5ab461..bee19ea 100644 --- a/internal/game/events.go +++ b/internal/game/events.go @@ -23,8 +23,10 @@ func (pje *PlayerJoinEvent) Type() EventType { } func (pje *PlayerJoinEvent) Handle(game *LastMUDGame, delta time.Duration) { - game.world.AddPlayerToDefaultRoom(CreatePlayer(pje.connectionId, nil)) - game.enqeueOutput(game.CreateOutput(pje.connectionId, []byte("Welcome to LastMUD!"))) + p := CreateJoiningPlayer(pje.connectionId) + game.world.AddPlayerToDefaultRoom(p) + game.enqeueOutput(game.CreateOutput(p.Identity(), []byte("Welcome to LastMUD!"))) + game.enqeueOutput(game.CreateOutput(p.Identity(), []byte("Please enter your name:"))) } type PlayerLeaveEvent struct { @@ -51,7 +53,7 @@ type PlayerCommandEvent struct { } func (game *LastMUDGame) CreatePlayerCommandEvent(connId uuid.UUID, cmdString string) (event *PlayerCommandEvent, err error) { - cmdCtx, err := command.CreateCommandContext(game.CommandRegistry(), cmdString) + cmdCtx, err := command.CreateCommandContext(game.commandRegistry(), cmdString) if err != nil { return nil, err @@ -77,17 +79,23 @@ func (pce *PlayerCommandEvent) Handle(game *LastMUDGame, delta time.Duration) { return } + event := pce.parseCommandIntoEvent(game, player) +} + +func (pce *PlayerCommandEvent) parseCommandIntoEvent(game *LastMUDGame, player *Player) 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 + return nil } - game.EnqueueEvent(game.CreatePlayerSayEvent(player, speech)) + return game.CreatePlayerSayEvent(player, speech) } + + return nil } type PlayerSayEvent struct { diff --git a/internal/game/game.go b/internal/game/game.go index e9eeab3..13ccabe 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -12,6 +12,9 @@ import ( const TickRate = time.Duration(50 * time.Millisecond) +const MaxEnqueuedOutputPerTick = 100 +const MaxEnqueuedGameEventsPerTick = 100 + type GameOutput struct { connId uuid.UUID contents []byte @@ -36,8 +39,9 @@ type LastMUDGame struct { ctx context.Context wg *sync.WaitGroup - commandRegistry *command.CommandRegistry - world *World + cmdRegistry *command.CommandRegistry + + world *World eventBus *EventBus @@ -48,12 +52,12 @@ func CreateGame(ctx context.Context, wg *sync.WaitGroup) (game *LastMUDGame) { game = &LastMUDGame{ wg: wg, ctx: ctx, - eventBus: CreateEventBus(), - output: make(chan GameOutput, 10), + eventBus: CreateEventBus(MaxEnqueuedGameEventsPerTick), + output: make(chan GameOutput, MaxEnqueuedOutputPerTick), world: CreateWorld(), } - game.commandRegistry = game.CreateGameCommandRegistry() + game.cmdRegistry = game.CreateGameCommandRegistry() wg.Add(1) go game.start() @@ -61,6 +65,23 @@ 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: + return &output + default: + return nil + } +} + +func (game *LastMUDGame) commandRegistry() *command.CommandRegistry { + return game.cmdRegistry +} + func (game *LastMUDGame) start() { defer game.wg.Done() defer game.shutdown() @@ -102,27 +123,10 @@ func (game *LastMUDGame) shouldStop() bool { } } -func (game *LastMUDGame) EnqueueEvent(event GameEvent) { - game.eventBus.Push(event) -} - func (game *LastMUDGame) enqeueOutput(output GameOutput) { game.output <- output } -func (game *LastMUDGame) ConsumeNextOutput() *GameOutput { - select { - case output := <-game.output: - return &output - default: - return nil - } -} - -func (game *LastMUDGame) CommandRegistry() *command.CommandRegistry { - return game.commandRegistry -} - func (g *LastMUDGame) tick(delta time.Duration) { for { event := g.eventBus.Pop() diff --git a/internal/game/player.go b/internal/game/player.go index c4e464b..bc98392 100644 --- a/internal/game/player.go +++ b/internal/game/player.go @@ -5,12 +5,23 @@ import "github.com/google/uuid" type Player struct { id uuid.UUID + state PlayerState + currentRoom *Room } -func CreatePlayer(identity uuid.UUID, room *Room) *Player { +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, } } diff --git a/internal/game/traits.go b/internal/game/traits.go deleted file mode 100644 index cde26fe..0000000 --- a/internal/game/traits.go +++ /dev/null @@ -1 +0,0 @@ -package game diff --git a/internal/server/connection.go b/internal/server/connection.go index 98db36d..a2bd550 100644 --- a/internal/server/connection.go +++ b/internal/server/connection.go @@ -11,7 +11,11 @@ import ( "github.com/google/uuid" ) -const MaxLastSeenTime = 120 * time.Second +const MaxLastSeenTime = 90 * time.Second + +const CheckAlivePeriod = 50 * time.Millisecond + +const DeleteBeforeAndMoveToStartOfLine = "\033[1K\r" type Connection struct { ctx context.Context @@ -53,6 +57,13 @@ func (c *Connection) Id() uuid.UUID { return c.identity } +func (c *Connection) Write(output []byte) (err error) { + output = append([]byte(DeleteBeforeAndMoveToStartOfLine+"< "), output...) + output = append(output, []byte("\n> ")...) + _, err = c.conn.Write(output) + return +} + func (c *Connection) listen() { defer c.wg.Done() @@ -100,6 +111,8 @@ func (c *Connection) checkAlive() { if err != nil { break } + + time.Sleep(CheckAlivePeriod) } } @@ -119,10 +132,3 @@ func (c *Connection) closeConnection() { logging.Info("Disconnected: ", c.conn.RemoteAddr()) } - -func (c *Connection) Write(output []byte) (err error) { - output = append([]byte("< "), output...) - output = append(output, []byte("\n> ")...) - _, err = c.conn.Write(output) - return -} diff --git a/internal/server/error.go b/internal/server/error.go deleted file mode 100644 index ea43f68..0000000 --- a/internal/server/error.go +++ /dev/null @@ -1,15 +0,0 @@ -package server - -type inputEmptyError struct { - msg string -} - -func newInputEmptyError() *inputEmptyError { - return &inputEmptyError{ - msg: "No input available at this moment", - } -} - -func (err *inputEmptyError) Error() string { - return err.msg -}