From 45abc33c7f6b2f9c2244fc59d7dc0c6dd826bdff Mon Sep 17 00:00:00 2001 From: mvvasilev Date: Sun, 22 Jun 2025 17:54:07 +0300 Subject: [PATCH] No longer blocks randomly --- internal/game/event.go | 47 +++++++++++++++++++++++++ internal/game/events.go | 44 ++++++++++++++++++++++++ internal/game/game.go | 65 +++++++++++++++++++++++++++++++---- internal/game/player.go | 27 +++++++++------ internal/game/room.go | 42 ++++++++++++++++++++++ internal/game/traits.go | 44 ------------------------ internal/game/world.go | 64 ++++++++++++++++++++++++++++++++++ internal/logging/logging.go | 8 ++++- internal/server/connection.go | 40 +++++++++++++++------ internal/server/server.go | 47 +++++++++++++++++++------ 10 files changed, 345 insertions(+), 83 deletions(-) create mode 100644 internal/game/event.go create mode 100644 internal/game/events.go create mode 100644 internal/game/room.go create mode 100644 internal/game/world.go diff --git a/internal/game/event.go b/internal/game/event.go new file mode 100644 index 0000000..81642b4 --- /dev/null +++ b/internal/game/event.go @@ -0,0 +1,47 @@ +package game + +import "time" + +type EventType int + +const ( + PlayerJoin EventType = iota + PlayerCommand + PlayerLeave +) + +type GameEvent interface { + Type() EventType + Handle(game *LastMUDGame, delta time.Duration) +} + +type EventBus struct { + events chan GameEvent +} + +func CreateEventBus() *EventBus { + return &EventBus{ + events: make(chan GameEvent, 10), + } +} + +func (eb *EventBus) HasNext() bool { + return len(eb.events) > 0 +} + +func (eb *EventBus) Pop() (event GameEvent) { + select { + case event := <-eb.events: + return event + default: + return nil + } +} + +func (eb *EventBus) Push(event GameEvent) { + eb.events <- event +} + +func (eb *EventBus) close() { + close(eb.events) +} diff --git a/internal/game/events.go b/internal/game/events.go new file mode 100644 index 0000000..f836513 --- /dev/null +++ b/internal/game/events.go @@ -0,0 +1,44 @@ +package game + +import ( + "time" + + "github.com/google/uuid" +) + +type PlayerJoinEvent struct { + connectionId uuid.UUID +} + +func 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) { + game.world.AddPlayerToDefaultRoom(CreatePlayer(pje.connectionId, nil)) + game.enqeueOutput(CreateOutput(pje.connectionId, []byte("Welcome to LastMUD\n"))) +} + +type PlayerLeaveEvent struct { + connectionId uuid.UUID +} + +func CreatePlayerLeaveEvent(connId uuid.UUID) *PlayerLeaveEvent { + return &PlayerLeaveEvent{ + connectionId: connId, + } +} + +func (ple *PlayerLeaveEvent) Type() EventType { + return PlayerJoin +} + +func (ple *PlayerLeaveEvent) Handle(game *LastMUDGame, delta time.Duration) { + game.world.RemovePlayerById(ple.connectionId.String()) +} diff --git a/internal/game/game.go b/internal/game/game.go index 6d2388b..846bb83 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -6,22 +6,49 @@ import ( "time" "code.haedhutner.dev/mvv/LastMUD/internal/logging" + "github.com/google/uuid" ) const TickRate = time.Duration(50 * time.Millisecond) -type GameSignal struct { +type GameOutput struct { + connId uuid.UUID + contents []byte +} + +func CreateOutput(connId uuid.UUID, contents []byte) GameOutput { + return GameOutput{ + connId: connId, + contents: contents, + } +} + +func (g GameOutput) Id() uuid.UUID { + return g.connId +} + +func (g GameOutput) Contents() []byte { + return g.contents } type LastMUDGame struct { ctx context.Context wg *sync.WaitGroup + + world *World + + eventBus *EventBus + + output chan GameOutput } func CreateGame(ctx context.Context, wg *sync.WaitGroup) (game *LastMUDGame) { game = &LastMUDGame{ - wg: wg, - ctx: ctx, + wg: wg, + ctx: ctx, + eventBus: CreateEventBus(), + output: make(chan GameOutput, 10), + world: CreateWorld(), } wg.Add(1) @@ -58,6 +85,8 @@ func (game *LastMUDGame) start() { func (game *LastMUDGame) shutdown() { logging.Info("Stopping LastMUD...") + close(game.output) + game.eventBus.close() } func (game *LastMUDGame) shouldStop() bool { @@ -69,7 +98,31 @@ func (game *LastMUDGame) shouldStop() bool { } } -func (g *LastMUDGame) tick(delta time.Duration) { - // logging.Debug("Tick") - // TODO +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 (g *LastMUDGame) tick(delta time.Duration) { + for { + event := g.eventBus.Pop() + + if event == nil { + return + } + + event.Handle(g, delta) + } } diff --git a/internal/game/player.go b/internal/game/player.go index bdae4a3..481d9bd 100644 --- a/internal/game/player.go +++ b/internal/game/player.go @@ -1,19 +1,24 @@ package game +import "github.com/google/uuid" + type Player struct { - GameObject - Name - Description - Position - Velocity + id uuid.UUID + + currentRoom *Room } -func CreatePlayer(name, description string, x, y int) *Player { +func CreatePlayer(identity uuid.UUID, room *Room) *Player { return &Player{ - GameObject: CreateGameObject(), - Name: WithName(name), - Description: WithDescription(description), - Position: WithPosition(x, y), - Velocity: WithVelocity(0, 0), + id: identity, + currentRoom: room, } } + +func (p *Player) Identity() string { + return p.id.String() +} + +func (p *Player) SetRoom(r *Room) { + p.currentRoom = r +} diff --git a/internal/game/room.go b/internal/game/room.go new file mode 100644 index 0000000..78cd88d --- /dev/null +++ b/internal/game/room.go @@ -0,0 +1,42 @@ +package game + +type RoomPlayer interface { + Identity() string + SetRoom(room *Room) +} + +type Room struct { + North *Room + South *Room + East *Room + West *Room + + Name string + Description string + + players map[string]RoomPlayer +} + +func CreateRoom(name, description string) *Room { + return &Room{ + Name: name, + Description: description, + players: map[string]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[string]RoomPlayer { + return r.players +} diff --git a/internal/game/traits.go b/internal/game/traits.go index c4a7f92..cde26fe 100644 --- a/internal/game/traits.go +++ b/internal/game/traits.go @@ -1,45 +1 @@ package game - -import "github.com/google/uuid" - -type GameObject struct { - uuid uuid.UUID -} - -func CreateGameObject() GameObject { - return GameObject{ - uuid: uuid.New(), - } -} - -type Position struct { - x, y int -} - -func WithPosition(x, y int) Position { - return Position{x, y} -} - -type Velocity struct { - velX, velY int -} - -func WithVelocity(velX, velY int) Velocity { - return Velocity{velX, velY} -} - -type Name struct { - name string -} - -func WithName(name string) Name { - return Name{name} -} - -type Description struct { - description string -} - -func WithDescription(description string) Description { - return Description{description} -} diff --git a/internal/game/world.go b/internal/game/world.go new file mode 100644 index 0000000..ed9fcf8 --- /dev/null +++ b/internal/game/world.go @@ -0,0 +1,64 @@ +package game + +type World struct { + rooms []*Room + players map[string]*Player + defaultRoom *Room +} + +func CreateWorld() *World { + forest := CreateRoom("Forest", "A dense, misty forest stretches endlessly, its towering trees whispering secrets through rustling leaves. Sunbeams filter through the canopy, dappling the mossy ground with golden light.") + cabin := CreateRoom("Wooden Cabin", "The cabin’s interior is cozy and rustic, with wooden beams overhead and a stone fireplace crackling warmly. A wool rug lies on creaky floorboards, and shelves brim with books, mugs, and old lanterns.") + lake := CreateRoom("Ethermere Lake", "Ethermire Lake lies shrouded in mist, its dark, still waters reflecting a sky perpetually overcast. Whispers ride the wind, and strange lights flicker beneath the surface, never breaking it.") + graveyard := CreateRoom("Graveyard", "An overgrown graveyard shrouded in fog, with cracked headstones and leaning statues. The wind sighs through dead trees, and unseen footsteps echo faintly among the mossy graves.") + chapel := CreateRoom("Chapel of the Hollow Light", "This ruined chapel leans under ivy and age. Faint light filters through shattered stained glass, casting broken rainbows across dust-choked pews and a long-silent altar.") + + forest.North = cabin + forest.South = graveyard + forest.East = lake + forest.West = chapel + + cabin.South = forest + cabin.West = chapel + cabin.East = lake + + chapel.North = cabin + chapel.South = graveyard + chapel.East = forest + + lake.West = forest + lake.North = cabin + lake.South = graveyard + + graveyard.North = forest + graveyard.West = chapel + graveyard.East = lake + + return &World{ + rooms: []*Room{ + forest, + cabin, + lake, + graveyard, + chapel, + }, + defaultRoom: forest, + players: map[string]*Player{}, + } +} + +func (w *World) AddPlayerToDefaultRoom(p *Player) { + w.players[p.Identity()] = p + w.defaultRoom.PlayerJoinRoom(p) + p.SetRoom(w.defaultRoom) +} + +func (w *World) RemovePlayerById(id string) { + p, ok := w.players[id] + + if ok { + p.currentRoom.PlayerLeaveRoom(p) + delete(w.players, id) + return + } +} diff --git a/internal/logging/logging.go b/internal/logging/logging.go index c165158..06304f1 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -79,10 +79,16 @@ func CreateLogger(maxFileLevel LogLevel, maxDisplayedLevel LogLevel, filePath st logFilePath := fmt.Sprintf("%s-%s%s", base, timestamp, ext) // "./base/dir/log-2006-01-02_15-04-05.txt" + mkdirErr := os.MkdirAll(filepath.Dir(logFilePath), 0755) + + if mkdirErr != nil { + err(os.Stdout, false, timestampFormat, mkdirErr) + } + file, fileErr := os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if fileErr != nil { - err(os.Stdout, false, "Logging: Unable to write to file", filePath, fileErr) + err(os.Stdout, false, timestampFormat, fileErr) } else { logger.file = file } diff --git a/internal/server/connection.go b/internal/server/connection.go index 2a884e3..f752da3 100644 --- a/internal/server/connection.go +++ b/internal/server/connection.go @@ -7,25 +7,29 @@ import ( "sync" "time" + "code.haedhutner.dev/mvv/LastMUD/internal/game" "code.haedhutner.dev/mvv/LastMUD/internal/logging" "github.com/google/uuid" ) const MaxLastSeenTime = 120 * time.Second +const MaxEnqueuedInputMessages = 10 type Connection struct { ctx context.Context wg *sync.WaitGroup + server *Server + identity uuid.UUID conn *net.TCPConn lastSeen time.Time - inputChannel chan string + inputChannel chan []byte } -func CreateConnection(conn *net.TCPConn, ctx context.Context, wg *sync.WaitGroup) (c *Connection) { +func CreateConnection(server *Server, conn *net.TCPConn, ctx context.Context, wg *sync.WaitGroup) (c *Connection) { logging.Info("Connect: ", conn.RemoteAddr()) conn.SetKeepAlive(true) @@ -34,9 +38,10 @@ func CreateConnection(conn *net.TCPConn, ctx context.Context, wg *sync.WaitGroup c = &Connection{ ctx: ctx, wg: wg, + server: server, identity: uuid.New(), conn: conn, - inputChannel: make(chan string), + inputChannel: make(chan []byte, MaxEnqueuedInputMessages), lastSeen: time.Now(), } @@ -44,9 +49,15 @@ func CreateConnection(conn *net.TCPConn, ctx context.Context, wg *sync.WaitGroup go c.listen() go c.checkAlive() + server.game.EnqueueEvent(game.CreatePlayerJoinEvent(c.Id())) + return } +func (c *Connection) Id() uuid.UUID { + return c.identity +} + func (c *Connection) listen() { defer c.wg.Done() @@ -55,13 +66,18 @@ func (c *Connection) listen() { for { c.conn.SetReadDeadline(time.Time{}) - message, err := bufio.NewReader(c.conn).ReadString('\n') + message, err := bufio.NewReader(c.conn).ReadBytes('\n') if err != nil { logging.Warn(err) break } + if len(c.inputChannel) == MaxEnqueuedInputMessages { + c.conn.Write([]byte("You have too many commands enqueued. Please wait until some are processed.\n")) + continue + } + c.inputChannel <- message c.conn.Write([]byte(message)) @@ -76,12 +92,12 @@ func (c *Connection) checkAlive() { for { if c.shouldClose() { - c.Write("Server shutting down, bye bye!\r\n") + c.Write([]byte("Server shutting down, bye bye!\r\n")) break } if time.Since(c.lastSeen) > MaxLastSeenTime { - c.Write("You have been away for too long, bye bye!\r\n") + c.Write([]byte("You have been away for too long, bye bye!\r\n")) break } @@ -103,21 +119,25 @@ func (c *Connection) shouldClose() bool { } func (c *Connection) closeConnection() { + close(c.inputChannel) + c.conn.Close() + c.server.game.EnqueueEvent(game.CreatePlayerLeaveEvent(c.Id())) + logging.Info("Disconnected: ", c.conn.RemoteAddr()) } -func (c *Connection) NextInput() (input string, err error) { +func (c *Connection) NextInput() (input []byte, err error) { select { case val := <-c.inputChannel: return val, nil default: - return "", newInputEmptyError() + return nil, newInputEmptyError() } } -func (c *Connection) Write(output string) (err error) { - _, err = c.conn.Write([]byte(output)) +func (c *Connection) Write(output []byte) (err error) { + _, err = c.conn.Write(output) return } diff --git a/internal/server/server.go b/internal/server/server.go index 4ed2281..cf1072d 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -8,6 +8,7 @@ import ( "code.haedhutner.dev/mvv/LastMUD/internal/game" "code.haedhutner.dev/mvv/LastMUD/internal/logging" + "github.com/google/uuid" ) type Server struct { @@ -16,19 +17,19 @@ type Server struct { listener *net.TCPListener - connections []*Connection + connections map[uuid.UUID]*Connection game *game.LastMUDGame } func CreateServer(ctx context.Context, wg *sync.WaitGroup, port string) (srv *Server, err error) { - logging.Info(" _ _ __ __ _ _ ____ ") - logging.Info(" | | __ _ ___| |_| \\/ | | | | _ \\ ") - logging.Info(" | | / _` / __| __| |\\/| | | | | | | | ") - logging.Info(" | |__| (_| \\__ \\ |_| | | | |_| | |_| | ") - logging.Info(" |_____\\__,_|___/\\__|_| |_|\\___/|____/ ") - logging.Info(" ") + logging.Info(" _ _ __ __ _ _ ____") + logging.Info("| | __ _ ___| |_| \\/ | | | | _ \\") + logging.Info("| | / _` / __| __| |\\/| | | | | | | |") + logging.Info("| |__| (_| \\__ \\ |_| | | | |_| | |_| |") + logging.Info("|_____\\__,_|___/\\__|_| |_|\\___/|____/") + logging.Info("") addr, err := net.ResolveTCPAddr("tcp", port) @@ -50,13 +51,14 @@ func CreateServer(ctx context.Context, wg *sync.WaitGroup, port string) (srv *Se ctx: ctx, wg: wg, listener: ln, - connections: []*Connection{}, + connections: map[uuid.UUID]*Connection{}, } srv.game = game.CreateGame(ctx, srv.wg) - srv.wg.Add(1) + srv.wg.Add(2) go srv.listen() + go srv.consumeGameOutput() return } @@ -85,8 +87,31 @@ func (srv *Server) listen() { continue } - c := CreateConnection(tcpConn, srv.ctx, srv.wg) - srv.connections = append(srv.connections, c) + c := CreateConnection(srv, tcpConn, srv.ctx, srv.wg) + + srv.connections[c.Id()] = c + } +} + +func (srv *Server) consumeGameOutput() { + defer srv.wg.Done() + + for { + if srv.shouldStop() { + break + } + + output := srv.game.ConsumeNextOutput() + + if output == nil { + continue + } + + conn, ok := srv.connections[output.Id()] + + if ok { + conn.Write(output.Contents()) + } } }