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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"flag"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
@ -13,10 +14,15 @@ import (
|
||||||
_ "net/http/pprof"
|
_ "net/http/pprof"
|
||||||
|
|
||||||
"code.haedhutner.dev/mvv/LastMUD/internal/server"
|
"code.haedhutner.dev/mvv/LastMUD/internal/server"
|
||||||
"golang.org/x/term"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var enableDiagnostics bool = false
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
flag.BoolVar(&enableDiagnostics, "d", false, "Enable pprof server ( port :6060 ). Disabled by default.")
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
wg := sync.WaitGroup{}
|
wg := sync.WaitGroup{}
|
||||||
|
|
||||||
|
@ -29,27 +35,19 @@ func main() {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
if enableDiagnostics {
|
||||||
log.Println(http.ListenAndServe("localhost:6060", nil))
|
go func() {
|
||||||
}()
|
log.Println(http.ListenAndServe("localhost:6060", nil))
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
processInput()
|
processInput()
|
||||||
}
|
}
|
||||||
|
|
||||||
func processInput() {
|
func processInput() {
|
||||||
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer term.Restore(int(os.Stdin.Fd()), oldState)
|
|
||||||
|
|
||||||
sigChan := make(chan os.Signal, 1)
|
sigChan := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
buf := make([]byte, 1)
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
// If interrupt received, stop
|
// If interrupt received, stop
|
||||||
select {
|
select {
|
||||||
|
@ -58,13 +56,6 @@ func processInput() {
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Proper TUI for the server
|
|
||||||
os.Stdin.Read(buf)
|
|
||||||
|
|
||||||
if buf[0] == 'q' {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
time.Sleep(50 * time.Millisecond)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -4,13 +4,11 @@ go 1.24.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
golang.org/x/term v0.32.0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/stretchr/testify v1.10.0 // indirect
|
github.com/stretchr/testify v1.10.0 // indirect
|
||||||
golang.org/x/sys v0.33.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
@ -175,7 +175,7 @@ func RemoveResource(world *World, r Resource) {
|
||||||
delete(world.resources, r)
|
delete(world.resources, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerComponent[T Component](world *World, compType ComponentType) {
|
func registerComponent(world *World, compType ComponentType) {
|
||||||
if _, ok := world.componentsByType[compType]; ok {
|
if _, ok := world.componentsByType[compType]; ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -184,7 +184,7 @@ func registerComponent[T Component](world *World, compType ComponentType) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetComponent[T Component](world *World, entity Entity, component T) {
|
func SetComponent[T Component](world *World, entity Entity, component T) {
|
||||||
registerComponent[T](world, component.Type())
|
registerComponent(world, component.Type())
|
||||||
|
|
||||||
compStorage := world.componentsByType[component.Type()]
|
compStorage := world.componentsByType[component.Type()]
|
||||||
|
|
||||||
|
@ -197,7 +197,7 @@ func GetComponent[T Component](world *World, entity Entity) (component T, exists
|
||||||
val, exists := storage.Get(entity)
|
val, exists := storage.Get(entity)
|
||||||
casted, castSuccess := val.(T)
|
casted, castSuccess := val.(T)
|
||||||
|
|
||||||
return casted, (exists && castSuccess)
|
return casted, exists && castSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteComponent[T Component](world *World, entity Entity) {
|
func DeleteComponent[T Component](world *World, entity Entity) {
|
||||||
|
@ -209,9 +209,10 @@ func DeleteComponent[T Component](world *World, entity Entity) {
|
||||||
func GetComponentStorage[T Component](world *World) (compStorage *ComponentStorage) {
|
func GetComponentStorage[T Component](world *World) (compStorage *ComponentStorage) {
|
||||||
var zero T
|
var zero T
|
||||||
|
|
||||||
|
// This is ok because the `Type` function is expected to return a hard-coded value and not depend on component state
|
||||||
compType := zero.Type()
|
compType := zero.Type()
|
||||||
|
|
||||||
registerComponent[T](world, compType)
|
registerComponent(world, compType)
|
||||||
|
|
||||||
return world.componentsByType[compType]
|
return world.componentsByType[compType]
|
||||||
}
|
}
|
||||||
|
@ -292,7 +293,11 @@ func RegisterSystem(world *World, s *System) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func RegisterSystems(world *World, systems ...*System) {
|
func RegisterSystems(world *World, systems ...*System) {
|
||||||
for _, s := range systems {
|
world.systems = append(world.systems, systems...)
|
||||||
RegisterSystem(world, s)
|
slices.SortFunc(
|
||||||
}
|
world.systems,
|
||||||
|
func(a, b *System) int {
|
||||||
|
return a.priority - b.priority
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
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
|
return TypeCommandTokens
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ArgName = string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ArgMessageContent ArgName = "messageContent"
|
||||||
|
ArgAccountName = "accountName"
|
||||||
|
ArgAccountPassword = "accountPassword"
|
||||||
|
)
|
||||||
|
|
||||||
type Arg struct {
|
type Arg struct {
|
||||||
Value any
|
Value any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ArgsMap = map[ArgName]Arg
|
||||||
|
|
||||||
type ArgsComponent struct {
|
type ArgsComponent struct {
|
||||||
Args map[string]Arg
|
Args ArgsMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ac ArgsComponent) Type() ecs.ComponentType {
|
func (ac ArgsComponent) Type() ecs.ComponentType {
|
||||||
|
@ -76,8 +86,12 @@ func (ac ArgsComponent) Type() ecs.ComponentType {
|
||||||
type Command string
|
type Command string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
CommandSay Command = "say"
|
CommandSay Command = "say"
|
||||||
CommandQuit = "quit"
|
CommandQuit = "quit"
|
||||||
|
CommandHelp = "help"
|
||||||
|
CommandSetName = "setname"
|
||||||
|
CommandLogin = "login"
|
||||||
|
CommandRegister = "register"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CommandComponent struct {
|
type CommandComponent struct {
|
||||||
|
@ -87,14 +101,3 @@ type CommandComponent struct {
|
||||||
func (cc CommandComponent) Type() ecs.ComponentType {
|
func (cc CommandComponent) Type() ecs.ComponentType {
|
||||||
return TypeCommand
|
return TypeCommand
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateTokenizedCommand(world *ecs.World, player ecs.Entity, commandString string, tokens []Token) ecs.Entity {
|
|
||||||
command := ecs.NewEntity()
|
|
||||||
|
|
||||||
ecs.SetComponent(world, command, PlayerComponent{Player: player})
|
|
||||||
ecs.SetComponent(world, command, CommandStringComponent{Command: commandString})
|
|
||||||
ecs.SetComponent(world, command, TokensComponent{Tokens: tokens})
|
|
||||||
ecs.SetComponent(world, command, CommandStateComponent{State: CommandStateTokenized})
|
|
||||||
|
|
||||||
return command
|
|
||||||
}
|
|
||||||
|
|
|
@ -25,6 +25,9 @@ const (
|
||||||
TypeCommandState
|
TypeCommandState
|
||||||
TypeCommandArgs
|
TypeCommandArgs
|
||||||
TypeCommand
|
TypeCommand
|
||||||
|
|
||||||
|
TypeAccount
|
||||||
|
TypePassword
|
||||||
)
|
)
|
||||||
|
|
||||||
type Direction byte
|
type Direction byte
|
||||||
|
|
|
@ -2,7 +2,6 @@ package data
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
|
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type EventType string
|
type EventType string
|
||||||
|
@ -23,39 +22,3 @@ type EventComponent struct {
|
||||||
func (is EventComponent) Type() ecs.ComponentType {
|
func (is EventComponent) Type() ecs.ComponentType {
|
||||||
return TypeEvent
|
return TypeEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreatePlayerConnectEvent(world *ecs.World, connectionId uuid.UUID) {
|
|
||||||
event := ecs.NewEntity()
|
|
||||||
|
|
||||||
ecs.SetComponent(world, event, EventComponent{EventType: EventPlayerConnect})
|
|
||||||
ecs.SetComponent(world, event, ConnectionIdComponent{ConnectionId: connectionId})
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreatePlayerDisconnectEvent(world *ecs.World, connectionId uuid.UUID) {
|
|
||||||
event := ecs.NewEntity()
|
|
||||||
|
|
||||||
ecs.SetComponent(world, event, EventComponent{EventType: EventPlayerDisconnect})
|
|
||||||
ecs.SetComponent(world, event, ConnectionIdComponent{ConnectionId: connectionId})
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreatePlayerCommandEvent(world *ecs.World, connectionId uuid.UUID, command string) {
|
|
||||||
event := ecs.NewEntity()
|
|
||||||
|
|
||||||
ecs.SetComponent(world, event, EventComponent{EventType: EventPlayerCommand})
|
|
||||||
ecs.SetComponent(world, event, ConnectionIdComponent{ConnectionId: connectionId})
|
|
||||||
ecs.SetComponent(world, event, CommandStringComponent{Command: command})
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateParseCommandEvent(world *ecs.World, command ecs.Entity) {
|
|
||||||
event := ecs.NewEntity()
|
|
||||||
|
|
||||||
ecs.SetComponent(world, event, EventComponent{EventType: EventParseCommand})
|
|
||||||
ecs.SetComponent(world, event, EntityComponent{Entity: command})
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateCommandExecutedEvent(world *ecs.World, command ecs.Entity) {
|
|
||||||
event := ecs.NewEntity()
|
|
||||||
|
|
||||||
ecs.SetComponent(world, event, EventComponent{EventType: EventCommandExecuted})
|
|
||||||
ecs.SetComponent(world, event, EntityComponent{Entity: command})
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ package data
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
|
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ContentsComponent struct {
|
type ContentsComponent struct {
|
||||||
|
@ -18,14 +17,3 @@ type CloseConnectionComponent struct{}
|
||||||
func (cc CloseConnectionComponent) Type() ecs.ComponentType {
|
func (cc CloseConnectionComponent) Type() ecs.ComponentType {
|
||||||
return TypeCloseConnection
|
return TypeCloseConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateGameOutput(world *ecs.World, connectionId uuid.UUID, contents []byte, shouldClose bool) {
|
|
||||||
gameOutput := ecs.NewEntity()
|
|
||||||
|
|
||||||
ecs.SetComponent(world, gameOutput, ConnectionIdComponent{ConnectionId: connectionId})
|
|
||||||
ecs.SetComponent(world, gameOutput, ContentsComponent{Contents: contents})
|
|
||||||
|
|
||||||
if shouldClose {
|
|
||||||
ecs.SetComponent(world, gameOutput, CloseConnectionComponent{})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ package data
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
|
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type PlayerState = byte
|
type PlayerState = byte
|
||||||
|
@ -36,21 +35,3 @@ type IsPlayerComponent struct{}
|
||||||
func (c IsPlayerComponent) Type() ecs.ComponentType {
|
func (c IsPlayerComponent) Type() ecs.ComponentType {
|
||||||
return TypeIsPlayer
|
return TypeIsPlayer
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreatePlayer(world *ecs.World, id uuid.UUID, state PlayerState) (entity ecs.Entity, err error) {
|
|
||||||
entity = ecs.NewEntity()
|
|
||||||
|
|
||||||
defaultRoom, err := ecs.GetResource[ecs.Entity](world, ResourceDefaultRoom)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ecs.SetComponent(world, entity, ConnectionIdComponent{ConnectionId: id})
|
|
||||||
ecs.SetComponent(world, entity, PlayerStateComponent{State: state})
|
|
||||||
ecs.SetComponent(world, entity, NameComponent{Name: id.String()})
|
|
||||||
ecs.SetComponent(world, entity, InRoomComponent{Room: defaultRoom})
|
|
||||||
ecs.SetComponent(world, entity, IsPlayerComponent{})
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
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 {
|
func (c NeighborsComponent) Type() ecs.ComponentType {
|
||||||
return TypeNeighbors
|
return TypeNeighbors
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateRoom(
|
|
||||||
world *ecs.World,
|
|
||||||
name, description string,
|
|
||||||
north, south, east, west ecs.Entity,
|
|
||||||
) ecs.Entity {
|
|
||||||
entity := ecs.NewEntity()
|
|
||||||
|
|
||||||
ecs.SetComponent(world, entity, IsRoomComponent{})
|
|
||||||
ecs.SetComponent(world, entity, NameComponent{Name: name})
|
|
||||||
ecs.SetComponent(world, entity, DescriptionComponent{Description: description})
|
|
||||||
ecs.SetComponent(world, entity, NeighborsComponent{North: north, South: south, East: east, West: west})
|
|
||||||
|
|
||||||
return entity
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,56 +1,57 @@
|
||||||
package game
|
package game
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"code.haedhutner.dev/mvv/LastMUD/internal/game/logic/world"
|
||||||
"context"
|
"context"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
|
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
|
||||||
"code.haedhutner.dev/mvv/LastMUD/internal/game/data"
|
"code.haedhutner.dev/mvv/LastMUD/internal/game/data"
|
||||||
"code.haedhutner.dev/mvv/LastMUD/internal/game/systems"
|
"code.haedhutner.dev/mvv/LastMUD/internal/game/logic"
|
||||||
"code.haedhutner.dev/mvv/LastMUD/internal/logging"
|
"code.haedhutner.dev/mvv/LastMUD/internal/logging"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
const TickRate = time.Duration(50 * time.Millisecond)
|
const TickRate = 50 * time.Millisecond
|
||||||
|
|
||||||
type GameOutput struct {
|
type Output struct {
|
||||||
connId uuid.UUID
|
connId uuid.UUID
|
||||||
contents []byte
|
contents []byte
|
||||||
closeConnection bool
|
closeConnection bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g GameOutput) Id() uuid.UUID {
|
func (g Output) Id() uuid.UUID {
|
||||||
return g.connId
|
return g.connId
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g GameOutput) Contents() []byte {
|
func (g Output) Contents() []byte {
|
||||||
return g.contents
|
return g.contents
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g GameOutput) ShouldCloseConnection() bool {
|
func (g Output) ShouldCloseConnection() bool {
|
||||||
return g.closeConnection
|
return g.closeConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
type LastMUDGame struct {
|
type Game struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
wg *sync.WaitGroup
|
wg *sync.WaitGroup
|
||||||
|
|
||||||
world *data.GameWorld
|
world *World
|
||||||
|
|
||||||
output chan GameOutput
|
output chan Output
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateGame(ctx context.Context, wg *sync.WaitGroup) (game *LastMUDGame) {
|
func CreateGame(ctx context.Context, wg *sync.WaitGroup) (game *Game) {
|
||||||
game = &LastMUDGame{
|
game = &Game{
|
||||||
wg: wg,
|
wg: wg,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
output: make(chan GameOutput),
|
output: make(chan Output),
|
||||||
world: data.CreateGameWorld(),
|
world: CreateGameWorld(),
|
||||||
}
|
}
|
||||||
|
|
||||||
ecs.RegisterSystems(game.world.World, systems.CreateSystems()...)
|
ecs.RegisterSystems(game.world.World, logic.CreateSystems()...)
|
||||||
|
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go game.start()
|
go game.start()
|
||||||
|
@ -58,8 +59,8 @@ func CreateGame(ctx context.Context, wg *sync.WaitGroup) (game *LastMUDGame) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Will block if no output present
|
// ConsumeNextOutput will block if no output present
|
||||||
func (game *LastMUDGame) ConsumeNextOutput() *GameOutput {
|
func (game *Game) ConsumeNextOutput() *Output {
|
||||||
select {
|
select {
|
||||||
case output := <-game.output:
|
case output := <-game.output:
|
||||||
return &output
|
return &output
|
||||||
|
@ -68,19 +69,19 @@ func (game *LastMUDGame) ConsumeNextOutput() *GameOutput {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (game *LastMUDGame) ConnectPlayer(connectionId uuid.UUID) {
|
func (game *Game) ConnectPlayer(connectionId uuid.UUID) {
|
||||||
data.CreatePlayerConnectEvent(game.world.World, connectionId)
|
world.CreatePlayerConnectEvent(game.world.World, connectionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (game *LastMUDGame) DisconnectPlayer(connectionId uuid.UUID) {
|
func (game *Game) DisconnectPlayer(connectionId uuid.UUID) {
|
||||||
data.CreatePlayerDisconnectEvent(game.world.World, connectionId)
|
world.CreatePlayerDisconnectEvent(game.world.World, connectionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (game *LastMUDGame) SendPlayerCommand(connectionId uuid.UUID, command string) {
|
func (game *Game) SendPlayerCommand(connectionId uuid.UUID, command string) {
|
||||||
data.CreatePlayerCommandEvent(game.world.World, connectionId, command)
|
world.CreatePlayerCommandEvent(game.world.World, connectionId, command)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (game *LastMUDGame) start() {
|
func (game *Game) start() {
|
||||||
defer game.wg.Done()
|
defer game.wg.Done()
|
||||||
defer game.shutdown()
|
defer game.shutdown()
|
||||||
|
|
||||||
|
@ -106,11 +107,11 @@ func (game *LastMUDGame) start() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (game *LastMUDGame) consumeOutputs() {
|
func (game *Game) consumeOutputs() {
|
||||||
entities := ecs.FindEntitiesWithComponents(game.world.World, data.TypeConnectionId, data.TypeContents)
|
entities := ecs.FindEntitiesWithComponents(game.world.World, data.TypeConnectionId, data.TypeContents)
|
||||||
|
|
||||||
for _, entity := range entities {
|
for _, entity := range entities {
|
||||||
output := GameOutput{}
|
output := Output{}
|
||||||
|
|
||||||
connId, _ := ecs.GetComponent[data.ConnectionIdComponent](game.world.World, entity)
|
connId, _ := ecs.GetComponent[data.ConnectionIdComponent](game.world.World, entity)
|
||||||
output.connId = connId.ConnectionId
|
output.connId = connId.ConnectionId
|
||||||
|
@ -130,12 +131,12 @@ func (game *LastMUDGame) consumeOutputs() {
|
||||||
ecs.DeleteEntities(game.world.World, entities...)
|
ecs.DeleteEntities(game.world.World, entities...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (game *LastMUDGame) shutdown() {
|
func (game *Game) shutdown() {
|
||||||
logging.Info("Stopping LastMUD...")
|
logging.Info("Stopping LastMUD...")
|
||||||
close(game.output)
|
close(game.output)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (game *LastMUDGame) shouldStop() bool {
|
func (game *Game) shouldStop() bool {
|
||||||
select {
|
select {
|
||||||
case <-game.ctx.Done():
|
case <-game.ctx.Done():
|
||||||
return true
|
return true
|
||||||
|
@ -144,11 +145,11 @@ func (game *LastMUDGame) shouldStop() bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (game *LastMUDGame) enqeueOutput(output GameOutput) {
|
func (game *Game) enqeueOutput(output Output) {
|
||||||
game.output <- output
|
game.output <- output
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *LastMUDGame) tick(delta time.Duration) {
|
func (game *Game) tick(delta time.Duration) {
|
||||||
g.world.Tick(delta)
|
game.world.Tick(delta)
|
||||||
g.consumeOutputs()
|
game.consumeOutputs()
|
||||||
}
|
}
|
||||||
|
|
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 (
|
import (
|
||||||
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
|
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
|
||||||
"code.haedhutner.dev/mvv/LastMUD/internal/game/data"
|
"code.haedhutner.dev/mvv/LastMUD/internal/game/data"
|
||||||
|
"code.haedhutner.dev/mvv/LastMUD/internal/game/logic/world"
|
||||||
"code.haedhutner.dev/mvv/LastMUD/internal/logging"
|
"code.haedhutner.dev/mvv/LastMUD/internal/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
func handlePlayerConnectEvent(world *ecs.World, event ecs.Entity) (err error) {
|
func HandlePlayerConnect(w *ecs.World, event ecs.Entity) (err error) {
|
||||||
logging.Info("Player connect")
|
logging.Info("Player connect")
|
||||||
|
|
||||||
connectionId, ok := ecs.GetComponent[data.ConnectionIdComponent](world, event)
|
connectionId, ok := ecs.GetComponent[data.ConnectionIdComponent](w, event)
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return createEventHandlerError(data.EventPlayerConnect, "Event does not contain connectionId")
|
return createEventHandlerError(data.EventPlayerConnect, "Event does not contain connectionId")
|
||||||
}
|
}
|
||||||
|
|
||||||
data.CreatePlayer(world, connectionId.ConnectionId, data.PlayerStateJoining)
|
world.CreateJoiningPlayer(w, connectionId.ConnectionId)
|
||||||
data.CreateGameOutput(world, connectionId.ConnectionId, []byte("Welcome to LastMUD!"), false)
|
world.CreateGameOutput(w, connectionId.ConnectionId, []byte("Welcome to LastMUD!"))
|
||||||
|
world.CreateGameOutput(w, connectionId.ConnectionId, []byte("Before interacting with the game, you must either login or create a new account. Do so using the 'register' and 'login' command(s)."))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlePlayerDisconnectEvent(world *ecs.World, event ecs.Entity) (err error) {
|
func HandlePlayerDisconnect(w *ecs.World, event ecs.Entity) (err error) {
|
||||||
logging.Info("Player disconnect")
|
logging.Info("Player disconnect")
|
||||||
|
|
||||||
connectionId, ok := ecs.GetComponent[data.ConnectionIdComponent](world, event)
|
connectionId, ok := ecs.GetComponent[data.ConnectionIdComponent](w, event)
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return createEventHandlerError(data.EventPlayerDisconnect, "Event does not contain connectionId")
|
return createEventHandlerError(data.EventPlayerDisconnect, "Event does not contain connectionId")
|
||||||
}
|
}
|
||||||
|
|
||||||
playerEntity := ecs.QueryFirstEntityWithComponent(
|
playerEntity := ecs.QueryFirstEntityWithComponent(
|
||||||
world,
|
w,
|
||||||
func(c data.ConnectionIdComponent) bool { return c.ConnectionId == connectionId.ConnectionId },
|
func(c data.ConnectionIdComponent) bool { return c.ConnectionId == connectionId.ConnectionId },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -39,7 +41,7 @@ func handlePlayerDisconnectEvent(world *ecs.World, event ecs.Entity) (err error)
|
||||||
return createEventHandlerError(data.EventPlayerDisconnect, "Connection id cannot be associated with a player entity")
|
return createEventHandlerError(data.EventPlayerDisconnect, "Connection id cannot be associated with a player entity")
|
||||||
}
|
}
|
||||||
|
|
||||||
ecs.DeleteEntity(world, playerEntity)
|
ecs.DeleteEntity(w, playerEntity)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package systems
|
package event
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -9,7 +9,7 @@ import (
|
||||||
"code.haedhutner.dev/mvv/LastMUD/internal/logging"
|
"code.haedhutner.dev/mvv/LastMUD/internal/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
type EventHandler func(world *ecs.World, event ecs.Entity) (err error)
|
type Handler func(world *ecs.World, event ecs.Entity) (err error)
|
||||||
|
|
||||||
type eventError struct {
|
type eventError struct {
|
||||||
err string
|
err string
|
||||||
|
@ -31,10 +31,10 @@ func eventTypeQuery(eventType data.EventType) func(comp data.EventComponent) boo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateEventHandler(eventType data.EventType, handler EventHandler) ecs.SystemExecutor {
|
func CreateHandler(eventType data.EventType, handler Handler) ecs.SystemExecutor {
|
||||||
return func(world *ecs.World, delta time.Duration) (err error) {
|
return func(world *ecs.World, delta time.Duration) (err error) {
|
||||||
events := ecs.QueryEntitiesWithComponent(world, eventTypeQuery(eventType))
|
events := ecs.QueryEntitiesWithComponent(world, eventTypeQuery(eventType))
|
||||||
processedEvents := []ecs.Entity{}
|
var processedEvents []ecs.Entity
|
||||||
|
|
||||||
for event := range events {
|
for event := range events {
|
||||||
logging.Debug("Handling event of type ", eventType)
|
logging.Debug("Handling event of type ", eventType)
|
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 (
|
import (
|
||||||
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
|
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
|
||||||
|
"code.haedhutner.dev/mvv/LastMUD/internal/game/data"
|
||||||
|
"code.haedhutner.dev/mvv/LastMUD/internal/game/logic/world"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
type World struct {
|
||||||
ResourceDefaultRoom ecs.Resource = "world:room:default"
|
|
||||||
)
|
|
||||||
|
|
||||||
type GameWorld struct {
|
|
||||||
*ecs.World
|
*ecs.World
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateGameWorld() (gw *GameWorld) {
|
func CreateGameWorld() (gw *World) {
|
||||||
gw = &GameWorld{
|
gw = &World{
|
||||||
World: ecs.CreateWorld(),
|
World: ecs.CreateWorld(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,9 +20,9 @@ func CreateGameWorld() (gw *GameWorld) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func defineRooms(world *ecs.World) {
|
func defineRooms(w *ecs.World) {
|
||||||
forest := CreateRoom(
|
forest := world.CreateRoom(
|
||||||
world,
|
w,
|
||||||
"Forest",
|
"Forest",
|
||||||
"A dense, misty forest stretches endlessly, its towering trees whispering secrets through rustling leaves. Sunbeams filter through the canopy, dappling the mossy ground with golden light.",
|
"A dense, misty forest stretches endlessly, its towering trees whispering secrets through rustling leaves. Sunbeams filter through the canopy, dappling the mossy ground with golden light.",
|
||||||
ecs.NilEntity(),
|
ecs.NilEntity(),
|
||||||
|
@ -33,10 +31,10 @@ func defineRooms(world *ecs.World) {
|
||||||
ecs.NilEntity(),
|
ecs.NilEntity(),
|
||||||
)
|
)
|
||||||
|
|
||||||
ecs.SetResource(world, ResourceDefaultRoom, forest)
|
ecs.SetResource(w, data.ResourceDefaultRoom, forest)
|
||||||
|
|
||||||
cabin := CreateRoom(
|
cabin := world.CreateRoom(
|
||||||
world,
|
w,
|
||||||
"Wooden Cabin",
|
"Wooden Cabin",
|
||||||
"The 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.",
|
"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(),
|
ecs.NilEntity(),
|
||||||
|
@ -45,8 +43,8 @@ func defineRooms(world *ecs.World) {
|
||||||
ecs.NilEntity(),
|
ecs.NilEntity(),
|
||||||
)
|
)
|
||||||
|
|
||||||
lake := CreateRoom(
|
lake := world.CreateRoom(
|
||||||
world,
|
w,
|
||||||
"Ethermere Lake",
|
"Ethermere Lake",
|
||||||
"Ethermire Lake lies shrouded in mist, its dark, still waters reflecting a sky perpetually overcast. Whispers ride the wind, and strange lights flicker beneath the surface, never breaking it.",
|
"Ethermire Lake lies shrouded in mist, its dark, still waters reflecting a sky perpetually overcast. Whispers ride the wind, and strange lights flicker beneath the surface, never breaking it.",
|
||||||
ecs.NilEntity(),
|
ecs.NilEntity(),
|
||||||
|
@ -55,8 +53,8 @@ func defineRooms(world *ecs.World) {
|
||||||
ecs.NilEntity(),
|
ecs.NilEntity(),
|
||||||
)
|
)
|
||||||
|
|
||||||
graveyard := CreateRoom(
|
graveyard := world.CreateRoom(
|
||||||
world,
|
w,
|
||||||
"Graveyard",
|
"Graveyard",
|
||||||
"An overgrown graveyard shrouded in fog, with cracked headstones and leaning statues. The wind sighs through dead trees, and unseen footsteps echo faintly among the mossy graves.",
|
"An overgrown graveyard shrouded in fog, with cracked headstones and leaning statues. The wind sighs through dead trees, and unseen footsteps echo faintly among the mossy graves.",
|
||||||
ecs.NilEntity(),
|
ecs.NilEntity(),
|
||||||
|
@ -65,8 +63,8 @@ func defineRooms(world *ecs.World) {
|
||||||
ecs.NilEntity(),
|
ecs.NilEntity(),
|
||||||
)
|
)
|
||||||
|
|
||||||
chapel := CreateRoom(
|
chapel := world.CreateRoom(
|
||||||
world,
|
w,
|
||||||
"Chapel of the Hollow Light",
|
"Chapel of the Hollow Light",
|
||||||
"This ruined chapel leans under ivy and age. Faint light filters through shattered stained glass, casting broken rainbows across dust-choked pews and a long-silent altar.",
|
"This ruined chapel leans under ivy and age. Faint light filters through shattered stained glass, casting broken rainbows across dust-choked pews and a long-silent altar.",
|
||||||
ecs.NilEntity(),
|
ecs.NilEntity(),
|
||||||
|
@ -75,32 +73,32 @@ func defineRooms(world *ecs.World) {
|
||||||
ecs.NilEntity(),
|
ecs.NilEntity(),
|
||||||
)
|
)
|
||||||
|
|
||||||
ecs.SetComponent(world, forest, NeighborsComponent{
|
ecs.SetComponent(w, forest, data.NeighborsComponent{
|
||||||
North: cabin,
|
North: cabin,
|
||||||
South: graveyard,
|
South: graveyard,
|
||||||
East: lake,
|
East: lake,
|
||||||
West: chapel,
|
West: chapel,
|
||||||
})
|
})
|
||||||
|
|
||||||
ecs.SetComponent(world, cabin, NeighborsComponent{
|
ecs.SetComponent(w, cabin, data.NeighborsComponent{
|
||||||
South: graveyard,
|
South: graveyard,
|
||||||
West: chapel,
|
West: chapel,
|
||||||
East: lake,
|
East: lake,
|
||||||
})
|
})
|
||||||
|
|
||||||
ecs.SetComponent(world, chapel, NeighborsComponent{
|
ecs.SetComponent(w, chapel, data.NeighborsComponent{
|
||||||
North: cabin,
|
North: cabin,
|
||||||
South: graveyard,
|
South: graveyard,
|
||||||
East: forest,
|
East: forest,
|
||||||
})
|
})
|
||||||
|
|
||||||
ecs.SetComponent(world, lake, NeighborsComponent{
|
ecs.SetComponent(w, lake, data.NeighborsComponent{
|
||||||
West: forest,
|
West: forest,
|
||||||
North: cabin,
|
North: cabin,
|
||||||
South: graveyard,
|
South: graveyard,
|
||||||
})
|
})
|
||||||
|
|
||||||
ecs.SetComponent(world, graveyard, NeighborsComponent{
|
ecs.SetComponent(w, graveyard, data.NeighborsComponent{
|
||||||
North: forest,
|
North: forest,
|
||||||
West: chapel,
|
West: chapel,
|
||||||
East: lake,
|
East: lake,
|
|
@ -27,6 +27,8 @@ type Connection struct {
|
||||||
|
|
||||||
conn *net.TCPConn
|
conn *net.TCPConn
|
||||||
lastSeen time.Time
|
lastSeen time.Time
|
||||||
|
|
||||||
|
closeChan chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateConnection(server *Server, conn *net.TCPConn, ctx context.Context, wg *sync.WaitGroup) (c *Connection) {
|
func CreateConnection(server *Server, conn *net.TCPConn, ctx context.Context, wg *sync.WaitGroup) (c *Connection) {
|
||||||
|
@ -36,12 +38,13 @@ func CreateConnection(server *Server, conn *net.TCPConn, ctx context.Context, wg
|
||||||
conn.SetKeepAlivePeriod(1 * time.Second)
|
conn.SetKeepAlivePeriod(1 * time.Second)
|
||||||
|
|
||||||
c = &Connection{
|
c = &Connection{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
wg: wg,
|
wg: wg,
|
||||||
server: server,
|
server: server,
|
||||||
identity: uuid.New(),
|
identity: uuid.New(),
|
||||||
conn: conn,
|
conn: conn,
|
||||||
lastSeen: time.Now(),
|
lastSeen: time.Now(),
|
||||||
|
closeChan: make(chan struct{}, 1),
|
||||||
}
|
}
|
||||||
|
|
||||||
c.wg.Add(2)
|
c.wg.Add(2)
|
||||||
|
@ -115,8 +118,19 @@ func (c *Connection) shouldClose() bool {
|
||||||
case <-c.ctx.Done():
|
case <-c.ctx.Done():
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-c.closeChan:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Connection) CommandClose() {
|
||||||
|
c.closeChan <- struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Connection) closeConnection() {
|
func (c *Connection) closeConnection() {
|
||||||
|
|
|
@ -19,7 +19,7 @@ type Server struct {
|
||||||
|
|
||||||
connections map[uuid.UUID]*Connection
|
connections map[uuid.UUID]*Connection
|
||||||
|
|
||||||
lastmudgame *game.LastMUDGame
|
lastmudgame *game.Game
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateServer(ctx context.Context, wg *sync.WaitGroup, port string) (srv *Server, err error) {
|
func CreateServer(ctx context.Context, wg *sync.WaitGroup, port string) (srv *Server, err error) {
|
||||||
|
@ -63,7 +63,7 @@ func CreateServer(ctx context.Context, wg *sync.WaitGroup, port string) (srv *Se
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *Server) game() *game.LastMUDGame {
|
func (srv *Server) game() *game.Game {
|
||||||
return srv.lastmudgame
|
return srv.lastmudgame
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,7 +118,7 @@ func (srv *Server) consumeGameOutput() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if output.ShouldCloseConnection() {
|
if output.ShouldCloseConnection() {
|
||||||
conn.closeConnection()
|
conn.CommandClose()
|
||||||
delete(srv.connections, output.Id())
|
delete(srv.connections, output.Id())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue