Restructure some things, move things about

This commit is contained in:
Miroslav Vasilev 2025-06-28 11:24:06 +03:00
parent 574dc2fa4a
commit 308c343068
38 changed files with 781 additions and 548 deletions

8
.gitignore vendored
View file

@ -1,2 +1,6 @@
log/* log/
*.log target/
coverage/
.idea/
*.log

25
Dockerfile Normal file
View file

@ -0,0 +1,25 @@
# Stage 1: Build the Go application
FROM golang:1.24 as builder
WORKDIR /lastmudserver
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN ./bin/build.sh
# Stage 2: Create a smaller image with the compiled binary
FROM debian:stable
WORKDIR /lastmudserver
COPY --from=builder /lastmudserver/target/lastmudserver .
RUN chmod 777 lastmudserver
EXPOSE 8000
CMD ["./lastmudserver"]

9
bin/build.sh Executable file
View file

@ -0,0 +1,9 @@
#!/bin/bash
set -e
pushd $(dirname "$0")/.. # run from root dir
go build -o target/lastmudserver cmd/lastmudserver/main.go
popd

9
bin/build_docker.sh Executable file
View file

@ -0,0 +1,9 @@
#!/bin/bash
set -e
pushd $(dirname "$0")/.. # run from root dir
docker build -t lastmudserver .
popd

9
bin/run.sh Executable file
View file

@ -0,0 +1,9 @@
#!/bin/bash
set -e
pushd $(dirname "$0")/.. # run from root dir
go run cmd/lastmudserver/main.go
popd

12
bin/test_coverage.sh Executable file
View file

@ -0,0 +1,12 @@
#!/bin/bash
pushd $(dirname "$0")/.. # run from root dir
rm -rf ./coverage
mkdir ./coverage/
go test --cover -coverpkg=./internal... -covermode=count -coverprofile=./coverage/cover.out ./...
go tool cover -html=./coverage/cover.out
popd # switch back to dir we started from

View file

@ -2,6 +2,7 @@ package main
import ( import (
"context" "context"
"flag"
"log" "log"
"os" "os"
"os/signal" "os/signal"
@ -13,10 +14,15 @@ import (
_ "net/http/pprof" _ "net/http/pprof"
"code.haedhutner.dev/mvv/LastMUD/internal/server" "code.haedhutner.dev/mvv/LastMUD/internal/server"
"golang.org/x/term"
) )
var enableDiagnostics bool = false
func main() { func main() {
flag.BoolVar(&enableDiagnostics, "d", false, "Enable pprof server ( port :6060 ). Disabled by default.")
flag.Parse()
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
wg := sync.WaitGroup{} wg := sync.WaitGroup{}
@ -29,27 +35,19 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
go func() { if enableDiagnostics {
log.Println(http.ListenAndServe("localhost:6060", nil)) go func() {
}() log.Println(http.ListenAndServe("localhost:6060", nil))
}()
}
processInput() processInput()
} }
func processInput() { func processInput() {
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
if err != nil {
panic(err)
}
defer term.Restore(int(os.Stdin.Fd()), oldState)
sigChan := make(chan os.Signal, 1) sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
buf := make([]byte, 1)
for { for {
// If interrupt received, stop // If interrupt received, stop
select { select {
@ -58,13 +56,6 @@ func processInput() {
default: default:
} }
// TODO: Proper TUI for the server
os.Stdin.Read(buf)
if buf[0] == 'q' {
return
}
time.Sleep(50 * time.Millisecond) time.Sleep(50 * time.Millisecond)
} }
} }

2
go.mod
View file

@ -4,13 +4,11 @@ go 1.24.4
require ( require (
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
golang.org/x/term v0.32.0
) )
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v1.10.0 // indirect github.com/stretchr/testify v1.10.0 // indirect
golang.org/x/sys v0.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

View file

@ -175,7 +175,7 @@ func RemoveResource(world *World, r Resource) {
delete(world.resources, r) delete(world.resources, r)
} }
func registerComponent[T Component](world *World, compType ComponentType) { func registerComponent(world *World, compType ComponentType) {
if _, ok := world.componentsByType[compType]; ok { if _, ok := world.componentsByType[compType]; ok {
return return
} }
@ -184,7 +184,7 @@ func registerComponent[T Component](world *World, compType ComponentType) {
} }
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()) registerComponent(world, component.Type())
compStorage := world.componentsByType[component.Type()] compStorage := world.componentsByType[component.Type()]
@ -197,7 +197,7 @@ func GetComponent[T Component](world *World, entity Entity) (component T, exists
val, exists := storage.Get(entity) val, exists := storage.Get(entity)
casted, castSuccess := val.(T) casted, castSuccess := val.(T)
return casted, (exists && castSuccess) return casted, exists && castSuccess
} }
func DeleteComponent[T Component](world *World, entity Entity) { func DeleteComponent[T Component](world *World, entity Entity) {
@ -209,9 +209,10 @@ func DeleteComponent[T Component](world *World, entity Entity) {
func GetComponentStorage[T Component](world *World) (compStorage *ComponentStorage) { func GetComponentStorage[T Component](world *World) (compStorage *ComponentStorage) {
var zero T var zero T
// This is ok because the `Type` function is expected to return a hard-coded value and not depend on component state
compType := zero.Type() compType := zero.Type()
registerComponent[T](world, compType) registerComponent(world, compType)
return world.componentsByType[compType] return world.componentsByType[compType]
} }
@ -292,7 +293,11 @@ func RegisterSystem(world *World, s *System) {
} }
func RegisterSystems(world *World, systems ...*System) { func RegisterSystems(world *World, systems ...*System) {
for _, s := range systems { world.systems = append(world.systems, systems...)
RegisterSystem(world, s) slices.SortFunc(
} world.systems,
func(a, b *System) int {
return a.priority - b.priority
},
)
} }

View file

@ -0,0 +1,19 @@
package data
import "code.haedhutner.dev/mvv/LastMUD/internal/ecs"
type AccountComponent struct {
Account ecs.Entity
}
func (ac AccountComponent) Type() ecs.ComponentType {
return TypeAccount
}
type PasswordComponent struct {
EncryptedPassword string
}
func (pc PasswordComponent) Type() ecs.ComponentType {
return TypePassword
}

View file

@ -61,12 +61,22 @@ func (tc TokensComponent) Type() ecs.ComponentType {
return TypeCommandTokens return TypeCommandTokens
} }
type ArgName = string
const (
ArgMessageContent ArgName = "messageContent"
ArgAccountName = "accountName"
ArgAccountPassword = "accountPassword"
)
type Arg struct { type Arg struct {
Value any Value any
} }
type ArgsMap = map[ArgName]Arg
type ArgsComponent struct { type ArgsComponent struct {
Args map[string]Arg Args ArgsMap
} }
func (ac ArgsComponent) Type() ecs.ComponentType { func (ac ArgsComponent) Type() ecs.ComponentType {
@ -76,8 +86,12 @@ func (ac ArgsComponent) Type() ecs.ComponentType {
type Command string type Command string
const ( const (
CommandSay Command = "say" CommandSay Command = "say"
CommandQuit = "quit" CommandQuit = "quit"
CommandHelp = "help"
CommandSetName = "setname"
CommandLogin = "login"
CommandRegister = "register"
) )
type CommandComponent struct { type CommandComponent struct {
@ -87,14 +101,3 @@ type CommandComponent struct {
func (cc CommandComponent) Type() ecs.ComponentType { func (cc CommandComponent) Type() ecs.ComponentType {
return TypeCommand return TypeCommand
} }
func CreateTokenizedCommand(world *ecs.World, player ecs.Entity, commandString string, tokens []Token) ecs.Entity {
command := ecs.NewEntity()
ecs.SetComponent(world, command, PlayerComponent{Player: player})
ecs.SetComponent(world, command, CommandStringComponent{Command: commandString})
ecs.SetComponent(world, command, TokensComponent{Tokens: tokens})
ecs.SetComponent(world, command, CommandStateComponent{State: CommandStateTokenized})
return command
}

View file

@ -25,6 +25,9 @@ const (
TypeCommandState TypeCommandState
TypeCommandArgs TypeCommandArgs
TypeCommand TypeCommand
TypeAccount
TypePassword
) )
type Direction byte type Direction byte

View file

@ -2,7 +2,6 @@ package data
import ( import (
"code.haedhutner.dev/mvv/LastMUD/internal/ecs" "code.haedhutner.dev/mvv/LastMUD/internal/ecs"
"github.com/google/uuid"
) )
type EventType string type EventType string
@ -23,39 +22,3 @@ type EventComponent struct {
func (is EventComponent) Type() ecs.ComponentType { func (is EventComponent) Type() ecs.ComponentType {
return TypeEvent 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})
}
func CreateParseCommandEvent(world *ecs.World, command ecs.Entity) {
event := ecs.NewEntity()
ecs.SetComponent(world, event, EventComponent{EventType: EventParseCommand})
ecs.SetComponent(world, event, EntityComponent{Entity: command})
}
func CreateCommandExecutedEvent(world *ecs.World, command ecs.Entity) {
event := ecs.NewEntity()
ecs.SetComponent(world, event, EventComponent{EventType: EventCommandExecuted})
ecs.SetComponent(world, event, EntityComponent{Entity: command})
}

View file

@ -2,7 +2,6 @@ package data
import ( import (
"code.haedhutner.dev/mvv/LastMUD/internal/ecs" "code.haedhutner.dev/mvv/LastMUD/internal/ecs"
"github.com/google/uuid"
) )
type ContentsComponent struct { type ContentsComponent struct {
@ -18,14 +17,3 @@ type CloseConnectionComponent struct{}
func (cc CloseConnectionComponent) Type() ecs.ComponentType { func (cc CloseConnectionComponent) Type() ecs.ComponentType {
return TypeCloseConnection return TypeCloseConnection
} }
func CreateGameOutput(world *ecs.World, connectionId uuid.UUID, contents []byte, shouldClose bool) {
gameOutput := ecs.NewEntity()
ecs.SetComponent(world, gameOutput, ConnectionIdComponent{ConnectionId: connectionId})
ecs.SetComponent(world, gameOutput, ContentsComponent{Contents: contents})
if shouldClose {
ecs.SetComponent(world, gameOutput, CloseConnectionComponent{})
}
}

View file

@ -2,7 +2,6 @@ package data
import ( import (
"code.haedhutner.dev/mvv/LastMUD/internal/ecs" "code.haedhutner.dev/mvv/LastMUD/internal/ecs"
"github.com/google/uuid"
) )
type PlayerState = byte type PlayerState = byte
@ -36,21 +35,3 @@ type IsPlayerComponent struct{}
func (c IsPlayerComponent) Type() ecs.ComponentType { func (c IsPlayerComponent) Type() ecs.ComponentType {
return TypeIsPlayer 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,7 @@
package data
import "code.haedhutner.dev/mvv/LastMUD/internal/ecs"
const (
ResourceDefaultRoom ecs.Resource = "world:room:default"
)

View file

@ -16,18 +16,3 @@ type NeighborsComponent struct {
func (c NeighborsComponent) Type() ecs.ComponentType { func (c NeighborsComponent) Type() ecs.ComponentType {
return TypeNeighbors 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,56 +1,57 @@
package game package game
import ( import (
"code.haedhutner.dev/mvv/LastMUD/internal/game/logic/world"
"context" "context"
"sync" "sync"
"time" "time"
"code.haedhutner.dev/mvv/LastMUD/internal/ecs" "code.haedhutner.dev/mvv/LastMUD/internal/ecs"
"code.haedhutner.dev/mvv/LastMUD/internal/game/data" "code.haedhutner.dev/mvv/LastMUD/internal/game/data"
"code.haedhutner.dev/mvv/LastMUD/internal/game/systems" "code.haedhutner.dev/mvv/LastMUD/internal/game/logic"
"code.haedhutner.dev/mvv/LastMUD/internal/logging" "code.haedhutner.dev/mvv/LastMUD/internal/logging"
"github.com/google/uuid" "github.com/google/uuid"
) )
const TickRate = time.Duration(50 * time.Millisecond) const TickRate = 50 * time.Millisecond
type GameOutput struct { type Output struct {
connId uuid.UUID connId uuid.UUID
contents []byte contents []byte
closeConnection bool closeConnection bool
} }
func (g GameOutput) Id() uuid.UUID { func (g Output) Id() uuid.UUID {
return g.connId return g.connId
} }
func (g GameOutput) Contents() []byte { func (g Output) Contents() []byte {
return g.contents return g.contents
} }
func (g GameOutput) ShouldCloseConnection() bool { func (g Output) ShouldCloseConnection() bool {
return g.closeConnection return g.closeConnection
} }
type LastMUDGame struct { type Game struct {
ctx context.Context ctx context.Context
wg *sync.WaitGroup wg *sync.WaitGroup
world *data.GameWorld world *World
output chan GameOutput output chan Output
} }
func CreateGame(ctx context.Context, wg *sync.WaitGroup) (game *LastMUDGame) { func CreateGame(ctx context.Context, wg *sync.WaitGroup) (game *Game) {
game = &LastMUDGame{ game = &Game{
wg: wg, wg: wg,
ctx: ctx, ctx: ctx,
output: make(chan GameOutput), output: make(chan Output),
world: data.CreateGameWorld(), world: CreateGameWorld(),
} }
ecs.RegisterSystems(game.world.World, systems.CreateSystems()...) ecs.RegisterSystems(game.world.World, logic.CreateSystems()...)
wg.Add(1) wg.Add(1)
go game.start() go game.start()
@ -58,8 +59,8 @@ func CreateGame(ctx context.Context, wg *sync.WaitGroup) (game *LastMUDGame) {
return return
} }
// Will block if no output present // ConsumeNextOutput will block if no output present
func (game *LastMUDGame) ConsumeNextOutput() *GameOutput { func (game *Game) ConsumeNextOutput() *Output {
select { select {
case output := <-game.output: case output := <-game.output:
return &output return &output
@ -68,19 +69,19 @@ func (game *LastMUDGame) ConsumeNextOutput() *GameOutput {
} }
} }
func (game *LastMUDGame) ConnectPlayer(connectionId uuid.UUID) { func (game *Game) ConnectPlayer(connectionId uuid.UUID) {
data.CreatePlayerConnectEvent(game.world.World, connectionId) world.CreatePlayerConnectEvent(game.world.World, connectionId)
} }
func (game *LastMUDGame) DisconnectPlayer(connectionId uuid.UUID) { func (game *Game) DisconnectPlayer(connectionId uuid.UUID) {
data.CreatePlayerDisconnectEvent(game.world.World, connectionId) world.CreatePlayerDisconnectEvent(game.world.World, connectionId)
} }
func (game *LastMUDGame) SendPlayerCommand(connectionId uuid.UUID, command string) { func (game *Game) SendPlayerCommand(connectionId uuid.UUID, command string) {
data.CreatePlayerCommandEvent(game.world.World, connectionId, command) world.CreatePlayerCommandEvent(game.world.World, connectionId, command)
} }
func (game *LastMUDGame) start() { func (game *Game) start() {
defer game.wg.Done() defer game.wg.Done()
defer game.shutdown() defer game.shutdown()
@ -106,11 +107,11 @@ func (game *LastMUDGame) start() {
} }
} }
func (game *LastMUDGame) consumeOutputs() { func (game *Game) consumeOutputs() {
entities := ecs.FindEntitiesWithComponents(game.world.World, data.TypeConnectionId, data.TypeContents) entities := ecs.FindEntitiesWithComponents(game.world.World, data.TypeConnectionId, data.TypeContents)
for _, entity := range entities { for _, entity := range entities {
output := GameOutput{} output := Output{}
connId, _ := ecs.GetComponent[data.ConnectionIdComponent](game.world.World, entity) connId, _ := ecs.GetComponent[data.ConnectionIdComponent](game.world.World, entity)
output.connId = connId.ConnectionId output.connId = connId.ConnectionId
@ -130,12 +131,12 @@ func (game *LastMUDGame) consumeOutputs() {
ecs.DeleteEntities(game.world.World, entities...) ecs.DeleteEntities(game.world.World, entities...)
} }
func (game *LastMUDGame) shutdown() { func (game *Game) shutdown() {
logging.Info("Stopping LastMUD...") logging.Info("Stopping LastMUD...")
close(game.output) close(game.output)
} }
func (game *LastMUDGame) shouldStop() bool { func (game *Game) shouldStop() bool {
select { select {
case <-game.ctx.Done(): case <-game.ctx.Done():
return true return true
@ -144,11 +145,11 @@ func (game *LastMUDGame) shouldStop() bool {
} }
} }
func (game *LastMUDGame) enqeueOutput(output GameOutput) { func (game *Game) enqeueOutput(output Output) {
game.output <- output game.output <- output
} }
func (g *LastMUDGame) tick(delta time.Duration) { func (game *Game) tick(delta time.Duration) {
g.world.Tick(delta) game.world.Tick(delta)
g.consumeOutputs() game.consumeOutputs()
} }

View file

@ -0,0 +1,67 @@
package command
import (
"code.haedhutner.dev/mvv/LastMUD/internal/game/logic/world"
"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 commandError struct {
err string
}
func createCommandError(v ...any) *commandError {
return &commandError{
err: fmt.Sprint("Error handling command: ", v),
}
}
func (e *commandError) Error() string {
return e.err
}
type Handler func(w *ecs.World, delta time.Duration, player ecs.Entity, args map[string]data.Arg) (err error)
func commandQuery(command data.Command) func(comp data.CommandComponent) bool {
return func(comp data.CommandComponent) bool {
return comp.Cmd == command
}
}
func CreateHandler(command data.Command, handler Handler) ecs.SystemExecutor {
return func(w *ecs.World, delta time.Duration) (err error) {
commands := ecs.QueryEntitiesWithComponent(w, commandQuery(command))
var processedCommands []ecs.Entity
for c := range commands {
logging.Debug("Handling command of type ", command)
player, _ := ecs.GetComponent[data.PlayerComponent](w, c)
args, _ := ecs.GetComponent[data.ArgsComponent](w, c)
err := handler(w, delta, player.Player, args.Args)
if err != nil {
logging.Info("Issue while handling command ", command, ": ", err)
connId, _ := ecs.GetComponent[data.ConnectionIdComponent](w, player.Player)
world.CreateGameOutput(w, connId.ConnectionId, []byte(err.Error()))
}
ecs.SetComponent(w, c, data.CommandStateComponent{State: data.CommandStateExecuted})
// data.CreateCommandExecutedEvent(world, c) // Not needed right now
processedCommands = append(processedCommands, c)
}
ecs.DeleteEntities(w, processedCommands...)
return
}
}

View file

@ -0,0 +1,84 @@
package command
import (
"strings"
"code.haedhutner.dev/mvv/LastMUD/internal/game/data"
)
func oneOf(value string, tests ...string) bool {
for _, t := range tests {
if value == t {
return true
}
}
return false
}
type commandParser = func(tokens []data.Token) (matches bool, args data.ArgsMap)
var Parsers = map[data.Command]commandParser{
data.CommandSay: func(tokens []data.Token) (matches bool, args data.ArgsMap) {
matches = len(tokens) > 1
matches = matches && oneOf(tokens[0].Lexeme, "say", "lc", "localchat")
if !matches {
return
}
var lexemes []string
for _, t := range tokens[1:] {
lexemes = append(lexemes, t.Lexeme)
}
args = data.ArgsMap{
data.ArgMessageContent: {Value: strings.Join(lexemes, " ")},
}
return
},
data.CommandQuit: func(tokens []data.Token) (matches bool, args data.ArgsMap) {
matches = len(tokens) >= 1
matches = matches && oneOf(tokens[0].Lexeme, "quit", "disconnect", "q", "leave")
return
},
data.CommandRegister: func(tokens []data.Token) (matches bool, args data.ArgsMap) {
matches = len(tokens) >= 3
matches = matches && oneOf(tokens[0].Lexeme, "register", "signup")
if !matches {
return
}
accountName := tokens[1].Lexeme
accountPassword := tokens[2].Lexeme
args = data.ArgsMap{
data.ArgAccountName: {Value: accountName},
data.ArgAccountPassword: {Value: accountPassword},
}
return
},
data.CommandLogin: func(tokens []data.Token) (matches bool, args data.ArgsMap) {
matches = len(tokens) >= 3
matches = matches && oneOf(tokens[0].Lexeme, "login", "signin")
if !matches {
return
}
accountName := tokens[1].Lexeme
accountPassword := tokens[2].Lexeme
args = data.ArgsMap{
data.ArgAccountName: {Value: accountName},
data.ArgAccountPassword: {Value: accountPassword},
}
return
},
}

View file

@ -0,0 +1,63 @@
package command
import (
"code.haedhutner.dev/mvv/LastMUD/internal/game/logic/world"
"time"
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
"code.haedhutner.dev/mvv/LastMUD/internal/game/data"
)
func HandleSay(w *ecs.World, _ time.Duration, player ecs.Entity, args data.ArgsMap) (err error) {
playerRoom, ok := ecs.GetComponent[data.InRoomComponent](w, player)
if !ok {
return createCommandError("Player is not in any room!")
}
playerName, ok := ecs.GetComponent[data.NameComponent](w, player)
if !ok {
return createCommandError("Player has no name!")
}
allPlayersInRoom := ecs.QueryEntitiesWithComponent(w, func(comp data.InRoomComponent) bool {
return comp.Room == playerRoom.Room
})
messageArg, ok := args[data.ArgMessageContent]
if !ok {
return createCommandError("No message")
}
message, ok := messageArg.Value.(string)
if !ok {
return createCommandError("Can't interpret message as string")
}
if message == "" {
return nil
}
for p := range allPlayersInRoom {
connId, _ := ecs.GetComponent[data.ConnectionIdComponent](w, p)
world.CreateGameOutput(w, connId.ConnectionId, []byte(playerName.Name+": "+message))
}
return
}
func HandleQuit(w *ecs.World, _ time.Duration, player ecs.Entity, _ data.ArgsMap) (err error) {
connId, _ := ecs.GetComponent[data.ConnectionIdComponent](w, player)
world.CreateClosingGameOutput(w, connId.ConnectionId, []byte("Goodbye!"))
return
}
func HandleRegister(world *ecs.World, delta time.Duration, player ecs.Entity, args map[data.ArgName]data.Arg) (err error) {
return
}

View file

@ -0,0 +1,165 @@
package event
import (
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
"code.haedhutner.dev/mvv/LastMUD/internal/game/data"
"code.haedhutner.dev/mvv/LastMUD/internal/game/logic/command"
"code.haedhutner.dev/mvv/LastMUD/internal/game/logic/world"
"fmt"
"regexp"
)
type commandParseError struct {
err string
}
func createCommandParseError(v ...any) *commandParseError {
return &commandParseError{
err: fmt.Sprint("Error parsing command: ", v),
}
}
func (e *commandParseError) Error() string {
return e.err
}
func HandlePlayerCommand(w *ecs.World, event ecs.Entity) (err error) {
commandString, ok := ecs.GetComponent[data.CommandStringComponent](w, event)
if !ok {
return createCommandParseError("No command string found for event")
}
eventConnId, ok := ecs.GetComponent[data.ConnectionIdComponent](w, event)
if !ok {
return createCommandParseError("No connection id found for event")
}
player := ecs.NilEntity()
for p := range ecs.IterateEntitiesWithComponent[data.IsPlayerComponent](w) {
playerConnId, ok := ecs.GetComponent[data.ConnectionIdComponent](w, p)
if ok && playerConnId.ConnectionId == eventConnId.ConnectionId {
player = p
break
}
}
if player == ecs.NilEntity() {
return createCommandParseError("Unable to find valid player with provided connection id")
}
tokens, err := tokenize(commandString.Command)
if err != nil {
return createCommandParseError("Error with tokenization: ", err)
}
cmd := world.CreateTokenizedCommand(w, player, commandString.Command, tokens)
world.CreateParseCommandEvent(w, cmd)
return
}
func tokenize(commandString string) (tokens []data.Token, err error) {
tokens = []data.Token{}
pos := 0
inputLen := len(commandString)
// Continue iterating until we reach the end of the input
for pos < inputLen {
matched := false
remaining := commandString[pos:]
// Iterate through each token type and test its pattern
for tokenType, pattern := range data.TokenPatterns {
// If the token pattern doesn't compile, panic ( why do we have invalid patterns?! )
tokenPattern := regexp.MustCompile(pattern)
// If the location of the match isn't nil, that means we've found a match
if loc := tokenPattern.FindStringIndex(remaining); loc != nil {
lexeme := remaining[loc[0]:loc[1]]
pos += loc[1]
matched = true
// Skip whitespace
if tokenType == data.TokenWhitespace {
break
}
tokens = append(
tokens,
data.Token{
Type: tokenType,
Lexeme: lexeme,
Index: pos,
},
)
break
}
}
// Unknown tokens are still added
if !matched {
tokens = append(
tokens,
data.Token{
Type: data.TokenUnknown,
Lexeme: commandString[pos : pos+1],
Index: pos,
},
)
pos++
}
}
// Mark the end of the tokens
tokens = append(tokens, data.Token{Type: data.TokenEOF, Lexeme: "", Index: pos})
return
}
func HandleParseCommand(w *ecs.World, event ecs.Entity) (err error) {
cmdEnt, ok := ecs.GetComponent[data.EntityComponent](w, event)
if !ok {
return createCommandParseError("No command entity provided in event")
}
tokens, ok := ecs.GetComponent[data.TokensComponent](w, cmdEnt.Entity)
if !ok {
return createCommandParseError("No tokens provided in command entity")
}
var foundMatch bool
for cmd, parser := range command.Parsers {
match, args := parser(tokens.Tokens)
if !match {
continue
}
ecs.SetComponent(w, cmdEnt.Entity, data.ArgsComponent{Args: args})
ecs.SetComponent(w, cmdEnt.Entity, data.CommandComponent{Cmd: cmd})
ecs.SetComponent(w, cmdEnt.Entity, data.CommandStateComponent{State: data.CommandStateParsed})
foundMatch = true
}
player, _ := ecs.GetComponent[data.PlayerComponent](w, cmdEnt.Entity)
connectionId, _ := ecs.GetComponent[data.ConnectionIdComponent](w, player.Player)
if !foundMatch {
world.CreateGameOutput(w, connectionId.ConnectionId, []byte("Unknown command"))
ecs.DeleteEntity(w, cmdEnt.Entity)
}
return
}

View file

@ -1,37 +1,39 @@
package systems package event
import ( import (
"code.haedhutner.dev/mvv/LastMUD/internal/ecs" "code.haedhutner.dev/mvv/LastMUD/internal/ecs"
"code.haedhutner.dev/mvv/LastMUD/internal/game/data" "code.haedhutner.dev/mvv/LastMUD/internal/game/data"
"code.haedhutner.dev/mvv/LastMUD/internal/game/logic/world"
"code.haedhutner.dev/mvv/LastMUD/internal/logging" "code.haedhutner.dev/mvv/LastMUD/internal/logging"
) )
func handlePlayerConnectEvent(world *ecs.World, event ecs.Entity) (err error) { func HandlePlayerConnect(w *ecs.World, event ecs.Entity) (err error) {
logging.Info("Player connect") logging.Info("Player connect")
connectionId, ok := ecs.GetComponent[data.ConnectionIdComponent](world, event) connectionId, ok := ecs.GetComponent[data.ConnectionIdComponent](w, event)
if !ok { if !ok {
return createEventHandlerError(data.EventPlayerConnect, "Event does not contain connectionId") return createEventHandlerError(data.EventPlayerConnect, "Event does not contain connectionId")
} }
data.CreatePlayer(world, connectionId.ConnectionId, data.PlayerStateJoining) world.CreateJoiningPlayer(w, connectionId.ConnectionId)
data.CreateGameOutput(world, connectionId.ConnectionId, []byte("Welcome to LastMUD!"), false) world.CreateGameOutput(w, connectionId.ConnectionId, []byte("Welcome to LastMUD!"))
world.CreateGameOutput(w, connectionId.ConnectionId, []byte("Before interacting with the game, you must either login or create a new account. Do so using the 'register' and 'login' command(s)."))
return return
} }
func handlePlayerDisconnectEvent(world *ecs.World, event ecs.Entity) (err error) { func HandlePlayerDisconnect(w *ecs.World, event ecs.Entity) (err error) {
logging.Info("Player disconnect") logging.Info("Player disconnect")
connectionId, ok := ecs.GetComponent[data.ConnectionIdComponent](world, event) connectionId, ok := ecs.GetComponent[data.ConnectionIdComponent](w, event)
if !ok { if !ok {
return createEventHandlerError(data.EventPlayerDisconnect, "Event does not contain connectionId") return createEventHandlerError(data.EventPlayerDisconnect, "Event does not contain connectionId")
} }
playerEntity := ecs.QueryFirstEntityWithComponent( playerEntity := ecs.QueryFirstEntityWithComponent(
world, w,
func(c data.ConnectionIdComponent) bool { return c.ConnectionId == connectionId.ConnectionId }, func(c data.ConnectionIdComponent) bool { return c.ConnectionId == connectionId.ConnectionId },
) )
@ -39,7 +41,7 @@ func handlePlayerDisconnectEvent(world *ecs.World, event ecs.Entity) (err error)
return createEventHandlerError(data.EventPlayerDisconnect, "Connection id cannot be associated with a player entity") return createEventHandlerError(data.EventPlayerDisconnect, "Connection id cannot be associated with a player entity")
} }
ecs.DeleteEntity(world, playerEntity) ecs.DeleteEntity(w, playerEntity)
return return
} }

View file

@ -1,4 +1,4 @@
package systems package event
import ( import (
"fmt" "fmt"
@ -9,7 +9,7 @@ import (
"code.haedhutner.dev/mvv/LastMUD/internal/logging" "code.haedhutner.dev/mvv/LastMUD/internal/logging"
) )
type EventHandler func(world *ecs.World, event ecs.Entity) (err error) type Handler func(world *ecs.World, event ecs.Entity) (err error)
type eventError struct { type eventError struct {
err string err string
@ -31,10 +31,10 @@ func eventTypeQuery(eventType data.EventType) func(comp data.EventComponent) boo
} }
} }
func CreateEventHandler(eventType data.EventType, handler EventHandler) ecs.SystemExecutor { func CreateHandler(eventType data.EventType, handler Handler) ecs.SystemExecutor {
return func(world *ecs.World, delta time.Duration) (err error) { return func(world *ecs.World, delta time.Duration) (err error) {
events := ecs.QueryEntitiesWithComponent(world, eventTypeQuery(eventType)) events := ecs.QueryEntitiesWithComponent(world, eventTypeQuery(eventType))
processedEvents := []ecs.Entity{} var processedEvents []ecs.Entity
for event := range events { for event := range events {
logging.Debug("Handling event of type ", eventType) logging.Debug("Handling event of type ", eventType)

View file

@ -0,0 +1,28 @@
package logic
import (
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
"code.haedhutner.dev/mvv/LastMUD/internal/game/data"
"code.haedhutner.dev/mvv/LastMUD/internal/game/logic/command"
"code.haedhutner.dev/mvv/LastMUD/internal/game/logic/event"
)
const (
EventOffset = 0
CommandOffset = 10000
)
func CreateSystems() []*ecs.System {
return []*ecs.System{
// Event Handlers
ecs.CreateSystem("PlayerConnectEventHandler", EventOffset+0, event.CreateHandler(data.EventPlayerConnect, event.HandlePlayerConnect)),
ecs.CreateSystem("PlayerDisconnectEventHandler", EventOffset+1, event.CreateHandler(data.EventPlayerDisconnect, event.HandlePlayerDisconnect)),
ecs.CreateSystem("PlayerCommandEventHandler", EventOffset+2, event.CreateHandler(data.EventPlayerCommand, event.HandlePlayerCommand)),
ecs.CreateSystem("ParseCommandEventHandler", EventOffset+4, event.CreateHandler(data.EventParseCommand, event.HandleParseCommand)),
// Command Handlers
ecs.CreateSystem("SayCommandHandler", CommandOffset+0, command.CreateHandler(data.CommandSay, command.HandleSay)),
ecs.CreateSystem("QuitCommandHandler", CommandOffset+1, command.CreateHandler(data.CommandQuit, command.HandleQuit)),
ecs.CreateSystem("RegisterCommandHandler", CommandOffset+2, command.CreateHandler(data.CommandRegister, command.HandleRegister)),
}
}

View file

@ -0,0 +1,15 @@
package world
import (
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
"code.haedhutner.dev/mvv/LastMUD/internal/game/data"
)
func CreateAccount(world *ecs.World, username, encryptedPassword string) ecs.Entity {
account := ecs.NewEntity()
ecs.SetComponent(world, account, data.NameComponent{Name: username})
ecs.SetComponent(world, account, data.PasswordComponent{EncryptedPassword: encryptedPassword})
return account
}

View file

@ -0,0 +1,17 @@
package world
import (
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
"code.haedhutner.dev/mvv/LastMUD/internal/game/data"
)
func CreateTokenizedCommand(world *ecs.World, player ecs.Entity, commandString string, tokens []data.Token) ecs.Entity {
command := ecs.NewEntity()
ecs.SetComponent(world, command, data.PlayerComponent{Player: player})
ecs.SetComponent(world, command, data.CommandStringComponent{Command: commandString})
ecs.SetComponent(world, command, data.TokensComponent{Tokens: tokens})
ecs.SetComponent(world, command, data.CommandStateComponent{State: data.CommandStateTokenized})
return command
}

View file

@ -0,0 +1,43 @@
package world
import (
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
"code.haedhutner.dev/mvv/LastMUD/internal/game/data"
"github.com/google/uuid"
)
func CreatePlayerConnectEvent(world *ecs.World, connectionId uuid.UUID) {
event := ecs.NewEntity()
ecs.SetComponent(world, event, data.EventComponent{EventType: data.EventPlayerConnect})
ecs.SetComponent(world, event, data.ConnectionIdComponent{ConnectionId: connectionId})
}
func CreatePlayerDisconnectEvent(world *ecs.World, connectionId uuid.UUID) {
event := ecs.NewEntity()
ecs.SetComponent(world, event, data.EventComponent{EventType: data.EventPlayerDisconnect})
ecs.SetComponent(world, event, data.ConnectionIdComponent{ConnectionId: connectionId})
}
func CreatePlayerCommandEvent(world *ecs.World, connectionId uuid.UUID, command string) {
event := ecs.NewEntity()
ecs.SetComponent(world, event, data.EventComponent{EventType: data.EventPlayerCommand})
ecs.SetComponent(world, event, data.ConnectionIdComponent{ConnectionId: connectionId})
ecs.SetComponent(world, event, data.CommandStringComponent{Command: command})
}
func CreateParseCommandEvent(world *ecs.World, command ecs.Entity) {
event := ecs.NewEntity()
ecs.SetComponent(world, event, data.EventComponent{EventType: data.EventParseCommand})
ecs.SetComponent(world, event, data.EntityComponent{Entity: command})
}
func CreateCommandExecutedEvent(world *ecs.World, command ecs.Entity) {
event := ecs.NewEntity()
ecs.SetComponent(world, event, data.EventComponent{EventType: data.EventCommandExecuted})
ecs.SetComponent(world, event, data.EntityComponent{Entity: command})
}

View file

@ -0,0 +1,26 @@
package world
import (
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
"code.haedhutner.dev/mvv/LastMUD/internal/game/data"
"github.com/google/uuid"
)
func CreateGameOutput(w *ecs.World, connectionId uuid.UUID, contents []byte) ecs.Entity {
gameOutput := ecs.NewEntity()
ecs.SetComponent(w, gameOutput, data.ConnectionIdComponent{ConnectionId: connectionId})
ecs.SetComponent(w, gameOutput, data.ContentsComponent{Contents: contents})
return gameOutput
}
func CreateClosingGameOutput(w *ecs.World, connectionId uuid.UUID, contents []byte) ecs.Entity {
gameOutput := ecs.NewEntity()
ecs.SetComponent(w, gameOutput, data.ConnectionIdComponent{ConnectionId: connectionId})
ecs.SetComponent(w, gameOutput, data.ContentsComponent{Contents: contents})
ecs.SetComponent(w, gameOutput, data.CloseConnectionComponent{})
return gameOutput
}

View file

@ -0,0 +1,18 @@
package world
import (
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
"code.haedhutner.dev/mvv/LastMUD/internal/game/data"
"github.com/google/uuid"
)
func CreateJoiningPlayer(world *ecs.World, connectionId uuid.UUID) (entity ecs.Entity) {
entity = ecs.NewEntity()
ecs.SetComponent(world, entity, data.ConnectionIdComponent{ConnectionId: connectionId})
ecs.SetComponent(world, entity, data.PlayerStateComponent{State: data.PlayerStateJoining})
ecs.SetComponent(world, entity, data.NameComponent{Name: connectionId.String()})
ecs.SetComponent(world, entity, data.IsPlayerComponent{})
return
}

View file

@ -0,0 +1,21 @@
package world
import (
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
"code.haedhutner.dev/mvv/LastMUD/internal/game/data"
)
func CreateRoom(
world *ecs.World,
name, description string,
north, south, east, west ecs.Entity,
) ecs.Entity {
entity := ecs.NewEntity()
ecs.SetComponent(world, entity, data.IsRoomComponent{})
ecs.SetComponent(world, entity, data.NameComponent{Name: name})
ecs.SetComponent(world, entity, data.DescriptionComponent{Description: description})
ecs.SetComponent(world, entity, data.NeighborsComponent{North: north, South: south, East: east, West: west})
return entity
}

View file

@ -1,208 +0,0 @@
package systems
import (
"fmt"
"regexp"
"time"
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
"code.haedhutner.dev/mvv/LastMUD/internal/game/data"
"code.haedhutner.dev/mvv/LastMUD/internal/logging"
)
type CommandHandler func(world *ecs.World, delta time.Duration, player ecs.Entity, args map[string]data.Arg) (err error)
func commandQuery(command data.Command) func(comp data.CommandComponent) bool {
return func(comp data.CommandComponent) bool {
return comp.Cmd == command
}
}
func CreateCommandHandler(command data.Command, handler CommandHandler) ecs.SystemExecutor {
return func(world *ecs.World, delta time.Duration) (err error) {
commands := ecs.QueryEntitiesWithComponent(world, commandQuery(command))
processedCommands := []ecs.Entity{}
for c := range commands {
logging.Debug("Handling command of type ", command)
player, _ := ecs.GetComponent[data.PlayerComponent](world, c)
args, _ := ecs.GetComponent[data.ArgsComponent](world, c)
err := handler(world, delta, player.Player, args.Args)
if err != nil {
logging.Info("Issue while handling command ", command, ": ", err)
connId, _ := ecs.GetComponent[data.ConnectionIdComponent](world, player.Player)
data.CreateGameOutput(world, connId.ConnectionId, []byte(err.Error()), false)
}
ecs.SetComponent(world, c, data.CommandStateComponent{State: data.CommandStateExecuted})
// data.CreateCommandExecutedEvent(world, c) // Not needed right now
processedCommands = append(processedCommands, c)
}
ecs.DeleteEntities(world, processedCommands...)
return
}
}
type commandError struct {
err string
}
func createCommandError(v ...any) *commandError {
return &commandError{
err: fmt.Sprint("Error handling command: ", v),
}
}
func (e *commandError) Error() string {
return e.err
}
func handlePlayerCommandEvent(world *ecs.World, event ecs.Entity) (err error) {
commandString, ok := ecs.GetComponent[data.CommandStringComponent](world, event)
if !ok {
return createCommandError("Unable to handle command, no command string found for event")
}
eventConnId, ok := ecs.GetComponent[data.ConnectionIdComponent](world, event)
if !ok {
return createCommandError("Unable to handle command, no connection id found for event")
}
player := ecs.NilEntity()
for p := range ecs.IterateEntitiesWithComponent[data.IsPlayerComponent](world) {
playerConnId, ok := ecs.GetComponent[data.ConnectionIdComponent](world, p)
if ok && playerConnId.ConnectionId == eventConnId.ConnectionId {
player = p
break
}
}
if player == ecs.NilEntity() {
return createCommandError("Unable to find valid player with provided connection id")
}
tokens, err := tokenize(commandString.Command)
if err != nil {
return createCommandError("Error with tokenization: ", err)
}
command := data.CreateTokenizedCommand(world, player, commandString.Command, tokens)
data.CreateParseCommandEvent(world, command)
return
}
func tokenize(commandString string) (tokens []data.Token, err error) {
tokens = []data.Token{}
pos := 0
inputLen := len(commandString)
// Continue iterating until we reach the end of the input
for pos < inputLen {
matched := false
remaining := commandString[pos:]
// Iterate through each token type and test its pattern
for tokenType, pattern := range data.TokenPatterns {
// If the token pattern doesn't compile, panic ( why do we have invalid patterns?! )
tokenPattern := regexp.MustCompile(pattern)
// If the location of the match isn't nil, that means we've found a match
if loc := tokenPattern.FindStringIndex(remaining); loc != nil {
lexeme := remaining[loc[0]:loc[1]]
pos += loc[1]
matched = true
// Skip whitespace
if tokenType == data.TokenWhitespace {
break
}
tokens = append(
tokens,
data.Token{
Type: tokenType,
Lexeme: lexeme,
Index: pos,
},
)
break
}
}
// Unknown tokens are still added
if !matched {
tokens = append(
tokens,
data.Token{
Type: data.TokenUnknown,
Lexeme: commandString[pos : pos+1],
Index: pos,
},
)
pos++
}
}
// Mark the end of the tokens
tokens = append(tokens, data.Token{Type: data.TokenEOF, Lexeme: "", Index: pos})
return
}
func parseCommand(world *ecs.World, event ecs.Entity) (err error) {
command, ok := ecs.GetComponent[data.EntityComponent](world, event)
if !ok {
return createCommandError("Unable to parse command: no command entity provided in event")
}
tokens, ok := ecs.GetComponent[data.TokensComponent](world, command.Entity)
if !ok {
return createCommandError("Unable to parse command: no tokens provided in command entity")
}
var foundMatch bool
for cmd, parser := range commandParsers {
match, args := parser(tokens.Tokens)
if !match {
continue
}
ecs.SetComponent(world, command.Entity, data.ArgsComponent{Args: args})
ecs.SetComponent(world, command.Entity, data.CommandComponent{Cmd: cmd})
ecs.SetComponent(world, command.Entity, data.CommandStateComponent{State: data.CommandStateParsed})
foundMatch = true
}
player, _ := ecs.GetComponent[data.PlayerComponent](world, command.Entity)
connectionId, _ := ecs.GetComponent[data.ConnectionIdComponent](world, player.Player)
if !foundMatch {
data.CreateGameOutput(world, connectionId.ConnectionId, []byte("Unknown command"), false)
ecs.DeleteEntity(world, command.Entity)
}
return
}

View file

@ -1,48 +0,0 @@
package systems
import (
"strings"
"code.haedhutner.dev/mvv/LastMUD/internal/game/data"
)
func oneOf(value string, tests ...string) bool {
for _, t := range tests {
if value == t {
return true
}
}
return false
}
type commandParser = func(tokens []data.Token) (matches bool, args map[string]data.Arg)
var commandParsers = map[data.Command]commandParser{
data.CommandSay: func(tokens []data.Token) (matches bool, args map[string]data.Arg) {
matches = len(tokens) > 1
matches = matches && oneOf(tokens[0].Lexeme, "say", "lc", "localchat")
if !matches {
return
}
lexemes := []string{}
for _, t := range tokens[1:] {
lexemes = append(lexemes, t.Lexeme)
}
args = map[string]data.Arg{
"messageContent": {Value: strings.Join(lexemes, " ")},
}
return
},
data.CommandQuit: func(tokens []data.Token) (matches bool, args map[string]data.Arg) {
matches = len(tokens) >= 1
matches = matches && oneOf(tokens[0].Lexeme, "quit", "disconnect", "q", "leave")
return
},
}

View file

@ -1,60 +0,0 @@
package systems
import (
"time"
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
"code.haedhutner.dev/mvv/LastMUD/internal/game/data"
)
func handleSayCommand(world *ecs.World, delta time.Duration, player ecs.Entity, args map[string]data.Arg) (err error) {
playerRoom, ok := ecs.GetComponent[data.InRoomComponent](world, player)
if !ok {
return createCommandError("Player is not in any room!")
}
playerName, ok := ecs.GetComponent[data.NameComponent](world, player)
if !ok {
return createCommandError("Player has no name!")
}
allPlayersInRoom := ecs.QueryEntitiesWithComponent(world, func(comp data.InRoomComponent) bool {
return comp.Room == playerRoom.Room
})
messageArg, ok := args["messageContent"]
if !ok {
return createCommandError("No message")
}
message, ok := messageArg.Value.(string)
if !ok {
return createCommandError("Can't interpret message as string")
}
if message == "" {
return nil
}
for p := range allPlayersInRoom {
connId, _ := ecs.GetComponent[data.ConnectionIdComponent](world, p)
data.CreateGameOutput(world, connId.ConnectionId, []byte(playerName.Name+": "+message), false)
}
return
}
func handleQuitCommand(world *ecs.World, delta time.Duration, player ecs.Entity, _ map[string]data.Arg) (err error) {
connId, _ := ecs.GetComponent[data.ConnectionIdComponent](world, player)
data.CreateGameOutput(world, connId.ConnectionId, []byte("Goodbye!"), true)
data.CreatePlayerDisconnectEvent(world, connId.ConnectionId)
return
}

View file

@ -1,24 +0,0 @@
package systems
import (
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
"code.haedhutner.dev/mvv/LastMUD/internal/game/data"
)
type SystemPriorityOffset = int
const (
EventOffset = 0
CommandOffset = 10000
)
func CreateSystems() []*ecs.System {
return []*ecs.System{
ecs.CreateSystem("PlayerConnectEventHandler", EventOffset+0, CreateEventHandler(data.EventPlayerConnect, handlePlayerConnectEvent)),
ecs.CreateSystem("PlayerDisconnectEventHandler", EventOffset+1, CreateEventHandler(data.EventPlayerDisconnect, handlePlayerDisconnectEvent)),
ecs.CreateSystem("PlayerCommandEventHandler", EventOffset+2, CreateEventHandler(data.EventPlayerCommand, handlePlayerCommandEvent)),
ecs.CreateSystem("ParseCommandEventHandler", EventOffset+4, CreateEventHandler(data.EventParseCommand, parseCommand)),
ecs.CreateSystem("SayCommandHandler", CommandOffset+0, CreateCommandHandler(data.CommandSay, handleSayCommand)),
ecs.CreateSystem("QuitCommandHandler", CommandOffset+1, CreateCommandHandler(data.CommandQuit, handleQuitCommand)),
}
}

View file

@ -1,19 +1,17 @@
package data package game
import ( import (
"code.haedhutner.dev/mvv/LastMUD/internal/ecs" "code.haedhutner.dev/mvv/LastMUD/internal/ecs"
"code.haedhutner.dev/mvv/LastMUD/internal/game/data"
"code.haedhutner.dev/mvv/LastMUD/internal/game/logic/world"
) )
const ( type World struct {
ResourceDefaultRoom ecs.Resource = "world:room:default"
)
type GameWorld struct {
*ecs.World *ecs.World
} }
func CreateGameWorld() (gw *GameWorld) { func CreateGameWorld() (gw *World) {
gw = &GameWorld{ gw = &World{
World: ecs.CreateWorld(), World: ecs.CreateWorld(),
} }
@ -22,9 +20,9 @@ func CreateGameWorld() (gw *GameWorld) {
return return
} }
func defineRooms(world *ecs.World) { func defineRooms(w *ecs.World) {
forest := CreateRoom( forest := world.CreateRoom(
world, w,
"Forest", "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.", "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.",
ecs.NilEntity(), ecs.NilEntity(),
@ -33,10 +31,10 @@ func defineRooms(world *ecs.World) {
ecs.NilEntity(), ecs.NilEntity(),
) )
ecs.SetResource(world, ResourceDefaultRoom, forest) ecs.SetResource(w, data.ResourceDefaultRoom, forest)
cabin := CreateRoom( cabin := world.CreateRoom(
world, w,
"Wooden Cabin", "Wooden Cabin",
"The cabins 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.", "The cabins 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.",
ecs.NilEntity(), ecs.NilEntity(),
@ -45,8 +43,8 @@ func defineRooms(world *ecs.World) {
ecs.NilEntity(), ecs.NilEntity(),
) )
lake := CreateRoom( lake := world.CreateRoom(
world, w,
"Ethermere Lake", "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.", "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.",
ecs.NilEntity(), ecs.NilEntity(),
@ -55,8 +53,8 @@ func defineRooms(world *ecs.World) {
ecs.NilEntity(), ecs.NilEntity(),
) )
graveyard := CreateRoom( graveyard := world.CreateRoom(
world, w,
"Graveyard", "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.", "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.",
ecs.NilEntity(), ecs.NilEntity(),
@ -65,8 +63,8 @@ func defineRooms(world *ecs.World) {
ecs.NilEntity(), ecs.NilEntity(),
) )
chapel := CreateRoom( chapel := world.CreateRoom(
world, w,
"Chapel of the Hollow Light", "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.", "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.",
ecs.NilEntity(), ecs.NilEntity(),
@ -75,32 +73,32 @@ func defineRooms(world *ecs.World) {
ecs.NilEntity(), ecs.NilEntity(),
) )
ecs.SetComponent(world, forest, NeighborsComponent{ ecs.SetComponent(w, forest, data.NeighborsComponent{
North: cabin, North: cabin,
South: graveyard, South: graveyard,
East: lake, East: lake,
West: chapel, West: chapel,
}) })
ecs.SetComponent(world, cabin, NeighborsComponent{ ecs.SetComponent(w, cabin, data.NeighborsComponent{
South: graveyard, South: graveyard,
West: chapel, West: chapel,
East: lake, East: lake,
}) })
ecs.SetComponent(world, chapel, NeighborsComponent{ ecs.SetComponent(w, chapel, data.NeighborsComponent{
North: cabin, North: cabin,
South: graveyard, South: graveyard,
East: forest, East: forest,
}) })
ecs.SetComponent(world, lake, NeighborsComponent{ ecs.SetComponent(w, lake, data.NeighborsComponent{
West: forest, West: forest,
North: cabin, North: cabin,
South: graveyard, South: graveyard,
}) })
ecs.SetComponent(world, graveyard, NeighborsComponent{ ecs.SetComponent(w, graveyard, data.NeighborsComponent{
North: forest, North: forest,
West: chapel, West: chapel,
East: lake, East: lake,

View file

@ -27,6 +27,8 @@ type Connection struct {
conn *net.TCPConn conn *net.TCPConn
lastSeen time.Time lastSeen time.Time
closeChan chan struct{}
} }
func CreateConnection(server *Server, 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) {
@ -36,12 +38,13 @@ func CreateConnection(server *Server, conn *net.TCPConn, ctx context.Context, wg
conn.SetKeepAlivePeriod(1 * time.Second) conn.SetKeepAlivePeriod(1 * time.Second)
c = &Connection{ c = &Connection{
ctx: ctx, ctx: ctx,
wg: wg, wg: wg,
server: server, server: server,
identity: uuid.New(), identity: uuid.New(),
conn: conn, conn: conn,
lastSeen: time.Now(), lastSeen: time.Now(),
closeChan: make(chan struct{}, 1),
} }
c.wg.Add(2) c.wg.Add(2)
@ -115,8 +118,19 @@ func (c *Connection) shouldClose() bool {
case <-c.ctx.Done(): case <-c.ctx.Done():
return true return true
default: default:
return false
} }
select {
case <-c.closeChan:
return true
default:
}
return false
}
func (c *Connection) CommandClose() {
c.closeChan <- struct{}{}
} }
func (c *Connection) closeConnection() { func (c *Connection) closeConnection() {

View file

@ -19,7 +19,7 @@ type Server struct {
connections map[uuid.UUID]*Connection connections map[uuid.UUID]*Connection
lastmudgame *game.LastMUDGame lastmudgame *game.Game
} }
func CreateServer(ctx context.Context, wg *sync.WaitGroup, port string) (srv *Server, err error) { func CreateServer(ctx context.Context, wg *sync.WaitGroup, port string) (srv *Server, err error) {
@ -63,7 +63,7 @@ func CreateServer(ctx context.Context, wg *sync.WaitGroup, port string) (srv *Se
return return
} }
func (srv *Server) game() *game.LastMUDGame { func (srv *Server) game() *game.Game {
return srv.lastmudgame return srv.lastmudgame
} }
@ -118,7 +118,7 @@ func (srv *Server) consumeGameOutput() {
} }
if output.ShouldCloseConnection() { if output.ShouldCloseConnection() {
conn.closeConnection() conn.CommandClose()
delete(srv.connections, output.Id()) delete(srv.connections, output.Id())
} }
} }