diff --git a/golib/buildclient.sh b/golib/buildclient.sh index 273c077..c4f210c 100755 --- a/golib/buildclient.sh +++ b/golib/buildclient.sh @@ -10,6 +10,6 @@ popd pushd $GOPATH/src/github.com/jerold/Catan/golib/client/bindings gopherjs build -m -du -lah bindings.js +du -h bindings.js mv bindings.js ../../../lib/src popd diff --git a/golib/catannet/cn_test.go b/golib/catannet/cn_test.go index 898804b..0a272ef 100644 --- a/golib/catannet/cn_test.go +++ b/golib/catannet/cn_test.go @@ -13,6 +13,7 @@ func TestPerformance(t *testing.T) { } for i := 0; i < 100; i++ { + b.Pieces[i] = &PieceLocation{} b.Pieces[i].Location = &Coordinate{ X: 1054, Y: 4538, @@ -23,6 +24,7 @@ func TestPerformance(t *testing.T) { } } for i := 0; i < 50; i++ { + b.Tiles[i] = &Tile{} b.Tiles[i].Location = &Coordinate{ X: 5354, Y: 58459, diff --git a/golib/catannet/messages.ng b/golib/catannet/messages.ng index 4e0b615..32cbf56 100644 --- a/golib/catannet/messages.ng +++ b/golib/catannet/messages.ng @@ -9,35 +9,77 @@ struct Heartbeat { } struct SaveGameRequest { - ID int32 // Optional ID + ID GameID // Optional ID + Revision int32 Board *GameBoard Players []*Player } struct SaveGameResponse { - ID int32 + ID GameID } struct LoadGameRequest { - ID int32 + ID GameID } struct LoadGameResponse { - ID int32 + ID GameID Board *GameBoard Players []*Player } +// Listen to a game ID for changes. +struct JoinGame { + ID int32 // ID of game to listen to + PlayerID int32 // Player to take over. -1 means server will assign you next available. + Name string // Name of yourself +} + +// Event is sent to all listeners +struct BoardEvent { + ID GameID + Ops []*BoardOperation +} + +struct PlayerEvent { + ID GameID +} + // Game State Structs +// Board Operation could be a change to tile, edge, or plot +struct BoardOperation { + OpType OperationType + Value dynamic +} + +enum OperationType { + Add = 1 + Remove = 2 + Update = 3 +} + +struct GameID { + ID int32 + Revision int32 +} struct GameBoard { - Pieces []*PieceLocation + Edges []*EdgePiece + Plots []*PlotPiece Tiles []*Tile + Thief *Coordinate } -struct PieceLocation { - Piece *GamePiece - Location *Coordinate +struct EdgePiece { + Piece *GamePiece + Start *Coordinate + End *Coordinate +} + +struct PlotPiece { + Piece *GamePiece + Location *Coordinate } struct Coordinate { @@ -49,6 +91,7 @@ struct Tile { Location *Coordinate Type TileType // Land or Water Product Commodity // Commodity Type + Value int16 // For land this is the roll. For water this is port facing. } struct GamePiece { diff --git a/golib/client/bindings/clientjs.go b/golib/client/bindings/clientjs.go index f8728ea..1c35410 100644 --- a/golib/client/bindings/clientjs.go +++ b/golib/client/bindings/clientjs.go @@ -39,15 +39,22 @@ func (c *ClientJS) SaveGame(jso *js.Object) { c.Outgoing <- catannet.NewPacket(sg) } +func (c *ClientJS) JoinGame(jso *js.Object) { + sg := catannetjs.JoinGameFromJS(jso) + c.Outgoing <- catannet.NewPacket(sg) +} + type ClientEvents struct { - onLoadGame LoadCallback - onSaveGame SaveCallback - onConnect FieldlessCallback + onLoadGame LoadCallback + onSaveGame SaveCallback + onConnect FieldlessCallback + onNotifyGame GameChangeCallback } type FieldlessCallback func() type LoadCallback func(*catannet.LoadGameResponse) type SaveCallback func(*catannet.SaveGameResponse) +type GameChangeCallback func(*catannet.BoardEvent) func (ce *ClientEvents) OnSaveGame(cb SaveCallback) { ce.onSaveGame = cb @@ -61,6 +68,10 @@ func (ce *ClientEvents) OnConnect(cb FieldlessCallback) { ce.onConnect = cb } +func (ce *ClientEvents) OnListenEvent(cb GameChangeCallback) { + ce.onNotifyGame = cb +} + func (c *ClientJS) Dial(url string) { go func() { if url == "" { @@ -70,8 +81,8 @@ func (c *ClientJS) Dial(url string) { if c.events.onConnect != nil { c.events.onConnect() } + go runClient(c) }) - go runClient(c) }() } @@ -92,6 +103,10 @@ func runClient(c *ClientJS) { if c.events.onLoadGame != nil { c.events.onLoadGame(packet.NetMsg.(*catannet.LoadGameResponse)) } + case catannet.BoardEventMsgType: + if (c.events.onNotifyGame) != nil { + c.events.onNotifyGame(packet.NetMsg.(*catannet.BoardEvent)) + } } } print("runClient exiting.") diff --git a/golib/server/main.go b/golib/server/main.go index 7781650..8103078 100644 --- a/golib/server/main.go +++ b/golib/server/main.go @@ -5,6 +5,7 @@ import ( "net/http" "os" "os/signal" + "sync" "github.com/jerold/Catan/golib/catannet" "github.com/jerold/Catan/golib/client" @@ -19,9 +20,17 @@ func main() { func runServer() { os.Mkdir("storage", 0777) fmt.Printf("Starting server now.\n") + ss := &ServerState{ + sl: &ServerListeners{ + listeners: map[int32][]*client.Client{}, + m: &sync.Mutex{}, + }, + gamerevs: map[int32]int32{}, + gm: &sync.Mutex{}, + } http.Handle("/ws", websocket.Handler(func(conn *websocket.Conn) { c := createClient(conn) - runClient(&c) + runClient(&c, ss) })) go func() { @@ -70,7 +79,7 @@ func createClient(conn *websocket.Conn) client.Client { } } -func runClient(c *client.Client) { +func runClient(c *client.Client, ss *ServerState) { go client.Sender(c) go client.Reader(c) @@ -85,15 +94,26 @@ func runClient(c *client.Client) { case *catannet.SaveGameRequest: fmt.Printf(" %s: Requests game to be saved.\n", c.Name) go func(sg *catannet.SaveGameRequest) { - resp, err := SaveGame(sg) + resp, err := SaveGame(sg, ss) if err != nil { fmt.Printf("Failed to save game: %s\n", err) // TODO: HANDLE ERR HERE. - resp.ID = -1 + resp.ID.ID = -1 } else { fmt.Printf(" %s: Saved game under id: %d\n", c.Name, resp.ID) } c.Outgoing <- catannet.NewPacket(resp) + update := catannet.ListenEvent{ + ID: resp.ID, + Board: tmsg.Board, + Players: tmsg.Players, + } + eventPacket := catannet.NewPacket(update) + ss.sl.m.Lock() + for _, li := range ss.sl.listeners[update.ID.ID] { + li.Outgoing <- eventPacket + } + ss.sl.m.Unlock() }(tmsg) case *catannet.LoadGameRequest: fmt.Printf(" %s: Requests game %d to be loaded.\n", c.Name, tmsg.ID) @@ -105,6 +125,10 @@ func runClient(c *client.Client) { } c.Outgoing <- catannet.NewPacket(lgr) }(tmsg) + case *catannet.ListenSubscribe: + ss.sl.m.Lock() + ss.sl.listeners[tmsg.ID] = append(ss.sl.listeners[tmsg.ID], c) + ss.sl.m.Unlock() } } diff --git a/golib/server/server b/golib/server/server index 8601048..07c1785 100755 Binary files a/golib/server/server and b/golib/server/server differ diff --git a/golib/server/storage.go b/golib/server/storage.go index c4e5737..7267a21 100644 --- a/golib/server/storage.go +++ b/golib/server/storage.go @@ -1,35 +1,94 @@ package main import ( + "fmt" "io/ioutil" + "os" + "path/filepath" "strconv" + "strings" + "sync" "sync/atomic" "github.com/jerold/Catan/golib/catannet" + "github.com/jerold/Catan/golib/client" ) -var lastID int32 +type ServerState struct { + sl *ServerListeners + lastID int32 + gamerevs map[int32]int32 + gm *sync.Mutex +} + +// Listener subscriptions +// TODO: Turn this into a full live game instead of just a listener. +type ServerListeners struct { + listeners map[int32][]*client.Client + m *sync.Mutex +} -func SaveGame(game *catannet.SaveGameRequest) (catannet.SaveGameResponse, error) { - game.ID = atomic.AddInt32(&lastID, 1) +func SaveGame(game *catannet.SaveGameRequest, ss *ServerState) (catannet.SaveGameResponse, error) { + if game.ID.ID == 0 { + game.ID.ID = atomic.AddInt32(&ss.lastID, 1) + } + if game.ID.Revision == 0 { + ss.gm.Lock() + if _, ok := ss.gamerevs[game.ID.ID]; !ok { + ss.gm.Unlock() + r := getSaveRev(game.ID) + ss.gm.Lock() + // If this is a new save, it will be -1 + ss.gamerevs[game.ID.ID] = r.Revision + } + ss.gamerevs[game.ID.ID]++ + game.ID.Revision = ss.gamerevs[game.ID.ID] + ss.gm.Unlock() + } data := make([]byte, game.Len()) game.Serialize(data) - // fmt.Printf("game board\n", game.Board.Pieces) - // for _, p := range game.Board.Pieces { - // fmt.Printf(" Piece: %d @ (%d,%d)\n", p.Piece.Type, p.Location.X, p.Location.Y) - // } err := ioutil.WriteFile(saveFile(game.ID), data, 0655) return catannet.SaveGameResponse{ ID: game.ID, }, err } -func saveFile(id int32) string { - return "storage/" + strconv.Itoa(int(id)) + ".save" +func saveFile(id catannet.GameID) string { + return "storage/" + strconv.Itoa(int(id.ID)) + "." + strconv.Itoa(int(id.Revision)) + ".save" } -func LoadGame(id int32) (catannet.LoadGameResponse, error) { - data, err := ioutil.ReadFile(saveFile(id)) +func getSaveRev(id catannet.GameID) catannet.GameID { + maxrev := -1 + err := filepath.Walk("storage/", func(path string, _ os.FileInfo, _ error) error { + ext := filepath.Ext(path) + if ext != "save" { + return nil + } + name := filepath.Base(path) + fnp := strings.Split(name, ".") + rev, err := strconv.Atoi(fnp[1]) + if err != nil { + return nil + } + if rev > maxrev { + maxrev = rev + } + return nil + }) + if err != nil { + fmt.Printf("Failed to find revision: %s.\n", err) + // Not sure what to do here + return catannet.GameID{ID: -1, Revision: -1} + } + return catannet.GameID{ID: id.ID, Revision: int32(maxrev)} +} + +func LoadGame(id catannet.GameID) (catannet.LoadGameResponse, error) { + fn := saveFile(id) + if id.Revision <= 0 { + fn = saveFile(getSaveRev(id)) + } + data, err := ioutil.ReadFile(fn) lgr := catannet.LoadGameResponse{ ID: id, Board: &catannet.GameBoard{}, diff --git a/initgo.sh b/initgo.sh index e0382e6..a950c71 100755 --- a/initgo.sh +++ b/initgo.sh @@ -1,8 +1,8 @@ #!/bin/bash +# Get go deps +go get -u golang.org/x/net/websocket + ./golib/install_tools.sh ./golib/buildclient.sh ./golib/buildserver.sh - -# Get go deps -go get -u golang.org/x/net/websocket diff --git a/lib/catannet.dart b/lib/catannet.dart index fef487a..0911a43 100644 --- a/lib/catannet.dart +++ b/lib/catannet.dart @@ -2,19 +2,77 @@ library catannet; import "package:js/js.dart"; +import "dart:async"; + part "src/catannet/catannet.dart"; + +enum EventType { + Connected, + Disconnected, + LoadGame, + SaveGame, + ListenEvent +} + +class NetEvent { + EventType type; + Object state; + + NetEvent({this.type, this.state}); +} + +class DartClient { + Client _client; + Stream events; + StreamController _eventController; + + LoadGame(LoadGameRequest r) { + _client.LoadGame(r); + } + + SaveGame(SaveGameRequest r) { + _client.SaveGame(r); + } + + JoinGame(JoinGame r) { + _client.JoinGame(r); + } + + DartClient(String url) { + _eventController = new StreamController(); + events = _eventController.stream; + this._client = NewClient(); + var _jsevents = this._client.Events(); + _jsevents.OnConnect(allowInterop(this._onConnect)); + _jsevents.OnSaveGame(allowInterop((msg) => _onMessage(msg, EventType.SaveGame))); + _jsevents.OnLoadGame(allowInterop((msg) => _onMessage(msg, EventType.LoadGame))); + _jsevents.OnListenEvent(allowInterop((msg) => _onMessage(msg, EventType.ListenEvent))); + + this._client.Dial(url); + } + + _onConnect() { + _eventController.add(new NetEvent(type: EventType.Connected, state: true)); + } + + _onMessage(dynamic msg, EventType et) { + _eventController.add(new NetEvent(type:et, state: msg)); + } +} + @JS() external Client NewClient(); - typedef OnSaveFunction(SaveGameResponse sgr); typedef OnLoadFunction(LoadGameResponse lgr); +typedef OnEventFunction(ListenEvent li); @JS() class Client { external void LoadGame(LoadGameRequest lg); external void SaveGame(SaveGameRequest sg); + external void JoinGame(JoinGame ls); external void Dial(String url); @@ -26,4 +84,5 @@ class ClientEvents { external void OnSaveGame(OnSaveFunction handle); external void OnLoadGame(OnLoadFunction handle); external void OnConnect(Function handle); + external void OnListenEvent(OnEventFunction handle); } diff --git a/lib/src/game_module/store.dart b/lib/src/game_module/store.dart index 9576a73..51ee94e 100644 --- a/lib/src/game_module/store.dart +++ b/lib/src/game_module/store.dart @@ -52,15 +52,25 @@ class GameStore extends w_flux.Store { DimmerType _currentDimmer = DimmerType.None; DimmerType get currentDimmer => _currentDimmer; - cnet.Client netclient; + cnet.DartClient netclient; + cnet.GameID gid; GameStore(this._actions) { + netclient = new cnet.DartClient(""); // default connects to localhost + netclient.events.listen(_handleNetEvent); + gid = new cnet.GameID(ID: 0); + String mapParam = Uri.base.queryParameters['map']; List tileStrings = _splitMapParam(mapParam); - if (tileStrings.length > 0) + String gidParam = Uri.base.queryParameters['gid']; + if (tileStrings.length > 0) { _startNewGameFromURI(tileStrings); - else + } else if (gidParam != null && gidParam.length > 0) { + _startGameFromID(gidParam); + _startNewGame(); + } else { _startNewGame(); + } triggerOnAction(_actions.setInteractionPoint, _setInteractionPoint); @@ -72,39 +82,78 @@ class GameStore extends w_flux.Store { triggerOnAction(_actions.hideDimmer, _hideDimmer); _board.listen(_pushBoardToURI); + } - netclient = cnet.NewClient(); - var netevents = netclient.Events(); - netevents.OnConnect(allowInterop((){ - print("connected!"); - - // Example of creating a game and saving it. - var players = new List(0); - var pieces = new List(10); - var tiles = new List(10); - for (var i = 0; i < 10; i++) { - pieces[i] = new cnet.PieceLocation( - Piece: new cnet.GamePiece(Owner: i%2, Type: cnet.PieceType.Road), - Location: new cnet.Coordinate(X:i, Y:i%5) - ); - } - for (var i = 0; i < 10; i++) { - tiles[i] = new cnet.Tile(Location: new cnet.Coordinate(X: i, Y: i%3), Type: cnet.TileType.LandTile, Product: i%6); + _saveGame() { + var players = new List(0); + var pieces = new List(); + this.board.edges.forEach((k, edge) { + // if (edge is Boat) { + // return; + // } + Road r = edge; + // TODO: get coords from r.ends() ? + pieces.add(new cnet.PieceLocation( + // TODO: get a real player ID + Piece: new cnet.GamePiece(Owner: r.owner._colorIndex, Type: cnet.PieceType.Road), + Location: new cnet.Coordinate(X: 0, Y: 0))); + }); + this.board.plots.forEach((k, plot) { + int t = cnet.PieceType.Settlement; + if (plot is City) { + t = cnet.PieceType.City; + } + // TODO: get coords from r.ends() ? + pieces.add(new cnet.PieceLocation( + // TODO: get a real player ID + Piece: new cnet.GamePiece(Owner: 0, Type: t), + Location: new cnet.Coordinate(X: 0, Y: 0))); + }); + var tiles = new List(); + this.board.tiles.forEach((k, v) { + tiles.add(new cnet.Tile( + // TODO: get real coords + Location: new cnet.Coordinate(X: 0, Y: 0), + // TODO: make sure these are all land tiles? + Type: cnet.TileType.LandTile, + // TODO: get product + Product: 0)); + }); + var gb = new cnet.GameBoard(Pieces: pieces, Tiles: tiles); + cnet.SaveGameRequest r = new cnet.SaveGameRequest(ID: gid, Board: gb, Players: players); + netclient.SaveGame(r); + } + + _handleNetEvent(cnet.NetEvent event) { + switch (event.type) { + case cnet.EventType.Connected: + print("connected to server."); + break; + case cnet.EventType.Disconnected: + break; + case cnet.EventType.SaveGame: + cnet.SaveGameResponse sgr = event.state; + print("game saved: " + sgr.ID.ID.toString() + " Revision: " + sgr.ID.Revision.toString()); + if (sgr.ID.Revision == 0) { + netclient.SubscribeGame(new cnet.ListenSubscribe(ID: sgr.ID.ID)); + // sub on first save only. } - var gb = new cnet.GameBoard(Pieces: pieces, Tiles: tiles); - cnet.SaveGameRequest r = new cnet.SaveGameRequest(Board: gb, Players: players); - netclient.SaveGame(r); - } )); - netevents.OnSaveGame(allowInterop((cnet.SaveGameResponse sgr) { - print("game saved: " + sgr.ID.toString()); - cnet.LoadGameRequest r = new cnet.LoadGameRequest(ID: sgr.ID); - netclient.LoadGame(r); - })); - netevents.OnLoadGame(allowInterop((cnet.LoadGameResponse lgr) { - print("game loaded."); - print(lgr); - })); - netclient.Dial(""); //defaults to localhost + break; + case cnet.EventType.LoadGame: + cnet.LoadGameResponse lgr = event.state; + print("game loaded: ID: " + lgr.ID.ID.toString() + " Revision: " + lgr.ID.Revision.toString()); + gid = lgr.ID; + // TODO: load the board + // sub after we load a game. + break; + case cnet.EventType.ListenEvent: + cnet.ListenEvent li = event.state; + print("notification of board change for board: " + + li.ID.ID.toString() + + " Revision: " + + li.ID.Revision.toString()); + break; + } } _startNewGame([_]) { @@ -115,6 +164,13 @@ class GameStore extends w_flux.Store { _pushBoardToURI(); } + _startGameFromID(String gidStr) { + int id = int.parse(gidStr); + gid.ID = id; + netclient.SubscribeGame(new cnet.ListenSubscribe(ID: id)); + netclient.LoadGame(new cnet.LoadGameRequest(ID: gid)); + } + _startNewGameFromURI(List tileStrings) { List keys = new List(); List types = new List(); @@ -138,16 +194,17 @@ class GameStore extends w_flux.Store { '${tile.key.toString().padLeft(4, "0")}${tile.roll.toString().padLeft(2, "0")}${stringFromTerrain(tile.terrain)}'); } if (tile is Port) { - mapParam.add( - '${tile.key.toString().padLeft(4, "0")}-${tile.facingIndex + 1}${stringFromTerrain(tile.terrain)}'); + mapParam + .add('${tile.key.toString().padLeft(4, "0")}-${tile.facingIndex + 1}${stringFromTerrain(tile.terrain)}'); } }); Uri current = Uri.base; - Map params = - new Map.from(current.queryParameters); + Map params = new Map.from(current.queryParameters); params['map'] = mapParam.join(''); current = current.replace(queryParameters: params); window.history.pushState('', '', current.toString()); + + _saveGame(); } List _splitMapParam(String mapParam) {