├── README.md ├── backend.go ├── constants.go ├── errors.go ├── events.go ├── example ├── game.go ├── inputs.go └── main.go ├── ggpo_go_logo.png ├── go.mod ├── go.sum ├── internal ├── buffer │ ├── ringbuffer.go │ ├── ringbuffer_test.go │ ├── staticbuffer.go │ └── staticbuffer_test.go ├── input │ ├── game_input.go │ ├── game_input_test.go │ ├── input_queue.go │ └── input_queue_test.go ├── messages │ ├── message.go │ └── message_test.go ├── mocks │ ├── connection.go │ ├── game.go │ ├── message_handler.go │ └── session.go ├── polling │ ├── poll.go │ └── poll_test.go ├── protocol │ ├── protocol.go │ └── protocol_test.go ├── sync │ ├── timesync.go │ └── timesync_test.go └── util │ ├── ordered_map.go │ ├── ordered_map_test.go │ └── utils.go ├── logger.go ├── peer.go ├── peer_test.go ├── player.go ├── session.go ├── spectator.go ├── spectator_test.go ├── sync.go ├── sync_test.go ├── synctest.go ├── synctest_test.go └── transport ├── connection.go ├── message_handler.go └── udp.go /README.md: -------------------------------------------------------------------------------- 1 | # [![GGPO-Go LOGO](./ggpo_go_logo.png)](https://github.com/assemblaj/ggpo) 2 | # GGPO-Go - GGPO Port Into Go 3 | GGPO-Go is a port of the GGPO rollback netcode library into Go. Currently unfinished. 4 | 5 | ## Usage 6 | General usage would be best explained by looking at the code in the example folder. 7 | 8 | But to view the example, follow the following steps: 9 | 10 | - Clone the repository. 11 | - Travel to the example folder 12 | - enter: 13 | `go run . ` 14 | 15 | An example usage would be to open one command line input with the following command 16 | `go run . 7000 2 local 127.0.0.1:7001 1` 17 | and another with this command 18 | `go run . 7001 2 127.0.0.1:7000 local 2` 19 | 20 | -------------------------------------------------------------------------------- /backend.go: -------------------------------------------------------------------------------- 1 | package ggpo 2 | 3 | import ( 4 | "github.com/assemblaj/ggpo/internal/polling" 5 | "github.com/assemblaj/ggpo/internal/protocol" 6 | "github.com/assemblaj/ggpo/transport" 7 | ) 8 | 9 | /* 10 | Remember to 11 | */ 12 | type Backend interface { 13 | Idle(timeout int, timeFunc ...polling.FuncTimeType) error 14 | AddPlayer(player *Player, handle *PlayerHandle) error 15 | AddLocalInput(player PlayerHandle, values []byte, size int) error 16 | SyncInput(disconnectFlags *int) ([][]byte, error) 17 | AdvanceFrame(checksum uint32) error 18 | DisconnectPlayer(handle PlayerHandle) error 19 | GetNetworkStats(handle PlayerHandle) (protocol.NetworkStats, error) 20 | SetFrameDelay(player PlayerHandle, delay int) error 21 | SetDisconnectTimeout(timeout int) error 22 | SetDisconnectNotifyStart(timeout int) error 23 | Close() error 24 | Start() 25 | InitializeConnection(c ...transport.Connection) error 26 | } 27 | -------------------------------------------------------------------------------- /constants.go: -------------------------------------------------------------------------------- 1 | package ggpo 2 | 3 | const ( 4 | MaxPlayers = 4 5 | MaxPredictionFrames = 8 6 | MaxSpectators = 32 7 | SpectatorInputInterval = 4 8 | ) 9 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package ggpo 2 | 3 | import "fmt" 4 | 5 | type Error struct { 6 | Code ErrorCode 7 | Name string 8 | } 9 | 10 | func (e Error) Error() string { 11 | return fmt.Sprintf("ggpo: %s:%d", e.Name, e.Code) 12 | } 13 | 14 | type ErrorCode int 15 | 16 | const ( 17 | Ok ErrorCode = 0 18 | ErrorCodeSuccess ErrorCode = 0 19 | ErrorCodeGeneralFailure ErrorCode = -1 20 | ErrorCodeInvalidSession ErrorCode = 1 21 | ErrorCodeInvalidPlayerHandle ErrorCode = 2 22 | ErrorCodePlayerOutOfRange ErrorCode = 3 23 | ErrorCodePredictionThreshod ErrorCode = 4 24 | ErrorCodeUnsupported ErrorCode = 5 25 | ErrorCodeNotSynchronized ErrorCode = 6 26 | ErrorCodeInRollback ErrorCode = 7 27 | ErrorCodeInputDropped ErrorCode = 8 28 | ErrorCodePlayerDisconnected ErrorCode = 9 29 | ErrorCodeTooManySpectators ErrorCode = 10 30 | ErrorCodeInvalidRequest ErrorCode = 11 31 | ) 32 | 33 | func Success(result ErrorCode) bool { 34 | return result == ErrorCodeSuccess 35 | } 36 | -------------------------------------------------------------------------------- /events.go: -------------------------------------------------------------------------------- 1 | package ggpo 2 | 3 | type EventCode int 4 | 5 | const ( 6 | EventCodeConnectedToPeer EventCode = 1000 7 | EventCodeSynchronizingWithPeer EventCode = 1001 8 | EventCodeSynchronizedWithPeer EventCode = 1002 9 | EventCodeRunning EventCode = 1003 10 | EventCodeDisconnectedFromPeer EventCode = 1004 11 | EventCodeTimeSync EventCode = 1005 12 | EventCodeConnectionInterrupted EventCode = 1006 13 | EventCodeConnectionResumed EventCode = 1007 14 | EventCodeSyncTestDesync EventCode = 1008 15 | EventCodeDesync EventCode = 1009 16 | ) 17 | 18 | // the original had a union a named struct for each event type, 19 | // but mostly each just had the PlayerHandle, so instead i'm 20 | // including the extra specific fields in every event object 21 | // Hopefully I remember I did this like this 22 | type Event struct { 23 | Code EventCode 24 | Player PlayerHandle 25 | Count int // synchronizing 26 | Total int // synchronizing 27 | FramesAhead float32 // timesync 28 | TimeSyncPeriodInFrames int // timesync 29 | DisconnectTimeout int // connection interrupted 30 | CurrentState int // SyncTestDesync 31 | LastVerified int // SyncTestDesync 32 | NumFrameOfDesync int // Desync 33 | LocalChecksum int // Desync 34 | RemoteChecksum int // Desync 35 | } 36 | -------------------------------------------------------------------------------- /example/game.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/md5" 6 | "crypto/sha1" 7 | "encoding/gob" 8 | "fmt" 9 | "image/color" 10 | "log" 11 | "math" 12 | "time" 13 | 14 | "github.com/assemblaj/ggpo" 15 | "github.com/hajimehoshi/ebiten/v2" 16 | "github.com/hajimehoshi/ebiten/v2/ebitenutil" 17 | ) 18 | 19 | type GameSession struct { 20 | backend ggpo.Backend 21 | game *Game 22 | saveStates map[int]*Game 23 | } 24 | 25 | var backend ggpo.Backend 26 | var start, next, now int64 27 | 28 | const FRAME_DELAY int = 2 29 | 30 | var currentPlayer int = 1 31 | 32 | type Game struct { 33 | Players []Player 34 | syncTest bool 35 | } 36 | 37 | type Player struct { 38 | X float64 39 | Y float64 40 | Color color.Color 41 | PlayerNum int 42 | } 43 | 44 | func (p *Player) clone() Player { 45 | result := Player{} 46 | result.X = p.X 47 | result.Y = p.Y 48 | result.Color = p.Color 49 | result.PlayerNum = p.PlayerNum 50 | return result 51 | } 52 | 53 | func (g *Game) clone() (result *Game) { 54 | result = &Game{} 55 | *result = *g 56 | 57 | result.Players = make([]Player, len(g.Players)) 58 | for i := range g.Players { 59 | result.Players[i] = g.Players[i].clone() 60 | } 61 | return 62 | } 63 | 64 | func (g *Game) Update() error { 65 | fmt.Println("Idling ") 66 | err := backend.Idle(int(math.Max(0, float64(next-now-1)))) 67 | if err != nil { 68 | panic(err) 69 | } 70 | fmt.Println("Idling Complete") 71 | g.RunFrame() 72 | return nil 73 | } 74 | 75 | func (g *Game) RunFrame() { 76 | input := g.ReadInputs() 77 | buffer := encodeInputs(input) 78 | 79 | fmt.Println("Attempting to add local inputs") 80 | result := backend.AddLocalInput(ggpo.PlayerHandle(currentPlayer), buffer, len(buffer)) 81 | 82 | if g.syncTest { 83 | input = g.ReadInputsP2() 84 | buffer = encodeInputs(input) 85 | result = backend.AddLocalInput(ggpo.PlayerHandle(1), buffer, len(buffer)) 86 | } 87 | fmt.Println("Attempt to add local inputs complete") 88 | if result == nil { 89 | fmt.Println("Attempt to add local inputs was successful") 90 | var values [][]byte 91 | disconnectFlags := 0 92 | 93 | fmt.Println("Attempting to synchronize inputs") 94 | values, result = backend.SyncInput(&disconnectFlags) 95 | if result == nil { 96 | fmt.Println("Attempt synchronize inputs was sucessful") 97 | inputs := decodeInputs(values) 98 | fmt.Println("Advancing Frame from game loop") 99 | g.AdvanceFrame(inputs, disconnectFlags) 100 | } else { 101 | fmt.Printf("Attempt synchronize inputs was unsuccessful: %s\n", result) 102 | } 103 | } else { 104 | fmt.Printf("Attempt to add local inputs unsuccessful: %s\n", result) 105 | } 106 | } 107 | 108 | func (g *Game) AdvanceFrame(inputs []InputBits, disconnectFlags int) { 109 | g.UpdateByInputs(inputs) 110 | err := backend.AdvanceFrame(uint32(g.Checksum())) 111 | 112 | if err != nil { 113 | panic(err) 114 | } 115 | } 116 | 117 | func (g *Game) UpdateByInputs(inputs []InputBits) { 118 | for i, input := range inputs { 119 | if input.isButtonOn(int(ebiten.KeyArrowUp)) { 120 | g.Players[i].Y-- 121 | } 122 | if input.isButtonOn(int(ebiten.KeyArrowDown)) { 123 | g.Players[i].Y++ 124 | } 125 | if input.isButtonOn(int(ebiten.KeyArrowLeft)) { 126 | g.Players[i].X-- 127 | } 128 | if input.isButtonOn(int(ebiten.KeyArrowRight)) { 129 | g.Players[i].X++ 130 | } 131 | if input.isButtonOn(int(ebiten.KeyW)) { 132 | g.Players[i].Y-- 133 | } 134 | if input.isButtonOn(int(ebiten.KeyS)) { 135 | g.Players[i].Y++ 136 | } 137 | if input.isButtonOn(int(ebiten.KeyA)) { 138 | g.Players[i].X-- 139 | } 140 | if input.isButtonOn(int(ebiten.KeyD)) { 141 | g.Players[i].X++ 142 | } 143 | } 144 | } 145 | 146 | func (g *Game) ReadInputs() InputBits { 147 | var in InputBits 148 | 149 | if ebiten.IsKeyPressed(ebiten.KeyArrowUp) { 150 | in.setButton(int(ebiten.KeyArrowUp)) 151 | } 152 | if ebiten.IsKeyPressed(ebiten.KeyArrowDown) { 153 | in.setButton(int(ebiten.KeyArrowDown)) 154 | } 155 | if ebiten.IsKeyPressed(ebiten.KeyArrowLeft) { 156 | in.setButton(int(ebiten.KeyArrowLeft)) 157 | } 158 | if ebiten.IsKeyPressed(ebiten.KeyArrowRight) { 159 | in.setButton(int(ebiten.KeyArrowRight)) 160 | } 161 | return in 162 | } 163 | 164 | func (g *Game) ReadInputsP2() InputBits { 165 | var in InputBits 166 | 167 | if ebiten.IsKeyPressed(ebiten.KeyW) { 168 | in.setButton(int(ebiten.KeyW)) 169 | } 170 | if ebiten.IsKeyPressed(ebiten.KeyS) { 171 | in.setButton(int(ebiten.KeyS)) 172 | } 173 | if ebiten.IsKeyPressed(ebiten.KeyA) { 174 | in.setButton(int(ebiten.KeyA)) 175 | } 176 | if ebiten.IsKeyPressed(ebiten.KeyD) { 177 | in.setButton(int(ebiten.KeyD)) 178 | } 179 | return in 180 | 181 | } 182 | 183 | func (g *Game) Checksum() int { 184 | h := sha1.New() 185 | h.Write([]byte(g.String())) 186 | toSum := h.Sum(nil) 187 | sum := 0 188 | for _, v := range toSum { 189 | sum += int(v) 190 | } 191 | return sum 192 | } 193 | 194 | func (g *Game) Draw(screen *ebiten.Image) { 195 | for _, p := range g.Players { 196 | ebitenutil.DrawRect(screen, p.X, p.Y, 50, 50, p.Color) 197 | } 198 | ebitenutil.DebugPrint(screen, fmt.Sprintf("Player 1: X: %.2f Y:%.2f Player 2 X: %.2f Y: %.2f\nChecksum: %d", 199 | g.Players[0].X, g.Players[0].Y, g.Players[1].X, g.Players[1].Y, g.Checksum())) 200 | } 201 | 202 | func (g *Game) Layout(outsideWidth, insideWidth int) (screenWidth, screenHeight int) { 203 | return 320, 240 204 | } 205 | 206 | func (g *GameSession) SaveGameState(stateID int) int { 207 | g.saveStates[stateID] = g.game.clone() 208 | checksum := calculateChecksum([]byte(g.saveStates[stateID].String())) 209 | return checksum 210 | } 211 | 212 | func calculateChecksum(buffer []byte) int { 213 | cSum := md5.Sum(buffer) 214 | checksum := 0 215 | for i := 0; i < len(cSum); i++ { 216 | checksum += int(cSum[i]) 217 | } 218 | return checksum 219 | } 220 | 221 | func (g *GameSession) LoadGameState(stateID int) { 222 | *g.game = *g.saveStates[stateID] 223 | } 224 | 225 | func (g *GameSession) LogGameState(fileName string, buffer []byte, len int) { 226 | var game2 Game 227 | var buf bytes.Buffer = *bytes.NewBuffer(buffer) 228 | dec := gob.NewDecoder(&buf) 229 | err := dec.Decode(&game2) 230 | if err != nil { 231 | log.Fatal("decode error:", err) 232 | } 233 | log.Printf("%s Game State: %s\n", fileName, game2.String()) 234 | } 235 | 236 | func (g *GameSession) SetBackend(backend ggpo.Backend) { 237 | } 238 | 239 | func (g *Game) String() string { 240 | return fmt.Sprintf("%s : %s ", g.Players[0].String(), g.Players[1].String()) 241 | } 242 | 243 | func (p *Player) String() string { 244 | return fmt.Sprintf("Player %d: X:%f Y:%f Color: %s", p.PlayerNum, p.X, p.Y, p.Color) 245 | } 246 | 247 | func (g *GameSession) AdvanceFrame(flags int) { 248 | fmt.Println("Advancing frame from callback. ") 249 | var discconectFlags int 250 | 251 | // Make sure we fetch the inputs from GGPO and use these to update 252 | // the game state instead of reading from the keyboard. 253 | inputs, result := g.backend.SyncInput(&discconectFlags) 254 | if result == nil { 255 | input := decodeInputs(inputs) 256 | g.game.AdvanceFrame(input, discconectFlags) 257 | } 258 | } 259 | 260 | func (g *GameSession) OnEvent(info *ggpo.Event) { 261 | switch info.Code { 262 | case ggpo.EventCodeConnectedToPeer: 263 | log.Println("EventCodeConnectedToPeer") 264 | case ggpo.EventCodeSynchronizingWithPeer: 265 | log.Println("EventCodeSynchronizingWithPeer") 266 | case ggpo.EventCodeSynchronizedWithPeer: 267 | log.Println("EventCodeSynchronizedWithPeer") 268 | case ggpo.EventCodeRunning: 269 | log.Println("EventCodeRunning") 270 | case ggpo.EventCodeDisconnectedFromPeer: 271 | log.Println("EventCodeDisconnectedFromPeer") 272 | case ggpo.EventCodeTimeSync: 273 | log.Printf("EventCodeTimeSync: FramesAhead %f TimeSyncPeriodInFrames: %d\n", info.FramesAhead, info.TimeSyncPeriodInFrames) 274 | if info.FramesAhead > 0 { 275 | dur := float32(time.Second/60) * info.FramesAhead 276 | time.Sleep(time.Duration(dur)) 277 | } 278 | case ggpo.EventCodeConnectionInterrupted: 279 | log.Println("EventCodeconnectionInterrupted") 280 | case ggpo.EventCodeConnectionResumed: 281 | log.Println("EventCodeconnectionInterrupted") 282 | case ggpo.EventCodeDesync: 283 | log.Printf("Desync Error! LocalCheckSum %d Remote Checksum %d\n", info.LocalChecksum, info.RemoteChecksum) 284 | panic("DesyncError") 285 | } 286 | } 287 | 288 | func GameInitSpectator(localPort int, numPlayers int, hostIp string, hostPort int) *Game { 289 | var inputBits InputBits = 0 290 | 291 | var inputSize int = len(encodeInputs(inputBits)) 292 | session := NewGameSession() 293 | 294 | spectator := ggpo.NewSpectator(&session, localPort, numPlayers, inputSize, hostIp, hostPort) 295 | backend = &spectator 296 | spectator.InitializeConnection() 297 | spectator.Start() 298 | 299 | return session.game 300 | } 301 | 302 | func GameInit(localPort int, numPlayers int, players []ggpo.Player, numSpectators int) *Game { 303 | var result error 304 | var inputBits InputBits = 0 305 | var inputSize int = len(encodeInputs(inputBits)) 306 | 307 | session := NewGameSession() 308 | 309 | peer := ggpo.NewPeer(&session, localPort, numPlayers, inputSize) 310 | //peer := ggpo.NewSyncTest(&session, numPlayers, 8, inputSize, true) 311 | backend = &peer 312 | session.backend = backend 313 | peer.InitializeConnection() 314 | 315 | //session.SetDisconnectTimeout(3000) 316 | //session.SetDisconnectNotifyStart(1000) 317 | var localHandle ggpo.PlayerHandle 318 | for i := 0; i < numPlayers+numSpectators; i++ { 319 | var handle ggpo.PlayerHandle 320 | result = peer.AddPlayer(&players[i], &handle) 321 | 322 | if players[i].PlayerType == ggpo.PlayerTypeLocal { 323 | currentPlayer = int(handle) 324 | } 325 | if result != nil { 326 | log.Fatalf("There's an issue from AddPlayer") 327 | } 328 | if players[i].PlayerType == ggpo.PlayerTypeLocal { 329 | localHandle = handle 330 | } 331 | } 332 | peer.SetDisconnectTimeout(3000) 333 | peer.SetDisconnectNotifyStart(1000) 334 | peer.SetFrameDelay(localHandle, FRAME_DELAY) 335 | 336 | peer.Start() 337 | return session.game 338 | } 339 | 340 | func NewGameSession() GameSession { 341 | g := GameSession{} 342 | game := NewGame() 343 | g.game = &game 344 | g.saveStates = make(map[int]*Game) 345 | return g 346 | } 347 | 348 | func NewGame() Game { 349 | var player1 = Player{ 350 | X: 50, 351 | Y: 50, 352 | Color: color.RGBA{255, 0, 0, 255}, 353 | PlayerNum: 1} 354 | var player2 = Player{ 355 | X: 150, 356 | Y: 50, 357 | Color: color.RGBA{0, 0, 255, 255}, 358 | PlayerNum: 2} 359 | return Game{ 360 | Players: []Player{player1, player2}, 361 | ///syncTest: true, 362 | } 363 | } 364 | 365 | func init() { 366 | 367 | // have to register everything with gob, maybe automate this? 368 | // have to inititalize all arrays 369 | gob.Register(color.RGBA{}) 370 | gob.Register(Input{}) 371 | 372 | } 373 | -------------------------------------------------------------------------------- /example/inputs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "fmt" 7 | "log" 8 | ) 9 | 10 | type Input struct { 11 | ButtonMap int 12 | } 13 | 14 | type InputBits int 15 | 16 | func (i *InputBits) isButtonOn(button int) bool { 17 | return *i&(1< 0 18 | } 19 | 20 | func (i *InputBits) setButton(button int) { 21 | *i |= (1 << button) 22 | } 23 | 24 | func readI32(b []byte) int32 { 25 | if len(b) < 4 { 26 | return 0 27 | } 28 | return int32(b[0]) | int32(b[1])<<8 | int32(b[2])<<16 | int32(b[3])<<24 29 | } 30 | 31 | func writeI32(i32 int32) []byte { 32 | b := []byte{byte(i32), byte(i32 >> 8), byte(i32 >> 16), byte(i32 >> 24)} 33 | return b 34 | } 35 | 36 | func (i *Input) isButtonOn(button int) bool { 37 | return i.ButtonMap&(1< 0 38 | } 39 | 40 | func (i *Input) setButton(button int) { 41 | i.ButtonMap |= (1 << button) 42 | } 43 | 44 | func (i Input) String() string { 45 | return fmt.Sprintf("Input %d", i.ButtonMap) 46 | } 47 | 48 | func NewInput() Input { 49 | return Input{} 50 | } 51 | 52 | func encodeInputs(inputs InputBits) []byte { 53 | return writeI32(int32(inputs)) 54 | } 55 | 56 | func decodeInputs(buffer [][]byte) []InputBits { 57 | var inputs = make([]InputBits, len(buffer)) 58 | for i, b := range buffer { 59 | inputs[i] = InputBits(readI32(b)) 60 | } 61 | return inputs 62 | } 63 | 64 | func decodeInputsGob(buffer [][]byte) []Input { 65 | var inputs = make([]Input, len(buffer)) 66 | for i, b := range buffer { 67 | var buf bytes.Buffer = *bytes.NewBuffer(b) 68 | dec := gob.NewDecoder(&buf) 69 | err := dec.Decode(&inputs[i]) 70 | if err != nil { 71 | log.Printf("decode error: %s. Returning empty input\n", err) 72 | // hack 73 | inputs[i] = NewInput() 74 | //panic("eof") 75 | } else { 76 | log.Printf("inputs properly decoded: %s\n", inputs[i]) 77 | } 78 | } 79 | return inputs 80 | } 81 | 82 | func encodeInputsGob(inputs Input) []byte { 83 | var buf bytes.Buffer 84 | enc := gob.NewEncoder(&buf) 85 | err := enc.Encode(&inputs) 86 | if err != nil { 87 | log.Fatal("encode error ", err) 88 | } 89 | return buf.Bytes() 90 | } 91 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "os" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | // "net/http" 12 | // _ "net/http/pprof" 13 | 14 | "github.com/assemblaj/ggpo" 15 | "github.com/hajimehoshi/ebiten/v2" 16 | ) 17 | 18 | type peerAddress struct { 19 | ip string 20 | port int 21 | } 22 | 23 | func getPeerAddress(address string) peerAddress { 24 | peerIPSlice := strings.Split(address, ":") 25 | if len(peerIPSlice) < 2 { 26 | panic("Please enter IP as ip:port") 27 | } 28 | peerPort, err := strconv.Atoi(peerIPSlice[1]) 29 | if err != nil { 30 | panic("Please enter integer port") 31 | } 32 | return peerAddress{ 33 | ip: peerIPSlice[0], 34 | port: peerPort, 35 | } 36 | } 37 | 38 | func main() { 39 | 40 | // go func() { 41 | // log.Println(http.ListenAndServe("localhost:6060", nil)) 42 | // }() 43 | 44 | argsWithoutProg := os.Args[1:] 45 | if len(argsWithoutProg) < 4 { 46 | panic("Must enter ('local' |IP adress) ('local' |IP adress) currentPlayer or spectate :") 47 | } 48 | var localPort, numPlayers int 49 | var err error 50 | localPort, err = strconv.Atoi(argsWithoutProg[0]) 51 | if err != nil { 52 | panic("Plase enter integer port") 53 | } 54 | 55 | numPlayers, err = strconv.Atoi(argsWithoutProg[1]) 56 | if err != nil { 57 | panic("Please enter integer numPlayers") 58 | } 59 | 60 | // logFileName := "" 61 | // if len(argsWithoutProg) > 4 { 62 | // logFileName = "Player" + argsWithoutProg[4] + ".log" 63 | // } else { 64 | // logFileName = "Spectator.log" 65 | // } 66 | 67 | // f, err := os.OpenFile(logFileName, os.O_CREATE|os.O_RDWR, 0666) 68 | // if err != nil { 69 | // panic(err) 70 | // } 71 | 72 | // // don't forget to close it 73 | // defer f.Close() 74 | // logger := log.New(f, "Logger:", log.Ldate|log.Ltime|log.Lshortfile) 75 | // ggpo.EnableLogs() 76 | // ggpo.SetLogger(logger) 77 | 78 | var game *Game 79 | if argsWithoutProg[2] == "spectate" { 80 | hostIp := argsWithoutProg[3] 81 | hostAddress := getPeerAddress(hostIp) 82 | game = GameInitSpectator(localPort, numPlayers, hostAddress.ip, hostAddress.port) 83 | } else { 84 | ipAddress := []string{argsWithoutProg[2], argsWithoutProg[3]} 85 | 86 | currentPlayer, err = strconv.Atoi(argsWithoutProg[4]) 87 | if err != nil { 88 | panic("Please enter integer currentPlayer") 89 | } 90 | 91 | players := make([]ggpo.Player, ggpo.MaxPlayers+ggpo.MaxSpectators) 92 | var i int 93 | for i = 0; i < numPlayers; i++ { 94 | if ipAddress[i] == "local" { 95 | players[i] = ggpo.NewLocalPlayer(20, i+1) 96 | } else { 97 | remoteAddress := getPeerAddress(ipAddress[i]) 98 | players[i] = ggpo.NewRemotePlayer(20, i+1, remoteAddress.ip, remoteAddress.port) 99 | } 100 | } 101 | 102 | offset := 5 103 | numSpectators := 0 104 | for offset < len(argsWithoutProg) { 105 | remoteAddress := getPeerAddress(argsWithoutProg[offset]) 106 | players[i] = ggpo.NewSpectatorPlayer(20, remoteAddress.ip, remoteAddress.port) 107 | numSpectators++ 108 | i++ 109 | offset++ 110 | } 111 | game = GameInit(localPort, numPlayers, players, numSpectators) 112 | } 113 | 114 | flag.Parse() 115 | start = time.Now().UnixMilli() 116 | next = start 117 | now = start 118 | if err := ebiten.RunGame(game); err != nil { 119 | log.Fatal(err) 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /ggpo_go_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/assemblaj/ggpo/467f3782a9cf617a759449d6a44fdd10c5dcf905/ggpo_go_logo.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/assemblaj/ggpo 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/hajimehoshi/ebiten/v2 v2.3.4 7 | golang.org/x/exp v0.0.0-20220609121020-a51bd0440498 8 | ) 9 | 10 | require ( 11 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220320163800-277f93cfa958 // indirect 12 | github.com/gofrs/flock v0.8.1 // indirect 13 | github.com/jezek/xgb v1.0.0 // indirect 14 | golang.org/x/exp/shiny v0.0.0-20220613132600-b0d781184e0d // indirect 15 | golang.org/x/image v0.0.0-20220321031419-a8550c1d254a // indirect 16 | golang.org/x/mobile v0.0.0-20220518205345-8578da9835fd // indirect 17 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect 18 | golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 2 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220320163800-277f93cfa958 h1:TL70PMkdPCt9cRhKTqsm+giRpgrd0IGEj763nNr2VFY= 3 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20220320163800-277f93cfa958/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 4 | github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= 5 | github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= 6 | github.com/hajimehoshi/bitmapfont/v2 v2.2.0/go.mod h1:Llj2wTYXMuCTJEw2ATNIO6HbFPOoBYPs08qLdFAxOsQ= 7 | github.com/hajimehoshi/ebiten/v2 v2.3.4 h1:PEPbid818lZCScgUReBt13r9THPJir/Wc49DGIWdw2M= 8 | github.com/hajimehoshi/ebiten/v2 v2.3.4/go.mod h1:vxwpo0q0oSi1cIll0Q3Ui33TVZgeHuFVYzIRk7FwuVk= 9 | github.com/hajimehoshi/file2byteslice v0.0.0-20210813153925-5340248a8f41/go.mod h1:CqqAHp7Dk/AqQiwuhV1yT2334qbA/tFWQW0MD2dGqUE= 10 | github.com/hajimehoshi/go-mp3 v0.3.3/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= 11 | github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= 12 | github.com/hajimehoshi/oto/v2 v2.1.0/go.mod h1:9i0oYbpJ8BhVGkXDKdXKfFthX1JUNfXjeTp944W8TGM= 13 | github.com/jakecoffman/cp v1.1.0/go.mod h1:JjY/Fp6d8E1CHnu74gWNnU0+b9VzEdUVPoJxg2PsTQg= 14 | github.com/jezek/xgb v1.0.0 h1:s2rRzAV8KQRlpsYA7Uyxoidv1nodMF0m6dIG6FhhVLQ= 15 | github.com/jezek/xgb v1.0.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= 16 | github.com/jfreymuth/oggvorbis v1.0.3/go.mod h1:1U4pqWmghcoVsCJJ4fRBKv9peUJMBHixthRlBeD6uII= 17 | github.com/jfreymuth/vorbis v1.0.2/go.mod h1:DoftRo4AznKnShRl1GxiTFCseHr4zR9BN3TWXyuzrqQ= 18 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 19 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 20 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 21 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 22 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 23 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= 24 | github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 25 | github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 26 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 27 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 28 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 29 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 30 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 31 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= 32 | golang.org/x/exp v0.0.0-20220609121020-a51bd0440498 h1:TF0FvLUGEq/8wOt/9AV1nj6D4ViZGUIGCMQfCv7VRXY= 33 | golang.org/x/exp v0.0.0-20220609121020-a51bd0440498/go.mod h1:yh0Ynu2b5ZUe3MQfp2nM0ecK7wsgouWTDN0FNeJuIys= 34 | golang.org/x/exp/shiny v0.0.0-20220613132600-b0d781184e0d h1:3fmC1S0vAPNLt6CUsdXpv2GMIrro+hc2KsEjgIi3QcQ= 35 | golang.org/x/exp/shiny v0.0.0-20220613132600-b0d781184e0d/go.mod h1:VjAR7z0ngyATZTELrBSkxOOHhhlnVUxDye4mcjx5h/8= 36 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 37 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 38 | golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= 39 | golang.org/x/image v0.0.0-20220321031419-a8550c1d254a h1:LnH9RNcpPv5Kzi15lXg42lYMPUf0x8CuPv1YnvBWZAg= 40 | golang.org/x/image v0.0.0-20220321031419-a8550c1d254a/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= 41 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 42 | golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 43 | golang.org/x/mobile v0.0.0-20220518205345-8578da9835fd h1:x1GptNaTtxPAlTVIAJk61fuXg0y17h09DTxyb+VNC/k= 44 | golang.org/x/mobile v0.0.0-20220518205345-8578da9835fd/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ= 45 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 46 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 47 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= 48 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 49 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 50 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 51 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 52 | golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 53 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 54 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 55 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 56 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 57 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 58 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 59 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 60 | golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 61 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 62 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 63 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 64 | golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 65 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 66 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 67 | golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f h1:8w7RhxzTVgUzw/AH/9mUV5q0vMgy40SQRursCcfmkCw= 68 | golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 69 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 70 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 71 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 72 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 73 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 74 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 75 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 76 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 77 | golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= 78 | golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= 79 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 80 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 81 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 82 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 83 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 84 | -------------------------------------------------------------------------------- /internal/buffer/ringbuffer.go: -------------------------------------------------------------------------------- 1 | package buffer 2 | 3 | import "errors" 4 | 5 | type RingBuffer[T any] struct { 6 | elements []T 7 | head int 8 | tail int 9 | size int 10 | capacity int 11 | } 12 | 13 | func NewRingBuffer[T any](capacity int) RingBuffer[T] { 14 | return RingBuffer[T]{ 15 | elements: make([]T, capacity), 16 | capacity: capacity, 17 | } 18 | } 19 | 20 | func (r *RingBuffer[T]) Front() (T, error) { 21 | var element T 22 | if r.size == r.capacity { 23 | return element, errors.New("ggpo RingBuffer Front : r.size == capacity") 24 | } 25 | element = r.elements[r.tail] 26 | return element, nil 27 | } 28 | 29 | func (r *RingBuffer[T]) Item(i int) (T, error) { 30 | var element T 31 | if i >= r.size { 32 | return element, errors.New("ggpo RingBuffer Item: i >= r.size") 33 | } 34 | element = r.elements[(r.tail+i)%r.capacity] 35 | return element, nil 36 | } 37 | 38 | // hmm this fails if its at its max size 39 | // seems to not make sense 40 | func (r *RingBuffer[T]) Pop() error { 41 | if r.size == r.capacity { 42 | return errors.New("ggpo RingBuffer Pop : r.size == r.capacity") 43 | } 44 | r.tail = (r.tail + 1) % r.capacity 45 | r.size-- 46 | return nil 47 | } 48 | 49 | func (r *RingBuffer[T]) Push(element T) error { 50 | if r.size == r.capacity-1 { 51 | return errors.New("ggpo RingBuffer Push : r.size == r.capacity -1 ") 52 | } 53 | r.elements[r.head] = element 54 | r.head = (r.head + 1) % r.capacity 55 | r.size++ 56 | return nil 57 | } 58 | 59 | func (r *RingBuffer[T]) Size() int { 60 | return r.size 61 | } 62 | 63 | // It's supposed to be size == 0, I have no idea why it's been making it to <1 but it's been 64 | // breaking me code 65 | // - me considering checking for r <= 0 66 | func (r *RingBuffer[T]) Empty() bool { 67 | return r.size <= 0 68 | } 69 | -------------------------------------------------------------------------------- /internal/buffer/ringbuffer_test.go: -------------------------------------------------------------------------------- 1 | package buffer_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/assemblaj/ggpo/internal/buffer" 7 | ) 8 | 9 | func TestRingBufferSizeZero(t *testing.T) { 10 | rb := buffer.NewRingBuffer[int](0) 11 | rbSize := rb.Size() 12 | if rbSize != 0 { 13 | t.Errorf("Expected Size 0, got %d", rbSize) 14 | } 15 | 16 | } 17 | func TestRingBufferIsEmpty(t *testing.T) { 18 | rb := buffer.NewRingBuffer[int](1) 19 | 20 | if !rb.Empty() { 21 | t.Errorf("Expected to be empty, instead not empty") 22 | } 23 | 24 | } 25 | 26 | func TestRingBufferPush(t *testing.T) { 27 | rb := buffer.NewRingBuffer[int](2) 28 | rb.Push(1) 29 | if rb.Empty() { 30 | t.Errorf("Expected to have value, instead was empty") 31 | } 32 | 33 | } 34 | 35 | func TestRingBufferPop(t *testing.T) { 36 | rb := buffer.NewRingBuffer[int](2) 37 | rb.Push(1) 38 | rb.Pop() 39 | if !rb.Empty() { 40 | t.Errorf("Expected to be empty, instead not empty") 41 | } 42 | 43 | } 44 | 45 | func TestRingBufferItem(t *testing.T) { 46 | rb := buffer.NewRingBuffer[int](3) 47 | rb.Push(1) 48 | rb.Push(2) 49 | var val int 50 | 51 | val, _ = rb.Item(0) 52 | if val != 1 { 53 | t.Errorf("Expected to have value: 1, instead was %d", val) 54 | } 55 | 56 | val, _ = rb.Item(1) 57 | if val != 2 { 58 | t.Errorf("Expected to have value: 2, instead was %d", val) 59 | } 60 | } 61 | 62 | func TestRingBufferFront(t *testing.T) { 63 | rb := buffer.NewRingBuffer[int](3) 64 | rb.Push(1) 65 | rb.Push(2) 66 | var val int 67 | 68 | val, _ = rb.Front() 69 | if val != 1 { 70 | t.Errorf("Expected to have value: 1, instead was %d", val) 71 | } 72 | 73 | rb.Pop() 74 | val, _ = rb.Front() 75 | if val != 2 { 76 | t.Errorf("Expected to have value: 2, instead was %d", val) 77 | } 78 | } 79 | 80 | func TestRingBufferPushOverMaxError(t *testing.T) { 81 | rb := buffer.NewRingBuffer[int](3) 82 | rb.Push(1) 83 | rb.Push(1) 84 | rb.Push(1) 85 | err := rb.Push(1) 86 | if err == nil { 87 | t.Errorf("Pushing past capcity should create an arror.") 88 | } 89 | } 90 | 91 | func TestRingBufferItemError(t *testing.T) { 92 | rb := buffer.NewRingBuffer[int](3) 93 | _, err := rb.Item(1232) 94 | if err == nil { 95 | t.Errorf("Trying to get an item larger than the size of the buffer should be an error.") 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /internal/buffer/staticbuffer.go: -------------------------------------------------------------------------------- 1 | package buffer 2 | 3 | import "errors" 4 | 5 | type StaticBuffer[T any] struct { 6 | elements []T 7 | size int 8 | capacity int 9 | } 10 | 11 | func NewStaticBuffer[T any](capacity int) StaticBuffer[T] { 12 | return StaticBuffer[T]{ 13 | elements: make([]T, capacity), 14 | capacity: capacity, 15 | } 16 | } 17 | 18 | func (s *StaticBuffer[T]) Get(i int) (T, error) { 19 | var element T 20 | if i < 0 || i >= s.size { 21 | return element, errors.New("ggpo StaticBuffer Get: i < 0 || i >= s.size") 22 | } 23 | return s.elements[i], nil 24 | } 25 | 26 | func (s *StaticBuffer[T]) PushBack(t T) error { 27 | if s.size == s.capacity-1 { 28 | return errors.New("ggpo StaticBuffer PushBack :s.size == s.capacity-1") 29 | } 30 | s.elements[s.size] = t 31 | s.size++ 32 | return nil 33 | } 34 | 35 | func (s *StaticBuffer[T]) Size() int { 36 | return s.size 37 | } 38 | -------------------------------------------------------------------------------- /internal/buffer/staticbuffer_test.go: -------------------------------------------------------------------------------- 1 | package buffer_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/assemblaj/ggpo/internal/buffer" 7 | ) 8 | 9 | func TestNewStaticBuffer(t *testing.T) { 10 | sb := buffer.NewStaticBuffer[int](16) 11 | pushedValue := 88 12 | err := sb.PushBack(pushedValue) 13 | if err != nil { 14 | t.Errorf("Got error when pushing back value onto static buffer") 15 | } 16 | got, err := sb.Get(0) 17 | if err != nil { 18 | t.Errorf("Got error when trying to get value from static buffer") 19 | } 20 | want := pushedValue 21 | if want != got { 22 | t.Errorf("expected '%d' but got '%d'", want, got) 23 | } 24 | 25 | } 26 | 27 | func TestStaticBufferGetNegativeValue(t *testing.T) { 28 | sb := buffer.NewStaticBuffer[int](16) 29 | pushedValue := 88 30 | err := sb.PushBack(pushedValue) 31 | if err != nil { 32 | t.Errorf("Got error when pushing back value onto static buffer") 33 | } 34 | _, err = sb.Get(-1) 35 | if err == nil { 36 | t.Errorf("Should have returned error when trying to retrieve negative index.") 37 | } 38 | 39 | } 40 | 41 | func TestStaticBufferGetEmptyBuffer(t *testing.T) { 42 | sb := buffer.NewStaticBuffer[int](16) 43 | _, err := sb.Get(0) 44 | if err == nil { 45 | t.Errorf("Should have returned error when trying to retrieve from an empty buffer.") 46 | } 47 | 48 | } 49 | 50 | func TestStaticBufferAddOverCapacity(t *testing.T) { 51 | capacity := 16 52 | sb := buffer.NewStaticBuffer[int](capacity) 53 | for i := 0; i < capacity-1; i++ { 54 | sb.PushBack(i) 55 | } 56 | err := sb.PushBack(capacity) 57 | if err == nil { 58 | t.Errorf("Should have returned error for trying to push back past the capacity.") 59 | } 60 | } 61 | 62 | func TestStaticBufferSize(t *testing.T) { 63 | capacity := 16 64 | toAdd := 5 65 | sb := buffer.NewStaticBuffer[int](capacity) 66 | for i := 0; i < toAdd; i++ { 67 | sb.PushBack(i) 68 | } 69 | want := toAdd 70 | got := sb.Size() 71 | if want != got { 72 | t.Errorf("expected '%d' but got '%d'", want, got) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /internal/input/game_input.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/assemblaj/ggpo/internal/util" 10 | ) 11 | 12 | const ( 13 | GameInputMaxBytes = 9 14 | GameInputMaxPlayers = 2 15 | NullFrame = -1 16 | ) 17 | 18 | type GameInput struct { 19 | Frame int 20 | Size int 21 | Bits []byte 22 | Inputs [][]byte 23 | Checksum uint32 24 | } 25 | 26 | // Will come back to this if the needing the offset becomes a thing 27 | func NewGameInput(frame int, bits []byte, size int, offset ...int) (GameInput, error) { 28 | if size <= 0 { 29 | return GameInput{}, errors.New("ggpo: newGameInput: size must be greater than 0") 30 | } 31 | /* Not useful for our purposes 32 | if len(offset) == 0 { 33 | Assert(size <= GAMEINPUT_MAX_BYTES*GAMEINPUT_MAX_PLAYERS) 34 | } else { 35 | Assert(size <= GAMEINPUT_MAX_BYTES) 36 | }*/ 37 | return GameInput{ 38 | Frame: frame, 39 | Size: size, 40 | Bits: bits, 41 | }, nil 42 | 43 | } 44 | 45 | func (g *GameInput) IsNull() bool { 46 | return g.Frame == NullFrame 47 | } 48 | 49 | func (g *GameInput) Value(i int) bool { 50 | return (g.Bits[i/8] & (1 << (i % 8))) != 0 51 | } 52 | 53 | func (g *GameInput) Set(i int) { 54 | g.Bits[i/8] |= (1 << (i % 8)) 55 | } 56 | 57 | func (g *GameInput) Clear(i int) { 58 | g.Bits[i/8] &= ^(1 << (i % 8)) 59 | } 60 | 61 | func (g *GameInput) Erase() { 62 | for i := 0; i < len(g.Bits); i++ { 63 | g.Bits[i] = 0 64 | } 65 | } 66 | 67 | func (g *GameInput) Clone() *GameInput { 68 | gi := GameInput{} 69 | gi = *g 70 | gi.Bits = make([]byte, len(g.Bits)) 71 | copy(gi.Bits, g.Bits) 72 | return &gi 73 | } 74 | 75 | func (g *GameInput) Log(prefix string, showFrame bool) { 76 | util.Log.Printf("%s%s", prefix, g) 77 | } 78 | 79 | func (g GameInput) String() string { 80 | retval := fmt.Sprintf("(frame:%d size:%d", g.Frame, g.Size) 81 | builder := strings.Builder{} 82 | for i := 0; i < len(g.Bits); i++ { 83 | builder.WriteByte(g.Bits[i]) 84 | } 85 | builder.WriteString(")") 86 | return retval + builder.String() 87 | } 88 | 89 | func (g *GameInput) Equal(other *GameInput, bitsonly bool) (bool, error) { 90 | if !bitsonly && g.Frame != other.Frame { 91 | util.Log.Printf("frames don't match: %d, %d\n", g.Frame, other.Frame) 92 | } 93 | if g.Size != other.Size { 94 | util.Log.Printf("sizes don't match: %d, %d\n", g.Size, other.Size) 95 | } 96 | if !bytes.Equal(g.Bits, other.Bits) { 97 | util.Log.Printf("bits don't match\n") 98 | } 99 | if !(g.Size > 0 && other.Size > 0) { 100 | return false, errors.New("ggpo: GameInput Equal : !(g.Size > 0 && other.Size > 0)") 101 | } 102 | return (bitsonly || g.Frame == other.Frame) && 103 | g.Size == other.Size && 104 | bytes.Equal(g.Bits, other.Bits), nil 105 | } 106 | -------------------------------------------------------------------------------- /internal/input/game_input_test.go: -------------------------------------------------------------------------------- 1 | package input_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/assemblaj/ggpo/internal/input" 7 | ) 8 | 9 | func TestNewGameInput(t *testing.T) { 10 | _, err := input.NewGameInput(0, []byte{1, 2, 3, 4}, 4) 11 | if err != nil { 12 | t.Errorf("Creating a perfectly valid input return nil.") 13 | } 14 | } 15 | 16 | func TestGameInputEqual(t *testing.T) { 17 | gi, _ := input.NewGameInput(0, []byte{1, 2, 3, 4}, 4) 18 | gi2, _ := input.NewGameInput(0, []byte{1, 2, 3, 4}, 4) 19 | isEqual, err := gi.Equal(&gi2, false) 20 | if err != nil { 21 | t.Errorf("Checking for equality on inputs created from the exact same data created an error.") 22 | } 23 | 24 | if !isEqual { 25 | t.Errorf("Inputs created from the exact same parameters are said to not be equal.") 26 | } 27 | } 28 | 29 | func TestGameInputEqualBitsOnly(t *testing.T) { 30 | gi, _ := input.NewGameInput(1, []byte{1, 2, 3, 4}, 4) 31 | gi2, _ := input.NewGameInput(0, []byte{1, 2, 3, 4}, 4) 32 | isEqual, err := gi.Equal(&gi2, true) 33 | if err != nil { 34 | t.Errorf("Checking for equality created an error.") 35 | } 36 | 37 | if !isEqual { 38 | t.Errorf("Inputs created with the same bytes but with different frames are said to be unequal even though the equal checks for bits only.") 39 | } 40 | } 41 | 42 | func TestGameInputEqualDifferentSizes(t *testing.T) { 43 | gi, _ := input.NewGameInput(1, []byte{1, 2, 3, 4}, 4) 44 | gi2, _ := input.NewGameInput(0, []byte{1, 2, 3, 4, 5, 6}, 6) 45 | isEqual, err := gi.Equal(&gi2, false) 46 | if err != nil { 47 | t.Errorf("Checking for equality on inputs that are a different size returned an error instead of just being false..") 48 | } 49 | if isEqual { 50 | t.Errorf("Inputs with different sizes returned as equal. ") 51 | } 52 | } 53 | 54 | func TestGameInputEqualZeroSize(t *testing.T) { 55 | gi, _ := input.NewGameInput(1, []byte{1, 2, 3, 4}, 4) 56 | gi2 := input.GameInput{} 57 | _, err := gi.Equal(&gi2, false) 58 | if err == nil { 59 | t.Errorf("Checking for equality on inputs with sizes <= 0 didn't return an error.") 60 | } 61 | } 62 | 63 | func TestGameInputErase(t *testing.T) { 64 | gi, _ := input.NewGameInput(1, []byte{1, 2, 3, 4}, 4) 65 | gi.Erase() 66 | for i := 0; i < len(gi.Bits); i++ { 67 | if gi.Bits[0] != 0 { 68 | t.Errorf("Erased inputs aren't zero'd.") 69 | } 70 | } 71 | } 72 | 73 | func TestGameInputString(t *testing.T) { 74 | gi, _ := input.NewGameInput(0, []byte{1, 2, 3, 4}, 4) 75 | gi2, _ := input.NewGameInput(0, []byte{1, 2, 3, 4}, 4) 76 | 77 | if gi.String() != gi2.String() { 78 | t.Errorf("Inputs created from the exact same parameters did not created the same strings.") 79 | } 80 | } 81 | 82 | func TestGameZeroSize(t *testing.T) { 83 | _, err := input.NewGameInput(0, []byte{1, 2, 3, 4}, 0) 84 | 85 | if err == nil { 86 | t.Errorf("Inputs with sizes <= 0 should create an error.") 87 | } 88 | } 89 | 90 | func TestGameInputIsNull(t *testing.T) { 91 | input, _ := input.NewGameInput(input.NullFrame, []byte{1, 2, 3, 4}, 4) 92 | want := true 93 | got := input.IsNull() 94 | if want != got { 95 | t.Errorf("want %t got %t.", want, got) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /internal/input/input_queue.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/assemblaj/ggpo/internal/util" 7 | ) 8 | 9 | const ( 10 | InputQueueLength = 128 11 | DefaultInputSize = 4 12 | ) 13 | 14 | type InputQueue struct { 15 | id int 16 | head int 17 | tail int 18 | length int 19 | firstFrame bool 20 | 21 | lastUserAddedFrame int 22 | lastAddedFrame int 23 | firstIncorrectFrame int 24 | lastFrameRequested int 25 | 26 | frameDelay int 27 | 28 | inputs []GameInput 29 | prediction GameInput 30 | } 31 | 32 | func NewInputQueue(id int, inputSize int) InputQueue { 33 | var err error 34 | inputs := make([]GameInput, InputQueueLength) 35 | for i, _ := range inputs { 36 | defaultInput := make([]byte, inputSize) 37 | inputs[i], err = NewGameInput(-1, defaultInput, inputSize) 38 | if err != nil { 39 | panic(err) 40 | } 41 | } 42 | defaultInput := make([]byte, inputSize) 43 | prediction, err := NewGameInput(NullFrame, defaultInput, inputSize) 44 | if err != nil { 45 | panic(err) 46 | } 47 | return InputQueue{ 48 | id: id, 49 | firstFrame: true, 50 | lastUserAddedFrame: NullFrame, 51 | firstIncorrectFrame: NullFrame, 52 | lastFrameRequested: NullFrame, 53 | lastAddedFrame: NullFrame, 54 | prediction: prediction, 55 | inputs: inputs, 56 | } 57 | 58 | } 59 | 60 | func (i *InputQueue) LastConfirmedFrame() int { 61 | util.Log.Printf("returning last confirmed frame %d.\n", i.lastUserAddedFrame) 62 | return i.lastAddedFrame 63 | } 64 | 65 | func (i *InputQueue) FirstIncorrectFrame() int { 66 | return i.firstIncorrectFrame 67 | } 68 | 69 | func (i *InputQueue) DiscardConfirmedFrames(frame int) error { 70 | if frame < 0 { 71 | return errors.New("ggpo: InputQueue discardConfirmedFrames: frames <= 0") 72 | } 73 | 74 | if i.lastFrameRequested != NullFrame { 75 | frame = util.Min(frame, i.lastFrameRequested) 76 | } 77 | util.Log.Printf("discarding confirmed frames up to %d (last_added:%d length:%d [head:%d tail:%d]).\n", 78 | frame, i.lastAddedFrame, i.length, i.head, i.tail) 79 | if frame >= i.lastAddedFrame { 80 | i.tail = i.head 81 | } else { 82 | offset := frame - i.inputs[i.tail].Frame + 1 83 | 84 | util.Log.Printf("difference of %d frames.\n", offset) 85 | if offset < 0 { 86 | return errors.New("ggpo: InputQueue discardConfirmedFrames: offet < 0") 87 | } 88 | 89 | i.tail = (i.tail + offset) % InputQueueLength 90 | i.length -= offset 91 | } 92 | 93 | util.Log.Printf("after discarding, new tail is %d (frame:%d).\n", i.tail, i.inputs[i.tail].Frame) 94 | if i.length < 0 { 95 | return errors.New("ggpo: InputQueue discardConfirmedFrames: i.length < 0 ") 96 | } 97 | 98 | return nil 99 | } 100 | 101 | func (i *InputQueue) ResetPrediction(frame int) error { 102 | if !(i.firstIncorrectFrame == NullFrame || frame <= i.firstIncorrectFrame) { 103 | return errors.New("ggpo: InputQueue ResetPrediction: i.firstIncorrentFrame != NullFrame && frame > i.firstIncorrectFrame") 104 | } 105 | util.Log.Printf("resetting all prediction errors back to frame %d.\n", frame) 106 | 107 | i.prediction.Frame = NullFrame 108 | i.firstIncorrectFrame = NullFrame 109 | i.lastFrameRequested = NullFrame 110 | return nil 111 | } 112 | 113 | func (i *InputQueue) GetConfirmedInput(requestedFrame int, input *GameInput) (bool, error) { 114 | if !(i.firstIncorrectFrame == NullFrame || requestedFrame < i.firstIncorrectFrame) { 115 | return false, errors.New("ggpo: InputQueue GetConfirmedInput : i.firstIncorrectFrame != NullFrame && requestedFrame >") 116 | } 117 | 118 | offset := requestedFrame % InputQueueLength 119 | if i.inputs[offset].Frame != requestedFrame { 120 | return false, nil 121 | } 122 | *input = i.inputs[offset] 123 | return true, nil 124 | } 125 | 126 | func (i *InputQueue) GetInput(requestedFrame int, input *GameInput) (bool, error) { 127 | util.Log.Printf("requesting input frame %d.\n", requestedFrame) 128 | if i.firstIncorrectFrame != NullFrame { 129 | return false, errors.New("ggpo: InputQueue GetInput : i.firstIncorrectFrame != NullFrame") 130 | } 131 | 132 | i.lastFrameRequested = requestedFrame 133 | 134 | if requestedFrame < i.inputs[i.tail].Frame { 135 | return false, errors.New("ggpo: InputQueue GetInput : requestedFrame < i.inputs[i.tail].Frame") 136 | } 137 | if i.prediction.Frame == NullFrame { 138 | offset := requestedFrame - i.inputs[i.tail].Frame 139 | 140 | if offset < i.length { 141 | offset = (offset + i.tail) % InputQueueLength 142 | if i.inputs[offset].Frame != requestedFrame { 143 | return false, errors.New("ggpo: InputQueue GetInput : i.inputs[offset].Frame != requestedFrame") 144 | } 145 | *input = i.inputs[offset] 146 | util.Log.Printf("returning confirmed frame number %d.\n", input.Frame) 147 | return true, nil 148 | } 149 | 150 | if requestedFrame == 0 { 151 | util.Log.Printf("basing new prediction frame from nothing, you're client wants frame 0.\n") 152 | i.prediction.Erase() 153 | } else if i.lastAddedFrame == NullFrame { 154 | util.Log.Printf("basing new prediction frame from nothing, since we have no frames yet.\n") 155 | i.prediction.Erase() 156 | } else { 157 | util.Log.Printf("basing new prediction frame from previously added frame (queue entry:%d, frame:%d).\n", 158 | previousFrame(i.head), i.inputs[previousFrame(i.head)].Frame) 159 | i.prediction = i.inputs[previousFrame(i.head)] 160 | } 161 | i.prediction.Frame++ 162 | } 163 | 164 | if i.prediction.Frame < 0 { 165 | return false, errors.New("ggpo: InputQueue GetInput : i.prediction.Frame < 0") 166 | } 167 | *input = i.prediction 168 | input.Frame = requestedFrame 169 | util.Log.Printf("returning prediction frame number %d (%d).\n", input.Frame, i.prediction.Frame) 170 | 171 | return false, nil 172 | } 173 | 174 | func (i *InputQueue) AddInput(input *GameInput) error { 175 | var newFrame int 176 | var err error 177 | util.Log.Printf("adding input frame number %d to queue.\n", input.Frame) 178 | 179 | if !(i.lastUserAddedFrame == NullFrame || input.Frame == i.lastUserAddedFrame+1) { 180 | return errors.New("ggpo : InputQueue AddInput : !(i.lastUserAddedFrame == NullFrame || input.Frame == i.lastUserAddedFrame+1)") 181 | } 182 | i.lastUserAddedFrame = input.Frame 183 | 184 | newFrame, err = i.AdvanceQueueHead(input.Frame) 185 | if err != nil { 186 | panic(err) 187 | } 188 | 189 | if newFrame != NullFrame { 190 | i.AddDelayedInputToQueue(input, newFrame) 191 | } 192 | 193 | input.Frame = newFrame 194 | return nil 195 | } 196 | 197 | func (i *InputQueue) AddDelayedInputToQueue(input *GameInput, frameNumber int) error { 198 | util.Log.Printf("adding delayed input frame number %d to queue.\n", frameNumber) 199 | 200 | // Assert(input.Size == i.prediction.Size) No 201 | if !(i.lastAddedFrame == NullFrame || frameNumber == i.lastAddedFrame+1) { 202 | return errors.New("ggpo: InputQueue AddDelayedInputToQueue : i.lastAddedFrame != NullFrame && frameNumber != i.lastAddedFrame+1") 203 | } 204 | if !(frameNumber == 0 || i.inputs[previousFrame(i.head)].Frame == frameNumber-1) { 205 | return errors.New("ggpo: InputQueue AddDelayedInputToQueue : frameNumber != 0 && i.inputs[previousFrame(i.head)].Frame == frameNumber-1") 206 | } 207 | /* 208 | * Add the frame to the back of the queue 209 | */ 210 | i.inputs[i.head] = *input 211 | i.inputs[i.head].Frame = frameNumber 212 | i.head = (i.head + 1) % InputQueueLength 213 | i.length++ 214 | i.firstFrame = false 215 | 216 | i.lastAddedFrame = frameNumber 217 | if i.prediction.Frame != NullFrame { 218 | if frameNumber != i.prediction.Frame { 219 | return errors.New("ggpo: InputQueue AddDelayedInputToQueue : frameNumber != i.prediction.Frame") 220 | } 221 | equal, err := i.prediction.Equal(input, true) 222 | if err != nil { 223 | panic(err) 224 | } 225 | if i.firstIncorrectFrame == NullFrame && !equal { 226 | util.Log.Printf("frame %d does not match prediction. marking error.\n", frameNumber) 227 | i.firstIncorrectFrame = frameNumber 228 | } 229 | 230 | if i.prediction.Frame == i.lastFrameRequested && i.firstIncorrectFrame == NullFrame { 231 | util.Log.Printf("prediction is correct! dumping out of prediction mode.\n") 232 | i.prediction.Frame = NullFrame 233 | } else { 234 | i.prediction.Frame++ 235 | } 236 | } 237 | 238 | if i.length > InputQueueLength { 239 | return errors.New("ggpo: InputQueue AddDelayedInputToQueue : i.length > InputQueueLength") 240 | } 241 | return nil 242 | } 243 | 244 | func (i *InputQueue) AdvanceQueueHead(frame int) (int, error) { 245 | util.Log.Printf("advancing queue head to frame %d.\n", frame) 246 | 247 | var expectedFrame int 248 | if i.firstFrame { 249 | expectedFrame = 0 250 | } else { 251 | expectedFrame = i.inputs[previousFrame(i.head)].Frame + 1 252 | } 253 | 254 | frame += i.frameDelay 255 | if expectedFrame > frame { 256 | util.Log.Printf("Dropping input frame %d (expected next frame to be %d).\n", 257 | frame, expectedFrame) 258 | return NullFrame, nil 259 | } 260 | 261 | for expectedFrame < frame { 262 | util.Log.Printf("Adding padding frame %d to account for change in frame delay.\n", 263 | expectedFrame) 264 | lastFrame := i.inputs[previousFrame(i.head)] 265 | err := i.AddDelayedInputToQueue(&lastFrame, expectedFrame) 266 | if err != nil { 267 | panic(err) 268 | } 269 | expectedFrame++ 270 | } 271 | 272 | if !(frame == 0 || frame == i.inputs[previousFrame(i.head)].Frame+1) { 273 | return 0, errors.New("ggpo: InputQueue AdvanceQueueHead : frame != 0 && frame != i.inputs[previousFrame(i.head)].Frame+") 274 | } 275 | return frame, nil 276 | } 277 | 278 | func previousFrame(offset int) int { 279 | if offset == 0 { 280 | return InputQueueLength - 1 281 | } else { 282 | return offset - 1 283 | } 284 | } 285 | 286 | func (i *InputQueue) SetFrameDelay(delay int) { 287 | i.frameDelay = delay 288 | } 289 | 290 | func (i *InputQueue) Length() int { 291 | return i.length 292 | } 293 | -------------------------------------------------------------------------------- /internal/input/input_queue_test.go: -------------------------------------------------------------------------------- 1 | package input_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/assemblaj/ggpo/internal/input" 7 | ) 8 | 9 | /* 10 | Charecterization Tests 11 | */ 12 | func TestFirstLastConfirmedFrame(t *testing.T) { 13 | queue := input.NewInputQueue(0, 50) 14 | want := input.NullFrame 15 | got := queue.LastConfirmedFrame() 16 | if want != got { 17 | t.Errorf("expected '%#v' but got '%#v'", want, got) 18 | } 19 | } 20 | 21 | func TestFirstFirstIncorrectFrame(t *testing.T) { 22 | queue := input.NewInputQueue(0, 50) 23 | want := input.NullFrame 24 | got := queue.FirstIncorrectFrame() 25 | if want != got { 26 | t.Errorf("expected '%#v' but got '%#v'", want, got) 27 | } 28 | } 29 | 30 | // AddInput 31 | func TestAddFirstInput(t *testing.T) { 32 | queue := input.NewInputQueue(0, 50) 33 | input, _ := input.NewGameInput(0, nil, 50) 34 | queue.AddInput(&input) 35 | want := 0 36 | got := queue.LastConfirmedFrame() 37 | if want != got { 38 | t.Errorf("expected '%#v' but got '%#v'", want, got) 39 | } 40 | } 41 | 42 | func TestAddFirstInputLength(t *testing.T) { 43 | queue := input.NewInputQueue(0, 50) 44 | input, _ := input.NewGameInput(0, nil, 50) 45 | queue.AddInput(&input) 46 | want := 1 47 | got := queue.Length() 48 | if want != got { 49 | t.Errorf("expected '%#v' but got '%#v'", want, got) 50 | } 51 | 52 | } 53 | 54 | func TestAddTenInputsDifferentFrame(t *testing.T) { 55 | queue := input.NewInputQueue(0, 50) 56 | for i := 0; i < 10; i++ { 57 | input, _ := input.NewGameInput(i, nil, 50) 58 | queue.AddInput(&input) 59 | } 60 | want := 10 61 | got := queue.Length() 62 | if want != got { 63 | t.Errorf("expected '%#v' but got '%#v'", want, got) 64 | } 65 | 66 | } 67 | 68 | func TestAddTenInputsSameFrame(t *testing.T) { 69 | queue := input.NewInputQueue(0, 50) 70 | for i := 0; i < 10; i++ { 71 | input, _ := input.NewGameInput(0, nil, 50) 72 | queue.AddInput(&input) 73 | } 74 | want := 1 75 | got := queue.Length() 76 | if want != got { 77 | t.Errorf("expected '%#v' but got '%#v'", want, got) 78 | } 79 | } 80 | 81 | // GetInput 82 | func TestGetInputEmptyQueue(t *testing.T) { 83 | queue := input.NewInputQueue(0, 50) 84 | var input input.GameInput 85 | _, err := queue.GetInput(0, &input) 86 | if err != nil { 87 | t.Errorf("expected nil, got an error %s", err) 88 | } 89 | } 90 | 91 | func TestInputQueueNegativeInputSize(t *testing.T) { 92 | defer func() { 93 | if r := recover(); r == nil { 94 | t.Errorf("The code did not panic due to negative input size passed.") 95 | } 96 | }() 97 | input.NewInputQueue(0, -80) 98 | } 99 | func TestInputQueueDiscardConfirmedFramesNegative(t *testing.T) { 100 | queue := input.NewInputQueue(0, 4) 101 | err := queue.DiscardConfirmedFrames(-1) 102 | if err == nil { 103 | t.Errorf("DiscardConfirmedFrames should throw an error when the frame number passed is negative.") 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /internal/messages/message.go: -------------------------------------------------------------------------------- 1 | package messages 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/gob" 7 | "errors" 8 | "fmt" 9 | "unsafe" 10 | ) 11 | 12 | const ( 13 | MaxCompressedBits = 4096 14 | UDPMsgMaxPlayers = 4 15 | ) 16 | 17 | const ( 18 | Int64size = 8 19 | Int32size = 4 20 | Int16size = 2 21 | Int8size = 1 22 | ) 23 | 24 | func init() { 25 | gob.Register(&SyncRequestPacket{}) 26 | gob.Register(&SyncReplyPacket{}) 27 | gob.Register(&QualityReportPacket{}) 28 | gob.Register(&QualityReplyPacket{}) 29 | gob.Register(&InputPacket{}) 30 | gob.Register(&InputAckPacket{}) 31 | gob.Register(&KeepAlivePacket{}) 32 | } 33 | 34 | type UDPMessage interface { 35 | Type() UDPMessageType 36 | Header() UDPHeader 37 | SetHeader(magicNumber uint16, sequenceNumber uint16) 38 | PacketSize() int 39 | ToBytes() []byte 40 | FromBytes([]byte) error 41 | } 42 | 43 | type UDPMessageType int 44 | 45 | const ( 46 | InvalidMsg UDPMessageType = iota 47 | SyncRequestMsg 48 | SyncReplyMsg 49 | InputMsg 50 | QualityReportMsg 51 | QualityReplyMsg 52 | KeepAliveMsg 53 | InputAckMsg 54 | ) 55 | 56 | type UdpConnectStatus struct { 57 | Disconnected bool 58 | LastFrame int32 59 | } 60 | 61 | func (u *UdpConnectStatus) Size() int { 62 | sum := int(unsafe.Sizeof(u.Disconnected)) 63 | sum += int(unsafe.Sizeof(u.LastFrame)) 64 | return sum 65 | } 66 | 67 | func (u *UdpConnectStatus) ToBytes() []byte { 68 | buf := make([]byte, u.Size()) 69 | if u.Disconnected { 70 | buf[0] = 1 71 | } else { 72 | buf[0] = 0 73 | } 74 | binary.BigEndian.PutUint32(buf[1:], uint32(u.LastFrame)) 75 | return buf 76 | } 77 | func (u *UdpConnectStatus) FromBytes(buffer []byte) { 78 | if buffer[0] == 1 { 79 | u.Disconnected = true 80 | } else { 81 | u.Disconnected = false 82 | } 83 | u.LastFrame = int32(binary.BigEndian.Uint32(buffer[1:])) 84 | } 85 | 86 | type UDPHeader struct { 87 | Magic uint16 88 | SequenceNumber uint16 89 | HeaderType uint8 90 | } 91 | 92 | func (u UDPHeader) Size() int { 93 | sum := int(unsafe.Sizeof(u.Magic)) 94 | sum += int(unsafe.Sizeof(u.SequenceNumber)) 95 | sum += int(unsafe.Sizeof(u.HeaderType)) 96 | return sum 97 | } 98 | 99 | func (u UDPHeader) ToBytes() []byte { 100 | buf := make([]byte, 5) 101 | binary.BigEndian.PutUint16(buf[:2], u.Magic) 102 | binary.BigEndian.PutUint16(buf[2:4], u.SequenceNumber) 103 | buf[4] = u.HeaderType 104 | return buf 105 | } 106 | 107 | func (u *UDPHeader) FromBytes(buffer []byte) { 108 | u.Magic = binary.BigEndian.Uint16(buffer[:2]) 109 | u.SequenceNumber = binary.BigEndian.Uint16(buffer[2:4]) 110 | u.HeaderType = buffer[4] 111 | } 112 | 113 | type SyncRequestPacket struct { 114 | MessageHeader UDPHeader 115 | RandomRequest uint32 116 | RemoteMagic uint16 117 | RemoteEndpoint uint8 118 | RemoteInputDelay uint8 119 | } 120 | 121 | func (s *SyncRequestPacket) Type() UDPMessageType { return SyncRequestMsg } 122 | func (s *SyncRequestPacket) Header() UDPHeader { return s.MessageHeader } 123 | func (s *SyncRequestPacket) SetHeader(magicNumber uint16, sequenceNumber uint16) { 124 | s.MessageHeader.Magic = magicNumber 125 | s.MessageHeader.SequenceNumber = sequenceNumber 126 | } 127 | func (s *SyncRequestPacket) PacketSize() int { 128 | sum := s.MessageHeader.Size() 129 | sum += int(unsafe.Sizeof(s.RandomRequest)) 130 | sum += int(unsafe.Sizeof(s.RemoteMagic)) 131 | sum += int(unsafe.Sizeof(s.RemoteEndpoint)) 132 | sum += int(unsafe.Sizeof(s.RemoteInputDelay)) 133 | return sum 134 | } 135 | func (s *SyncRequestPacket) String() string { 136 | return fmt.Sprintf("sync-request (%d).\n", s.RandomRequest) 137 | } 138 | func (s *SyncRequestPacket) ToBytes() []byte { 139 | buf := make([]byte, s.PacketSize()) 140 | copy(buf, s.MessageHeader.ToBytes()) 141 | binary.BigEndian.PutUint32(buf[5:9], s.RandomRequest) 142 | binary.BigEndian.PutUint16(buf[9:11], s.RemoteMagic) 143 | buf[11] = s.RemoteEndpoint 144 | buf[12] = s.RemoteInputDelay 145 | return buf 146 | } 147 | 148 | func (s *SyncRequestPacket) FromBytes(buffer []byte) error { 149 | if len(buffer) < s.PacketSize() { 150 | return errors.New("invalid packet") 151 | } 152 | s.MessageHeader.FromBytes(buffer) 153 | s.RandomRequest = binary.BigEndian.Uint32(buffer[5:9]) 154 | s.RemoteMagic = binary.BigEndian.Uint16(buffer[9:11]) 155 | s.RemoteEndpoint = buffer[11] 156 | s.RemoteInputDelay = buffer[12] 157 | return nil 158 | } 159 | 160 | type SyncReplyPacket struct { 161 | MessageHeader UDPHeader 162 | RandomReply uint32 163 | } 164 | 165 | func (s *SyncReplyPacket) Type() UDPMessageType { return SyncReplyMsg } 166 | func (s *SyncReplyPacket) Header() UDPHeader { return s.MessageHeader } 167 | func (s *SyncReplyPacket) SetHeader(magicNumber uint16, sequenceNumber uint16) { 168 | s.MessageHeader.Magic = magicNumber 169 | s.MessageHeader.SequenceNumber = sequenceNumber 170 | } 171 | func (s *SyncReplyPacket) PacketSize() int { 172 | sum := s.MessageHeader.Size() 173 | sum += int(unsafe.Sizeof(s.RandomReply)) 174 | return sum 175 | } 176 | func (s *SyncReplyPacket) String() string { return fmt.Sprintf("sync-reply (%d).\n", s.RandomReply) } 177 | 178 | func (s *SyncReplyPacket) ToBytes() []byte { 179 | buf := make([]byte, s.PacketSize()) 180 | copy(buf, s.MessageHeader.ToBytes()) 181 | binary.BigEndian.PutUint32(buf[5:9], s.RandomReply) 182 | return buf 183 | } 184 | 185 | func (s *SyncReplyPacket) FromBytes(buffer []byte) error { 186 | if len(buffer) < s.PacketSize() { 187 | return errors.New("invalid packet") 188 | } 189 | s.MessageHeader.FromBytes(buffer) 190 | s.RandomReply = binary.BigEndian.Uint32(buffer[5:9]) 191 | return nil 192 | } 193 | 194 | type QualityReportPacket struct { 195 | MessageHeader UDPHeader 196 | FrameAdvantage int8 197 | Ping uint64 198 | } 199 | 200 | func (q *QualityReportPacket) Type() UDPMessageType { return QualityReportMsg } 201 | func (q *QualityReportPacket) Header() UDPHeader { return q.MessageHeader } 202 | func (q *QualityReportPacket) SetHeader(magicNumber uint16, sequenceNumber uint16) { 203 | q.MessageHeader.Magic = magicNumber 204 | q.MessageHeader.SequenceNumber = sequenceNumber 205 | } 206 | func (q *QualityReportPacket) PacketSize() int { 207 | sum := q.MessageHeader.Size() 208 | sum += int(unsafe.Sizeof(q.FrameAdvantage)) 209 | sum += int(unsafe.Sizeof(q.Ping)) 210 | return sum 211 | } 212 | 213 | func (q *QualityReportPacket) String() string { return "quality report.\n" } 214 | 215 | func (q *QualityReportPacket) ToBytes() []byte { 216 | buf := make([]byte, q.PacketSize()) 217 | copy(buf, q.MessageHeader.ToBytes()) 218 | buf[5] = uint8(q.FrameAdvantage) 219 | binary.BigEndian.PutUint64(buf[6:14], q.Ping) 220 | return buf 221 | } 222 | 223 | func (q *QualityReportPacket) FromBytes(buffer []byte) error { 224 | if len(buffer) < q.PacketSize() { 225 | return errors.New("invalid packet") 226 | } 227 | q.MessageHeader.FromBytes(buffer) 228 | q.FrameAdvantage = int8(buffer[5]) 229 | q.Ping = binary.BigEndian.Uint64(buffer[6:14]) 230 | return nil 231 | } 232 | 233 | type QualityReplyPacket struct { 234 | MessageHeader UDPHeader 235 | Pong uint64 236 | } 237 | 238 | func (q *QualityReplyPacket) Type() UDPMessageType { return QualityReplyMsg } 239 | func (q *QualityReplyPacket) Header() UDPHeader { return q.MessageHeader } 240 | func (q *QualityReplyPacket) SetHeader(magicNumber uint16, sequenceNumber uint16) { 241 | q.MessageHeader.Magic = magicNumber 242 | q.MessageHeader.SequenceNumber = sequenceNumber 243 | } 244 | func (q *QualityReplyPacket) PacketSize() int { 245 | sum := q.MessageHeader.Size() 246 | sum += int(unsafe.Sizeof(q.Pong)) 247 | return sum 248 | } 249 | 250 | func (q *QualityReplyPacket) String() string { return "quality reply.\n" } 251 | 252 | func (q *QualityReplyPacket) ToBytes() []byte { 253 | buf := make([]byte, q.PacketSize()) 254 | copy(buf, q.MessageHeader.ToBytes()) 255 | binary.BigEndian.PutUint64(buf[5:13], q.Pong) 256 | return buf 257 | } 258 | 259 | func (q *QualityReplyPacket) FromBytes(buffer []byte) error { 260 | if len(buffer) < q.PacketSize() { 261 | return errors.New("invalid packet") 262 | } 263 | q.MessageHeader.FromBytes(buffer) 264 | q.Pong = binary.BigEndian.Uint64(buffer[5:13]) 265 | return nil 266 | } 267 | 268 | type InputPacket struct { 269 | MessageHeader UDPHeader 270 | PeerConnectStatus []UdpConnectStatus 271 | StartFrame uint32 272 | 273 | DisconectRequested bool 274 | AckFrame int32 275 | Checksum uint32 276 | NumBits uint16 277 | InputSize uint8 278 | Bits []byte 279 | } 280 | 281 | func (i *InputPacket) Type() UDPMessageType { return InputMsg } 282 | func (i *InputPacket) Header() UDPHeader { return i.MessageHeader } 283 | func (i *InputPacket) SetHeader(magicNumber uint16, sequenceNumber uint16) { 284 | i.MessageHeader.Magic = magicNumber 285 | i.MessageHeader.SequenceNumber = sequenceNumber 286 | } 287 | 288 | // may go back and make this calculate the Bits, Sizes and IsSpectator better 289 | func (i *InputPacket) PacketSize() int { 290 | size := 0 291 | size += int(unsafe.Sizeof(i.MessageHeader)) 292 | size += 1 // will store total 293 | for _, s := range i.PeerConnectStatus { 294 | size += s.Size() 295 | } 296 | size += int(unsafe.Sizeof(i.StartFrame)) 297 | size += int(unsafe.Sizeof(i.DisconectRequested)) 298 | size += int(unsafe.Sizeof(i.AckFrame)) 299 | size += int(unsafe.Sizeof(i.Checksum)) 300 | size += int(unsafe.Sizeof(i.NumBits)) 301 | size += int(unsafe.Sizeof(i.InputSize)) 302 | size += 1 // will store total 303 | size += len(i.Bits) 304 | //size += 1 // will store total 305 | return size 306 | } 307 | func (i *InputPacket) ToBytes() []byte { 308 | buf := make([]byte, i.PacketSize()) 309 | copy(buf, i.MessageHeader.ToBytes()) 310 | buf[5] = byte(len(i.PeerConnectStatus)) 311 | offset := 6 312 | for _, p := range i.PeerConnectStatus { 313 | pcBuf := p.ToBytes() 314 | copy(buf[offset:offset+len(pcBuf)], pcBuf) 315 | offset += len(pcBuf) 316 | } 317 | binary.BigEndian.PutUint32(buf[offset:], i.StartFrame) 318 | offset += 4 319 | if i.DisconectRequested { 320 | buf[offset] = 1 321 | } else { 322 | buf[offset] = 0 323 | } 324 | offset++ 325 | binary.BigEndian.PutUint32(buf[offset:], uint32(i.AckFrame)) 326 | offset += Int32size 327 | binary.BigEndian.PutUint32(buf[offset:], i.Checksum) 328 | offset += Int32size 329 | binary.BigEndian.PutUint16(buf[offset:], i.NumBits) 330 | offset += 2 331 | buf[offset] = i.InputSize 332 | offset++ 333 | buf[offset] = byte(len(i.Bits)) 334 | offset++ 335 | copy(buf[offset:offset+len(i.Bits)], i.Bits) 336 | offset += len(i.Bits) 337 | /* 338 | for _, input := range i.Bits { 339 | buf[offset] = byte(len(input)) 340 | offset++ 341 | copy(buf[offset:offset+len(input)], input) 342 | offset += len(input) 343 | }*/ 344 | return buf 345 | } 346 | 347 | func (i *InputPacket) FromBytes(buffer []byte) error { 348 | if len(buffer) < i.PacketSize() { 349 | return errors.New("invalid packet") 350 | } 351 | 352 | i.MessageHeader.FromBytes(buffer) 353 | totalConnectionStatus := buffer[5] 354 | i.PeerConnectStatus = make([]UdpConnectStatus, totalConnectionStatus) 355 | pcsSize := i.PeerConnectStatus[0].Size() 356 | offset := 6 357 | for p := 0; p < int(totalConnectionStatus); p++ { 358 | i.PeerConnectStatus[p].FromBytes(buffer[offset : offset+pcsSize]) 359 | offset += pcsSize 360 | } 361 | i.StartFrame = binary.BigEndian.Uint32(buffer[offset : offset+4]) 362 | offset += 4 363 | if buffer[offset] == 1 { 364 | i.DisconectRequested = true 365 | } else if buffer[offset] == 0 { 366 | i.DisconectRequested = false 367 | } 368 | offset++ 369 | i.AckFrame = int32(binary.BigEndian.Uint32(buffer[offset : offset+Int32size])) 370 | offset += Int32size 371 | i.Checksum = binary.BigEndian.Uint32(buffer[offset : offset+Int32size]) 372 | offset += Int32size 373 | i.NumBits = binary.BigEndian.Uint16(buffer[offset : offset+2]) 374 | offset += 2 375 | i.InputSize = buffer[offset] 376 | offset++ 377 | totalBits := buffer[offset] 378 | offset++ 379 | i.Bits = make([]byte, totalBits) 380 | copy(i.Bits, buffer[offset:offset+int(totalBits)]) 381 | offset += int(totalBits) 382 | /* 383 | i.Bits = make([][]byte, totalBitsSlices) 384 | for b, _ := range i.Bits { 385 | curBitsSize := int(buffer[offset]) 386 | offset++ 387 | i.Bits[b] = buffer[offset : offset+curBitsSize] 388 | offset += curBitsSize 389 | }*/ 390 | 391 | return nil 392 | } 393 | 394 | func (i InputPacket) String() string { 395 | return fmt.Sprintf("game-compressed-input %d (+ %d bits).\n", 396 | i.StartFrame, i.NumBits) 397 | } 398 | 399 | type InputAckPacket struct { 400 | MessageHeader UDPHeader 401 | AckFrame int32 402 | } 403 | 404 | func (i *InputAckPacket) Type() UDPMessageType { return InputAckMsg } 405 | func (i *InputAckPacket) Header() UDPHeader { return i.MessageHeader } 406 | func (i *InputAckPacket) SetHeader(magicNumber uint16, sequenceNumber uint16) { 407 | i.MessageHeader.Magic = magicNumber 408 | i.MessageHeader.SequenceNumber = sequenceNumber 409 | } 410 | func (i *InputAckPacket) PacketSize() int { 411 | sum := i.MessageHeader.Size() 412 | sum += int(unsafe.Sizeof(i.AckFrame)) 413 | return sum 414 | } 415 | 416 | func (i *InputAckPacket) ToBytes() []byte { 417 | buf := make([]byte, i.PacketSize()) 418 | copy(buf, i.MessageHeader.ToBytes()) 419 | binary.BigEndian.PutUint32(buf[5:], uint32(i.AckFrame)) 420 | return buf 421 | } 422 | 423 | func (i *InputAckPacket) FromBytes(buffer []byte) error { 424 | if len(buffer) < i.PacketSize() { 425 | return errors.New("invalid packet") 426 | } 427 | i.MessageHeader.FromBytes(buffer) 428 | i.AckFrame = int32(binary.BigEndian.Uint32(buffer[5:])) 429 | return nil 430 | } 431 | 432 | func (i *InputAckPacket) String() string { return "input ack.\n" } 433 | 434 | type KeepAlivePacket struct { 435 | MessageHeader UDPHeader 436 | } 437 | 438 | func (k *KeepAlivePacket) Type() UDPMessageType { return KeepAliveMsg } 439 | func (k *KeepAlivePacket) Header() UDPHeader { return k.MessageHeader } 440 | func (k *KeepAlivePacket) SetHeader(magicNumber uint16, sequenceNumber uint16) { 441 | k.MessageHeader.Magic = magicNumber 442 | k.MessageHeader.SequenceNumber = sequenceNumber 443 | } 444 | func (k *KeepAlivePacket) PacketSize() int { 445 | return k.MessageHeader.Size() 446 | } 447 | func (k *KeepAlivePacket) String() string { return "keep alive.\n" } 448 | 449 | func (k *KeepAlivePacket) ToBytes() []byte { 450 | return k.MessageHeader.ToBytes() 451 | } 452 | func (k *KeepAlivePacket) FromBytes(buffer []byte) error { 453 | if len(buffer) < k.PacketSize() { 454 | return errors.New("invalid packet") 455 | } 456 | k.MessageHeader.FromBytes(buffer) 457 | return nil 458 | } 459 | 460 | func NewUDPMessage(t UDPMessageType) UDPMessage { 461 | header := UDPHeader{HeaderType: uint8(t)} 462 | var msg UDPMessage 463 | switch t { 464 | case SyncRequestMsg: 465 | msg = &SyncRequestPacket{ 466 | MessageHeader: header} 467 | case SyncReplyMsg: 468 | msg = &SyncReplyPacket{ 469 | MessageHeader: header} 470 | case QualityReportMsg: 471 | msg = &QualityReportPacket{ 472 | MessageHeader: header} 473 | case QualityReplyMsg: 474 | msg = &QualityReplyPacket{ 475 | MessageHeader: header} 476 | case InputAckMsg: 477 | msg = &InputAckPacket{ 478 | MessageHeader: header} 479 | case InputMsg: 480 | msg = &InputPacket{ 481 | MessageHeader: header} 482 | case KeepAliveMsg: 483 | fallthrough 484 | default: 485 | msg = &KeepAlivePacket{ 486 | MessageHeader: header} 487 | } 488 | return msg 489 | } 490 | 491 | func EncodeMessage(packet UDPMessage) ([]byte, error) { 492 | var buf bytes.Buffer 493 | enc := gob.NewEncoder(&buf) 494 | encErr := enc.Encode(&packet) 495 | if encErr != nil { 496 | return nil, encErr 497 | } 498 | return buf.Bytes(), nil 499 | } 500 | 501 | func DecodeMessage(buffer []byte) (UDPMessage, error) { 502 | buf := bytes.NewBuffer(buffer) 503 | dec := gob.NewDecoder(buf) 504 | var msg UDPMessage 505 | var err error 506 | 507 | if err = dec.Decode(&msg); err != nil { 508 | return nil, err 509 | } 510 | return msg, nil 511 | } 512 | 513 | func DecodeMessageBinary(buffer []byte) (UDPMessage, error) { 514 | msgType, err := GetPacketTypeFromBuffer(buffer) 515 | if err != nil { 516 | return nil, err 517 | } 518 | 519 | switch msgType { 520 | case SyncRequestMsg: 521 | var syncRequestPacket SyncRequestPacket 522 | err = syncRequestPacket.FromBytes(buffer) 523 | if err != nil { 524 | return nil, err 525 | } 526 | return &syncRequestPacket, nil 527 | case SyncReplyMsg: 528 | var syncReplyPacket SyncReplyPacket 529 | err = syncReplyPacket.FromBytes(buffer) 530 | if err != nil { 531 | return nil, err 532 | } 533 | return &syncReplyPacket, nil 534 | case QualityReportMsg: 535 | var qualityReportPacket QualityReportPacket 536 | err := qualityReportPacket.FromBytes(buffer) 537 | if err != nil { 538 | return nil, err 539 | } 540 | return &qualityReportPacket, nil 541 | case QualityReplyMsg: 542 | var qualityReplyPacket QualityReplyPacket 543 | err := qualityReplyPacket.FromBytes(buffer) 544 | if err != nil { 545 | return nil, err 546 | } 547 | return &qualityReplyPacket, nil 548 | case InputAckMsg: 549 | var inputAckPacket InputAckPacket 550 | err = inputAckPacket.FromBytes(buffer) 551 | if err != nil { 552 | return nil, err 553 | } 554 | return &inputAckPacket, nil 555 | case InputMsg: 556 | var inputPacket InputPacket 557 | err = inputPacket.FromBytes(buffer) 558 | if err != nil { 559 | return nil, err 560 | } 561 | return &inputPacket, nil 562 | case KeepAliveMsg: 563 | var keepAlivePacket KeepAlivePacket 564 | err = keepAlivePacket.FromBytes(buffer) 565 | if err != nil { 566 | return nil, err 567 | } 568 | return &keepAlivePacket, nil 569 | default: 570 | return nil, errors.New("message not recognized") 571 | } 572 | } 573 | 574 | func GetPacketTypeFromBuffer(buffer []byte) (UDPMessageType, error) { 575 | if buffer == nil { 576 | return 0, errors.New("nil buffer") 577 | } 578 | if len(buffer) < 5 { 579 | return 0, errors.New("invalid header") 580 | } 581 | return UDPMessageType(buffer[4]), nil 582 | } 583 | -------------------------------------------------------------------------------- /internal/messages/message_test.go: -------------------------------------------------------------------------------- 1 | package messages_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | messages "github.com/assemblaj/ggpo/internal/messages" 9 | ) 10 | 11 | func TestEncodeDecodeUDPMessage(t *testing.T) { 12 | want := &messages.SyncRequestPacket{} 13 | 14 | packetBuffer, err := messages.EncodeMessage(&messages.SyncRequestPacket{}) 15 | if err != nil { 16 | t.Errorf("Error in EncodeMessage %s", err) 17 | } 18 | 19 | packet, err := messages.DecodeMessage(packetBuffer) 20 | got := packet.(*messages.SyncRequestPacket) 21 | if err != nil { 22 | t.Errorf("Error in DecodeMessage %s", err) 23 | } 24 | 25 | if *want != *got { 26 | t.Errorf("expected '%#v' but got '%#v'", want, got) 27 | } 28 | } 29 | 30 | func BenchmarkBinaryEncodeVsGob(b *testing.B) { 31 | msg := messages.NewUDPMessage(messages.SyncRequestMsg) 32 | 33 | buf := msg.ToBytes() 34 | fmt.Printf("Binary buffer size: %d\n", len(buf)) 35 | 36 | buf, _ = messages.EncodeMessage(msg) 37 | fmt.Printf("Gob buffer size: %d\n", len(buf)) 38 | } 39 | 40 | func BenchmarkBinaryEncode(b *testing.B) { 41 | msg := messages.NewUDPMessage(messages.SyncRequestMsg) 42 | for i := 0; i < b.N; i++ { 43 | msg.ToBytes() 44 | } 45 | } 46 | 47 | func BenchmarkGobEncode(b *testing.B) { 48 | for i := 0; i < b.N; i++ { 49 | messages.EncodeMessage(&messages.SyncRequestPacket{}) 50 | } 51 | } 52 | 53 | func TestEncodeHeader(t *testing.T) { 54 | msg := messages.NewUDPMessage(messages.SyncRequestMsg) 55 | workingBuf := msg.ToBytes() 56 | testedBuf := messages.UDPHeader{HeaderType: 1}.ToBytes() 57 | want, err := messages.GetPacketTypeFromBuffer(workingBuf) 58 | if err != nil { 59 | t.Errorf("%s", err) 60 | } 61 | got, err := messages.GetPacketTypeFromBuffer(testedBuf) 62 | if err != nil { 63 | t.Errorf("%s", err) 64 | } 65 | if want != got { 66 | t.Errorf("expected '%#v' but got '%#v'", want, got) 67 | } 68 | } 69 | func TestEncodeDecodeHeader(t *testing.T) { 70 | want := messages.UDPHeader{HeaderType: uint8(messages.SyncRequestMsg), SequenceNumber: 8, Magic: 25} 71 | buf := want.ToBytes() 72 | got := messages.UDPHeader{} 73 | got.FromBytes(buf) 74 | 75 | if got != want { 76 | t.Errorf("expected '%#v' but got '%#v'", want, got) 77 | } 78 | } 79 | 80 | func TestEncodeDecodeUDPConnectionState(t *testing.T) { 81 | want := messages.UdpConnectStatus{Disconnected: true, LastFrame: -1} 82 | buf := want.ToBytes() 83 | got := messages.UdpConnectStatus{} 84 | got.FromBytes(buf) 85 | 86 | if got != want { 87 | t.Errorf("expected '%#v' but got '%#v'", want, got) 88 | } 89 | } 90 | 91 | func TestEncodeDecodeSyncRequestPacket(t *testing.T) { 92 | packet := messages.NewUDPMessage(messages.SyncRequestMsg) 93 | want := packet.(*messages.SyncRequestPacket) 94 | want.RandomRequest = 23 95 | want.RemoteEndpoint = 24 96 | want.RemoteMagic = 9000 97 | 98 | buf := want.ToBytes() 99 | 100 | got := messages.SyncRequestPacket{} 101 | got.FromBytes(buf) 102 | if got != *want { 103 | t.Errorf("expected '%#v' but got '%#v'", want, got) 104 | } 105 | } 106 | 107 | func TestEncodeDecodeSyncReplyPacket(t *testing.T) { 108 | packet := messages.NewUDPMessage(messages.SyncReplyMsg) 109 | want := packet.(*messages.SyncReplyPacket) 110 | want.RandomReply = 23 111 | 112 | buf := want.ToBytes() 113 | 114 | got := messages.SyncReplyPacket{} 115 | got.FromBytes(buf) 116 | if got != *want { 117 | t.Errorf("expected '%#v' but got '%#v'", want, got) 118 | } 119 | } 120 | func TestEncodeDecodeQualityReportPacket(t *testing.T) { 121 | packet := messages.NewUDPMessage(messages.QualityReportMsg) 122 | want := packet.(*messages.QualityReportPacket) 123 | want.FrameAdvantage = 90 124 | want.Ping = 202 125 | 126 | buf := want.ToBytes() 127 | 128 | got := messages.QualityReportPacket{} 129 | got.FromBytes(buf) 130 | if got != *want { 131 | t.Errorf("expected '%#v' but got '%#v'", want, got) 132 | } 133 | } 134 | 135 | func TestEncodeDecodeQualityReplytPacket(t *testing.T) { 136 | packet := messages.NewUDPMessage(messages.QualityReplyMsg) 137 | want := packet.(*messages.QualityReplyPacket) 138 | want.Pong = 23434 139 | 140 | buf := want.ToBytes() 141 | 142 | got := messages.QualityReplyPacket{} 143 | got.FromBytes(buf) 144 | if got != *want { 145 | t.Errorf("expected '%#v' but got '%#v'", want, got) 146 | } 147 | } 148 | 149 | func TestEncodeDecodeInputAckPacket(t *testing.T) { 150 | packet := messages.NewUDPMessage(messages.InputAckMsg) 151 | want := packet.(*messages.InputAckPacket) 152 | want.AckFrame = 112341 153 | 154 | buf := want.ToBytes() 155 | 156 | got := messages.InputAckPacket{} 157 | got.FromBytes(buf) 158 | if got != *want { 159 | t.Errorf("expected '%#v' but got '%#v'", want, got) 160 | } 161 | } 162 | func TestEncodeDecodeKeepAlivePacket(t *testing.T) { 163 | packet := messages.NewUDPMessage(messages.KeepAliveMsg) 164 | want := packet.(*messages.KeepAlivePacket) 165 | 166 | buf := want.ToBytes() 167 | 168 | got := messages.KeepAlivePacket{} 169 | got.FromBytes(buf) 170 | if got != *want { 171 | t.Errorf("expected '%#v' but got '%#v'", want, got) 172 | } 173 | } 174 | func TestEncodeInput(t *testing.T) { 175 | packet := messages.NewUDPMessage(messages.InputMsg) 176 | want := packet.(*messages.InputPacket) 177 | want.InputSize = 20 178 | want.StartFrame = 50 179 | want.NumBits = 643 180 | want.Checksum = 98790 181 | want.DisconectRequested = false 182 | want.PeerConnectStatus = make([]messages.UdpConnectStatus, 2) 183 | want.PeerConnectStatus[0] = messages.UdpConnectStatus{ 184 | Disconnected: false, 185 | LastFrame: -1, 186 | } 187 | want.PeerConnectStatus[1] = messages.UdpConnectStatus{ 188 | Disconnected: true, 189 | LastFrame: 80, 190 | } 191 | want.Bits = []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12} 192 | 193 | buf := want.ToBytes() 194 | 195 | got := messages.InputPacket{} 196 | got.FromBytes(buf) 197 | //gobBuf, _ := messages.EncodeMessage(want) 198 | //fmt.Printf("Gob input size: %d Binary input size %d\n", len(gobBuf), len(buf)) 199 | 200 | if got.InputSize != want.InputSize { 201 | t.Errorf("expected Input Size '%d' but got '%d'", want.InputSize, got.InputSize) 202 | } 203 | if got.StartFrame != want.StartFrame { 204 | t.Errorf("expected Start Frame '%d' but got '%d'", want.StartFrame, got.StartFrame) 205 | } 206 | if got.Checksum != want.Checksum { 207 | t.Errorf("expected Checksum '%d' but got '%d'", want.Checksum, got.Checksum) 208 | } 209 | if got.NumBits != want.NumBits { 210 | t.Errorf("expected Num Bits '%d' but got '%d'", want.NumBits, got.NumBits) 211 | } 212 | if got.DisconectRequested != want.DisconectRequested { 213 | t.Errorf("expected Disconnect Requested '%t' but got '%t'", want.DisconectRequested, got.DisconectRequested) 214 | } 215 | if len(got.PeerConnectStatus) != len(want.PeerConnectStatus) { 216 | t.Errorf("expected Peer Connect Status length'%#v' but got '%#v'", len(want.PeerConnectStatus), len(got.PeerConnectStatus)) 217 | } 218 | 219 | for i := 0; i < len(want.PeerConnectStatus); i++ { 220 | if got.PeerConnectStatus[i] != want.PeerConnectStatus[i] { 221 | t.Errorf("expected Peer Connect Status '%#v' but got '%#v'", want.PeerConnectStatus[i], got.PeerConnectStatus[i]) 222 | } 223 | } 224 | 225 | if len(got.Bits) != len(want.Bits) { 226 | t.Errorf("expected Bits length'%#v' but got '%#v'", len(want.Bits), len(got.Bits)) 227 | } 228 | 229 | if !bytes.Equal(got.Bits, want.Bits) { 230 | t.Errorf("expected Bits Slice '%#v' but got '%#v'", want.Bits, got.Bits) 231 | } 232 | 233 | } 234 | 235 | /* 236 | /* 237 | This test made sense before messages.UDPMessage was a pointer 238 | func TestPackets(t *testing.T) { 239 | packetTests := []struct { 240 | name string 241 | packetType messages.UDPMessage 242 | want messages.UDPMessage 243 | }{ 244 | {name: "SyncRequest", packetType: &messages.SyncRequestPacket{}, want: &messages.SyncRequestPacket{}}, 245 | {name: "SyncReply", packetType: &messages.SyncReplyPacket{}, want: &messages.SyncReplyPacket{}}, 246 | {name: "QualityReport", packetType: &messages.QualityReportPacket{}, want: &messages.QualityReportPacket{}}, 247 | {name: "QualityReply", packetType: &messages.QualityReplyPacket{}, want: &messages.QualityReplyPacket{}}, 248 | //{name: "Input", packetType: Input{}, want: Input{}}, 249 | {name: "InputAck", packetType: &messages.InputAckPacket{}, want: &messages.InputAckPacket{}}, 250 | {name: "KeepAlive", packetType: &messages.KeepAlivePacket{}, want: &messages.KeepAlivePacket{}}, 251 | } 252 | 253 | for _, tt := range packetTests { 254 | packetBuffer, err := messages.EncodeMessage(tt.packetType) 255 | if err != nil { 256 | t.Errorf("Error in EncodeMessage %s", err) 257 | } 258 | 259 | got, err := messages.DecodeMessage(packetBuffer) 260 | if err != nil { 261 | t.Errorf("Error in DecodeMessage %s", err) 262 | } 263 | 264 | if got != tt.want { 265 | t.Errorf("%s got %v want %v", tt.name, got, tt.want) 266 | } 267 | } 268 | 269 | } 270 | */ 271 | 272 | func TestExtractInputFromBytes(t *testing.T) { 273 | want := messages.InputPacket{ 274 | StartFrame: 0, 275 | AckFrame: 12, 276 | Checksum: 5002, 277 | } 278 | inputBytes, err := messages.EncodeMessage(&want) 279 | if err != nil { 280 | t.Errorf("Error in EncodeMessage %s", err) 281 | } 282 | 283 | packet, err := messages.DecodeMessage(inputBytes) 284 | if err != nil { 285 | t.Errorf("Error in DecodeMessage %s", err) 286 | } 287 | 288 | got := packet.(*messages.InputPacket) 289 | if want.StartFrame != got.StartFrame || want.AckFrame != got.AckFrame || want.Checksum != got.Checksum { 290 | t.Errorf("expected '%#v' but got '%#v'", want, got) 291 | } 292 | 293 | } 294 | 295 | func TestNewUDPMessage(t *testing.T) { 296 | 297 | want := messages.InputPacket{ 298 | AckFrame: 0, 299 | StartFrame: 5, 300 | Checksum: 16, 301 | } 302 | 303 | packet := messages.NewUDPMessage(messages.InputMsg) 304 | got := packet.(*messages.InputPacket) 305 | got.AckFrame = 0 306 | got.StartFrame = 5 307 | got.Checksum = 16 308 | 309 | if want.StartFrame != got.StartFrame || want.AckFrame != got.AckFrame || want.Checksum != got.Checksum { 310 | t.Errorf("expected '%#v' but got '%#v'", want, got) 311 | } 312 | 313 | } 314 | func TestDecodeMessageBinaryNil(t *testing.T) { 315 | _, err := messages.DecodeMessageBinary(nil) 316 | if err == nil { 317 | t.Errorf("A nil buffer should cause an error.") 318 | } 319 | } 320 | 321 | func TestDecodeMessageBinaryTooSmall(t *testing.T) { 322 | _, err := messages.DecodeMessageBinary([]byte{1}) 323 | if err == nil { 324 | t.Errorf("A buffer with a size < 4 should cause an error.") 325 | } 326 | } 327 | 328 | func TestDecodeMessageBinaryInvalidBuffer(t *testing.T) { 329 | _, err := messages.DecodeMessageBinary([]byte{3, 3, 3, 3, 3, 3, 3, 3}) 330 | if err == nil { 331 | t.Errorf("Invalid buffer should've created an error. ") 332 | } 333 | } 334 | 335 | func TestDecodeMessageBinaryAllInvalid(t *testing.T) { 336 | fakePacket := []byte{3, 3, 3, 3, 3, 3, 3, 3} 337 | for i := 1; i < 8; i++ { 338 | fakePacket[4] = byte(i) 339 | _, err := messages.DecodeMessageBinary(fakePacket) 340 | if messages.UDPMessageType(i) == messages.KeepAliveMsg { 341 | continue 342 | } 343 | if err == nil { 344 | t.Errorf("Invalid buffer should've created and error.") 345 | } 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /internal/mocks/connection.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/assemblaj/ggpo/internal/messages" 8 | "github.com/assemblaj/ggpo/transport" 9 | ) 10 | 11 | type FakeConnection struct { 12 | SendMap map[string][]messages.UDPMessage 13 | LastSentMessage messages.UDPMessage 14 | } 15 | 16 | func NewFakeConnection() FakeConnection { 17 | return FakeConnection{ 18 | SendMap: make(map[string][]messages.UDPMessage), 19 | } 20 | } 21 | func (f *FakeConnection) SendTo(msg messages.UDPMessage, remoteIp string, remotePort int) { 22 | portStr := strconv.Itoa(remotePort) 23 | addresssStr := remoteIp + ":" + portStr 24 | sendSlice, ok := f.SendMap[addresssStr] 25 | if !ok { 26 | sendSlice := make([]messages.UDPMessage, 2) 27 | f.SendMap[addresssStr] = sendSlice 28 | } 29 | sendSlice = append(sendSlice, msg) 30 | f.SendMap[addresssStr] = sendSlice 31 | f.LastSentMessage = msg 32 | } 33 | 34 | func (f *FakeConnection) Read(messageChan chan transport.MessageChannelItem) { 35 | 36 | } 37 | 38 | func (f *FakeConnection) Close() { 39 | 40 | } 41 | 42 | type FakeP2PConnection struct { 43 | remoteHandler transport.MessageHandler 44 | localPort int 45 | remotePort int 46 | localIP string 47 | printOutput bool 48 | LastSentMessage messages.UDPMessage 49 | MessageHistory []messages.UDPMessage 50 | } 51 | 52 | func (f *FakeP2PConnection) SendTo(msg messages.UDPMessage, remoteIp string, remotePort int) { 53 | if f.printOutput { 54 | fmt.Printf("f.localIP %s f.localPort %d msg %s size %d\n", f.localIP, f.localPort, msg, msg.PacketSize()) 55 | } 56 | f.LastSentMessage = msg 57 | f.MessageHistory = append(f.MessageHistory, msg) 58 | f.remoteHandler.HandleMessage(f.localIP, f.localPort, msg, msg.PacketSize()) 59 | } 60 | 61 | func (f *FakeP2PConnection) Read(messageChan chan transport.MessageChannelItem) { 62 | } 63 | 64 | func (f *FakeP2PConnection) Close() { 65 | 66 | } 67 | 68 | func NewFakeP2PConnection(remoteHandler transport.MessageHandler, localPort int, localIP string) FakeP2PConnection { 69 | f := FakeP2PConnection{} 70 | f.remoteHandler = remoteHandler 71 | f.localPort = localPort 72 | f.localIP = localIP 73 | f.MessageHistory = make([]messages.UDPMessage, 10) 74 | return f 75 | } 76 | 77 | type FakeMultiplePeerConnection struct { 78 | remoteHandler []transport.MessageHandler 79 | localPort int 80 | localIP string 81 | printOutput bool 82 | } 83 | 84 | func (f *FakeMultiplePeerConnection) SendTo(msg messages.UDPMessage, remoteIp string, remotePort int) { 85 | if f.printOutput { 86 | fmt.Printf("f.localIP %s f.localPort %d msg %s size %d\n", f.localIP, f.localPort, msg, msg.PacketSize()) 87 | } 88 | for _, r := range f.remoteHandler { 89 | r.HandleMessage(f.localIP, f.localPort, msg, msg.PacketSize()) 90 | } 91 | } 92 | 93 | func (f *FakeMultiplePeerConnection) Read(messageChan chan transport.MessageChannelItem) { 94 | } 95 | 96 | func (f *FakeMultiplePeerConnection) Close() { 97 | 98 | } 99 | 100 | func NewFakeMultiplePeerConnection(remoteHandler []transport.MessageHandler, localPort int, localIP string) FakeMultiplePeerConnection { 101 | f := FakeMultiplePeerConnection{} 102 | f.remoteHandler = remoteHandler 103 | f.localPort = localPort 104 | f.localIP = localIP 105 | return f 106 | } 107 | -------------------------------------------------------------------------------- /internal/mocks/game.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "crypto/sha1" 5 | "fmt" 6 | ) 7 | 8 | type FakeGame struct { 9 | Players []FakePlayer 10 | } 11 | 12 | func NewFakeGame() FakeGame { 13 | players := make([]FakePlayer, 2) 14 | players[0].PlayerNum = 1 15 | players[1].PlayerNum = 2 16 | return FakeGame{ 17 | Players: players, 18 | } 19 | } 20 | func (f *FakeGame) clone() (result *FakeGame) { 21 | result = &FakeGame{} 22 | *result = *f 23 | result.Players = make([]FakePlayer, len(f.Players)) 24 | for i := range f.Players { 25 | result.Players[i] = f.Players[i].clone() 26 | } 27 | return result 28 | } 29 | 30 | func (f *FakeGame) Checksum() int { 31 | h := sha1.New() 32 | h.Write([]byte(f.String())) 33 | toSum := h.Sum(nil) 34 | sum := 0 35 | for _, v := range toSum { 36 | sum += int(v) 37 | } 38 | return sum 39 | } 40 | 41 | func (g *FakeGame) String() string { 42 | return fmt.Sprintf("%s : %s ", g.Players[0].String(), g.Players[1].String()) 43 | } 44 | 45 | func (g *FakeGame) UpdateByInputs(inputs [][]byte) { 46 | for i, input := range inputs { 47 | if len(input) > 0 { 48 | g.Players[i].X += float64(input[0]) 49 | g.Players[i].Y += float64(input[1]) 50 | } 51 | } 52 | } 53 | 54 | type FakePlayer struct { 55 | X float64 56 | Y float64 57 | PlayerNum int 58 | } 59 | 60 | func (p *FakePlayer) clone() FakePlayer { 61 | result := FakePlayer{} 62 | result.X = p.X 63 | result.Y = p.Y 64 | result.PlayerNum = p.PlayerNum 65 | return result 66 | } 67 | 68 | func (p *FakePlayer) String() string { 69 | return fmt.Sprintf("Player %d: X:%f Y:%f", p.PlayerNum, p.X, p.Y) 70 | } 71 | -------------------------------------------------------------------------------- /internal/mocks/message_handler.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "github.com/assemblaj/ggpo/internal/messages" 5 | "github.com/assemblaj/ggpo/internal/protocol" 6 | ) 7 | 8 | type FakeMessageHandler struct { 9 | Endpoint *protocol.UdpProtocol 10 | } 11 | 12 | func (f *FakeMessageHandler) HandleMessage(ipAddress string, port int, msg messages.UDPMessage, length int) { 13 | if f.Endpoint.HandlesMsg(ipAddress, port) { 14 | f.Endpoint.OnMsg(msg, length) 15 | } 16 | } 17 | 18 | func NewFakeMessageHandler(endpoint *protocol.UdpProtocol) FakeMessageHandler { 19 | f := FakeMessageHandler{} 20 | f.Endpoint = endpoint 21 | return f 22 | } 23 | -------------------------------------------------------------------------------- /internal/mocks/session.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | 7 | "github.com/assemblaj/ggpo" 8 | "github.com/assemblaj/ggpo/internal/util" 9 | ) 10 | 11 | type FakeSession struct { 12 | Game FakeGame 13 | SaveStates map[int]*FakeGame 14 | } 15 | 16 | func NewFakeSession() FakeSession { 17 | return FakeSession{ 18 | Game: NewFakeGame(), 19 | SaveStates: make(map[int]*FakeGame), 20 | } 21 | } 22 | func (s *FakeSession) beginGame(game string) bool { 23 | util.Log.Println("Starting Game!") 24 | return true 25 | } 26 | 27 | func (s *FakeSession) SaveGameState(stateID int) int { 28 | s.SaveStates[stateID] = s.Game.clone() 29 | return ggpo.DefaultChecksum 30 | } 31 | 32 | func (s *FakeSession) LoadGameState(stateID int) { 33 | s.Game = *s.SaveStates[stateID] 34 | } 35 | 36 | func (s *FakeSession) LogGameState(fileName string, buffer []byte, len int) bool { 37 | var game2 FakeGame 38 | var buf bytes.Buffer = *bytes.NewBuffer(buffer) 39 | dec := gob.NewDecoder(&buf) 40 | err := dec.Decode(&game2) 41 | if err != nil { 42 | util.Log.Fatal("decode error:", err) 43 | } 44 | util.Log.Printf("%s Game State: %s\n", fileName, &game2) 45 | return true 46 | } 47 | 48 | func (s *FakeSession) freeBuffer(buffer []byte) { 49 | 50 | } 51 | 52 | func (s *FakeSession) OnEvent(info *ggpo.Event) { 53 | switch info.Code { 54 | case ggpo.EventCodeConnectedToPeer: 55 | util.Log.Println("EventCodeConnectedToPeer") 56 | case ggpo.EventCodeSynchronizingWithPeer: 57 | util.Log.Println("EventCodeSynchronizingWithPeer") 58 | case ggpo.EventCodeSynchronizedWithPeer: 59 | util.Log.Println("EventCodeSynchronizedWithPeer") 60 | case ggpo.EventCodeRunning: 61 | util.Log.Println("EventCodeRunning") 62 | case ggpo.EventCodeDisconnectedFromPeer: 63 | util.Log.Println("EventCodeDisconnectedFromPeer") 64 | case ggpo.EventCodeTimeSync: 65 | util.Log.Println("EventCodeTimeSync") 66 | case ggpo.EventCodeConnectionInterrupted: 67 | util.Log.Println("EventCodeconnectionInterrupted") 68 | case ggpo.EventCodeConnectionResumed: 69 | util.Log.Println("EventCodeconnectionInterrupted") 70 | } 71 | } 72 | func (s *FakeSession) AdvanceFrame(flags int) { 73 | } 74 | 75 | func (s *FakeSession) SetBackend(backend ggpo.Backend) {} 76 | 77 | type FakeSessionWithBackend struct { 78 | backend ggpo.Backend 79 | Game FakeGame 80 | SaveStates map[int]*FakeGame 81 | } 82 | 83 | func NewFakeSessionWithBackend() FakeSessionWithBackend { 84 | return FakeSessionWithBackend{ 85 | Game: NewFakeGame(), 86 | SaveStates: make(map[int]*FakeGame), 87 | } 88 | } 89 | 90 | func (f *FakeSessionWithBackend) SetBackend(backend ggpo.Backend) { 91 | f.backend = backend 92 | } 93 | 94 | func (f *FakeSessionWithBackend) beginGame(game string) bool { 95 | util.Log.Println("Starting Game!") 96 | return true 97 | } 98 | 99 | func (f *FakeSessionWithBackend) SaveGameState(stateID int) int { 100 | f.SaveStates[stateID] = f.Game.clone() 101 | return f.SaveStates[stateID].Checksum() 102 | } 103 | 104 | func (f *FakeSessionWithBackend) LoadGameState(stateID int) { 105 | f.Game = *f.SaveStates[stateID] 106 | } 107 | 108 | func (f *FakeSessionWithBackend) LogGameState(fileName string, buffer []byte, len int) { 109 | var game2 FakeGame 110 | var buf bytes.Buffer = *bytes.NewBuffer(buffer) 111 | dec := gob.NewDecoder(&buf) 112 | err := dec.Decode(&game2) 113 | if err != nil { 114 | util.Log.Fatal("decode error:", err) 115 | } 116 | util.Log.Printf("%s Game State: %s\n", fileName, &game2) 117 | } 118 | 119 | func (f *FakeSessionWithBackend) freeBuffer(buffer []byte) { 120 | 121 | } 122 | 123 | func (f *FakeSessionWithBackend) OnEvent(info *ggpo.Event) { 124 | switch info.Code { 125 | case ggpo.EventCodeConnectedToPeer: 126 | util.Log.Println("EventCodeConnectedToPeer") 127 | case ggpo.EventCodeSynchronizingWithPeer: 128 | util.Log.Println("EventCodeSynchronizingWithPeer") 129 | case ggpo.EventCodeSynchronizedWithPeer: 130 | util.Log.Println("EventCodeSynchronizedWithPeer") 131 | case ggpo.EventCodeRunning: 132 | util.Log.Println("EventCodeRunning") 133 | case ggpo.EventCodeDisconnectedFromPeer: 134 | util.Log.Println("EventCodeDisconnectedFromPeer") 135 | case ggpo.EventCodeTimeSync: 136 | util.Log.Println("EventCodeTimeSync") 137 | case ggpo.EventCodeConnectionInterrupted: 138 | util.Log.Println("EventCodeconnectionInterrupted") 139 | case ggpo.EventCodeConnectionResumed: 140 | util.Log.Println("EventCodeconnectionInterrupted") 141 | } 142 | } 143 | func (f *FakeSessionWithBackend) AdvanceFrame(flags int) { 144 | var discconectFlags int 145 | // Make sure we fetch the inputs from GGPO and use these to update 146 | // the game state instead of reading from the keyboard. 147 | vals, result := f.backend.SyncInput(&discconectFlags) 148 | if result == nil { 149 | f.Game.UpdateByInputs(vals) 150 | f.backend.AdvanceFrame(ggpo.DefaultChecksum) 151 | } 152 | } 153 | 154 | func MakeSessionCallBacks(session FakeSession) ggpo.SessionCallbacks { 155 | var sessionCallbacks ggpo.SessionCallbacks 156 | //sessionCallbacks.AdvanceFrame = session.AdvanceFrame 157 | sessionCallbacks.BeginGame = session.beginGame 158 | sessionCallbacks.FreeBuffer = session.freeBuffer 159 | //sessionCallbacks.LoadGameState = session.LoadGameState 160 | sessionCallbacks.LogGameState = session.LogGameState 161 | //sessionCallbacks.OnEvent = session.OnEvent 162 | //sessionCallbacks.SaveGameState = session.SaveGameState 163 | return sessionCallbacks 164 | } 165 | 166 | func MakeSessionCallBacksBackend(session FakeSessionWithBackend) ggpo.SessionCallbacks { 167 | var sessionCallbacks ggpo.SessionCallbacks 168 | //sessionCallbacks.AdvanceFrame = session.AdvanceFrame 169 | sessionCallbacks.BeginGame = session.beginGame 170 | sessionCallbacks.FreeBuffer = session.freeBuffer 171 | //sessionCallbacks.LoadGameState = session.LoadGameState 172 | //sessionCallbacks.LogGameState = session.LogGameState 173 | //sessionCallbacks.OnEvent = session.OnEvent 174 | //sessionCallbacks.SaveGameState = session.SaveGameState 175 | return sessionCallbacks 176 | } 177 | -------------------------------------------------------------------------------- /internal/polling/poll.go: -------------------------------------------------------------------------------- 1 | package polling 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/assemblaj/ggpo/internal/buffer" 7 | ) 8 | 9 | type FuncTimeType func() int64 10 | 11 | func DefaultTime() int64 { 12 | return time.Now().UnixMilli() 13 | } 14 | 15 | type Poll struct { 16 | startTime int64 17 | handleCount int 18 | loopSinks buffer.StaticBuffer[PollSinkCb] 19 | } 20 | 21 | type Poller interface { 22 | RegisterLoop(sink PollSink, cookie []byte) 23 | Pump(timeFunc ...FuncTimeType) bool 24 | } 25 | 26 | type PollSinkCb struct { 27 | sink PollSink 28 | cookie []byte 29 | } 30 | 31 | type PollSink interface { 32 | OnLoopPoll(timeFunc FuncTimeType) bool 33 | } 34 | 35 | func NewPoll() Poll { 36 | return Poll{ 37 | startTime: 0, 38 | handleCount: 0, 39 | loopSinks: buffer.NewStaticBuffer[PollSinkCb](16), 40 | } 41 | } 42 | 43 | func (p *Poll) RegisterLoop(sink PollSink, cookie []byte) { 44 | err := p.loopSinks.PushBack( 45 | PollSinkCb{ 46 | sink: sink, 47 | cookie: cookie}) 48 | if err != nil { 49 | panic(err) 50 | } 51 | } 52 | 53 | func (p *Poll) Pump(timeFunc ...FuncTimeType) bool { 54 | finished := false 55 | if p.startTime == 0 { 56 | p.startTime = time.Now().UnixMilli() 57 | } 58 | for i := 0; i < p.loopSinks.Size(); i++ { 59 | cb, err := p.loopSinks.Get(i) 60 | if err != nil { 61 | panic(err) 62 | } 63 | if len(timeFunc) != 0 { 64 | finished = !(cb.sink.OnLoopPoll(timeFunc[0]) || finished) 65 | } else { 66 | finished = !(cb.sink.OnLoopPoll(DefaultTime) || finished) 67 | } 68 | } 69 | return finished 70 | 71 | } 72 | -------------------------------------------------------------------------------- /internal/polling/poll_test.go: -------------------------------------------------------------------------------- 1 | package polling_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/assemblaj/ggpo/internal/polling" 7 | ) 8 | 9 | type funcTimeType func() int64 10 | 11 | type FakeSink struct { 12 | used bool 13 | } 14 | 15 | func NewFakeSink() FakeSink { 16 | return FakeSink{} 17 | } 18 | 19 | func (f *FakeSink) OnLoopPoll(timeFunc polling.FuncTimeType) bool { 20 | f.used = true 21 | return true 22 | } 23 | 24 | type FakeFalseSink struct { 25 | } 26 | 27 | func NewFakeFalseSink() FakeFalseSink { 28 | return FakeFalseSink{} 29 | } 30 | 31 | func (f FakeFalseSink) OnLoopPoll(timeFunc polling.FuncTimeType) bool { 32 | return false 33 | } 34 | 35 | func TestRegisterollPanic(t *testing.T) { 36 | poll := polling.NewPoll() 37 | maxSinks := 16 38 | sink := NewFakeSink() 39 | defer func() { 40 | if r := recover(); r == nil { 41 | t.Errorf("The code did not panic when attempting to add more than the max static buffer of sinks.") 42 | } 43 | }() 44 | for i := 0; i < maxSinks+1; i++ { 45 | poll.RegisterLoop(&sink, nil) 46 | } 47 | } 48 | 49 | func TestPollPumpFalse(t *testing.T) { 50 | poll := polling.NewPoll() 51 | sink := NewFakeFalseSink() 52 | poll.RegisterLoop(sink, nil) 53 | want := true 54 | got := poll.Pump(polling.DefaultTime) 55 | if want != got { 56 | t.Errorf("expected '%#v' but got '%#v'", want, got) 57 | } 58 | } 59 | func TestPollPumpIteration(t *testing.T) { 60 | poll := polling.NewPoll() 61 | sink := NewFakeSink() 62 | poll.RegisterLoop(&sink, nil) 63 | poll.Pump(polling.DefaultTime) 64 | want := true 65 | got := sink.used 66 | if want != got { 67 | t.Errorf("expected '%#v' but got '%#v'", want, got) 68 | } 69 | } 70 | 71 | func TestPollPumpIterationMultiple(t *testing.T) { 72 | poll := polling.NewPoll() 73 | maxSinks := 15 74 | sinks := make([]FakeSink, maxSinks) 75 | for i := 0; i < maxSinks; i++ { 76 | newSink := NewFakeSink() 77 | sinks[i] = newSink 78 | poll.RegisterLoop(&sinks[i], nil) 79 | } 80 | poll.Pump(polling.DefaultTime) 81 | for i := 0; i < maxSinks; i++ { 82 | want := true 83 | got := sinks[i].used 84 | if want != got { 85 | t.Errorf("expected '%#v' but got '%#v'", want, got) 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /internal/protocol/protocol.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math/rand" 7 | "time" 8 | 9 | "github.com/assemblaj/ggpo/internal/buffer" 10 | "github.com/assemblaj/ggpo/internal/input" 11 | "github.com/assemblaj/ggpo/internal/messages" 12 | "github.com/assemblaj/ggpo/internal/polling" 13 | "github.com/assemblaj/ggpo/internal/sync" 14 | "github.com/assemblaj/ggpo/internal/util" 15 | "github.com/assemblaj/ggpo/transport" 16 | ) 17 | 18 | const ( 19 | // UDPHeaderSize is the size of the IP + UDP headers. 20 | UDPHeaderSize = 28 21 | NumSyncPackets = 5 22 | SyncRetryInterval = 2000 23 | SyncFirstRetryInterval = 500 24 | RunningRetryInterval = 200 25 | KeepAliveInterval = 200 26 | QualityReportInterval = 1000 27 | NetworkStatsInterval = 1000 28 | UDPShutdownTimer = 5000 29 | MaxSeqDistance = 1 << 15 30 | ) 31 | 32 | type UdpProtocol struct { 33 | stats UdpProtocolStats // may not need these 34 | event UdpProtocolEvent // 35 | 36 | // Network transmission information 37 | connection transport.Connection 38 | peerAddress string 39 | peerPort int 40 | magicNumber uint16 41 | queue int 42 | remoteMagicNumber uint16 43 | connected bool 44 | sendLatency int64 45 | ooPercent int 46 | ooPacket OoPacket 47 | sendQueue buffer.RingBuffer[QueueEntry] 48 | 49 | // Stats 50 | roundTripTime int64 51 | packetsSent int 52 | bytesSent int 53 | kbpsSent int 54 | statsStartTime int64 55 | 56 | // The State Machine 57 | localConnectStatus *[]messages.UdpConnectStatus 58 | peerConnectStatus []messages.UdpConnectStatus 59 | currentState UdpProtocolState 60 | state UdpProtocolStateInfo 61 | 62 | // Fairness 63 | localFrameAdvantage float32 64 | remoteFrameAdvantage float32 65 | 66 | // Packet Loss 67 | pendingOutput buffer.RingBuffer[input.GameInput] 68 | lastRecievedInput input.GameInput 69 | lastSentInput input.GameInput 70 | lastAckedInput input.GameInput 71 | lastSendTime int64 72 | lastRecvTime int64 73 | shutdownTimeout int64 74 | disconnectEventSent bool 75 | disconnectTimeout int64 76 | disconnectNotifyStart int64 77 | disconnectNotifySent bool 78 | 79 | nextSendSeq uint16 80 | nextRecvSeq uint16 81 | 82 | // Rift synchronization 83 | timesync sync.TimeSync 84 | 85 | // Event Queue 86 | eventQueue buffer.RingBuffer[UdpProtocolEvent] 87 | 88 | RemoteChecksumsThisFrame util.OrderedMap[int, uint32] 89 | RemoteChecksums util.OrderedMap[int, uint32] 90 | } 91 | 92 | type NetworkStats struct { 93 | Network NetworkNetworkStats 94 | Timesync NetworkTimeSyncStats 95 | } 96 | 97 | type NetworkNetworkStats struct { 98 | SendQueueLen int 99 | RecvQueueLen int 100 | Ping int64 101 | KbpsSent int 102 | } 103 | type NetworkTimeSyncStats struct { 104 | LocalFramesBehind float32 105 | RemoteFramesBehind float32 106 | AvgLocalFramesBehind float32 107 | AvgRemoteFramesBehind float32 108 | } 109 | 110 | type UdpProtocolStats struct { 111 | ping int 112 | remoteFrameAdvtange int 113 | localFrameAdvantage int 114 | sendQueueLen int 115 | udp transport.UdpStats 116 | } 117 | 118 | type UdpProtocolEvent struct { 119 | eventType UdpProtocolEventType 120 | Input input.GameInput // for Input message 121 | Total int // for synchronizing 122 | Count int // 123 | DisconnectTimeout int // network interrupted 124 | } 125 | 126 | func (upe UdpProtocolEvent) Type() UdpProtocolEventType { 127 | return upe.eventType 128 | } 129 | 130 | // for logging purposes only 131 | func (upe UdpProtocolEvent) String() string { 132 | str := "(event:" 133 | switch upe.eventType { 134 | case UnknownEvent: 135 | str += "Unknown" 136 | break 137 | case ConnectedEvent: 138 | str += "Connected" 139 | break 140 | case SynchronizingEvent: 141 | str += "Synchronizing" 142 | break 143 | case SynchronziedEvent: 144 | str += "Synchronized" 145 | break 146 | case InputEvent: 147 | str += "Input" 148 | break 149 | case DisconnectedEvent: 150 | str += "Disconnected" 151 | break 152 | case NetworkInterruptedEvent: 153 | str += "NetworkInterrupted" 154 | break 155 | case NetworkResumedEvent: 156 | str += "NetworkResumed" 157 | break 158 | } 159 | str += ").\n" 160 | return str 161 | } 162 | 163 | type UdpProtocolEventType int 164 | 165 | const ( 166 | UnknownEvent UdpProtocolEventType = iota - 1 167 | ConnectedEvent 168 | SynchronizingEvent 169 | SynchronziedEvent 170 | InputEvent 171 | DisconnectedEvent 172 | NetworkInterruptedEvent 173 | NetworkResumedEvent 174 | ) 175 | 176 | type UdpProtocolState int 177 | 178 | const ( 179 | SyncingState UdpProtocolState = iota 180 | SynchronziedState 181 | RunningState 182 | DisconnectedState 183 | ) 184 | 185 | type UdpProtocolStateInfo struct { 186 | roundTripRemaining uint32 // sync 187 | random uint32 188 | 189 | lastQualityReportTime int64 // running 190 | lastNetworkStatsInterval int64 191 | lastInputPacketRecvTime int64 192 | } 193 | 194 | type QueueEntry struct { 195 | queueTime int64 196 | destIp string 197 | msg messages.UDPMessage 198 | destPort int 199 | } 200 | 201 | func (q QueueEntry) String() string { 202 | return fmt.Sprintf("Entry : queueTime %d destIp %s msg %s", q.queueTime, q.destIp, q.msg) 203 | } 204 | 205 | func NewQueEntry(time int64, destIp string, destPort int, m messages.UDPMessage) QueueEntry { 206 | return QueueEntry{ 207 | queueTime: time, 208 | destIp: destIp, 209 | destPort: destPort, 210 | msg: m, 211 | } 212 | } 213 | 214 | type OoPacket struct { 215 | sendTime int64 216 | destIp string 217 | msg messages.UDPMessage 218 | } 219 | 220 | func NewUdpProtocol(connection transport.Connection, queue int, ip string, port int, status *[]messages.UdpConnectStatus) UdpProtocol { 221 | var magicNumber uint16 222 | for { 223 | magicNumber = uint16(rand.Int()) 224 | if magicNumber != 0 { 225 | break 226 | } 227 | } 228 | peerConnectStatus := make([]messages.UdpConnectStatus, messages.UDPMsgMaxPlayers) 229 | for i := 0; i < len(peerConnectStatus); i++ { 230 | peerConnectStatus[i].LastFrame = -1 231 | } 232 | lastSentInput, _ := input.NewGameInput(-1, nil, 1) 233 | lastRecievedInput, _ := input.NewGameInput(-1, nil, 1) 234 | lastAckedInput, _ := input.NewGameInput(-1, nil, 1) 235 | 236 | protocol := UdpProtocol{ 237 | connection: connection, 238 | queue: queue, 239 | localConnectStatus: status, 240 | peerConnectStatus: peerConnectStatus, 241 | peerAddress: ip, 242 | peerPort: port, 243 | magicNumber: magicNumber, 244 | pendingOutput: buffer.NewRingBuffer[input.GameInput](64), 245 | sendQueue: buffer.NewRingBuffer[QueueEntry](64), 246 | eventQueue: buffer.NewRingBuffer[UdpProtocolEvent](64), 247 | timesync: sync.NewTimeSync(), 248 | lastSentInput: lastSentInput, 249 | lastRecievedInput: lastRecievedInput, 250 | lastAckedInput: lastAckedInput, 251 | RemoteChecksums: util.NewOrderedMap[int, uint32](16), 252 | RemoteChecksumsThisFrame: util.NewOrderedMap[int, uint32](16), 253 | } 254 | //poll.RegisterLoop(&protocol, nil) 255 | return protocol 256 | } 257 | 258 | func (u *UdpProtocol) StartPollLoop() { 259 | u.RemoteChecksumsThisFrame.Clear() 260 | } 261 | 262 | func (u *UdpProtocol) EndPollLoop() { 263 | if u.RemoteChecksumsThisFrame.Len() > 0 { 264 | highestFrameNum := u.RemoteChecksumsThisFrame.Greatest() 265 | u.RemoteChecksums.Set(highestFrameNum.Key, highestFrameNum.Value) 266 | } 267 | } 268 | 269 | func (u *UdpProtocol) SetIncomingRemoteChecksum(frame int, checksum uint32) { 270 | u.RemoteChecksumsThisFrame.Set(frame, checksum) 271 | } 272 | 273 | func (u *UdpProtocol) SetFrameDelay(delay int) { 274 | u.timesync.SetFrameDelay(delay) 275 | } 276 | 277 | func (u *UdpProtocol) RemoteFrameDelay() int { 278 | return u.timesync.RemoteFrameDelay 279 | } 280 | 281 | func (u *UdpProtocol) OnLoopPoll(timeFunc polling.FuncTimeType) bool { 282 | 283 | // originally was if !udp 284 | if u.connection == nil { 285 | return true 286 | } 287 | now := timeFunc() 288 | 289 | var nextInterval int64 290 | 291 | err := u.PumpSendQueue() 292 | if err != nil { 293 | panic(err) 294 | } 295 | 296 | switch u.currentState { 297 | case SyncingState: 298 | if int(u.state.roundTripRemaining) == NumSyncPackets { 299 | nextInterval = SyncFirstRetryInterval 300 | } else { 301 | nextInterval = SyncRetryInterval 302 | } 303 | if u.lastSendTime > 0 && u.lastSendTime+nextInterval < now { 304 | util.Log.Printf("No luck syncing after %d ms... Re-queueing sync packet.\n", nextInterval) 305 | u.SendSyncRequest() 306 | } 307 | break 308 | case RunningState: 309 | if u.state.lastInputPacketRecvTime == 0 || u.state.lastInputPacketRecvTime+RunningRetryInterval > now { 310 | util.Log.Printf("Haven't exchanged packets in a while (last received:%d last sent:%d). Resending.\n", 311 | u.lastRecievedInput.Frame, u.lastSentInput.Frame) 312 | err := u.SendPendingOutput() 313 | if err != nil { 314 | panic(err) 315 | } 316 | u.state.lastInputPacketRecvTime = now 317 | } 318 | 319 | //if (!u.State.running.last_quality_report_time || _state.running.last_quality_report_time + QUALITY_REPORT_INTERVAL < now) { 320 | if u.state.lastQualityReportTime == 0 || uint32(u.state.lastQualityReportTime)+uint32(QualityReportInterval) < uint32(now) { 321 | msg := messages.NewUDPMessage(messages.QualityReportMsg) 322 | qualityReport := msg.(*messages.QualityReportPacket) 323 | qualityReport.Ping = uint64(time.Now().UnixMilli()) 324 | qualityReport.FrameAdvantage = int8(util.Min(255.0, u.timesync.LocalAdvantage()*10)) 325 | u.SendMsg(qualityReport) 326 | u.state.lastQualityReportTime = now 327 | } 328 | 329 | if u.state.lastNetworkStatsInterval == 0 || u.state.lastNetworkStatsInterval+NetworkStatsInterval < now { 330 | u.UpdateNetworkStats() 331 | u.state.lastNetworkStatsInterval = now 332 | } 333 | 334 | if u.lastSendTime > 0 && u.lastSendTime+KeepAliveInterval < now { 335 | util.Log.Println("Sending keep alive packet") 336 | msg := messages.NewUDPMessage(messages.KeepAliveMsg) 337 | u.SendMsg(msg) 338 | } 339 | 340 | if u.disconnectTimeout > 0 && u.disconnectNotifyStart > 0 && 341 | !u.disconnectNotifySent && (u.lastRecvTime+u.disconnectNotifyStart < now) { 342 | util.Log.Printf("Endpoint has stopped receiving packets for %d ms. Sending notification.\n", u.disconnectNotifyStart) 343 | e := UdpProtocolEvent{ 344 | eventType: NetworkInterruptedEvent} 345 | e.DisconnectTimeout = int(u.disconnectTimeout) - int(u.disconnectNotifyStart) 346 | u.QueueEvent(&e) 347 | u.disconnectNotifySent = true 348 | } 349 | 350 | if u.disconnectTimeout > 0 && (u.lastRecvTime+u.disconnectTimeout < now) { 351 | if !u.disconnectEventSent { 352 | util.Log.Printf("Endpoint has stopped receiving packets for %d ms. Disconnecting.\n", 353 | u.disconnectTimeout) 354 | u.QueueEvent(&UdpProtocolEvent{ 355 | eventType: DisconnectedEvent}) 356 | u.disconnectEventSent = true 357 | } 358 | } 359 | break 360 | case DisconnectedState: 361 | if u.shutdownTimeout < now { 362 | util.Log.Printf("Shutting down udp connection.\n") 363 | u.connection = nil 364 | u.shutdownTimeout = 0 365 | } 366 | } 367 | return true 368 | } 369 | 370 | // all this method did, and all bitvector did, in the original 371 | // was incode input as bits, etc 372 | // go globs can do a lot of that for us, so i've forgone much of that logic 373 | // https://github.com/pond3r/ggpo/blob/7ddadef8546a7d99ff0b3530c6056bc8ee4b9c0a/src/lib/ggpo/network/udp_proto.cpp#L111 374 | func (u *UdpProtocol) SendPendingOutput() error { 375 | msg := messages.NewUDPMessage(messages.InputMsg) 376 | inputMsg := msg.(*messages.InputPacket) 377 | 378 | var j, offset int 379 | 380 | if u.pendingOutput.Size() > 0 { 381 | last := u.lastAckedInput 382 | // bits = msg.Input.Bits 383 | input, err := u.pendingOutput.Front() 384 | if err != nil { 385 | panic(err) 386 | } 387 | inputMsg.StartFrame = uint32(input.Frame) 388 | inputMsg.InputSize = uint8(input.Size) 389 | 390 | if !(last.Frame == -1 || last.Frame+1 == int(inputMsg.StartFrame)) { 391 | return errors.New("ggpo UdpProtocol SendPendingOutput: !((last.Frame == -1 || last.Frame+1 == int(msg.Input.StartFrame))) ") 392 | } 393 | 394 | for j = 0; j < u.pendingOutput.Size(); j++ { 395 | current, err := u.pendingOutput.Item(j) 396 | inputMsg.Checksum = current.Checksum 397 | if err != nil { 398 | panic(err) 399 | } 400 | inputMsg.Bits = append(inputMsg.Bits, current.Bits...) 401 | last = current // might get rid of this 402 | u.lastSentInput = current 403 | } 404 | } else { 405 | inputMsg.StartFrame = 0 406 | inputMsg.InputSize = 0 407 | } 408 | 409 | inputMsg.AckFrame = int32(u.lastRecievedInput.Frame) 410 | // msg.Input.NumBits = uint16(offset) caused major bug, numbits would always be 0 411 | // causing input events to never be recieved 412 | inputMsg.NumBits = uint16(inputMsg.InputSize) 413 | 414 | inputMsg.DisconectRequested = u.currentState == DisconnectedState 415 | 416 | if u.localConnectStatus != nil { 417 | inputMsg.PeerConnectStatus = make([]messages.UdpConnectStatus, len(*u.localConnectStatus)) 418 | copy(inputMsg.PeerConnectStatus, *u.localConnectStatus) 419 | } else { 420 | inputMsg.PeerConnectStatus = make([]messages.UdpConnectStatus, messages.UDPMsgMaxPlayers) 421 | } 422 | 423 | // may not even need this. 424 | if offset >= messages.MaxCompressedBits { 425 | return errors.New("ggpo UdpProtocol SendPendingOutput: offset >= MaxCompressedBits") 426 | } 427 | 428 | u.SendMsg(inputMsg) 429 | return nil 430 | } 431 | 432 | func (u *UdpProtocol) SendInputAck() { 433 | msg := messages.NewUDPMessage(messages.InputAckMsg) 434 | inputAck := msg.(*messages.InputAckPacket) 435 | inputAck.AckFrame = int32(u.lastRecievedInput.Frame) 436 | u.SendMsg(inputAck) 437 | } 438 | 439 | func (u *UdpProtocol) GetEvent() (*UdpProtocolEvent, error) { 440 | if u.eventQueue.Size() == 0 { 441 | return nil, errors.New("ggpo UdpProtocol GetEvent:no events") 442 | } 443 | e, err := u.eventQueue.Front() 444 | if err != nil { 445 | panic(err) 446 | } 447 | err = u.eventQueue.Pop() 448 | if err != nil { 449 | panic(err) 450 | } 451 | return &e, nil 452 | } 453 | 454 | func (u *UdpProtocol) QueueEvent(evt *UdpProtocolEvent) { 455 | util.Log.Printf("Queueing event %s", *evt) 456 | err := u.eventQueue.Push(*evt) 457 | // if there's no more room left in the queue, make room. 458 | if err != nil { 459 | //u.eventQueue.Pop() 460 | //u.eventQueue.Push(*evt) 461 | panic(err) 462 | } 463 | } 464 | 465 | func (u *UdpProtocol) Disconnect() { 466 | u.currentState = DisconnectedState 467 | u.shutdownTimeout = time.Now().UnixMilli() + UDPShutdownTimer 468 | } 469 | 470 | func (u *UdpProtocol) SendSyncRequest() { 471 | u.state.random = uint32(rand.Int() & 0xFFFF) 472 | msg := messages.NewUDPMessage(messages.SyncRequestMsg) 473 | syncRequest := msg.(*messages.SyncRequestPacket) 474 | syncRequest.RandomRequest = u.state.random 475 | syncRequest.RemoteInputDelay = uint8(u.timesync.FrameDelay2) 476 | u.SendMsg(syncRequest) 477 | } 478 | 479 | func (u *UdpProtocol) SendMsg(msg messages.UDPMessage) { 480 | util.Log.Printf("In UdpProtocol send %s", msg) 481 | u.packetsSent++ 482 | u.lastSendTime = time.Now().UnixMilli() 483 | u.bytesSent += msg.PacketSize() 484 | msg.SetHeader(u.magicNumber, u.nextSendSeq) 485 | u.nextSendSeq++ 486 | if u.peerAddress == "" { 487 | panic("peerAdress empty, why?") 488 | } 489 | var err error 490 | err = u.sendQueue.Push(NewQueEntry( 491 | time.Now().UnixMilli(), u.peerAddress, u.peerPort, msg)) 492 | if err != nil { 493 | panic(err) 494 | } 495 | 496 | err = u.PumpSendQueue() 497 | if err != nil { 498 | panic(err) 499 | } 500 | } 501 | 502 | func (u *UdpProtocol) OnInput(msg messages.UDPMessage, length int) (bool, error) { 503 | inputMessage := msg.(*messages.InputPacket) 504 | 505 | // If a disconnect is requested, go ahead and disconnect now. 506 | disconnectRequested := inputMessage.DisconectRequested 507 | if disconnectRequested { 508 | if u.currentState != DisconnectedState && !u.disconnectEventSent { 509 | util.Log.Printf("Disconnecting endpoint on remote request.\n") 510 | u.QueueEvent(&UdpProtocolEvent{ 511 | eventType: DisconnectedEvent, 512 | }) 513 | u.disconnectEventSent = true 514 | } 515 | } else { 516 | // update the peer connection status if this peer is still considered to be part 517 | // of the network 518 | remoteStatus := inputMessage.PeerConnectStatus 519 | for i := 0; i < len(u.peerConnectStatus); i++ { 520 | if remoteStatus[i].LastFrame < u.peerConnectStatus[i].LastFrame { 521 | return false, errors.New("ggpo UdpProtocol OnInput: remoteStatus[i].LastFrame < u.peerConnectStatus[i].LastFrame") 522 | } 523 | u.peerConnectStatus[i].Disconnected = u.peerConnectStatus[i].Disconnected || remoteStatus[i].Disconnected 524 | u.peerConnectStatus[i].LastFrame = util.Max(u.peerConnectStatus[i].LastFrame, remoteStatus[i].LastFrame) 525 | } 526 | } 527 | 528 | // Decompress the input (gob should already have done this) 529 | lastRecievedFrameNumber := u.lastRecievedInput.Frame 530 | 531 | currentFrame := inputMessage.StartFrame 532 | 533 | u.lastRecievedInput.Size = int(inputMessage.InputSize) 534 | if u.lastRecievedInput.Frame < 0 { 535 | u.lastRecievedInput.Frame = int(inputMessage.StartFrame) - 1 536 | } 537 | 538 | offset := 0 539 | 540 | for offset < len(inputMessage.Bits) { 541 | if currentFrame > uint32(u.lastRecievedInput.Frame+1) { 542 | return false, errors.New("ggpo UdpProtocol OnInput: currentFrame > uint32(u.lastRecievedInput.Frame + 1)") 543 | } 544 | useInputs := currentFrame == uint32(u.lastRecievedInput.Frame+1) 545 | if useInputs { 546 | if currentFrame != uint32(u.lastRecievedInput.Frame)+1 { 547 | return false, errors.New("ggpo UdpProtocol OnInput: currentFrame != uint32(u.lastRecievedInput.Frame) +1") 548 | } 549 | u.lastRecievedInput.Bits = inputMessage.Bits[offset : offset+int(inputMessage.InputSize)] 550 | u.lastRecievedInput.Frame = int(currentFrame) 551 | u.lastRecievedInput.Checksum = inputMessage.Checksum 552 | evt := UdpProtocolEvent{ 553 | eventType: InputEvent, 554 | Input: u.lastRecievedInput, 555 | } 556 | u.state.lastInputPacketRecvTime = time.Now().UnixMilli() 557 | util.Log.Printf("Sending frame %d to emu queue %d.\n", u.lastRecievedInput.Frame, u.queue) 558 | u.QueueEvent(&evt) 559 | u.SendInputAck() 560 | } else { 561 | util.Log.Printf("Skipping past frame:(%d) current is %d.\n", currentFrame, u.lastRecievedInput.Frame) 562 | 563 | } 564 | offset += int(inputMessage.InputSize) 565 | currentFrame++ 566 | } 567 | 568 | if u.lastRecievedInput.Frame < lastRecievedFrameNumber { 569 | return false, errors.New("ggpo UdpProtocol OnInput: u.lastRecievedInput.Frame < lastRecievedFrameNumber") 570 | } 571 | 572 | // Get rid of our buffered input 573 | for u.pendingOutput.Size() > 0 { 574 | input, err := u.pendingOutput.Front() 575 | if err != nil { 576 | panic(err) 577 | } 578 | if int32(input.Frame) < inputMessage.AckFrame { 579 | util.Log.Printf("Throwing away pending output frame %d\n", input.Frame) 580 | u.lastAckedInput = input 581 | err := u.pendingOutput.Pop() 582 | if err != nil { 583 | panic(err) 584 | } 585 | } else { 586 | break 587 | } 588 | } 589 | return true, nil 590 | } 591 | 592 | func (u *UdpProtocol) OnInputAck(msg messages.UDPMessage, len int) (bool, error) { 593 | inputAck := msg.(*messages.InputAckPacket) 594 | // Get rid of our buffered input 595 | for u.pendingOutput.Size() > 0 { 596 | input, err := u.pendingOutput.Front() 597 | if err != nil { 598 | panic(err) 599 | } 600 | if int32(input.Frame) < inputAck.AckFrame { 601 | util.Log.Printf("Throwing away pending output frame %d\n", input.Frame) 602 | u.lastAckedInput = input 603 | err = u.pendingOutput.Pop() 604 | if err != nil { 605 | panic(err) 606 | } 607 | } else { 608 | break 609 | } 610 | } 611 | return true, nil 612 | } 613 | 614 | func (u *UdpProtocol) OnQualityReport(msg messages.UDPMessage, len int) (bool, error) { 615 | qualityReport := msg.(*messages.QualityReportPacket) 616 | reply := messages.NewUDPMessage(messages.QualityReplyMsg) 617 | replyPacket := reply.(*messages.QualityReplyPacket) 618 | replyPacket.Pong = qualityReport.Ping 619 | u.SendMsg(replyPacket) 620 | 621 | u.remoteFrameAdvantage = float32(qualityReport.FrameAdvantage) / 10.0 622 | return true, nil 623 | } 624 | 625 | func (u *UdpProtocol) OnQualityReply(msg messages.UDPMessage, len int) (bool, error) { 626 | qualityReply := msg.(*messages.QualityReplyPacket) 627 | u.roundTripTime = time.Now().UnixMilli() - int64(qualityReply.Pong) 628 | return true, nil 629 | } 630 | 631 | func (u *UdpProtocol) OnKeepAlive(msg messages.UDPMessage, len int) (bool, error) { 632 | return true, nil 633 | } 634 | 635 | func (u *UdpProtocol) GetNetworkStats() NetworkStats { 636 | s := NetworkStats{} 637 | s.Network.Ping = u.roundTripTime 638 | s.Network.SendQueueLen = u.pendingOutput.Size() 639 | s.Network.KbpsSent = u.kbpsSent 640 | s.Timesync.RemoteFramesBehind = u.timesync.RemoteAdvantage() 641 | s.Timesync.LocalFramesBehind = u.timesync.LocalAdvantage() 642 | s.Timesync.AvgLocalFramesBehind = u.timesync.AvgLocalAdvantageSinceStart() 643 | s.Timesync.AvgRemoteFramesBehind = u.timesync.AvgRemoteAdvantageSinceStart() 644 | return s 645 | } 646 | 647 | func (u *UdpProtocol) SetLocalFrameNumber(localFrame int) { 648 | remoteFrame := float32(int64(u.lastRecievedInput.Frame) + (u.roundTripTime * 60.0 / 2000.0)) 649 | u.localFrameAdvantage = ((remoteFrame - float32(localFrame)) - float32(u.timesync.FrameDelay2)) 650 | } 651 | 652 | func (u *UdpProtocol) RecommendFrameDelay() float32 { 653 | return u.timesync.ReccomendFrameWaitDuration(false) 654 | } 655 | 656 | func (u *UdpProtocol) SetDisconnectTimeout(timeout int) { 657 | u.disconnectTimeout = int64(timeout) 658 | } 659 | 660 | func (u *UdpProtocol) SetDisconnectNotifyStart(timeout int) { 661 | u.disconnectNotifyStart = int64(timeout) 662 | } 663 | 664 | func (u *UdpProtocol) PumpSendQueue() error { 665 | var entry QueueEntry 666 | var err error 667 | 668 | for !u.sendQueue.Empty() { 669 | entry, err = u.sendQueue.Front() 670 | if err != nil { 671 | panic(err) 672 | } 673 | 674 | if u.sendLatency > 0 { 675 | jitter := (u.sendLatency * 2 / 3) + ((rand.Int63() % u.sendLatency) / 3) 676 | if time.Now().UnixMilli() < entry.queueTime+jitter { 677 | break 678 | } 679 | } 680 | if u.ooPercent > 0 && u.ooPacket.msg == nil && ((rand.Int() % 100) < u.ooPercent) { 681 | delay := rand.Int63() % (u.sendLatency*10 + 1000) 682 | util.Log.Printf("creating rogue oop (seq: %d delay: %d)\n", 683 | entry.msg.Header().SequenceNumber, delay) 684 | u.ooPacket.sendTime = time.Now().UnixMilli() + delay 685 | u.ooPacket.msg = entry.msg 686 | u.ooPacket.destIp = entry.destIp 687 | } else { 688 | if entry.destIp == "" { 689 | return errors.New("ggpo UdpProtocol PumpSendQueue: entry.destIp == \"\"") 690 | } 691 | u.connection.SendTo(entry.msg, entry.destIp, entry.destPort) 692 | // would delete the udpmsg here 693 | } 694 | err := u.sendQueue.Pop() 695 | if err != nil { 696 | panic(err) 697 | } 698 | } 699 | if u.ooPacket.msg != nil && u.ooPacket.sendTime < time.Now().UnixMilli() { 700 | util.Log.Printf("sending rogue oop!") 701 | u.connection.SendTo(u.ooPacket.msg, u.peerAddress, u.peerPort) 702 | u.ooPacket.msg = nil 703 | } 704 | return nil 705 | } 706 | 707 | func (u *UdpProtocol) ClearSendQueue() { 708 | for !u.sendQueue.Empty() { 709 | // i'd manually delete the QueueEntry in a language where I could 710 | err := u.sendQueue.Pop() 711 | if err != nil { 712 | panic(err) 713 | } 714 | } 715 | } 716 | 717 | // going to call deletes close 718 | func (u *UdpProtocol) Close() { 719 | u.ClearSendQueue() 720 | if u.connection != nil { 721 | u.connection.Close() 722 | } 723 | } 724 | 725 | func (u *UdpProtocol) HandlesMsg(ipAddress string, port int) bool { 726 | if u.connection == nil { 727 | return false 728 | } 729 | return u.peerAddress == ipAddress && u.peerPort == port 730 | } 731 | 732 | func (u *UdpProtocol) SendInput(input *input.GameInput) { 733 | if u.connection != nil { 734 | if u.currentState == RunningState { 735 | // check to see if this is a good time to adjust for the rift 736 | u.timesync.AdvanceFrames(input, u.localFrameAdvantage, u.remoteFrameAdvantage) 737 | 738 | // Save this input packet. 739 | err := u.pendingOutput.Push(*input) 740 | // if for whatever reason the capacity is full, pop off the end of the buffer and try again 741 | if err != nil { 742 | //u.pendingOutput.Pop() 743 | //u.pendingOutput.Push(*input) 744 | panic(err) 745 | } 746 | } 747 | err := u.SendPendingOutput() 748 | if err != nil { 749 | panic(err) 750 | } 751 | } 752 | } 753 | 754 | func (u *UdpProtocol) UpdateNetworkStats() { 755 | now := time.Now().UnixMilli() 756 | if u.statsStartTime == 0 { 757 | u.statsStartTime = now 758 | } 759 | 760 | totalBytesSent := u.bytesSent + (UDPHeaderSize * u.packetsSent) 761 | seconds := float64(now-u.statsStartTime) / 1000.0 762 | bps := float64(totalBytesSent) / seconds 763 | udpOverhead := float64(100.0 * (float64(UDPHeaderSize * u.packetsSent)) / float64(u.bytesSent)) 764 | u.kbpsSent = int(bps / 1024) 765 | 766 | util.Log.Printf("Network Stats -- Bandwidth: %.2f KBps Packets Sent: %5d (%.2f pps) KB Sent: %.2f UDP Overhead: %.2f %%.\n", 767 | float64(u.kbpsSent), 768 | u.packetsSent, 769 | float64(u.packetsSent*1000)/float64(now-u.statsStartTime), 770 | float64(totalBytesSent/1024.0), 771 | udpOverhead) 772 | } 773 | 774 | func (u *UdpProtocol) Synchronize() { 775 | if u.connection != nil { 776 | u.currentState = SyncingState 777 | u.state.roundTripRemaining = uint32(NumSyncPackets) 778 | u.SendSyncRequest() 779 | } 780 | } 781 | 782 | func (u *UdpProtocol) GetPeerConnectStatus(id int, frame *int32) bool { 783 | *frame = u.peerConnectStatus[id].LastFrame 784 | // !u.peerConnectStatus[id].Disconnected from the C/++ world 785 | return !u.peerConnectStatus[id].Disconnected 786 | } 787 | 788 | func (u *UdpProtocol) OnInvalid(msg messages.UDPMessage, len int) (bool, error) { 789 | // Assert(false) // ? ASSERT(FALSE && "Invalid msg in UdpProtocol"); 790 | // ah 791 | util.Log.Printf("Invalid msg in UdpProtocol ") 792 | return false, errors.New("ggpo UdpProtocol OnInvalid: invalid msg in UdpProtocol") 793 | } 794 | 795 | func (u *UdpProtocol) OnSyncRequest(msg messages.UDPMessage, len int) (bool, error) { 796 | request := msg.(*messages.SyncRequestPacket) 797 | reply := messages.NewUDPMessage(messages.SyncReplyMsg) 798 | syncReply := reply.(*messages.SyncReplyPacket) 799 | syncReply.RandomReply = request.RandomRequest 800 | u.timesync.RemoteFrameDelay = int(request.RemoteInputDelay) 801 | u.SendMsg(syncReply) 802 | return true, nil 803 | } 804 | 805 | func (u *UdpProtocol) OnMsg(msg messages.UDPMessage, length int) { 806 | handled := false 807 | var err error 808 | type UdpProtocolDispatchFunc func(msg messages.UDPMessage, length int) (bool, error) 809 | 810 | table := []UdpProtocolDispatchFunc{ 811 | u.OnInvalid, 812 | u.OnSyncRequest, 813 | u.OnSyncReply, 814 | u.OnInput, 815 | u.OnQualityReport, 816 | u.OnQualityReply, 817 | u.OnKeepAlive, 818 | u.OnInputAck} 819 | 820 | // filter out messages that don't match what we expect 821 | seq := msg.Header().SequenceNumber 822 | if msg.Header().HeaderType != uint8(messages.SyncRequestMsg) && msg.Header().HeaderType != uint8(messages.SyncReplyMsg) { 823 | if msg.Header().Magic != u.remoteMagicNumber { 824 | util.Log.Printf("recv rejecting %s", msg) 825 | return 826 | } 827 | 828 | // filer out out-of-order packets 829 | skipped := seq - u.nextRecvSeq 830 | if skipped > uint16(MaxSeqDistance) { 831 | util.Log.Printf("dropping out of order packet (seq: %d, last seq:%d)\n", seq, u.nextRecvSeq) 832 | return 833 | } 834 | } 835 | 836 | u.nextRecvSeq = seq 837 | util.Log.Printf("recv %s on queue %d\n", msg, u.queue) 838 | if int(msg.Header().HeaderType) >= len(table) { 839 | u.OnInvalid(msg, length) 840 | } else { 841 | handled, err = table[int(msg.Header().HeaderType)](msg, length) 842 | } 843 | if err != nil { 844 | panic(err) 845 | } 846 | 847 | if handled { 848 | u.lastRecvTime = time.Now().UnixMilli() 849 | if u.disconnectNotifySent && u.currentState == RunningState { 850 | u.QueueEvent( 851 | &UdpProtocolEvent{ 852 | eventType: NetworkResumedEvent, 853 | }) 854 | u.disconnectNotifySent = false 855 | } 856 | } 857 | } 858 | 859 | func (u *UdpProtocol) OnSyncReply(msg messages.UDPMessage, length int) (bool, error) { 860 | syncReply := msg.(*messages.SyncReplyPacket) 861 | if u.currentState != SyncingState { 862 | util.Log.Println("Ignoring SyncReply while not synching.") 863 | return msg.Header().Magic == u.remoteMagicNumber, nil 864 | } 865 | 866 | if syncReply.RandomReply != u.state.random { 867 | util.Log.Printf("sync reply %d != %d. Keep looking...\n", 868 | syncReply.RandomReply, u.state.random) 869 | return false, nil 870 | } 871 | 872 | if !u.connected { 873 | u.QueueEvent(&UdpProtocolEvent{ 874 | eventType: ConnectedEvent}) 875 | u.connected = true 876 | } 877 | 878 | util.Log.Printf("Checking sync state (%d round trips remaining).\n", u.state.roundTripRemaining) 879 | u.state.roundTripRemaining-- 880 | if u.state.roundTripRemaining == 0 { 881 | util.Log.Printf("Synchronized!\n") 882 | u.QueueEvent(&UdpProtocolEvent{ 883 | eventType: SynchronziedEvent, 884 | }) 885 | u.currentState = RunningState 886 | u.lastRecievedInput.Frame = -1 887 | u.remoteMagicNumber = msg.Header().Magic 888 | } else { 889 | evt := UdpProtocolEvent{ 890 | eventType: SynchronizingEvent, 891 | } 892 | evt.Total = NumSyncPackets 893 | evt.Count = NumSyncPackets - int(u.state.roundTripRemaining) 894 | u.QueueEvent(&evt) 895 | u.SendSyncRequest() 896 | } 897 | 898 | return true, nil 899 | } 900 | 901 | func (u *UdpProtocol) IsInitialized() bool { 902 | return u.connection != nil 903 | } 904 | 905 | func (u *UdpProtocol) IsSynchronized() bool { 906 | return u.currentState == RunningState 907 | } 908 | 909 | func (u *UdpProtocol) IsRunning() bool { 910 | return u.currentState == RunningState 911 | } 912 | -------------------------------------------------------------------------------- /internal/sync/timesync.go: -------------------------------------------------------------------------------- 1 | package sync 2 | 3 | import ( 4 | "github.com/assemblaj/ggpo/internal/input" 5 | "github.com/assemblaj/ggpo/internal/util" 6 | ) 7 | 8 | const ( 9 | FrameWindowSize = 120 10 | MinUniqueFrames = 10 11 | MinFrameAdvantage = 3 12 | MaxFrameAdvantage = 9 13 | ) 14 | 15 | type TimeSync struct { 16 | local []float32 17 | remote []float32 18 | lastInputs []*input.GameInput 19 | nextPrediction int 20 | RemoteFrameDelay int 21 | FrameDelay2 int 22 | numFrame int 23 | avgLocal float32 24 | avgRemote float32 25 | clearedInitial bool 26 | } 27 | 28 | func NewTimeSync() TimeSync { 29 | return TimeSync{ 30 | local: make([]float32, FrameWindowSize), 31 | remote: make([]float32, FrameWindowSize), 32 | lastInputs: make([]*input.GameInput, MinUniqueFrames), 33 | nextPrediction: FrameWindowSize * 3, 34 | } 35 | } 36 | 37 | func (t *TimeSync) SetFrameDelay(frame int) { 38 | t.FrameDelay2 = frame 39 | } 40 | 41 | func (t *TimeSync) AdvanceFrames(input *input.GameInput, advantage float32, radvantage float32) { 42 | // Remember the last frame and frame advantage 43 | t.lastInputs[input.Frame%len(t.lastInputs)] = input 44 | t.local[input.Frame%len(t.local)] = advantage 45 | t.remote[input.Frame%len(t.remote)] = radvantage 46 | 47 | t.avgLocal = ((float32(t.numFrame) * t.avgLocal) + advantage) / (float32(t.numFrame) + 1) 48 | t.avgRemote = ((float32(t.numFrame) * t.avgRemote) + radvantage) / (float32(t.numFrame) + 1) 49 | 50 | t.numFrame++ 51 | // Clear after first 3 seconds, as this is a bit crazy 52 | if !t.clearedInitial && t.numFrame == 240 { 53 | t.clearedInitial = true 54 | t.numFrame = 0 55 | } 56 | } 57 | 58 | func (t *TimeSync) LocalAdvantage() float32 { 59 | var i int 60 | var sum float32 61 | var advantage float32 62 | for i = 0; i < len(t.local); i++ { 63 | sum += t.local[i] 64 | } 65 | advantage = float32(sum) / float32(len(t.local)) 66 | return advantage 67 | } 68 | 69 | func (t *TimeSync) RemoteAdvantage() float32 { 70 | var i int 71 | var sum float32 72 | var radvantage float32 73 | for i = 0; i < len(t.remote); i++ { 74 | sum += t.remote[i] 75 | } 76 | radvantage = float32(sum) / float32(len(t.remote)) 77 | return radvantage 78 | } 79 | 80 | func (t *TimeSync) AvgLocalAdvantageSinceStart() float32 { 81 | return t.avgLocal 82 | } 83 | 84 | func (t *TimeSync) AvgRemoteAdvantageSinceStart() float32 { 85 | return t.avgRemote 86 | } 87 | 88 | func (t *TimeSync) ReccomendFrameWaitDuration(requireIdleInput bool) float32 { 89 | // Average our local and remote frame advantages 90 | 91 | advantage := t.LocalAdvantage() 92 | 93 | radvantage := t.RemoteAdvantage() 94 | 95 | // var count int = 0 96 | // count++ 97 | 98 | // See if someone should take action. The person furthest ahead 99 | // needs to slow down so the other user can catch up. 100 | // Only do this if both clients agree on who's ahead!! 101 | // if advantage >= radvantage { 102 | // return 0 103 | // } 104 | 105 | // Both clients agree that we're the one ahead. Split 106 | // the difference between the two to figure out how long to 107 | // sleep for. 108 | sleepFrames := ((radvantage - advantage) / 2.0) 109 | 110 | //util.Log.Printf("iteration %d: sleep frames is %d\n", count, sleepFrames) 111 | util.Log.Printf("In TimeSync: sleep frames is %f\n", sleepFrames) 112 | 113 | // Some things just aren't worth correcting for. Make sure 114 | // the difference is relevant before proceeding. 115 | // if sleepFrames < MinFrameAdvantage { 116 | // return 0 117 | // } 118 | 119 | // Make sure our input had been "idle enough" before recommending 120 | // a sleep. This tries to make the emulator sleep while the 121 | // user's input isn't sweeping in arcs (e.g. fireball motions in 122 | // Street Fighter), which could cause the player to miss moves. 123 | // if requireIdleInput { 124 | // for i = 1; i < len(t.lastInputs); i++ { 125 | // equal, err := t.lastInputs[i].Equal(t.lastInputs[0], true) 126 | // if err != nil { 127 | // panic(err) 128 | // } 129 | // if !equal { 130 | // util.Log.Printf("iteration %d: rejecting due to input stuff at position %d...!!!\n", count, i) 131 | // return 0 132 | // } 133 | // } 134 | // } 135 | 136 | // Success!!! Recommend the number of frames to sleep and adjust 137 | if sleepFrames > 0 { 138 | return float32(util.Min(sleepFrames, MaxFrameAdvantage)) 139 | } else { 140 | return float32(util.Max(sleepFrames, -MaxFrameAdvantage)) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /internal/sync/timesync_test.go: -------------------------------------------------------------------------------- 1 | package sync_test 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "github.com/assemblaj/ggpo/internal/input" 8 | "github.com/assemblaj/ggpo/internal/sync" 9 | ) 10 | 11 | /* 12 | These are all charectization tests for the new rift synchronization now. 13 | */ 14 | var Delta float64 = 0.000001 15 | 16 | func AlmostEqual(a, b float32) bool { 17 | return math.Abs(float64(a-b)) <= Delta 18 | } 19 | 20 | func TestTimeSyncRecommendFrameDuration(t *testing.T) { 21 | ts := sync.NewTimeSync() 22 | frame := 5 23 | bytes := []byte{1, 2, 3, 4} 24 | size := 4 25 | input, _ := input.NewGameInput(frame, bytes, size) 26 | ts.AdvanceFrames(&input, 8, 9) 27 | want := 0.004167 28 | got := ts.ReccomendFrameWaitDuration(false) 29 | if !AlmostEqual(float32(want), got) { 30 | t.Errorf("expected '%f' but got '%f'", want, got) 31 | } 32 | } 33 | func TestTimeSyncRecommendFrameDurationIdleInput(t *testing.T) { 34 | ts := sync.NewTimeSync() 35 | frame := 5 36 | bytes := []byte{1, 2, 3, 4} 37 | size := 4 38 | input, _ := input.NewGameInput(frame, bytes, size) 39 | ts.AdvanceFrames(&input, 8, 9) 40 | want := 0.004167 41 | got := ts.ReccomendFrameWaitDuration(true) 42 | if !AlmostEqual(float32(want), got) { 43 | t.Errorf("expected '%f' but got '%f'", want, got) 44 | } 45 | } 46 | 47 | func TestTimeSyncHighLocalFrameAdvantage(t *testing.T) { 48 | ts := sync.NewTimeSync() 49 | frame := 0 50 | bytes := []byte{1, 2, 3, 4} 51 | size := 4 52 | input, _ := input.NewGameInput(frame, bytes, size) 53 | ts.AdvanceFrames(&input, 9, 800) 54 | want := 3.295833 55 | got := ts.ReccomendFrameWaitDuration(false) 56 | if !AlmostEqual(float32(want), got) { 57 | t.Errorf("expected '%f' but got '%f'", want, got) 58 | } 59 | 60 | } 61 | func TestTimeSyncHighLocalFrameAdvantageRequireIdleInput(t *testing.T) { 62 | ts := sync.NewTimeSync() 63 | frameCount := 20 64 | for i := 0; i < frameCount; i++ { 65 | frame := i 66 | bytes := []byte{1, 2, 3, 4} 67 | size := 4 68 | input, _ := input.NewGameInput(frame, bytes, size) 69 | ts.AdvanceFrames(&input, 9, 800) 70 | } 71 | want := 9.0 72 | got := ts.ReccomendFrameWaitDuration(true) 73 | if !AlmostEqual(float32(want), got) { 74 | t.Errorf("expected '%f' but got '%f'", want, got) 75 | } 76 | } 77 | 78 | func TestTimeSyncHighRemoteFrameAdvantage(t *testing.T) { 79 | ts := sync.NewTimeSync() 80 | frame := 0 81 | bytes := []byte{1, 2, 3, 4} 82 | size := 4 83 | input, _ := input.NewGameInput(frame, bytes, size) 84 | ts.AdvanceFrames(&input, 800, 9) 85 | want := float32(-3.295833) 86 | got := ts.ReccomendFrameWaitDuration(false) 87 | if !AlmostEqual(float32(want), got) { 88 | t.Errorf("expected '%f' but got '%f'", want, got) 89 | } 90 | 91 | } 92 | 93 | func TestTimeSyncNoFrameAdvantage(t *testing.T) { 94 | ts := sync.NewTimeSync() 95 | frame := 0 96 | bytes := []byte{1, 2, 3, 4} 97 | size := 4 98 | input, _ := input.NewGameInput(frame, bytes, size) 99 | ts.AdvanceFrames(&input, 0, 0) 100 | want := 0.0 101 | got := ts.ReccomendFrameWaitDuration(false) 102 | if !AlmostEqual(float32(want), got) { 103 | t.Errorf("expected '%f' but got '%f'", want, got) 104 | } 105 | } 106 | 107 | func TestTimeSyncNegativeLocalFrameAdvantage(t *testing.T) { 108 | ts := sync.NewTimeSync() 109 | frame := 0 110 | bytes := []byte{1, 2, 3, 4} 111 | size := 4 112 | input, _ := input.NewGameInput(frame, bytes, size) 113 | ts.AdvanceFrames(&input, -1, 9) 114 | want := 0.041667 115 | got := ts.ReccomendFrameWaitDuration(false) 116 | if !AlmostEqual(float32(want), got) { 117 | t.Errorf("expected '%f' but got '%f'", want, got) 118 | } 119 | } 120 | func TestTimeSyncBothNegativeFrameAdvantage(t *testing.T) { 121 | ts := sync.NewTimeSync() 122 | frame := 0 123 | bytes := []byte{1, 2, 3, 4} 124 | size := 4 125 | input, _ := input.NewGameInput(frame, bytes, size) 126 | ts.AdvanceFrames(&input, -2000, -2000) 127 | want := 0.0 128 | got := ts.ReccomendFrameWaitDuration(false) 129 | if !AlmostEqual(float32(want), got) { 130 | t.Errorf("expected '%f' but got '%f'", want, got) 131 | } 132 | } 133 | 134 | func TestTimeSyncAdvanceFramesAndAdvantage(t *testing.T) { 135 | ts := sync.NewTimeSync() 136 | totalFrames := 20 137 | for i := 0; i < totalFrames; i++ { 138 | frame := i 139 | bytes := []byte{1, 2, 3, 4} 140 | size := 4 141 | input, _ := input.NewGameInput(frame, bytes, size) 142 | ts.AdvanceFrames(&input, 0, float32(i)) 143 | } 144 | want := 0.791667 145 | got := ts.ReccomendFrameWaitDuration(false) 146 | if !AlmostEqual(float32(want), got) { 147 | t.Errorf("expected '%f' but got '%f'", want, got) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /internal/util/ordered_map.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "sort" 5 | 6 | "golang.org/x/exp/constraints" 7 | ) 8 | 9 | type OrderedMap[K constraints.Ordered, V any] struct { 10 | keys []K 11 | m map[K]V 12 | } 13 | 14 | type KeyValuePair[K constraints.Ordered, V any] struct { 15 | Key K 16 | Value V 17 | } 18 | 19 | func NewOrderedMap[K constraints.Ordered, V any](capacity int) OrderedMap[K, V] { 20 | return OrderedMap[K, V]{ 21 | keys: make([]K, 0, capacity), 22 | m: make(map[K]V), 23 | } 24 | } 25 | 26 | func (om *OrderedMap[K, V]) Set(key K, value V) { 27 | // Add the key to the slice if it doesn't already exist 28 | if _, ok := om.m[key]; !ok { 29 | om.keys = append(om.keys, key) 30 | } 31 | om.m[key] = value 32 | } 33 | 34 | func (om *OrderedMap[K, V]) Get(key K) (V, bool) { 35 | v, ok := om.m[key] 36 | return v, ok 37 | } 38 | 39 | func (om *OrderedMap[K, V]) Delete(key K) { 40 | delete(om.m, key) 41 | // Find the index of the key in the slice and remove it 42 | for i, k := range om.keys { 43 | if k == key { 44 | om.keys = append(om.keys[:i], om.keys[i+1:]...) 45 | break 46 | } 47 | } 48 | } 49 | 50 | func (om *OrderedMap[K, V]) Len() int { 51 | return len(om.m) 52 | } 53 | 54 | func (om *OrderedMap[K, V]) Keys() []K { 55 | return om.keys 56 | } 57 | 58 | func (om *OrderedMap[K, V]) Greatest() KeyValuePair[K, V] { 59 | sort.Slice(om.keys, func(i, j int) bool { 60 | return om.keys[i] > om.keys[j] 61 | }) 62 | 63 | return KeyValuePair[K, V]{ 64 | Key: om.keys[0], 65 | Value: om.m[om.keys[0]], 66 | } 67 | } 68 | 69 | func (om *OrderedMap[K, V]) Clear() { 70 | om.keys = om.keys[:0] 71 | for k := range om.m { 72 | delete(om.m, k) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /internal/util/ordered_map_test.go: -------------------------------------------------------------------------------- 1 | package util_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/assemblaj/ggpo/internal/util" 7 | ) 8 | 9 | func TestOrderedMapLenZero(t *testing.T) { 10 | om := util.NewOrderedMap[int, uint32](0) 11 | omSize := om.Len() 12 | if omSize != 0 { 13 | t.Errorf("Expected Size 0, got %d", omSize) 14 | } 15 | 16 | } 17 | func TestOrderedMapSet(t *testing.T) { 18 | om := util.NewOrderedMap[int, uint32](2) 19 | key := 8 20 | var value uint32 = 5555 21 | om.Set(key, value) 22 | if om.Len() == 0 { 23 | t.Errorf("Expected to have value, instead was empty") 24 | } 25 | want := value 26 | got, ok := om.Get(key) 27 | if !ok { 28 | t.Errorf("Expected value for key %d, value not available", key) 29 | } 30 | 31 | if want != got { 32 | t.Errorf("Wanted %d got %d", want, got) 33 | } 34 | } 35 | 36 | func TestOrderedMapDelete(t *testing.T) { 37 | om := util.NewOrderedMap[int, uint32](2) 38 | key := 8 39 | var value uint32 = 5555 40 | om.Set(key, value) 41 | om.Delete(key) 42 | if om.Len() != 0 { 43 | t.Errorf("Expected to be empty, instead not empty") 44 | } 45 | if len(om.Keys()) != 0 { 46 | t.Errorf("Expected keys to be empty, instead not empty") 47 | } 48 | } 49 | 50 | func TestOrderedMapClear(t *testing.T) { 51 | om := util.NewOrderedMap[int, uint32](3) 52 | key1, key2 := 898, 343 53 | var value1, value2 uint32 = 32423423, 234234234 54 | om.Set(key1, value1) 55 | om.Set(key2, value2) 56 | om.Clear() 57 | if om.Len() != 0 { 58 | t.Errorf("Expected to be empty, instead not empty") 59 | } 60 | if len(om.Keys()) != 0 { 61 | t.Errorf("Expected keys to be empty, instead not empty") 62 | } 63 | } 64 | 65 | func TestOrderedMapGreatest(t *testing.T) { 66 | om := util.NewOrderedMap[int, uint32](3) 67 | iter := 20 68 | for i := 0; i <= iter; i++ { 69 | om.Set(i, uint32(i*100)) 70 | } 71 | kv := om.Greatest() 72 | if kv.Key != iter { 73 | t.Errorf("Expected greatest key to be %d, insteat %d", iter, kv.Key) 74 | } 75 | if kv.Value != uint32(iter*100) { 76 | t.Errorf("Expected value for greatest key to be %d, was instead %d", iter*100, kv.Value) 77 | } 78 | } 79 | 80 | func TestOrderedMapGet(t *testing.T) { 81 | om := util.NewOrderedMap[int, uint32](3) 82 | key1, key2 := 898, 343 83 | var value1, value2 uint32 = 32423423, 234234234 84 | om.Set(key1, value1) 85 | om.Set(key2, value2) 86 | v1, ok := om.Get(key1) 87 | if !ok { 88 | t.Errorf("Value should be avaiable but not avaiable.") 89 | } 90 | if v1 != value1 { 91 | t.Errorf("Expected %d, got %d ", v1, value1) 92 | } 93 | v2, ok := om.Get(key2) 94 | if !ok { 95 | t.Errorf("Value should be avaiable but not avaiable.") 96 | } 97 | if v2 != value2 { 98 | t.Errorf("Expected %d, got %d ", v1, value1) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /internal/util/utils.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "math" 7 | 8 | "golang.org/x/exp/constraints" 9 | ) 10 | 11 | // Log is the util.Log.Logger used when printing log messages. 12 | var Log = DiscardLogger() 13 | 14 | // DiscardLogger returns a util.Log.Logger which does not print anything. 15 | func DiscardLogger() *log.Logger { 16 | return log.New(io.Discard, "", 0) 17 | } 18 | 19 | func Min[T constraints.Ordered](a, b T) T { 20 | if a < b { 21 | return a 22 | } 23 | return b 24 | } 25 | 26 | func Max[T constraints.Ordered](a, b T) T { 27 | if a > b { 28 | return a 29 | } 30 | return b 31 | } 32 | 33 | func MaxAbsFloat32(a, b float32) float32 { 34 | if math.Abs(float64(a)) > math.Abs(float64(b)) { 35 | return a 36 | } else { 37 | return b 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package ggpo 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/assemblaj/ggpo/internal/util" 8 | ) 9 | 10 | // SetLogger sets the log.Logger used when printing log messages. Use this 11 | // method for more precise control over the logging output and format. Use 12 | // EnableLogs and DisableLogs for simple logging to os.Stdout. Log messages 13 | // are not printed by default. 14 | func SetLogger(l *log.Logger) { 15 | util.Log = l 16 | } 17 | 18 | // EnableLogs enables printing log messages to os.Stdout. Use SetLogger for 19 | // more precise control over the logging output and format. Log messages are 20 | // not printed by default. 21 | func EnableLogs() { 22 | SetLogger(log.New(os.Stdout, "GGPO ", log.Ldate|log.Ltime|log.Lmsgprefix)) 23 | } 24 | 25 | // DisableLogs disables printing log messages. 26 | func DisableLogs() { 27 | SetLogger(util.DiscardLogger()) 28 | } 29 | -------------------------------------------------------------------------------- /player.go: -------------------------------------------------------------------------------- 1 | package ggpo 2 | 3 | type PlayerHandle int 4 | 5 | type PlayerType int 6 | 7 | const ( 8 | PlayerTypeLocal PlayerType = iota 9 | PlayerTypeRemote 10 | PlayerTypeSpectator 11 | ) 12 | 13 | const InvalidHandle int = -1 14 | 15 | type Player struct { 16 | Size int 17 | PlayerType PlayerType 18 | PlayerNum int 19 | Remote RemotePlayer 20 | } 21 | 22 | func NewLocalPlayer(size int, playerNum int) Player { 23 | return Player{ 24 | Size: size, 25 | PlayerNum: playerNum, 26 | PlayerType: PlayerTypeLocal} 27 | } 28 | 29 | func NewRemotePlayer(size int, playerNum int, ipAdress string, port int) Player { 30 | return Player{ 31 | Size: size, 32 | PlayerNum: playerNum, 33 | PlayerType: PlayerTypeRemote, 34 | Remote: RemotePlayer{ 35 | IpAdress: ipAdress, 36 | Port: port}, 37 | } 38 | } 39 | func NewSpectatorPlayer(size int, ipAdress string, port int) Player { 40 | return Player{ 41 | Size: size, 42 | PlayerType: PlayerTypeSpectator, 43 | Remote: RemotePlayer{ 44 | IpAdress: ipAdress, 45 | Port: port}, 46 | } 47 | } 48 | 49 | type RemotePlayer struct { 50 | IpAdress string 51 | Port int 52 | } 53 | 54 | type LocalEndpoint struct { 55 | playerNum int 56 | } 57 | -------------------------------------------------------------------------------- /session.go: -------------------------------------------------------------------------------- 1 | package ggpo 2 | 3 | type Session interface { 4 | SaveGameState(stateID int) int 5 | LoadGameState(stateID int) 6 | AdvanceFrame(flags int) 7 | OnEvent(info *Event) 8 | //SetBackend(backend Backend) 9 | } 10 | 11 | const DefaultChecksum = 0 12 | 13 | type SessionCallbacks struct { 14 | BeginGame beginGame 15 | SaveGameState saveGameState 16 | LoadGameState loadGameState 17 | LogGameState logGameState 18 | FreeBuffer freeBuffer 19 | AdvanceFrame advanceFrame 20 | OnEvent onEvent 21 | } 22 | 23 | type beginGame func(game string) bool 24 | 25 | /* 26 | type saveGameState func(len *int, checksum *int, frame int) ([]byte, bool) 27 | type loadGameState func(buffer []byte, len int) bool 28 | */ 29 | type saveGameState func(stateID int) ([]byte, bool) 30 | type loadGameState func(stateID int) bool 31 | 32 | type logGameState func(fileName string, buffer []byte, len int) bool 33 | type freeBuffer func(buffer []byte) 34 | type advanceFrame func(flags int) bool 35 | type onEvent func(info *Event) bool 36 | -------------------------------------------------------------------------------- /spectator.go: -------------------------------------------------------------------------------- 1 | package ggpo 2 | 3 | import ( 4 | "github.com/assemblaj/ggpo/internal/input" 5 | "github.com/assemblaj/ggpo/internal/messages" 6 | "github.com/assemblaj/ggpo/internal/polling" 7 | "github.com/assemblaj/ggpo/internal/protocol" 8 | "github.com/assemblaj/ggpo/internal/util" 9 | "github.com/assemblaj/ggpo/transport" 10 | ) 11 | 12 | const SpectatorFrameBufferSize int = 32 13 | const DefaultMaxFramesBehind int = 10 14 | const DefaultCatchupSpeed int = 1 15 | 16 | type Spectator struct { 17 | session Session 18 | poll polling.Poller 19 | connection transport.Connection 20 | host protocol.UdpProtocol 21 | synchonizing bool 22 | inputSize int 23 | numPlayers int 24 | nextInputToSend int 25 | inputs []input.GameInput 26 | hostIp string 27 | hostPort int 28 | framesBehind int 29 | localPort int 30 | currentFrame int 31 | messageChannel chan transport.MessageChannelItem 32 | } 33 | 34 | func NewSpectator(cb Session, localPort int, numPlayers int, inputSize int, hostIp string, hostPort int) Spectator { 35 | s := Spectator{} 36 | s.numPlayers = numPlayers 37 | s.inputSize = inputSize 38 | s.nextInputToSend = 0 39 | 40 | s.session = cb 41 | s.synchonizing = true 42 | 43 | inputs := make([]input.GameInput, SpectatorFrameBufferSize) 44 | for _, i := range inputs { 45 | i.Frame = -1 46 | } 47 | s.inputs = inputs 48 | //port := strconv.Itoa(hostPort) 49 | //s.udp = NewUdp(&s, localPort) 50 | s.hostIp = hostIp 51 | s.hostPort = hostPort 52 | s.localPort = localPort 53 | var poll polling.Poll = polling.NewPoll() 54 | s.poll = &poll 55 | s.messageChannel = make(chan transport.MessageChannelItem, 200) 56 | //go s.udp.Read() 57 | return s 58 | } 59 | 60 | func (s *Spectator) Idle(timeout int, timeFunc ...polling.FuncTimeType) error { 61 | s.HandleMessages() 62 | if len(timeFunc) == 0 { 63 | s.poll.Pump() 64 | } else { 65 | s.poll.Pump(timeFunc[0]) 66 | } 67 | s.PollUdpProtocolEvents() 68 | 69 | if s.framesBehind > 0 { 70 | for s.nextInputToSend < s.currentFrame { 71 | s.session.AdvanceFrame(0) 72 | util.Log.Printf("In Spectator: skipping frame %d\n", s.nextInputToSend) 73 | s.nextInputToSend++ 74 | } 75 | s.framesBehind = 0 76 | } 77 | 78 | return nil 79 | } 80 | 81 | func (s *Spectator) SyncInput(disconnectFlags *int) ([][]byte, error) { 82 | // Wait until we've started to return inputs 83 | if s.synchonizing { 84 | return nil, Error{Code: ErrorCodeNotSynchronized, Name: "ErrorCodeNotSynchronized"} 85 | } 86 | 87 | input := s.inputs[s.nextInputToSend%SpectatorFrameBufferSize] 88 | s.currentFrame = input.Frame 89 | if input.Frame < s.nextInputToSend { 90 | // Haved recieved input from the host yet. Wait 91 | return nil, Error{Code: ErrorCodePredictionThreshod, Name: "ErrorCodePredictionThreshod"} 92 | 93 | } 94 | if input.Frame > s.nextInputToSend { 95 | s.framesBehind = input.Frame - s.nextInputToSend 96 | // The host is way way way far ahead of the spetator. How'd this 97 | // happen? Any, the input we need is gone forever. 98 | return nil, Error{Code: ErrorCodeGeneralFailure, Name: "ErrorCodeGeneralFailure"} 99 | } 100 | //s.framesBehind = 0 101 | 102 | //Assert(size >= s.inputSize*s.numPlayers) 103 | values := make([][]byte, s.numPlayers) 104 | offset := 0 105 | counter := 0 106 | for offset < len(input.Bits) { 107 | values[counter] = input.Bits[offset : s.inputSize+offset] 108 | offset += s.inputSize 109 | counter++ 110 | } 111 | 112 | if disconnectFlags != nil { 113 | *disconnectFlags = 0 // xxx: we should get them from the host! -pond3r 114 | } 115 | s.nextInputToSend++ 116 | return values, nil 117 | } 118 | 119 | func (s *Spectator) AdvanceFrame(checksum uint32) error { 120 | util.Log.Printf("End of frame (%d)...\n", s.nextInputToSend-1) 121 | s.Idle(0) 122 | s.PollUdpProtocolEvents() 123 | 124 | return nil 125 | } 126 | 127 | func (s *Spectator) PollUdpProtocolEvents() { 128 | for { 129 | evt, ok := s.host.GetEvent() 130 | if ok != nil { 131 | break 132 | } else { 133 | s.OnUdpProtocolEvent(evt) 134 | } 135 | } 136 | } 137 | 138 | func (s *Spectator) OnUdpProtocolEvent(evt *protocol.UdpProtocolEvent) { 139 | var info Event 140 | switch evt.Type() { 141 | case protocol.ConnectedEvent: 142 | info.Code = EventCodeConnectedToPeer 143 | info.Player = 0 144 | s.session.OnEvent(&info) 145 | 146 | case protocol.SynchronizingEvent: 147 | info.Code = EventCodeSynchronizingWithPeer 148 | info.Player = 0 149 | info.Count = evt.Count 150 | info.Total = evt.Total 151 | s.session.OnEvent(&info) 152 | 153 | case protocol.SynchronziedEvent: 154 | if s.synchonizing { 155 | info.Code = EventCodeSynchronizedWithPeer 156 | info.Player = 0 157 | s.session.OnEvent(&info) 158 | 159 | info.Code = EventCodeRunning 160 | s.session.OnEvent(&info) 161 | s.synchonizing = false 162 | } 163 | 164 | case protocol.NetworkInterruptedEvent: 165 | info.Code = EventCodeConnectionInterrupted 166 | info.Player = 0 167 | info.DisconnectTimeout = evt.DisconnectTimeout 168 | s.session.OnEvent(&info) 169 | 170 | case protocol.NetworkResumedEvent: 171 | info.Code = EventCodeConnectionResumed 172 | info.Player = 0 173 | s.session.OnEvent(&info) 174 | 175 | case protocol.DisconnectedEvent: 176 | info.Code = EventCodeDisconnectedFromPeer 177 | info.Player = 0 178 | s.session.OnEvent(&info) 179 | 180 | case protocol.InputEvent: 181 | input := evt.Input 182 | 183 | s.host.SetLocalFrameNumber(input.Frame) 184 | s.host.SendInputAck() 185 | s.inputs[input.Frame%SpectatorFrameBufferSize] = input 186 | } 187 | } 188 | 189 | func (s *Spectator) HandleMessage(ipAddress string, port int, msg messages.UDPMessage, len int) { 190 | if s.host.HandlesMsg(ipAddress, port) { 191 | s.host.OnMsg(msg, len) 192 | } 193 | } 194 | 195 | func (p *Spectator) AddLocalInput(player PlayerHandle, values []byte, size int) error { 196 | return nil 197 | } 198 | 199 | func (s *Spectator) AddPlayer(player *Player, handle *PlayerHandle) error { 200 | return Error{Code: ErrorCodeInvalidRequest, Name: "ErrorCodeInvalidRequest"} 201 | } 202 | 203 | // We must 'impliment' these for this to be a true Session 204 | func (s *Spectator) DisconnectPlayer(handle PlayerHandle) error { 205 | return Error{Code: ErrorCodeInvalidRequest, Name: "ErrorCodeInvalidRequest"} 206 | } 207 | func (s *Spectator) GetNetworkStats(handle PlayerHandle) (protocol.NetworkStats, error) { 208 | return protocol.NetworkStats{}, Error{Code: ErrorCodeInvalidRequest, Name: "ErrorCodeInvalidRequest"} 209 | } 210 | func (s *Spectator) SetFrameDelay(player PlayerHandle, delay int) error { 211 | return Error{Code: ErrorCodeInvalidRequest, Name: "ErrorCodeInvalidRequest"} 212 | } 213 | func (s *Spectator) SetDisconnectTimeout(timeout int) error { 214 | return Error{Code: ErrorCodeInvalidRequest, Name: "ErrorCodeInvalidRequest"} 215 | } 216 | func (s *Spectator) SetDisconnectNotifyStart(timeout int) error { 217 | return Error{Code: ErrorCodeInvalidRequest, Name: "ErrorCodeInvalidRequest"} 218 | } 219 | func (s *Spectator) Close() error { 220 | return Error{Code: ErrorCodeInvalidRequest, Name: "ErrorCodeInvalidRequest"} 221 | } 222 | func (s *Spectator) InitializeConnection(c ...transport.Connection) error { 223 | if len(c) == 0 { 224 | s.connection = transport.NewUdp(s, s.localPort) 225 | return nil 226 | } 227 | s.connection = c[0] 228 | return nil 229 | } 230 | 231 | func (s *Spectator) HandleMessages() { 232 | for i := 0; i < len(s.messageChannel); i++ { 233 | mi := <-s.messageChannel 234 | s.HandleMessage(mi.Peer.Ip, mi.Peer.Port, mi.Message, mi.Length) 235 | } 236 | } 237 | 238 | func (s *Spectator) Start() { 239 | go s.connection.Read(s.messageChannel) 240 | 241 | s.host = protocol.NewUdpProtocol(s.connection, 0, s.hostIp, s.hostPort, nil) 242 | s.poll.RegisterLoop(&s.host, nil) 243 | s.host.Synchronize() 244 | 245 | } 246 | -------------------------------------------------------------------------------- /spectator_test.go: -------------------------------------------------------------------------------- 1 | package ggpo_test 2 | 3 | import ( 4 | "bytes" 5 | "math" 6 | "testing" 7 | "time" 8 | 9 | "github.com/assemblaj/ggpo/internal/mocks" 10 | "github.com/assemblaj/ggpo/internal/protocol" 11 | "github.com/assemblaj/ggpo/transport" 12 | 13 | "github.com/assemblaj/ggpo" 14 | ) 15 | 16 | func TestNewSpectatorBackendSession(t *testing.T) { 17 | session := mocks.NewFakeSession() 18 | localPort := 6000 19 | remotePort := 6001 20 | remoteIp := "127.2.1.1" 21 | numPlayers := 2 22 | inputSize := 4 23 | p2p := ggpo.NewPeer(&session, localPort, numPlayers, inputSize) 24 | 25 | session2 := mocks.NewFakeSession() 26 | p2p2 := ggpo.NewPeer(&session2, remotePort, numPlayers, inputSize) 27 | 28 | hostIp := "127.2.1.1" 29 | specPort := 6005 30 | stb := ggpo.NewSpectator(&session, specPort, 2, 4, hostIp, localPort) 31 | 32 | connection := mocks.NewFakeMultiplePeerConnection([]transport.MessageHandler{&p2p2, &stb}, localPort, remoteIp) 33 | connection2 := mocks.NewFakeMultiplePeerConnection([]transport.MessageHandler{&p2p}, remotePort, remoteIp) 34 | connection3 := mocks.NewFakeMultiplePeerConnection([]transport.MessageHandler{&p2p}, specPort, remoteIp) 35 | 36 | p2p.InitializeConnection(&connection) 37 | p2p2.InitializeConnection(&connection2) 38 | stb.InitializeConnection(&connection3) 39 | 40 | player1 := ggpo.NewLocalPlayer(20, 1) 41 | var p1Handle ggpo.PlayerHandle 42 | player2 := ggpo.NewRemotePlayer(20, 2, remoteIp, remotePort) 43 | var p2Handle ggpo.PlayerHandle 44 | spectator := ggpo.NewSpectatorPlayer(20, remoteIp, specPort) 45 | var specHandle ggpo.PlayerHandle 46 | p2p.AddPlayer(&player1, &p1Handle) 47 | p2p.AddPlayer(&player2, &p2Handle) 48 | p2p.AddPlayer(&spectator, &specHandle) 49 | 50 | player1 = ggpo.NewRemotePlayer(20, 1, remoteIp, localPort) 51 | player2 = ggpo.NewLocalPlayer(20, 2) 52 | var p2handle1 ggpo.PlayerHandle 53 | var p2handle2 ggpo.PlayerHandle 54 | p2p2.AddPlayer(&player1, &p2handle1) 55 | p2p2.AddPlayer(&player2, &p2handle2) 56 | 57 | stb.Start() 58 | advance := func() int64 { 59 | return time.Now().Add(time.Millisecond * 2000).UnixMilli() 60 | } 61 | for i := 0; i < protocol.NumSyncPackets; i++ { 62 | p2p.Idle(0, advance) 63 | p2p2.Idle(0, advance) 64 | stb.Idle(0, advance) 65 | } 66 | inputBytes := []byte{1, 2, 3, 4} 67 | err := p2p.AddLocalInput(p1Handle, inputBytes, len(inputBytes)) 68 | if err != nil { 69 | t.Errorf("Error when adding local input, %s", err) 70 | } 71 | 72 | err = p2p2.AddLocalInput(p2handle2, inputBytes, len(inputBytes)) 73 | if err != nil { 74 | t.Errorf("Error when adding local input, %s", err) 75 | } 76 | var disconnectFlags int 77 | _, err = stb.SyncInput(&disconnectFlags) 78 | if err != nil { 79 | t.Errorf("Error when synchronizing input on spectator, %s", err) 80 | } 81 | 82 | } 83 | func TestNewSpectatorBackendInput(t *testing.T) { 84 | session := mocks.NewFakeSession() 85 | localPort := 6000 86 | remotePort := 6001 87 | remoteIp := "127.2.1.1" 88 | numPlayers := 2 89 | inputSize := 4 90 | p2p := ggpo.NewPeer(&session, localPort, numPlayers, inputSize) 91 | 92 | session2 := mocks.NewFakeSession() 93 | p2p2 := ggpo.NewPeer(&session2, remotePort, numPlayers, inputSize) 94 | 95 | hostIp := "127.2.1.1" 96 | specPort := 6005 97 | stb := ggpo.NewSpectator(&session, specPort, 2, 4, hostIp, localPort) 98 | 99 | connection := mocks.NewFakeMultiplePeerConnection([]transport.MessageHandler{&p2p2, &stb}, localPort, remoteIp) 100 | connection2 := mocks.NewFakeMultiplePeerConnection([]transport.MessageHandler{&p2p}, remotePort, remoteIp) 101 | connection3 := mocks.NewFakeMultiplePeerConnection([]transport.MessageHandler{&p2p}, specPort, remoteIp) 102 | 103 | p2p.InitializeConnection(&connection) 104 | p2p2.InitializeConnection(&connection2) 105 | stb.InitializeConnection(&connection3) 106 | 107 | player1 := ggpo.NewLocalPlayer(20, 1) 108 | var p1Handle ggpo.PlayerHandle 109 | player2 := ggpo.NewRemotePlayer(20, 2, remoteIp, remotePort) 110 | var p2Handle ggpo.PlayerHandle 111 | spectator := ggpo.NewSpectatorPlayer(20, remoteIp, specPort) 112 | var specHandle ggpo.PlayerHandle 113 | p2p.AddPlayer(&player1, &p1Handle) 114 | p2p.AddPlayer(&player2, &p2Handle) 115 | p2p.AddPlayer(&spectator, &specHandle) 116 | 117 | player1 = ggpo.NewRemotePlayer(20, 1, remoteIp, localPort) 118 | player2 = ggpo.NewLocalPlayer(20, 2) 119 | var p2handle1 ggpo.PlayerHandle 120 | var p2handle2 ggpo.PlayerHandle 121 | p2p2.AddPlayer(&player1, &p2handle1) 122 | p2p2.AddPlayer(&player2, &p2handle2) 123 | 124 | stb.Start() 125 | advance := func() int64 { 126 | return time.Now().Add(time.Millisecond * 2000).UnixMilli() 127 | } 128 | for i := 0; i < protocol.NumSyncPackets; i++ { 129 | p2p.Idle(0, advance) 130 | p2p2.Idle(0, advance) 131 | stb.Idle(0, advance) 132 | } 133 | inputBytes := []byte{1, 2, 3, 4} 134 | inputBytes2 := []byte{5, 6, 7, 8} 135 | 136 | for i := 0; i < 2; i++ { 137 | p2p2.Idle(0) 138 | err := p2p2.AddLocalInput(p2Handle, inputBytes2, len(inputBytes2)) 139 | if err != nil { 140 | t.Errorf(" Error when adding local input to p2, %s", err) 141 | } 142 | p2p2.AdvanceFrame(ggpo.DefaultChecksum) 143 | 144 | p2p.Idle(0) 145 | err = p2p.AddLocalInput(p1Handle, inputBytes, len(inputBytes)) 146 | if err != nil { 147 | t.Errorf("Error when adding local input to p1, %s", err) 148 | } 149 | p2p.AdvanceFrame(ggpo.DefaultChecksum) 150 | 151 | stb.Idle(0) 152 | stb.AdvanceFrame(ggpo.DefaultChecksum) 153 | } 154 | var ignore int 155 | vals, err := stb.SyncInput(&ignore) 156 | if err != nil { 157 | t.Errorf("Error when spectator synchronize inputs. %s", err) 158 | } 159 | if !bytes.Equal(inputBytes, vals[0]) { 160 | t.Errorf("Returned p1 input %v doesn't equal given p1 input %v", vals[0], inputBytes) 161 | } 162 | if !bytes.Equal(inputBytes2, vals[1]) { 163 | t.Errorf("Returned p1 input %v doesn't equal given p1 input %v", vals[1], inputBytes2) 164 | } 165 | 166 | } 167 | 168 | /*WIP*/ 169 | func TestNewSpectatorBackendBehind(t *testing.T) { 170 | 171 | var p2p ggpo.Peer 172 | session := mocks.NewFakeSessionWithBackend() 173 | session.SetBackend(&p2p) 174 | localPort := 6000 175 | remotePort := 6001 176 | remoteIp := "127.2.1.1" 177 | numPlayers := 2 178 | inputSize := 4 179 | p2p = ggpo.NewPeer(&session, localPort, numPlayers, inputSize) 180 | 181 | var p2p2 ggpo.Peer 182 | session2 := mocks.NewFakeSessionWithBackend() 183 | session2.SetBackend(&p2p2) 184 | 185 | p2p2 = ggpo.NewPeer(&session2, remotePort, numPlayers, inputSize) 186 | 187 | var stb ggpo.Spectator 188 | 189 | session3 := mocks.NewFakeSessionWithBackend() 190 | session3.SetBackend(&stb) 191 | 192 | hostIp := "127.2.1.1" 193 | specPort := 6005 194 | stb = ggpo.NewSpectator(&session3, specPort, 2, 4, hostIp, localPort) 195 | 196 | connection := mocks.NewFakeMultiplePeerConnection([]transport.MessageHandler{&p2p2, &stb}, localPort, remoteIp) 197 | connection2 := mocks.NewFakeMultiplePeerConnection([]transport.MessageHandler{&p2p}, remotePort, remoteIp) 198 | connection3 := mocks.NewFakeMultiplePeerConnection([]transport.MessageHandler{&p2p}, specPort, remoteIp) 199 | 200 | p2p.InitializeConnection(&connection) 201 | p2p2.InitializeConnection(&connection2) 202 | stb.InitializeConnection(&connection3) 203 | 204 | player1 := ggpo.NewLocalPlayer(20, 1) 205 | var p1Handle ggpo.PlayerHandle 206 | player2 := ggpo.NewRemotePlayer(20, 2, remoteIp, remotePort) 207 | var p2Handle ggpo.PlayerHandle 208 | spectator := ggpo.NewSpectatorPlayer(20, remoteIp, specPort) 209 | var specHandle ggpo.PlayerHandle 210 | p2p.AddPlayer(&player1, &p1Handle) 211 | p2p.AddPlayer(&player2, &p2Handle) 212 | p2p.AddPlayer(&spectator, &specHandle) 213 | 214 | player1 = ggpo.NewRemotePlayer(20, 1, remoteIp, localPort) 215 | player2 = ggpo.NewLocalPlayer(20, 2) 216 | var p2handle1 ggpo.PlayerHandle 217 | var p2handle2 ggpo.PlayerHandle 218 | p2p2.AddPlayer(&player1, &p2handle1) 219 | p2p2.AddPlayer(&player2, &p2handle2) 220 | 221 | stb.Start() 222 | advance := func() int64 { 223 | return time.Now().Add(time.Millisecond * 2000).UnixMilli() 224 | } 225 | for i := 0; i < protocol.NumSyncPackets; i++ { 226 | p2p.Idle(0, advance) 227 | p2p2.Idle(0, advance) 228 | stb.Idle(0, advance) 229 | } 230 | inputBytes := []byte{1, 2, 3, 4} 231 | inputBytes2 := []byte{5, 6, 7, 8} 232 | var ignore int 233 | 234 | for i := 0; i < 10; i++ { 235 | p2p2.Idle(0) 236 | err := p2p2.AddLocalInput(p2Handle, inputBytes2, len(inputBytes2)) 237 | if err != nil { 238 | t.Errorf(" Error when adding local input to p2, %s", err) 239 | } 240 | p2p2.SyncInput(&ignore) 241 | p2p2.AdvanceFrame(ggpo.DefaultChecksum) 242 | 243 | p2p.Idle(0) 244 | err = p2p.AddLocalInput(p1Handle, inputBytes, len(inputBytes)) 245 | if err != nil { 246 | t.Errorf("Error when adding local input to p1, %s", err) 247 | } 248 | p2p.SyncInput(&ignore) 249 | p2p.AdvanceFrame(ggpo.DefaultChecksum) 250 | 251 | if i == 0 { 252 | stb.Idle(0) 253 | stb.SyncInput(&ignore) 254 | stb.AdvanceFrame(ggpo.DefaultChecksum) 255 | } 256 | } 257 | stb.Idle(0) 258 | stb.SyncInput(&ignore) 259 | } 260 | 261 | func TestNewSpectatorBackendCharacterization(t *testing.T) { 262 | session := mocks.NewFakeSession() 263 | localPort := 6000 264 | remotePort := 6001 265 | remoteIp := "127.2.1.1" 266 | numPlayers := 2 267 | inputSize := 4 268 | p2p := ggpo.NewPeer(&session, localPort, numPlayers, inputSize) 269 | 270 | session2 := mocks.NewFakeSession() 271 | p2p2 := ggpo.NewPeer(&session2, remotePort, numPlayers, inputSize) 272 | 273 | hostIp := "127.2.1.1" 274 | specPort := 6005 275 | stb := ggpo.NewSpectator(&session, specPort, 2, 4, hostIp, localPort) 276 | 277 | connection := mocks.NewFakeMultiplePeerConnection([]transport.MessageHandler{&p2p2, &stb}, localPort, remoteIp) 278 | connection2 := mocks.NewFakeMultiplePeerConnection([]transport.MessageHandler{&p2p}, remotePort, remoteIp) 279 | connection3 := mocks.NewFakeMultiplePeerConnection([]transport.MessageHandler{&p2p}, specPort, remoteIp) 280 | 281 | p2p.InitializeConnection(&connection) 282 | p2p2.InitializeConnection(&connection2) 283 | stb.InitializeConnection(&connection3) 284 | 285 | player1 := ggpo.NewLocalPlayer(20, 1) 286 | var p1Handle ggpo.PlayerHandle 287 | player2 := ggpo.NewRemotePlayer(20, 2, remoteIp, remotePort) 288 | var p2Handle ggpo.PlayerHandle 289 | spectator := ggpo.NewSpectatorPlayer(20, remoteIp, specPort) 290 | var specHandle ggpo.PlayerHandle 291 | p2p.AddPlayer(&player1, &p1Handle) 292 | p2p.AddPlayer(&player2, &p2Handle) 293 | p2p.AddPlayer(&spectator, &specHandle) 294 | 295 | player1 = ggpo.NewRemotePlayer(20, 1, remoteIp, localPort) 296 | player2 = ggpo.NewLocalPlayer(20, 2) 297 | var p2handle1 ggpo.PlayerHandle 298 | var p2handle2 ggpo.PlayerHandle 299 | p2p2.AddPlayer(&player1, &p2handle1) 300 | p2p2.AddPlayer(&player2, &p2handle2) 301 | 302 | stb.Start() 303 | advance := func() int64 { 304 | return time.Now().Add(time.Millisecond * 2000).UnixMilli() 305 | } 306 | for i := 0; i < protocol.NumSyncPackets; i++ { 307 | p2p.Idle(0, advance) 308 | p2p2.Idle(0, advance) 309 | stb.Idle(0, advance) 310 | } 311 | inputBytes := []byte{1, 2, 3, 4} 312 | inputBytes2 := []byte{5, 6, 7, 8} 313 | var ignore int 314 | defer func() { 315 | if r := recover(); r == nil { 316 | t.Errorf("The code did not panic when framecount hadn't been incremented..") 317 | } 318 | }() 319 | 320 | for i := 0; i < 2; i++ { 321 | p2p2.Idle(0, advance) 322 | err := p2p2.AddLocalInput(p2Handle, inputBytes2, len(inputBytes2)) 323 | if err != nil { 324 | t.Errorf(" Error when adding local input to p2, %s", err) 325 | } else { 326 | _, err = p2p2.SyncInput(&ignore) 327 | if err == nil { 328 | p2p2.AdvanceFrame(ggpo.DefaultChecksum) 329 | } 330 | } 331 | 332 | p2p.Idle(0, advance) 333 | err = p2p.AddLocalInput(p1Handle, inputBytes, len(inputBytes)) 334 | if err != nil { 335 | t.Errorf("Error when adding local input to p1, %s", err) 336 | } else { 337 | _, err = p2p.SyncInput(&ignore) 338 | if err == nil { 339 | p2p.AdvanceFrame(ggpo.DefaultChecksum) 340 | } 341 | } 342 | 343 | stb.Idle(0, advance) 344 | _, err = stb.SyncInput(&ignore) 345 | if err == nil { 346 | stb.AdvanceFrame(ggpo.DefaultChecksum) 347 | } 348 | } 349 | } 350 | 351 | func TestNewSpectatorBackendNoInputYet(t *testing.T) { 352 | session := mocks.NewFakeSession() 353 | localPort := 6000 354 | remotePort := 6001 355 | remoteIp := "127.2.1.1" 356 | numPlayers := 2 357 | inputSize := 4 358 | p2p := ggpo.NewPeer(&session, localPort, numPlayers, inputSize) 359 | 360 | session2 := mocks.NewFakeSession() 361 | p2p2 := ggpo.NewPeer(&session2, remotePort, numPlayers, inputSize) 362 | 363 | hostIp := "127.2.1.1" 364 | specPort := 6005 365 | stb := ggpo.NewSpectator(&session, specPort, 2, 4, hostIp, localPort) 366 | 367 | connection := mocks.NewFakeMultiplePeerConnection([]transport.MessageHandler{&p2p2, &stb}, localPort, remoteIp) 368 | connection2 := mocks.NewFakeMultiplePeerConnection([]transport.MessageHandler{&p2p}, remotePort, remoteIp) 369 | connection3 := mocks.NewFakeMultiplePeerConnection([]transport.MessageHandler{&p2p}, specPort, remoteIp) 370 | 371 | p2p.InitializeConnection(&connection) 372 | p2p2.InitializeConnection(&connection2) 373 | stb.InitializeConnection(&connection3) 374 | 375 | player1 := ggpo.NewLocalPlayer(20, 1) 376 | var p1Handle ggpo.PlayerHandle 377 | player2 := ggpo.NewRemotePlayer(20, 2, remoteIp, remotePort) 378 | var p2Handle ggpo.PlayerHandle 379 | spectator := ggpo.NewSpectatorPlayer(20, remoteIp, specPort) 380 | var specHandle ggpo.PlayerHandle 381 | p2p.AddPlayer(&player1, &p1Handle) 382 | p2p.AddPlayer(&player2, &p2Handle) 383 | p2p.AddPlayer(&spectator, &specHandle) 384 | 385 | player1 = ggpo.NewRemotePlayer(20, 1, remoteIp, localPort) 386 | player2 = ggpo.NewLocalPlayer(20, 2) 387 | var p2handle1 ggpo.PlayerHandle 388 | var p2handle2 ggpo.PlayerHandle 389 | p2p2.AddPlayer(&player1, &p2handle1) 390 | p2p2.AddPlayer(&player2, &p2handle2) 391 | 392 | stb.Start() 393 | advance := func() int64 { 394 | return time.Now().Add(time.Millisecond * 2000).UnixMilli() 395 | } 396 | for i := 0; i < protocol.NumSyncPackets; i++ { 397 | p2p.Idle(0, advance) 398 | p2p2.Idle(0, advance) 399 | stb.Idle(0, advance) 400 | } 401 | 402 | var ignore int 403 | stb.SyncInput(&ignore) 404 | stb.Idle(0) 405 | _, err := stb.SyncInput(&ignore) 406 | ggErr := err.(ggpo.Error) 407 | if ggErr.Code != ggpo.ErrorCodePredictionThreshod { 408 | t.Errorf("Should have recieved ErrorCodePredictionThreshold from spectator because no inputs had been sent yet. ") 409 | } 410 | } 411 | 412 | /* WIP, need to be able to test that the spectator is disconnected */ 413 | func TestNewSpectatorBackendDisconnect(t *testing.T) { 414 | session := mocks.NewFakeSession() 415 | localPort := 6000 416 | remotePort := 6001 417 | remoteIp := "127.2.1.1" 418 | numPlayers := 2 419 | inputSize := 4 420 | p2p := ggpo.NewPeer(&session, localPort, numPlayers, inputSize) 421 | 422 | session2 := mocks.NewFakeSession() 423 | p2p2 := ggpo.NewPeer(&session2, remotePort, numPlayers, inputSize) 424 | 425 | hostIp := "127.2.1.1" 426 | specPort := 6005 427 | stb := ggpo.NewSpectator(&session, specPort, 2, 4, hostIp, localPort) 428 | 429 | connection := mocks.NewFakeMultiplePeerConnection([]transport.MessageHandler{&p2p2, &stb}, localPort, remoteIp) 430 | connection2 := mocks.NewFakeMultiplePeerConnection([]transport.MessageHandler{&p2p}, remotePort, remoteIp) 431 | connection3 := mocks.NewFakeMultiplePeerConnection([]transport.MessageHandler{&p2p}, specPort, remoteIp) 432 | 433 | p2p.InitializeConnection(&connection) 434 | p2p2.InitializeConnection(&connection2) 435 | stb.InitializeConnection(&connection3) 436 | 437 | player1 := ggpo.NewLocalPlayer(20, 1) 438 | var p1Handle ggpo.PlayerHandle 439 | player2 := ggpo.NewRemotePlayer(20, 2, remoteIp, remotePort) 440 | var p2Handle ggpo.PlayerHandle 441 | spectator := ggpo.NewSpectatorPlayer(20, remoteIp, specPort) 442 | var specHandle ggpo.PlayerHandle 443 | p2p.AddPlayer(&player1, &p1Handle) 444 | p2p.AddPlayer(&player2, &p2Handle) 445 | p2p.AddPlayer(&spectator, &specHandle) 446 | 447 | player1 = ggpo.NewRemotePlayer(20, 1, remoteIp, localPort) 448 | player2 = ggpo.NewLocalPlayer(20, 2) 449 | var p2handle1 ggpo.PlayerHandle 450 | var p2handle2 ggpo.PlayerHandle 451 | p2p2.AddPlayer(&player1, &p2handle1) 452 | p2p2.AddPlayer(&player2, &p2handle2) 453 | 454 | stb.Start() 455 | 456 | p2p.SetDisconnectTimeout(3000) 457 | p2p2.SetDisconnectTimeout(3000) 458 | p2p.SetDisconnectNotifyStart(1000) 459 | p2p2.SetDisconnectNotifyStart(1000) 460 | 461 | advance := func() int64 { 462 | return time.Now().Add(time.Millisecond * 2000).UnixMilli() 463 | } 464 | for i := 0; i < protocol.NumSyncPackets; i++ { 465 | p2p.Idle(0, advance) 466 | p2p2.Idle(0, advance) 467 | stb.Idle(0, advance) 468 | } 469 | timeout := func() int64 { 470 | return time.Now().Add(time.Millisecond * 9500).UnixMilli() 471 | } 472 | var currentTime func() int64 473 | input1 := []byte{1, 2, 3, 4} 474 | input2 := []byte{5, 6, 7, 8} 475 | 476 | doPollTimeOuts := 0 477 | var p1now, p2now, p1next, p2next int 478 | p1now = int(time.Now().UnixMilli()) 479 | p1next = p1now 480 | p2next = p1now 481 | p2now = p1now 482 | currentTime = advance 483 | for i := 0; i < ggpo.MaxPredictionFrames; i++ { 484 | doPollTimeOuts = int(math.Max(0, float64(p1next-p1now-1))) 485 | p2p.Idle(doPollTimeOuts, currentTime) 486 | if p1now >= p1next { 487 | err := p2p.AddLocalInput(p1Handle, input1, 4) 488 | if err == nil { 489 | //_, err = p2p.SyncInput(&ignore) 490 | if err == nil { 491 | p2p.AdvanceFrame(ggpo.DefaultChecksum) 492 | } 493 | } 494 | p1next = p1now + 1000/60 495 | } 496 | 497 | doPollTimeOuts = int(math.Max(0, float64(p2next-p2now-1))) 498 | p2p2.Idle(doPollTimeOuts, currentTime) 499 | if p2now >= p2next { 500 | err := p2p2.AddLocalInput(p2handle2, input2, 4) 501 | if err == nil { 502 | //_, err = p2p2.SyncInput(&ignore) 503 | if err == nil { 504 | p2p2.AdvanceFrame(ggpo.DefaultChecksum) 505 | } 506 | } 507 | p2next = p2now + 1000/60 508 | } 509 | 510 | if i == ggpo.MaxPredictionFrames-2 { 511 | currentTime = timeout 512 | } 513 | } 514 | 515 | } 516 | 517 | func TestNoAddingSpectatorAfterSynchronization(t *testing.T) { 518 | session := mocks.NewFakeSession() 519 | localPort := 6000 520 | remotePort := 6001 521 | remoteIp := "127.2.1.1" 522 | numPlayers := 2 523 | inputSize := 4 524 | p2p := ggpo.NewPeer(&session, localPort, numPlayers, inputSize) 525 | 526 | session2 := mocks.NewFakeSession() 527 | p2p2 := ggpo.NewPeer(&session2, remotePort, numPlayers, inputSize) 528 | 529 | hostIp := "127.2.1.1" 530 | specPort := 6005 531 | stb := ggpo.NewSpectator(&session, specPort, 2, 4, hostIp, localPort) 532 | 533 | connection := mocks.NewFakeMultiplePeerConnection([]transport.MessageHandler{&p2p2, &stb}, localPort, remoteIp) 534 | connection2 := mocks.NewFakeMultiplePeerConnection([]transport.MessageHandler{&p2p}, remotePort, remoteIp) 535 | connection3 := mocks.NewFakeMultiplePeerConnection([]transport.MessageHandler{&p2p}, specPort, remoteIp) 536 | 537 | p2p.InitializeConnection(&connection) 538 | p2p2.InitializeConnection(&connection2) 539 | stb.InitializeConnection(&connection3) 540 | 541 | player1 := ggpo.NewLocalPlayer(20, 1) 542 | var p1Handle ggpo.PlayerHandle 543 | player2 := ggpo.NewRemotePlayer(20, 2, remoteIp, remotePort) 544 | var p2Handle ggpo.PlayerHandle 545 | spectator := ggpo.NewSpectatorPlayer(20, remoteIp, specPort) 546 | var specHandle ggpo.PlayerHandle 547 | p2p.AddPlayer(&player1, &p1Handle) 548 | p2p.AddPlayer(&player2, &p2Handle) 549 | 550 | player1 = ggpo.NewRemotePlayer(20, 1, remoteIp, localPort) 551 | player2 = ggpo.NewLocalPlayer(20, 2) 552 | var p2handle1 ggpo.PlayerHandle 553 | var p2handle2 ggpo.PlayerHandle 554 | p2p2.AddPlayer(&player1, &p2handle1) 555 | p2p2.AddPlayer(&player2, &p2handle2) 556 | 557 | stb.Start() 558 | 559 | advance := func() int64 { 560 | return time.Now().Add(time.Millisecond * 2000).UnixMilli() 561 | } 562 | for i := 0; i < protocol.NumSyncPackets; i++ { 563 | p2p.Idle(0, advance) 564 | p2p2.Idle(0, advance) 565 | } 566 | 567 | err := p2p.AddPlayer(&spectator, &specHandle) 568 | if err == nil { 569 | t.Errorf("Spectators should not be able to be added after synchronization.") 570 | } 571 | } 572 | 573 | func TestSpectatorBackendDissconnectPlayerError(t *testing.T) { 574 | session := mocks.NewFakeSession() 575 | hostIp := "127.2.1.1" 576 | hostPort := 6001 577 | localPort := 6000 578 | stb := ggpo.NewSpectator(&session, localPort, 2, 4, hostIp, hostPort) 579 | err := stb.DisconnectPlayer(ggpo.PlayerHandle(1)) 580 | if err == nil { 581 | t.Errorf("The code did not error when using an unsupported Feature.") 582 | } 583 | } 584 | 585 | func TestSpectatorBackendGetNetworkStatsError(t *testing.T) { 586 | session := mocks.NewFakeSession() 587 | hostIp := "127.2.1.1" 588 | hostPort := 6001 589 | localPort := 6000 590 | stb := ggpo.NewSpectator(&session, localPort, 2, 4, hostIp, hostPort) 591 | _, err := stb.GetNetworkStats(ggpo.PlayerHandle(1)) 592 | if err == nil { 593 | t.Errorf("The code did not error when using an unsupported Feature.") 594 | } 595 | } 596 | 597 | func TestSpectatorBackendSetFrameDelayError(t *testing.T) { 598 | session := mocks.NewFakeSession() 599 | hostIp := "127.2.1.1" 600 | hostPort := 6001 601 | localPort := 6000 602 | stb := ggpo.NewSpectator(&session, localPort, 2, 4, hostIp, hostPort) 603 | err := stb.SetFrameDelay(ggpo.PlayerHandle(1), 20) 604 | if err == nil { 605 | t.Errorf("The code did not error when using an unsupported Feature.") 606 | } 607 | } 608 | 609 | func TestSpectatorBackendSetDisconnectTimeoutError(t *testing.T) { 610 | session := mocks.NewFakeSession() 611 | hostIp := "127.2.1.1" 612 | hostPort := 6001 613 | localPort := 6000 614 | stb := ggpo.NewSpectator(&session, localPort, 2, 4, hostIp, hostPort) 615 | err := stb.SetDisconnectTimeout(20) 616 | if err == nil { 617 | t.Errorf("The code did not error when using an unsupported Feature.") 618 | } 619 | } 620 | func TestSpectatorBackendSetDisconnectNotifyStartError(t *testing.T) { 621 | session := mocks.NewFakeSession() 622 | hostIp := "127.2.1.1" 623 | hostPort := 6001 624 | localPort := 6000 625 | stb := ggpo.NewSpectator(&session, localPort, 2, 4, hostIp, hostPort) 626 | err := stb.SetDisconnectNotifyStart(20) 627 | if err == nil { 628 | t.Errorf("The code did not error when using an unsupported Feature.") 629 | } 630 | } 631 | 632 | func TestSpectatorBackendCloseError(t *testing.T) { 633 | session := mocks.NewFakeSession() 634 | hostIp := "127.2.1.1" 635 | hostPort := 6001 636 | localPort := 6000 637 | stb := ggpo.NewSpectator(&session, localPort, 2, 4, hostIp, hostPort) 638 | err := stb.Close() 639 | if err == nil { 640 | t.Errorf("The code did not error when using an unsupported Feature.") 641 | } 642 | } 643 | func TestSpectatorBackendAddPlayerError(t *testing.T) { 644 | session := mocks.NewFakeSession() 645 | hostIp := "127.2.1.1" 646 | hostPort := 6001 647 | localPort := 6000 648 | stb := ggpo.NewSpectator(&session, localPort, 2, 4, hostIp, hostPort) 649 | var player ggpo.Player 650 | var playerHandle ggpo.PlayerHandle 651 | err := stb.AddPlayer(&player, &playerHandle) 652 | if err == nil { 653 | t.Errorf("The code did not error when using an unsupported Feature.") 654 | } 655 | } 656 | -------------------------------------------------------------------------------- /sync.go: -------------------------------------------------------------------------------- 1 | package ggpo 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/assemblaj/ggpo/internal/util" 7 | 8 | "github.com/assemblaj/ggpo/internal/input" 9 | "github.com/assemblaj/ggpo/internal/messages" 10 | ) 11 | 12 | type Sync struct { 13 | session Session 14 | savedState savedState 15 | config SyncConfig 16 | 17 | rollingBack bool 18 | lastConfirmedFrame int 19 | frameCount int 20 | maxPredictionFrames int 21 | 22 | inputQueues []input.InputQueue 23 | 24 | localConnectStatus []messages.UdpConnectStatus 25 | } 26 | 27 | //const MaxPredictionFrames int = 8 28 | 29 | type SyncConfig struct { 30 | session Session 31 | numPredictionFrames int 32 | numPlayers int 33 | inputSize int 34 | } 35 | 36 | type SyncEvntType int 37 | 38 | const SyncConfirmedInput SyncEvntType = 0 39 | 40 | type savedFrame struct { 41 | frame int 42 | checksum int 43 | } 44 | 45 | type savedState struct { 46 | frames []savedFrame 47 | head int 48 | } 49 | 50 | func NewSync(status []messages.UdpConnectStatus, config *SyncConfig) Sync { 51 | s := Sync{ 52 | config: *config, 53 | session: config.session, 54 | maxPredictionFrames: config.numPredictionFrames, 55 | localConnectStatus: status, 56 | frameCount: 0, 57 | lastConfirmedFrame: -1, 58 | rollingBack: false, 59 | savedState: savedState{ 60 | frames: make([]savedFrame, MaxPredictionFrames+2)}, 61 | } 62 | s.CreateQueues(*config) 63 | return s 64 | } 65 | func NewSyncConfig(session Session, numPredictionFrames int, 66 | numPlayers int, inputSize int) SyncConfig { 67 | return SyncConfig{ 68 | session: session, 69 | numPredictionFrames: numPredictionFrames, 70 | numPlayers: numPlayers, 71 | inputSize: inputSize, 72 | } 73 | } 74 | func (s *SyncConfig) InputSize() int { 75 | return s.inputSize 76 | } 77 | 78 | // using close to mean delete 79 | func (s *Sync) Close() { 80 | // delete frames manually here rather than in a destructor of the sendFrame 81 | // structure so we cna efficiently copy frames via weak references 82 | // - pond3r 83 | s.inputQueues = nil 84 | } 85 | 86 | func (s *Sync) SetLastConfirmedFrame(frame int) { 87 | s.lastConfirmedFrame = frame 88 | if s.lastConfirmedFrame > 0 { 89 | for i := 0; i < s.config.numPlayers; i++ { 90 | err := s.inputQueues[i].DiscardConfirmedFrames(frame - 1) 91 | if err != nil { 92 | panic(err) 93 | } 94 | } 95 | } 96 | } 97 | 98 | func (s *Sync) AddLocalInput(queue int, input *input.GameInput) bool { 99 | framesBehind := s.frameCount - s.lastConfirmedFrame 100 | if s.frameCount >= s.maxPredictionFrames && framesBehind >= s.maxPredictionFrames { 101 | util.Log.Printf("Rejecting input from emulator: reached prediction barrier.\n") 102 | return false 103 | } 104 | 105 | if s.frameCount == 0 { 106 | s.SaveCurrentFrame() 107 | } 108 | 109 | util.Log.Printf("Sending undelayed local frame %d to queue %d.\n", s.frameCount, queue) 110 | input.Frame = s.frameCount 111 | err := s.inputQueues[queue].AddInput(input) 112 | if err != nil { 113 | panic(err) 114 | } 115 | 116 | return true 117 | } 118 | 119 | func (s *Sync) AddRemoteInput(queue int, input *input.GameInput) { 120 | 121 | err := s.inputQueues[queue].AddInput(input) 122 | if err != nil { 123 | panic(err) 124 | } 125 | } 126 | 127 | // originally took in a void ptr buffer and filled it with input 128 | // maybe i should return that the filled buffer instead idk 129 | // used by p2pbackend 130 | func (s *Sync) GetConfirmedInputs(frame int) ([][]byte, int) { 131 | disconnectFlags := 0 132 | //Assert(size >= s.config.numPlayers*s.config.inputSize) 133 | 134 | //values := make([]byte, size) 135 | var values [][]byte 136 | for i := 0; i < s.config.numPlayers; i++ { 137 | var input input.GameInput 138 | if (s.localConnectStatus[i].Disconnected) && (int32(frame) > s.localConnectStatus[i].LastFrame) { 139 | disconnectFlags |= (1 << i) 140 | input.Erase() 141 | } else { 142 | _, err := s.inputQueues[i].GetConfirmedInput(frame, &input) 143 | if err != nil { 144 | panic(err) 145 | } 146 | } 147 | // this was originally a memcpy 148 | values = append(values, input.Bits) 149 | } 150 | return values, disconnectFlags 151 | } 152 | 153 | // used by p2pbackend 154 | func (s *Sync) SynchronizeInputs() ([][]byte, int) { 155 | disconnectFlags := 0 156 | //Assert(size >= s.config.numPlayers*s.config.inputSize) 157 | 158 | //values := make([]byte, size) 159 | var values [][]byte 160 | for i := 0; i < s.config.numPlayers; i++ { 161 | var input input.GameInput 162 | if s.localConnectStatus[i].Disconnected && int32(s.frameCount) > s.localConnectStatus[i].LastFrame { 163 | disconnectFlags |= (1 << i) 164 | input.Erase() 165 | } else { 166 | _, err := s.inputQueues[i].GetInput(s.frameCount, &input) 167 | if err != nil { 168 | panic(err) 169 | } 170 | } 171 | values = append(values, input.Bits) 172 | } 173 | return values, disconnectFlags 174 | } 175 | 176 | func (s *Sync) CheckSimulation(timeout int) { 177 | 178 | var seekTo int 179 | if !s.CheckSimulationConsistency(&seekTo) { 180 | err := s.AdjustSimulation(seekTo) 181 | if err != nil { 182 | panic(err) 183 | } 184 | } 185 | } 186 | 187 | func (s *Sync) AdvanceFrame() { 188 | s.frameCount++ 189 | 190 | s.SaveCurrentFrame() 191 | } 192 | 193 | func (s *Sync) AdjustSimulation(seekTo int) error { 194 | frameCount := s.frameCount 195 | count := s.frameCount - seekTo 196 | 197 | util.Log.Printf("Catching up\n") 198 | s.rollingBack = true 199 | 200 | // flush our input queue and load the last frame 201 | err := s.LoadFrame(seekTo) 202 | if err != nil { 203 | panic(err) 204 | } 205 | 206 | if s.frameCount != seekTo { 207 | return errors.New("ggpo Sync AdjustSimulation: s.frameCount != seekTo") 208 | } 209 | 210 | // Advance frame by frame (stuffing notifications back to 211 | // the master). 212 | s.ResetPrediction(s.frameCount) 213 | for i := 0; i < count; i++ { 214 | s.session.AdvanceFrame(0) 215 | } 216 | 217 | if s.frameCount != frameCount { 218 | return errors.New("ggpo Sync AdjustSimulation: s.frameCount != frameCount") 219 | } 220 | s.rollingBack = false 221 | 222 | util.Log.Printf("---\n") 223 | return nil 224 | } 225 | 226 | func (s *Sync) LoadFrame(frame int) error { 227 | if frame == s.frameCount { 228 | util.Log.Printf("Skipping NOP.\n") 229 | return nil 230 | } 231 | // Move the head pointer back and load it up 232 | var err error 233 | s.savedState.head, err = s.FindSavedFrameIndex(frame) 234 | if err != nil { 235 | panic(err) 236 | } 237 | state := s.savedState.frames[s.savedState.head] 238 | 239 | util.Log.Printf("=== Loading frame info %d (checksum: %08x).\n", 240 | state.frame, state.checksum) 241 | //s.callbacks.LoadGameState(state.buf, state.cbuf) 242 | s.session.LoadGameState(s.savedState.head) 243 | 244 | // Reset framecount and the head of the state ring-buffer to point in 245 | // advance of the current frame (as if we had just finished executing it). 246 | s.frameCount = state.frame 247 | 248 | s.savedState.head = (s.savedState.head + 1) % len(s.savedState.frames) 249 | return nil 250 | } 251 | 252 | func (s *Sync) SaveCurrentFrame() { 253 | 254 | // originally was 255 | // SavedFrame *state = _savedstate.frames + _savedstate.head; 256 | state := s.savedState.frames[s.savedState.head] 257 | state.frame = s.frameCount 258 | checksum := s.session.SaveGameState(s.savedState.head) 259 | state.checksum = checksum 260 | 261 | s.savedState.frames[s.savedState.head] = state 262 | util.Log.Printf("=== Saved frame info %d (checksum: %08x).\n", state.frame, state.checksum) 263 | s.savedState.head = (s.savedState.head + 1) % len(s.savedState.frames) 264 | } 265 | 266 | func (s *Sync) GetLastSavedFrame() savedFrame { 267 | i := s.savedState.head - 1 268 | for i < 0 { 269 | i = len(s.savedState.frames) - 1 270 | } 271 | return s.savedState.frames[i] 272 | } 273 | 274 | // Trying to load a frame when it hasn't been saved causes an error 275 | // that will panic up the chair. 276 | func (s *Sync) FindSavedFrameIndex(frame int) (int, error) { 277 | 278 | count := len(s.savedState.frames) 279 | var i int 280 | for i = 0; i < count; i++ { 281 | if s.savedState.frames[i].frame == frame { 282 | break 283 | } 284 | } 285 | if i == count { 286 | return 0, errors.New("ggpo Sync FindSavedFrameIndex: i == count") 287 | } 288 | return i, nil 289 | } 290 | 291 | func (s *Sync) CreateQueues(config SyncConfig) bool { 292 | 293 | s.inputQueues = make([]input.InputQueue, s.config.numPlayers) 294 | for i := 0; i < s.config.numPlayers; i++ { 295 | s.inputQueues[i] = input.NewInputQueue(i, s.config.inputSize) 296 | } 297 | return true 298 | } 299 | 300 | func (s *Sync) CheckSimulationConsistency(seekTo *int) bool { 301 | 302 | firstInorrect := input.NullFrame 303 | for i := 0; i < s.config.numPlayers; i++ { 304 | incorrect := s.inputQueues[i].FirstIncorrectFrame() 305 | util.Log.Printf("considering incorrect frame %d reported by queue %d.\n", incorrect, i) 306 | 307 | if incorrect != input.NullFrame && (firstInorrect == input.NullFrame || incorrect < firstInorrect) { 308 | firstInorrect = incorrect 309 | } 310 | } 311 | 312 | if firstInorrect == input.NullFrame { 313 | util.Log.Printf("prediction ok. proceeding.\n") 314 | return true 315 | } 316 | *seekTo = firstInorrect 317 | return false 318 | } 319 | 320 | func (s *Sync) SetFrameDelay(queue int, delay int) { 321 | s.inputQueues[queue].SetFrameDelay(delay) 322 | } 323 | 324 | func (s *Sync) ResetPrediction(frameNumber int) { 325 | for i := 0; i < s.config.numPlayers; i++ { 326 | err := s.inputQueues[i].ResetPrediction(frameNumber) 327 | if err != nil { 328 | panic(err) 329 | } 330 | } 331 | } 332 | 333 | func (s *Sync) FrameCount() int { 334 | return s.frameCount 335 | } 336 | 337 | func (s *Sync) InRollback() bool { 338 | return s.rollingBack 339 | } 340 | -------------------------------------------------------------------------------- /sync_test.go: -------------------------------------------------------------------------------- 1 | package ggpo_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/assemblaj/ggpo/internal/input" 8 | "github.com/assemblaj/ggpo/internal/messages" 9 | "github.com/assemblaj/ggpo/internal/mocks" 10 | 11 | "github.com/assemblaj/ggpo" 12 | ) 13 | 14 | /* 15 | Characterization Tests Basically 16 | */ 17 | func TestNewSync(t *testing.T) { 18 | session := mocks.NewFakeSession() 19 | 20 | peerConnection := []messages.UdpConnectStatus{ 21 | {Disconnected: false, LastFrame: 12}, 22 | {Disconnected: false, LastFrame: 13}, 23 | } 24 | syncConfig := ggpo.NewSyncConfig( 25 | &session, 8, 2, 4, 26 | ) 27 | sync := ggpo.NewSync(peerConnection, &syncConfig) 28 | want := 0 29 | got := sync.FrameCount() 30 | if want != got { 31 | t.Errorf("expected '%#v' but got '%#v'", want, got) 32 | } 33 | } 34 | 35 | // Using Trying to load a frame when it hasn't been saved 36 | func TestSyncLoadFrameCharacterization(t *testing.T) { 37 | session := mocks.NewFakeSession() 38 | 39 | peerConnection := []messages.UdpConnectStatus{ 40 | {Disconnected: false, LastFrame: 12}, 41 | {Disconnected: false, LastFrame: 13}, 42 | } 43 | syncConfig := ggpo.NewSyncConfig( 44 | &session, 8, 2, 4, 45 | ) 46 | sync := ggpo.NewSync(peerConnection, &syncConfig) 47 | defer func() { 48 | if r := recover(); r == nil { 49 | t.Errorf("The code did not panic when load frame tried to load an unsaved frame") 50 | } 51 | }() 52 | sync.LoadFrame(6) 53 | 54 | } 55 | func TestSyncIncrementFrame(t *testing.T) { 56 | session := mocks.NewFakeSession() 57 | 58 | peerConnection := []messages.UdpConnectStatus{ 59 | {Disconnected: false, LastFrame: 12}, 60 | {Disconnected: false, LastFrame: 13}, 61 | } 62 | syncConfig := ggpo.NewSyncConfig( 63 | &session, 8, 2, 4, 64 | ) 65 | sync := ggpo.NewSync(peerConnection, &syncConfig) 66 | sync.AdvanceFrame() 67 | //sync.LoadFrame(0) 68 | want := 1 69 | got := sync.FrameCount() 70 | if got != want { 71 | t.Errorf("expected '%#v' but got '%#v'", want, got) 72 | } 73 | } 74 | func TestSyncAdustSimulationPanicIfSeekToUnsavedFrame(t *testing.T) { 75 | session := mocks.NewFakeSession() 76 | 77 | peerConnection := []messages.UdpConnectStatus{ 78 | {Disconnected: false, LastFrame: 12}, 79 | {Disconnected: false, LastFrame: 13}, 80 | } 81 | syncConfig := ggpo.NewSyncConfig( 82 | &session, 8, 2, 4, 83 | ) 84 | sync := ggpo.NewSync(peerConnection, &syncConfig) 85 | defer func() { 86 | if r := recover(); r == nil { 87 | t.Errorf("The code did not panic when AdjustSimulation attempted to load an unsaved frame.") 88 | } 89 | }() 90 | sync.AdjustSimulation(18) 91 | 92 | } 93 | func TestSyncAjdustSimulationError(t *testing.T) { 94 | session := mocks.NewFakeSession() 95 | 96 | peerConnection := []messages.UdpConnectStatus{ 97 | {Disconnected: false, LastFrame: 12}, 98 | {Disconnected: false, LastFrame: 13}, 99 | } 100 | syncConfig := ggpo.NewSyncConfig( 101 | &session, 8, 2, 4, 102 | ) 103 | sync := ggpo.NewSync(peerConnection, &syncConfig) 104 | frameCount := 2 105 | for i := 0; i < frameCount; i++ { 106 | sync.AdvanceFrame() 107 | } 108 | 109 | err := sync.AdjustSimulation(1) 110 | if err == nil { 111 | t.Errorf("The code did not error when AdustSimulation was seeking to the non current frame.") 112 | } 113 | } 114 | 115 | func TestSyncAdjustSimulationSucess(t *testing.T) { 116 | session := mocks.NewFakeSession() 117 | 118 | peerConnection := []messages.UdpConnectStatus{ 119 | {Disconnected: false, LastFrame: 12}, 120 | {Disconnected: false, LastFrame: 13}, 121 | } 122 | syncConfig := ggpo.NewSyncConfig( 123 | &session, 8, 2, 4, 124 | ) 125 | sync := ggpo.NewSync(peerConnection, &syncConfig) 126 | frameCount := 2 127 | for i := 0; i < frameCount; i++ { 128 | sync.AdvanceFrame() 129 | } 130 | 131 | err := sync.AdjustSimulation(frameCount) 132 | if err != nil { 133 | t.Errorf("The code errored when AdustSimulation was seeking to the current frame.") 134 | } 135 | } 136 | 137 | func TestSyncAddLocalInput(t *testing.T) { 138 | session := mocks.NewFakeSession() 139 | 140 | peerConnection := []messages.UdpConnectStatus{ 141 | {Disconnected: false, LastFrame: 12}, 142 | {Disconnected: false, LastFrame: 13}, 143 | } 144 | syncConfig := ggpo.NewSyncConfig( 145 | &session, 8, 2, 4, 146 | ) 147 | sync := ggpo.NewSync(peerConnection, &syncConfig) 148 | queue := 0 149 | input := input.GameInput{} 150 | success := sync.AddLocalInput(queue, &input) 151 | want := 0 152 | got := input.Frame 153 | if success == false { 154 | t.Errorf("The AddLocalInput failed.") 155 | } 156 | if want != got { 157 | t.Errorf("expected '%#v' but got '%#v'", want, got) 158 | } 159 | } 160 | func TestSyncAddLocalInputAfterIncrementFrame(t *testing.T) { 161 | session := mocks.NewFakeSession() 162 | 163 | peerConnection := []messages.UdpConnectStatus{ 164 | {Disconnected: false, LastFrame: 12}, 165 | {Disconnected: false, LastFrame: 13}, 166 | } 167 | syncConfig := ggpo.NewSyncConfig( 168 | &session, 8, 2, 4, 169 | ) 170 | sync := ggpo.NewSync(peerConnection, &syncConfig) 171 | frameCount := 2 172 | for i := 0; i < frameCount; i++ { 173 | sync.AdvanceFrame() 174 | } 175 | 176 | queue := 0 177 | input := input.GameInput{} 178 | success := sync.AddLocalInput(queue, &input) 179 | want := 2 180 | got := input.Frame 181 | if success == false { 182 | t.Errorf("The AddLocalInput failed.") 183 | } 184 | if want != got { 185 | t.Errorf("expected '%#v' but got '%#v'", want, got) 186 | } 187 | } 188 | func TestSyncSynchronizeInputsNoInput(t *testing.T) { 189 | session := mocks.NewFakeSession() 190 | 191 | peerConnection := []messages.UdpConnectStatus{ 192 | {Disconnected: false, LastFrame: 12}, 193 | {Disconnected: false, LastFrame: 13}, 194 | } 195 | syncConfig := ggpo.NewSyncConfig( 196 | &session, 8, 2, 4, 197 | ) 198 | sync := ggpo.NewSync(peerConnection, &syncConfig) 199 | inputs, disconnectFlags := sync.SynchronizeInputs() 200 | want := 2 201 | got := len(inputs) 202 | if want != got { 203 | t.Errorf("expected '%#v' but got '%#v'", want, got) 204 | } 205 | 206 | defaultInputs := make([]byte, 4) 207 | if !bytes.Equal(inputs[0], defaultInputs) || !bytes.Equal(inputs[1], defaultInputs) { 208 | t.Errorf("expected input to be default inputs.") 209 | } 210 | 211 | want = 0 212 | got = disconnectFlags 213 | if want != got { 214 | t.Errorf("expected '%#v' but got '%#v'", want, got) 215 | } 216 | } 217 | 218 | func TestSyncSynchronizeInputsWithLocalInputs(t *testing.T) { 219 | session := mocks.NewFakeSession() 220 | 221 | peerConnection := []messages.UdpConnectStatus{ 222 | {Disconnected: false, LastFrame: 12}, 223 | {Disconnected: false, LastFrame: 13}, 224 | } 225 | syncConfig := ggpo.NewSyncConfig( 226 | &session, 8, 2, 4, 227 | ) 228 | sync := ggpo.NewSync(peerConnection, &syncConfig) 229 | queue := 0 230 | input := input.GameInput{Bits: []byte{1, 2, 3, 4}} 231 | sync.AddLocalInput(queue, &input) 232 | 233 | inputs, disconnectFlags := sync.SynchronizeInputs() 234 | want := 2 235 | got := len(inputs) 236 | if want != got { 237 | t.Errorf("expected '%#v' but got '%#v'", want, got) 238 | } 239 | if len(inputs[0]) == 0 { 240 | t.Errorf("expected input for local not to be zero.") 241 | } 242 | 243 | want = 0 244 | got = disconnectFlags 245 | if want != got { 246 | t.Errorf("expected '%#v' but got '%#v'", want, got) 247 | } 248 | } 249 | 250 | func TestSyncSynchronizeInputsWithRemoteInputs(t *testing.T) { 251 | session := mocks.NewFakeSession() 252 | 253 | peerConnection := []messages.UdpConnectStatus{ 254 | {Disconnected: false, LastFrame: 12}, 255 | {Disconnected: false, LastFrame: 13}, 256 | } 257 | syncConfig := ggpo.NewSyncConfig( 258 | &session, 8, 2, 4, 259 | ) 260 | sync := ggpo.NewSync(peerConnection, &syncConfig) 261 | queue := 1 262 | input := input.GameInput{Bits: []byte{1, 2, 3, 4}} 263 | sync.AddRemoteInput(queue, &input) 264 | 265 | inputs, _ := sync.SynchronizeInputs() 266 | want := []byte{1, 2, 3, 4} 267 | got := inputs[1] 268 | if !bytes.Equal(want, got) { 269 | t.Errorf("expected '%#v' but got '%#v'", want, got) 270 | } 271 | if len(inputs[1]) == 0 { 272 | t.Errorf("expected input for remote not to be zero.") 273 | } 274 | 275 | } 276 | 277 | func TestSyncSynchronizeInputsWithBothInputs(t *testing.T) { 278 | session := mocks.NewFakeSession() 279 | 280 | peerConnection := []messages.UdpConnectStatus{ 281 | {Disconnected: false, LastFrame: 12}, 282 | {Disconnected: false, LastFrame: 13}, 283 | } 284 | syncConfig := ggpo.NewSyncConfig( 285 | &session, 8, 2, 4, 286 | ) 287 | sync := ggpo.NewSync(peerConnection, &syncConfig) 288 | queue := 1 289 | input1 := input.GameInput{Bits: []byte{1, 2, 3, 4}} 290 | input2 := input.GameInput{Bits: []byte{5, 6, 7, 8}} 291 | sync.AddRemoteInput(queue, &input1) 292 | sync.AddLocalInput(0, &input2) 293 | 294 | inputs, _ := sync.SynchronizeInputs() 295 | want := []byte{1, 2, 3, 4} 296 | got := inputs[1] 297 | if !bytes.Equal(want, got) { 298 | t.Errorf("expected '%#v' but got '%#v'", want, got) 299 | } 300 | 301 | want = []byte{5, 6, 7, 8} 302 | got = inputs[0] 303 | 304 | if !bytes.Equal(want, got) { 305 | t.Errorf("expected '%#v' but got '%#v'", want, got) 306 | } 307 | 308 | } 309 | 310 | func TestSyncGetConfirmedInputs(t *testing.T) { 311 | session := mocks.NewFakeSession() 312 | 313 | peerConnection := []messages.UdpConnectStatus{ 314 | {Disconnected: false, LastFrame: 12}, 315 | {Disconnected: false, LastFrame: 13}, 316 | } 317 | syncConfig := ggpo.NewSyncConfig( 318 | &session, 8, 2, 4, 319 | ) 320 | sync := ggpo.NewSync(peerConnection, &syncConfig) 321 | queue := 1 322 | input1 := input.GameInput{Bits: []byte{1, 2, 3, 4}} 323 | input2 := input.GameInput{Bits: []byte{5, 6, 7, 8}} 324 | sync.AddRemoteInput(queue, &input1) 325 | sync.AddLocalInput(0, &input2) 326 | 327 | inputs, _ := sync.GetConfirmedInputs(0) 328 | want := []byte{1, 2, 3, 4} 329 | got := inputs[1] 330 | if !bytes.Equal(want, got) { 331 | t.Errorf("expected '%#v' but got '%#v'", want, got) 332 | } 333 | 334 | want = []byte{5, 6, 7, 8} 335 | got = inputs[0] 336 | 337 | if !bytes.Equal(want, got) { 338 | t.Errorf("expected '%#v' but got '%#v'", want, got) 339 | } 340 | 341 | } 342 | 343 | // Characterization Test 344 | func TestSyncAddLocalInputPanic(t *testing.T) { 345 | session := mocks.NewFakeSession() 346 | 347 | peerConnection := []messages.UdpConnectStatus{ 348 | {Disconnected: false, LastFrame: 12}, 349 | {Disconnected: false, LastFrame: 13}, 350 | } 351 | syncConfig := ggpo.NewSyncConfig( 352 | &session, 8, 2, 4, 353 | ) 354 | sync := ggpo.NewSync(peerConnection, &syncConfig) 355 | queue := 1 356 | input1 := input.GameInput{Bits: []byte{1, 2, 3, 4}} 357 | input2 := input.GameInput{Bits: []byte{5, 6, 7, 8}} 358 | sync.AddRemoteInput(queue, &input1) 359 | sync.AddLocalInput(0, &input2) 360 | //sync.SetLastConfirmedFrame(8) 361 | defer func() { 362 | if r := recover(); r == nil { 363 | t.Errorf("The code did not panic when AddLocalInput attempted to add an input even when the frame hadn't been incremented.") 364 | } 365 | }() 366 | sync.AddLocalInput(0, &input2) 367 | 368 | } 369 | 370 | func TestSyncAddLocalInputNoPanic(t *testing.T) { 371 | session := mocks.NewFakeSession() 372 | 373 | peerConnection := []messages.UdpConnectStatus{ 374 | {Disconnected: false, LastFrame: 12}, 375 | {Disconnected: false, LastFrame: 13}, 376 | } 377 | syncConfig := ggpo.NewSyncConfig( 378 | &session, 8, 2, 4, 379 | ) 380 | sync := ggpo.NewSync(peerConnection, &syncConfig) 381 | input1 := input.GameInput{Bits: []byte{1, 2, 3, 4}} 382 | input2 := input.GameInput{Bits: []byte{5, 6, 7, 8}} 383 | 384 | sync.AddLocalInput(0, &input1) 385 | sync.AdvanceFrame() 386 | sync.AddLocalInput(0, &input2) 387 | } 388 | 389 | func TestSyncAddRemoteInputPanic(t *testing.T) { 390 | session := mocks.NewFakeSession() 391 | 392 | peerConnection := []messages.UdpConnectStatus{ 393 | {Disconnected: false, LastFrame: 12}, 394 | {Disconnected: false, LastFrame: 13}, 395 | } 396 | syncConfig := ggpo.NewSyncConfig( 397 | &session, 8, 2, 4, 398 | ) 399 | sync := ggpo.NewSync(peerConnection, &syncConfig) 400 | input1 := input.GameInput{Bits: []byte{1, 2, 3, 4}} 401 | input2 := input.GameInput{Bits: []byte{5, 6, 7, 8}} 402 | sync.AddRemoteInput(1, &input2) 403 | defer func() { 404 | if r := recover(); r == nil { 405 | t.Errorf("The code did not panic when AddRemoteInput attempted to add an input even when the frame hadn't been incremented.") 406 | } 407 | }() 408 | sync.AddRemoteInput(1, &input1) 409 | } 410 | 411 | // Characterization mocks. No idea why this works and the above doesn't. 412 | func TestSyncAddRemoteInputNoPanic(t *testing.T) { 413 | session := mocks.NewFakeSession() 414 | 415 | peerConnection := []messages.UdpConnectStatus{ 416 | {Disconnected: false, LastFrame: 12}, 417 | {Disconnected: false, LastFrame: 13}, 418 | } 419 | syncConfig := ggpo.NewSyncConfig( 420 | &session, 8, 2, 4, 421 | ) 422 | sync := ggpo.NewSync(peerConnection, &syncConfig) 423 | input1 := input.GameInput{Bits: []byte{1, 2, 3, 4}} 424 | input2 := input.GameInput{Bits: []byte{5, 6, 7, 8}} 425 | sync.AddRemoteInput(1, &input2) 426 | sync.AddLocalInput(0, &input1) 427 | sync.AdvanceFrame() 428 | sync.AddLocalInput(0, &input1) 429 | sync.AddRemoteInput(1, &input1) 430 | } 431 | func TestSyncAddFrameDelay(t *testing.T) { 432 | session := mocks.NewFakeSession() 433 | 434 | peerConnection := []messages.UdpConnectStatus{ 435 | {Disconnected: false, LastFrame: 12}, 436 | {Disconnected: false, LastFrame: 13}, 437 | } 438 | syncConfig := ggpo.NewSyncConfig( 439 | &session, 8, 2, 4, 440 | ) 441 | sync := ggpo.NewSync(peerConnection, &syncConfig) 442 | input1 := input.GameInput{Bits: []byte{1, 2, 3, 4}} 443 | input2 := input.GameInput{Bits: []byte{5, 6, 7, 8}} 444 | frameDelay := 5 445 | sync.SetFrameDelay(0, 5) 446 | 447 | want := sync.FrameCount() + frameDelay 448 | sync.AddRemoteInput(1, &input2) 449 | sync.AddLocalInput(0, &input1) 450 | got := input1.Frame 451 | 452 | if want != got { 453 | t.Errorf("The Input delay was not applied correctly, expected input to be at frame %d but got %d", want, got) 454 | } 455 | /* 456 | sync.AdvanceFrame() 457 | sync.AddLocalInput(0, &input) 458 | sync.AddRemoteInput(1, &input) 459 | */ 460 | } 461 | func TestSyncUseAfterClose(t *testing.T) { 462 | session := mocks.NewFakeSession() 463 | 464 | peerConnection := []messages.UdpConnectStatus{ 465 | {Disconnected: false, LastFrame: 12}, 466 | {Disconnected: false, LastFrame: 13}, 467 | } 468 | syncConfig := ggpo.NewSyncConfig( 469 | &session, 8, 2, 4, 470 | ) 471 | sync := ggpo.NewSync(peerConnection, &syncConfig) 472 | queue := 1 473 | input1 := input.GameInput{Bits: []byte{1, 2, 3, 4}} 474 | input2 := input.GameInput{Bits: []byte{5, 6, 7, 8}} 475 | sync.AddRemoteInput(queue, &input1) 476 | sync.AddLocalInput(0, &input2) 477 | sync.Close() 478 | defer func() { 479 | if r := recover(); r == nil { 480 | t.Errorf("The code did not panic when attempting to use sync after Close") 481 | } 482 | }() 483 | 484 | sync.AddLocalInput(0, &input2) 485 | } 486 | -------------------------------------------------------------------------------- /synctest.go: -------------------------------------------------------------------------------- 1 | package ggpo 2 | 3 | import ( 4 | "github.com/assemblaj/ggpo/internal/util" 5 | "os" 6 | 7 | "github.com/assemblaj/ggpo/internal/buffer" 8 | "github.com/assemblaj/ggpo/internal/input" 9 | "github.com/assemblaj/ggpo/internal/polling" 10 | "github.com/assemblaj/ggpo/internal/protocol" 11 | "github.com/assemblaj/ggpo/transport" 12 | ) 13 | 14 | type SyncTest struct { 15 | session Session 16 | sync Sync 17 | numPlayers int 18 | checkDistance int 19 | lastVerified int 20 | rollingBack bool 21 | running bool 22 | logFp os.File 23 | game string 24 | 25 | currentInput input.GameInput 26 | lastInput input.GameInput 27 | savedFrames buffer.RingBuffer[savedInfo] 28 | strict bool 29 | leniantRevert bool 30 | } 31 | 32 | type savedInfo struct { 33 | frame int 34 | checksum int 35 | buf []byte 36 | cbuf int 37 | input input.GameInput 38 | } 39 | 40 | func NewSyncTest(cb Session, 41 | numPlayers int, 42 | frames int, inputSize int, strict bool) SyncTest { 43 | s := SyncTest{ 44 | session: cb, 45 | numPlayers: numPlayers, 46 | checkDistance: frames, 47 | savedFrames: buffer.NewRingBuffer[savedInfo](32)} 48 | s.currentInput.Erase() 49 | s.currentInput.Bits = make([]byte, input.GameInputMaxBytes*input.GameInputMaxPlayers) 50 | s.currentInput.Size = inputSize 51 | var config SyncConfig 52 | config.session = s.session 53 | config.numPredictionFrames = MaxPredictionFrames 54 | config.inputSize = inputSize 55 | s.sync = NewSync(nil, &config) 56 | s.strict = strict 57 | return s 58 | } 59 | 60 | func (s *SyncTest) Idle(timeout int, timeFunc ...polling.FuncTimeType) error { 61 | if !s.running { 62 | var info Event 63 | info.Code = EventCodeRunning 64 | s.session.OnEvent(&info) 65 | s.running = true 66 | } 67 | return nil 68 | } 69 | 70 | func (s *SyncTest) AddPlayer(player *Player, handle *PlayerHandle) error { 71 | if player.PlayerNum < 1 || player.PlayerNum > s.numPlayers { 72 | return Error{Code: ErrorCodePlayerOutOfRange, Name: "ErrorCodePlayerOutOfRange"} 73 | } 74 | *handle = (PlayerHandle(player.PlayerNum - 1)) 75 | return nil 76 | } 77 | 78 | func (s *SyncTest) AddLocalInput(player PlayerHandle, values []byte, size int) error { 79 | if !s.running { 80 | return Error{Code: ErrorCodeNotSynchronized, Name: "ErrorCodeNotSynchronized"} 81 | } 82 | //index := int(player) 83 | //for i := 0; i < size; i++ { 84 | // s.currentInput.Bits[index*size+i] |= values[i] 85 | //} 86 | //copy(s.currentInput.Bits, values) 87 | index := int(player) 88 | start := index * size 89 | end := start + size 90 | copy(s.currentInput.Bits[start:end], values) 91 | return nil 92 | } 93 | 94 | func (s *SyncTest) SyncInput(discconectFlags *int) ([][]byte, error) { 95 | if s.rollingBack { 96 | var info savedInfo 97 | var err error 98 | info, err = s.savedFrames.Front() 99 | if err != nil { 100 | panic(err) 101 | } 102 | s.lastInput = *info.input.Clone() 103 | } else { 104 | if s.sync.FrameCount() == 0 { 105 | s.sync.SaveCurrentFrame() 106 | } 107 | s.lastInput = *s.currentInput.Clone() 108 | } 109 | //var values = make([]byte, len(s.lastInput.Bits)) 110 | //copy(values, s.lastInput.Bits) 111 | 112 | values := make([][]byte, s.numPlayers) 113 | offset := 0 114 | counter := 0 115 | for offset < len(s.lastInput.Bits) && counter < s.numPlayers { 116 | values[counter] = s.lastInput.Bits[offset : s.lastInput.Size+offset] 117 | offset += s.lastInput.Size 118 | counter++ 119 | } 120 | if *discconectFlags > 0 { 121 | *discconectFlags = 0 122 | } 123 | return values, nil 124 | } 125 | 126 | func (s *SyncTest) AdvanceFrame(checksum uint32) error { 127 | s.sync.AdvanceFrame() 128 | s.currentInput.Erase() 129 | 130 | util.Log.Printf("End of frame(%d)...\n", s.sync.FrameCount()) 131 | 132 | if s.rollingBack { 133 | return nil 134 | } 135 | 136 | frame := s.sync.FrameCount() 137 | // Hold onto the current frame in our queue of saved states. We'll need 138 | // the checksum later to verify that our replay of the same frame got the 139 | // same results 140 | var err error 141 | var info savedInfo 142 | info.frame = frame 143 | info.input = s.lastInput 144 | info.checksum = s.sync.GetLastSavedFrame().checksum 145 | 146 | err = s.savedFrames.Push(info) 147 | if err != nil { 148 | panic(err) 149 | } 150 | 151 | if frame-s.lastVerified == s.checkDistance { 152 | // We've gone far enough ahead and should now now start replaying frames 153 | // Load the last verified frame and set the rollback flag to true. 154 | err = s.sync.LoadFrame(s.lastVerified) 155 | if err != nil { 156 | panic(err) 157 | } 158 | 159 | s.rollingBack = true 160 | for !s.savedFrames.Empty() { 161 | s.session.AdvanceFrame(0) 162 | 163 | // Verify that the checksum of this frame is the same as the one in our 164 | // list 165 | info, err = s.savedFrames.Front() 166 | if err != nil { 167 | panic(err) 168 | } 169 | err = s.savedFrames.Pop() 170 | if err != nil { 171 | panic(err) 172 | } 173 | if info.frame != s.sync.FrameCount() { 174 | util.Log.Printf("Frame number %d does not match saved frame number %d", info.frame, frame) 175 | if s.strict { 176 | panic("RaiseSyncError") 177 | } else { 178 | util.Log.Println("RaiseSyncError: Continuing as normal.") 179 | //s.revert() 180 | //break 181 | } 182 | } 183 | checksum := s.sync.GetLastSavedFrame().checksum 184 | if info.checksum != checksum { 185 | s.LogGameStates(info) 186 | util.Log.Printf("Checksum for frame %d does not match saved (%d != %d)", frame, checksum, info.checksum) 187 | if s.strict { 188 | panic("RaiseSyncError") 189 | } else { 190 | util.Log.Println("RaiseSyncError: Continuing as normal..") 191 | //s.revert() 192 | //break 193 | } 194 | lastVerifiedStateID, _ := s.sync.FindSavedFrameIndex(s.lastVerified) 195 | s.session.OnEvent(&Event{ 196 | Code: EventCodeSyncTestDesync, 197 | CurrentState: s.sync.savedState.head, 198 | LastVerified: lastVerifiedStateID, 199 | }) 200 | } 201 | util.Log.Printf("Checksum %08d for frame %d matches.\n", checksum, info.frame) 202 | } 203 | s.lastVerified = frame 204 | s.rollingBack = false 205 | } 206 | return nil 207 | } 208 | 209 | func (s *SyncTest) revert() { 210 | err := s.sync.LoadFrame(s.lastVerified) 211 | if err != nil { 212 | panic(err) 213 | } 214 | s.leniantRevert = true 215 | for !s.savedFrames.Empty() { 216 | err = s.savedFrames.Pop() 217 | if err != nil { 218 | panic(err) 219 | } 220 | } 221 | } 222 | 223 | func (s *SyncTest) LogGameStates(info savedInfo) { 224 | //s.session.LogGameState("saved:", info.buf, len(info.buf)) 225 | //s.session.LogGameState("loaded:", s.sync.GetLastSavedFrame().buf, s.sync.GetLastSavedFrame().cbuf) 226 | } 227 | 228 | // We must 'impliment' these for this to be a true Session 229 | func (s *SyncTest) DisconnectPlayer(handle PlayerHandle) error { 230 | return Error{Code: ErrorCodeInvalidRequest, Name: "ErrorCodeInvalidRequest"} 231 | } 232 | func (s *SyncTest) GetNetworkStats(handle PlayerHandle) (protocol.NetworkStats, error) { 233 | return protocol.NetworkStats{}, Error{Code: ErrorCodeInvalidRequest, Name: "ErrorCodeInvalidRequest"} 234 | } 235 | func (s *SyncTest) SetFrameDelay(player PlayerHandle, delay int) error { 236 | return Error{Code: ErrorCodeInvalidRequest, Name: "ErrorCodeInvalidRequest"} 237 | } 238 | func (s *SyncTest) SetDisconnectTimeout(timeout int) error { 239 | return Error{Code: ErrorCodeInvalidRequest, Name: "ErrorCodeInvalidRequest"} 240 | } 241 | func (s *SyncTest) SetDisconnectNotifyStart(timeout int) error { 242 | return Error{Code: ErrorCodeInvalidRequest, Name: "ErrorCodeInvalidRequest"} 243 | } 244 | func (s *SyncTest) Close() error { 245 | return Error{Code: ErrorCodeInvalidRequest, Name: "ErrorCodeInvalidRequest"} 246 | } 247 | 248 | func (s *SyncTest) Start() {} 249 | 250 | func (s *SyncTest) InitializeConnection(c ...transport.Connection) error { 251 | return nil 252 | } 253 | -------------------------------------------------------------------------------- /synctest_test.go: -------------------------------------------------------------------------------- 1 | package ggpo_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/assemblaj/ggpo/internal/mocks" 8 | 9 | "github.com/assemblaj/ggpo" 10 | ) 11 | 12 | func TestNewSyncTestBackend(t *testing.T) { 13 | session := mocks.NewFakeSession() 14 | player := ggpo.NewLocalPlayer(20, 1) 15 | stb := ggpo.NewSyncTest(&session, 1, 8, 4, true) 16 | var handle ggpo.PlayerHandle 17 | stb.AddPlayer(&player, &handle) 18 | 19 | } 20 | 21 | func TestSyncTestBackendAddPlayerOver(t *testing.T) { 22 | session := mocks.NewFakeSession() 23 | player := ggpo.NewLocalPlayer(20, 2) 24 | stb := ggpo.NewSyncTest(&session, 1, 8, 4, true) 25 | var handle ggpo.PlayerHandle 26 | err := stb.AddPlayer(&player, &handle) 27 | if err == nil { 28 | t.Errorf("There should be an error for adding a player greater than the total num Players.") 29 | } 30 | } 31 | 32 | func TestSyncTestBackendAddPlayerNegative(t *testing.T) { 33 | session := mocks.NewFakeSession() 34 | player := ggpo.NewLocalPlayer(20, -1) 35 | stb := ggpo.NewSyncTest(&session, 1, 8, 4, true) 36 | var handle ggpo.PlayerHandle 37 | err := stb.AddPlayer(&player, &handle) 38 | if err == nil { 39 | t.Errorf("There should be an error for adding a player with a negative number") 40 | } 41 | } 42 | 43 | func TestSyncTestBackendAddLocalInputError(t *testing.T) { 44 | session := mocks.NewFakeSession() 45 | player := ggpo.NewLocalPlayer(20, 1) 46 | stb := ggpo.NewSyncTest(&session, 1, 8, 4, true) 47 | var handle ggpo.PlayerHandle 48 | stb.AddPlayer(&player, &handle) 49 | err := stb.AddLocalInput(handle, []byte{1, 2, 3, 4}, 4) 50 | if err == nil { 51 | t.Errorf("There should be an error for adding local input when sync test isn't running yet.") 52 | } 53 | } 54 | 55 | func TestSyncTestBackendAddLocalInput(t *testing.T) { 56 | session := mocks.NewFakeSession() 57 | player := ggpo.NewLocalPlayer(20, 1) 58 | stb := ggpo.NewSyncTest(&session, 1, 8, 4, true) 59 | var handle ggpo.PlayerHandle 60 | stb.AddPlayer(&player, &handle) 61 | stb.Idle(0) 62 | err := stb.AddLocalInput(handle, []byte{1, 2, 3, 4}, 4) 63 | if err != nil { 64 | t.Errorf("There shouldn't be an error, adding local input should be successful.") 65 | } 66 | } 67 | 68 | func TestSyncTestBackendSyncInput(t *testing.T) { 69 | session := mocks.NewFakeSession() 70 | player := ggpo.NewLocalPlayer(20, 1) 71 | stb := ggpo.NewSyncTest(&session, 1, 8, 4, true) 72 | var handle ggpo.PlayerHandle 73 | stb.AddPlayer(&player, &handle) 74 | stb.Idle(0) 75 | inputBytes := []byte{1, 2, 3, 4} 76 | stb.AddLocalInput(handle, []byte{1, 2, 3, 4}, 4) 77 | var disconnectFlags int 78 | input, _ := stb.SyncInput(&disconnectFlags) 79 | got := input[0] 80 | want := inputBytes 81 | if !bytes.Equal(got, want) { 82 | t.Errorf("expected '%#v' but got '%#v'", want, got) 83 | } 84 | } 85 | 86 | func TestSyncTestBackendIncrementFramePanic(t *testing.T) { 87 | session := mocks.NewFakeSession() 88 | player := ggpo.NewLocalPlayer(20, 1) 89 | checkDistance := 8 90 | stb := ggpo.NewSyncTest(&session, 1, checkDistance, 4, true) 91 | var handle ggpo.PlayerHandle 92 | stb.AddPlayer(&player, &handle) 93 | defer func() { 94 | if r := recover(); r == nil { 95 | t.Errorf("The code did not panic wen trying to load a state that hadn't been saved") 96 | } 97 | }() 98 | for i := 0; i < checkDistance; i++ { 99 | stb.AdvanceFrame(ggpo.DefaultChecksum) 100 | } 101 | } 102 | 103 | func TestSyncTestBackendIncrementFrameCharacterization(t *testing.T) { 104 | session := mocks.NewFakeSession() 105 | player := ggpo.NewLocalPlayer(20, 1) 106 | checkDistance := 8 107 | stb := ggpo.NewSyncTest(&session, 1, checkDistance, 4, true) 108 | var handle ggpo.PlayerHandle 109 | stb.AddPlayer(&player, &handle) 110 | stb.Idle(0) 111 | inputBytes := []byte{1, 2, 3, 4} 112 | var disconnectFlags int 113 | for i := 0; i < checkDistance-1; i++ { 114 | stb.AddLocalInput(handle, inputBytes, 4) 115 | stb.SyncInput(&disconnectFlags) 116 | stb.AdvanceFrame(ggpo.DefaultChecksum) 117 | } 118 | 119 | defer func() { 120 | if r := recover(); r == nil { 121 | t.Errorf("The code did not panic due to a SyncError") 122 | } 123 | }() 124 | stb.AdvanceFrame(ggpo.DefaultChecksum) 125 | 126 | } 127 | 128 | // Attempt to simulate STB workflow in a test harness. Always panics on check 129 | // distance frame. Do not full understand why even though it works perfectly 130 | // fine in real time. 131 | func TestSyncTestBackendIncrementFrame(t *testing.T) { 132 | session := mocks.NewFakeSession() 133 | player := ggpo.NewLocalPlayer(20, 1) 134 | checkDistance := 8 135 | stb := ggpo.NewSyncTest(&session, 1, checkDistance, 4, true) 136 | var handle ggpo.PlayerHandle 137 | stb.AddPlayer(&player, &handle) 138 | inputBytes := []byte{1, 2, 3, 4} 139 | var disconnectFlags int 140 | var result error 141 | for i := 0; i < checkDistance-1; i++ { 142 | stb.Idle(0) 143 | result = stb.AddLocalInput(handle, inputBytes, 4) 144 | if result == nil { 145 | _, result = stb.SyncInput(&disconnectFlags) 146 | if result == nil { 147 | stb.AdvanceFrame(ggpo.DefaultChecksum) 148 | } 149 | } 150 | } 151 | 152 | defer func() { 153 | if r := recover(); r == nil { 154 | t.Errorf("The code did not panic due to a SyncError") 155 | } 156 | }() 157 | stb.AdvanceFrame(ggpo.DefaultChecksum) 158 | } 159 | 160 | /*Again, WIP, I don't know how to test that this is working, but it is. */ 161 | func TestSyncTestBackendChecksumCheck(t *testing.T) { 162 | session := mocks.NewFakeSessionWithBackend() 163 | var stb ggpo.SyncTest 164 | session.SetBackend(&stb) 165 | player := ggpo.NewLocalPlayer(20, 1) 166 | checkDistance := 8 167 | stb = ggpo.NewSyncTest(&session, 1, checkDistance, 4, true) 168 | 169 | var handle ggpo.PlayerHandle 170 | stb.AddPlayer(&player, &handle) 171 | inputBytes := []byte{1, 2, 3, 4} 172 | var disconnectFlags int 173 | var result error 174 | 175 | for i := 0; i < checkDistance+1; i++ { 176 | stb.Idle(0) 177 | result = stb.AddLocalInput(handle, inputBytes, 4) 178 | if result == nil { 179 | vals, result := stb.SyncInput(&disconnectFlags) 180 | if result == nil { 181 | session.Game.UpdateByInputs(vals) 182 | stb.AdvanceFrame(ggpo.DefaultChecksum) 183 | } 184 | } 185 | } 186 | } 187 | 188 | func TestSyncTestBackendMultiplePlayers(t *testing.T) { 189 | session := mocks.NewFakeSessionWithBackend() 190 | var stb ggpo.SyncTest 191 | session.SetBackend(&stb) 192 | player1 := ggpo.NewLocalPlayer(20, 1) 193 | player2 := ggpo.NewLocalPlayer(20, 2) 194 | checkDistance := 8 195 | stb = ggpo.NewSyncTest(&session, 2, checkDistance, 2, true) 196 | 197 | var handle1 ggpo.PlayerHandle 198 | stb.AddPlayer(&player1, &handle1) 199 | 200 | var handle2 ggpo.PlayerHandle 201 | stb.AddPlayer(&player2, &handle2) 202 | 203 | ib1 := []byte{2, 4} 204 | ib2 := []byte{1, 3} 205 | var disconnectFlags int 206 | var result error 207 | 208 | res1 := []float64{0, 0} 209 | res2 := []float64{0, 0} 210 | for i := 0; i < checkDistance+1; i++ { 211 | res1[0] += float64(ib1[0]) 212 | res1[1] += float64(ib1[1]) 213 | res2[0] += float64(ib2[0]) 214 | res2[1] += float64(ib2[1]) 215 | } 216 | 217 | for i := 0; i < checkDistance+1; i++ { 218 | stb.Idle(0) 219 | result = stb.AddLocalInput(handle1, ib1, 2) 220 | if result == nil { 221 | result = stb.AddLocalInput(handle2, ib2, 2) 222 | } 223 | if result == nil { 224 | vals, result := stb.SyncInput(&disconnectFlags) 225 | if result == nil { 226 | session.Game.UpdateByInputs(vals) 227 | stb.AdvanceFrame(ggpo.DefaultChecksum) 228 | } 229 | } 230 | } 231 | if session.Game.Players[0].X != res1[0] || session.Game.Players[0].Y != res1[1] || 232 | session.Game.Players[1].X != res2[0] || session.Game.Players[1].Y != res2[1] { 233 | t.Errorf("Invalid result in 2 Player SyncTest Session") 234 | } 235 | } 236 | 237 | // Unsupported functions 238 | func TestSyncTestBackendDissconnectPlayerError(t *testing.T) { 239 | session := mocks.NewFakeSession() 240 | checkDistance := 8 241 | stb := ggpo.NewSyncTest(&session, 1, checkDistance, 4, true) 242 | err := stb.DisconnectPlayer(ggpo.PlayerHandle(1)) 243 | if err == nil { 244 | t.Errorf("The code did not error when using an unsupported Feature.") 245 | } 246 | } 247 | 248 | func TestSyncTestBackendGetNetworkStatsError(t *testing.T) { 249 | session := mocks.NewFakeSession() 250 | checkDistance := 8 251 | stb := ggpo.NewSyncTest(&session, 1, checkDistance, 4, true) 252 | _, err := stb.GetNetworkStats(ggpo.PlayerHandle(1)) 253 | if err == nil { 254 | t.Errorf("The code did not error when using an unsupported Feature.") 255 | } 256 | } 257 | 258 | func TestSyncTestBackendSetFrameDelayError(t *testing.T) { 259 | session := mocks.NewFakeSession() 260 | checkDistance := 8 261 | stb := ggpo.NewSyncTest(&session, 1, checkDistance, 4, true) 262 | err := stb.SetFrameDelay(ggpo.PlayerHandle(1), 20) 263 | if err == nil { 264 | t.Errorf("The code did not error when using an unsupported Feature.") 265 | } 266 | } 267 | 268 | func TestSyncTestBackendSetDisconnectTimeoutError(t *testing.T) { 269 | session := mocks.NewFakeSession() 270 | checkDistance := 8 271 | stb := ggpo.NewSyncTest(&session, 1, checkDistance, 4, true) 272 | err := stb.SetDisconnectTimeout(20) 273 | if err == nil { 274 | t.Errorf("The code did not error when using an unsupported Feature.") 275 | } 276 | } 277 | 278 | func TestSyncTestBackendSetDisconnectNotifyStartError(t *testing.T) { 279 | session := mocks.NewFakeSession() 280 | checkDistance := 8 281 | stb := ggpo.NewSyncTest(&session, 1, checkDistance, 4, true) 282 | err := stb.SetDisconnectNotifyStart(20) 283 | if err == nil { 284 | t.Errorf("The code did not error when using an unsupported Feature.") 285 | } 286 | } 287 | 288 | func TestSyncTestBackendCloseError(t *testing.T) { 289 | session := mocks.NewFakeSession() 290 | checkDistance := 8 291 | stb := ggpo.NewSyncTest(&session, 1, checkDistance, 4, true) 292 | err := stb.Close() 293 | if err == nil { 294 | t.Errorf("The code did not error when using an unsupported Feature.") 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /transport/connection.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "github.com/assemblaj/ggpo/internal/messages" 5 | ) 6 | 7 | type Connection interface { 8 | SendTo(msg messages.UDPMessage, remoteIp string, remotePort int) 9 | Close() 10 | Read(messageChan chan MessageChannelItem) 11 | } 12 | 13 | type peerAddress struct { 14 | Ip string 15 | Port int 16 | } 17 | 18 | type MessageChannelItem struct { 19 | Peer peerAddress 20 | Message messages.UDPMessage 21 | Length int 22 | } 23 | -------------------------------------------------------------------------------- /transport/message_handler.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import "github.com/assemblaj/ggpo/internal/messages" 4 | 5 | type MessageHandler interface { 6 | HandleMessage(ipAddress string, port int, msg messages.UDPMessage, len int) 7 | } 8 | -------------------------------------------------------------------------------- /transport/udp.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "net" 5 | "strconv" 6 | 7 | "github.com/assemblaj/ggpo/internal/messages" 8 | "github.com/assemblaj/ggpo/internal/util" 9 | ) 10 | 11 | const ( 12 | MaxUDPEndpoints = 16 13 | MaxUDPPacketSize = 4096 14 | ) 15 | 16 | type Udp struct { 17 | Stats UdpStats // may not need this, may just be a service used by others 18 | 19 | socket net.Conn 20 | messageHandler MessageHandler 21 | listener net.PacketConn 22 | localPort int 23 | ipAddress string 24 | sendChan chan sendRequest 25 | } 26 | 27 | type UdpStats struct { 28 | BytesSent int 29 | PacketsSent int 30 | KbpsSent float64 31 | } 32 | 33 | func getPeerAddress(address net.Addr) peerAddress { 34 | switch addr := address.(type) { 35 | case *net.UDPAddr: 36 | return peerAddress{ 37 | Ip: addr.IP.String(), 38 | Port: addr.Port, 39 | } 40 | } 41 | return peerAddress{} 42 | } 43 | 44 | func (u Udp) Close() { 45 | if u.listener != nil { 46 | u.listener.Close() 47 | } 48 | } 49 | 50 | type sendRequest struct { 51 | msg messages.UDPMessage 52 | remoteIp string 53 | remotePort int 54 | } 55 | 56 | func NewUdp(messageHandler MessageHandler, localPort int) Udp { 57 | u := Udp{} 58 | 59 | u.sendChan = make(chan sendRequest, 256) // Create a buffered channel 60 | 61 | go func() { // Start a goroutine to handle sending of messages 62 | for req := range u.sendChan { 63 | RemoteEP := net.UDPAddr{IP: net.ParseIP(req.remoteIp), Port: req.remotePort} 64 | buf := req.msg.ToBytes() 65 | _, err := u.listener.WriteTo(buf, &RemoteEP) 66 | if err != nil { 67 | util.Log.Printf("WriteTo error: %s", err) 68 | } 69 | } 70 | }() 71 | 72 | u.messageHandler = messageHandler 73 | portStr := strconv.Itoa(localPort) 74 | 75 | u.localPort = localPort 76 | util.Log.Printf("binding udp socket to port %d.\n", localPort) 77 | u.listener, _ = net.ListenPacket("udp", "0.0.0.0:"+portStr) 78 | return u 79 | } 80 | 81 | // dst should be sockaddr 82 | // maybe create Gob encoder and decoder members 83 | // instead of creating them on each message send 84 | func (u Udp) SendTo(msg messages.UDPMessage, remoteIp string, remotePort int) { 85 | if msg == nil || remoteIp == "" { 86 | return 87 | } 88 | 89 | // RemoteEP := net.UDPAddr{IP: net.ParseIP(remoteIp), Port: remotePort} 90 | // buf := msg.ToBytes() 91 | // u.listener.WriteTo(buf, &RemoteEP) 92 | u.sendChan <- sendRequest{msg: msg, remoteIp: remoteIp, remotePort: remotePort} // Add the request to the channel 93 | } 94 | 95 | func (u Udp) Read(messageChan chan MessageChannelItem) { 96 | defer u.listener.Close() 97 | recvBuf := make([]byte, MaxUDPPacketSize*2) 98 | for { 99 | len, addr, err := u.listener.ReadFrom(recvBuf) 100 | if err != nil { 101 | util.Log.Printf("conn.Read error returned: %s\n", err) 102 | break 103 | } else if len <= 0 { 104 | util.Log.Printf("no data recieved\n") 105 | } else if len > 0 { 106 | util.Log.Printf("recvfrom returned (len:%d from:%s).\n", len, addr.String()) 107 | peer := getPeerAddress(addr) 108 | 109 | msg, err := messages.DecodeMessageBinary(recvBuf) 110 | if err != nil { 111 | util.Log.Printf("Error decoding message: %s", err) 112 | continue 113 | } 114 | messageChan <- MessageChannelItem{Peer: peer, Message: msg, Length: len} 115 | } 116 | 117 | } 118 | } 119 | 120 | func (u Udp) IsInitialized() bool { 121 | return u.listener != nil 122 | } 123 | --------------------------------------------------------------------------------