You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

321 lines
6.1 KiB
Go

package sim
import (
"fmt"
"time"
"github.com/gorilla/websocket"
"github.com/jordanorelli/astro-domu/internal/math"
"github.com/jordanorelli/astro-domu/internal/wire"
"github.com/jordanorelli/blammo"
)
// world is the entire simulated world. A world consists of many rooms.
type world struct {
*blammo.Log
inbox chan Request
connect chan connect
nextID chan int
done chan bool
lastEntityID int
rooms map[string]*room
players map[string]*player
}
type connect struct {
conn *websocket.Conn
login wire.Login
failed chan error
}
func newWorld(log *blammo.Log) *world {
bounds := math.CreateRect(10, 10)
foyer := room{
Log: log.Child("foyer"),
name: "foyer",
Rect: bounds,
tiles: make([]tile, bounds.Area()),
players: make(map[string]*player),
}
foyer.addEntity(&entity{
ID: -1,
Position: math.Vec{5, 4},
Glyph: 'o',
solid: true,
pickupable: true,
name: "a rock",
behavior: doNothing{},
})
foyer.addEntity(&entity{
ID: -2,
Position: math.Vec{5, 5},
Glyph: 'o',
solid: true,
pickupable: true,
name: "another rock",
behavior: doNothing{},
})
foyer.addEntity(&entity{
ID: -3,
Position: math.Vec{5, 6},
Glyph: 'o',
solid: true,
pickupable: true,
name: "yet another rock (YAR)",
behavior: doNothing{},
})
foyer.addEntity(&entity{
ID: -4,
Position: math.Vec{9, 5},
Glyph: '◇',
name: "Door to Hall",
behavior: &door{
Log: log.Child("door"),
to: "hall",
exit: -5,
},
})
hall := room{
Log: log.Child("hall"),
name: "hall",
Rect: math.CreateRect(20, 4),
tiles: make([]tile, 80),
players: make(map[string]*player),
}
hall.addEntity(&entity{
ID: -5,
Position: math.Vec{0, 2},
Glyph: '◇',
name: "Door to Foyer",
behavior: &door{
Log: log.Child("door"),
to: "foyer",
exit: -4,
},
})
kitchen := room{
Log: log.Child("kitchen"),
name: "kitchen",
Rect: math.CreateRect(6, 12),
tiles: make([]tile, 72),
players: make(map[string]*player),
}
foyer.addEntity(&entity{
ID: -6,
Position: math.Vec{9, 7},
Glyph: '◇',
name: "Door to Kitchen",
behavior: &door{
Log: log.Child("door"),
to: "kitchen",
exit: -7,
},
})
kitchen.addEntity(&entity{
ID: -7,
Position: math.Vec{0, 2},
Glyph: '◇',
name: "Door to Foyer",
behavior: &door{
Log: log.Child("door"),
to: "foyer",
exit: -6,
},
})
log.Info("created foyer with bounds: %#v having width: %d height: %d area: %d", foyer.Rect, foyer.Width, foyer.Height, foyer.Area())
return &world{
Log: log,
rooms: map[string]*room{
"foyer": &foyer,
"hall": &hall,
"kitchen": &kitchen,
},
done: make(chan bool),
inbox: make(chan Request),
connect: make(chan connect),
players: make(map[string]*player),
nextID: make(chan int),
}
}
func (w *world) run(hz int) {
defer w.Info("simulation has exited run loop")
go func() {
lastID := 1
for {
select {
case <-w.done:
return
case w.nextID <- lastID:
lastID++
}
}
}()
period := time.Second / time.Duration(hz)
w.Info("starting world with a tick rate of %dhz, frame duration of %v", hz, period)
ticker := time.NewTicker(period)
lastTick := time.Now()
for {
select {
case c := <-w.connect:
w.register(c)
w.Info("finished registration for: %v", c)
case req := <-w.inbox:
w.Info("read request off of inbox: %v", req)
w.handleRequest(req)
w.Info("finished handling request: %v", req)
case <-ticker.C:
w.tick(time.Since(lastTick))
lastTick = time.Now()
case <-w.done:
for _, p := range w.players {
p.stop <- true
}
return
}
}
}
func (w *world) handleRequest(req Request) {
w.Info("read from inbox: %v", req)
if req.From == "" {
w.Error("request has no from: %v", req)
return
}
p, ok := w.players[req.From]
if !ok {
w.Error("received non login request of type %T from unknown player %q", req.Wants, req.From)
return
}
if p.pending == nil {
p.pending = &req
} else {
p.outbox <- wire.ErrorResponse(req.Seq, "you already have a request for this frame")
}
}
func (w *world) register(c connect) {
w.Info("register: %#v", c.login)
r := w.rooms["foyer"]
if hall, ok := w.rooms["hall"]; ok {
if len(hall.players) < len(r.players) {
r = hall
}
}
if len(r.players) >= 100 {
c.failed <- fmt.Errorf("room is full")
close(c.failed)
return
}
p := player{
Log: w.Log.Child("players").Child(c.login.Name),
name: c.login.Name,
outbox: make(chan wire.Response, 8),
pending: &Request{
From: c.login.Name,
Wants: &spawnPlayer{},
},
}
p.avatar = &entity{
ID: <-w.nextID,
Glyph: '@',
solid: true,
behavior: &p,
}
r.players[c.login.Name] = &p
w.players[c.login.Name] = &p
w.Info("starting player...")
p.start(w.inbox, c.conn, r)
}
func (w *world) stop() error {
w.Info("stopping simulation")
w.done <- true
w.Info("simulation stopped")
return nil
}
func (w *world) tick(d time.Duration) {
// run all player effects
for _, r := range w.rooms {
for _, p := range r.players {
if p.pending == nil {
continue
}
req := p.pending
p.pending = nil
res := req.Wants.exec(w, r, p, req.Seq)
if res.reply != nil {
p.send(wire.Response{Re: req.Seq, Body: res.reply})
} else {
p.send(wire.Response{Re: req.Seq, Body: wire.OK{}})
}
}
}
// run all object updates
for _, r := range w.rooms {
for _, t := range r.tiles {
t.update(d)
}
}
// check all overlapping entities
for _, r := range w.rooms {
for _, t := range r.tiles {
t.overlaps()
}
}
// send frame data to all players
for _, r := range w.rooms {
frame := wire.Frame{
RoomName: r.name,
RoomSize: r.Rect,
Entities: r.allEntities(),
Players: r.playerAvatars(),
}
delta := r.lastFrame.Diff(frame)
if delta != nil {
w.Info("%s delta: %s", r.name, delta)
}
r.lastFrame = frame
for _, p := range r.players {
switch {
case p.fullSync:
p.send(wire.Response{Body: frame})
p.fullSync = false
case delta != nil:
p.send(wire.Response{Body: delta})
}
}
}
}