From b8d8127bc043f86a040896b8bfbaaf282c919432 Mon Sep 17 00:00:00 2001 From: mvvasilev Date: Sun, 22 Jun 2025 22:27:56 +0300 Subject: [PATCH] Basic chatting --- internal/{ => game}/command/command.go | 22 ++----- internal/{ => game}/command/error.go | 14 +++++ internal/{ => game}/command/parameter.go | 0 internal/{ => game}/command/registry.go | 17 ++--- internal/{ => game}/command/tokenizer.go | 0 internal/game/commands.go | 31 ++++++++++ internal/game/event.go | 10 ++- internal/game/events.go | 79 ++++++++++++++++++++++-- internal/game/game.go | 12 +++- internal/game/player.go | 8 ++- internal/game/room.go | 19 ++++-- internal/game/world.go | 52 ++++++++++------ internal/server/connection.go | 47 +++++--------- internal/server/server.go | 10 ++- 14 files changed, 224 insertions(+), 97 deletions(-) rename internal/{ => game}/command/command.go (70%) rename internal/{ => game}/command/error.go (62%) rename internal/{ => game}/command/parameter.go (100%) rename internal/{ => game}/command/registry.go (76%) rename internal/{ => game}/command/tokenizer.go (100%) create mode 100644 internal/game/commands.go diff --git a/internal/command/command.go b/internal/game/command/command.go similarity index 70% rename from internal/command/command.go rename to internal/game/command/command.go index 0e82e54..44d6e9d 100644 --- a/internal/command/command.go +++ b/internal/game/command/command.go @@ -12,22 +12,12 @@ func CreateCommand(cmdDef CommandDefinition, parameters []Parameter) Command { } } -func (cmd Command) Execute() (err error) { - return cmd.commandDefinition.work(cmd.params...) +func (cmd Command) Definition() CommandDefinition { + return cmd.commandDefinition } -type commandContextError struct { - err string -} - -func createCommandContextError(err string) *commandContextError { - return &commandContextError{ - err: err, - } -} - -func (cce *commandContextError) Error() string { - return cce.err +func (cmd Command) Parameters() []Parameter { + return cmd.params } type CommandContext struct { @@ -65,6 +55,6 @@ func CreateCommandContext(commandRegistry *CommandRegistry, commandString string return } -func (ctx *CommandContext) ExecuteCommand() (err error) { - return ctx.command.Execute() +func (ctx *CommandContext) Command() Command { + return ctx.command } diff --git a/internal/command/error.go b/internal/game/command/error.go similarity index 62% rename from internal/command/error.go rename to internal/game/command/error.go index 3e7ffea..cc26bcf 100644 --- a/internal/command/error.go +++ b/internal/game/command/error.go @@ -17,3 +17,17 @@ func createCommandError(cmdName string, msg string, msgArgs ...any) *commandErro func (cmdErr *commandError) Error() string { return "Error with command '" + cmdErr.cmdName + "': " + cmdErr.message } + +type commandContextError struct { + err string +} + +func createCommandContextError(err string) *commandContextError { + return &commandContextError{ + err: err, + } +} + +func (cce *commandContextError) Error() string { + return cce.err +} diff --git a/internal/command/parameter.go b/internal/game/command/parameter.go similarity index 100% rename from internal/command/parameter.go rename to internal/game/command/parameter.go diff --git a/internal/command/registry.go b/internal/game/command/registry.go similarity index 76% rename from internal/command/registry.go rename to internal/game/command/registry.go index 7ae8711..a6e559b 100644 --- a/internal/command/registry.go +++ b/internal/game/command/registry.go @@ -1,6 +1,8 @@ package command -import "log" +import ( + "code.haedhutner.dev/mvv/LastMUD/internal/logging" +) type TokenMatcher func(tokens []Token) bool @@ -12,20 +14,17 @@ type CommandDefinition struct { name string tokenMatcher TokenMatcher parameterParser ParameterParser - work CommandWork } func CreateCommandDefinition( name string, tokenMatcher TokenMatcher, parameterParser ParameterParser, - work CommandWork, ) CommandDefinition { return CommandDefinition{ name: name, tokenMatcher: tokenMatcher, parameterParser: parameterParser, - work: work, } } @@ -41,10 +40,6 @@ func (def CommandDefinition) ParseParameters(tokens []Token) []Parameter { return def.parameterParser(tokens) } -func (def CommandDefinition) ExecuteFunc() CommandWork { - return def.work -} - type CommandRegistry struct { commandDefinitions []CommandDefinition } @@ -55,14 +50,10 @@ func CreateCommandRegistry(commandDefinitions ...CommandDefinition) *CommandRegi } } -func (comReg *CommandRegistry) Register(newCommandDefinitions ...CommandDefinition) { - comReg.commandDefinitions = append(comReg.commandDefinitions, newCommandDefinitions...) -} - func (comReg *CommandRegistry) Match(tokens []Token) (comDef *CommandDefinition) { for _, v := range comReg.commandDefinitions { if v.Match(tokens) { - log.Println("Found match", v.Name()) + logging.Debug("Found match", v.Name()) return &v } } diff --git a/internal/command/tokenizer.go b/internal/game/command/tokenizer.go similarity index 100% rename from internal/command/tokenizer.go rename to internal/game/command/tokenizer.go diff --git a/internal/game/commands.go b/internal/game/commands.go new file mode 100644 index 0000000..de8b67e --- /dev/null +++ b/internal/game/commands.go @@ -0,0 +1,31 @@ +package game + +import "code.haedhutner.dev/mvv/LastMUD/internal/game/command" + +type CommandType = string + +const ( + SayCommand CommandType = "say" +) + +func (game *LastMUDGame) CreateGameCommandRegistry() *command.CommandRegistry { + return command.CreateCommandRegistry( + command.CreateCommandDefinition( + SayCommand, + func(tokens []command.Token) bool { + return len(tokens) > 1 && tokens[0].Lexeme() == "say" + }, + func(tokens []command.Token) []command.Parameter { + saying := "" + + for _, t := range tokens[1:] { + saying += t.Lexeme() + } + + return []command.Parameter{ + command.CreateParameter(saying), + } + }, + ), + ) +} diff --git a/internal/game/event.go b/internal/game/event.go index 81642b4..911c808 100644 --- a/internal/game/event.go +++ b/internal/game/event.go @@ -1,6 +1,10 @@ package game -import "time" +import ( + "time" + + "code.haedhutner.dev/mvv/LastMUD/internal/logging" +) type EventType int @@ -8,6 +12,8 @@ const ( PlayerJoin EventType = iota PlayerCommand PlayerLeave + + PlayerSpeak ) type GameEvent interface { @@ -32,6 +38,7 @@ func (eb *EventBus) HasNext() bool { func (eb *EventBus) Pop() (event GameEvent) { select { case event := <-eb.events: + logging.Info("Popped event of type ", event.Type(), ":", event) return event default: return nil @@ -40,6 +47,7 @@ func (eb *EventBus) Pop() (event GameEvent) { func (eb *EventBus) Push(event GameEvent) { eb.events <- event + logging.Info("Enqueued event of type ", event.Type(), ":", event) } func (eb *EventBus) close() { diff --git a/internal/game/events.go b/internal/game/events.go index f836513..1aa2693 100644 --- a/internal/game/events.go +++ b/internal/game/events.go @@ -3,6 +3,8 @@ package game import ( "time" + "code.haedhutner.dev/mvv/LastMUD/internal/game/command" + "code.haedhutner.dev/mvv/LastMUD/internal/logging" "github.com/google/uuid" ) @@ -10,7 +12,7 @@ type PlayerJoinEvent struct { connectionId uuid.UUID } -func CreatePlayerJoinEvent(connId uuid.UUID) *PlayerJoinEvent { +func (game *LastMUDGame) CreatePlayerJoinEvent(connId uuid.UUID) *PlayerJoinEvent { return &PlayerJoinEvent{ connectionId: connId, } @@ -22,23 +24,90 @@ func (pje *PlayerJoinEvent) Type() EventType { 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"))) + game.enqeueOutput(game.CreateOutput(pje.connectionId, []byte("Welcome to LastMUD\n"))) } type PlayerLeaveEvent struct { connectionId uuid.UUID } -func CreatePlayerLeaveEvent(connId uuid.UUID) *PlayerLeaveEvent { +func (game *LastMUDGame) CreatePlayerLeaveEvent(connId uuid.UUID) *PlayerLeaveEvent { return &PlayerLeaveEvent{ connectionId: connId, } } func (ple *PlayerLeaveEvent) Type() EventType { - return PlayerJoin + return PlayerLeave } func (ple *PlayerLeaveEvent) Handle(game *LastMUDGame, delta time.Duration) { - game.world.RemovePlayerById(ple.connectionId.String()) + game.world.RemovePlayerById(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) { + player := game.world.FindPlayerById(pce.connectionId) + + if player == nil { + logging.Error("Unable to handle player command from player with id", pce.connectionId, ": Player does not exist") + return + } + + 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 + } + + game.EnqueueEvent(game.CreatePlayerSayEvent(player, speech)) + } +} + +type PlayerSayEvent struct { + player *Player + speech string +} + +func (game *LastMUDGame) CreatePlayerSayEvent(player *Player, speech string) *PlayerSayEvent { + return &PlayerSayEvent{ + player: player, + speech: speech, + } +} + +func (pse *PlayerSayEvent) Type() EventType { + return PlayerSpeak +} + +func (pse *PlayerSayEvent) Handle(game *LastMUDGame, delta time.Duration) { + for _, p := range pse.player.CurrentRoom().Players() { + game.enqeueOutput(game.CreateOutput(p.Identity(), []byte(pse.player.id.String()+" in "+pse.player.CurrentRoom().Name+": "+pse.speech))) + } } diff --git a/internal/game/game.go b/internal/game/game.go index 846bb83..e9eeab3 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -5,6 +5,7 @@ import ( "sync" "time" + "code.haedhutner.dev/mvv/LastMUD/internal/game/command" "code.haedhutner.dev/mvv/LastMUD/internal/logging" "github.com/google/uuid" ) @@ -16,7 +17,7 @@ type GameOutput struct { contents []byte } -func CreateOutput(connId uuid.UUID, contents []byte) GameOutput { +func (game *LastMUDGame) CreateOutput(connId uuid.UUID, contents []byte) GameOutput { return GameOutput{ connId: connId, contents: contents, @@ -35,7 +36,8 @@ type LastMUDGame struct { ctx context.Context wg *sync.WaitGroup - world *World + commandRegistry *command.CommandRegistry + world *World eventBus *EventBus @@ -51,6 +53,8 @@ func CreateGame(ctx context.Context, wg *sync.WaitGroup) (game *LastMUDGame) { world: CreateWorld(), } + game.commandRegistry = game.CreateGameCommandRegistry() + wg.Add(1) go game.start() @@ -115,6 +119,10 @@ func (game *LastMUDGame) ConsumeNextOutput() *GameOutput { } } +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 481d9bd..c4e464b 100644 --- a/internal/game/player.go +++ b/internal/game/player.go @@ -15,10 +15,14 @@ func CreatePlayer(identity uuid.UUID, room *Room) *Player { } } -func (p *Player) Identity() string { - return p.id.String() +func (p *Player) Identity() uuid.UUID { + return p.id } func (p *Player) SetRoom(r *Room) { p.currentRoom = r } + +func (p *Player) CurrentRoom() *Room { + return p.currentRoom +} diff --git a/internal/game/room.go b/internal/game/room.go index 78cd88d..d047142 100644 --- a/internal/game/room.go +++ b/internal/game/room.go @@ -1,11 +1,15 @@ package game +import "github.com/google/uuid" + type RoomPlayer interface { - Identity() string + Identity() uuid.UUID SetRoom(room *Room) } type Room struct { + world *World + North *Room South *Room East *Room @@ -14,14 +18,15 @@ type Room struct { Name string Description string - players map[string]RoomPlayer + players map[uuid.UUID]RoomPlayer } -func CreateRoom(name, description string) *Room { +func CreateRoom(world *World, name, description string) *Room { return &Room{ + world: world, Name: name, Description: description, - players: map[string]RoomPlayer{}, + players: map[uuid.UUID]RoomPlayer{}, } } @@ -37,6 +42,10 @@ func (r *Room) PlayerLeaveRoom(player RoomPlayer) (err error) { return } -func (r *Room) Players() map[string]RoomPlayer { +func (r *Room) Players() map[uuid.UUID]RoomPlayer { return r.players } + +func (r *Room) World() *World { + return r.world +} diff --git a/internal/game/world.go b/internal/game/world.go index ed9fcf8..ba15af0 100644 --- a/internal/game/world.go +++ b/internal/game/world.go @@ -1,17 +1,23 @@ package game +import "github.com/google/uuid" + type World struct { rooms []*Room - players map[string]*Player + players map[uuid.UUID]*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.") +func CreateWorld() (world *World) { + world = &World{ + players: map[uuid.UUID]*Player{}, + } + + forest := CreateRoom(world, "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(world, "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(world, "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(world, "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(world, "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 @@ -34,17 +40,17 @@ func CreateWorld() *World { graveyard.West = chapel graveyard.East = lake - return &World{ - rooms: []*Room{ - forest, - cabin, - lake, - graveyard, - chapel, - }, - defaultRoom: forest, - players: map[string]*Player{}, + world.rooms = []*Room{ + forest, + cabin, + lake, + graveyard, + chapel, } + + world.defaultRoom = forest + + return } func (w *World) AddPlayerToDefaultRoom(p *Player) { @@ -53,7 +59,7 @@ func (w *World) AddPlayerToDefaultRoom(p *Player) { p.SetRoom(w.defaultRoom) } -func (w *World) RemovePlayerById(id string) { +func (w *World) RemovePlayerById(id uuid.UUID) { p, ok := w.players[id] if ok { @@ -62,3 +68,13 @@ func (w *World) RemovePlayerById(id string) { return } } + +func (w *World) FindPlayerById(id uuid.UUID) *Player { + p, ok := w.players[id] + + if ok { + return p + } else { + return nil + } +} diff --git a/internal/server/connection.go b/internal/server/connection.go index f752da3..492a0c3 100644 --- a/internal/server/connection.go +++ b/internal/server/connection.go @@ -7,13 +7,11 @@ 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 @@ -25,8 +23,6 @@ type Connection struct { conn *net.TCPConn lastSeen time.Time - - inputChannel chan []byte } func CreateConnection(server *Server, conn *net.TCPConn, ctx context.Context, wg *sync.WaitGroup) (c *Connection) { @@ -36,20 +32,19 @@ func CreateConnection(server *Server, conn *net.TCPConn, ctx context.Context, wg conn.SetKeepAlivePeriod(1 * time.Second) c = &Connection{ - ctx: ctx, - wg: wg, - server: server, - identity: uuid.New(), - conn: conn, - inputChannel: make(chan []byte, MaxEnqueuedInputMessages), - lastSeen: time.Now(), + ctx: ctx, + wg: wg, + server: server, + identity: uuid.New(), + conn: conn, + lastSeen: time.Now(), } c.wg.Add(2) go c.listen() go c.checkAlive() - server.game.EnqueueEvent(game.CreatePlayerJoinEvent(c.Id())) + server.game().EnqueueEvent(server.game().CreatePlayerJoinEvent(c.Id())) return } @@ -66,22 +61,21 @@ func (c *Connection) listen() { for { c.conn.SetReadDeadline(time.Time{}) - message, err := bufio.NewReader(c.conn).ReadBytes('\n') + message, err := bufio.NewReader(c.conn).ReadString('\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 + event, err := c.server.game().CreatePlayerCommandEvent(c.Id(), message) + + if err != nil { + c.conn.Write([]byte(err.Error() + "\n")) + } else { + c.server.game().EnqueueEvent(event) } - c.inputChannel <- message - - c.conn.Write([]byte(message)) - c.lastSeen = time.Now() } } @@ -119,24 +113,13 @@ func (c *Connection) shouldClose() bool { } func (c *Connection) closeConnection() { - close(c.inputChannel) - c.conn.Close() - c.server.game.EnqueueEvent(game.CreatePlayerLeaveEvent(c.Id())) + c.server.game().EnqueueEvent(c.server.game().CreatePlayerLeaveEvent(c.Id())) logging.Info("Disconnected: ", c.conn.RemoteAddr()) } -func (c *Connection) NextInput() (input []byte, err error) { - select { - case val := <-c.inputChannel: - return val, nil - default: - return nil, newInputEmptyError() - } -} - 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 cf1072d..7235ddc 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -19,7 +19,7 @@ type Server struct { connections map[uuid.UUID]*Connection - game *game.LastMUDGame + lastmudgame *game.LastMUDGame } func CreateServer(ctx context.Context, wg *sync.WaitGroup, port string) (srv *Server, err error) { @@ -54,7 +54,7 @@ func CreateServer(ctx context.Context, wg *sync.WaitGroup, port string) (srv *Se connections: map[uuid.UUID]*Connection{}, } - srv.game = game.CreateGame(ctx, srv.wg) + srv.lastmudgame = game.CreateGame(ctx, srv.wg) srv.wg.Add(2) go srv.listen() @@ -63,6 +63,10 @@ func CreateServer(ctx context.Context, wg *sync.WaitGroup, port string) (srv *Se return } +func (srv *Server) game() *game.LastMUDGame { + return srv.lastmudgame +} + func (srv *Server) listen() { defer srv.wg.Done() defer srv.shutdown() @@ -101,7 +105,7 @@ func (srv *Server) consumeGameOutput() { break } - output := srv.game.ConsumeNextOutput() + output := srv.lastmudgame.ConsumeNextOutput() if output == nil { continue