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

2
go.mod
View file

@ -4,13 +4,11 @@ go 1.24.4
require (
github.com/google/uuid v1.6.0
golang.org/x/term v0.32.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.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
)

View file

@ -175,7 +175,7 @@ func RemoveResource(world *World, r Resource) {
delete(world.resources, r)
}
func registerComponent[T Component](world *World, compType ComponentType) {
func registerComponent(world *World, compType ComponentType) {
if _, ok := world.componentsByType[compType]; ok {
return
}
@ -184,7 +184,7 @@ func registerComponent[T Component](world *World, compType ComponentType) {
}
func SetComponent[T Component](world *World, entity Entity, component T) {
registerComponent[T](world, component.Type())
registerComponent(world, 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)
casted, castSuccess := val.(T)
return casted, (exists && castSuccess)
return casted, exists && castSuccess
}
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) {
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()
registerComponent[T](world, compType)
registerComponent(world, compType)
return world.componentsByType[compType]
}
@ -292,7 +293,11 @@ func RegisterSystem(world *World, s *System) {
}
func RegisterSystems(world *World, systems ...*System) {
for _, s := range systems {
RegisterSystem(world, s)
}
world.systems = append(world.systems, systems...)
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
}
type ArgName = string
const (
ArgMessageContent ArgName = "messageContent"
ArgAccountName = "accountName"
ArgAccountPassword = "accountPassword"
)
type Arg struct {
Value any
}
type ArgsMap = map[ArgName]Arg
type ArgsComponent struct {
Args map[string]Arg
Args ArgsMap
}
func (ac ArgsComponent) Type() ecs.ComponentType {
@ -76,8 +86,12 @@ func (ac ArgsComponent) Type() ecs.ComponentType {
type Command string
const (
CommandSay Command = "say"
CommandQuit = "quit"
CommandSay Command = "say"
CommandQuit = "quit"
CommandHelp = "help"
CommandSetName = "setname"
CommandLogin = "login"
CommandRegister = "register"
)
type CommandComponent struct {
@ -87,14 +101,3 @@ type CommandComponent struct {
func (cc CommandComponent) Type() ecs.ComponentType {
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
TypeCommandArgs
TypeCommand
TypeAccount
TypePassword
)
type Direction byte

View file

@ -2,7 +2,6 @@ package data
import (
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
"github.com/google/uuid"
)
type EventType string
@ -23,39 +22,3 @@ type EventComponent struct {
func (is EventComponent) Type() ecs.ComponentType {
return TypeEvent
}
func CreatePlayerConnectEvent(world *ecs.World, connectionId uuid.UUID) {
event := ecs.NewEntity()
ecs.SetComponent(world, event, EventComponent{EventType: EventPlayerConnect})
ecs.SetComponent(world, event, ConnectionIdComponent{ConnectionId: connectionId})
}
func CreatePlayerDisconnectEvent(world *ecs.World, connectionId uuid.UUID) {
event := ecs.NewEntity()
ecs.SetComponent(world, event, EventComponent{EventType: EventPlayerDisconnect})
ecs.SetComponent(world, event, ConnectionIdComponent{ConnectionId: connectionId})
}
func CreatePlayerCommandEvent(world *ecs.World, connectionId uuid.UUID, command string) {
event := ecs.NewEntity()
ecs.SetComponent(world, event, EventComponent{EventType: EventPlayerCommand})
ecs.SetComponent(world, event, ConnectionIdComponent{ConnectionId: connectionId})
ecs.SetComponent(world, event, CommandStringComponent{Command: command})
}
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 (
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
"github.com/google/uuid"
)
type ContentsComponent struct {
@ -18,14 +17,3 @@ type CloseConnectionComponent struct{}
func (cc CloseConnectionComponent) Type() ecs.ComponentType {
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 (
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
"github.com/google/uuid"
)
type PlayerState = byte
@ -36,21 +35,3 @@ type IsPlayerComponent struct{}
func (c IsPlayerComponent) Type() ecs.ComponentType {
return TypeIsPlayer
}
func CreatePlayer(world *ecs.World, id uuid.UUID, state PlayerState) (entity ecs.Entity, err error) {
entity = ecs.NewEntity()
defaultRoom, err := ecs.GetResource[ecs.Entity](world, ResourceDefaultRoom)
if err != nil {
return
}
ecs.SetComponent(world, entity, ConnectionIdComponent{ConnectionId: id})
ecs.SetComponent(world, entity, PlayerStateComponent{State: state})
ecs.SetComponent(world, entity, NameComponent{Name: id.String()})
ecs.SetComponent(world, entity, InRoomComponent{Room: defaultRoom})
ecs.SetComponent(world, entity, IsPlayerComponent{})
return
}

View file

@ -0,0 +1,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 {
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
import (
"code.haedhutner.dev/mvv/LastMUD/internal/game/logic/world"
"context"
"sync"
"time"
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
"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"
"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
contents []byte
closeConnection bool
}
func (g GameOutput) Id() uuid.UUID {
func (g Output) Id() uuid.UUID {
return g.connId
}
func (g GameOutput) Contents() []byte {
func (g Output) Contents() []byte {
return g.contents
}
func (g GameOutput) ShouldCloseConnection() bool {
func (g Output) ShouldCloseConnection() bool {
return g.closeConnection
}
type LastMUDGame struct {
type Game struct {
ctx context.Context
wg *sync.WaitGroup
world *data.GameWorld
world *World
output chan GameOutput
output chan Output
}
func CreateGame(ctx context.Context, wg *sync.WaitGroup) (game *LastMUDGame) {
game = &LastMUDGame{
func CreateGame(ctx context.Context, wg *sync.WaitGroup) (game *Game) {
game = &Game{
wg: wg,
ctx: ctx,
output: make(chan GameOutput),
world: data.CreateGameWorld(),
output: make(chan Output),
world: CreateGameWorld(),
}
ecs.RegisterSystems(game.world.World, systems.CreateSystems()...)
ecs.RegisterSystems(game.world.World, logic.CreateSystems()...)
wg.Add(1)
go game.start()
@ -58,8 +59,8 @@ func CreateGame(ctx context.Context, wg *sync.WaitGroup) (game *LastMUDGame) {
return
}
// Will block if no output present
func (game *LastMUDGame) ConsumeNextOutput() *GameOutput {
// ConsumeNextOutput will block if no output present
func (game *Game) ConsumeNextOutput() *Output {
select {
case output := <-game.output:
return &output
@ -68,19 +69,19 @@ func (game *LastMUDGame) ConsumeNextOutput() *GameOutput {
}
}
func (game *LastMUDGame) ConnectPlayer(connectionId uuid.UUID) {
data.CreatePlayerConnectEvent(game.world.World, connectionId)
func (game *Game) ConnectPlayer(connectionId uuid.UUID) {
world.CreatePlayerConnectEvent(game.world.World, connectionId)
}
func (game *LastMUDGame) DisconnectPlayer(connectionId uuid.UUID) {
data.CreatePlayerDisconnectEvent(game.world.World, connectionId)
func (game *Game) DisconnectPlayer(connectionId uuid.UUID) {
world.CreatePlayerDisconnectEvent(game.world.World, connectionId)
}
func (game *LastMUDGame) SendPlayerCommand(connectionId uuid.UUID, command string) {
data.CreatePlayerCommandEvent(game.world.World, connectionId, command)
func (game *Game) SendPlayerCommand(connectionId uuid.UUID, command string) {
world.CreatePlayerCommandEvent(game.world.World, connectionId, command)
}
func (game *LastMUDGame) start() {
func (game *Game) start() {
defer game.wg.Done()
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)
for _, entity := range entities {
output := GameOutput{}
output := Output{}
connId, _ := ecs.GetComponent[data.ConnectionIdComponent](game.world.World, entity)
output.connId = connId.ConnectionId
@ -130,12 +131,12 @@ func (game *LastMUDGame) consumeOutputs() {
ecs.DeleteEntities(game.world.World, entities...)
}
func (game *LastMUDGame) shutdown() {
func (game *Game) shutdown() {
logging.Info("Stopping LastMUD...")
close(game.output)
}
func (game *LastMUDGame) shouldStop() bool {
func (game *Game) shouldStop() bool {
select {
case <-game.ctx.Done():
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
}
func (g *LastMUDGame) tick(delta time.Duration) {
g.world.Tick(delta)
g.consumeOutputs()
func (game *Game) tick(delta time.Duration) {
game.world.Tick(delta)
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 (
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
"code.haedhutner.dev/mvv/LastMUD/internal/game/data"
"code.haedhutner.dev/mvv/LastMUD/internal/game/logic/world"
"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")
connectionId, ok := ecs.GetComponent[data.ConnectionIdComponent](world, event)
connectionId, ok := ecs.GetComponent[data.ConnectionIdComponent](w, event)
if !ok {
return createEventHandlerError(data.EventPlayerConnect, "Event does not contain connectionId")
}
data.CreatePlayer(world, connectionId.ConnectionId, data.PlayerStateJoining)
data.CreateGameOutput(world, connectionId.ConnectionId, []byte("Welcome to LastMUD!"), false)
world.CreateJoiningPlayer(w, connectionId.ConnectionId)
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
}
func handlePlayerDisconnectEvent(world *ecs.World, event ecs.Entity) (err error) {
func HandlePlayerDisconnect(w *ecs.World, event ecs.Entity) (err error) {
logging.Info("Player disconnect")
connectionId, ok := ecs.GetComponent[data.ConnectionIdComponent](world, event)
connectionId, ok := ecs.GetComponent[data.ConnectionIdComponent](w, event)
if !ok {
return createEventHandlerError(data.EventPlayerDisconnect, "Event does not contain connectionId")
}
playerEntity := ecs.QueryFirstEntityWithComponent(
world,
w,
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")
}
ecs.DeleteEntity(world, playerEntity)
ecs.DeleteEntity(w, playerEntity)
return
}

View file

@ -1,4 +1,4 @@
package systems
package event
import (
"fmt"
@ -9,7 +9,7 @@ import (
"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 {
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) {
events := ecs.QueryEntitiesWithComponent(world, eventTypeQuery(eventType))
processedEvents := []ecs.Entity{}
var processedEvents []ecs.Entity
for event := range events {
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 (
"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 (
ResourceDefaultRoom ecs.Resource = "world:room:default"
)
type GameWorld struct {
type World struct {
*ecs.World
}
func CreateGameWorld() (gw *GameWorld) {
gw = &GameWorld{
func CreateGameWorld() (gw *World) {
gw = &World{
World: ecs.CreateWorld(),
}
@ -22,9 +20,9 @@ func CreateGameWorld() (gw *GameWorld) {
return
}
func defineRooms(world *ecs.World) {
forest := CreateRoom(
world,
func defineRooms(w *ecs.World) {
forest := world.CreateRoom(
w,
"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.",
ecs.NilEntity(),
@ -33,10 +31,10 @@ func defineRooms(world *ecs.World) {
ecs.NilEntity(),
)
ecs.SetResource(world, ResourceDefaultRoom, forest)
ecs.SetResource(w, data.ResourceDefaultRoom, forest)
cabin := CreateRoom(
world,
cabin := world.CreateRoom(
w,
"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.",
ecs.NilEntity(),
@ -45,8 +43,8 @@ func defineRooms(world *ecs.World) {
ecs.NilEntity(),
)
lake := CreateRoom(
world,
lake := world.CreateRoom(
w,
"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.",
ecs.NilEntity(),
@ -55,8 +53,8 @@ func defineRooms(world *ecs.World) {
ecs.NilEntity(),
)
graveyard := CreateRoom(
world,
graveyard := world.CreateRoom(
w,
"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.",
ecs.NilEntity(),
@ -65,8 +63,8 @@ func defineRooms(world *ecs.World) {
ecs.NilEntity(),
)
chapel := CreateRoom(
world,
chapel := world.CreateRoom(
w,
"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.",
ecs.NilEntity(),
@ -75,32 +73,32 @@ func defineRooms(world *ecs.World) {
ecs.NilEntity(),
)
ecs.SetComponent(world, forest, NeighborsComponent{
ecs.SetComponent(w, forest, data.NeighborsComponent{
North: cabin,
South: graveyard,
East: lake,
West: chapel,
})
ecs.SetComponent(world, cabin, NeighborsComponent{
ecs.SetComponent(w, cabin, data.NeighborsComponent{
South: graveyard,
West: chapel,
East: lake,
})
ecs.SetComponent(world, chapel, NeighborsComponent{
ecs.SetComponent(w, chapel, data.NeighborsComponent{
North: cabin,
South: graveyard,
East: forest,
})
ecs.SetComponent(world, lake, NeighborsComponent{
ecs.SetComponent(w, lake, data.NeighborsComponent{
West: forest,
North: cabin,
South: graveyard,
})
ecs.SetComponent(world, graveyard, NeighborsComponent{
ecs.SetComponent(w, graveyard, data.NeighborsComponent{
North: forest,
West: chapel,
East: lake,

View file

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

View file

@ -19,7 +19,7 @@ type Server struct {
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) {
@ -63,7 +63,7 @@ func CreateServer(ctx context.Context, wg *sync.WaitGroup, port string) (srv *Se
return
}
func (srv *Server) game() *game.LastMUDGame {
func (srv *Server) game() *game.Game {
return srv.lastmudgame
}
@ -118,7 +118,7 @@ func (srv *Server) consumeGameOutput() {
}
if output.ShouldCloseConnection() {
conn.closeConnection()
conn.CommandClose()
delete(srv.connections, output.Id())
}
}