├── .gitignore ├── .gitmodules ├── README.md ├── demo.gif ├── go.mod ├── go.sum ├── index.html └── server.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.nes 2 | melody-jsnes 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "jsnes"] 2 | path = jsnes 3 | url = https://github.com/bfirsh/jsnes 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # melody-jsnes 2 | 3 | > Super simplistic Multiplayer NES server in Go. 4 | 5 | melody-jsnes is a demo project showing off Go's real-time web app 6 | capabilities. Its design is straight forward, it just snapshots the 7 | canvas of player one and sends it to player two and sends back inputs 8 | from player two. Images data goes in direction, key codes in the other. 9 | 10 | ![demo](https://cdn.rawgit.com/olahol/melody-jsnes/master/demo.gif "Me playing a perfectly legal version of Contra with my friends") 11 | 12 | ## Usage 13 | 14 | You will need to have at least one NES ROM with the extension `.nes`. 15 | 16 | $ git clone --recursive https://github.com/olahol/melody-jsnes 17 | $ go get 18 | $ go build 19 | $ ./melody-jsnes game.nes 20 | $ $BROWSER http://localhost:5000 21 | 22 | ## Contributors 23 | 24 | * Ola Holmström (@olahol) 25 | * Chris Cacciatore (@cacciatc) 26 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olahol/melody-jsnes/7acf271737df094a918cea2382f875733ace1c17/demo.gif -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/olahol/melody-jsnes 2 | 3 | go 1.19 4 | 5 | require github.com/olahol/melody v1.1.1 6 | 7 | require github.com/gorilla/websocket v1.5.0 // indirect 8 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 3 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 4 | github.com/olahol/melody v1.1.1 h1:amgBhR7pDY0rA0JHWprgLF0LnVztognAwEQgf/WYLVM= 5 | github.com/olahol/melody v1.1.1/go.mod h1:GgkTl6Y7yWj/HtfD48Q5vLKPVoZOH+Qqgfa7CvJgJM4= 6 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 7 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 8 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 9 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | melody-jsnes 6 | 38 | 39 | 40 | 41 | 42 |
43 |

Waiting for Player 2

44 |

Arrow keys = joypad, X = a, Z = b, Enter = start, Ctrl = select

45 |
46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 221 | 222 | 223 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "sync" 10 | 11 | "github.com/olahol/melody" 12 | ) 13 | 14 | var ( 15 | //go:embed jsnes/source/*.js 16 | jsnesSourceDir embed.FS 17 | 18 | //go:embed jsnes/lib/*.js 19 | jsnesLibDir embed.FS 20 | 21 | //go:embed index.html 22 | indexHTML []byte 23 | ) 24 | 25 | func main() { 26 | port := flag.Int("p", 5000, "port to listen on") 27 | 28 | flag.Parse() 29 | 30 | rom := flag.Arg(0) 31 | 32 | if rom == "" { 33 | log.Fatalln("no rom file") 34 | } 35 | 36 | http.Handle("/jsnes/source/", http.FileServer(http.FS(jsnesSourceDir))) 37 | http.Handle("/jsnes/lib/", http.FileServer(http.FS(jsnesLibDir))) 38 | 39 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 40 | if r.URL.Path != "/" { 41 | http.NotFound(w, r) 42 | return 43 | } 44 | 45 | w.Write(indexHTML) 46 | }) 47 | 48 | http.HandleFunc("/rom", func(w http.ResponseWriter, r *http.Request) { 49 | http.ServeFile(w, r, rom) 50 | }) 51 | 52 | m := melody.New() 53 | m.Upgrader.ReadBufferSize = 65536 54 | m.Upgrader.WriteBufferSize = 65536 55 | m.Config.MaxMessageSize = 65536 56 | m.Config.MessageBufferSize = 2048 57 | 58 | http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { 59 | m.HandleRequest(w, r) 60 | }) 61 | 62 | var mutex sync.Mutex 63 | pairs := make(map[*melody.Session]*melody.Session) 64 | 65 | m.HandleConnect(func(s *melody.Session) { 66 | log.Println("connect") 67 | mutex.Lock() 68 | var partner *melody.Session 69 | for player1, player2 := range pairs { 70 | if player2 == nil { 71 | partner = player1 72 | pairs[partner] = s 73 | log.Println("start") 74 | partner.Write([]byte("join 1")) 75 | s.Write([]byte("join 2")) 76 | break 77 | } 78 | } 79 | pairs[s] = partner 80 | mutex.Unlock() 81 | }) 82 | 83 | m.HandleDisconnect(func(s *melody.Session) { 84 | log.Println("disconnect") 85 | mutex.Lock() 86 | partner := pairs[s] 87 | if partner != nil { 88 | pairs[partner] = nil 89 | log.Println("stop") 90 | partner.Write([]byte("part")) 91 | } 92 | delete(pairs, s) 93 | mutex.Unlock() 94 | }) 95 | 96 | relay := func(fn func(*melody.Session, []byte) error) func(*melody.Session, []byte) { 97 | return func(s *melody.Session, msg []byte) { 98 | partner := pairs[s] 99 | if partner != nil { 100 | fn(partner, msg) 101 | } 102 | } 103 | } 104 | 105 | m.HandleMessage(relay((*melody.Session).Write)) 106 | m.HandleMessageBinary(relay((*melody.Session).WriteBinary)) 107 | 108 | log.Printf("listening on http://localhost:%d", *port) 109 | 110 | http.ListenAndServe(fmt.Sprint(":", *port), nil) 111 | } 112 | --------------------------------------------------------------------------------