├── .gitignore ├── LICENSE.md ├── README.md ├── cmd ├── asset-server │ └── asset-server.go └── dev-server │ ├── dev-server.go │ └── internal │ └── devwebserver │ ├── devwebserver.go │ ├── index.html │ └── wasm_exec.js ├── dist ├── index.html └── wasm_exec.js ├── go.mod ├── go.sum ├── internal ├── app │ └── app.go ├── asset │ ├── asset.go │ ├── background.png │ ├── player_idle_left.png │ └── player_idle_right.png ├── ent │ └── player.go ├── input │ ├── input.go │ ├── input_headless.go │ └── input_noheadless.go ├── monotime │ ├── monotime.go │ ├── monotime_js.go │ ├── monotime_nojs.go │ └── monotime_windows.go ├── netcode │ ├── client │ │ └── client.go │ ├── client_or_server │ │ ├── client_or_server_client.go │ │ └── client_or_server_server.go │ ├── netcode.go │ ├── netconf │ │ └── netconf.go │ ├── netconst │ │ └── netconst.go │ ├── packbuf │ │ ├── packbuf_test.go │ │ ├── packbufread.go │ │ └── packbufwrite.go │ ├── packs │ │ ├── packs.go │ │ └── packs_test.go │ ├── rtt │ │ ├── rtt.go │ │ └── rtt_test.go │ └── server │ │ └── server.go ├── netdriver │ └── webrtcdriver │ │ ├── webrtcclient │ │ └── webrtcclient.go │ │ ├── webrtcserver │ │ ├── stunserver │ │ │ └── stunserver.go │ │ └── webrtcserver.go │ │ └── webrtcshared │ │ └── webrtcshared.go ├── renderer │ ├── internal │ │ ├── ebiten │ │ │ └── ebiten.go │ │ ├── headless │ │ │ └── headless.go │ │ └── rendereriface │ │ │ └── rendereriface.go │ ├── renderer.go │ ├── renderer_ebiten.go │ └── renderer_headless.go └── world │ └── world.go ├── main.go └── screens └── web-browser-and-server.png /.gitignore: -------------------------------------------------------------------------------- 1 | *.zip 2 | /toy-webrtc-mmo.exe 3 | /toy-webrtc-mmo.exe~ 4 | /toy-webrtc-mmo 5 | /server 6 | /server.exe 7 | /server.exe~ 8 | /dev-server 9 | /dev-server.exe 10 | /cmd/serve/serve 11 | /cmd/serve/serve.exe 12 | /asset-server 13 | /asset-server.exe 14 | /dist/main.wasm 15 | /TODO.md 16 | /.vscode 17 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2021 Jae Bentvelzen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ebiten WebRTC Toy MMO 2 | 3 | ⚠️ *This is a piece of incomplete hobby work and not robust. Please read the "Why does this project exist?" section.* 4 | 5 | ## What is this project? 6 | 7 | This is an authoritative server model demonstrating a very simple platformer game where players can move left, right and jump around. When running from the client from the browser, which in my testing, works fine on Chrome, Firefox and on Safari with my iPhone 6S. 8 | 9 | ![screenshot of game running in a browser with the server running natively over the top](./screens/web-browser-and-server.png) 10 | 11 | **Controls:** 12 | 13 | - Move Left = Left Arrow Key or Mouse/Touch Left Side of Screen 14 | - Move Right = Right Arrow Key or Mouse/Touch Right Side of Screen 15 | - Jump = Spacebar or Mouse/Touch Middle of Screen 16 | 17 | ## Why does this project exist? 18 | 19 | This is a project that exists because I wanted practice applying concepts blogged about on [Gaffer On Games](https://gafferongames.com/), get better at networking code and leverage WebRTC to get UDP in the browser, as [UDP is recommended over TCP for real-time games](https://gafferongames.com/post/why_cant_i_send_udp_packets_from_a_browser/). 20 | 21 | I consider myself to be game development hobbyist who isn't that experienced, so this project is likely to have code smells, mistakes or whatever else. However, I still thought it'd be valuable to put online, even in it's current incomplete state, because I've personally found that half-baked game developer examples get me closer to solving problems I wanted to solve, even if they lack correctness. 22 | 23 | ## Known problems and design choices 24 | 25 | Here's a list of design choices made as well as known problems. There's more I'm probably not thinking of but hopefully they're somewhat commented in the code. 26 | 27 | - We don't timeout the connection of the client reliably. It can hang trying to connect if UDP ports are blocked on either the server or client-side as it'll never end up opening a Data Channel. 28 | - We don't support `ICERestart`, ie. if someones connection shifts from WiFi to 4G, the connection will probably be lost. 29 | - We haven't thought about making the jitter buffer nice for getting client state from the server, so I'm not sure how smooth other players movement will be in poorer network conditions. 30 | - The server doesn't reflect clients leaving on other clients. 31 | - If the server is closed, the clients aren't notified or booted out. 32 | - We chose to create packet data using Go structs and reflection instead of protobuf as protobuf comes with the overhead of requiring additional tools for code generation and adds a non-trivial amount of byte overhead. A [Gaffer On Games article](https://gafferongames.com/post/reading_and_writing_packets/) goes into detail on why hand-rolling packet types once you know your data is the better option. We didn't end up doing any sort of compression on packet data in this project. 33 | 34 | ## How to run locally and develop 35 | 36 | The following commands need to be run from the root directory of the project. 37 | 38 | ### Server 39 | 40 | ```sh 41 | go build -tags "server" -o server && ./server 42 | ``` 43 | 44 | We also have a headless server option which allows us to run on machines without graphic rendering capabilities. 45 | 46 | ```sh 47 | go build -tags "server headless" -o server && ./server 48 | ``` 49 | 50 | ### Client 51 | 52 | #### Native 53 | 54 | ```sh 55 | go build && ./toy-webrtc-mmo 56 | ``` 57 | 58 | #### Web 59 | 60 | Start a web server that will serve the game client on http://localhost:8080/. This will rebuild the client everytime you refresh the page. 61 | 62 | ```sh 63 | go build -o dev-server ./cmd/dev-server && ./dev-server 64 | ``` 65 | 66 | ## How to deploy and server configuration 67 | 68 | ### Client 69 | 70 | 1.) Build WASM binary 71 | 72 | ```sh 73 | GOOS=js GOARCH=wasm go build -o dist/main.wasm 74 | ``` 75 | 76 | 2.) Zip up contents and put on a host somewhere. I put it on my Amazon EC2 instance and then served it with the "Asset Server" below. 77 | 78 | ### Server 79 | 80 | This command builds a headless server which has two advantages: 81 | 82 | - No overhead from rendering or draw calls 83 | - Cross-compilation with environment variables just works 84 | 85 | ``` 86 | go build -tags "server headless" -o server 87 | ``` 88 | 89 | ### Asset Server (optional)** 90 | 91 | This is the server that serves the asset files for the web client: 92 | 93 | - index.html 94 | - main.wasm 95 | - wasm_exec.js 96 | 97 | 1) Build asset-server from the "cmd/asset-server" folder 98 | 99 | ``` 100 | go build -o asset-server ./cmd/asset-server 101 | ``` 102 | 103 | 2) Copy both "asset-server" and the "dist" folder to your server 104 | 105 | 3) Run the "asset-server" on your server. For me, my server was an Amazon EC2 instance. 106 | 107 | ``` 108 | ./asset-server 109 | ``` 110 | 111 | ### Server configuration 112 | 113 | | Type | Port | Description | 114 | | ----------- | ----------- | ----------- | 115 | | TCP | 50000 | Allow HTTP POST access to get WebRTC SDP / ConnectAuth | 116 | | TCP | 8080 | (Optional if you serve web files elsewhere) If using Asset Server, allow HTTP access for the web game client (serving assets, WASM file) | 117 | | UDP | 3478 | Allow STUN server | 118 | | UDP | 10000 - 11999 | UDP ports used by WebRTC DataChannels (We called `SetEphemeralUDPPortRange` in our code to make the UDP port range predictable / lockdownable) | 119 | 120 | 121 | ## Credits 122 | 123 | * [Pions WebRTC Authors and Contributors](https://github.com/pion/webrtc) for all their hard work on the Golang WebRTC libraries and allowing me to get this working in pure Go. 124 | * [Glenn Fiedler](https://gafferongames.com/) for all their detailed blog posts on doing netcode, including *why you dont do* certain things. 125 | * [Chen Tao](https://github.com/jagt) for their tool [clumsy](https://jagt.github.io/clumsy/), which has been invaluable for simulating and debugging weird network conditions on Windows. 126 | -------------------------------------------------------------------------------- /cmd/asset-server/asset-server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | ) 7 | 8 | // main will start serving all files in the "dist" folder on the server 9 | // on port 8080 10 | func main() { 11 | fs := http.FileServer(http.Dir("./dist")) 12 | http.Handle("/", fs) 13 | 14 | log.Println("Listening on :8080...") 15 | err := http.ListenAndServe(":8080", nil) 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /cmd/dev-server/dev-server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/silbinarywolf/toy-webrtc-mmo/cmd/dev-server/internal/devwebserver" 4 | 5 | func main() { 6 | devwebserver.Serve() 7 | } 8 | -------------------------------------------------------------------------------- /cmd/dev-server/internal/devwebserver/devwebserver.go: -------------------------------------------------------------------------------- 1 | package devwebserver 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | "os" 11 | "os/exec" 12 | "path/filepath" 13 | "runtime" 14 | "strings" 15 | "time" 16 | 17 | "golang.org/x/tools/go/packages" 18 | ) 19 | 20 | const ( 21 | packagePath = "github.com/silbinarywolf/toy-webrtc-mmo/cmd/dev-server/internal/devwebserver" 22 | ) 23 | 24 | var flagSet = flag.NewFlagSet("serve", flag.ExitOnError) 25 | 26 | // Serve will serve a build of the application to the web browser. 27 | // This function will block until exit. 28 | func Serve() { 29 | tags := flagSet.String("tags", "", "a list of build tags to consider satisfied during the build") 30 | //verbose := flagSet.Bool("verbose", false, "verbose") 31 | 32 | // Setup 33 | args := Arguments{} 34 | args.Port = ":8080" 35 | args.Directory = "." 36 | if tags != nil { 37 | args.Tags = *tags 38 | } 39 | arguments = args 40 | 41 | // Validation of settings 42 | dir := args.Directory 43 | if dir != "." { 44 | panic("Specifying a custom directory is not currently supported.") 45 | } 46 | 47 | // Get default resources 48 | var err error 49 | wasmJSPath, err = getDefaultWasmJSPath(args.Directory) 50 | if err != nil { 51 | panic(err) 52 | } 53 | //fmt.Printf("wasm_exec.js: %s\n", wasmJSPath) 54 | indexHTMLPath, err = getDefaultIndexHTMLPath(args.Directory) 55 | if err != nil { 56 | panic(err) 57 | } 58 | //fmt.Printf("index.html: %s\n", indexHTMLPath) 59 | 60 | // Start server 61 | fmt.Printf("Listening on http://localhost%s...\n", args.Port) 62 | http.HandleFunc("/", handle) 63 | //shared.OpenBrowser("http://localhost" + args.Port) 64 | if err := http.ListenAndServe(args.Port, nil); err != nil { 65 | panic(err) 66 | } 67 | } 68 | 69 | var wasmJSPath string 70 | 71 | var indexHTMLPath string 72 | 73 | var ( 74 | arguments Arguments 75 | tmpOutputDir = "" 76 | ) 77 | 78 | type Arguments struct { 79 | Port string // :8080 80 | Directory string // . 81 | Tags string // ie. "debug" 82 | } 83 | 84 | func handle(w http.ResponseWriter, r *http.Request) { 85 | output, err := ensureTmpOutputDir() 86 | if err != nil { 87 | http.Error(w, err.Error(), http.StatusInternalServerError) 88 | return 89 | } 90 | dir := arguments.Directory 91 | tags := arguments.Tags 92 | 93 | // Get path and package 94 | upath := r.URL.Path[1:] 95 | pkg := filepath.Dir(upath) 96 | fpath := filepath.Join(".", filepath.Base(upath)) 97 | if strings.HasSuffix(r.URL.Path, "/") { 98 | fpath = filepath.Join(fpath, "index.html") 99 | } 100 | 101 | parts := strings.Split(upath, "/") 102 | isAsset := len(parts) > 0 && parts[0] == "asset" 103 | 104 | if isAsset { 105 | // Load asset 106 | log.Print("serving asset: " + upath) 107 | 108 | // todo(Jake): 2018-12-30 109 | // Improve this so when "data" folder support 110 | // is added, this allows any filetype from the "data" folder. 111 | switch ext := filepath.Ext(upath); ext { 112 | case ".ttf", 113 | ".data", 114 | ".json": 115 | http.ServeFile(w, r, upath) 116 | } 117 | return 118 | } 119 | 120 | switch filepath.Base(fpath) { 121 | case "index.html": 122 | log.Print("serving index.html: " + indexHTMLPath) 123 | http.ServeFile(w, r, indexHTMLPath) 124 | case "wasm_exec.js": 125 | log.Print("serving index.html: " + wasmJSPath) 126 | http.ServeFile(w, r, wasmJSPath) 127 | return 128 | case "main.wasm": 129 | if _, err := os.Stat(fpath); os.IsNotExist(err) { 130 | // go build 131 | args := []string{"build", "-o", filepath.Join(output, "main.wasm")} 132 | if tags != "" { 133 | args = append(args, "-tags", tags) 134 | } 135 | args = append(args, pkg) 136 | log.Print("go ", strings.Join(args, " ")) 137 | cmdBuild := exec.Command(gobin(), args...) 138 | cmdBuild.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm") 139 | cmdBuild.Dir = dir 140 | out, err := cmdBuild.CombinedOutput() 141 | if err != nil { 142 | log.Print(err) 143 | log.Print(string(out)) 144 | http.Error(w, string(out), http.StatusInternalServerError) 145 | return 146 | } 147 | if len(out) > 0 { 148 | log.Print(string(out)) 149 | } 150 | 151 | f, err := os.Open(filepath.Join(output, "main.wasm")) 152 | if err != nil { 153 | http.Error(w, err.Error(), http.StatusInternalServerError) 154 | return 155 | } 156 | defer f.Close() 157 | http.ServeContent(w, r, "main.wasm", time.Now(), f) 158 | return 159 | } 160 | } 161 | } 162 | 163 | func gobin() string { 164 | return filepath.Join(runtime.GOROOT(), "bin", "go") 165 | } 166 | 167 | func ensureTmpOutputDir() (string, error) { 168 | if tmpOutputDir != "" { 169 | return tmpOutputDir, nil 170 | } 171 | 172 | tmp, err := ioutil.TempDir("", "") 173 | if err != nil { 174 | return "", err 175 | } 176 | tmpOutputDir = tmp 177 | return tmpOutputDir, nil 178 | } 179 | 180 | var ( 181 | cmdDir string 182 | cmdErr error 183 | ) 184 | 185 | func computeCmdSourceDir(gameDir string) (string, error) { 186 | if cmdDir == "" && cmdErr == nil { 187 | cmdDir, cmdErr = computeCmdSourceDirUncached(gameDir) 188 | } 189 | return cmdDir, cmdErr 190 | } 191 | 192 | func computeCmdSourceDirUncached(gameDir string) (string, error) { 193 | currentDir, err := filepath.Abs(gameDir) 194 | if err != nil { 195 | return "", err 196 | } 197 | cfg := &packages.Config{ 198 | Dir: currentDir, 199 | } 200 | pkgs, err := packages.Load(cfg, packagePath) 201 | if err != nil { 202 | return "", err 203 | } 204 | if len(pkgs) == 0 { 205 | return "", errors.New("Unable to find package: " + packagePath) 206 | } 207 | pkg := pkgs[0] 208 | if len(pkg.GoFiles) == 0 { 209 | return "", errors.New("Cannot find *.go files in:" + currentDir) 210 | } 211 | dir := filepath.Dir(pkg.GoFiles[0]) 212 | return dir, nil 213 | } 214 | 215 | func getDefaultWasmJSPath(gameDir string) (string, error) { 216 | const baseName = "wasm_exec.js" 217 | // Look for user-override 218 | /* { 219 | dir := gameDir + "/html/" + baseName 220 | if _, err := os.Stat(dir); !os.IsNotExist(err) { 221 | return dir, nil 222 | } 223 | } */ 224 | // Look for engine default 225 | dir, err := computeCmdSourceDir(gameDir) 226 | if err != nil { 227 | return "", err 228 | } 229 | dir = dir + "/" + baseName 230 | return dir, nil 231 | } 232 | 233 | func getDefaultIndexHTMLPath(gameDir string) (string, error) { 234 | const baseName = "index.html" 235 | // Look for user-override 236 | /* { 237 | dir := gameDir + "/html/" + baseName 238 | if _, err := os.Stat(dir); !os.IsNotExist(err) { 239 | return dir, nil 240 | } 241 | } */ 242 | // Look for engine default 243 | dir, err := computeCmdSourceDir(gameDir) 244 | if err != nil { 245 | return "", err 246 | } 247 | dir = dir + "/" + baseName 248 | return dir, nil 249 | } 250 | -------------------------------------------------------------------------------- /cmd/dev-server/internal/devwebserver/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 33 | 34 | 35 |
36 |

Loading...

