2025-06-20 16:26:39 +03:00
|
|
|
package game
|
|
|
|
|
|
|
|
import (
|
2025-06-28 11:24:06 +03:00
|
|
|
"code.haedhutner.dev/mvv/LastMUD/internal/game/logic/world"
|
2025-06-20 16:26:39 +03:00
|
|
|
"context"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
2025-06-26 23:48:54 +03:00
|
|
|
"code.haedhutner.dev/mvv/LastMUD/internal/ecs"
|
|
|
|
"code.haedhutner.dev/mvv/LastMUD/internal/game/data"
|
2025-06-28 11:24:06 +03:00
|
|
|
"code.haedhutner.dev/mvv/LastMUD/internal/game/logic"
|
2025-06-20 16:26:39 +03:00
|
|
|
"code.haedhutner.dev/mvv/LastMUD/internal/logging"
|
2025-06-27 21:36:15 +03:00
|
|
|
|
2025-06-22 17:54:07 +03:00
|
|
|
"github.com/google/uuid"
|
2025-06-20 16:26:39 +03:00
|
|
|
)
|
|
|
|
|
2025-06-28 11:24:06 +03:00
|
|
|
const TickRate = 50 * time.Millisecond
|
2025-06-20 16:26:39 +03:00
|
|
|
|
2025-06-29 18:21:22 +03:00
|
|
|
type InputType = string
|
|
|
|
|
|
|
|
const (
|
|
|
|
Connect InputType = "Connect"
|
|
|
|
Disconnect = "Disconnect"
|
|
|
|
Command = "Command"
|
|
|
|
)
|
|
|
|
|
|
|
|
type Input struct {
|
|
|
|
connId uuid.UUID
|
|
|
|
inputType InputType
|
|
|
|
command string
|
|
|
|
}
|
|
|
|
|
2025-06-28 11:24:06 +03:00
|
|
|
type Output struct {
|
2025-06-27 21:36:15 +03:00
|
|
|
connId uuid.UUID
|
|
|
|
contents []byte
|
|
|
|
closeConnection bool
|
2025-06-22 17:54:07 +03:00
|
|
|
}
|
|
|
|
|
2025-06-28 11:24:06 +03:00
|
|
|
func (g Output) Id() uuid.UUID {
|
2025-06-22 17:54:07 +03:00
|
|
|
return g.connId
|
|
|
|
}
|
|
|
|
|
2025-06-28 11:24:06 +03:00
|
|
|
func (g Output) Contents() []byte {
|
2025-06-22 17:54:07 +03:00
|
|
|
return g.contents
|
2025-06-20 16:26:39 +03:00
|
|
|
}
|
|
|
|
|
2025-06-28 11:24:06 +03:00
|
|
|
func (g Output) ShouldCloseConnection() bool {
|
2025-06-27 21:36:15 +03:00
|
|
|
return g.closeConnection
|
|
|
|
}
|
|
|
|
|
2025-06-28 11:24:06 +03:00
|
|
|
type Game struct {
|
2025-06-20 16:26:39 +03:00
|
|
|
ctx context.Context
|
|
|
|
wg *sync.WaitGroup
|
2025-06-22 17:54:07 +03:00
|
|
|
|
2025-06-28 11:24:06 +03:00
|
|
|
world *World
|
2025-06-22 17:54:07 +03:00
|
|
|
|
2025-06-29 18:21:22 +03:00
|
|
|
input chan Input
|
2025-06-28 11:24:06 +03:00
|
|
|
output chan Output
|
2025-06-29 18:21:22 +03:00
|
|
|
|
|
|
|
stop context.CancelFunc
|
2025-06-20 16:26:39 +03:00
|
|
|
}
|
|
|
|
|
2025-06-28 11:24:06 +03:00
|
|
|
func CreateGame(ctx context.Context, wg *sync.WaitGroup) (game *Game) {
|
2025-06-29 18:21:22 +03:00
|
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
|
|
|
2025-06-28 11:24:06 +03:00
|
|
|
game = &Game{
|
2025-06-26 23:48:54 +03:00
|
|
|
wg: wg,
|
|
|
|
ctx: ctx,
|
2025-06-29 18:21:22 +03:00
|
|
|
input: make(chan Input, 1000),
|
|
|
|
output: make(chan Output, 1000),
|
2025-06-28 11:24:06 +03:00
|
|
|
world: CreateGameWorld(),
|
2025-06-29 18:21:22 +03:00
|
|
|
stop: cancel,
|
2025-06-20 16:26:39 +03:00
|
|
|
}
|
|
|
|
|
2025-06-28 11:24:06 +03:00
|
|
|
ecs.RegisterSystems(game.world.World, logic.CreateSystems()...)
|
2025-06-22 22:27:56 +03:00
|
|
|
|
2025-06-20 16:26:39 +03:00
|
|
|
wg.Add(1)
|
|
|
|
go game.start()
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2025-06-28 11:24:06 +03:00
|
|
|
// ConsumeNextOutput will block if no output present
|
|
|
|
func (game *Game) ConsumeNextOutput() *Output {
|
2025-06-29 18:21:22 +03:00
|
|
|
if game.shouldStop() {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2025-06-24 16:37:26 +03:00
|
|
|
select {
|
|
|
|
case output := <-game.output:
|
|
|
|
return &output
|
|
|
|
default:
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-06-29 18:21:22 +03:00
|
|
|
func (game *Game) Connect(connectionId uuid.UUID) {
|
|
|
|
if game.shouldStop() {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
game.input <- Input{
|
|
|
|
inputType: Connect,
|
|
|
|
connId: connectionId,
|
|
|
|
}
|
2025-06-26 23:48:54 +03:00
|
|
|
}
|
|
|
|
|
2025-06-29 18:21:22 +03:00
|
|
|
func (game *Game) Disconnect(connectionId uuid.UUID) {
|
|
|
|
if game.shouldStop() {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
game.input <- Input{
|
|
|
|
inputType: Disconnect,
|
|
|
|
connId: connectionId,
|
|
|
|
}
|
2025-06-26 23:48:54 +03:00
|
|
|
}
|
|
|
|
|
2025-06-29 18:21:22 +03:00
|
|
|
func (game *Game) SendCommand(connectionId uuid.UUID, cmd string) {
|
|
|
|
if game.shouldStop() {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
game.input <- Input{
|
|
|
|
inputType: Command,
|
|
|
|
connId: connectionId,
|
|
|
|
command: cmd,
|
|
|
|
}
|
2025-06-26 23:48:54 +03:00
|
|
|
}
|
|
|
|
|
2025-06-28 11:24:06 +03:00
|
|
|
func (game *Game) start() {
|
2025-06-20 16:26:39 +03:00
|
|
|
defer game.wg.Done()
|
|
|
|
defer game.shutdown()
|
|
|
|
|
|
|
|
logging.Info("Starting LastMUD...")
|
|
|
|
|
|
|
|
lastTick := time.Now()
|
|
|
|
|
|
|
|
for {
|
|
|
|
now := time.Now()
|
|
|
|
|
|
|
|
if game.shouldStop() {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
game.tick(now.Sub(lastTick))
|
|
|
|
|
|
|
|
// Tick at regular intervals
|
|
|
|
if time.Since(lastTick) < TickRate {
|
|
|
|
time.Sleep(TickRate - time.Since(lastTick))
|
|
|
|
}
|
|
|
|
|
|
|
|
lastTick = now
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-06-28 11:24:06 +03:00
|
|
|
func (game *Game) consumeOutputs() {
|
2025-06-29 18:21:22 +03:00
|
|
|
entities := ecs.FindEntitiesWithComponents(game.world.World, data.TypeIsOutput, data.TypeConnectionId, data.TypeContents)
|
2025-06-26 23:48:54 +03:00
|
|
|
|
|
|
|
for _, entity := range entities {
|
2025-06-28 11:24:06 +03:00
|
|
|
output := Output{}
|
2025-06-27 21:36:15 +03:00
|
|
|
|
2025-06-26 23:48:54 +03:00
|
|
|
connId, _ := ecs.GetComponent[data.ConnectionIdComponent](game.world.World, entity)
|
2025-06-27 21:36:15 +03:00
|
|
|
output.connId = connId.ConnectionId
|
|
|
|
|
|
|
|
contents, hasContents := ecs.GetComponent[data.ContentsComponent](game.world.World, entity)
|
|
|
|
|
|
|
|
if hasContents {
|
|
|
|
output.contents = contents.Contents
|
|
|
|
}
|
|
|
|
|
|
|
|
_, shouldClose := ecs.GetComponent[data.CloseConnectionComponent](game.world.World, entity)
|
|
|
|
output.closeConnection = shouldClose
|
2025-06-26 23:48:54 +03:00
|
|
|
|
2025-06-27 21:36:15 +03:00
|
|
|
game.enqeueOutput(output)
|
2025-06-26 23:48:54 +03:00
|
|
|
}
|
2025-06-27 09:45:55 +03:00
|
|
|
|
|
|
|
ecs.DeleteEntities(game.world.World, entities...)
|
2025-06-26 23:48:54 +03:00
|
|
|
}
|
|
|
|
|
2025-06-28 11:24:06 +03:00
|
|
|
func (game *Game) shutdown() {
|
2025-06-20 16:26:39 +03:00
|
|
|
logging.Info("Stopping LastMUD...")
|
2025-06-22 17:54:07 +03:00
|
|
|
close(game.output)
|
2025-06-29 18:21:22 +03:00
|
|
|
close(game.input)
|
2025-06-20 16:26:39 +03:00
|
|
|
}
|
|
|
|
|
2025-06-28 11:24:06 +03:00
|
|
|
func (game *Game) shouldStop() bool {
|
2025-06-20 16:26:39 +03:00
|
|
|
select {
|
|
|
|
case <-game.ctx.Done():
|
|
|
|
return true
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-06-29 18:21:22 +03:00
|
|
|
func (game *Game) nextInput() *Input {
|
|
|
|
select {
|
|
|
|
case input := <-game.input:
|
|
|
|
return &input
|
|
|
|
default:
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-06-28 11:24:06 +03:00
|
|
|
func (game *Game) enqeueOutput(output Output) {
|
2025-06-22 17:54:07 +03:00
|
|
|
game.output <- output
|
|
|
|
}
|
|
|
|
|
2025-06-28 11:24:06 +03:00
|
|
|
func (game *Game) tick(delta time.Duration) {
|
2025-06-29 18:21:22 +03:00
|
|
|
for {
|
|
|
|
input := game.nextInput()
|
|
|
|
|
|
|
|
if input == nil {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
switch input.inputType {
|
|
|
|
case Connect:
|
|
|
|
world.CreatePlayerConnectEvent(game.world.World, input.connId)
|
|
|
|
case Disconnect:
|
|
|
|
world.CreatePlayerDisconnectEvent(game.world.World, input.connId)
|
|
|
|
case Command:
|
|
|
|
world.CreateSubmitInputEvent(game.world.World, input.connId, input.command)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-06-28 11:24:06 +03:00
|
|
|
game.world.Tick(delta)
|
2025-06-29 18:21:22 +03:00
|
|
|
|
2025-06-28 11:24:06 +03:00
|
|
|
game.consumeOutputs()
|
2025-06-20 16:26:39 +03:00
|
|
|
}
|