diff --git a/README.md b/README.md index 03af325..b52ff29 100644 --- a/README.md +++ b/README.md @@ -28,5 +28,6 @@ Please share issues and suggestions [here](https://code.rocketnine.space/tslocum ## Dependencies - [ebitengine](https://github.com/hajimehoshi/ebiten) - Game engine +- [ggpo-go](https://github.com/assemblaj/ggpo) - Rollback networking library - [gohan](https://code.rocketnine.space/tslocum/gohan) - Entity Component System framework - [etk](https://code.rocketnine.space/tslocum/etk) - Graphical User Interface toolkit diff --git a/flags.go b/flags.go new file mode 100644 index 0000000..0248f85 --- /dev/null +++ b/flags.go @@ -0,0 +1,46 @@ +//go:build !js || !wasm +// +build !js !wasm + +package main + +import ( + "flag" + "log" + "os" + + "code.rocketnine.space/tslocum/boxbrawl/world" + "github.com/assemblaj/ggpo" + "github.com/hajimehoshi/ebiten/v2" +) + +func parseFlags() { + var ( + fullscreen bool + hostAddress string + connectAddress string + printDebug bool + ) + flag.BoolVar(&fullscreen, "fullscreen", false, "run in fullscreen mode") + flag.StringVar(&hostAddress, "host", "", "start hosting a match on specified address:port") + flag.StringVar(&connectAddress, "connect", "", "connect to a match at specified address:port") + flag.IntVar(&world.LocalPort, "local", 0, "set local port (this is not normally required)") + flag.BoolVar(&printDebug, "debug", false, "enable printing debug messages") + flag.Parse() + + if fullscreen { + ebiten.SetFullscreen(true) + } + + if printDebug { + ggpo.SetLogger(log.New(os.Stderr, "GGPO ", log.Ldate|log.Ltime|log.Lmsgprefix)) + } + + if hostAddress != "" { + world.ConnectPromptHost = true + world.ConnectPromptText = hostAddress + world.ConnectPromptConfirmed = true + } else if connectAddress != "" { + world.ConnectPromptText = connectAddress + world.ConnectPromptConfirmed = true + } +} diff --git a/flags_web.go b/flags_web.go new file mode 100644 index 0000000..1ad7396 --- /dev/null +++ b/flags_web.go @@ -0,0 +1,15 @@ +//go:build js && wasm +// +build js,wasm + +package main + +import ( + "code.rocketnine.space/tslocum/boxbrawl/world" + "github.com/hajimehoshi/ebiten/v2" +) + +func parseFlags() { + world.WASM = true + + ebiten.SetFullscreen(true) +} diff --git a/game/game.go b/game/game.go index a5ba73c..42d9610 100644 --- a/game/game.go +++ b/game/game.go @@ -1,29 +1,69 @@ package game import ( + "crypto/sha1" + "image/color" "log" "os" + "strconv" + "strings" "code.rocketnine.space/tslocum/boxbrawl/entity" "code.rocketnine.space/tslocum/boxbrawl/system" "code.rocketnine.space/tslocum/boxbrawl/world" "code.rocketnine.space/tslocum/etk" "code.rocketnine.space/tslocum/gohan" + "github.com/assemblaj/ggpo" "github.com/hajimehoshi/ebiten/v2" ) -type Game struct{} +var backend ggpo.Backend -func NewGame() (*Game, error) { - g := &Game{} +var currentPlayer = 1 - entity.NewOnceEntity() +type Game struct { + Players []Player +} - gohan.AddSystem(&system.UISystem{}) +var addedGame bool + +func NewGame() (*Game, error) { + var player1 = Player{ + X: 50, + Y: 50, + Color: color.RGBA{255, 0, 0, 255}, + PlayerNum: 1, + } + var player2 = Player{ + X: 150, + Y: 50, + Color: color.RGBA{0, 0, 255, 255}, + PlayerNum: 2, + } + g := &Game{ + Players: []Player{player1, player2}, + } + + if !addedGame { + entity.NewOnceEntity() + gohan.AddSystem(&system.UISystem{}) + addedGame = true + } return g, nil } +func (g *Game) clone() (result *Game) { + result = &Game{} + *result = *g + + result.Players = make([]Player, len(g.Players)) + for i := range g.Players { + result.Players[i] = g.Players[i].clone() + } + return +} + func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { if outsideWidth != world.ScreenWidth || outsideHeight != world.ScreenHeight { if world.ScreenWidth != 0 || world.ScreenHeight != 0 { @@ -35,11 +75,71 @@ func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeigh } func (g *Game) Update() error { - if ebiten.IsWindowBeingClosed() { + if ebiten.IsWindowBeingClosed() || (!world.WASM && ebiten.IsKeyPressed(ebiten.KeyEscape)) { g.Exit() return nil } + if world.ConnectPromptConfirmed && !world.ConnectPromptActive { + address := "" + port := world.ConnectPromptText + if strings.ContainsRune(port, ':') { + split := strings.Split(port, ":") + if len(split) == 2 { + address = split[0] + port = split[1] + } + } + p, err := strconv.Atoi(port) + if err != nil { + log.Fatalf("failed to read port: %s", err) + } + + log.Println("start networking") + + localPort := world.LocalPort + if localPort == 0 { + localPort = p + } + + numPlayers := 2 + playerSize := 20 + players := make([]ggpo.Player, numPlayers) + if world.ConnectPromptHost { + log.Printf("Hosting at " + address + ":" + port + "...") + + players[0] = ggpo.NewLocalPlayer(playerSize, 1) + players[1] = ggpo.NewRemotePlayer(playerSize, 2, "127.0.0.1", localPort) + } else { + log.Printf("Connecting to " + address + ":" + port + "...") + + players[0] = ggpo.NewRemotePlayer(playerSize, 1, address, p) + players[1] = ggpo.NewLocalPlayer(playerSize, 2) + currentPlayer = 2 + } + + if world.LocalPort != 0 { + log.Printf("Client port for connection: %d", world.LocalPort) + } + + l := p + if !world.ConnectPromptHost { + l = localPort + } + + g.InitNetworking(l, numPlayers, players, 0) + + world.ConnectPromptActive = true + } + + if world.ConnectPromptActive { + err := backend.Idle(0) // TODO Why 0? + if err != nil { + panic(err) + } + g.RunFrame() + } + return gohan.Update() } @@ -53,3 +153,152 @@ func (g *Game) Draw(screen *ebiten.Image) { func (g *Game) Exit() { os.Exit(0) } + +func (g *Game) InitNetworking(localPort int, numPlayers int, players []ggpo.Player, numSpectators int) { + var result error + var inputBits InputBits = 0 + var inputSize int = len(encodeInputs(inputBits)) + + session := NewGameSession() + + peer := ggpo.NewPeer(session, localPort, numPlayers, inputSize) + //peer := ggpo.NewSyncTest(&session, numPlayers, 8, inputSize, true) + backend = &peer + session.backend = backend + peer.InitializeConnection() + peer.Start() + + //session.SetDisconnectTimeout(3000) + //session.SetDisconnectNotifyStart(1000) + + for i := 0; i < numPlayers+numSpectators; i++ { + var handle ggpo.PlayerHandle + result = peer.AddPlayer(&players[i], &handle) + if players[i].PlayerType == ggpo.PlayerTypeLocal { + currentPlayer = int(handle) + } + if result != nil { + log.Fatalf("There's an issue from AddPlayer") + } + if players[i].PlayerType == ggpo.PlayerTypeLocal { + peer.SetFrameDelay(handle, FRAME_DELAY) + } + } + peer.SetDisconnectTimeout(3000) + peer.SetDisconnectNotifyStart(1000) +} + +func (g *Game) RunFrame() { + input := g.ReadInputs() + buffer := encodeInputs(input) + + //fmt.Println("Attempting to add local inputs") + result := backend.AddLocalInput(ggpo.PlayerHandle(currentPlayer), buffer, len(buffer)) + + //fmt.Println("Attempt to add local inputs complete") + if result == nil { + //fmt.Println("Attempt to add local inputs was successful") + var values [][]byte + disconnectFlags := 0 + + //fmt.Println("Attempting to synchronize inputs") + values, result = backend.SyncInput(&disconnectFlags) + if result == nil { + //fmt.Println("Attempt synchronize inputs was sucessful") + inputs := decodeInputs(values) + //fmt.Println("Advancing Frame from game loop") + g.AdvanceFrame(inputs, disconnectFlags) + } else { + //fmt.Printf("Attempt synchronize inputs was unsuccessful: %s\n", result) + } + } else { + //fmt.Printf("Attempt to add local inputs unsuccessful: %s\n", result) + } +} + +func (g *Game) AdvanceFrame(inputs []InputBits, disconnectFlags int) { + log.Println("ADVANCE FRAME") + + g.UpdateByInputs(inputs) + err := backend.AdvanceFrame(uint32(g.Checksum())) + if err != nil { + panic(err) + } +} + +func (g *Game) UpdateByInputs(inputs []InputBits) { + for i, input := range inputs { + if input.isButtonOn(int(ebiten.KeyArrowUp)) { + g.Players[i].Y-- + } + if input.isButtonOn(int(ebiten.KeyArrowDown)) { + g.Players[i].Y++ + } + if input.isButtonOn(int(ebiten.KeyArrowLeft)) { + g.Players[i].X-- + } + if input.isButtonOn(int(ebiten.KeyArrowRight)) { + g.Players[i].X++ + } + if input.isButtonOn(int(ebiten.KeyW)) { + g.Players[i].Y-- + } + if input.isButtonOn(int(ebiten.KeyS)) { + g.Players[i].Y++ + } + if input.isButtonOn(int(ebiten.KeyA)) { + g.Players[i].X-- + } + if input.isButtonOn(int(ebiten.KeyD)) { + g.Players[i].X++ + } + } +} + +func (g *Game) ReadInputs() InputBits { + var in InputBits + + if ebiten.IsKeyPressed(ebiten.KeyArrowUp) { + in.setButton(int(ebiten.KeyArrowUp)) + } + if ebiten.IsKeyPressed(ebiten.KeyArrowDown) { + in.setButton(int(ebiten.KeyArrowDown)) + } + if ebiten.IsKeyPressed(ebiten.KeyArrowLeft) { + in.setButton(int(ebiten.KeyArrowLeft)) + } + if ebiten.IsKeyPressed(ebiten.KeyArrowRight) { + in.setButton(int(ebiten.KeyArrowRight)) + } + return in +} + +func (g *Game) ReadInputsP2() InputBits { + var in InputBits + + if ebiten.IsKeyPressed(ebiten.KeyW) { + in.setButton(int(ebiten.KeyW)) + } + if ebiten.IsKeyPressed(ebiten.KeyS) { + in.setButton(int(ebiten.KeyS)) + } + if ebiten.IsKeyPressed(ebiten.KeyA) { + in.setButton(int(ebiten.KeyA)) + } + if ebiten.IsKeyPressed(ebiten.KeyD) { + in.setButton(int(ebiten.KeyD)) + } + return in + +} + +func (g *Game) Checksum() int { + h := sha1.New() + h.Write([]byte(g.String())) + toSum := h.Sum(nil) + sum := 0 + for _, v := range toSum { + sum += int(v) + } + return sum +} diff --git a/game/inputs.go b/game/inputs.go new file mode 100644 index 0000000..beea2c0 --- /dev/null +++ b/game/inputs.go @@ -0,0 +1,90 @@ +package game + +import ( + "bytes" + "encoding/gob" + "fmt" + "log" +) + +type Input struct { + ButtonMap int +} + +type InputBits int + +func (i *InputBits) isButtonOn(button int) bool { + return *i&(1< 0 +} + +func (i *InputBits) setButton(button int) { + *i |= (1 << button) +} + +func readI32(b []byte) int32 { + if len(b) < 4 { + return 0 + } + return int32(b[0]) | int32(b[1])<<8 | int32(b[2])<<16 | int32(b[3])<<24 +} + +func writeI32(i32 int32) []byte { + b := []byte{byte(i32), byte(i32 >> 8), byte(i32 >> 16), byte(i32 >> 24)} + return b +} + +func (i *Input) isButtonOn(button int) bool { + return i.ButtonMap&(1< 0 +} + +func (i *Input) setButton(button int) { + i.ButtonMap |= (1 << button) +} + +func (i Input) String() string { + return fmt.Sprintf("Input %d", i.ButtonMap) +} + +func NewInput() Input { + return Input{} +} + +func encodeInputs(inputs InputBits) []byte { + return writeI32(int32(inputs)) +} + +func decodeInputs(buffer [][]byte) []InputBits { + var inputs = make([]InputBits, len(buffer)) + for i, b := range buffer { + inputs[i] = InputBits(readI32(b)) + } + return inputs +} + +func decodeInputsGob(buffer [][]byte) []Input { + var inputs = make([]Input, len(buffer)) + for i, b := range buffer { + var buf bytes.Buffer = *bytes.NewBuffer(b) + dec := gob.NewDecoder(&buf) + err := dec.Decode(&inputs[i]) + if err != nil { + log.Printf("decode error: %s. Returning empty input\n", err) + // hack + inputs[i] = NewInput() + //panic("eof") + } else { + log.Printf("inputs properly decoded: %s\n", inputs[i]) + } + } + return inputs +} + +func encodeInputsGob(inputs Input) []byte { + var buf bytes.Buffer + enc := gob.NewEncoder(&buf) + err := enc.Encode(&inputs) + if err != nil { + log.Fatal("encode error ", err) + } + return buf.Bytes() +} diff --git a/game/player.go b/game/player.go new file mode 100644 index 0000000..2001ea9 --- /dev/null +++ b/game/player.go @@ -0,0 +1,19 @@ +package game + +import "image/color" + +type Player struct { + X float64 + Y float64 + Color color.Color + PlayerNum int +} + +func (p *Player) clone() Player { + result := Player{} + result.X = p.X + result.Y = p.Y + result.Color = p.Color + result.PlayerNum = p.PlayerNum + return result +} diff --git a/game/session.go b/game/session.go new file mode 100644 index 0000000..d7e0c6b --- /dev/null +++ b/game/session.go @@ -0,0 +1,102 @@ +package game + +import ( + "bytes" + "crypto/md5" + "encoding/gob" + "fmt" + "log" + + "github.com/assemblaj/ggpo" +) + +const FRAME_DELAY int = 2 + +type GameSession struct { + backend ggpo.Backend + game *Game + saveStates map[int]*Game +} + +func NewGameSession() *GameSession { + g := &GameSession{} + game, _ := NewGame() + g.game = game + g.saveStates = make(map[int]*Game) + return g +} + +func (g *GameSession) SaveGameState(stateID int) int { + g.saveStates[stateID] = g.game.clone() + checksum := calculateChecksum([]byte(g.saveStates[stateID].String())) + return checksum +} + +func calculateChecksum(buffer []byte) int { + cSum := md5.Sum(buffer) + checksum := 0 + for i := 0; i < len(cSum); i++ { + checksum += int(cSum[i]) + } + return checksum +} + +func (g *GameSession) LoadGameState(stateID int) { + *g.game = *g.saveStates[stateID] +} + +func (g *GameSession) LogGameState(fileName string, buffer []byte, len int) { + var game2 Game + var buf bytes.Buffer = *bytes.NewBuffer(buffer) + dec := gob.NewDecoder(&buf) + err := dec.Decode(&game2) + if err != nil { + log.Fatal("decode error:", err) + } + log.Printf("%s Game State: %s\n", fileName, game2.String()) +} + +func (g *GameSession) SetBackend(backend ggpo.Backend) { +} + +func (g *Game) String() string { + return fmt.Sprintf("%s : %s ", g.Players[0].String(), g.Players[1].String()) +} + +func (p *Player) String() string { + return fmt.Sprintf("Player %d: X:%f Y:%f Color: %s", p.PlayerNum, p.X, p.Y, p.Color) +} + +func (g *GameSession) AdvanceFrame(flags int) { + fmt.Println("Advancing frame from callback. ") + var discconectFlags int + + // Make sure we fetch the inputs from GGPO and use these to update + // the game state instead of reading from the keyboard. + inputs, result := g.backend.SyncInput(&discconectFlags) + if result == nil { + input := decodeInputs(inputs) + g.game.AdvanceFrame(input, discconectFlags) + } +} + +func (g *GameSession) OnEvent(info *ggpo.Event) { + switch info.Code { + case ggpo.EventCodeConnectedToPeer: + log.Println("EventCodeConnectedToPeer") + case ggpo.EventCodeSynchronizingWithPeer: + log.Println("EventCodeSynchronizingWithPeer") + case ggpo.EventCodeSynchronizedWithPeer: + log.Println("EventCodeSynchronizedWithPeer") + case ggpo.EventCodeRunning: + log.Println("EventCodeRunning") + case ggpo.EventCodeDisconnectedFromPeer: + log.Println("EventCodeDisconnectedFromPeer") + case ggpo.EventCodeTimeSync: + log.Println("EventCodeTimeSync") + case ggpo.EventCodeConnectionInterrupted: + log.Println("EventCodeconnectionInterrupted") + case ggpo.EventCodeConnectionResumed: + log.Println("EventCodeconnectionInterrupted") + } +} diff --git a/go.mod b/go.mod index 241ccd8..dec7a07 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.19 require ( code.rocketnine.space/tslocum/etk v0.0.0-20230103193701-368514415e01 code.rocketnine.space/tslocum/gohan v1.0.0 + github.com/assemblaj/ggpo v0.0.0-20230105182823-b13b11d28a8e github.com/hajimehoshi/ebiten/v2 v2.4.15 ) @@ -14,9 +15,10 @@ require ( github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect github.com/hajimehoshi/file2byteslice v1.0.0 // indirect github.com/jezek/xgb v1.1.0 // indirect - golang.org/x/exp/shiny v0.0.0-20221230185412-738e83a70c30 // indirect - golang.org/x/image v0.2.0 // indirect + golang.org/x/exp v0.0.0-20230105000112-eab7a2c85304 // indirect + golang.org/x/exp/shiny v0.0.0-20230105000112-eab7a2c85304 // indirect + golang.org/x/image v0.3.0 // indirect golang.org/x/mobile v0.0.0-20221110043201-43a038452099 // indirect - golang.org/x/sys v0.3.0 // indirect - golang.org/x/text v0.5.0 // indirect + golang.org/x/sys v0.4.0 // indirect + golang.org/x/text v0.6.0 // indirect ) diff --git a/go.sum b/go.sum index 59c3265..8830a5a 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,8 @@ code.rocketnine.space/tslocum/gohan v1.0.0/go.mod h1:12yOt5Ygl/RVwnnZSVZRuS1W6gC code.rocketnine.space/tslocum/messeji v1.0.2 h1:3/68FnXWaBDMhfUGb8FvNpVgAHY8DX+VL7pyA/CcY94= code.rocketnine.space/tslocum/messeji v1.0.2/go.mod h1:bSXsyjvKhFXQ7GsUxWZdO2JX83xOT/VTqFCR04thk+c= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/assemblaj/ggpo v0.0.0-20230105182823-b13b11d28a8e h1:d/uJpkRgHj5EJ06CX0TFFQLfvAeHlfWY+xfLUBkVeus= +github.com/assemblaj/ggpo v0.0.0-20230105182823-b13b11d28a8e/go.mod h1:ZKiAYEZgxDlGHGeP/VZsv1+xIRo9kQpgUFmjP/PR0lQ= github.com/ebitengine/purego v0.0.0-20220905075623-aeed57cda744/go.mod h1:Eh8I3yvknDYZeCuXH9kRNaPuHEwvXDCk378o9xszmHg= github.com/ebitengine/purego v0.1.1 h1:HI8nW+LniW9Yb34k34jBs8nz+PNzsw68o7JF8jWFHHE= github.com/ebitengine/purego v0.1.1/go.mod h1:Eh8I3yvknDYZeCuXH9kRNaPuHEwvXDCk378o9xszmHg= @@ -36,13 +38,15 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= -golang.org/x/exp/shiny v0.0.0-20221230185412-738e83a70c30 h1:GoCh95fUWm4yMPxPDXFjbjwUFoO+RJy052MW5+PWh3s= -golang.org/x/exp/shiny v0.0.0-20221230185412-738e83a70c30/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0= +golang.org/x/exp v0.0.0-20230105000112-eab7a2c85304 h1:YUqj+XKtfrn3kXjFIiZ8jwKROD7ioAOOHUuo3ZZ2opc= +golang.org/x/exp v0.0.0-20230105000112-eab7a2c85304/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp/shiny v0.0.0-20230105000112-eab7a2c85304 h1:ezMmyIKsGPwRz+IHa53wCpw87I2TremhqQ8o79ytDEk= +golang.org/x/exp/shiny v0.0.0-20230105000112-eab7a2c85304/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.1.0/go.mod h1:iyPr49SD/G/TBxYVB/9RRtGUT5eNbo2u4NamWeQcD5c= -golang.org/x/image v0.2.0 h1:/DcQ0w3VHKCC5p0/P2B0JpAZ9Z++V2KOo2fyU89CXBQ= -golang.org/x/image v0.2.0/go.mod h1:la7oBXb9w3YFjBqaAwtynVioc1ZvOnNteUNrifGNmAI= +golang.org/x/image v0.3.0 h1:HTDXbdK9bjfSWkPzDJIw89W8CAtfFGduujWs33NLLsg= +golang.org/x/image v0.3.0/go.mod h1:fXd9211C/0VTlYuAcOhW8dY/RtEJqODXOWBDpmYBf+A= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mobile v0.0.0-20220722155234-aaac322e2105/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ= @@ -73,8 +77,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220818161305-2296e01440c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -82,8 +86,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= diff --git a/main.go b/main.go index e312b2b..8b22545 100644 --- a/main.go +++ b/main.go @@ -20,18 +20,13 @@ func main() { ebiten.SetTPS(world.TPS) ebiten.SetRunnableOnUnfocused(true) - //parseFlags() + parseFlags() g, err := game.NewGame() if err != nil { log.Fatal(err) } - // TODO - if false { - world.ConnectPromptVisible = true - } - sigc := make(chan os.Signal, 1) signal.Notify(sigc, syscall.SIGINT, diff --git a/system/ui.go b/system/ui.go index 5758daf..775eb68 100644 --- a/system/ui.go +++ b/system/ui.go @@ -2,7 +2,6 @@ package system import ( "fmt" - "log" "code.rocketnine.space/tslocum/boxbrawl/component" "code.rocketnine.space/tslocum/boxbrawl/world" @@ -17,8 +16,15 @@ const uiStartPrompt = `Box Brawl ` const uiComputerPrompt = `Press to play against the computer.` const uiHostPrompt = `Press to host a match against a remote player.` +const uiHostInfoPrompt = `Type a port to host a match against a remote player.` +const uiHostStartPrompt = `Press to start hosting.` const uiRemotePrompt = `Type an IP address and port (address:port) to play against a remote player.` const uiConnectPrompt = `Press to connect.` +const uiBrowserPrompt = `Playing against remote players is unavailable in the browser version. + +Download Box Brawl for Windows or Linux to play against remote players.` +const uiHostListeningPrompt = `Waiting for a connection on %s...` +const uiClientConnectingPrompt = `Connecting to %s...` type UISystem struct { *component.Once @@ -38,7 +44,6 @@ func (u *UISystem) initialize() { inputDemo.AddChild(u.buffer) etk.SetRoot(inputDemo) - etk.Layout(world.ScreenWidth, world.ScreenHeight) u.debugImg = ebiten.NewImage(128, 128) @@ -49,15 +54,31 @@ func (u *UISystem) initialize() { func (u *UISystem) updateBuffer() { prompt := []byte(uiStartPrompt) - promptEntered := len(world.ConnectPromptText) != 0 - if promptEntered { - prompt = append(prompt, []byte("\n\n"+world.ConnectPromptText)...) - prompt = append(prompt, []byte("\n\n"+uiConnectPrompt)...) - prompt = append(prompt, []byte("\n\n"+uiRemotePrompt)...) - } else { + if world.WASM { prompt = append(prompt, []byte("\n\n"+uiComputerPrompt)...) - prompt = append(prompt, []byte("\n\n"+uiHostPrompt)...) - prompt = append(prompt, []byte("\n\n"+uiRemotePrompt)...) + prompt = append(prompt, []byte("\n\n"+uiBrowserPrompt)...) + } else if world.ConnectPromptActive { + if world.ConnectPromptHost { + prompt = append(prompt, []byte("\n\n"+fmt.Sprintf(uiHostListeningPrompt, world.ConnectPromptText))...) + } else { + prompt = append(prompt, []byte("\n\n"+fmt.Sprintf(uiClientConnectingPrompt, world.ConnectPromptText))...) + } + } else { + promptEntered := len(world.ConnectPromptText) != 0 + if promptEntered || world.ConnectPromptHost { + prompt = append(prompt, []byte("\n\n"+world.ConnectPromptText)...) + if world.ConnectPromptHost { + prompt = append(prompt, []byte("\n\n"+uiHostInfoPrompt)...) + prompt = append(prompt, []byte("\n\n"+uiHostStartPrompt)...) + } else { + prompt = append(prompt, []byte("\n\n"+uiConnectPrompt)...) + prompt = append(prompt, []byte("\n\n"+uiRemotePrompt)...) + } + } else { + prompt = append(prompt, []byte("\n\n"+uiComputerPrompt)...) + prompt = append(prompt, []byte("\n\n"+uiHostPrompt)...) + prompt = append(prompt, []byte("\n\n"+uiRemotePrompt)...) + } } u.buffer.Clear() @@ -73,55 +94,66 @@ func (u *UISystem) Update(e gohan.Entity) error { return nil } - var a string - if inpututil.IsKeyJustPressed(ebiten.KeyDigit0) { - a += "0" - } - if inpututil.IsKeyJustPressed(ebiten.KeyDigit1) { - a += "1" - } - if inpututil.IsKeyJustPressed(ebiten.KeyDigit2) { - a += "2" - } - if inpututil.IsKeyJustPressed(ebiten.KeyDigit3) { - a += "3" - } - if inpututil.IsKeyJustPressed(ebiten.KeyDigit4) { - a += "4" - } - if inpututil.IsKeyJustPressed(ebiten.KeyDigit5) { - a += "5" - } - if inpututil.IsKeyJustPressed(ebiten.KeyDigit6) { - a += "6" - } - if inpututil.IsKeyJustPressed(ebiten.KeyDigit7) { - a += "7" - } - if inpututil.IsKeyJustPressed(ebiten.KeyDigit8) { - a += "8" - } - if inpututil.IsKeyJustPressed(ebiten.KeyDigit9) { - a += "9" - } - if inpututil.IsKeyJustPressed(ebiten.KeyPeriod) { - a += "." - } - if inpututil.IsKeyJustPressed(ebiten.KeySemicolon) && ebiten.IsKeyPressed(ebiten.KeyShift) { - a += ":" - } - if a != "" { - world.ConnectPromptText += a - u.updateBuffer() - } - if inpututil.IsKeyJustPressed(ebiten.KeyBackspace) && len(world.ConnectPromptText) != 0 { - world.ConnectPromptText = world.ConnectPromptText[:len(world.ConnectPromptText)-1] - u.updateBuffer() + if !world.WASM { + if len(world.ConnectPromptText) == 0 && ebiten.IsKeyPressed(ebiten.KeyH) { + world.ConnectPromptHost = true + u.updateBuffer() + } + + var a string + if inpututil.IsKeyJustPressed(ebiten.KeyDigit0) { + a += "0" + } + if inpututil.IsKeyJustPressed(ebiten.KeyDigit1) { + a += "1" + } + if inpututil.IsKeyJustPressed(ebiten.KeyDigit2) { + a += "2" + } + if inpututil.IsKeyJustPressed(ebiten.KeyDigit3) { + a += "3" + } + if inpututil.IsKeyJustPressed(ebiten.KeyDigit4) { + a += "4" + } + if inpututil.IsKeyJustPressed(ebiten.KeyDigit5) { + a += "5" + } + if inpututil.IsKeyJustPressed(ebiten.KeyDigit6) { + a += "6" + } + if inpututil.IsKeyJustPressed(ebiten.KeyDigit7) { + a += "7" + } + if inpututil.IsKeyJustPressed(ebiten.KeyDigit8) { + a += "8" + } + if inpututil.IsKeyJustPressed(ebiten.KeyDigit9) { + a += "9" + } + if inpututil.IsKeyJustPressed(ebiten.KeyPeriod) { + a += "." + } + if inpututil.IsKeyJustPressed(ebiten.KeySemicolon) && ebiten.IsKeyPressed(ebiten.KeyShift) { + a += ":" + } + if a != "" { + world.ConnectPromptText += a + u.updateBuffer() + } + if inpututil.IsKeyJustPressed(ebiten.KeyBackspace) { + if len(world.ConnectPromptText) != 0 { + world.ConnectPromptText = world.ConnectPromptText[:len(world.ConnectPromptText)-1] + u.updateBuffer() + } else if world.ConnectPromptHost { + world.ConnectPromptHost = false + u.updateBuffer() + } + } } if inpututil.IsKeyJustPressed(ebiten.KeyEnter) || inpututil.IsKeyJustPressed(ebiten.KeyKPEnter) { - log.Println("Start game") - return nil + world.ConnectPromptConfirmed = true } return etk.Update() diff --git a/world/world.go b/world/world.go index 7363fdd..a389ca8 100644 --- a/world/world.go +++ b/world/world.go @@ -10,9 +10,15 @@ const ( var ( ScreenWidth, ScreenHeight = 0, 0 + LocalPort int + ConnectPromptVisible = true ConnectPromptText string + ConnectPromptHost bool ConnectPromptConfirmed bool + ConnectPromptActive bool Debug = 1 + + WASM bool )