37 |
38 |
39 |
40 |
41 | 42 | 43 | 105 | 106 | -------------------------------------------------------------------------------- /cmd/dev-server/internal/devwebserver/wasm_exec.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | (() => { 6 | // Map multiple JavaScript environments to a single common API, 7 | // preferring web standards over Node.js API. 8 | // 9 | // Environments considered: 10 | // - Browsers 11 | // - Node.js 12 | // - Electron 13 | // - Parcel 14 | // - Webpack 15 | 16 | if (typeof global !== "undefined") { 17 | // global already exists 18 | } else if (typeof window !== "undefined") { 19 | window.global = window; 20 | } else if (typeof self !== "undefined") { 21 | self.global = self; 22 | } else { 23 | throw new Error("cannot export Go (neither global, window nor self is defined)"); 24 | } 25 | 26 | if (!global.require && typeof require !== "undefined") { 27 | global.require = require; 28 | } 29 | 30 | if (!global.fs && global.require) { 31 | const fs = require("fs"); 32 | if (typeof fs === "object" && fs !== null && Object.keys(fs).length !== 0) { 33 | global.fs = fs; 34 | } 35 | } 36 | 37 | const enosys = () => { 38 | const err = new Error("not implemented"); 39 | err.code = "ENOSYS"; 40 | return err; 41 | }; 42 | 43 | if (!global.fs) { 44 | let outputBuf = ""; 45 | global.fs = { 46 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused 47 | writeSync(fd, buf) { 48 | outputBuf += decoder.decode(buf); 49 | const nl = outputBuf.lastIndexOf("\n"); 50 | if (nl != -1) { 51 | console.log(outputBuf.substr(0, nl)); 52 | outputBuf = outputBuf.substr(nl + 1); 53 | } 54 | return buf.length; 55 | }, 56 | write(fd, buf, offset, length, position, callback) { 57 | if (offset !== 0 || length !== buf.length || position !== null) { 58 | callback(enosys()); 59 | return; 60 | } 61 | const n = this.writeSync(fd, buf); 62 | callback(null, n); 63 | }, 64 | chmod(path, mode, callback) { callback(enosys()); }, 65 | chown(path, uid, gid, callback) { callback(enosys()); }, 66 | close(fd, callback) { callback(enosys()); }, 67 | fchmod(fd, mode, callback) { callback(enosys()); }, 68 | fchown(fd, uid, gid, callback) { callback(enosys()); }, 69 | fstat(fd, callback) { callback(enosys()); }, 70 | fsync(fd, callback) { callback(null); }, 71 | ftruncate(fd, length, callback) { callback(enosys()); }, 72 | lchown(path, uid, gid, callback) { callback(enosys()); }, 73 | link(path, link, callback) { callback(enosys()); }, 74 | lstat(path, callback) { callback(enosys()); }, 75 | mkdir(path, perm, callback) { callback(enosys()); }, 76 | open(path, flags, mode, callback) { callback(enosys()); }, 77 | read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, 78 | readdir(path, callback) { callback(enosys()); }, 79 | readlink(path, callback) { callback(enosys()); }, 80 | rename(from, to, callback) { callback(enosys()); }, 81 | rmdir(path, callback) { callback(enosys()); }, 82 | stat(path, callback) { callback(enosys()); }, 83 | symlink(path, link, callback) { callback(enosys()); }, 84 | truncate(path, length, callback) { callback(enosys()); }, 85 | unlink(path, callback) { callback(enosys()); }, 86 | utimes(path, atime, mtime, callback) { callback(enosys()); }, 87 | }; 88 | } 89 | 90 | if (!global.process) { 91 | global.process = { 92 | getuid() { return -1; }, 93 | getgid() { return -1; }, 94 | geteuid() { return -1; }, 95 | getegid() { return -1; }, 96 | getgroups() { throw enosys(); }, 97 | pid: -1, 98 | ppid: -1, 99 | umask() { throw enosys(); }, 100 | cwd() { throw enosys(); }, 101 | chdir() { throw enosys(); }, 102 | } 103 | } 104 | 105 | if (!global.crypto && global.require) { 106 | const nodeCrypto = require("crypto"); 107 | global.crypto = { 108 | getRandomValues(b) { 109 | nodeCrypto.randomFillSync(b); 110 | }, 111 | }; 112 | } 113 | if (!global.crypto) { 114 | throw new Error("global.crypto is not available, polyfill required (getRandomValues only)"); 115 | } 116 | 117 | if (!global.performance) { 118 | global.performance = { 119 | now() { 120 | const [sec, nsec] = process.hrtime(); 121 | return sec * 1000 + nsec / 1000000; 122 | }, 123 | }; 124 | } 125 | 126 | if (!global.TextEncoder && global.require) { 127 | global.TextEncoder = require("util").TextEncoder; 128 | } 129 | if (!global.TextEncoder) { 130 | throw new Error("global.TextEncoder is not available, polyfill required"); 131 | } 132 | 133 | if (!global.TextDecoder && global.require) { 134 | global.TextDecoder = require("util").TextDecoder; 135 | } 136 | if (!global.TextDecoder) { 137 | throw new Error("global.TextDecoder is not available, polyfill required"); 138 | } 139 | 140 | // End of polyfills for common API. 141 | 142 | const encoder = new TextEncoder("utf-8"); 143 | const decoder = new TextDecoder("utf-8"); 144 | 145 | global.Go = class { 146 | constructor() { 147 | this.argv = ["js"]; 148 | this.env = {}; 149 | this.exit = (code) => { 150 | if (code !== 0) { 151 | console.warn("exit code:", code); 152 | } 153 | }; 154 | this._exitPromise = new Promise((resolve) => { 155 | this._resolveExitPromise = resolve; 156 | }); 157 | this._pendingEvent = null; 158 | this._scheduledTimeouts = new Map(); 159 | this._nextCallbackTimeoutID = 1; 160 | 161 | const setInt64 = (addr, v) => { 162 | this.mem.setUint32(addr + 0, v, true); 163 | this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); 164 | } 165 | 166 | const getInt64 = (addr) => { 167 | const low = this.mem.getUint32(addr + 0, true); 168 | const high = this.mem.getInt32(addr + 4, true); 169 | return low + high * 4294967296; 170 | } 171 | 172 | const loadValue = (addr) => { 173 | const f = this.mem.getFloat64(addr, true); 174 | if (f === 0) { 175 | return undefined; 176 | } 177 | if (!isNaN(f)) { 178 | return f; 179 | } 180 | 181 | const id = this.mem.getUint32(addr, true); 182 | return this._values[id]; 183 | } 184 | 185 | const storeValue = (addr, v) => { 186 | const nanHead = 0x7FF80000; 187 | 188 | if (typeof v === "number" && v !== 0) { 189 | if (isNaN(v)) { 190 | this.mem.setUint32(addr + 4, nanHead, true); 191 | this.mem.setUint32(addr, 0, true); 192 | return; 193 | } 194 | this.mem.setFloat64(addr, v, true); 195 | return; 196 | } 197 | 198 | if (v === undefined) { 199 | this.mem.setFloat64(addr, 0, true); 200 | return; 201 | } 202 | 203 | let id = this._ids.get(v); 204 | if (id === undefined) { 205 | id = this._idPool.pop(); 206 | if (id === undefined) { 207 | id = this._values.length; 208 | } 209 | this._values[id] = v; 210 | this._goRefCounts[id] = 0; 211 | this._ids.set(v, id); 212 | } 213 | this._goRefCounts[id]++; 214 | let typeFlag = 0; 215 | switch (typeof v) { 216 | case "object": 217 | if (v !== null) { 218 | typeFlag = 1; 219 | } 220 | break; 221 | case "string": 222 | typeFlag = 2; 223 | break; 224 | case "symbol": 225 | typeFlag = 3; 226 | break; 227 | case "function": 228 | typeFlag = 4; 229 | break; 230 | } 231 | this.mem.setUint32(addr + 4, nanHead | typeFlag, true); 232 | this.mem.setUint32(addr, id, true); 233 | } 234 | 235 | const loadSlice = (addr) => { 236 | const array = getInt64(addr + 0); 237 | const len = getInt64(addr + 8); 238 | return new Uint8Array(this._inst.exports.mem.buffer, array, len); 239 | } 240 | 241 | const loadSliceOfValues = (addr) => { 242 | const array = getInt64(addr + 0); 243 | const len = getInt64(addr + 8); 244 | const a = new Array(len); 245 | for (let i = 0; i < len; i++) { 246 | a[i] = loadValue(array + i * 8); 247 | } 248 | return a; 249 | } 250 | 251 | const loadString = (addr) => { 252 | const saddr = getInt64(addr + 0); 253 | const len = getInt64(addr + 8); 254 | return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); 255 | } 256 | 257 | const timeOrigin = Date.now() - performance.now(); 258 | this.importObject = { 259 | go: { 260 | // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) 261 | // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported 262 | // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). 263 | // This changes the SP, thus we have to update the SP used by the imported function. 264 | 265 | // func wasmExit(code int32) 266 | "runtime.wasmExit": (sp) => { 267 | sp >>>= 0; 268 | const code = this.mem.getInt32(sp + 8, true); 269 | this.exited = true; 270 | delete this._inst; 271 | delete this._values; 272 | delete this._goRefCounts; 273 | delete this._ids; 274 | delete this._idPool; 275 | this.exit(code); 276 | }, 277 | 278 | // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) 279 | "runtime.wasmWrite": (sp) => { 280 | sp >>>= 0; 281 | const fd = getInt64(sp + 8); 282 | const p = getInt64(sp + 16); 283 | const n = this.mem.getInt32(sp + 24, true); 284 | fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); 285 | }, 286 | 287 | // func resetMemoryDataView() 288 | "runtime.resetMemoryDataView": (sp) => { 289 | sp >>>= 0; 290 | this.mem = new DataView(this._inst.exports.mem.buffer); 291 | }, 292 | 293 | // func nanotime1() int64 294 | "runtime.nanotime1": (sp) => { 295 | sp >>>= 0; 296 | setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); 297 | }, 298 | 299 | // func walltime1() (sec int64, nsec int32) 300 | "runtime.walltime1": (sp) => { 301 | sp >>>= 0; 302 | const msec = (new Date).getTime(); 303 | setInt64(sp + 8, msec / 1000); 304 | this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); 305 | }, 306 | 307 | // func scheduleTimeoutEvent(delay int64) int32 308 | "runtime.scheduleTimeoutEvent": (sp) => { 309 | sp >>>= 0; 310 | const id = this._nextCallbackTimeoutID; 311 | this._nextCallbackTimeoutID++; 312 | this._scheduledTimeouts.set(id, setTimeout( 313 | () => { 314 | this._resume(); 315 | while (this._scheduledTimeouts.has(id)) { 316 | // for some reason Go failed to register the timeout event, log and try again 317 | // (temporary workaround for https://github.com/golang/go/issues/28975) 318 | console.warn("scheduleTimeoutEvent: missed timeout event"); 319 | this._resume(); 320 | } 321 | }, 322 | getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early 323 | )); 324 | this.mem.setInt32(sp + 16, id, true); 325 | }, 326 | 327 | // func clearTimeoutEvent(id int32) 328 | "runtime.clearTimeoutEvent": (sp) => { 329 | sp >>>= 0; 330 | const id = this.mem.getInt32(sp + 8, true); 331 | clearTimeout(this._scheduledTimeouts.get(id)); 332 | this._scheduledTimeouts.delete(id); 333 | }, 334 | 335 | // func getRandomData(r []byte) 336 | "runtime.getRandomData": (sp) => { 337 | sp >>>= 0; 338 | crypto.getRandomValues(loadSlice(sp + 8)); 339 | }, 340 | 341 | // func finalizeRef(v ref) 342 | "syscall/js.finalizeRef": (sp) => { 343 | sp >>>= 0; 344 | const id = this.mem.getUint32(sp + 8, true); 345 | this._goRefCounts[id]--; 346 | if (this._goRefCounts[id] === 0) { 347 | const v = this._values[id]; 348 | this._values[id] = null; 349 | this._ids.delete(v); 350 | this._idPool.push(id); 351 | } 352 | }, 353 | 354 | // func stringVal(value string) ref 355 | "syscall/js.stringVal": (sp) => { 356 | sp >>>= 0; 357 | storeValue(sp + 24, loadString(sp + 8)); 358 | }, 359 | 360 | // func valueGet(v ref, p string) ref 361 | "syscall/js.valueGet": (sp) => { 362 | sp >>>= 0; 363 | const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); 364 | sp = this._inst.exports.getsp() >>> 0; // see comment above 365 | storeValue(sp + 32, result); 366 | }, 367 | 368 | // func valueSet(v ref, p string, x ref) 369 | "syscall/js.valueSet": (sp) => { 370 | sp >>>= 0; 371 | Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); 372 | }, 373 | 374 | // func valueDelete(v ref, p string) 375 | "syscall/js.valueDelete": (sp) => { 376 | sp >>>= 0; 377 | Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); 378 | }, 379 | 380 | // func valueIndex(v ref, i int) ref 381 | "syscall/js.valueIndex": (sp) => { 382 | sp >>>= 0; 383 | storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); 384 | }, 385 | 386 | // valueSetIndex(v ref, i int, x ref) 387 | "syscall/js.valueSetIndex": (sp) => { 388 | sp >>>= 0; 389 | Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); 390 | }, 391 | 392 | // func valueCall(v ref, m string, args []ref) (ref, bool) 393 | "syscall/js.valueCall": (sp) => { 394 | sp >>>= 0; 395 | try { 396 | const v = loadValue(sp + 8); 397 | const m = Reflect.get(v, loadString(sp + 16)); 398 | const args = loadSliceOfValues(sp + 32); 399 | const result = Reflect.apply(m, v, args); 400 | sp = this._inst.exports.getsp() >>> 0; // see comment above 401 | storeValue(sp + 56, result); 402 | this.mem.setUint8(sp + 64, 1); 403 | } catch (err) { 404 | storeValue(sp + 56, err); 405 | this.mem.setUint8(sp + 64, 0); 406 | } 407 | }, 408 | 409 | // func valueInvoke(v ref, args []ref) (ref, bool) 410 | "syscall/js.valueInvoke": (sp) => { 411 | sp >>>= 0; 412 | try { 413 | const v = loadValue(sp + 8); 414 | const args = loadSliceOfValues(sp + 16); 415 | const result = Reflect.apply(v, undefined, args); 416 | sp = this._inst.exports.getsp() >>> 0; // see comment above 417 | storeValue(sp + 40, result); 418 | this.mem.setUint8(sp + 48, 1); 419 | } catch (err) { 420 | storeValue(sp + 40, err); 421 | this.mem.setUint8(sp + 48, 0); 422 | } 423 | }, 424 | 425 | // func valueNew(v ref, args []ref) (ref, bool) 426 | "syscall/js.valueNew": (sp) => { 427 | sp >>>= 0; 428 | try { 429 | const v = loadValue(sp + 8); 430 | const args = loadSliceOfValues(sp + 16); 431 | const result = Reflect.construct(v, args); 432 | sp = this._inst.exports.getsp() >>> 0; // see comment above 433 | storeValue(sp + 40, result); 434 | this.mem.setUint8(sp + 48, 1); 435 | } catch (err) { 436 | storeValue(sp + 40, err); 437 | this.mem.setUint8(sp + 48, 0); 438 | } 439 | }, 440 | 441 | // func valueLength(v ref) int 442 | "syscall/js.valueLength": (sp) => { 443 | sp >>>= 0; 444 | setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); 445 | }, 446 | 447 | // valuePrepareString(v ref) (ref, int) 448 | "syscall/js.valuePrepareString": (sp) => { 449 | sp >>>= 0; 450 | const str = encoder.encode(String(loadValue(sp + 8))); 451 | storeValue(sp + 16, str); 452 | setInt64(sp + 24, str.length); 453 | }, 454 | 455 | // valueLoadString(v ref, b []byte) 456 | "syscall/js.valueLoadString": (sp) => { 457 | sp >>>= 0; 458 | const str = loadValue(sp + 8); 459 | loadSlice(sp + 16).set(str); 460 | }, 461 | 462 | // func valueInstanceOf(v ref, t ref) bool 463 | "syscall/js.valueInstanceOf": (sp) => { 464 | sp >>>= 0; 465 | this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0); 466 | }, 467 | 468 | // func copyBytesToGo(dst []byte, src ref) (int, bool) 469 | "syscall/js.copyBytesToGo": (sp) => { 470 | sp >>>= 0; 471 | const dst = loadSlice(sp + 8); 472 | const src = loadValue(sp + 32); 473 | if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { 474 | this.mem.setUint8(sp + 48, 0); 475 | return; 476 | } 477 | const toCopy = src.subarray(0, dst.length); 478 | dst.set(toCopy); 479 | setInt64(sp + 40, toCopy.length); 480 | this.mem.setUint8(sp + 48, 1); 481 | }, 482 | 483 | // func copyBytesToJS(dst ref, src []byte) (int, bool) 484 | "syscall/js.copyBytesToJS": (sp) => { 485 | sp >>>= 0; 486 | const dst = loadValue(sp + 8); 487 | const src = loadSlice(sp + 16); 488 | if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { 489 | this.mem.setUint8(sp + 48, 0); 490 | return; 491 | } 492 | const toCopy = src.subarray(0, dst.length); 493 | dst.set(toCopy); 494 | setInt64(sp + 40, toCopy.length); 495 | this.mem.setUint8(sp + 48, 1); 496 | }, 497 | 498 | "debug": (value) => { 499 | console.log(value); 500 | }, 501 | } 502 | }; 503 | } 504 | 505 | async run(instance) { 506 | if (!(instance instanceof WebAssembly.Instance)) { 507 | throw new Error("Go.run: WebAssembly.Instance expected"); 508 | } 509 | this._inst = instance; 510 | this.mem = new DataView(this._inst.exports.mem.buffer); 511 | this._values = [ // JS values that Go currently has references to, indexed by reference id 512 | NaN, 513 | 0, 514 | null, 515 | true, 516 | false, 517 | global, 518 | this, 519 | ]; 520 | this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id 521 | this._ids = new Map([ // mapping from JS values to reference ids 522 | [0, 1], 523 | [null, 2], 524 | [true, 3], 525 | [false, 4], 526 | [global, 5], 527 | [this, 6], 528 | ]); 529 | this._idPool = []; // unused ids that have been garbage collected 530 | this.exited = false; // whether the Go program has exited 531 | 532 | // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. 533 | let offset = 4096; 534 | 535 | const strPtr = (str) => { 536 | const ptr = offset; 537 | const bytes = encoder.encode(str + "\0"); 538 | new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); 539 | offset += bytes.length; 540 | if (offset % 8 !== 0) { 541 | offset += 8 - (offset % 8); 542 | } 543 | return ptr; 544 | }; 545 | 546 | const argc = this.argv.length; 547 | 548 | const argvPtrs = []; 549 | this.argv.forEach((arg) => { 550 | argvPtrs.push(strPtr(arg)); 551 | }); 552 | argvPtrs.push(0); 553 | 554 | const keys = Object.keys(this.env).sort(); 555 | keys.forEach((key) => { 556 | argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); 557 | }); 558 | argvPtrs.push(0); 559 | 560 | const argv = offset; 561 | argvPtrs.forEach((ptr) => { 562 | this.mem.setUint32(offset, ptr, true); 563 | this.mem.setUint32(offset + 4, 0, true); 564 | offset += 8; 565 | }); 566 | 567 | this._inst.exports.run(argc, argv); 568 | if (this.exited) { 569 | this._resolveExitPromise(); 570 | } 571 | await this._exitPromise; 572 | } 573 | 574 | _resume() { 575 | if (this.exited) { 576 | throw new Error("Go program has already exited"); 577 | } 578 | this._inst.exports.resume(); 579 | if (this.exited) { 580 | this._resolveExitPromise(); 581 | } 582 | } 583 | 584 | _makeFuncWrapper(id) { 585 | const go = this; 586 | return function () { 587 | const event = { id: id, this: this, args: arguments }; 588 | go._pendingEvent = event; 589 | go._resume(); 590 | return event.result; 591 | }; 592 | } 593 | } 594 | 595 | if ( 596 | typeof module !== "undefined" && 597 | global.require && 598 | global.require.main === module && 599 | global.process && 600 | global.process.versions && 601 | !global.process.versions.electron 602 | ) { 603 | if (process.argv.length < 3) { 604 | console.error("usage: go_js_wasm_exec [wasm binary] [arguments]"); 605 | process.exit(1); 606 | } 607 | 608 | const go = new Go(); 609 | go.argv = process.argv.slice(2); 610 | go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env); 611 | go.exit = process.exit; 612 | WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => { 613 | process.on("exit", (code) => { // Node.js exits if no event handler is pending 614 | if (code === 0 && !go.exited) { 615 | // deadlock, make Go print error and stack traces 616 | go._pendingEvent = { id: 0 }; 617 | go._resume(); 618 | } 619 | }); 620 | return go.run(result.instance); 621 | }).catch((err) => { 622 | console.error(err); 623 | process.exit(1); 624 | }); 625 | } 626 | })(); 627 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 33 | 34 | 35 |
36 |

Loading...

37 |
38 |
39 |
40 |
41 | 42 | 43 | 105 | 106 | -------------------------------------------------------------------------------- /dist/wasm_exec.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | (() => { 6 | // Map multiple JavaScript environments to a single common API, 7 | // preferring web standards over Node.js API. 8 | // 9 | // Environments considered: 10 | // - Browsers 11 | // - Node.js 12 | // - Electron 13 | // - Parcel 14 | // - Webpack 15 | 16 | if (typeof global !== "undefined") { 17 | // global already exists 18 | } else if (typeof window !== "undefined") { 19 | window.global = window; 20 | } else if (typeof self !== "undefined") { 21 | self.global = self; 22 | } else { 23 | throw new Error("cannot export Go (neither global, window nor self is defined)"); 24 | } 25 | 26 | if (!global.require && typeof require !== "undefined") { 27 | global.require = require; 28 | } 29 | 30 | if (!global.fs && global.require) { 31 | const fs = require("fs"); 32 | if (typeof fs === "object" && fs !== null && Object.keys(fs).length !== 0) { 33 | global.fs = fs; 34 | } 35 | } 36 | 37 | const enosys = () => { 38 | const err = new Error("not implemented"); 39 | err.code = "ENOSYS"; 40 | return err; 41 | }; 42 | 43 | if (!global.fs) { 44 | let outputBuf = ""; 45 | global.fs = { 46 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused 47 | writeSync(fd, buf) { 48 | outputBuf += decoder.decode(buf); 49 | const nl = outputBuf.lastIndexOf("\n"); 50 | if (nl != -1) { 51 | console.log(outputBuf.substr(0, nl)); 52 | outputBuf = outputBuf.substr(nl + 1); 53 | } 54 | return buf.length; 55 | }, 56 | write(fd, buf, offset, length, position, callback) { 57 | if (offset !== 0 || length !== buf.length || position !== null) { 58 | callback(enosys()); 59 | return; 60 | } 61 | const n = this.writeSync(fd, buf); 62 | callback(null, n); 63 | }, 64 | chmod(path, mode, callback) { callback(enosys()); }, 65 | chown(path, uid, gid, callback) { callback(enosys()); }, 66 | close(fd, callback) { callback(enosys()); }, 67 | fchmod(fd, mode, callback) { callback(enosys()); }, 68 | fchown(fd, uid, gid, callback) { callback(enosys()); }, 69 | fstat(fd, callback) { callback(enosys()); }, 70 | fsync(fd, callback) { callback(null); }, 71 | ftruncate(fd, length, callback) { callback(enosys()); }, 72 | lchown(path, uid, gid, callback) { callback(enosys()); }, 73 | link(path, link, callback) { callback(enosys()); }, 74 | lstat(path, callback) { callback(enosys()); }, 75 | mkdir(path, perm, callback) { callback(enosys()); }, 76 | open(path, flags, mode, callback) { callback(enosys()); }, 77 | read(fd, buffer, offset, length, position, callback) { callback(enosys()); }, 78 | readdir(path, callback) { callback(enosys()); }, 79 | readlink(path, callback) { callback(enosys()); }, 80 | rename(from, to, callback) { callback(enosys()); }, 81 | rmdir(path, callback) { callback(enosys()); }, 82 | stat(path, callback) { callback(enosys()); }, 83 | symlink(path, link, callback) { callback(enosys()); }, 84 | truncate(path, length, callback) { callback(enosys()); }, 85 | unlink(path, callback) { callback(enosys()); }, 86 | utimes(path, atime, mtime, callback) { callback(enosys()); }, 87 | }; 88 | } 89 | 90 | if (!global.process) { 91 | global.process = { 92 | getuid() { return -1; }, 93 | getgid() { return -1; }, 94 | geteuid() { return -1; }, 95 | getegid() { return -1; }, 96 | getgroups() { throw enosys(); }, 97 | pid: -1, 98 | ppid: -1, 99 | umask() { throw enosys(); }, 100 | cwd() { throw enosys(); }, 101 | chdir() { throw enosys(); }, 102 | } 103 | } 104 | 105 | if (!global.crypto && global.require) { 106 | const nodeCrypto = require("crypto"); 107 | global.crypto = { 108 | getRandomValues(b) { 109 | nodeCrypto.randomFillSync(b); 110 | }, 111 | }; 112 | } 113 | if (!global.crypto) { 114 | throw new Error("global.crypto is not available, polyfill required (getRandomValues only)"); 115 | } 116 | 117 | if (!global.performance) { 118 | global.performance = { 119 | now() { 120 | const [sec, nsec] = process.hrtime(); 121 | return sec * 1000 + nsec / 1000000; 122 | }, 123 | }; 124 | } 125 | 126 | if (!global.TextEncoder && global.require) { 127 | global.TextEncoder = require("util").TextEncoder; 128 | } 129 | if (!global.TextEncoder) { 130 | throw new Error("global.TextEncoder is not available, polyfill required"); 131 | } 132 | 133 | if (!global.TextDecoder && global.require) { 134 | global.TextDecoder = require("util").TextDecoder; 135 | } 136 | if (!global.TextDecoder) { 137 | throw new Error("global.TextDecoder is not available, polyfill required"); 138 | } 139 | 140 | // End of polyfills for common API. 141 | 142 | const encoder = new TextEncoder("utf-8"); 143 | const decoder = new TextDecoder("utf-8"); 144 | 145 | global.Go = class { 146 | constructor() { 147 | this.argv = ["js"]; 148 | this.env = {}; 149 | this.exit = (code) => { 150 | if (code !== 0) { 151 | console.warn("exit code:", code); 152 | } 153 | }; 154 | this._exitPromise = new Promise((resolve) => { 155 | this._resolveExitPromise = resolve; 156 | }); 157 | this._pendingEvent = null; 158 | this._scheduledTimeouts = new Map(); 159 | this._nextCallbackTimeoutID = 1; 160 | 161 | const setInt64 = (addr, v) => { 162 | this.mem.setUint32(addr + 0, v, true); 163 | this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true); 164 | } 165 | 166 | const getInt64 = (addr) => { 167 | const low = this.mem.getUint32(addr + 0, true); 168 | const high = this.mem.getInt32(addr + 4, true); 169 | return low + high * 4294967296; 170 | } 171 | 172 | const loadValue = (addr) => { 173 | const f = this.mem.getFloat64(addr, true); 174 | if (f === 0) { 175 | return undefined; 176 | } 177 | if (!isNaN(f)) { 178 | return f; 179 | } 180 | 181 | const id = this.mem.getUint32(addr, true); 182 | return this._values[id]; 183 | } 184 | 185 | const storeValue = (addr, v) => { 186 | const nanHead = 0x7FF80000; 187 | 188 | if (typeof v === "number" && v !== 0) { 189 | if (isNaN(v)) { 190 | this.mem.setUint32(addr + 4, nanHead, true); 191 | this.mem.setUint32(addr, 0, true); 192 | return; 193 | } 194 | this.mem.setFloat64(addr, v, true); 195 | return; 196 | } 197 | 198 | if (v === undefined) { 199 | this.mem.setFloat64(addr, 0, true); 200 | return; 201 | } 202 | 203 | let id = this._ids.get(v); 204 | if (id === undefined) { 205 | id = this._idPool.pop(); 206 | if (id === undefined) { 207 | id = this._values.length; 208 | } 209 | this._values[id] = v; 210 | this._goRefCounts[id] = 0; 211 | this._ids.set(v, id); 212 | } 213 | this._goRefCounts[id]++; 214 | let typeFlag = 0; 215 | switch (typeof v) { 216 | case "object": 217 | if (v !== null) { 218 | typeFlag = 1; 219 | } 220 | break; 221 | case "string": 222 | typeFlag = 2; 223 | break; 224 | case "symbol": 225 | typeFlag = 3; 226 | break; 227 | case "function": 228 | typeFlag = 4; 229 | break; 230 | } 231 | this.mem.setUint32(addr + 4, nanHead | typeFlag, true); 232 | this.mem.setUint32(addr, id, true); 233 | } 234 | 235 | const loadSlice = (addr) => { 236 | const array = getInt64(addr + 0); 237 | const len = getInt64(addr + 8); 238 | return new Uint8Array(this._inst.exports.mem.buffer, array, len); 239 | } 240 | 241 | const loadSliceOfValues = (addr) => { 242 | const array = getInt64(addr + 0); 243 | const len = getInt64(addr + 8); 244 | const a = new Array(len); 245 | for (let i = 0; i < len; i++) { 246 | a[i] = loadValue(array + i * 8); 247 | } 248 | return a; 249 | } 250 | 251 | const loadString = (addr) => { 252 | const saddr = getInt64(addr + 0); 253 | const len = getInt64(addr + 8); 254 | return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); 255 | } 256 | 257 | const timeOrigin = Date.now() - performance.now(); 258 | this.importObject = { 259 | go: { 260 | // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) 261 | // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported 262 | // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). 263 | // This changes the SP, thus we have to update the SP used by the imported function. 264 | 265 | // func wasmExit(code int32) 266 | "runtime.wasmExit": (sp) => { 267 | sp >>>= 0; 268 | const code = this.mem.getInt32(sp + 8, true); 269 | this.exited = true; 270 | delete this._inst; 271 | delete this._values; 272 | delete this._goRefCounts; 273 | delete this._ids; 274 | delete this._idPool; 275 | this.exit(code); 276 | }, 277 | 278 | // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) 279 | "runtime.wasmWrite": (sp) => { 280 | sp >>>= 0; 281 | const fd = getInt64(sp + 8); 282 | const p = getInt64(sp + 16); 283 | const n = this.mem.getInt32(sp + 24, true); 284 | fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); 285 | }, 286 | 287 | // func resetMemoryDataView() 288 | "runtime.resetMemoryDataView": (sp) => { 289 | sp >>>= 0; 290 | this.mem = new DataView(this._inst.exports.mem.buffer); 291 | }, 292 | 293 | // func nanotime1() int64 294 | "runtime.nanotime1": (sp) => { 295 | sp >>>= 0; 296 | setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); 297 | }, 298 | 299 | // func walltime1() (sec int64, nsec int32) 300 | "runtime.walltime1": (sp) => { 301 | sp >>>= 0; 302 | const msec = (new Date).getTime(); 303 | setInt64(sp + 8, msec / 1000); 304 | this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true); 305 | }, 306 | 307 | // func scheduleTimeoutEvent(delay int64) int32 308 | "runtime.scheduleTimeoutEvent": (sp) => { 309 | sp >>>= 0; 310 | const id = this._nextCallbackTimeoutID; 311 | this._nextCallbackTimeoutID++; 312 | this._scheduledTimeouts.set(id, setTimeout( 313 | () => { 314 | this._resume(); 315 | while (this._scheduledTimeouts.has(id)) { 316 | // for some reason Go failed to register the timeout event, log and try again 317 | // (temporary workaround for https://github.com/golang/go/issues/28975) 318 | console.warn("scheduleTimeoutEvent: missed timeout event"); 319 | this._resume(); 320 | } 321 | }, 322 | getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early 323 | )); 324 | this.mem.setInt32(sp + 16, id, true); 325 | }, 326 | 327 | // func clearTimeoutEvent(id int32) 328 | "runtime.clearTimeoutEvent": (sp) => { 329 | sp >>>= 0; 330 | const id = this.mem.getInt32(sp + 8, true); 331 | clearTimeout(this._scheduledTimeouts.get(id)); 332 | this._scheduledTimeouts.delete(id); 333 | }, 334 | 335 | // func getRandomData(r []byte) 336 | "runtime.getRandomData": (sp) => { 337 | sp >>>= 0; 338 | crypto.getRandomValues(loadSlice(sp + 8)); 339 | }, 340 | 341 | // func finalizeRef(v ref) 342 | "syscall/js.finalizeRef": (sp) => { 343 | sp >>>= 0; 344 | const id = this.mem.getUint32(sp + 8, true); 345 | this._goRefCounts[id]--; 346 | if (this._goRefCounts[id] === 0) { 347 | const v = this._values[id]; 348 | this._values[id] = null; 349 | this._ids.delete(v); 350 | this._idPool.push(id); 351 | } 352 | }, 353 | 354 | // func stringVal(value string) ref 355 | "syscall/js.stringVal": (sp) => { 356 | sp >>>= 0; 357 | storeValue(sp + 24, loadString(sp + 8)); 358 | }, 359 | 360 | // func valueGet(v ref, p string) ref 361 | "syscall/js.valueGet": (sp) => { 362 | sp >>>= 0; 363 | const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); 364 | sp = this._inst.exports.getsp() >>> 0; // see comment above 365 | storeValue(sp + 32, result); 366 | }, 367 | 368 | // func valueSet(v ref, p string, x ref) 369 | "syscall/js.valueSet": (sp) => { 370 | sp >>>= 0; 371 | Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); 372 | }, 373 | 374 | // func valueDelete(v ref, p string) 375 | "syscall/js.valueDelete": (sp) => { 376 | sp >>>= 0; 377 | Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16)); 378 | }, 379 | 380 | // func valueIndex(v ref, i int) ref 381 | "syscall/js.valueIndex": (sp) => { 382 | sp >>>= 0; 383 | storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); 384 | }, 385 | 386 | // valueSetIndex(v ref, i int, x ref) 387 | "syscall/js.valueSetIndex": (sp) => { 388 | sp >>>= 0; 389 | Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); 390 | }, 391 | 392 | // func valueCall(v ref, m string, args []ref) (ref, bool) 393 | "syscall/js.valueCall": (sp) => { 394 | sp >>>= 0; 395 | try { 396 | const v = loadValue(sp + 8); 397 | const m = Reflect.get(v, loadString(sp + 16)); 398 | const args = loadSliceOfValues(sp + 32); 399 | const result = Reflect.apply(m, v, args); 400 | sp = this._inst.exports.getsp() >>> 0; // see comment above 401 | storeValue(sp + 56, result); 402 | this.mem.setUint8(sp + 64, 1); 403 | } catch (err) { 404 | storeValue(sp + 56, err); 405 | this.mem.setUint8(sp + 64, 0); 406 | } 407 | }, 408 | 409 | // func valueInvoke(v ref, args []ref) (ref, bool) 410 | "syscall/js.valueInvoke": (sp) => { 411 | sp >>>= 0; 412 | try { 413 | const v = loadValue(sp + 8); 414 | const args = loadSliceOfValues(sp + 16); 415 | const result = Reflect.apply(v, undefined, args); 416 | sp = this._inst.exports.getsp() >>> 0; // see comment above 417 | storeValue(sp + 40, result); 418 | this.mem.setUint8(sp + 48, 1); 419 | } catch (err) { 420 | storeValue(sp + 40, err); 421 | this.mem.setUint8(sp + 48, 0); 422 | } 423 | }, 424 | 425 | // func valueNew(v ref, args []ref) (ref, bool) 426 | "syscall/js.valueNew": (sp) => { 427 | sp >>>= 0; 428 | try { 429 | const v = loadValue(sp + 8); 430 | const args = loadSliceOfValues(sp + 16); 431 | const result = Reflect.construct(v, args); 432 | sp = this._inst.exports.getsp() >>> 0; // see comment above 433 | storeValue(sp + 40, result); 434 | this.mem.setUint8(sp + 48, 1); 435 | } catch (err) { 436 | storeValue(sp + 40, err); 437 | this.mem.setUint8(sp + 48, 0); 438 | } 439 | }, 440 | 441 | // func valueLength(v ref) int 442 | "syscall/js.valueLength": (sp) => { 443 | sp >>>= 0; 444 | setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); 445 | }, 446 | 447 | // valuePrepareString(v ref) (ref, int) 448 | "syscall/js.valuePrepareString": (sp) => { 449 | sp >>>= 0; 450 | const str = encoder.encode(String(loadValue(sp + 8))); 451 | storeValue(sp + 16, str); 452 | setInt64(sp + 24, str.length); 453 | }, 454 | 455 | // valueLoadString(v ref, b []byte) 456 | "syscall/js.valueLoadString": (sp) => { 457 | sp >>>= 0; 458 | const str = loadValue(sp + 8); 459 | loadSlice(sp + 16).set(str); 460 | }, 461 | 462 | // func valueInstanceOf(v ref, t ref) bool 463 | "syscall/js.valueInstanceOf": (sp) => { 464 | sp >>>= 0; 465 | this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0); 466 | }, 467 | 468 | // func copyBytesToGo(dst []byte, src ref) (int, bool) 469 | "syscall/js.copyBytesToGo": (sp) => { 470 | sp >>>= 0; 471 | const dst = loadSlice(sp + 8); 472 | const src = loadValue(sp + 32); 473 | if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) { 474 | this.mem.setUint8(sp + 48, 0); 475 | return; 476 | } 477 | const toCopy = src.subarray(0, dst.length); 478 | dst.set(toCopy); 479 | setInt64(sp + 40, toCopy.length); 480 | this.mem.setUint8(sp + 48, 1); 481 | }, 482 | 483 | // func copyBytesToJS(dst ref, src []byte) (int, bool) 484 | "syscall/js.copyBytesToJS": (sp) => { 485 | sp >>>= 0; 486 | const dst = loadValue(sp + 8); 487 | const src = loadSlice(sp + 16); 488 | if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) { 489 | this.mem.setUint8(sp + 48, 0); 490 | return; 491 | } 492 | const toCopy = src.subarray(0, dst.length); 493 | dst.set(toCopy); 494 | setInt64(sp + 40, toCopy.length); 495 | this.mem.setUint8(sp + 48, 1); 496 | }, 497 | 498 | "debug": (value) => { 499 | console.log(value); 500 | }, 501 | } 502 | }; 503 | } 504 | 505 | async run(instance) { 506 | if (!(instance instanceof WebAssembly.Instance)) { 507 | throw new Error("Go.run: WebAssembly.Instance expected"); 508 | } 509 | this._inst = instance; 510 | this.mem = new DataView(this._inst.exports.mem.buffer); 511 | this._values = [ // JS values that Go currently has references to, indexed by reference id 512 | NaN, 513 | 0, 514 | null, 515 | true, 516 | false, 517 | global, 518 | this, 519 | ]; 520 | this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id 521 | this._ids = new Map([ // mapping from JS values to reference ids 522 | [0, 1], 523 | [null, 2], 524 | [true, 3], 525 | [false, 4], 526 | [global, 5], 527 | [this, 6], 528 | ]); 529 | this._idPool = []; // unused ids that have been garbage collected 530 | this.exited = false; // whether the Go program has exited 531 | 532 | // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. 533 | let offset = 4096; 534 | 535 | const strPtr = (str) => { 536 | const ptr = offset; 537 | const bytes = encoder.encode(str + "\0"); 538 | new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes); 539 | offset += bytes.length; 540 | if (offset % 8 !== 0) { 541 | offset += 8 - (offset % 8); 542 | } 543 | return ptr; 544 | }; 545 | 546 | const argc = this.argv.length; 547 | 548 | const argvPtrs = []; 549 | this.argv.forEach((arg) => { 550 | argvPtrs.push(strPtr(arg)); 551 | }); 552 | argvPtrs.push(0); 553 | 554 | const keys = Object.keys(this.env).sort(); 555 | keys.forEach((key) => { 556 | argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); 557 | }); 558 | argvPtrs.push(0); 559 | 560 | const argv = offset; 561 | argvPtrs.forEach((ptr) => { 562 | this.mem.setUint32(offset, ptr, true); 563 | this.mem.setUint32(offset + 4, 0, true); 564 | offset += 8; 565 | }); 566 | 567 | this._inst.exports.run(argc, argv); 568 | if (this.exited) { 569 | this._resolveExitPromise(); 570 | } 571 | await this._exitPromise; 572 | } 573 | 574 | _resume() { 575 | if (this.exited) { 576 | throw new Error("Go program has already exited"); 577 | } 578 | this._inst.exports.resume(); 579 | if (this.exited) { 580 | this._resolveExitPromise(); 581 | } 582 | } 583 | 584 | _makeFuncWrapper(id) { 585 | const go = this; 586 | return function () { 587 | const event = { id: id, this: this, args: arguments }; 588 | go._pendingEvent = event; 589 | go._resume(); 590 | return event.result; 591 | }; 592 | } 593 | } 594 | 595 | if ( 596 | typeof module !== "undefined" && 597 | global.require && 598 | global.require.main === module && 599 | global.process && 600 | global.process.versions && 601 | !global.process.versions.electron 602 | ) { 603 | if (process.argv.length < 3) { 604 | console.error("usage: go_js_wasm_exec [wasm binary] [arguments]"); 605 | process.exit(1); 606 | } 607 | 608 | const go = new Go(); 609 | go.argv = process.argv.slice(2); 610 | go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env); 611 | go.exit = process.exit; 612 | WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => { 613 | process.on("exit", (code) => { // Node.js exits if no event handler is pending 614 | if (code === 0 && !go.exited) { 615 | // deadlock, make Go print error and stack traces 616 | go._pendingEvent = { id: 0 }; 617 | go._resume(); 618 | } 619 | }); 620 | return go.run(result.instance); 621 | }).catch((err) => { 622 | console.error(err); 623 | process.exit(1); 624 | }); 625 | } 626 | })(); 627 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/silbinarywolf/toy-webrtc-mmo 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/hajimehoshi/ebiten/v2 v2.0.6 7 | github.com/pion/stun v0.3.5 8 | github.com/pion/turn/v2 v2.0.5 9 | github.com/pion/webrtc/v3 v3.0.12 10 | github.com/pkg/errors v0.9.1 11 | golang.org/x/tools v0.0.0-20201009162240-fcf82128ed91 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 6 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 7 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200707082815-5321531c36a2 h1:Ac1OEHHkbAZ6EUnJahF0GKcU0FjPc/V8F1DvjhKngFE= 8 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200707082815-5321531c36a2/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 9 | github.com/gofrs/flock v0.8.0 h1:MSdYClljsF3PbENUUEx85nkWfJSGfzYI9yEBZOJz6CY= 10 | github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= 11 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 12 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 13 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 14 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 15 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 16 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 17 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 18 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 19 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 20 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 21 | github.com/google/uuid v1.1.5 h1:kxhtnfFVi+rYdOALN0B3k9UT86zVJKfBimRaciULW4I= 22 | github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 23 | github.com/hajimehoshi/bitmapfont/v2 v2.1.0/go.mod h1:2BnYrkTQGThpr/CY6LorYtt/zEPNzvE/ND69CRTaHMs= 24 | github.com/hajimehoshi/ebiten/v2 v2.0.6 h1:sHNymgI+q80xasP69oFyrpup6r2qCNsKxqwsGEh6PWE= 25 | github.com/hajimehoshi/ebiten/v2 v2.0.6/go.mod h1:uS3OjMW3f2DRDMtWoIF7yMMmrMkv+fZ6pXcwR1pfA0Y= 26 | github.com/hajimehoshi/file2byteslice v0.0.0-20200812174855-0e5e8a80490e/go.mod h1:CqqAHp7Dk/AqQiwuhV1yT2334qbA/tFWQW0MD2dGqUE= 27 | github.com/hajimehoshi/go-mp3 v0.3.1/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM= 28 | github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= 29 | github.com/hajimehoshi/oto v0.6.8/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI= 30 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 31 | github.com/jakecoffman/cp v1.0.0/go.mod h1:JjY/Fp6d8E1CHnu74gWNnU0+b9VzEdUVPoJxg2PsTQg= 32 | github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk= 33 | github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0= 34 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 35 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 36 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 37 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 38 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 39 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 40 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 41 | github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 42 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 43 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 44 | github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= 45 | github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXmf0= 46 | github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg= 47 | github.com/pion/dtls/v2 v2.0.4/go.mod h1:qAkFscX0ZHoI1E07RfYPoRw3manThveu+mlTDdOxoGI= 48 | github.com/pion/dtls/v2 v2.0.7 h1:PNcUs/G1l9hb4jzMEorgFMxIBdp7fRN4LIApOTMtCYs= 49 | github.com/pion/dtls/v2 v2.0.7/go.mod h1:QuDII+8FVvk9Dp5t5vYIMTo7hh7uBkra+8QIm7QGm10= 50 | github.com/pion/ice/v2 v2.0.15 h1:KZrwa2ciL9od8+TUVJiYTNsCW9J5lktBjGwW1MacEnQ= 51 | github.com/pion/ice/v2 v2.0.15/go.mod h1:ZIiVGevpgAxF/cXiIVmuIUtCb3Xs4gCzCbXB6+nFkSI= 52 | github.com/pion/interceptor v0.0.10 h1:dXFyFWRJFwmzQqyn0U8dUAbOJu+JJnMVAqxmvTu30B4= 53 | github.com/pion/interceptor v0.0.10/go.mod h1:qzeuWuD/ZXvPqOnxNcnhWfkCZ2e1kwwslicyyPnhoK4= 54 | github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= 55 | github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= 56 | github.com/pion/mdns v0.0.4 h1:O4vvVqr4DGX63vzmO6Fw9vpy3lfztVWHGCQfyw0ZLSY= 57 | github.com/pion/mdns v0.0.4/go.mod h1:R1sL0p50l42S5lJs91oNdUL58nm0QHrhxnSegr++qC0= 58 | github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= 59 | github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= 60 | github.com/pion/rtcp v1.2.6 h1:1zvwBbyd0TeEuuWftrd/4d++m+/kZSeiguxU61LFWpo= 61 | github.com/pion/rtcp v1.2.6/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0= 62 | github.com/pion/rtp v1.6.2 h1:iGBerLX6JiDjB9NXuaPzHyxHFG9JsIEdgwTC0lp5n/U= 63 | github.com/pion/rtp v1.6.2/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= 64 | github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0= 65 | github.com/pion/sctp v1.7.11 h1:UCnj7MsobLKLuP/Hh+JMiI/6W5Bs/VF45lWKgHFjSIE= 66 | github.com/pion/sctp v1.7.11/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0= 67 | github.com/pion/sdp/v3 v3.0.4 h1:2Kf+dgrzJflNCSw3TV5v2VLeI0s/qkzy2r5jlR0wzf8= 68 | github.com/pion/sdp/v3 v3.0.4/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk= 69 | github.com/pion/srtp/v2 v2.0.1 h1:kgfh65ob3EcnFYA4kUBvU/menCp9u7qaJLXwWgpobzs= 70 | github.com/pion/srtp/v2 v2.0.1/go.mod h1:c8NWHhhkFf/drmHTAblkdu8++lsISEBBdAuiyxgqIsE= 71 | github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg= 72 | github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA= 73 | github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8= 74 | github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE= 75 | github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A= 76 | github.com/pion/transport v0.12.1/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q= 77 | github.com/pion/transport v0.12.2 h1:WYEjhloRHt1R86LhUKjC5y+P52Y11/QqEUalvtzVoys= 78 | github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q= 79 | github.com/pion/turn/v2 v2.0.5 h1:iwMHqDfPEDEOFzwWKT56eFmh6DYC6o/+xnLAEzgISbA= 80 | github.com/pion/turn/v2 v2.0.5/go.mod h1:APg43CFyt/14Uy7heYUOGWdkem/Wu4PhCO/bjyrTqMw= 81 | github.com/pion/udp v0.1.0 h1:uGxQsNyrqG3GLINv36Ff60covYmfrLoxzwnCsIYspXI= 82 | github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths= 83 | github.com/pion/webrtc/v3 v3.0.12 h1:XjTwtnx8dsyfBfESHAdBTnqmajku+2Y9RXXyC4KB0LM= 84 | github.com/pion/webrtc/v3 v3.0.12/go.mod h1:Y9t8I+0qeCCaUjg5wrweNVtK7reqZHc/MQdkznhzcro= 85 | github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= 86 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 87 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 88 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 89 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 90 | github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= 91 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 92 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 93 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 94 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 95 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 96 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 97 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 98 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 99 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 100 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 101 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 102 | golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 103 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= 104 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 105 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 106 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU= 107 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= 108 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 109 | golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 110 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 111 | golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM= 112 | golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 113 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 114 | golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 115 | golang.org/x/mobile v0.0.0-20210208171126-f462b3930c8f h1:aEcjdTsycgPqO/caTgnxfR9xwWOltP/21vtJyFztEy0= 116 | golang.org/x/mobile v0.0.0-20210208171126-f462b3930c8f/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= 117 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 118 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 119 | golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 120 | golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= 121 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 122 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 123 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 124 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 125 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 126 | golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 127 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 128 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 129 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 130 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 131 | golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 132 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 133 | golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 134 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= 135 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 136 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 137 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 138 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 139 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 140 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 141 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 142 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 143 | golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 144 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 145 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 146 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 147 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 148 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 149 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 150 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 151 | golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 152 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= 153 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 154 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 155 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 156 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 157 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 158 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 159 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 160 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 161 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 162 | golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 163 | golang.org/x/tools v0.0.0-20201009162240-fcf82128ed91 h1:UNUk0ao5UA0V4v2wikQWc4U+yG5UGoWku8MHs27mMqs= 164 | golang.org/x/tools v0.0.0-20201009162240-fcf82128ed91/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= 165 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 166 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 167 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 168 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 169 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 170 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 171 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 172 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 173 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 174 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 175 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 176 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 177 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 178 | gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 179 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 180 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 181 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 182 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 183 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 184 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 185 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 186 | -------------------------------------------------------------------------------- /internal/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "image" 5 | "strings" 6 | 7 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/asset" 8 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/ent" 9 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/input" 10 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/netcode" 11 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/netcode/client_or_server" 12 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/netcode/netconf" 13 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/renderer" 14 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/world" 15 | ) 16 | 17 | var ( 18 | backgroundImage renderer.Image 19 | ) 20 | 21 | type App struct { 22 | renderer.App 23 | 24 | hasInitialized bool 25 | clientOrServer netcode.Controller 26 | world world.World 27 | } 28 | 29 | func (app *App) Init() { 30 | // Load assets 31 | { 32 | ent.LoadPlayerAssets(app.App) 33 | img, _, err := image.Decode(strings.NewReader(asset.Background)) 34 | if err != nil { 35 | panic(err) 36 | } 37 | backgroundImage = app.NewImageFromImage(img) 38 | } 39 | 40 | app.SetRunnableOnUnfocused(true) 41 | 42 | // Load client/server 43 | app.clientOrServer = client_or_server.NewClientOrServer(netconf.Options{ 44 | // note(jae): 2021-04-17 45 | // if hosting non-locally, this should be your servers remote IP 46 | // ie. PublicIP: "220.240.114.91", 47 | PublicIP: "127.0.0.1", 48 | }) 49 | } 50 | 51 | func (app *App) Update() error { 52 | if !app.hasInitialized { 53 | app.Init() 54 | app.hasInitialized = true 55 | } 56 | 57 | //startTime := monotime.Now() 58 | 59 | // Update inputs 60 | app.world.PlayerInput = GetPlayerInput() 61 | for i := range app.world.Players { 62 | entity := app.world.Players[i] 63 | if entity != app.world.MyPlayer { 64 | continue 65 | } 66 | entity.Inputs = app.world.PlayerInput 67 | } 68 | 69 | // Handle networking 70 | // - client: create/update players 71 | // - server: take world snapshots, create/update players 72 | //clientOrServerStartTime := time.Now() 73 | app.clientOrServer.BeforeUpdate(&app.world) 74 | //fmt.Printf("client/server beforeUpdate time taken: %v\n", time.Since(clientOrServerStartTime)) 75 | if !app.clientOrServer.HasStartedOrConnected() { 76 | return nil 77 | } 78 | 79 | // Simulate the game 80 | app.world.Update() 81 | 82 | //fmt.Printf("frame Time taken: %v\n", time.Duration(monotime.Now()-startTime)) 83 | 84 | return nil 85 | } 86 | 87 | func (app *App) Draw(screen renderer.Screen) { 88 | if !app.clientOrServer.HasStartedOrConnected() { 89 | return 90 | } 91 | 92 | // Draws Background Image 93 | screen.DrawImage(backgroundImage, renderer.ImageOptions{ 94 | ScaleX: 1.5, 95 | ScaleY: 1.5, 96 | }) 97 | 98 | // Draw the players 99 | for _, entity := range app.world.Players { 100 | entity.Draw(screen) 101 | } 102 | 103 | // Show the message 104 | //msg := fmt.Sprintf("TPS: %0.2f\nPress the space key to jump.", ebiten.CurrentTPS()) 105 | //ebitenutil.DebugPrint(screen, msg) 106 | } 107 | 108 | func GetPlayerInput() ent.PlayerInput { 109 | // Handle keyboard input 110 | playerInput := ent.PlayerInput{ 111 | IsHoldingLeft: input.IsKeyPressed(input.KeyA) || input.IsKeyPressed(input.KeyLeft), 112 | IsHoldingRight: input.IsKeyPressed(input.KeyD) || input.IsKeyPressed(input.KeyRight), 113 | IsHoldingJump: input.IsKeyPressed(input.KeySpace), 114 | } 115 | 116 | // Detect mouse / touch for movement 117 | { 118 | windowWidth := world.ScreenWidth 119 | windowWidthThird := windowWidth / 3 120 | 121 | // Handle mouse 122 | if input.IsMouseButtonPressed(input.MouseButtonLeft) { 123 | x, _ := input.MousePosition() 124 | if x < windowWidthThird && !playerInput.IsHoldingRight { 125 | // Move left if touching left side of screen 126 | playerInput.IsHoldingLeft = true 127 | } 128 | if x > windowWidthThird && x < windowWidthThird*2 { 129 | // Jump if touching middle of screen 130 | playerInput.IsHoldingJump = true 131 | } 132 | if x > windowWidthThird*2 && !playerInput.IsHoldingLeft { 133 | // Move right if touching right side of screen 134 | playerInput.IsHoldingRight = true 135 | } 136 | } 137 | 138 | // Handle touch (mobile) 139 | // (TouchIDs returns nil for desktops) 140 | for _, touchID := range input.TouchIDs() { 141 | x, y := input.TouchPosition(touchID) 142 | if x == 0 && y == 0 { 143 | // skip if not touching anything 144 | continue 145 | } 146 | if x < windowWidthThird && !playerInput.IsHoldingRight { 147 | // Move left if touching left side of screen 148 | playerInput.IsHoldingLeft = true 149 | } 150 | if x > windowWidthThird && x < windowWidthThird*2 { 151 | // Jump if touching middle of screen 152 | playerInput.IsHoldingJump = true 153 | } 154 | if x > windowWidthThird*2 && !playerInput.IsHoldingLeft { 155 | // Move right if touching right side of screen 156 | playerInput.IsHoldingRight = true 157 | } 158 | } 159 | } 160 | 161 | return playerInput 162 | } 163 | 164 | func (g *App) Layout(outsideWidth, outsideHeight int) (int, int) { 165 | return world.ScreenWidth, world.ScreenHeight 166 | } 167 | 168 | func StartApp() { 169 | app := &App{} 170 | app.SetWindowSize(world.ScreenWidth, world.ScreenHeight) 171 | app.SetWindowTitle("Toy MMO Platformer WebRTC") 172 | if err := app.App.RunGame(app); err != nil { 173 | panic(err) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /internal/asset/asset.go: -------------------------------------------------------------------------------- 1 | package asset 2 | 3 | import ( 4 | _ "embed" 5 | _ "image/png" 6 | ) 7 | 8 | //go:embed player_idle_right.png 9 | var PlayerIdleRight string 10 | 11 | //go:embed player_idle_left.png 12 | var PlayerIdleLeft string 13 | 14 | //go:embed background.png 15 | var Background string 16 | -------------------------------------------------------------------------------- /internal/asset/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silbinarywolf/toy-webrtc-mmo/00588b2dca64fc7344de9222fa6187409b1fb90f/internal/asset/background.png -------------------------------------------------------------------------------- /internal/asset/player_idle_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silbinarywolf/toy-webrtc-mmo/00588b2dca64fc7344de9222fa6187409b1fb90f/internal/asset/player_idle_left.png -------------------------------------------------------------------------------- /internal/asset/player_idle_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silbinarywolf/toy-webrtc-mmo/00588b2dca64fc7344de9222fa6187409b1fb90f/internal/asset/player_idle_right.png -------------------------------------------------------------------------------- /internal/ent/player.go: -------------------------------------------------------------------------------- 1 | // ent is the entity package 2 | package ent 3 | 4 | import ( 5 | "image" 6 | "strings" 7 | 8 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/asset" 9 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/renderer" 10 | ) 11 | 12 | var ( 13 | leftSprite renderer.Image 14 | rightSprite renderer.Image 15 | ) 16 | 17 | func LoadPlayerAssets(app renderer.App) { 18 | img, _, err := image.Decode(strings.NewReader(asset.PlayerIdleRight)) 19 | if err != nil { 20 | panic(err) 21 | } 22 | rightSprite = app.NewImageFromImage(img) 23 | 24 | img, _, err = image.Decode(strings.NewReader(asset.PlayerIdleLeft)) 25 | if err != nil { 26 | panic(err) 27 | } 28 | leftSprite = app.NewImageFromImage(img) 29 | } 30 | 31 | type Player struct { 32 | // NetID is the unique ID of the player on the network 33 | NetID uint16 34 | // Inputs are the buttons/keys used by the player to make them move/etc 35 | Inputs PlayerInput 36 | X, Y float32 37 | Width, Height float32 38 | Hspeed, Vspeed float32 39 | DirLeft bool 40 | } 41 | 42 | type PlayerInput struct { 43 | IsHoldingLeft bool 44 | IsHoldingRight bool 45 | IsHoldingJump bool 46 | } 47 | 48 | func (self *Player) Init() { 49 | self.Width = 96 50 | self.Height = 96 51 | } 52 | 53 | func (self *Player) Update() { 54 | var ( 55 | groundY float32 = 720 - self.Height 56 | ) 57 | const ( 58 | // maxSpeed is the maximum horizontal speed for the entity 59 | maxSpeed float32 = 8 60 | // lubrication is how quickly an entity speeds up if holding down a key per frame 61 | lubrication float32 = 4 62 | // fricition is how quickly an entity slows down (ie. ice/slippy) 63 | friction float32 = 2 64 | // jumpPower is the initial vspeed to jump at 65 | jumpPower float32 = 12 66 | // gravity is applied to vspeed per step 67 | gravity float32 = 0.45 68 | ) 69 | 70 | // Update via input 71 | if self.Inputs.IsHoldingLeft { 72 | if self.Hspeed > 0 { 73 | self.Hspeed = 0 74 | } 75 | self.Hspeed -= lubrication 76 | self.DirLeft = true 77 | } else if self.Inputs.IsHoldingRight { 78 | if self.Hspeed < 0 { 79 | self.Hspeed = 0 80 | } 81 | self.Hspeed += lubrication 82 | self.DirLeft = false 83 | } 84 | if self.Inputs.IsHoldingJump && 85 | self.Vspeed >= 0 && 86 | self.Y+self.Height+1 > groundY { 87 | self.Vspeed = -jumpPower 88 | } 89 | 90 | // Update lubrication/friction (X axis) 91 | if self.Hspeed > 0 { 92 | self.Hspeed -= friction 93 | if self.Hspeed < 0 { 94 | self.Hspeed = 0 95 | } 96 | // Cap to max speed 97 | if self.Hspeed > maxSpeed { 98 | self.Hspeed = maxSpeed 99 | } 100 | } 101 | if self.Hspeed < 0 { 102 | self.Hspeed += friction 103 | if self.Hspeed > 0 { 104 | self.Hspeed = 0 105 | } 106 | // Cap to max speed 107 | if self.Hspeed < -maxSpeed { 108 | self.Hspeed = -maxSpeed 109 | } 110 | } 111 | self.X += self.Hspeed 112 | 113 | // Update gravity (Y axis) 114 | self.Vspeed += gravity 115 | if self.Vspeed > 20 { 116 | // cap fall speed 117 | self.Vspeed = 20 118 | } 119 | self.Y += self.Vspeed 120 | if self.Y+self.Height > groundY { 121 | self.Y = groundY - self.Height 122 | } 123 | } 124 | 125 | func (self *Player) Draw(screen renderer.Screen) { 126 | s := rightSprite 127 | if self.DirLeft { 128 | s = leftSprite 129 | } 130 | 131 | screen.DrawImage(s, renderer.ImageOptions{ 132 | X: self.X, 133 | Y: self.Y, 134 | }) 135 | } 136 | -------------------------------------------------------------------------------- /internal/input/input.go: -------------------------------------------------------------------------------- 1 | package input 2 | 3 | // Key represents a keyboard key. 4 | type Key int32 5 | 6 | // Only defining keys used by this game 7 | // 8 | // We indirectly use ebiten constants so that ebiten isn't included 9 | // as a package for headless builds 10 | const ( 11 | KeyA = Key(10) // Key(ebiten.KeyA) 12 | KeyD = Key(13) // Key(ebiten.KeyD) 13 | KeyLeft = Key(79) // Key(ebiten.KeyLeft) 14 | KeyRight = Key(92) // Key(ebiten.KeyRight) 15 | KeySpace = Key(100) // Key(ebiten.KeySpace) 16 | ) 17 | 18 | func IsKeyPressed(key Key) bool { 19 | return isKeyPressed(key) 20 | } 21 | 22 | // MouseButton represents a mouse button (left, right or middle) 23 | type MouseButton int32 24 | 25 | // Define all mouse buttons as there are only 3. 26 | // 27 | // We indirectly use ebiten constants so that ebiten isn't included 28 | // as a package for headless builds 29 | const ( 30 | MouseButtonLeft = MouseButton(0) 31 | MouseButtonRight = MouseButton(1) 32 | MouseButtonMiddle = MouseButton(2) 33 | ) 34 | 35 | func IsMouseButtonPressed(mouseButton MouseButton) bool { 36 | return isMouseButtonPressed(mouseButton) 37 | } 38 | 39 | // MousePosition returns the mouse/cursor position 40 | // 41 | // For headless builds, this always returns (0,0) 42 | func MousePosition() (int, int) { 43 | x, y := mousePosition() 44 | return x, y 45 | } 46 | 47 | type TouchID int 48 | 49 | func TouchIDs() []TouchID { 50 | return touchIDs() 51 | } 52 | 53 | func TouchPosition(touchID TouchID) (int, int) { 54 | x, y := touchPosition(touchID) 55 | return x, y 56 | } 57 | -------------------------------------------------------------------------------- /internal/input/input_headless.go: -------------------------------------------------------------------------------- 1 | // +build headless 2 | 3 | package input 4 | 5 | func isKeyPressed(key Key) bool { 6 | return false 7 | } 8 | 9 | func isMouseButtonPressed(mouseButton MouseButton) bool { 10 | return false 11 | } 12 | 13 | func mousePosition() (int, int) { 14 | return 0, 0 15 | } 16 | 17 | func touchIDs() []TouchID { 18 | return nil 19 | } 20 | 21 | func touchPosition(touchID TouchID) (int, int) { 22 | return 0, 0 23 | } 24 | -------------------------------------------------------------------------------- /internal/input/input_noheadless.go: -------------------------------------------------------------------------------- 1 | // +build !headless 2 | 3 | package input 4 | 5 | import "github.com/hajimehoshi/ebiten/v2" 6 | 7 | func isKeyPressed(key Key) bool { 8 | return ebiten.IsKeyPressed(ebiten.Key(key)) 9 | } 10 | 11 | func isMouseButtonPressed(mouseButton MouseButton) bool { 12 | return ebiten.IsMouseButtonPressed(ebiten.MouseButton(mouseButton)) 13 | } 14 | 15 | func mousePosition() (int, int) { 16 | x, y := ebiten.CursorPosition() 17 | return x, y 18 | } 19 | 20 | func touchIDs() []TouchID { 21 | touchIDs := ebiten.TouchIDs() 22 | if len(touchIDs) == 0 { 23 | return nil 24 | } 25 | r := make([]TouchID, len(touchIDs)) 26 | for i, touchID := range touchIDs { 27 | r[i] = TouchID(touchID) 28 | } 29 | return r 30 | } 31 | 32 | func touchPosition(touchID TouchID) (int, int) { 33 | x, y := ebiten.TouchPosition(ebiten.TouchID(touchID)) 34 | return x, y 35 | } 36 | -------------------------------------------------------------------------------- /internal/monotime/monotime.go: -------------------------------------------------------------------------------- 1 | package monotime 2 | 3 | import "time" 4 | 5 | // Now returns the current time more precisely for Web and Windows targets 6 | func Now() time.Duration { 7 | return now() 8 | } 9 | -------------------------------------------------------------------------------- /internal/monotime/monotime_js.go: -------------------------------------------------------------------------------- 1 | // +build js 2 | 3 | package monotime 4 | 5 | import ( 6 | "time" 7 | 8 | "syscall/js" 9 | ) 10 | 11 | func now() time.Duration { 12 | // time.Now() is not reliable until GopherJS supports performance.now(). 13 | return time.Duration(js.Global().Get("performance").Call("now").Float() * float64(time.Millisecond)) 14 | } 15 | -------------------------------------------------------------------------------- /internal/monotime/monotime_nojs.go: -------------------------------------------------------------------------------- 1 | // +build !js,!windows 2 | 3 | package monotime 4 | 5 | import ( 6 | "time" 7 | ) 8 | 9 | func now() time.Duration { 10 | return time.Now().UnixNano() 11 | } 12 | -------------------------------------------------------------------------------- /internal/monotime/monotime_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package monotime 4 | 5 | import ( 6 | "syscall" 7 | "time" 8 | "unsafe" 9 | ) 10 | 11 | var ( 12 | qpc *syscall.Proc 13 | freq uint64 14 | ) 15 | 16 | func init() { 17 | var err error 18 | dll, err := syscall.LoadDLL("kernel32.dll") 19 | if err != nil { 20 | panic(err) 21 | } 22 | qpc, err = dll.FindProc("QueryPerformanceCounter") 23 | if err != nil { 24 | panic(err) 25 | } 26 | 27 | // Get frequency once at initialization 28 | // docs: https://docs.microsoft.com/en-us/windows/desktop/SysInfo/acquiring-high-resolution-time-stamps 29 | qpf, err := dll.FindProc("QueryPerformanceFrequency") 30 | if err != nil { 31 | panic(err) 32 | } 33 | if ret, _, err := qpf.Call(uintptr(unsafe.Pointer(&freq))); ret == 0 { 34 | panic(err) 35 | } 36 | } 37 | 38 | func now() time.Duration { 39 | var ctr uint64 40 | if ret, _, err := qpc.Call(uintptr(unsafe.Pointer(&ctr))); ret == 0 { 41 | panic(err) 42 | } 43 | // 1.0e9 to convert seconds to nanoseconds 44 | res := time.Duration((1.0e9 * (ctr)) / freq) 45 | return res 46 | } 47 | -------------------------------------------------------------------------------- /internal/netcode/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "log" 9 | 10 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/netcode" 11 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/netcode/netconf" 12 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/netcode/netconst" 13 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/netcode/packs" 14 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/netcode/rtt" 15 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/netdriver/webrtcdriver/webrtcclient" 16 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/world" 17 | ) 18 | 19 | // compile-time assert we implement this interface 20 | var _ netcode.Controller = new(Controller) 21 | 22 | func New(options netconf.Options) *Controller { 23 | net := &Controller{} 24 | net.client = webrtcclient.New(webrtcclient.Options{ 25 | IPAddress: options.PublicIP + ":50000", 26 | ICEServerURLs: []string{"stun:" + options.PublicIP + ":3478"}, 27 | }) 28 | return net 29 | } 30 | 31 | type Controller struct { 32 | client *webrtcclient.Client 33 | 34 | buf *bytes.Buffer 35 | backingBuf [65536]byte 36 | rtt rtt.RoundTripTracking 37 | ackPacket packs.AckPacket 38 | 39 | frameCounter uint16 40 | frameInputBuffer []packs.ClientFrameInput 41 | 42 | hasStarted bool 43 | hasConnected bool 44 | } 45 | 46 | /* func (net *Controller) IsConnected() bool { 47 | return net.client.IsConnected() 48 | } */ 49 | 50 | func (net *Controller) HasStartedOrConnected() bool { 51 | return net.client.IsConnected() 52 | } 53 | 54 | func (net *Controller) init() { 55 | net.buf = bytes.NewBuffer(net.backingBuf[:]) 56 | net.client.Start() 57 | } 58 | 59 | func (net *Controller) BeforeUpdate(world *world.World) { 60 | if !net.hasStarted { 61 | net.init() 62 | net.hasStarted = true 63 | } 64 | if err := net.client.GetLastError(); err != nil { 65 | panic(fmt.Sprintf("%+v", err)) 66 | } 67 | if !net.client.IsConnected() { 68 | // If not ready yet, don't try to process packets 69 | return 70 | } 71 | if !net.hasConnected { 72 | // init 73 | world.MyPlayer = world.CreatePlayer() 74 | net.hasConnected = true 75 | } 76 | 77 | // Get frame count starting at 1 78 | net.frameCounter++ 79 | if net.frameCounter == 0 { 80 | net.frameCounter++ 81 | } 82 | 83 | // Store last N frames of inputs and remove from list as 84 | // server processes them 85 | if player := world.MyPlayer; player != nil { 86 | // maxClientInputBuffer is how many frames of input we hold onto so 87 | // that when we get world state from the server, we can replay our inputs 88 | // that haven't been simulated by the server yet. 89 | const maxClientInputBuffer = 60 90 | 91 | frameInputBufferData := packs.ClientFrameInput{ 92 | Frame: net.frameCounter, 93 | PlayerInput: player.Inputs, 94 | } 95 | if len(net.frameInputBuffer) >= maxClientInputBuffer { 96 | // move everything down 1 slot... 97 | for i := 1; i < len(net.frameInputBuffer); i++ { 98 | net.frameInputBuffer[i-1] = net.frameInputBuffer[i] 99 | } 100 | // override last record 101 | net.frameInputBuffer[len(net.frameInputBuffer)-1] = frameInputBufferData 102 | } else { 103 | net.frameInputBuffer = append(net.frameInputBuffer, frameInputBufferData) 104 | } 105 | } 106 | 107 | var lastWorldStatePacket *packs.ServerWorldStatePacket 108 | for { 109 | byteData, ok := net.client.Read() 110 | if !ok { 111 | // If no more packet data 112 | break 113 | } 114 | var buf bytes.Reader 115 | buf.Reset(byteData) 116 | for { 117 | sequenceID, packet, err := packs.Read(&buf) 118 | if err != nil { 119 | if errors.Is(err, io.EOF) { 120 | break 121 | } 122 | panic(err) 123 | } 124 | if _, ok := packet.(*packs.AckPacket); !ok { 125 | // To avoid recursion, we don't acknowledge acknowledgement packets 126 | net.ackPacket.SequenceIDList = append(net.ackPacket.SequenceIDList, sequenceID) 127 | } 128 | switch packet := packet.(type) { 129 | case *packs.AckPacket: 130 | for _, seqID := range packet.SequenceIDList { 131 | net.rtt.Ack(seqID) 132 | } 133 | case *packs.ServerWorldStatePacket: 134 | if packet.MyNetID == 0 { 135 | log.Printf("Bad packet from server, has ID of 0: %+v", packet) 136 | continue 137 | } 138 | // We only want to use the latest up to date world state 139 | if lastWorldStatePacket != nil { 140 | if rtt.IsWrappedUInt16GreaterThan(packet.LastSimulatedInputFrame, lastWorldStatePacket.LastSimulatedInputFrame) { 141 | lastWorldStatePacket = packet 142 | } 143 | } else { 144 | lastWorldStatePacket = packet 145 | } 146 | default: 147 | panic(fmt.Sprintf("unhandled packet type: %T", packet)) 148 | } 149 | } 150 | } 151 | 152 | // Use the latest up-to-date world state and snap to it 153 | // (also replay inputs the server hasn't simulated yet) 154 | if packet := lastWorldStatePacket; packet != nil { 155 | if world.MyPlayer != nil { 156 | world.MyPlayer.NetID = packet.MyNetID 157 | } 158 | for _, state := range packet.Players { 159 | hasFound := false 160 | for _, entity := range world.Players { 161 | if entity.NetID != state.NetID { 162 | continue 163 | } 164 | entity.X = state.X 165 | entity.Y = state.Y 166 | entity.Hspeed = state.Hspeed 167 | entity.Vspeed = state.Vspeed 168 | entity.DirLeft = state.DirLeft 169 | hasFound = true 170 | } 171 | if !hasFound { 172 | entity := world.CreatePlayer() 173 | entity.NetID = state.NetID 174 | entity.X = state.X 175 | entity.Y = state.Y 176 | entity.Hspeed = state.Hspeed 177 | entity.Vspeed = state.Vspeed 178 | entity.DirLeft = state.DirLeft 179 | } 180 | } 181 | 182 | // Replay inputs that haven't been processed by the server yet 183 | { 184 | var unprocessedFrameInputBuffer []packs.ClientFrameInput 185 | unprocessedCount := 0 186 | for i, frameInput := range net.frameInputBuffer { 187 | if rtt.IsWrappedUInt16GreaterThan(frameInput.Frame, packet.LastSimulatedInputFrame) { 188 | if unprocessedCount == 0 { 189 | // Only keep first item in frame input buffer 190 | unprocessedFrameInputBuffer = net.frameInputBuffer[i:] 191 | } 192 | // unprocessedCount is staying around for mostly future debugging purposes 193 | // this could be swapped to a boolean 194 | unprocessedCount++ 195 | } 196 | } 197 | if unprocessedCount > 0 { 198 | if player := world.MyPlayer; player != nil { 199 | prevInput := player.Inputs 200 | for _, inputFrame := range unprocessedFrameInputBuffer { 201 | player.Inputs = inputFrame.PlayerInput 202 | player.Update() 203 | } 204 | player.Inputs = prevInput 205 | } 206 | } 207 | } 208 | } 209 | 210 | // Send player input and acks to server every frame 211 | { 212 | net.buf.Reset() 213 | if len(net.ackPacket.SequenceIDList) > 0 { 214 | if err := packs.Write(net.buf, &net.rtt, &net.ackPacket); err != nil { 215 | panic(err) 216 | } 217 | net.ackPacket.SequenceIDList = net.ackPacket.SequenceIDList[:0] 218 | } 219 | { 220 | frameInputBuffer := net.frameInputBuffer 221 | if len(frameInputBuffer) > netconst.MaxServerInputBuffer { 222 | frameInputBuffer = frameInputBuffer[len(frameInputBuffer)-netconst.MaxServerInputBuffer:] 223 | } 224 | if err := packs.Write(net.buf, &net.rtt, &packs.ClientPlayerPacket{ 225 | InputBuffer: frameInputBuffer, 226 | }); err != nil { 227 | panic(err) 228 | } 229 | } 230 | // Upper limit of packets in gamedev are generally: "something like 1000 to 1200 bytes of payload data" 231 | // source: https://www.gafferongames.com/post/packet_fragmentation_and_reassembly/ 232 | if net.buf.Len() > 1000 { 233 | // note(jae): 2021-04-02 234 | // when i looked at raw packet data in Wireshark, packets were about ~100 bytes, even if i was sending ~20 bytes 235 | // of data. DTLS v1.2 / WebRTC / DataChannels may have a 100 byte overhead that I need to consider 236 | // when printing this kind of warning logic 237 | log.Printf("warning: client size of packet is %d, should be conservative and fit between 1000-1200", net.buf.Len()) 238 | } 239 | // DEBUG: uncomment to debug packet size 240 | //log.Printf("note: client sending size of packet is %d (rtt latency: %v)", net.buf.Len(), net.rtt.Latency()) 241 | if err := net.client.Send(net.buf.Bytes()); err != nil { 242 | panic(err) 243 | } 244 | } 245 | 246 | //fmt.Printf("Net RTT: %v\n", net.rtt.Latency()) 247 | } 248 | -------------------------------------------------------------------------------- /internal/netcode/client_or_server/client_or_server_client.go: -------------------------------------------------------------------------------- 1 | // +build !server 2 | 3 | package client_or_server 4 | 5 | import ( 6 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/netcode" 7 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/netcode/client" 8 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/netcode/netconf" 9 | ) 10 | 11 | // NewClientOrServer will return client for non-server tagged builds 12 | func NewClientOrServer(options netconf.Options) netcode.Controller { 13 | return client.New(options) 14 | } 15 | -------------------------------------------------------------------------------- /internal/netcode/client_or_server/client_or_server_server.go: -------------------------------------------------------------------------------- 1 | // +build server 2 | 3 | package client_or_server 4 | 5 | import ( 6 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/netcode" 7 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/netcode/netconf" 8 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/netcode/server" 9 | ) 10 | 11 | // NewClientOrServer will return server for server tagged builds 12 | func NewClientOrServer(options netconf.Options) netcode.Controller { 13 | return server.New(options) 14 | } 15 | -------------------------------------------------------------------------------- /internal/netcode/netcode.go: -------------------------------------------------------------------------------- 1 | package netcode 2 | 3 | import "github.com/silbinarywolf/toy-webrtc-mmo/internal/world" 4 | 5 | type Controller interface { 6 | BeforeUpdate(world *world.World) 7 | HasStartedOrConnected() bool 8 | } 9 | -------------------------------------------------------------------------------- /internal/netcode/netconf/netconf.go: -------------------------------------------------------------------------------- 1 | package netconf 2 | 3 | type Options struct { 4 | // PublicIP is used by the: 5 | // Client: to connect to server 6 | // Server: to setup the STUN server 7 | PublicIP string 8 | } 9 | -------------------------------------------------------------------------------- /internal/netcode/netconst/netconst.go: -------------------------------------------------------------------------------- 1 | // netconst are constants used by either the client and/server 2 | package netconst 3 | 4 | // MaxClientInputBuffer is how many frames of input we hold onto so that when we get world state from the 5 | // server, we can replay our inputs that haven't been simulated by the server yet. 6 | const MaxClientInputBuffer = 30 7 | 8 | // MaxServerInputBuffer is how many frames of inputs we fire at the server per frame 9 | // 10 | // We only send the server our last N frames of input as we don't want the server 11 | // to lag behind in bad network conditions. 12 | // 13 | // This value *kinda* says "in the worst network conditions, the server only has N frames to process and catch-up on" 14 | // 15 | // This value also used to be tied to "MaxClientInputBuffer" but when testing with 16 | // "clumsy" (packet loss/latency tool for Windows) at 350ms ping, we got a less janky user experience on the 17 | // client-side when we sent up only the last few frames but kept more for ourself 18 | const MaxServerInputBuffer = 10 19 | -------------------------------------------------------------------------------- /internal/netcode/packbuf/packbuf_test.go: -------------------------------------------------------------------------------- 1 | package packbuf 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "reflect" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | type sampleStructFormat struct { 14 | Bool bool 15 | Int int 16 | Int32 int32 17 | Int64 int64 18 | Uint16 uint16 19 | Float32 float32 20 | Float64 float64 21 | SliceOfPointerStruct []*sampleStructFormat 22 | SampleStructEmbeddedStruct 23 | } 24 | 25 | type SampleStructEmbeddedStruct struct { 26 | Int int 27 | } 28 | 29 | var testData = []sampleStructFormat{ 30 | { 31 | // test all zeroed data 32 | }, 33 | { 34 | Bool: true, 35 | Int: 9223372036854775807, 36 | Int32: 2147483647, 37 | Int64: 9223372036854775807, 38 | Uint16: 65535, 39 | Float32: 1.3333, 40 | Float64: 1.3333, 41 | SliceOfPointerStruct: []*sampleStructFormat{ 42 | { 43 | Bool: true, 44 | Int: 9223372036854775807, 45 | Int32: 2147483647, 46 | Int64: 9223372036854775807, 47 | Uint16: 65535, 48 | Float32: 1.3333, 49 | Float64: 1.3333, 50 | }, 51 | { 52 | Bool: true, 53 | Int: 9223372036854775807, 54 | Int32: 2147483647, 55 | Int64: 9223372036854775807, 56 | Uint16: 65535, 57 | Float32: 1.3333, 58 | Float64: 1.3333, 59 | }, 60 | }, 61 | SampleStructEmbeddedStruct: SampleStructEmbeddedStruct{ 62 | Int: 9223372036854775807, 63 | }, 64 | }, 65 | } 66 | 67 | func BenchmarkWriteStruct100(b *testing.B) { 68 | b.ReportAllocs() 69 | for n := 0; n < b.N; n++ { 70 | for _, testInput := range testData { 71 | buf := &bytes.Buffer{} 72 | buf.Reset() 73 | err := Write(buf, &testInput) 74 | if err != nil { 75 | b.Fatalf("unable to Write: %+v", err) 76 | } 77 | } 78 | } 79 | } 80 | 81 | func BenchmarkReadWriteStruct100(b *testing.B) { 82 | b.ReportAllocs() 83 | for n := 0; n < b.N; n++ { 84 | testReadWriteStruct(b) 85 | } 86 | } 87 | 88 | func TestReadWriteStruct(t *testing.T) { 89 | testReadWriteStruct(t) 90 | } 91 | 92 | type logger interface { 93 | Fatalf(format string, args ...interface{}) 94 | } 95 | 96 | func testReadWriteStruct(t logger) { 97 | for _, testInput := range testData { 98 | buf := &bytes.Buffer{} 99 | err := Write(buf, &testInput) 100 | if err != nil { 101 | t.Fatalf("unable to Write: %+v", err) 102 | } 103 | dat := buf.Bytes() 104 | var outputBuffer bytes.Reader 105 | outputBuffer.Reset(dat) 106 | var testOutput sampleStructFormat 107 | if err := Read(&outputBuffer, &testOutput); err != nil { 108 | t.Fatalf("unable to Read: %+v", err) 109 | } 110 | if diff := DeepEqual(testInput, testOutput); diff != nil { 111 | t.Fatalf("Unable to write/read packet: %v", diff) 112 | } 113 | } 114 | } 115 | 116 | // Below is a copy-paste of 117 | // https://github.com/go-test/deep/commit/8ed16920c079d9f721f068f915e1539e9ef3236c 118 | 119 | var ( 120 | // FloatPrecision is the number of decimal places to round float values 121 | // to when comparing. 122 | FloatPrecision = 10 123 | 124 | // MaxDiff specifies the maximum number of differences to return. 125 | MaxDiff = 10 126 | 127 | // MaxDepth specifies the maximum levels of a struct to recurse into, 128 | // if greater than zero. If zero, there is no limit. 129 | MaxDepth = 0 130 | 131 | // LogErrors causes errors to be logged to STDERR when true. 132 | LogErrors = false 133 | 134 | // CompareUnexportedFields causes unexported struct fields, like s in 135 | // T{s int}, to be compared when true. 136 | CompareUnexportedFields = false 137 | 138 | // NilSlicesAreEmpty causes a nil slice to be equal to an empty slice. 139 | NilSlicesAreEmpty = false 140 | 141 | // NilMapsAreEmpty causes a nil map to be equal to an empty map. 142 | NilMapsAreEmpty = false 143 | ) 144 | 145 | var ( 146 | // ErrMaxRecursion is logged when MaxDepth is reached. 147 | ErrMaxRecursion = errors.New("recursed to MaxDepth") 148 | 149 | // ErrTypeMismatch is logged when Equal passed two different types of values. 150 | ErrTypeMismatch = errors.New("variables are different reflect.Type") 151 | 152 | // ErrNotHandled is logged when a primitive Go kind is not handled. 153 | ErrNotHandled = errors.New("cannot compare the reflect.Kind") 154 | ) 155 | 156 | type cmp struct { 157 | diff []string 158 | buff []string 159 | floatFormat string 160 | } 161 | 162 | var errorType = reflect.TypeOf((*error)(nil)).Elem() 163 | 164 | // DeepEqual compares variables a and b, recursing into their structure up to 165 | // MaxDepth levels deep (if greater than zero), and returns a list of differences, 166 | // or nil if there are none. Some differences may not be found if an error is 167 | // also returned. 168 | // 169 | // If a type has an Equal method, like time.Equal, it is called to check for 170 | // equality. 171 | // 172 | // When comparing a struct, if a field has the tag `deep:"-"` then it will be 173 | // ignored. 174 | func DeepEqual(a, b interface{}) []string { 175 | aVal := reflect.ValueOf(a) 176 | bVal := reflect.ValueOf(b) 177 | c := &cmp{ 178 | diff: []string{}, 179 | buff: []string{}, 180 | floatFormat: fmt.Sprintf("%%.%df", FloatPrecision), 181 | } 182 | if a == nil && b == nil { 183 | return nil 184 | } else if a == nil && b != nil { 185 | c.saveDiff("", b) 186 | } else if a != nil && b == nil { 187 | c.saveDiff(a, "") 188 | } 189 | if len(c.diff) > 0 { 190 | return c.diff 191 | } 192 | 193 | c.equals(aVal, bVal, 0) 194 | if len(c.diff) > 0 { 195 | return c.diff // diffs 196 | } 197 | return nil // no diffs 198 | } 199 | 200 | func (c *cmp) equals(a, b reflect.Value, level int) { 201 | if MaxDepth > 0 && level > MaxDepth { 202 | logError(ErrMaxRecursion) 203 | return 204 | } 205 | 206 | // Check if one value is nil, e.g. T{x: *X} and T.x is nil 207 | if !a.IsValid() || !b.IsValid() { 208 | if a.IsValid() && !b.IsValid() { 209 | c.saveDiff(a.Type(), "") 210 | } else if !a.IsValid() && b.IsValid() { 211 | c.saveDiff("", b.Type()) 212 | } 213 | return 214 | } 215 | 216 | // If different types, they can't be equal 217 | aType := a.Type() 218 | bType := b.Type() 219 | if aType != bType { 220 | // Built-in types don't have a name, so don't report [3]int != [2]int as " != " 221 | if aType.Name() == "" || aType.Name() != bType.Name() { 222 | c.saveDiff(aType, bType) 223 | } else { 224 | // Type names can be the same, e.g. pkg/v1.Error and pkg/v2.Error 225 | // are both exported as pkg, so unless we include the full pkg path 226 | // the diff will be "pkg.Error != pkg.Error" 227 | // https://github.com/go-test/deep/issues/39 228 | aFullType := aType.PkgPath() + "." + aType.Name() 229 | bFullType := bType.PkgPath() + "." + bType.Name() 230 | c.saveDiff(aFullType, bFullType) 231 | } 232 | logError(ErrTypeMismatch) 233 | return 234 | } 235 | 236 | // Primitive https://golang.org/pkg/reflect/#Kind 237 | aKind := a.Kind() 238 | bKind := b.Kind() 239 | 240 | // Do a and b have underlying elements? Yes if they're ptr or interface. 241 | aElem := aKind == reflect.Ptr || aKind == reflect.Interface 242 | bElem := bKind == reflect.Ptr || bKind == reflect.Interface 243 | 244 | // If both types implement the error interface, compare the error strings. 245 | // This must be done before dereferencing because the interface is on a 246 | // pointer receiver. Re https://github.com/go-test/deep/issues/31, a/b might 247 | // be primitive kinds; see TestErrorPrimitiveKind. 248 | if aType.Implements(errorType) && bType.Implements(errorType) { 249 | if (!aElem || !a.IsNil()) && (!bElem || !b.IsNil()) { 250 | aString := a.MethodByName("Error").Call(nil)[0].String() 251 | bString := b.MethodByName("Error").Call(nil)[0].String() 252 | if aString != bString { 253 | c.saveDiff(aString, bString) 254 | return 255 | } 256 | } 257 | } 258 | 259 | // Dereference pointers and interface{} 260 | if aElem || bElem { 261 | if aElem { 262 | a = a.Elem() 263 | } 264 | if bElem { 265 | b = b.Elem() 266 | } 267 | c.equals(a, b, level+1) 268 | return 269 | } 270 | 271 | switch aKind { 272 | 273 | ///////////////////////////////////////////////////////////////////// 274 | // Iterable kinds 275 | ///////////////////////////////////////////////////////////////////// 276 | 277 | case reflect.Struct: 278 | /* 279 | The variables are structs like: 280 | type T struct { 281 | FirstName string 282 | LastName string 283 | } 284 | Type = .T, Kind = reflect.Struct 285 | Iterate through the fields (FirstName, LastName), recurse into their values. 286 | */ 287 | 288 | // Types with an Equal() method, like time.Time, only if struct field 289 | // is exported (CanInterface) 290 | if eqFunc := a.MethodByName("Equal"); eqFunc.IsValid() && eqFunc.CanInterface() { 291 | // Handle https://github.com/go-test/deep/issues/15: 292 | // Don't call T.Equal if the method is from an embedded struct, like: 293 | // type Foo struct { time.Time } 294 | // First, we'll encounter Equal(Ttime, time.Time) but if we pass b 295 | // as the 2nd arg we'll panic: "Call using pkg.Foo as type time.Time" 296 | // As far as I can tell, there's no way to see that the method is from 297 | // time.Time not Foo. So we check the type of the 1st (0) arg and skip 298 | // unless it's b type. Later, we'll encounter the time.Time anonymous/ 299 | // embedded field and then we'll have Equal(time.Time, time.Time). 300 | funcType := eqFunc.Type() 301 | if funcType.NumIn() == 1 && funcType.In(0) == bType { 302 | retVals := eqFunc.Call([]reflect.Value{b}) 303 | if !retVals[0].Bool() { 304 | c.saveDiff(a, b) 305 | } 306 | return 307 | } 308 | } 309 | 310 | for i := 0; i < a.NumField(); i++ { 311 | if aType.Field(i).PkgPath != "" && !CompareUnexportedFields { 312 | continue // skip unexported field, e.g. s in type T struct {s string} 313 | } 314 | 315 | if aType.Field(i).Tag.Get("deep") == "-" { 316 | continue // field wants to be ignored 317 | } 318 | 319 | c.push(aType.Field(i).Name) // push field name to buff 320 | 321 | // Get the Value for each field, e.g. FirstName has Type = string, 322 | // Kind = reflect.String. 323 | af := a.Field(i) 324 | bf := b.Field(i) 325 | 326 | // Recurse to compare the field values 327 | c.equals(af, bf, level+1) 328 | 329 | c.pop() // pop field name from buff 330 | 331 | if len(c.diff) >= MaxDiff { 332 | break 333 | } 334 | } 335 | case reflect.Map: 336 | /* 337 | The variables are maps like: 338 | map[string]int{ 339 | "foo": 1, 340 | "bar": 2, 341 | } 342 | Type = map[string]int, Kind = reflect.Map 343 | Or: 344 | type T map[string]int{} 345 | Type = .T, Kind = reflect.Map 346 | Iterate through the map keys (foo, bar), recurse into their values. 347 | */ 348 | 349 | if a.IsNil() || b.IsNil() { 350 | if NilMapsAreEmpty { 351 | if a.IsNil() && b.Len() != 0 { 352 | c.saveDiff("", b) 353 | return 354 | } else if a.Len() != 0 && b.IsNil() { 355 | c.saveDiff(a, "") 356 | return 357 | } 358 | } else { 359 | if a.IsNil() && !b.IsNil() { 360 | c.saveDiff("", b) 361 | } else if !a.IsNil() && b.IsNil() { 362 | c.saveDiff(a, "") 363 | } 364 | } 365 | return 366 | } 367 | 368 | if a.Pointer() == b.Pointer() { 369 | return 370 | } 371 | 372 | for _, key := range a.MapKeys() { 373 | c.push(fmt.Sprintf("map[%v]", key)) 374 | 375 | aVal := a.MapIndex(key) 376 | bVal := b.MapIndex(key) 377 | if bVal.IsValid() { 378 | c.equals(aVal, bVal, level+1) 379 | } else { 380 | c.saveDiff(aVal, "") 381 | } 382 | 383 | c.pop() 384 | 385 | if len(c.diff) >= MaxDiff { 386 | return 387 | } 388 | } 389 | 390 | for _, key := range b.MapKeys() { 391 | if aVal := a.MapIndex(key); aVal.IsValid() { 392 | continue 393 | } 394 | 395 | c.push(fmt.Sprintf("map[%v]", key)) 396 | c.saveDiff("", b.MapIndex(key)) 397 | c.pop() 398 | if len(c.diff) >= MaxDiff { 399 | return 400 | } 401 | } 402 | case reflect.Array: 403 | n := a.Len() 404 | for i := 0; i < n; i++ { 405 | c.push(fmt.Sprintf("array[%d]", i)) 406 | c.equals(a.Index(i), b.Index(i), level+1) 407 | c.pop() 408 | if len(c.diff) >= MaxDiff { 409 | break 410 | } 411 | } 412 | case reflect.Slice: 413 | if NilSlicesAreEmpty { 414 | if a.IsNil() && b.Len() != 0 { 415 | c.saveDiff("", b) 416 | return 417 | } else if a.Len() != 0 && b.IsNil() { 418 | c.saveDiff(a, "") 419 | return 420 | } 421 | } else { 422 | if a.IsNil() && !b.IsNil() { 423 | c.saveDiff("", b) 424 | return 425 | } else if !a.IsNil() && b.IsNil() { 426 | c.saveDiff(a, "") 427 | return 428 | } 429 | } 430 | 431 | aLen := a.Len() 432 | bLen := b.Len() 433 | 434 | if a.Pointer() == b.Pointer() && aLen == bLen { 435 | return 436 | } 437 | 438 | n := aLen 439 | if bLen > aLen { 440 | n = bLen 441 | } 442 | for i := 0; i < n; i++ { 443 | c.push(fmt.Sprintf("slice[%d]", i)) 444 | if i < aLen && i < bLen { 445 | c.equals(a.Index(i), b.Index(i), level+1) 446 | } else if i < aLen { 447 | c.saveDiff(a.Index(i), "") 448 | } else { 449 | c.saveDiff("", b.Index(i)) 450 | } 451 | c.pop() 452 | if len(c.diff) >= MaxDiff { 453 | break 454 | } 455 | } 456 | 457 | ///////////////////////////////////////////////////////////////////// 458 | // Primitive kinds 459 | ///////////////////////////////////////////////////////////////////// 460 | 461 | case reflect.Float32, reflect.Float64: 462 | // Round floats to FloatPrecision decimal places to compare with 463 | // user-defined precision. As is commonly know, floats have "imprecision" 464 | // such that 0.1 becomes 0.100000001490116119384765625. This cannot 465 | // be avoided; it can only be handled. Issue 30 suggested that floats 466 | // be compared using an epsilon: equal = |a-b| < epsilon. 467 | // In many cases the result is the same, but I think epsilon is a little 468 | // less clear for users to reason about. See issue 30 for details. 469 | aval := fmt.Sprintf(c.floatFormat, a.Float()) 470 | bval := fmt.Sprintf(c.floatFormat, b.Float()) 471 | if aval != bval { 472 | c.saveDiff(a.Float(), b.Float()) 473 | } 474 | case reflect.Bool: 475 | if a.Bool() != b.Bool() { 476 | c.saveDiff(a.Bool(), b.Bool()) 477 | } 478 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 479 | if a.Int() != b.Int() { 480 | c.saveDiff(a.Int(), b.Int()) 481 | } 482 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 483 | if a.Uint() != b.Uint() { 484 | c.saveDiff(a.Uint(), b.Uint()) 485 | } 486 | case reflect.String: 487 | if a.String() != b.String() { 488 | c.saveDiff(a.String(), b.String()) 489 | } 490 | 491 | default: 492 | logError(ErrNotHandled) 493 | } 494 | } 495 | 496 | func (c *cmp) push(name string) { 497 | c.buff = append(c.buff, name) 498 | } 499 | 500 | func (c *cmp) pop() { 501 | if len(c.buff) > 0 { 502 | c.buff = c.buff[0 : len(c.buff)-1] 503 | } 504 | } 505 | 506 | func (c *cmp) saveDiff(aval, bval interface{}) { 507 | if len(c.buff) > 0 { 508 | varName := strings.Join(c.buff, ".") 509 | c.diff = append(c.diff, fmt.Sprintf("%s: %v != %v", varName, aval, bval)) 510 | } else { 511 | c.diff = append(c.diff, fmt.Sprintf("%v != %v", aval, bval)) 512 | } 513 | } 514 | 515 | func logError(err error) { 516 | if LogErrors { 517 | log.Println(err) 518 | } 519 | } 520 | -------------------------------------------------------------------------------- /internal/netcode/packbuf/packbufread.go: -------------------------------------------------------------------------------- 1 | package packbuf 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "reflect" 9 | "strconv" 10 | ) 11 | 12 | const ( 13 | // maxStringSize is the maximum string that can be sent over the wire 14 | // the number chosen was arbitrary 15 | maxStringSize = 65535 16 | ) 17 | 18 | func Read(r io.Reader, data interface{}) error { 19 | err := readStruct(r, data) 20 | if err != nil { 21 | return err 22 | } 23 | return nil 24 | } 25 | 26 | func readStruct(buf io.Reader, structData interface{}) error { 27 | v := reflect.ValueOf(structData).Elem() 28 | for i := 0; i < v.NumField(); i++ { 29 | field := v.Field(i) 30 | if !field.CanSet() { 31 | continue 32 | } 33 | 34 | if t := reflect.TypeOf(field.Interface()); t.Kind() == reflect.Slice { 35 | var sliceLenCompact int32 36 | if err := binary.Read(buf, binary.LittleEndian, &sliceLenCompact); err != nil { 37 | return err 38 | } 39 | 40 | // 41 | sliceLen := int(sliceLenCompact) 42 | if sliceLen == 0 { 43 | // Ignore setting if no data 44 | // This ensures the data stays as "nil" 45 | continue 46 | } 47 | // todo(Jae): 2021-04-04 48 | // allow setting max limit on slice data structure via tags 49 | // on struct? ie. `packbuf:"maxsize:5"` 50 | // 51 | // this would stop the server from being able to receive weird 52 | // false packets 53 | 54 | slice := reflect.MakeSlice(t, sliceLen, sliceLen) 55 | field.Set(slice) 56 | switch sliceType := t.Elem(); sliceType.Kind() { 57 | case reflect.Uint8: 58 | value := make([]byte, sliceLen) 59 | if _, err := buf.Read(value); err != nil { 60 | return err 61 | } 62 | field.SetBytes(value) 63 | case reflect.Uint16: 64 | slice := make([]uint16, sliceLen) 65 | for i := 0; i < sliceLen; i++ { 66 | if err := binary.Read(buf, binary.LittleEndian, &slice[i]); err != nil { 67 | return err 68 | } 69 | } 70 | field.Set(reflect.ValueOf(slice)) 71 | case reflect.Struct: 72 | for i := 0; i < sliceLen; i++ { 73 | v := field.Index(i) 74 | if err := readStruct(buf, v.Addr().Interface()); err != nil { 75 | return err 76 | } 77 | } 78 | case reflect.Ptr: 79 | ptrToType := sliceType.Elem() 80 | if ptrToType.Kind() != reflect.Struct { 81 | return errors.New("unable to handle []*Type where Type is not a struct") 82 | } 83 | for i := 0; i < sliceLen; i++ { 84 | v := field.Index(i) 85 | v.Set(reflect.New(ptrToType)) 86 | if err := readStruct(buf, v.Interface()); err != nil { 87 | return err 88 | } 89 | } 90 | default: 91 | // custom types or structs must be explicitly typed 92 | // using calls to reflect.TypeOf on the defined type. 93 | return errors.New("TODO: Read: " + t.Elem().Kind().String()) 94 | } 95 | continue 96 | } 97 | if field.Kind() == reflect.Struct { 98 | if err := readStruct(buf, field.Addr().Interface()); err != nil { 99 | return err 100 | } 101 | continue 102 | } 103 | switch fieldType := field.Interface().(type) { 104 | case bool: 105 | var value byte 106 | if err := binary.Read(buf, binary.LittleEndian, &value); err != nil { 107 | return err 108 | } 109 | if value != 0 { 110 | field.SetBool(true) 111 | break 112 | } 113 | field.SetBool(false) 114 | case byte: 115 | var value byte 116 | if err := binary.Read(buf, binary.LittleEndian, &value); err != nil { 117 | return err 118 | } 119 | field.SetUint(uint64(value)) 120 | case uint16: 121 | var value uint16 122 | if err := binary.Read(buf, binary.LittleEndian, &value); err != nil { 123 | return err 124 | } 125 | field.SetUint(uint64(value)) 126 | case uint64: 127 | var value uint64 128 | if err := binary.Read(buf, binary.LittleEndian, &value); err != nil { 129 | return err 130 | } 131 | field.SetUint(value) 132 | case int, 133 | int64: 134 | // NOTE(Jae): 2020-05-16 135 | // "int" can be 32-bit or 64-bit in Golang spec, so assuming int64 (largest) 136 | var value int64 137 | if err := binary.Read(buf, binary.LittleEndian, &value); err != nil { 138 | return err 139 | } 140 | field.SetInt(value) 141 | case int32: 142 | var value int32 143 | if err := binary.Read(buf, binary.LittleEndian, &value); err != nil { 144 | return err 145 | } 146 | field.SetInt(int64(value)) 147 | case float32: 148 | var value float32 149 | if err := binary.Read(buf, binary.LittleEndian, &value); err != nil { 150 | return err 151 | } 152 | field.SetFloat(float64(value)) 153 | case float64: 154 | var value float64 155 | if err := binary.Read(buf, binary.LittleEndian, &value); err != nil { 156 | return err 157 | } 158 | field.SetFloat(value) 159 | case string: 160 | var stringSize uint16 161 | if err := binary.Read(buf, binary.LittleEndian, &stringSize); err != nil { 162 | return err 163 | } 164 | if stringSize > maxStringSize { 165 | return errors.New("cannot write string larger than " + strconv.Itoa(maxStringSize)) 166 | } 167 | if stringSize == 0 { 168 | // Nothing to write to field 169 | continue 170 | } 171 | stringData := make([]byte, stringSize) 172 | if _, err := buf.Read(stringData); err != nil { 173 | return err 174 | } 175 | field.SetString(string(stringData)) 176 | default: 177 | return fmt.Errorf("cannot read unsupported data type: %T in struct %T", fieldType, structData) 178 | } 179 | } 180 | return nil 181 | } 182 | -------------------------------------------------------------------------------- /internal/netcode/packbuf/packbufwrite.go: -------------------------------------------------------------------------------- 1 | package packbuf 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "math" 9 | "reflect" 10 | "strconv" 11 | ) 12 | 13 | func Write(w io.Writer, data interface{}) error { 14 | err := writeStruct(w, data) 15 | if err != nil { 16 | return err 17 | } 18 | return err 19 | } 20 | 21 | func writeStruct(w io.Writer, value interface{}) error { 22 | v := reflect.ValueOf(value).Elem() 23 | for i := 0; i < v.NumField(); i++ { 24 | field := v.Field(i) 25 | if !field.CanSet() { 26 | return errors.New("cannot serialize unexported field: " + field.String()) 27 | } 28 | switch field.Kind() { 29 | case reflect.Slice: 30 | sliceLenCompact := int32(field.Len()) 31 | if err := binary.Write(w, binary.LittleEndian, sliceLenCompact); err != nil { 32 | return err 33 | } 34 | 35 | // todo: change to field.Type() and ensure it works 36 | t := reflect.TypeOf(field.Interface()) 37 | sliceLen := field.Len() 38 | sliceType := t.Elem() 39 | switch sliceType.Kind() { // type of the slice element 40 | case reflect.Struct: 41 | for i := 0; i < sliceLen; i++ { 42 | v := field.Index(i) 43 | if err := writeStruct(w, v.Addr().Interface()); err != nil { 44 | return err 45 | } 46 | } 47 | case reflect.Ptr: 48 | ptrToType := sliceType.Elem() 49 | if ptrToType.Kind() != reflect.Struct { 50 | return errors.New("unable to handle []*Type where Type is not a struct") 51 | } 52 | for i := 0; i < sliceLen; i++ { 53 | v := field.Index(i) 54 | if err := writeStruct(w, v.Interface()); err != nil { 55 | return err 56 | } 57 | } 58 | default: 59 | switch fieldValue := field.Interface().(type) { 60 | case []uint8: 61 | if _, err := w.Write(fieldValue); err != nil { 62 | return err 63 | } 64 | case []uint16: 65 | for _, v := range fieldValue { 66 | if err := binary.Write(w, binary.LittleEndian, &v); err != nil { 67 | return err 68 | } 69 | } 70 | default: 71 | // custom types or structs must be explicitly typed 72 | // using calls to reflect.TypeOf on the defined type. 73 | return errors.New("TODO: writeStruct for type: " + t.Elem().Kind().String()) 74 | } 75 | } 76 | continue 77 | case reflect.Struct: 78 | if err := writeStruct(w, field.Addr().Interface()); err != nil { 79 | return err 80 | } 81 | continue 82 | } 83 | switch fieldValue := field.Interface().(type) { 84 | case bool: 85 | bsBack := [1]byte{0} 86 | bs := bsBack[:] 87 | if fieldValue { 88 | bs[0] = 1 89 | } 90 | if _, err := w.Write(bs); err != nil { 91 | return err 92 | } 93 | case byte: 94 | var bsBack [1]byte 95 | bs := bsBack[:] 96 | bs[0] = fieldValue 97 | binary.LittleEndian.PutUint64(bs, uint64(fieldValue)) 98 | if _, err := w.Write(bs); err != nil { 99 | return err 100 | } 101 | case int: 102 | // NOTE(Jae): 2021-03-07 103 | // "int" can be 32-bit or 64-bit in Golang spec, so assuming int64 (largest) 104 | var bsBack [8]byte 105 | bs := bsBack[:] 106 | binary.LittleEndian.PutUint64(bs, uint64(fieldValue)) 107 | if _, err := w.Write(bs); err != nil { 108 | return err 109 | } 110 | case int16: 111 | var bsBack [2]byte 112 | bs := bsBack[:] 113 | binary.LittleEndian.PutUint16(bs, uint16(fieldValue)) 114 | if _, err := w.Write(bs); err != nil { 115 | return err 116 | } 117 | case float32: 118 | var bsBack [4]byte 119 | bs := bsBack[:] 120 | binary.LittleEndian.PutUint32(bs, math.Float32bits(fieldValue)) 121 | if _, err := w.Write(bs); err != nil { 122 | return err 123 | } 124 | case int32, 125 | int64, 126 | uint16, 127 | uint64, 128 | float64: 129 | if err := binary.Write(w, binary.LittleEndian, fieldValue); err != nil { 130 | return err 131 | } 132 | case string: 133 | if len(fieldValue) > maxStringSize { 134 | return errors.New("cannot write string larger than " + strconv.Itoa(maxStringSize)) 135 | } 136 | if err := binary.Write(w, binary.LittleEndian, uint16(len(fieldValue))); err != nil { 137 | return err 138 | } 139 | // todo(jae): 2021-03-07 140 | // casts to []byte which could be slow, profile later and make fast 141 | if _, err := w.Write([]byte(fieldValue)); err != nil { 142 | return err 143 | } 144 | default: 145 | return fmt.Errorf("Cannot write unsupported data type: %T in packet type %T", fieldValue, value) 146 | } 147 | } 148 | return nil 149 | } 150 | -------------------------------------------------------------------------------- /internal/netcode/packs/packs.go: -------------------------------------------------------------------------------- 1 | // packs is a package that holds the packet data structures and associates an ID with them 2 | package packs 3 | 4 | import ( 5 | "encoding/binary" 6 | "io" 7 | "reflect" 8 | "strconv" 9 | 10 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/ent" 11 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/netcode/packbuf" 12 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/netcode/rtt" 13 | ) 14 | 15 | const ( 16 | // packetInvalid PacketID = 0 17 | packetAck PacketID = 1 18 | packetClientPlayerUpdate PacketID = 2 19 | packetWorldStateUpdate PacketID = 3 20 | ) 21 | 22 | // AckPacket is used by client/server to acknowledge that it recieved 23 | // a packet (or multiple packets) 24 | type AckPacket struct { 25 | SequenceIDList []uint16 26 | } 27 | 28 | func (packet *AckPacket) ID() PacketID { 29 | return packetAck 30 | } 31 | 32 | func init() { 33 | register(&AckPacket{}) 34 | } 35 | 36 | // ClientPlayerPacket is the data sent from the client to the server 37 | type ClientPlayerPacket struct { 38 | InputBuffer []ClientFrameInput 39 | } 40 | 41 | func (packet *ClientPlayerPacket) ID() PacketID { 42 | return packetClientPlayerUpdate 43 | } 44 | 45 | type ClientFrameInput struct { 46 | Frame uint16 47 | ent.PlayerInput 48 | } 49 | 50 | func init() { 51 | register(&ClientPlayerPacket{}) 52 | } 53 | 54 | // ServerWorldStatePacket is the data sent from the server to the client 55 | type ServerWorldStatePacket struct { 56 | MyNetID uint16 57 | // LastSimulatedInputFrame is the last input the server simulated in world.Update 58 | // this is utilized by the client to replay inputs that haven't been processed 59 | // by the server yet 60 | LastSimulatedInputFrame uint16 61 | Players []PlayerState 62 | } 63 | 64 | // PlayerState is the player state information we want to send per frame 65 | // 66 | // TODO(jae): 2021-04-02 67 | // lets add a system like packbuf package where we just pass in a slice 68 | // of []*ent.Player and any fields marked with `net:"x"` etc, will get sent 69 | // over the wire 70 | type PlayerState struct { 71 | NetID uint16 72 | X, Y float32 73 | Hspeed, Vspeed float32 74 | DirLeft bool 75 | } 76 | 77 | func (packet *ServerWorldStatePacket) ID() PacketID { 78 | return packetWorldStateUpdate 79 | } 80 | 81 | func init() { 82 | register(&ServerWorldStatePacket{}) 83 | } 84 | 85 | type PacketID uint8 86 | 87 | var packetIDToType = make(map[PacketID]reflect.Type) 88 | 89 | type Packet interface { 90 | ID() PacketID 91 | } 92 | 93 | type InvalidPacketID struct { 94 | id PacketID 95 | } 96 | 97 | func (err *InvalidPacketID) Error() string { 98 | return "invalid packet id: " + strconv.Itoa(int(err.id)) 99 | } 100 | 101 | func register(packet Packet) { 102 | id := packet.ID() 103 | if _, ok := packetIDToType[id]; ok { 104 | panic("cannot register a packet with the same id twice: " + strconv.Itoa(int(id))) 105 | } 106 | packetIDToType[id] = reflect.TypeOf(packet).Elem() 107 | } 108 | 109 | func Read(r io.Reader) (uint16, Packet, error) { 110 | var packetID PacketID 111 | if err := binary.Read(r, binary.LittleEndian, &packetID); err != nil { 112 | return 0, nil, err 113 | } 114 | packetType, ok := packetIDToType[packetID] 115 | if !ok { 116 | return 0, nil, &InvalidPacketID{ 117 | id: packetID, 118 | } 119 | } 120 | var seqID uint16 121 | if err := binary.Read(r, binary.LittleEndian, &seqID); err != nil { 122 | return 0, nil, err 123 | } 124 | packet := reflect.New(packetType).Interface().(Packet) 125 | if err := packbuf.Read(r, packet); err != nil { 126 | return 0, nil, err 127 | } 128 | return seqID, packet, nil 129 | } 130 | 131 | func Write(w io.Writer, rtt *rtt.RoundTripTracking, packet Packet) error { 132 | if err := binary.Write(w, binary.LittleEndian, packet.ID()); err != nil { 133 | return err 134 | } 135 | if err := binary.Write(w, binary.LittleEndian, rtt.Next()); err != nil { 136 | return err 137 | } 138 | if err := packbuf.Write(w, packet); err != nil { 139 | return err 140 | } 141 | return nil 142 | } 143 | -------------------------------------------------------------------------------- /internal/netcode/packs/packs_test.go: -------------------------------------------------------------------------------- 1 | // packs is a package that holds the packet data structures 2 | package packs 3 | 4 | import ( 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "log" 9 | "reflect" 10 | "strings" 11 | "testing" 12 | 13 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/netcode/rtt" 14 | ) 15 | 16 | // TestPacketWriteRead will test all the packet types to ensure writing / reading works 17 | // for each registered struct 18 | func TestPacketWriteRead(t *testing.T) { 19 | writerRtt := &rtt.RoundTripTracking{} 20 | for _, packetType := range packetIDToType { 21 | packet := reflect.New(packetType).Interface().(Packet) 22 | writer := bytes.NewBuffer(nil) 23 | err := Write(writer, writerRtt, packet) 24 | if err != nil { 25 | t.Errorf("Failed Packet.Write: %v\n", err) 26 | return 27 | } 28 | reader := bytes.NewReader(writer.Bytes()) 29 | seqID, packetOutput, err := Read(reader) 30 | if err != nil { 31 | t.Errorf("Failed Packet.Read: %v\n", err) 32 | return 33 | } 34 | if diff := DeepEqual(packet, packetOutput); diff != nil { 35 | t.Errorf("Unable to serialize/deserialize packet: %T (seq id: %d) ---- \n%v", packet, seqID, diff) 36 | return 37 | } 38 | } 39 | } 40 | 41 | // TestPacketDoubleWriteAndRead tests that at least two packet types can be written 42 | // and then read without breakage 43 | func TestPacketDoubleWriteAndDoubleRead(t *testing.T) { 44 | writerRtt := &rtt.RoundTripTracking{} 45 | for _, packetType := range packetIDToType { 46 | packet := reflect.New(packetType).Interface().(Packet) 47 | packetTwo := reflect.New(packetType).Interface().(Packet) 48 | writer := bytes.NewBuffer(nil) 49 | if err := Write(writer, writerRtt, packet); err != nil { 50 | t.Errorf("Failed Packet.Write: %v\n", err) 51 | return 52 | } 53 | if err := Write(writer, writerRtt, packetTwo); err != nil { 54 | t.Errorf("Failed second Packet.Write: %v\n", err) 55 | return 56 | } 57 | reader := bytes.NewReader(writer.Bytes()) 58 | { 59 | seqID, packetOutput, err := Read(reader) 60 | if err != nil { 61 | t.Errorf("Failed Packet.Read: %v\n", err) 62 | return 63 | } 64 | if diff := DeepEqual(packet, packetOutput); diff != nil { 65 | t.Errorf("Unable to Read packet: %T (seq id: %d) ---- \n%v", packet, seqID, diff) 66 | return 67 | } 68 | } 69 | { 70 | seqID, packetOutput, err := Read(reader) 71 | if err != nil { 72 | t.Errorf("Failed Packet.Read: %v\n", err) 73 | return 74 | } 75 | if diff := DeepEqual(packetTwo, packetOutput); diff != nil { 76 | t.Errorf("Unable to Read packet: %T (seq id: %d) ---- \n%v", packetTwo, seqID, diff) 77 | return 78 | } 79 | } 80 | } 81 | } 82 | 83 | // Below is a copy-paste of 84 | // https://github.com/go-test/deep/commit/8ed16920c079d9f721f068f915e1539e9ef3236c 85 | 86 | var ( 87 | // FloatPrecision is the number of decimal places to round float values 88 | // to when comparing. 89 | FloatPrecision = 10 90 | 91 | // MaxDiff specifies the maximum number of differences to return. 92 | MaxDiff = 10 93 | 94 | // MaxDepth specifies the maximum levels of a struct to recurse into, 95 | // if greater than zero. If zero, there is no limit. 96 | MaxDepth = 0 97 | 98 | // LogErrors causes errors to be logged to STDERR when true. 99 | LogErrors = false 100 | 101 | // CompareUnexportedFields causes unexported struct fields, like s in 102 | // T{s int}, to be compared when true. 103 | CompareUnexportedFields = false 104 | 105 | // NilSlicesAreEmpty causes a nil slice to be equal to an empty slice. 106 | NilSlicesAreEmpty = false 107 | 108 | // NilMapsAreEmpty causes a nil map to be equal to an empty map. 109 | NilMapsAreEmpty = false 110 | ) 111 | 112 | var ( 113 | // ErrMaxRecursion is logged when MaxDepth is reached. 114 | ErrMaxRecursion = errors.New("recursed to MaxDepth") 115 | 116 | // ErrTypeMismatch is logged when Equal passed two different types of values. 117 | ErrTypeMismatch = errors.New("variables are different reflect.Type") 118 | 119 | // ErrNotHandled is logged when a primitive Go kind is not handled. 120 | ErrNotHandled = errors.New("cannot compare the reflect.Kind") 121 | ) 122 | 123 | type cmp struct { 124 | diff []string 125 | buff []string 126 | floatFormat string 127 | } 128 | 129 | var errorType = reflect.TypeOf((*error)(nil)).Elem() 130 | 131 | // DeepEqual compares variables a and b, recursing into their structure up to 132 | // MaxDepth levels deep (if greater than zero), and returns a list of differences, 133 | // or nil if there are none. Some differences may not be found if an error is 134 | // also returned. 135 | // 136 | // If a type has an Equal method, like time.Equal, it is called to check for 137 | // equality. 138 | // 139 | // When comparing a struct, if a field has the tag `deep:"-"` then it will be 140 | // ignored. 141 | func DeepEqual(a, b interface{}) []string { 142 | aVal := reflect.ValueOf(a) 143 | bVal := reflect.ValueOf(b) 144 | c := &cmp{ 145 | diff: []string{}, 146 | buff: []string{}, 147 | floatFormat: fmt.Sprintf("%%.%df", FloatPrecision), 148 | } 149 | if a == nil && b == nil { 150 | return nil 151 | } else if a == nil && b != nil { 152 | c.saveDiff("", b) 153 | } else if a != nil && b == nil { 154 | c.saveDiff(a, "") 155 | } 156 | if len(c.diff) > 0 { 157 | return c.diff 158 | } 159 | 160 | c.equals(aVal, bVal, 0) 161 | if len(c.diff) > 0 { 162 | return c.diff // diffs 163 | } 164 | return nil // no diffs 165 | } 166 | 167 | func (c *cmp) equals(a, b reflect.Value, level int) { 168 | if MaxDepth > 0 && level > MaxDepth { 169 | logError(ErrMaxRecursion) 170 | return 171 | } 172 | 173 | // Check if one value is nil, e.g. T{x: *X} and T.x is nil 174 | if !a.IsValid() || !b.IsValid() { 175 | if a.IsValid() && !b.IsValid() { 176 | c.saveDiff(a.Type(), "") 177 | } else if !a.IsValid() && b.IsValid() { 178 | c.saveDiff("", b.Type()) 179 | } 180 | return 181 | } 182 | 183 | // If different types, they can't be equal 184 | aType := a.Type() 185 | bType := b.Type() 186 | if aType != bType { 187 | // Built-in types don't have a name, so don't report [3]int != [2]int as " != " 188 | if aType.Name() == "" || aType.Name() != bType.Name() { 189 | c.saveDiff(aType, bType) 190 | } else { 191 | // Type names can be the same, e.g. pkg/v1.Error and pkg/v2.Error 192 | // are both exported as pkg, so unless we include the full pkg path 193 | // the diff will be "pkg.Error != pkg.Error" 194 | // https://github.com/go-test/deep/issues/39 195 | aFullType := aType.PkgPath() + "." + aType.Name() 196 | bFullType := bType.PkgPath() + "." + bType.Name() 197 | c.saveDiff(aFullType, bFullType) 198 | } 199 | logError(ErrTypeMismatch) 200 | return 201 | } 202 | 203 | // Primitive https://golang.org/pkg/reflect/#Kind 204 | aKind := a.Kind() 205 | bKind := b.Kind() 206 | 207 | // Do a and b have underlying elements? Yes if they're ptr or interface. 208 | aElem := aKind == reflect.Ptr || aKind == reflect.Interface 209 | bElem := bKind == reflect.Ptr || bKind == reflect.Interface 210 | 211 | // If both types implement the error interface, compare the error strings. 212 | // This must be done before dereferencing because the interface is on a 213 | // pointer receiver. Re https://github.com/go-test/deep/issues/31, a/b might 214 | // be primitive kinds; see TestErrorPrimitiveKind. 215 | if aType.Implements(errorType) && bType.Implements(errorType) { 216 | if (!aElem || !a.IsNil()) && (!bElem || !b.IsNil()) { 217 | aString := a.MethodByName("Error").Call(nil)[0].String() 218 | bString := b.MethodByName("Error").Call(nil)[0].String() 219 | if aString != bString { 220 | c.saveDiff(aString, bString) 221 | return 222 | } 223 | } 224 | } 225 | 226 | // Dereference pointers and interface{} 227 | if aElem || bElem { 228 | if aElem { 229 | a = a.Elem() 230 | } 231 | if bElem { 232 | b = b.Elem() 233 | } 234 | c.equals(a, b, level+1) 235 | return 236 | } 237 | 238 | switch aKind { 239 | 240 | ///////////////////////////////////////////////////////////////////// 241 | // Iterable kinds 242 | ///////////////////////////////////////////////////////////////////// 243 | 244 | case reflect.Struct: 245 | /* 246 | The variables are structs like: 247 | type T struct { 248 | FirstName string 249 | LastName string 250 | } 251 | Type = .T, Kind = reflect.Struct 252 | Iterate through the fields (FirstName, LastName), recurse into their values. 253 | */ 254 | 255 | // Types with an Equal() method, like time.Time, only if struct field 256 | // is exported (CanInterface) 257 | if eqFunc := a.MethodByName("Equal"); eqFunc.IsValid() && eqFunc.CanInterface() { 258 | // Handle https://github.com/go-test/deep/issues/15: 259 | // Don't call T.Equal if the method is from an embedded struct, like: 260 | // type Foo struct { time.Time } 261 | // First, we'll encounter Equal(Ttime, time.Time) but if we pass b 262 | // as the 2nd arg we'll panic: "Call using pkg.Foo as type time.Time" 263 | // As far as I can tell, there's no way to see that the method is from 264 | // time.Time not Foo. So we check the type of the 1st (0) arg and skip 265 | // unless it's b type. Later, we'll encounter the time.Time anonymous/ 266 | // embedded field and then we'll have Equal(time.Time, time.Time). 267 | funcType := eqFunc.Type() 268 | if funcType.NumIn() == 1 && funcType.In(0) == bType { 269 | retVals := eqFunc.Call([]reflect.Value{b}) 270 | if !retVals[0].Bool() { 271 | c.saveDiff(a, b) 272 | } 273 | return 274 | } 275 | } 276 | 277 | for i := 0; i < a.NumField(); i++ { 278 | if aType.Field(i).PkgPath != "" && !CompareUnexportedFields { 279 | continue // skip unexported field, e.g. s in type T struct {s string} 280 | } 281 | 282 | if aType.Field(i).Tag.Get("deep") == "-" { 283 | continue // field wants to be ignored 284 | } 285 | 286 | c.push(aType.Field(i).Name) // push field name to buff 287 | 288 | // Get the Value for each field, e.g. FirstName has Type = string, 289 | // Kind = reflect.String. 290 | af := a.Field(i) 291 | bf := b.Field(i) 292 | 293 | // Recurse to compare the field values 294 | c.equals(af, bf, level+1) 295 | 296 | c.pop() // pop field name from buff 297 | 298 | if len(c.diff) >= MaxDiff { 299 | break 300 | } 301 | } 302 | case reflect.Map: 303 | /* 304 | The variables are maps like: 305 | map[string]int{ 306 | "foo": 1, 307 | "bar": 2, 308 | } 309 | Type = map[string]int, Kind = reflect.Map 310 | Or: 311 | type T map[string]int{} 312 | Type = .T, Kind = reflect.Map 313 | Iterate through the map keys (foo, bar), recurse into their values. 314 | */ 315 | 316 | if a.IsNil() || b.IsNil() { 317 | if NilMapsAreEmpty { 318 | if a.IsNil() && b.Len() != 0 { 319 | c.saveDiff("", b) 320 | return 321 | } else if a.Len() != 0 && b.IsNil() { 322 | c.saveDiff(a, "") 323 | return 324 | } 325 | } else { 326 | if a.IsNil() && !b.IsNil() { 327 | c.saveDiff("", b) 328 | } else if !a.IsNil() && b.IsNil() { 329 | c.saveDiff(a, "") 330 | } 331 | } 332 | return 333 | } 334 | 335 | if a.Pointer() == b.Pointer() { 336 | return 337 | } 338 | 339 | for _, key := range a.MapKeys() { 340 | c.push(fmt.Sprintf("map[%v]", key)) 341 | 342 | aVal := a.MapIndex(key) 343 | bVal := b.MapIndex(key) 344 | if bVal.IsValid() { 345 | c.equals(aVal, bVal, level+1) 346 | } else { 347 | c.saveDiff(aVal, "") 348 | } 349 | 350 | c.pop() 351 | 352 | if len(c.diff) >= MaxDiff { 353 | return 354 | } 355 | } 356 | 357 | for _, key := range b.MapKeys() { 358 | if aVal := a.MapIndex(key); aVal.IsValid() { 359 | continue 360 | } 361 | 362 | c.push(fmt.Sprintf("map[%v]", key)) 363 | c.saveDiff("", b.MapIndex(key)) 364 | c.pop() 365 | if len(c.diff) >= MaxDiff { 366 | return 367 | } 368 | } 369 | case reflect.Array: 370 | n := a.Len() 371 | for i := 0; i < n; i++ { 372 | c.push(fmt.Sprintf("array[%d]", i)) 373 | c.equals(a.Index(i), b.Index(i), level+1) 374 | c.pop() 375 | if len(c.diff) >= MaxDiff { 376 | break 377 | } 378 | } 379 | case reflect.Slice: 380 | if NilSlicesAreEmpty { 381 | if a.IsNil() && b.Len() != 0 { 382 | c.saveDiff("", b) 383 | return 384 | } else if a.Len() != 0 && b.IsNil() { 385 | c.saveDiff(a, "") 386 | return 387 | } 388 | } else { 389 | if a.IsNil() && !b.IsNil() { 390 | c.saveDiff("", b) 391 | return 392 | } else if !a.IsNil() && b.IsNil() { 393 | c.saveDiff(a, "") 394 | return 395 | } 396 | } 397 | 398 | aLen := a.Len() 399 | bLen := b.Len() 400 | 401 | if a.Pointer() == b.Pointer() && aLen == bLen { 402 | return 403 | } 404 | 405 | n := aLen 406 | if bLen > aLen { 407 | n = bLen 408 | } 409 | for i := 0; i < n; i++ { 410 | c.push(fmt.Sprintf("slice[%d]", i)) 411 | if i < aLen && i < bLen { 412 | c.equals(a.Index(i), b.Index(i), level+1) 413 | } else if i < aLen { 414 | c.saveDiff(a.Index(i), "") 415 | } else { 416 | c.saveDiff("", b.Index(i)) 417 | } 418 | c.pop() 419 | if len(c.diff) >= MaxDiff { 420 | break 421 | } 422 | } 423 | 424 | ///////////////////////////////////////////////////////////////////// 425 | // Primitive kinds 426 | ///////////////////////////////////////////////////////////////////// 427 | 428 | case reflect.Float32, reflect.Float64: 429 | // Round floats to FloatPrecision decimal places to compare with 430 | // user-defined precision. As is commonly know, floats have "imprecision" 431 | // such that 0.1 becomes 0.100000001490116119384765625. This cannot 432 | // be avoided; it can only be handled. Issue 30 suggested that floats 433 | // be compared using an epsilon: equal = |a-b| < epsilon. 434 | // In many cases the result is the same, but I think epsilon is a little 435 | // less clear for users to reason about. See issue 30 for details. 436 | aval := fmt.Sprintf(c.floatFormat, a.Float()) 437 | bval := fmt.Sprintf(c.floatFormat, b.Float()) 438 | if aval != bval { 439 | c.saveDiff(a.Float(), b.Float()) 440 | } 441 | case reflect.Bool: 442 | if a.Bool() != b.Bool() { 443 | c.saveDiff(a.Bool(), b.Bool()) 444 | } 445 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 446 | if a.Int() != b.Int() { 447 | c.saveDiff(a.Int(), b.Int()) 448 | } 449 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 450 | if a.Uint() != b.Uint() { 451 | c.saveDiff(a.Uint(), b.Uint()) 452 | } 453 | case reflect.String: 454 | if a.String() != b.String() { 455 | c.saveDiff(a.String(), b.String()) 456 | } 457 | 458 | default: 459 | logError(ErrNotHandled) 460 | } 461 | } 462 | 463 | func (c *cmp) push(name string) { 464 | c.buff = append(c.buff, name) 465 | } 466 | 467 | func (c *cmp) pop() { 468 | if len(c.buff) > 0 { 469 | c.buff = c.buff[0 : len(c.buff)-1] 470 | } 471 | } 472 | 473 | func (c *cmp) saveDiff(aval, bval interface{}) { 474 | if len(c.buff) > 0 { 475 | varName := strings.Join(c.buff, ".") 476 | c.diff = append(c.diff, fmt.Sprintf("%s: %v != %v", varName, aval, bval)) 477 | } else { 478 | c.diff = append(c.diff, fmt.Sprintf("%v != %v", aval, bval)) 479 | } 480 | } 481 | 482 | func logError(err error) { 483 | if LogErrors { 484 | log.Println(err) 485 | } 486 | } 487 | -------------------------------------------------------------------------------- /internal/netcode/rtt/rtt.go: -------------------------------------------------------------------------------- 1 | package rtt 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // todo(jae): 2021-04-02 8 | // Rename this to AckTracker, ack.Tracker? 9 | // PacketSequencer? 10 | // 11 | // As it tracks packet sequences and acknowledgements of 12 | // those packet sequences. 13 | 14 | const ( 15 | // roundTripTimeLimit is the amount of time that we store sent packets 16 | // which we use to determine the RTT / Latency between server/client. 17 | // 18 | // After reading blog posts by GafferonGames, he recommends to hold these for 1 second, as if those packets 19 | // haven't been acknolwedged after a second, they're most likely wildly out of date anyway and not helpful 20 | // for measuring RTT. 21 | // 22 | // Source: 23 | // https://gafferongames.com/post/reliability_ordering_and_congestion_avoidance_over_udp/#measuring-round-trip-time 24 | // 25 | maximumRoundTripTimeLimit = 1000 // in milliseconds 26 | 27 | maxFramerate = 60 28 | 29 | // roundTripTimeLimitInFrames is used to setup the fixed-size array for storing 30 | roundTripTimeLimitInFrames = (maximumRoundTripTimeLimit / 1000) * maxFramerate 31 | ) 32 | 33 | type RoundTripTracking struct { 34 | packetSequenceID uint16 35 | packetSequenceList [roundTripTimeLimitInFrames]packetSequence 36 | latency time.Duration 37 | } 38 | 39 | type packetSequence struct { 40 | SequenceID uint16 41 | Time time.Time 42 | } 43 | 44 | // Latency will return the smoothed average latency based on acknowledged 45 | // packets 46 | func (rtt *RoundTripTracking) Latency() time.Duration { 47 | return rtt.latency 48 | } 49 | 50 | func (rtt *RoundTripTracking) Next() uint16 { 51 | seqID := rtt.packetSequenceID 52 | rtt.packetSequenceID++ 53 | 54 | // Store current time in free slot (anything older than 1000ms) 55 | now := time.Now() 56 | for i, _ := range rtt.packetSequenceList { 57 | sequence := &rtt.packetSequenceList[i] 58 | if now.Sub(sequence.Time).Milliseconds() > maximumRoundTripTimeLimit { 59 | sequence.SequenceID = seqID 60 | sequence.Time = now 61 | break 62 | } 63 | } 64 | return seqID 65 | } 66 | 67 | func (rtt *RoundTripTracking) Ack(seqID uint16) { 68 | var foundSequence *packetSequence 69 | for i := range rtt.packetSequenceList { 70 | sequence := &rtt.packetSequenceList[i] 71 | if sequence.SequenceID == seqID { 72 | foundSequence = sequence 73 | break 74 | } 75 | } 76 | if foundSequence == nil { 77 | // If the sequence expired or is too old, we ignore it 78 | return 79 | } 80 | timePacketSent := time.Since(foundSequence.Time) 81 | if rtt.latency == 0 { 82 | rtt.latency = timePacketSent 83 | } else { 84 | rtt.latency = time.Duration(float64(rtt.latency) + (0.10 * float64(timePacketSent-rtt.latency))) 85 | } 86 | } 87 | 88 | // IsWrappedUInt16GreaterThan checks to see if a is greater than b but accounts 89 | // for overflowing numbers. 90 | // 91 | // This means that: 92 | // - If a = 101 and b = 100, then a is greater than b. 93 | // - If a = 1 and b is 65000, then a is greater than b. (as its overflowed and looped) 94 | // 95 | // Source: https://gafferongames.com/post/reliability_ordering_and_congestion_avoidance_over_udp/ 96 | func IsWrappedUInt16GreaterThan(s1 uint16, s2 uint16) bool { 97 | return ((s1 > s2) && (s1-s2 <= 32768)) || 98 | ((s1 < s2) && (s2-s1 > 32768)) 99 | } 100 | -------------------------------------------------------------------------------- /internal/netcode/rtt/rtt_test.go: -------------------------------------------------------------------------------- 1 | package rtt 2 | 3 | import "testing" 4 | 5 | type testCase struct { 6 | A uint16 7 | B uint16 8 | Output bool 9 | } 10 | 11 | var goldenTests = []testCase{ 12 | { 13 | A: 1, 14 | B: 0, 15 | Output: true, 16 | }, 17 | { 18 | A: 101, 19 | B: 100, 20 | Output: true, 21 | }, 22 | { 23 | A: 65001, 24 | B: 65000, 25 | Output: true, 26 | }, 27 | { 28 | A: 1, 29 | B: 65000, 30 | Output: true, 31 | }, 32 | { 33 | A: 300, 34 | B: 65000, 35 | Output: true, 36 | }, 37 | } 38 | 39 | // IsWrappedUInt16GreaterThan will test all the packet types to ensure writing / reading works 40 | // for each registered struct 41 | func TestIsWrappedUInt16GreaterThan(t *testing.T) { 42 | for _, test := range goldenTests { 43 | if res := IsWrappedUInt16GreaterThan(test.A, test.B); res != test.Output { 44 | t.Errorf("failed on input (%d, %d), returned %v but expected %v", test.A, test.B, res, test.Output) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /internal/netcode/server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "log" 8 | 9 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/ent" 10 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/netcode" 11 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/netcode/netconf" 12 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/netcode/netconst" 13 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/netcode/packs" 14 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/netcode/rtt" 15 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/netdriver/webrtcdriver/webrtcserver" 16 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/world" 17 | ) 18 | 19 | const ( 20 | enableDebugPrintingInputFrameBuffer = false 21 | ) 22 | 23 | // compile-time assert we implement this interface 24 | var _ netcode.Controller = new(Controller) 25 | 26 | func New(options netconf.Options) *Controller { 27 | net := &Controller{} 28 | net.server = webrtcserver.New(webrtcserver.Options{ 29 | PublicIP: options.PublicIP, 30 | ICEServerURLs: []string{"stun:" + options.PublicIP + ":3478"}, 31 | }) 32 | return net 33 | } 34 | 35 | type Controller struct { 36 | server *webrtcserver.Server 37 | gameConnections []*gameConnection 38 | 39 | buf *bytes.Buffer 40 | backingBuf [65536]byte 41 | 42 | hasStarted bool 43 | worldSnapshots [][]byte 44 | } 45 | 46 | // gameConnection is data specifically related to game-logic and de-coupled from our network driver 47 | type gameConnection struct { 48 | ID uint16 49 | Player *ent.Player 50 | IsUsed bool 51 | AckPacket packs.AckPacket 52 | 53 | rtt rtt.RoundTripTracking 54 | InputBuffer []packs.ClientFrameInput 55 | 56 | // LastInputFrameSimulated is the last frame number we've simulated 57 | LastInputFrameSimulated uint16 58 | // NextInputFrameToBeSimulated is the frame number we erecieved from the client that we're going to 59 | // simulate this frame 60 | NextInputFrameToBeSimulated uint16 61 | } 62 | 63 | func (net *Controller) init(world *world.World) { 64 | net.gameConnections = make([]*gameConnection, len(net.server.Connections())) 65 | for i := 0; i < len(net.server.Connections()); i++ { 66 | gameConn := &gameConnection{} 67 | // note: ID should never be 0 68 | gameConn.ID = uint16(i) + 1 69 | net.gameConnections[i] = gameConn 70 | } 71 | net.buf = bytes.NewBuffer(net.backingBuf[:]) 72 | 73 | log.Printf("starting server...") 74 | net.server.Start() 75 | log.Printf("server started") 76 | } 77 | 78 | func (net *Controller) HasStartedOrConnected() bool { 79 | return net.server.IsListening() 80 | } 81 | 82 | func (net *Controller) BeforeUpdate(world *world.World) { 83 | if !net.hasStarted { 84 | net.init(world) 85 | net.hasStarted = true 86 | } 87 | 88 | // Take a snapshot of world state so we can rewind the universe and 89 | // playback a players actions when we receive inputs 90 | /* const maxSnapshotCount = 10 91 | if len(net.worldSnapshots) >= maxSnapshotCount { 92 | for i := 1; i < len(net.worldSnapshots); i++ { 93 | net.worldSnapshots[i-1] = net.worldSnapshots[i] 94 | } 95 | net.worldSnapshots[len(net.worldSnapshots)-1] = world.Snapshot() 96 | } else { 97 | net.worldSnapshots = append(net.worldSnapshots, world.Snapshot()) 98 | } */ 99 | 100 | for i, conn := range net.server.Connections() { 101 | gameConn := net.gameConnections[i] 102 | if !conn.IsConnected() { 103 | if gameConn.IsUsed { 104 | world.RemovePlayer(gameConn.Player) 105 | 106 | // Reset slot 107 | id := gameConn.ID 108 | *gameConn = gameConnection{} 109 | // note: ID should never be 0 110 | gameConn.ID = id 111 | conn.Free() 112 | } 113 | continue 114 | } 115 | if !gameConn.IsUsed { 116 | log.Printf("New connection! Creating new player...\n") 117 | gameConn.Player = world.CreatePlayer() 118 | gameConn.Player.NetID = gameConn.ID 119 | if gameConn.ID == 0 { 120 | panic("developer mistake, ID should never be 0") 121 | } 122 | gameConn.IsUsed = true 123 | } 124 | 125 | // read packets 126 | MainReadLoop: 127 | for { 128 | byteData, ok := conn.Read() 129 | if !ok { 130 | break 131 | } 132 | var buf bytes.Reader 133 | buf.Reset(byteData) 134 | for { 135 | sequenceID, packet, err := packs.Read(&buf) 136 | if err != nil { 137 | if err == io.EOF { 138 | break 139 | } 140 | log.Printf("unable to read packet: %v", err) 141 | continue 142 | } 143 | if _, ok := packet.(*packs.AckPacket); !ok { 144 | // to avoid recursion, we don't acknowledge acknowledgement packets 145 | gameConn.AckPacket.SequenceIDList = append(gameConn.AckPacket.SequenceIDList, sequenceID) 146 | } 147 | switch packet := packet.(type) { 148 | case *packs.AckPacket: 149 | for _, seqID := range packet.SequenceIDList { 150 | gameConn.rtt.Ack(seqID) 151 | } 152 | case *packs.ClientPlayerPacket: 153 | if len(packet.InputBuffer) > netconst.MaxServerInputBuffer { 154 | fmt.Printf("disconnecting client, they sent %d input packets when the limit is %d", len(packet.InputBuffer), netconst.MaxServerInputBuffer) 155 | conn.CloseButDontFree() 156 | break MainReadLoop 157 | } 158 | if len(packet.InputBuffer) == 0 { 159 | // do nothing 160 | } else { 161 | if len(gameConn.InputBuffer) > 0 { 162 | // We only use the given input buffer if the last item 163 | // is on a later frame than the current input buffer 164 | prevInputBuffer := gameConn.InputBuffer[len(gameConn.InputBuffer)-1] 165 | nextInputBuffer := packet.InputBuffer[len(packet.InputBuffer)-1] 166 | if rtt.IsWrappedUInt16GreaterThan(nextInputBuffer.Frame, prevInputBuffer.Frame) { 167 | gameConn.InputBuffer = packet.InputBuffer 168 | } 169 | } else { 170 | gameConn.InputBuffer = packet.InputBuffer 171 | } 172 | } 173 | default: 174 | log.Printf("unhandled packet type: %T", packet) 175 | } 176 | } 177 | } 178 | } 179 | 180 | // note(jae): 2021-04-03 181 | for i, conn := range net.server.Connections() { 182 | if !conn.IsConnected() { 183 | continue 184 | } 185 | gameConn := net.gameConnections[i] 186 | if !gameConn.IsUsed || 187 | gameConn.Player == nil { 188 | // Skip if not used or have no player 189 | continue 190 | } 191 | // Get the next un-simulated input from the clients buffer of inputs 192 | // and set the client entity to use that when simulating this frame 193 | var inputBuffer packs.ClientFrameInput 194 | var foundCount int 195 | 196 | // note(jae): 2021-04-05 197 | // Consider replacing this with generous world rewinding... ie. 198 | // - get rtt of player (ie. 60ms - 300ms) 199 | // - rewind world N frames and process inputs in the past 200 | // 201 | // Why do this? 202 | // Well imagine you're playing a precise platformer like Celeste and you 203 | // press to jump to a platform and manage to just land on it by 1-pixel / 1 frame. 204 | // If the server ends up processing your input even 1 frame later, the server will end up 205 | // making you jump a frame later than you intended, and you'll just miss the platform. 206 | // ie. from the servers perspective, you pressed jump 1 frame late, so technically you never jumped and 207 | // you just fell off. 208 | // 209 | // (Imagine you're lagging by just 30ms, this means the server will process your inputs 1-2 frames later. 210 | // 60ms? 3-4 frames. 350ms? 21-22 frames.) 211 | // 212 | // By rewinding the world to be what it was from the players perspective, they will definitely make the jump on the server-side. 213 | // *But* this means that if we decide to mix pixel precise platforming and VS battles, we can get inconsistencies 214 | // where players might see another player falling over for a bit, then be corrected suddenly... or hackers could potentially 215 | // create tools to just say "we jumped 10 frames in the past" if they miss a jump and the server will correct it. 216 | // 217 | // People on good network connections could potentially figure out how to abuse this in other creative ways without hacking 218 | // too. 219 | if enableDebugPrintingInputFrameBuffer { 220 | fmt.Printf("------ Frame --------\n") 221 | fmt.Printf("Last simulated: %d\n", gameConn.LastInputFrameSimulated) 222 | } 223 | for _, otherInputBuffer := range gameConn.InputBuffer { 224 | if otherInputBuffer.Frame == 0 { 225 | log.Printf("invalid packet data or developer mistake, frame should never be 0. closing") 226 | conn.CloseButDontFree() 227 | break 228 | } 229 | if rtt.IsWrappedUInt16GreaterThan(otherInputBuffer.Frame, gameConn.LastInputFrameSimulated) { 230 | if foundCount == 0 { 231 | if enableDebugPrintingInputFrameBuffer { 232 | fmt.Printf("\n- Frame (found): %d\n", otherInputBuffer.Frame) 233 | } 234 | inputBuffer = otherInputBuffer 235 | } else if enableDebugPrintingInputFrameBuffer { 236 | fmt.Printf("%d, ", otherInputBuffer.Frame) 237 | } 238 | foundCount++ 239 | continue 240 | } 241 | if enableDebugPrintingInputFrameBuffer { 242 | fmt.Printf("%d, ", otherInputBuffer.Frame) 243 | } 244 | } 245 | if enableDebugPrintingInputFrameBuffer { 246 | fmt.Printf("\n------ End Frame --------\n") 247 | fmt.Printf("Net RTT: %v\n", gameConn.rtt.Latency()) 248 | } 249 | // note(jae): 2021-04-05 250 | // we might want to adjust this later so we can handle 251 | // input more smoothly with jitter 252 | // ie. foundCount > 1 or foundCount > 2 253 | // 254 | // We want to smooth out input packets because they can sometimes arrive inconsistently 255 | // ie. frame 1 - get 1 input packet 256 | // frame 2 - no input packet 257 | // frame 3 - get 2 input packets 258 | // 259 | // Because packets don't necessarily arrive together, a jitter buffer ensures that groups of inputs 260 | // are processed together such as a combo in a fighting game, or a precise platforming manuver. 261 | // Without this, a gap in the inputs being processed could lead to a combo/manuver being broken. 262 | // 263 | // The problem with using an input jitter buffer though is that we end up processing frames later. 264 | // (Which might be a non-problem if we implement the rewinding world system mentioned above in a big comment) 265 | if foundCount > 0 { 266 | if inputBuffer.Frame == 0 { 267 | log.Printf("invalid packet data or developer mistake, frame should never be 0. closing") 268 | conn.CloseButDontFree() 269 | break 270 | } 271 | gameConn.Player.Inputs = inputBuffer.PlayerInput 272 | gameConn.LastInputFrameSimulated = gameConn.NextInputFrameToBeSimulated 273 | gameConn.NextInputFrameToBeSimulated = inputBuffer.Frame 274 | } else { 275 | // reset to all zero values 276 | gameConn.Player.Inputs = ent.PlayerInput{} 277 | } 278 | } 279 | 280 | // Send player data to everybody on every frame 281 | // (this is not good engineering, this isnt even OK engineering) 282 | for i, conn := range net.server.Connections() { 283 | if !conn.IsConnected() { 284 | continue 285 | } 286 | gameConn := net.gameConnections[i] 287 | if !gameConn.IsUsed { 288 | // skip if not used 289 | continue 290 | } 291 | net.buf.Reset() 292 | if len(gameConn.AckPacket.SequenceIDList) > 0 { 293 | if err := packs.Write(net.buf, &gameConn.rtt, &gameConn.AckPacket); err != nil { 294 | log.Printf("failed to write ack packet: %v, closing connection", err) 295 | conn.CloseButDontFree() 296 | continue 297 | } 298 | gameConn.AckPacket.SequenceIDList = gameConn.AckPacket.SequenceIDList[:0] 299 | } 300 | if player := gameConn.Player; player != nil { 301 | stateUpdateList := make([]packs.PlayerState, 0, len(world.Players)) 302 | for _, entity := range world.Players { 303 | stateUpdateList = append(stateUpdateList, packs.PlayerState{ 304 | NetID: entity.NetID, 305 | X: entity.X, 306 | Y: entity.Y, 307 | Hspeed: entity.Hspeed, 308 | Vspeed: entity.Vspeed, 309 | DirLeft: entity.DirLeft, 310 | }) 311 | } 312 | if err := packs.Write(net.buf, &gameConn.rtt, &packs.ServerWorldStatePacket{ 313 | MyNetID: player.NetID, 314 | LastSimulatedInputFrame: gameConn.LastInputFrameSimulated, 315 | Players: stateUpdateList, 316 | }); err != nil { 317 | log.Printf("failed to write world update packet: %v", err) 318 | conn.CloseButDontFree() 319 | continue 320 | } 321 | } 322 | // Upper limit of packets in gamedev are generally: "something like 1000 to 1200 bytes of payload data" 323 | // source: https://www.gafferongames.com/post/packet_fragmentation_and_reassembly/ 324 | if net.buf.Len() > 1000 { 325 | // note(jae): 2021-04-02 326 | // when i looked at raw packet data in Wireshark, packets were about ~100 bytes, even if i was sending ~20 bytes 327 | // of data. DTLS v1.2 / WebRTC / DataChannels may have a 100 byte overhead that I need to consider 328 | // when printing this kind of warning logic 329 | log.Printf("warning: size of packet is %d, should be conservative and fit between 1000-1200", net.buf.Len()) 330 | } 331 | // DEBUG: uncomment to debug packet size 332 | //log.Printf("note: size of packet is %d (rtt latency: %v)", net.buf.Len(), gameConn.rtt.Latency()) 333 | 334 | if err := conn.Send(net.buf.Bytes()); err != nil { 335 | log.Printf("failed to send: %v", err) 336 | conn.CloseButDontFree() 337 | continue 338 | } 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /internal/netdriver/webrtcdriver/webrtcclient/webrtcclient.go: -------------------------------------------------------------------------------- 1 | package webrtcclient 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | "sync" 8 | "sync/atomic" 9 | 10 | "github.com/pion/webrtc/v3" 11 | "github.com/pkg/errors" 12 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/netdriver/webrtcdriver/webrtcshared" 13 | ) 14 | 15 | type Client struct { 16 | options Options 17 | 18 | mu sync.Mutex 19 | packets chan []byte 20 | peerConnection *webrtc.PeerConnection 21 | dataChannel *webrtc.DataChannel 22 | 23 | lastAtomicError atomic.Value 24 | 25 | // NOTE(Jae): 2020-06-05 26 | // It'd probably be a simpler/better API if instead 27 | // these were 1 value with 3 states: 28 | // - Connecting 29 | // - Connected 30 | // - Disconnected 31 | // But I can't be bothered rewriting this right now or figuring 32 | // out how to make that work with atomics correctly. 33 | _hasConnectedOnce atomic.Value 34 | _isConnected atomic.Value 35 | } 36 | 37 | type Options struct { 38 | IPAddress string 39 | ICEServerURLs []string 40 | } 41 | 42 | func New(options Options) *Client { 43 | if options.IPAddress == "" { 44 | panic("cannot provide empty IP address") 45 | } 46 | client := &Client{} 47 | client.options = options 48 | client._hasConnectedOnce.Store(false) 49 | client.setIsConnected(false) 50 | return client 51 | } 52 | 53 | func (client *Client) setIsConnected(v bool) { 54 | client._isConnected.Store(v) 55 | } 56 | 57 | func (client *Client) IsConnected() bool { 58 | v := client._isConnected.Load().(bool) 59 | return v 60 | } 61 | 62 | func (client *Client) setHasConnectedOnce(v bool) { 63 | client._hasConnectedOnce.Store(v) 64 | } 65 | 66 | func (client *Client) HasConnectedOnce() bool { 67 | v := client._hasConnectedOnce.Load().(bool) 68 | return v 69 | } 70 | 71 | func (client *Client) Disconnect() { 72 | client.close() 73 | client.setIsConnected(false) 74 | } 75 | 76 | func (client *Client) close() { 77 | client.mu.Lock() 78 | defer client.mu.Unlock() 79 | if client.dataChannel != nil { 80 | client.dataChannel.Close() 81 | client.dataChannel = nil 82 | } 83 | if client.peerConnection != nil { 84 | client.peerConnection.Close() 85 | client.peerConnection = nil 86 | } 87 | } 88 | 89 | func (client *Client) GetLastError() error { 90 | v := client.lastAtomicError.Load() 91 | if v == nil { 92 | return nil 93 | } 94 | return v.(error) 95 | } 96 | 97 | func (client *Client) Start() { 98 | go func() { 99 | if err := client.start(); err != nil { 100 | client.lastAtomicError.Store(err) 101 | return 102 | } 103 | }() 104 | } 105 | 106 | func (client *Client) Read() ([]byte, bool) { 107 | client.mu.Lock() 108 | defer client.mu.Unlock() 109 | select { 110 | case data := <-client.packets: 111 | return data, true 112 | default: 113 | // if no data 114 | return nil, false 115 | } 116 | } 117 | 118 | func (client *Client) Send(data []byte) error { 119 | client.mu.Lock() 120 | defer client.mu.Unlock() 121 | if client.dataChannel == nil { 122 | return nil 123 | } 124 | err := client.dataChannel.Send(data) 125 | return err 126 | } 127 | 128 | func postConnect(ipAddress string, offer webrtc.SessionDescription) (webrtcshared.ConnectResponse, error) { 129 | b := new(bytes.Buffer) 130 | err := json.NewEncoder(b).Encode(offer) 131 | if err != nil { 132 | return webrtcshared.ConnectResponse{}, errors.Wrap(err, "unable to encode JSON offer") 133 | } 134 | resp, err := http.Post("http://"+ipAddress+"/sdp", "application/json; charset=utf-8", b) 135 | if err != nil { 136 | return webrtcshared.ConnectResponse{}, errors.Wrap(err, "unable to post JSON offer to SDP") 137 | } 138 | dec := json.NewDecoder(resp.Body) 139 | dec.DisallowUnknownFields() 140 | var connectResp webrtcshared.ConnectResponse 141 | err = dec.Decode(&connectResp) 142 | if err != nil { 143 | // ignore error returned if we failed to close this response body 144 | _ = resp.Body.Close() 145 | 146 | return webrtcshared.ConnectResponse{}, errors.Wrap(err, "decode response from SDP post") 147 | } 148 | if err := resp.Body.Close(); err != nil { 149 | return webrtcshared.ConnectResponse{}, errors.Wrap(err, "failed to close response stream from SDP post") 150 | } 151 | if len(connectResp.Candidates) == 0 { 152 | return webrtcshared.ConnectResponse{}, errors.New("missing candidates from connection, expected more than 0 candidates") 153 | } 154 | return connectResp, nil 155 | } 156 | 157 | func (client *Client) start() error { 158 | // Create a new RTCPeerConnection 159 | config := webrtc.Configuration{ 160 | ICEServers: []webrtc.ICEServer{ 161 | { 162 | // URLs of ICE servers (can be STUN or TURN but we just use STUN) 163 | // eg. []string{"stun:stun.l.google.com:19302"} 164 | URLs: client.options.ICEServerURLs, 165 | }, 166 | }, 167 | } 168 | 169 | // Create a new RTCPeerConnection 170 | peerConnection, err := webrtc.NewPeerConnection(config) 171 | if err != nil { 172 | return errors.Wrap(err, "unable to start peer connection") 173 | } 174 | 175 | // Create a datachannel with label 'data' 176 | dataChannel, err := peerConnection.CreateDataChannel("data", &webrtc.DataChannelInit{ 177 | // NOTE(Jae): 2020-05-05 178 | // To force UDP mode 179 | // - ordered: false 180 | // - maxRetransmits: 0 181 | // Source: https://www.html5rocks.com/en/tutorials/webrtc/datachannels/ 182 | Ordered: new(bool), 183 | MaxRetransmits: new(uint16), 184 | }) 185 | if err != nil { 186 | peerConnection.Close() 187 | dataChannel.Close() 188 | return errors.Wrap(err, "unable to create data channel") 189 | } 190 | 191 | // Set the handler for ICE connection state 192 | // This will notify you when the peer has connected/disconnected 193 | peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { 194 | switch connectionState { 195 | case webrtc.ICEConnectionStateClosed, 196 | webrtc.ICEConnectionStateFailed: 197 | client.close() 198 | // note(jae): 2021-04-15 199 | // explicitly don't handle "disconnected" state as this can be temporary 200 | // and change back to "connected" state in flaky networks 201 | // see: https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/iceConnectionState 202 | //case webrtc.ICETransportStateDisconnected: 203 | } 204 | }) 205 | 206 | // Create an offer to send to the server 207 | offer, err := peerConnection.CreateOffer(&webrtc.OfferOptions{ 208 | // todo(Jae): 2021-28-02 209 | // Look into "ICERestart: true" 210 | // 211 | // As per release docs for webrtc/v3: 212 | // You can now initiate and accept an ICE Restart! This means that if a PeerConnection goes to Disconnected or 213 | // Failed because of network interruption it is no longer fatal. 214 | ICERestart: false, 215 | }) 216 | if err != nil { 217 | peerConnection.Close() 218 | dataChannel.Close() 219 | return errors.Wrap(err, "unable to create offer") 220 | } 221 | 222 | // Sets the LocalDescription, and starts our UDP listeners 223 | if err := peerConnection.SetLocalDescription(offer); err != nil { 224 | peerConnection.Close() 225 | dataChannel.Close() 226 | return errors.Wrap(err, "unable to set local description") 227 | } 228 | 229 | // Exchange the SDP offer and answer using an HTTP Post request. 230 | connectResp, err := postConnect(client.options.IPAddress, offer) 231 | if err != nil { 232 | peerConnection.Close() 233 | dataChannel.Close() 234 | return err 235 | } 236 | 237 | // Register channel opening handling 238 | dataChannel.OnOpen(func() { 239 | client.mu.Lock() 240 | client.peerConnection = peerConnection 241 | client.dataChannel = dataChannel 242 | client.packets = make(chan []byte) 243 | client.mu.Unlock() 244 | 245 | // note(jae): 2021-04-15 246 | // this mixing of mutexes and atomics here for the connection state 247 | // is probably flakey unknown ways I don't yet understand. 248 | // 249 | // Future improvement might be to just put everything under a mutex 250 | // until I have more confidence/practice with atomics 251 | client.setIsConnected(true) 252 | client.setHasConnectedOnce(true) 253 | }) 254 | 255 | // Note(jae): 2021-03-27 256 | // pions/webrtc WASM build is missing "dataChannel.OnError" implementation 257 | // and wont build 258 | //dataChannel.OnError(func(err error) { 259 | // fmt.Printf("Data channel error: %+v", err) 260 | //}) 261 | 262 | // Register text message handling 263 | dataChannel.OnMessage(func(msg webrtc.DataChannelMessage) { 264 | client.packets <- msg.Data 265 | }) 266 | 267 | dataChannel.OnClose(func() { 268 | close(client.packets) 269 | }) 270 | 271 | // Apply the answer as the remote description 272 | err = peerConnection.SetRemoteDescription(connectResp.Answer) 273 | if err != nil { 274 | peerConnection.Close() 275 | dataChannel.Close() 276 | return errors.Wrapf(err, "unable to set remote description: %v", connectResp.Answer) 277 | } 278 | for _, candidate := range connectResp.Candidates { 279 | if err := peerConnection.AddICECandidate(candidate); err != nil { 280 | peerConnection.Close() 281 | dataChannel.Close() 282 | return errors.Wrap(err, "unable to set remote description") 283 | } 284 | } 285 | 286 | return nil 287 | } 288 | -------------------------------------------------------------------------------- /internal/netdriver/webrtcdriver/webrtcserver/stunserver/stunserver.go: -------------------------------------------------------------------------------- 1 | package stunserver 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strconv" 7 | 8 | "github.com/pion/stun" 9 | "github.com/pion/turn/v2" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | const ( 14 | port = 3478 15 | hasLogging = true 16 | ) 17 | 18 | // stunLogger wraps a PacketConn and prints incoming/outgoing STUN packets 19 | // This pattern could be used to capture/inspect/modify data as well 20 | type stunLogger struct { 21 | net.PacketConn 22 | } 23 | 24 | func (s *stunLogger) WriteTo(p []byte, addr net.Addr) (n int, err error) { 25 | if n, err = s.PacketConn.WriteTo(p, addr); err == nil && stun.IsMessage(p) { 26 | msg := &stun.Message{Raw: p} 27 | if err = msg.Decode(); err != nil { 28 | return 29 | } 30 | 31 | fmt.Printf("Outbound STUN: %s \n", msg.String()) 32 | } 33 | 34 | return 35 | } 36 | 37 | func (s *stunLogger) ReadFrom(p []byte) (n int, addr net.Addr, err error) { 38 | if n, addr, err = s.PacketConn.ReadFrom(p); err == nil && stun.IsMessage(p) { 39 | msg := &stun.Message{Raw: p} 40 | if err = msg.Decode(); err != nil { 41 | return 42 | } 43 | 44 | fmt.Printf("Inbound STUN: %s \n", msg.String()) 45 | } 46 | 47 | return 48 | } 49 | 50 | // turnLogger wraps a Listener and prints accepting TURN connections 51 | /* type turnLogger struct { 52 | net.Listener 53 | } 54 | 55 | // Accept waits for and returns the next connection to the listener. 56 | func (t *turnLogger) Accept() (net.Conn, error) { 57 | conn, err := t.Listener.Accept() 58 | if err != nil { 59 | fmt.Printf("Failed TURN connection: %v", err) 60 | return nil, err 61 | } 62 | fmt.Printf("New TURN connection...") 63 | return conn, err 64 | } */ 65 | 66 | func ListenAndStart(publicIP string) (*turn.Server, error) { 67 | if publicIP == "" { 68 | return nil, errors.New("cannot give empty string for public ip") 69 | } 70 | portStr := strconv.Itoa(port) 71 | publicIPParsed := net.ParseIP(publicIP) 72 | 73 | //tcpListener, err := net.Listen("tcp4", "0.0.0.0:"+portStr) 74 | //if err != nil { 75 | // return nil, errors.Wrap(err, "failed to create TURN server listener") 76 | //} 77 | udpListener, err := net.ListenPacket("udp4", "0.0.0.0:"+portStr) 78 | if err != nil { 79 | return nil, errors.Wrap(err, "failed to create STUN server listener") 80 | } 81 | relayAddressGenerator := &turn.RelayAddressGeneratorStatic{ 82 | RelayAddress: publicIPParsed, 83 | Address: "0.0.0.0", 84 | } 85 | if hasLogging { 86 | //tcpListener = &turnLogger{Listener: tcpListener} 87 | udpListener = &stunLogger{udpListener} 88 | } 89 | s, err := turn.NewServer(turn.ServerConfig{ 90 | // Realm SHOULD be the domain name of the provider of the TURN server. 91 | // https://stackoverflow.com/a/63930426/5013410 92 | Realm: "silbinarywolf.com", 93 | // Set AuthHandler callback 94 | // This is called everytime a user tries to authenticate with the TURN server 95 | // Return the key for that user, or false when no user is found 96 | AuthHandler: func(username string, realm string, srcAddr net.Addr) ([]byte, bool) { 97 | // note(jae): 2021-04-15 98 | // this doesn't affect as we don't handle TURN for reasons commented below 99 | // CTRL+F/Search for: "low-latency games" 100 | // fmt.Printf("username: %s, realm: %s, srcAddr: %v\n", username, realm, srcAddr) 101 | return nil, false 102 | }, 103 | // PacketConnConfigs is a list of UDP Listeners and the configuration around them 104 | PacketConnConfigs: []turn.PacketConnConfig{ 105 | { 106 | PacketConn: udpListener, 107 | RelayAddressGenerator: relayAddressGenerator, 108 | }, 109 | }, 110 | // note(Jae): 2021-03-14 111 | // This TURN stuff hasn't been properly tested and so if uncommented 112 | // I'm unsure if it even works. 113 | // 114 | // In anycase, I dont think we *want* a TURN server based on my reading of this: 115 | // https://bloggeek.me/google-free-turn-server/ 116 | // 117 | // My current assumption is that we do NOT want it for low-latency games as it relays 118 | // all TCP packets through it. TCP is slow and not ideal for real-time game designs 119 | // like say Overwatch. 120 | // 121 | // ListenerConfigs: []turn.ListenerConfig{ 122 | // { 123 | // Listener: tcpListener, 124 | // RelayAddressGenerator: relayAddressGenerator, 125 | // }, 126 | //}, 127 | }) 128 | if err != nil { 129 | return nil, errors.Wrap(err, "unable to start TURN/STUN server") 130 | } 131 | 132 | // note(jae): 2021-04-15 133 | // after start-up the server never really wants to close this down but i've kept 134 | // the code here as a note to self on how to close it if we needed to 135 | //if err = s.Close(); err != nil { 136 | // return errors.Wrap(err, "unable to close TURN/STUN server") 137 | //} 138 | 139 | return s, nil 140 | } 141 | -------------------------------------------------------------------------------- /internal/netdriver/webrtcdriver/webrtcserver/webrtcserver.go: -------------------------------------------------------------------------------- 1 | package webrtcserver 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "log" 7 | "net" 8 | "net/http" 9 | "strconv" 10 | "sync" 11 | "sync/atomic" 12 | 13 | "github.com/pion/turn/v2" 14 | "github.com/pion/webrtc/v3" 15 | "github.com/pkg/errors" 16 | 17 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/netdriver/webrtcdriver/webrtcserver/stunserver" 18 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/netdriver/webrtcdriver/webrtcshared" 19 | ) 20 | 21 | const ( 22 | defaultHttpPort = 50000 23 | defaultMaxConnections = 256 24 | defaultPacketLimitPerClient = 256 25 | ) 26 | 27 | type Server struct { 28 | api *webrtc.API 29 | options Options 30 | stunServer *turn.Server 31 | connections []*Connection 32 | } 33 | 34 | type Options struct { 35 | // MaxConnections is the maximum client connections 36 | // 37 | // If not set, this will default to 256 38 | MaxConnections int 39 | // HttpPort of the SDP handler 40 | // 41 | // If not set, this will default to 50000 42 | HttpPort int 43 | PublicIP string 44 | ICEServerURLs []string 45 | 46 | isListening atomic.Value 47 | } 48 | 49 | type Connection struct { 50 | mu sync.Mutex 51 | peerConnection *webrtc.PeerConnection 52 | dataChannel *webrtc.DataChannel 53 | packets chan []byte 54 | isConnected bool 55 | isUsed bool 56 | } 57 | 58 | func (s *Server) Connections() []*Connection { 59 | return s.connections 60 | } 61 | 62 | func (conn *Connection) IsConnected() bool { 63 | conn.mu.Lock() 64 | defer conn.mu.Unlock() 65 | return conn.isConnected 66 | } 67 | 68 | func (conn *Connection) Read() ([]byte, bool) { 69 | conn.mu.Lock() 70 | defer conn.mu.Unlock() 71 | select { 72 | case data := <-conn.packets: 73 | return data, true 74 | default: 75 | // if no data 76 | return nil, false 77 | } 78 | } 79 | 80 | func (conn *Connection) Send(data []byte) error { 81 | conn.mu.Lock() 82 | defer conn.mu.Unlock() 83 | if conn.dataChannel == nil { 84 | // if connection closed, we don't care that 85 | // the packet never sent 86 | // 87 | // using io.ErrClosedPipe for nil dataChannels as 88 | // that's what dataChannel.Send() will use if the 89 | // pipe has been closed 90 | return io.ErrClosedPipe 91 | } 92 | return conn.dataChannel.Send(data) 93 | } 94 | 95 | // CloseButDontFree will close down the connection 96 | // 97 | // But it won't free up the server slot, that should be handled in a loop at the start 98 | // of the frame so it can cleanup player objects / etc 99 | // 100 | // ie. 101 | // for i, conn := range net.server.Connections() { 102 | // gameConn := net.gameConnections[i] 103 | // if !conn.IsConnected() && { 104 | // if gameConn.IsUsed { 105 | // world.RemovePlayer(gameConn.Player) 106 | // conn.Free() 107 | // } 108 | // continue 109 | // } 110 | // } 111 | func (conn *Connection) CloseButDontFree() { 112 | conn.mu.Lock() 113 | defer conn.mu.Unlock() 114 | conn.needsMutexLock_disconnectButKeepMarkedAsUsed() 115 | } 116 | 117 | // needsMutexLock_disconnectButKeepMarkedAsUsed will close the connection but the connection 118 | // slot will stay taken until the consuming code calls the "Free" method 119 | // 120 | // As the prefix suggests, you need to lock the conn and unlock before/after calling this 121 | func (conn *Connection) needsMutexLock_disconnectButKeepMarkedAsUsed() { 122 | if conn.peerConnection != nil { 123 | conn.peerConnection.Close() 124 | conn.peerConnection = nil 125 | } 126 | if conn.dataChannel != nil { 127 | conn.dataChannel.Close() 128 | conn.dataChannel = nil 129 | } 130 | conn.packets = nil 131 | conn.isConnected = false 132 | 133 | // note(jae): 2021-04-04 134 | // isUsed must stay as-is, we only allow this connection slot to be reused 135 | // after the consuming code of this library calls "Free()" on the connection 136 | } 137 | 138 | // Free must be called after a clients disconnection in consumer / user-code. 139 | func (conn *Connection) Free() { 140 | conn.mu.Lock() 141 | defer conn.mu.Unlock() 142 | if conn.isConnected { 143 | panic("cannot call Free if connection is still connected") 144 | } 145 | conn.isUsed = false 146 | } 147 | 148 | func New(options Options) *Server { 149 | if options.HttpPort == 0 { 150 | options.HttpPort = defaultHttpPort 151 | } 152 | if options.MaxConnections == 0 { 153 | options.MaxConnections = defaultMaxConnections 154 | } 155 | if options.PublicIP == "" { 156 | panic("cannot provide empty IP address") 157 | } 158 | 159 | s := &Server{} 160 | s.options.isListening.Store(false) 161 | s.options = options 162 | s.connections = make([]*Connection, options.MaxConnections) 163 | for i := 0; i < options.MaxConnections; i++ { 164 | conn := &Connection{} 165 | s.connections[i] = conn 166 | } 167 | return s 168 | } 169 | 170 | func (s *Server) IsListening() bool { 171 | v, ok := s.options.isListening.Load().(bool) 172 | if !ok { 173 | return false 174 | } 175 | return v 176 | } 177 | 178 | func (s *Server) handleSDP(w http.ResponseWriter, r *http.Request) { 179 | if r.Body == nil { 180 | http.Error(w, "Please send a request body", 400) 181 | return 182 | } 183 | // TODO(jae): 2021-02-28 184 | // Need to think about origin headers and consider locking this down 185 | w.Header().Set("Access-Control-Allow-Origin", "*") 186 | w.Header().Set("Access-Control-Allow-Methods", http.MethodPost) 187 | w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") 188 | if r.Method == http.MethodOptions { 189 | return 190 | } 191 | if r.Method != http.MethodPost { 192 | http.Error(w, "Please send a "+http.MethodPost+" request", 400) 193 | return 194 | } 195 | 196 | var offer webrtc.SessionDescription 197 | if err := json.NewDecoder(r.Body).Decode(&offer); err != nil { 198 | message := "error decoding offer" 199 | log.Printf("%s: %v", message, err) 200 | http.Error(w, message, 500) 201 | return 202 | } 203 | 204 | // Prepare the configuration 205 | config := webrtc.Configuration{ 206 | ICEServers: []webrtc.ICEServer{ 207 | { 208 | // URLs of ICE servers (can be STUN or TURN but we just use STUN) 209 | // eg. []string{"stun:stun.l.google.com:19302"} 210 | URLs: s.options.ICEServerURLs, 211 | }, 212 | }, 213 | SDPSemantics: webrtc.SDPSemanticsUnifiedPlan, 214 | } 215 | 216 | // Create a new RTCPeerConnection 217 | // TODO(Jae): 2020-05-05 218 | // Figure out how to either manually create a DataChannel 219 | // or set it up so its using UDP. 220 | // - maxRetransmits: 0 221 | // - ordered: 0 222 | // Source: https://www.html5rocks.com/en/tutorials/webrtc/datachannels/ 223 | peerConnection, err := s.api.NewPeerConnection(config) 224 | if err != nil { 225 | message := "error creating peer connection" 226 | log.Printf("%s: %v", message, err) 227 | http.Error(w, message, 500) 228 | return 229 | } 230 | 231 | // Set the handler for ICE connection state 232 | // This will notify you when the peer has connected/disconnected 233 | peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { 234 | switch connectionState { 235 | case webrtc.ICEConnectionStateClosed, 236 | webrtc.ICEConnectionStateFailed: 237 | peerConnection.Close() 238 | // note(jae): 2021-04-15 239 | // explicitly don't handle "disconnected" state as this can be temporary 240 | // and change back to "connected" state in flaky networks 241 | // see: https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/iceConnectionState 242 | //case webrtc.ICETransportStateDisconnected: 243 | } 244 | }) 245 | 246 | err = peerConnection.SetRemoteDescription(offer) 247 | if err != nil { 248 | peerConnection.Close() 249 | message := "error creating setting remote description" 250 | log.Printf("%s: %v", message, err) 251 | http.Error(w, message, 500) 252 | return 253 | } 254 | 255 | // Create answer 256 | answer, err := peerConnection.CreateAnswer(nil) 257 | if err != nil { 258 | peerConnection.Close() 259 | message := "error creating creating answer" 260 | log.Printf("%s: %v", message, err) 261 | http.Error(w, message, 500) 262 | return 263 | } 264 | 265 | // Sets the LocalDescription, and starts our UDP listeners 266 | err = peerConnection.SetLocalDescription(answer) 267 | if err != nil { 268 | peerConnection.Close() 269 | message := "error creating creating answer" 270 | log.Printf("%s: %v", message, err) 271 | http.Error(w, message, 500) 272 | return 273 | } 274 | 275 | waitChannel := make(chan struct{}) 276 | 277 | candidateList := make([]webrtc.ICECandidateInit, 0) 278 | candidateErrors := make([]error, 0) 279 | peerConnection.OnICECandidate(func(candidate *webrtc.ICECandidate) { 280 | if candidate == nil { 281 | // if nil than all ICE candidates have been sent from the client 282 | close(waitChannel) 283 | return 284 | } 285 | candidateInit := candidate.ToJSON() 286 | if err := peerConnection.AddICECandidate(candidateInit); err != nil { 287 | // note(jae): 2021-04-15 288 | // We dont do atomics/channels for sending back "hasError" as we assume 289 | // only this goroutine will be writing/reading to "hasError" at this point in time. 290 | // I doubt how sound this is but I'm not sure how to test this in anger either. 291 | candidateErrors = append(candidateErrors, err) 292 | return 293 | } 294 | // note(jae): 2021-04-15 295 | // We dont do atomics/channels for sending back candidates as we assume 296 | // only this goroutine will be writing/reading to candidates at this point in time. 297 | // I doubt how sound this is but I'm not sure how to test this in anger either. 298 | candidateList = append(candidateList, candidateInit) 299 | }) 300 | 301 | <-waitChannel 302 | 303 | if len(candidateErrors) > 0 { 304 | peerConnection.Close() 305 | message := "unexpected error. unable to add ice candidate(s)" 306 | log.Printf("%s: %v", message, candidateErrors) 307 | http.Error(w, message, 500) 308 | return 309 | } 310 | if len(candidateList) == 0 { 311 | peerConnection.Close() 312 | message := "unexpected error. received 0 candidates" 313 | log.Printf("%s: %v", message, err) 314 | http.Error(w, message, 500) 315 | return 316 | } 317 | 318 | // Find a free connection slot 319 | var foundConn *Connection 320 | for _, conn := range s.connections { 321 | conn.mu.Lock() 322 | if conn.isUsed { 323 | conn.mu.Unlock() 324 | continue 325 | } 326 | conn.isUsed = true 327 | conn.peerConnection = peerConnection 328 | conn.mu.Unlock() 329 | 330 | foundConn = conn 331 | break 332 | } 333 | if conn := foundConn; conn == nil { 334 | peerConnection.Close() 335 | 336 | message := "server is full" 337 | log.Print(message) 338 | http.Error(w, message, 503) 339 | return 340 | } 341 | 342 | peerConnection.OnDataChannel(foundConn.onDataChannel) 343 | 344 | if err := json.NewEncoder(w).Encode(&webrtcshared.ConnectResponse{ 345 | Candidates: candidateList, 346 | Answer: answer, 347 | }); err != nil { 348 | foundConn.mu.Lock() 349 | foundConn.needsMutexLock_disconnectButKeepMarkedAsUsed() 350 | foundConn.isUsed = false 351 | foundConn.mu.Unlock() 352 | 353 | message := "unexpected error, unable to encode connection response" 354 | log.Printf("%s: %v", message, err) 355 | http.Error(w, message, 500) 356 | return 357 | } 358 | } 359 | 360 | func (conn *Connection) onDataChannel(dataChannel *webrtc.DataChannel) { 361 | if err := isValidDataChannel(dataChannel); err != nil { 362 | log.Printf("invalid data channel: %v", err) 363 | 364 | // close incoming datachannel 365 | dataChannel.Close() 366 | 367 | // close connection 368 | // - if we don't have a single data channel yet 369 | // - if we got another data channel after the first (should never happen) 370 | conn.mu.Lock() 371 | conn.needsMutexLock_disconnectButKeepMarkedAsUsed() 372 | conn.isUsed = false 373 | conn.mu.Unlock() 374 | return 375 | } 376 | 377 | // setup connection 378 | conn.mu.Lock() 379 | defer conn.mu.Unlock() 380 | if conn.dataChannel != nil { 381 | // if we already have a data channel, close the new one 382 | // and ignore it 383 | // 384 | // In a real world scenario we might want to consider closing the connection 385 | // completely and logging it, as this case occurring could be considered a hack 386 | // attempt. 387 | // 388 | // (ie. someone making the app open another data channel when it was 389 | // never designed to) 390 | dataChannel.Close() 391 | return 392 | } 393 | conn.packets = make(chan []byte) 394 | // note(jae): 2021-04-04 395 | // we only consider a client actually connected once a datachannel 396 | // is opened. This is because if there are UDP port forwarding issues on the server 397 | // this codepath will never be reached. 398 | conn.isConnected = true 399 | conn.dataChannel = dataChannel 400 | conn.dataChannel.OnMessage(conn.onDataChannelMessage) 401 | conn.dataChannel.OnClose(conn.onDataChannelClose) 402 | } 403 | 404 | func isValidDataChannel(dataChannel *webrtc.DataChannel) error { 405 | if dataChannel.Ordered() { 406 | return errors.New("DataChannel tried to connect with \"ordered: true\". Server accepts \"ordered: false\" only for UDP") 407 | } 408 | if dataChannel.MaxRetransmits() == nil { 409 | return errors.New("DataChannel tried to connect with \"maxRetransmits\" not equal to 0. Was nil. Must be 0 for UDP") 410 | } 411 | if maxRetransmits := *dataChannel.MaxRetransmits(); maxRetransmits != 0 { 412 | return errors.New("DataChannel tried to connect with \"maxRetransmits\" not equal to 0. Instead was %v. Must be 0 for UDP") 413 | } 414 | maxPacketLifeTime := dataChannel.MaxPacketLifeTime() 415 | if maxPacketLifeTime != nil { 416 | return errors.New("DataChannel tried to connect with \"maxPacketLifeTime\" not nil. Must be nil for UDP") 417 | } 418 | return nil 419 | } 420 | 421 | func (conn *Connection) onDataChannelClose() { 422 | conn.mu.Lock() 423 | defer conn.mu.Unlock() 424 | conn.needsMutexLock_disconnectButKeepMarkedAsUsed() 425 | } 426 | 427 | func (conn *Connection) onDataChannelMessage(msg webrtc.DataChannelMessage) { 428 | conn.packets <- msg.Data 429 | } 430 | 431 | func (s *Server) Start() { 432 | go func() { 433 | if err := s.start(); err != nil { 434 | panic(err) 435 | } 436 | }() 437 | } 438 | 439 | func (s *Server) start() error { 440 | s.options.isListening.Store(false) 441 | stunServer, err := stunserver.ListenAndStart(s.options.PublicIP) 442 | if err != nil { 443 | return errors.Wrap(err, "failed to start stun server") 444 | } 445 | s.stunServer = stunServer 446 | defer s.stunServer.Close() 447 | 448 | // Setup WebRTC settings 449 | settings := webrtc.SettingEngine{} 450 | 451 | // note(jae): 2021-03-27 452 | // Set explicit UDP port ranges to allow on server box 453 | if err := settings.SetEphemeralUDPPortRange(10000, 11999); err != nil { 454 | return errors.Wrap(err, "failed to set UDP port range for server") 455 | } 456 | s.api = webrtc.NewAPI(webrtc.WithSettingEngine(settings)) 457 | 458 | http.HandleFunc("/sdp", s.handleSDP) 459 | 460 | httpServer := &http.Server{ 461 | Addr: ":" + strconv.Itoa(s.options.HttpPort), 462 | Handler: nil, 463 | } 464 | ln, err := net.Listen("tcp", httpServer.Addr) 465 | if err != nil { 466 | return errors.Wrap(err, "failed to listen on "+httpServer.Addr) 467 | } 468 | s.options.isListening.Store(true) 469 | if err := httpServer.Serve(ln); err != nil { 470 | return errors.Wrap(err, "server closed") 471 | } 472 | return nil 473 | } 474 | -------------------------------------------------------------------------------- /internal/netdriver/webrtcdriver/webrtcshared/webrtcshared.go: -------------------------------------------------------------------------------- 1 | package webrtcshared 2 | 3 | import ( 4 | "github.com/pion/webrtc/v3" 5 | ) 6 | 7 | type ConnectResponse struct { 8 | Candidates []webrtc.ICECandidateInit `json:"candidates"` 9 | Answer webrtc.SessionDescription `json:"answer"` 10 | } 11 | -------------------------------------------------------------------------------- /internal/renderer/internal/ebiten/ebiten.go: -------------------------------------------------------------------------------- 1 | package ebiten 2 | 3 | import ( 4 | "image" 5 | 6 | "github.com/hajimehoshi/ebiten/v2" 7 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/renderer/internal/rendereriface" 8 | ) 9 | 10 | var _ rendereriface.App = new(App) 11 | 12 | type App struct { 13 | } 14 | 15 | type ebitenGameAndScreen struct { 16 | rendereriface.Game 17 | screenDriver Screen 18 | } 19 | 20 | func (game *ebitenGameAndScreen) Draw(screen *ebiten.Image) { 21 | game.screenDriver.screen = screen 22 | game.Game.Draw(&game.screenDriver) 23 | } 24 | 25 | func (app *App) SetRunnableOnUnfocused(v bool) { 26 | ebiten.SetRunnableOnUnfocused(v) 27 | } 28 | 29 | func (app *App) SetWindowSize(width, height int) { 30 | ebiten.SetWindowSize(width, height) 31 | } 32 | 33 | func (app *App) SetWindowTitle(title string) { 34 | ebiten.SetWindowTitle(title) 35 | } 36 | 37 | func (app *App) NewImageFromImage(img image.Image) rendereriface.Image { 38 | return ebiten.NewImageFromImage(img) 39 | } 40 | 41 | func (app *App) RunGame(game rendereriface.Game) error { 42 | gameWrapper := ebitenGameAndScreen{} 43 | gameWrapper.Game = game 44 | return ebiten.RunGame(&gameWrapper) 45 | } 46 | 47 | type Screen struct { 48 | screen *ebiten.Image 49 | } 50 | 51 | var _ rendereriface.Screen = new(Screen) 52 | 53 | func (driver *Screen) DrawImage(img rendereriface.Image, options rendereriface.ImageOptions) { 54 | op := &ebiten.DrawImageOptions{} 55 | op.GeoM.Translate(float64(options.X), float64(options.Y)) 56 | if options.ScaleX != 0 && options.ScaleY != 0 { 57 | op.GeoM.Scale(float64(options.ScaleX), float64(options.ScaleY)) 58 | } 59 | driver.screen.DrawImage(img.(*ebiten.Image), op) 60 | } 61 | -------------------------------------------------------------------------------- /internal/renderer/internal/headless/headless.go: -------------------------------------------------------------------------------- 1 | // headless is the headless mode driver for the game so we can avoid building the 2 | // ebiten library into the server binary 3 | package headless 4 | 5 | import ( 6 | "image" 7 | "time" 8 | 9 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/renderer/internal/rendereriface" 10 | ) 11 | 12 | var _ rendereriface.App = new(App) 13 | 14 | type App struct { 15 | } 16 | 17 | func (app *App) SetRunnableOnUnfocused(v bool) { 18 | // n/a for headless 19 | } 20 | 21 | func (app *App) SetWindowSize(width, height int) { 22 | // n/a for headless 23 | } 24 | 25 | func (app *App) SetWindowTitle(title string) { 26 | // n/a for headless 27 | } 28 | 29 | func (app *App) RunGame(game rendereriface.Game) error { 30 | // note(jae): 2021-03-18 31 | // this should probably align with how the Ebiten clock works 32 | // but I'm going to take a lazy shortcut. 33 | tick := time.NewTicker(16 * time.Millisecond) 34 | for { 35 | select { 36 | case <-tick.C: 37 | if err := game.Update(); err != nil { 38 | return err 39 | } 40 | } 41 | } 42 | return nil 43 | } 44 | 45 | func (app *App) NewImageFromImage(img image.Image) rendereriface.Image { 46 | // n/a for headless 47 | return nil 48 | } 49 | 50 | type Screen struct { 51 | } 52 | -------------------------------------------------------------------------------- /internal/renderer/internal/rendereriface/rendereriface.go: -------------------------------------------------------------------------------- 1 | package rendereriface 2 | 3 | import "image" 4 | 5 | type ImageOptions struct { 6 | X, Y float32 7 | ScaleX, ScaleY float32 8 | } 9 | 10 | type Image interface { 11 | } 12 | 13 | // Game interface was copy-pasted out of Ebiten 14 | type Game interface { 15 | Update() error 16 | Draw(screen Screen) 17 | Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) 18 | } 19 | 20 | type App interface { 21 | SetRunnableOnUnfocused(v bool) 22 | SetWindowSize(screenWidth, screenHeight int) 23 | SetWindowTitle(title string) 24 | RunGame(game Game) error 25 | NewImageFromImage(img image.Image) Image 26 | } 27 | 28 | type Screen interface { 29 | DrawImage(img Image, options ImageOptions) 30 | } 31 | -------------------------------------------------------------------------------- /internal/renderer/renderer.go: -------------------------------------------------------------------------------- 1 | package renderer 2 | 3 | import ( 4 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/renderer/internal/rendereriface" 5 | ) 6 | 7 | // ImageOptions are draw options for an image 8 | type ImageOptions = rendereriface.ImageOptions 9 | 10 | // Image is a sprite loaded by the renderer 11 | type Image = rendereriface.Image 12 | 13 | type Screen = rendereriface.Screen 14 | 15 | // App is the implementation of the renderer 16 | type App = appImplementation // appImplementation changes type based on build tags, we do this so function calls are inlined and cost less, checked with "go build -gcflags=-m=2" 17 | -------------------------------------------------------------------------------- /internal/renderer/renderer_ebiten.go: -------------------------------------------------------------------------------- 1 | // +build !headless 2 | 3 | package renderer 4 | 5 | import ( 6 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/renderer/internal/ebiten" 7 | ) 8 | 9 | type appImplementation = ebiten.App 10 | -------------------------------------------------------------------------------- /internal/renderer/renderer_headless.go: -------------------------------------------------------------------------------- 1 | // +build headless 2 | 3 | package renderer 4 | 5 | import ( 6 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/renderer/internal/headless" 7 | ) 8 | 9 | type appImplementation = headless.App 10 | -------------------------------------------------------------------------------- /internal/world/world.go: -------------------------------------------------------------------------------- 1 | package world 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/ent" 7 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/netcode/packbuf" 8 | ) 9 | 10 | const ( 11 | ScreenWidth = 1280 12 | ScreenHeight = 720 13 | ) 14 | 15 | // worldSnapshot contains all data we want to be stored per-frame 16 | // for rewinding/replaying the simulation 17 | type worldSnapshot struct { 18 | Players []*ent.Player 19 | } 20 | 21 | type World struct { 22 | worldSnapshot 23 | PlayerInput ent.PlayerInput 24 | MyPlayer *ent.Player 25 | } 26 | 27 | func SyncWorldToSnapshot(worldSnapshot *worldSnapshot, snapshotData []byte) { 28 | r := bytes.NewReader(snapshotData) 29 | if err := packbuf.Read(r, worldSnapshot); err != nil { 30 | panic(err) 31 | } 32 | } 33 | 34 | func (worldSnapshot *worldSnapshot) Snapshot() []byte { 35 | w := bytes.NewBuffer(nil) 36 | if err := packbuf.Write(w, worldSnapshot); err != nil { 37 | panic(err) 38 | } 39 | return w.Bytes() 40 | } 41 | 42 | func (world *World) CreatePlayer() *ent.Player { 43 | // Add initial player (just for testing for now) 44 | world.Players = append(world.Players, &ent.Player{ 45 | X: 180, 46 | Y: 180, 47 | }) 48 | entity := world.Players[len(world.Players)-1] 49 | entity.Init() 50 | return entity 51 | } 52 | 53 | func (world *World) RemovePlayer(player *ent.Player) { 54 | // note(jae): 2021-03-04 55 | // slow-ordered remove. reasoning right now is because we want 56 | // draw order to be consistent and im being real lazy. 57 | for i, otherPlayer := range world.Players { 58 | if player == otherPlayer { 59 | world.Players = append(world.Players[:i], world.Players[i+1:]...) 60 | return 61 | } 62 | } 63 | } 64 | 65 | func (world *World) Update() { 66 | for i := range world.Players { 67 | entity := world.Players[i] 68 | entity.Update() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/silbinarywolf/toy-webrtc-mmo/internal/app" 5 | ) 6 | 7 | func main() { 8 | app.StartApp() 9 | } 10 | -------------------------------------------------------------------------------- /screens/web-browser-and-server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silbinarywolf/toy-webrtc-mmo/00588b2dca64fc7344de9222fa6187409b1fb90f/screens/web-browser-and-server.png --------------------------------------------------------------------------------