No longer blocks randomly
This commit is contained in:
parent
2f926d5457
commit
45abc33c7f
10 changed files with 345 additions and 83 deletions
47
internal/game/event.go
Normal file
47
internal/game/event.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package game
|
||||
|
||||
import "time"
|
||||
|
||||
type EventType int
|
||||
|
||||
const (
|
||||
PlayerJoin EventType = iota
|
||||
PlayerCommand
|
||||
PlayerLeave
|
||||
)
|
||||
|
||||
type GameEvent interface {
|
||||
Type() EventType
|
||||
Handle(game *LastMUDGame, delta time.Duration)
|
||||
}
|
||||
|
||||
type EventBus struct {
|
||||
events chan GameEvent
|
||||
}
|
||||
|
||||
func CreateEventBus() *EventBus {
|
||||
return &EventBus{
|
||||
events: make(chan GameEvent, 10),
|
||||
}
|
||||
}
|
||||
|
||||
func (eb *EventBus) HasNext() bool {
|
||||
return len(eb.events) > 0
|
||||
}
|
||||
|
||||
func (eb *EventBus) Pop() (event GameEvent) {
|
||||
select {
|
||||
case event := <-eb.events:
|
||||
return event
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (eb *EventBus) Push(event GameEvent) {
|
||||
eb.events <- event
|
||||
}
|
||||
|
||||
func (eb *EventBus) close() {
|
||||
close(eb.events)
|
||||
}
|
44
internal/game/events.go
Normal file
44
internal/game/events.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
package game
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type PlayerJoinEvent struct {
|
||||
connectionId uuid.UUID
|
||||
}
|
||||
|
||||
func CreatePlayerJoinEvent(connId uuid.UUID) *PlayerJoinEvent {
|
||||
return &PlayerJoinEvent{
|
||||
connectionId: connId,
|
||||
}
|
||||
}
|
||||
|
||||
func (pje *PlayerJoinEvent) Type() EventType {
|
||||
return PlayerJoin
|
||||
}
|
||||
|
||||
func (pje *PlayerJoinEvent) Handle(game *LastMUDGame, delta time.Duration) {
|
||||
game.world.AddPlayerToDefaultRoom(CreatePlayer(pje.connectionId, nil))
|
||||
game.enqeueOutput(CreateOutput(pje.connectionId, []byte("Welcome to LastMUD\n")))
|
||||
}
|
||||
|
||||
type PlayerLeaveEvent struct {
|
||||
connectionId uuid.UUID
|
||||
}
|
||||
|
||||
func CreatePlayerLeaveEvent(connId uuid.UUID) *PlayerLeaveEvent {
|
||||
return &PlayerLeaveEvent{
|
||||
connectionId: connId,
|
||||
}
|
||||
}
|
||||
|
||||
func (ple *PlayerLeaveEvent) Type() EventType {
|
||||
return PlayerJoin
|
||||
}
|
||||
|
||||
func (ple *PlayerLeaveEvent) Handle(game *LastMUDGame, delta time.Duration) {
|
||||
game.world.RemovePlayerById(ple.connectionId.String())
|
||||
}
|
|
@ -6,22 +6,49 @@ import (
|
|||
"time"
|
||||
|
||||
"code.haedhutner.dev/mvv/LastMUD/internal/logging"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const TickRate = time.Duration(50 * time.Millisecond)
|
||||
|
||||
type GameSignal struct {
|
||||
type GameOutput struct {
|
||||
connId uuid.UUID
|
||||
contents []byte
|
||||
}
|
||||
|
||||
func CreateOutput(connId uuid.UUID, contents []byte) GameOutput {
|
||||
return GameOutput{
|
||||
connId: connId,
|
||||
contents: contents,
|
||||
}
|
||||
}
|
||||
|
||||
func (g GameOutput) Id() uuid.UUID {
|
||||
return g.connId
|
||||
}
|
||||
|
||||
func (g GameOutput) Contents() []byte {
|
||||
return g.contents
|
||||
}
|
||||
|
||||
type LastMUDGame struct {
|
||||
ctx context.Context
|
||||
wg *sync.WaitGroup
|
||||
|
||||
world *World
|
||||
|
||||
eventBus *EventBus
|
||||
|
||||
output chan GameOutput
|
||||
}
|
||||
|
||||
func CreateGame(ctx context.Context, wg *sync.WaitGroup) (game *LastMUDGame) {
|
||||
game = &LastMUDGame{
|
||||
wg: wg,
|
||||
ctx: ctx,
|
||||
eventBus: CreateEventBus(),
|
||||
output: make(chan GameOutput, 10),
|
||||
world: CreateWorld(),
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
|
@ -58,6 +85,8 @@ func (game *LastMUDGame) start() {
|
|||
|
||||
func (game *LastMUDGame) shutdown() {
|
||||
logging.Info("Stopping LastMUD...")
|
||||
close(game.output)
|
||||
game.eventBus.close()
|
||||
}
|
||||
|
||||
func (game *LastMUDGame) shouldStop() bool {
|
||||
|
@ -69,7 +98,31 @@ func (game *LastMUDGame) shouldStop() bool {
|
|||
}
|
||||
}
|
||||
|
||||
func (g *LastMUDGame) tick(delta time.Duration) {
|
||||
// logging.Debug("Tick")
|
||||
// TODO
|
||||
func (game *LastMUDGame) EnqueueEvent(event GameEvent) {
|
||||
game.eventBus.Push(event)
|
||||
}
|
||||
|
||||
func (game *LastMUDGame) enqeueOutput(output GameOutput) {
|
||||
game.output <- output
|
||||
}
|
||||
|
||||
func (game *LastMUDGame) ConsumeNextOutput() *GameOutput {
|
||||
select {
|
||||
case output := <-game.output:
|
||||
return &output
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (g *LastMUDGame) tick(delta time.Duration) {
|
||||
for {
|
||||
event := g.eventBus.Pop()
|
||||
|
||||
if event == nil {
|
||||
return
|
||||
}
|
||||
|
||||
event.Handle(g, delta)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,24 @@
|
|||
package game
|
||||
|
||||
import "github.com/google/uuid"
|
||||
|
||||
type Player struct {
|
||||
GameObject
|
||||
Name
|
||||
Description
|
||||
Position
|
||||
Velocity
|
||||
id uuid.UUID
|
||||
|
||||
currentRoom *Room
|
||||
}
|
||||
|
||||
func CreatePlayer(name, description string, x, y int) *Player {
|
||||
func CreatePlayer(identity uuid.UUID, room *Room) *Player {
|
||||
return &Player{
|
||||
GameObject: CreateGameObject(),
|
||||
Name: WithName(name),
|
||||
Description: WithDescription(description),
|
||||
Position: WithPosition(x, y),
|
||||
Velocity: WithVelocity(0, 0),
|
||||
id: identity,
|
||||
currentRoom: room,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Player) Identity() string {
|
||||
return p.id.String()
|
||||
}
|
||||
|
||||
func (p *Player) SetRoom(r *Room) {
|
||||
p.currentRoom = r
|
||||
}
|
||||
|
|
42
internal/game/room.go
Normal file
42
internal/game/room.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
package game
|
||||
|
||||
type RoomPlayer interface {
|
||||
Identity() string
|
||||
SetRoom(room *Room)
|
||||
}
|
||||
|
||||
type Room struct {
|
||||
North *Room
|
||||
South *Room
|
||||
East *Room
|
||||
West *Room
|
||||
|
||||
Name string
|
||||
Description string
|
||||
|
||||
players map[string]RoomPlayer
|
||||
}
|
||||
|
||||
func CreateRoom(name, description string) *Room {
|
||||
return &Room{
|
||||
Name: name,
|
||||
Description: description,
|
||||
players: map[string]RoomPlayer{},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Room) PlayerJoinRoom(player RoomPlayer) (err error) {
|
||||
r.players[player.Identity()] = player
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Room) PlayerLeaveRoom(player RoomPlayer) (err error) {
|
||||
delete(r.players, player.Identity())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *Room) Players() map[string]RoomPlayer {
|
||||
return r.players
|
||||
}
|
|
@ -1,45 +1 @@
|
|||
package game
|
||||
|
||||
import "github.com/google/uuid"
|
||||
|
||||
type GameObject struct {
|
||||
uuid uuid.UUID
|
||||
}
|
||||
|
||||
func CreateGameObject() GameObject {
|
||||
return GameObject{
|
||||
uuid: uuid.New(),
|
||||
}
|
||||
}
|
||||
|
||||
type Position struct {
|
||||
x, y int
|
||||
}
|
||||
|
||||
func WithPosition(x, y int) Position {
|
||||
return Position{x, y}
|
||||
}
|
||||
|
||||
type Velocity struct {
|
||||
velX, velY int
|
||||
}
|
||||
|
||||
func WithVelocity(velX, velY int) Velocity {
|
||||
return Velocity{velX, velY}
|
||||
}
|
||||
|
||||
type Name struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func WithName(name string) Name {
|
||||
return Name{name}
|
||||
}
|
||||
|
||||
type Description struct {
|
||||
description string
|
||||
}
|
||||
|
||||
func WithDescription(description string) Description {
|
||||
return Description{description}
|
||||
}
|
||||
|
|
64
internal/game/world.go
Normal file
64
internal/game/world.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
package game
|
||||
|
||||
type World struct {
|
||||
rooms []*Room
|
||||
players map[string]*Player
|
||||
defaultRoom *Room
|
||||
}
|
||||
|
||||
func CreateWorld() *World {
|
||||
forest := CreateRoom("Forest", "A dense, misty forest stretches endlessly, its towering trees whispering secrets through rustling leaves. Sunbeams filter through the canopy, dappling the mossy ground with golden light.")
|
||||
cabin := CreateRoom("Wooden Cabin", "The cabin’s interior is cozy and rustic, with wooden beams overhead and a stone fireplace crackling warmly. A wool rug lies on creaky floorboards, and shelves brim with books, mugs, and old lanterns.")
|
||||
lake := CreateRoom("Ethermere Lake", "Ethermire Lake lies shrouded in mist, its dark, still waters reflecting a sky perpetually overcast. Whispers ride the wind, and strange lights flicker beneath the surface, never breaking it.")
|
||||
graveyard := CreateRoom("Graveyard", "An overgrown graveyard shrouded in fog, with cracked headstones and leaning statues. The wind sighs through dead trees, and unseen footsteps echo faintly among the mossy graves.")
|
||||
chapel := CreateRoom("Chapel of the Hollow Light", "This ruined chapel leans under ivy and age. Faint light filters through shattered stained glass, casting broken rainbows across dust-choked pews and a long-silent altar.")
|
||||
|
||||
forest.North = cabin
|
||||
forest.South = graveyard
|
||||
forest.East = lake
|
||||
forest.West = chapel
|
||||
|
||||
cabin.South = forest
|
||||
cabin.West = chapel
|
||||
cabin.East = lake
|
||||
|
||||
chapel.North = cabin
|
||||
chapel.South = graveyard
|
||||
chapel.East = forest
|
||||
|
||||
lake.West = forest
|
||||
lake.North = cabin
|
||||
lake.South = graveyard
|
||||
|
||||
graveyard.North = forest
|
||||
graveyard.West = chapel
|
||||
graveyard.East = lake
|
||||
|
||||
return &World{
|
||||
rooms: []*Room{
|
||||
forest,
|
||||
cabin,
|
||||
lake,
|
||||
graveyard,
|
||||
chapel,
|
||||
},
|
||||
defaultRoom: forest,
|
||||
players: map[string]*Player{},
|
||||
}
|
||||
}
|
||||
|
||||
func (w *World) AddPlayerToDefaultRoom(p *Player) {
|
||||
w.players[p.Identity()] = p
|
||||
w.defaultRoom.PlayerJoinRoom(p)
|
||||
p.SetRoom(w.defaultRoom)
|
||||
}
|
||||
|
||||
func (w *World) RemovePlayerById(id string) {
|
||||
p, ok := w.players[id]
|
||||
|
||||
if ok {
|
||||
p.currentRoom.PlayerLeaveRoom(p)
|
||||
delete(w.players, id)
|
||||
return
|
||||
}
|
||||
}
|
|
@ -79,10 +79,16 @@ func CreateLogger(maxFileLevel LogLevel, maxDisplayedLevel LogLevel, filePath st
|
|||
|
||||
logFilePath := fmt.Sprintf("%s-%s%s", base, timestamp, ext) // "./base/dir/log-2006-01-02_15-04-05.txt"
|
||||
|
||||
mkdirErr := os.MkdirAll(filepath.Dir(logFilePath), 0755)
|
||||
|
||||
if mkdirErr != nil {
|
||||
err(os.Stdout, false, timestampFormat, mkdirErr)
|
||||
}
|
||||
|
||||
file, fileErr := os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
|
||||
if fileErr != nil {
|
||||
err(os.Stdout, false, "Logging: Unable to write to file", filePath, fileErr)
|
||||
err(os.Stdout, false, timestampFormat, fileErr)
|
||||
} else {
|
||||
logger.file = file
|
||||
}
|
||||
|
|
|
@ -7,25 +7,29 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"code.haedhutner.dev/mvv/LastMUD/internal/game"
|
||||
"code.haedhutner.dev/mvv/LastMUD/internal/logging"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const MaxLastSeenTime = 120 * time.Second
|
||||
const MaxEnqueuedInputMessages = 10
|
||||
|
||||
type Connection struct {
|
||||
ctx context.Context
|
||||
wg *sync.WaitGroup
|
||||
|
||||
server *Server
|
||||
|
||||
identity uuid.UUID
|
||||
|
||||
conn *net.TCPConn
|
||||
lastSeen time.Time
|
||||
|
||||
inputChannel chan string
|
||||
inputChannel chan []byte
|
||||
}
|
||||
|
||||
func CreateConnection(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) {
|
||||
logging.Info("Connect: ", conn.RemoteAddr())
|
||||
|
||||
conn.SetKeepAlive(true)
|
||||
|
@ -34,9 +38,10 @@ func CreateConnection(conn *net.TCPConn, ctx context.Context, wg *sync.WaitGroup
|
|||
c = &Connection{
|
||||
ctx: ctx,
|
||||
wg: wg,
|
||||
server: server,
|
||||
identity: uuid.New(),
|
||||
conn: conn,
|
||||
inputChannel: make(chan string),
|
||||
inputChannel: make(chan []byte, MaxEnqueuedInputMessages),
|
||||
lastSeen: time.Now(),
|
||||
}
|
||||
|
||||
|
@ -44,9 +49,15 @@ func CreateConnection(conn *net.TCPConn, ctx context.Context, wg *sync.WaitGroup
|
|||
go c.listen()
|
||||
go c.checkAlive()
|
||||
|
||||
server.game.EnqueueEvent(game.CreatePlayerJoinEvent(c.Id()))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Connection) Id() uuid.UUID {
|
||||
return c.identity
|
||||
}
|
||||
|
||||
func (c *Connection) listen() {
|
||||
defer c.wg.Done()
|
||||
|
||||
|
@ -55,13 +66,18 @@ func (c *Connection) listen() {
|
|||
for {
|
||||
c.conn.SetReadDeadline(time.Time{})
|
||||
|
||||
message, err := bufio.NewReader(c.conn).ReadString('\n')
|
||||
message, err := bufio.NewReader(c.conn).ReadBytes('\n')
|
||||
|
||||
if err != nil {
|
||||
logging.Warn(err)
|
||||
break
|
||||
}
|
||||
|
||||
if len(c.inputChannel) == MaxEnqueuedInputMessages {
|
||||
c.conn.Write([]byte("You have too many commands enqueued. Please wait until some are processed.\n"))
|
||||
continue
|
||||
}
|
||||
|
||||
c.inputChannel <- message
|
||||
|
||||
c.conn.Write([]byte(message))
|
||||
|
@ -76,12 +92,12 @@ func (c *Connection) checkAlive() {
|
|||
|
||||
for {
|
||||
if c.shouldClose() {
|
||||
c.Write("Server shutting down, bye bye!\r\n")
|
||||
c.Write([]byte("Server shutting down, bye bye!\r\n"))
|
||||
break
|
||||
}
|
||||
|
||||
if time.Since(c.lastSeen) > MaxLastSeenTime {
|
||||
c.Write("You have been away for too long, bye bye!\r\n")
|
||||
c.Write([]byte("You have been away for too long, bye bye!\r\n"))
|
||||
break
|
||||
}
|
||||
|
||||
|
@ -103,21 +119,25 @@ func (c *Connection) shouldClose() bool {
|
|||
}
|
||||
|
||||
func (c *Connection) closeConnection() {
|
||||
close(c.inputChannel)
|
||||
|
||||
c.conn.Close()
|
||||
|
||||
c.server.game.EnqueueEvent(game.CreatePlayerLeaveEvent(c.Id()))
|
||||
|
||||
logging.Info("Disconnected: ", c.conn.RemoteAddr())
|
||||
}
|
||||
|
||||
func (c *Connection) NextInput() (input string, err error) {
|
||||
func (c *Connection) NextInput() (input []byte, err error) {
|
||||
select {
|
||||
case val := <-c.inputChannel:
|
||||
return val, nil
|
||||
default:
|
||||
return "", newInputEmptyError()
|
||||
return nil, newInputEmptyError()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Connection) Write(output string) (err error) {
|
||||
_, err = c.conn.Write([]byte(output))
|
||||
func (c *Connection) Write(output []byte) (err error) {
|
||||
_, err = c.conn.Write(output)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
"code.haedhutner.dev/mvv/LastMUD/internal/game"
|
||||
"code.haedhutner.dev/mvv/LastMUD/internal/logging"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
|
@ -16,19 +17,19 @@ type Server struct {
|
|||
|
||||
listener *net.TCPListener
|
||||
|
||||
connections []*Connection
|
||||
connections map[uuid.UUID]*Connection
|
||||
|
||||
game *game.LastMUDGame
|
||||
}
|
||||
|
||||
func CreateServer(ctx context.Context, wg *sync.WaitGroup, port string) (srv *Server, err error) {
|
||||
|
||||
logging.Info(" _ _ __ __ _ _ ____ ")
|
||||
logging.Info(" | | __ _ ___| |_| \\/ | | | | _ \\ ")
|
||||
logging.Info(" | | / _` / __| __| |\\/| | | | | | | | ")
|
||||
logging.Info(" | |__| (_| \\__ \\ |_| | | | |_| | |_| | ")
|
||||
logging.Info(" |_____\\__,_|___/\\__|_| |_|\\___/|____/ ")
|
||||
logging.Info(" ")
|
||||
logging.Info(" _ _ __ __ _ _ ____")
|
||||
logging.Info("| | __ _ ___| |_| \\/ | | | | _ \\")
|
||||
logging.Info("| | / _` / __| __| |\\/| | | | | | | |")
|
||||
logging.Info("| |__| (_| \\__ \\ |_| | | | |_| | |_| |")
|
||||
logging.Info("|_____\\__,_|___/\\__|_| |_|\\___/|____/")
|
||||
logging.Info("")
|
||||
|
||||
addr, err := net.ResolveTCPAddr("tcp", port)
|
||||
|
||||
|
@ -50,13 +51,14 @@ func CreateServer(ctx context.Context, wg *sync.WaitGroup, port string) (srv *Se
|
|||
ctx: ctx,
|
||||
wg: wg,
|
||||
listener: ln,
|
||||
connections: []*Connection{},
|
||||
connections: map[uuid.UUID]*Connection{},
|
||||
}
|
||||
|
||||
srv.game = game.CreateGame(ctx, srv.wg)
|
||||
|
||||
srv.wg.Add(1)
|
||||
srv.wg.Add(2)
|
||||
go srv.listen()
|
||||
go srv.consumeGameOutput()
|
||||
|
||||
return
|
||||
}
|
||||
|
@ -85,8 +87,31 @@ func (srv *Server) listen() {
|
|||
continue
|
||||
}
|
||||
|
||||
c := CreateConnection(tcpConn, srv.ctx, srv.wg)
|
||||
srv.connections = append(srv.connections, c)
|
||||
c := CreateConnection(srv, tcpConn, srv.ctx, srv.wg)
|
||||
|
||||
srv.connections[c.Id()] = c
|
||||
}
|
||||
}
|
||||
|
||||
func (srv *Server) consumeGameOutput() {
|
||||
defer srv.wg.Done()
|
||||
|
||||
for {
|
||||
if srv.shouldStop() {
|
||||
break
|
||||
}
|
||||
|
||||
output := srv.game.ConsumeNextOutput()
|
||||
|
||||
if output == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
conn, ok := srv.connections[output.Id()]
|
||||
|
||||
if ok {
|
||||
conn.Write(output.Contents())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue