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.

229 lines
5.2 KiB
Go

package sim
import (
"encoding/json"
"fmt"
"time"
"github.com/gorilla/websocket"
"github.com/jordanorelli/astro-domu/internal/math"
"github.com/jordanorelli/astro-domu/internal/wire"
"github.com/jordanorelli/blammo"
)
type player struct {
*blammo.Log
name string
outbox chan wire.Response
pending *Request
avatar *entity
stop chan bool
fullSync bool
}
func (p *player) start(c chan Request, conn *websocket.Conn, r *room) {
welcome := wire.Welcome{
Rooms: make(map[string]wire.Room),
Players: make(map[string]wire.Player),
}
ents := make(map[int]wire.Entity)
for id, e := range r.allEntities() {
ents[id] = wire.Entity{
ID: id,
Position: e.Position,
Glyph: e.Glyph,
}
}
welcome.Rooms[r.name] = wire.Room{
Name: r.name,
Rect: r.Rect,
Entities: ents,
}
for _, p := range r.players {
wp := wire.Player{
Name: p.name,
Room: r.name,
}
if p.avatar != nil {
wp.Avatar = p.avatar.ID
}
welcome.Players[p.name] = wp
}
p.Info("sending welcome to outbox")
p.send(wire.Response{Re: 1, Body: welcome})
p.Info("sent welcome, starting loops")
p.stop = make(chan bool, 1)
go p.readLoop(c, conn)
go p.runLoop(conn)
}
func (p *player) readLoop(c chan Request, conn *websocket.Conn) {
p.Info("readLoop started")
defer p.Info("readLoop ended")
for {
_, b, err := conn.ReadMessage()
if err != nil {
p.Error("read error: %v", err)
conn.Close()
c <- Request{
From: p.name,
Wants: effect(func(w *world, r *room, p *player, seq int) result {
r.removePlayer(p.name)
return result{}
}),
}
p.stop <- false
return
}
p.Log.Child("received-frame").Info(string(b))
var req wire.Request
if err := json.Unmarshal(b, &req); err != nil {
p.Error("unable to parse request: %v", err)
continue
}
effect, ok := req.Body.(Effect)
if !ok {
p.Error("request is not an effect, is %T", req.Body)
continue
}
c <- Request{
From: p.name,
Seq: req.Seq,
Wants: effect,
}
}
}
func (p *player) runLoop(conn *websocket.Conn) {
p.Info("runLoop started")
defer p.Info("runLoop ended")
for {
select {
case res := <-p.outbox:
if err, ok := res.Body.(error); ok {
p.Error("sending error: %s", err)
}
if err := sendResponse(conn, res); err != nil {
p.Error(err.Error())
}
case sendCloseFrame := <-p.stop:
if sendCloseFrame {
p.Info("sending close frame")
msg := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")
if err := conn.WriteMessage(websocket.CloseMessage, msg); err != nil {
p.Error("failed to write close message: %v", err)
} else {
p.Info("sent close frame")
}
}
return
}
}
}
func (p *player) send(res wire.Response) bool {
select {
case p.outbox <- res:
return true
default:
select {
case <-p.outbox:
return p.send(res)
default:
return false
}
}
}
func sendResponse(conn *websocket.Conn, res wire.Response) error {
payload, err := json.Marshal(res)
if err != nil {
return fmt.Errorf("failed to marshal outgoing response: %w", err)
}
if err := conn.SetWriteDeadline(time.Now().Add(3 * time.Second)); err != nil {
return fmt.Errorf("failed to set write deadline: %w", err)
}
w, err := conn.NextWriter(websocket.TextMessage)
if err != nil {
return fmt.Errorf("failed get a writer frame: %w", err)
}
if _, err := w.Write(payload); err != nil {
return fmt.Errorf("failed write payload: %w", err)
}
if err := w.Close(); err != nil {
return fmt.Errorf("failed to close write frame: %w", err)
}
return nil
}
func (p *player) update(dt time.Duration) {}
type spawnPlayer struct{}
func (s spawnPlayer) exec(w *world, r *room, p *player, seq int) result {
p.fullSync = true
e := entity{
ID: <-w.nextID,
Glyph: '@',
solid: true,
behavior: p,
}
p.avatar = &e
for n, _ := range r.tiles {
t := &r.tiles[n]
x, y := n%r.Width, n/r.Width
e.Position = math.Vec{x, y}
if t.addEntity(&e) {
p.Info("player added to tile at %s", e.Position)
return result{}
}
}
return result{}
}
type Move math.Vec
func (Move) NetTag() string { return "move" }
func (m *Move) exec(w *world, r *room, p *player, seq int) result {
pos := p.avatar.Position
target := pos.Add(math.Vec(*m))
p.Info("running move for player %s from %v to %v", p.name, p.avatar.Position, target)
if !r.Contains(target) {
p.Error("target cell (%d, %d) is out of bounds", target.X, target.Y)
return result{reply: wire.Errorf("target cell (%d, %d) is out of bounds", target.X, target.Y)}
}
currentTile := r.getTile(pos)
if !currentTile.hasEntity(p.avatar.ID) {
p.Error("player cannot move off of %s because they were not actually there", pos)
p.Error("tile %d: %v", pos, currentTile)
return result{reply: wire.Errorf("player cannot move off of %s because they were not actually there", pos)}
}
nextTile := r.getTile(target)
if nextTile.isOccupied() {
p.Error("target cell (%d, %d) is occupied", target.X, target.Y)
return result{reply: wire.Errorf("target cell (%d, %d) is occupied", target.X, target.Y)}
}
currentTile.removeEntity(p.avatar.ID)
nextTile.addEntity(p.avatar)
p.avatar.Position = target
return result{reply: wire.OK{}}
}
var lastEntityID = 0
func init() {
wire.Register(func() wire.Value { return new(Move) })
}