Restructure some things, move things about
This commit is contained in:
parent
574dc2fa4a
commit
308c343068
38 changed files with 781 additions and 548 deletions
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -1,2 +1,6 @@
|
|||
log/*
|
||||
*.log
|
||||
log/
|
||||
target/
|
||||
coverage/
|
||||
.idea/
|
||||
|
||||
*.log
|
||||
|
|
25
Dockerfile
Normal file
25
Dockerfile
Normal 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
9
bin/build.sh
Executable 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
9
bin/build_docker.sh
Executable 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
9
bin/run.sh
Executable 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
12
bin/test_coverage.sh
Executable 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
|
|
@ -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
2
go.mod
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
19
internal/game/data/account.go
Normal file
19
internal/game/data/account.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -25,6 +25,9 @@ const (
|
|||
TypeCommandState
|
||||
TypeCommandArgs
|
||||
TypeCommand
|
||||
|
||||
TypeAccount
|
||||
TypePassword
|
||||
)
|
||||
|
||||
type Direction byte
|
||||
|
|
|
@ -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})
|
||||
}
|
||||
|
|
|
@ -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{})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
7
internal/game/data/resources.go
Normal file
7
internal/game/data/resources.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package data
|
||||
|
||||
import "code.haedhutner.dev/mvv/LastMUD/internal/ecs"
|
||||
|
||||
const (
|
||||
ResourceDefaultRoom ecs.Resource = "world:room:default"
|
||||
)
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
67
internal/game/logic/command/command.go
Normal file
67
internal/game/logic/command/command.go
Normal 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
|
||||
}
|
||||
}
|
84
internal/game/logic/command/command_parsers.go
Normal file
84
internal/game/logic/command/command_parsers.go
Normal 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
|
||||
},
|
||||
}
|
63
internal/game/logic/command/commands.go
Normal file
63
internal/game/logic/command/commands.go
Normal 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
|
||||
}
|
165
internal/game/logic/event/command.go
Normal file
165
internal/game/logic/event/command.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
28
internal/game/logic/systems.go
Normal file
28
internal/game/logic/systems.go
Normal 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)),
|
||||
}
|
||||
}
|
15
internal/game/logic/world/account.go
Normal file
15
internal/game/logic/world/account.go
Normal 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
|
||||
}
|
17
internal/game/logic/world/command.go
Normal file
17
internal/game/logic/world/command.go
Normal 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
|
||||
}
|
43
internal/game/logic/world/events.go
Normal file
43
internal/game/logic/world/events.go
Normal 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})
|
||||
}
|
26
internal/game/logic/world/output.go
Normal file
26
internal/game/logic/world/output.go
Normal 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
|
||||
}
|
18
internal/game/logic/world/player.go
Normal file
18
internal/game/logic/world/player.go
Normal 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
|
||||
}
|
21
internal/game/logic/world/room.go
Normal file
21
internal/game/logic/world/room.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
},
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)),
|
||||
}
|
||||
}
|
|
@ -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 cabin’s interior is cozy and rustic, with wooden beams overhead and a stone fireplace crackling warmly. A wool rug lies on creaky floorboards, and shelves brim with books, mugs, and old lanterns.",
|
||||
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,
|
|
@ -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() {
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue