├── .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 | 
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 |
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 |
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
--------------------------------------------------------------------------------