Merge branch 'master' of github.com:giongto35/cloud-game

This commit is contained in:
giongto35 2019-12-10 01:10:53 +08:00
commit d9a91c4876
11 changed files with 80 additions and 68 deletions

6
Makefile vendored
View file

@ -73,13 +73,11 @@ dev.build-local:
dev.run: dev.build-local
./bin/overlord --v=5 &
./bin/overworker --overlordhost ws://localhost:8000/wso
./bin/overworker --overlordhost localhost:8000
dev.run-docker:
docker build . -t cloud-game-local
docker stop cloud-game-local || true
docker rm cloud-game-local || true
# Overlord and worker should be run separately.
docker run --privileged -v $PWD/games:/cloud-game/games -d --name cloud-game-local -p 8000:8000 -p 9000:9000 cloud-game-local bash -c "overlord --v=5 & overworker --overlordhost ws://localhost:8000/wso"
docker run --privileged -v $PWD/games:/cloud-game/games -d --name cloud-game-local -p 8000:8000 -p 9000:9000 cloud-game-local bash -c "overlord --v=5 & overworker --overlordhost localhost:8000"

2
README.md vendored
View file

@ -25,7 +25,7 @@ Crowd play: **[Pokemon Emerald](http://cloudretro.io/?id=652e45d78d2b91cd%7CPoke
2. Cross-platform compatibility: The game is run on web browser, the most universal built-in app. No console, plugin, external app or devices are needed. Chrome with the latest version and fully WebRTC support is recommended for the game.
3. Emulator agnostic: The game can be played directly without any extra effort to set up the gaming emulator or platform.
4. Collaborate gameplay: Follow the idea of crowdplay([TwitchPlaysPokemon](https://en.wikipedia.org/wiki/Twitch_Plays_Pok%C3%A9mon)), multiple players can play the same game together by addressing the same deeplink. The game experience is powered by cloud-gaming, so the game is much smoother. [Check CrowdPlay section](#crowd-play-play-game-together)
5. Vertically scaled: The infrastructure is designed to be able to scale under high traffic by adding more instances.
5. Horizontally scaled: The infrastructure is designed to be able to scale under high traffic by adding more instances.
6. Cloud storage: Game state is storing on online storage, so you can come back and continue playing your incomplete game later.
7. Online multiplayer: 2nd, 3rd player can join the same game in the same room.

BIN
docs/img/overlord.png vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Before After
Before After

View file

@ -5,7 +5,6 @@ import (
"time"
)
const defaultoverlord = "ws://localhost:9000/wso"
const DefaultSTUNTURN = `[{"urls":"stun:stun-turn.webgame2d.com:3478"},{"urls":"turn:stun-turn.webgame2d.com:3478","username":"root","credential":"root"}]`
const CODEC_VP8 = "VP8"
const CODEC_H264 = "H264"

View file

@ -14,6 +14,7 @@ type Config struct {
EnableAspectRatio bool
Width int
Height int
Zone string
MonitoringConfig monitoring.ServerMonitoringConfig
}
@ -21,11 +22,12 @@ type Config struct {
func NewDefaultConfig() Config {
return Config{
Port: 8800,
OverlordAddress: "ws://localhost:8000/wso",
OverlordAddress: "localhost:8000",
Scale: 1,
EnableAspectRatio: false,
Width: 320,
Height: 240,
Zone: "",
MonitoringConfig: monitoring.ServerMonitoringConfig{
Port: 6601,
URLPrefix: "/worker",
@ -37,6 +39,7 @@ func NewDefaultConfig() Config {
func (c *Config) AddFlags(fs *pflag.FlagSet) *Config {
fs.IntVarP(&c.Port, "port", "", 8800, "OverWorker server port")
fs.StringVarP(&c.OverlordAddress, "overlordhost", "", c.OverlordAddress, "OverWorker URL to connect")
fs.StringVarP(&c.Zone, "zone", "z", c.Zone, "Zone of the worker")
fs.IntVarP(&c.Scale, "scale", "s", c.Scale, "Set output viewport scale factor")
fs.BoolVarP(&c.EnableAspectRatio, "ar", "", c.EnableAspectRatio, "Enable Aspect Ratio")

View file

@ -7,7 +7,6 @@ import (
"html/template"
"log"
"math"
"math/rand"
"net/http"
"strings"
@ -25,9 +24,9 @@ const (
type Server struct {
cfg Config
// roomToServer map roomID to workerID
roomToServer map[string]string
// workerClients are the map serverID to worker Client
// roomToWorker map roomID to workerID
roomToWorker map[string]string
// workerClients are the map workerID to worker Client
workerClients map[string]*WorkerClient
}
@ -40,7 +39,7 @@ func NewServer(cfg Config) *Server {
// Mapping serverID to client
workerClients: map[string]*WorkerClient{},
// Mapping roomID to server
roomToServer: map[string]string{},
roomToWorker: map[string]string{},
}
}
@ -80,8 +79,11 @@ func (o *Server) WSO(w http.ResponseWriter, r *http.Request) {
// Register to workersClients map the client connection
address := util.GetRemoteAddress(c)
fmt.Println("Is public", util.IsPublicIP(address))
fmt.Println("Mode: ", *config.Mode, config.ProdEnv)
// Zone of the worker
zone := r.URL.Query().Get("zone")
fmt.Printf("Is public: %v zone: %v\n", util.IsPublicIP(address), zone)
if !util.IsPublicIP(address) && *config.Mode == config.ProdEnv {
// Don't accept private IP for worker's address in prod mode
// However, if the worker in the same host with overlord, we can get public IP of worker
@ -93,7 +95,7 @@ func (o *Server) WSO(w http.ResponseWriter, r *http.Request) {
return
}
}
client := NewWorkerClient(c, serverID, address, fmt.Sprintf(config.StunTurnTemplate, address, address))
client := NewWorkerClient(c, serverID, address, fmt.Sprintf(config.StunTurnTemplate, address, address), zone)
o.workerClients[serverID] = client
defer o.cleanConnection(client, serverID)
@ -110,9 +112,9 @@ func (o *Server) WSO(w http.ResponseWriter, r *http.Request) {
client.Listen()
}
// WSO handles all connections from frontend to overlord
// WSO handles all connections from user/frontend to overlord
func (o *Server) WS(w http.ResponseWriter, r *http.Request) {
log.Println("Browser connected to overlord")
log.Println("A user connected to overlord ", r.URL)
defer func() {
if r := recover(); r != nil {
log.Println("Warn: Something wrong. Recovered in ", r)
@ -134,18 +136,28 @@ func (o *Server) WS(w http.ResponseWriter, r *http.Request) {
// get roomID if it is embeded in request. Server will pair the frontend with the server running the room. It only happens when we are trying to access a running room over share link.
// TODO: Update link to the wiki
roomID := r.URL.Query().Get("room_id")
// zone param is to pick worker in that zone only
// if there is no zone param, we can pic
userZone := r.URL.Query().Get("zone")
if roomID != "" {
log.Printf("Detected roomID %v from URL", roomID)
if workerID, ok := o.roomToServer[roomID]; ok {
if workerID, ok := o.roomToWorker[roomID]; ok {
workerClient = o.workerClients[workerID]
log.Printf("Found running server with id=%v client=%v", workerID, workerClient)
if userZone != "" && workerClient.Zone != userZone {
// if there is zone param, we need to ensure ther worker in that zone
// if not we consider the room is missing
workerClient = nil
} else {
log.Printf("Found running server with id=%v client=%v", workerID, workerClient)
}
}
}
// If there is no existing server to connect to, we find the best possible worker for the frontend
if workerClient == nil {
// Get best server for frontend to connect to
workerClient, err = o.getBestWorkerClient(client)
workerClient, err = o.getBestWorkerClient(client, userZone)
if err != nil {
return
}
@ -187,7 +199,7 @@ func (o *Server) WS(w http.ResponseWriter, r *http.Request) {
wssession.WorkerClient.IsAvailable = true
}
func (o *Server) getBestWorkerClient(client *BrowserClient) (*WorkerClient, error) {
func (o *Server) getBestWorkerClient(client *BrowserClient, zone string) (*WorkerClient, error) {
if o.cfg.DebugHost != "" {
log.Println("Connecting to debug host instead prod servers", o.cfg.DebugHost)
wc := o.getWorkerFromAddress(o.cfg.DebugHost)
@ -200,14 +212,7 @@ func (o *Server) getBestWorkerClient(client *BrowserClient) (*WorkerClient, erro
workerClients := o.getAvailableWorkers()
var serverID string
var err error
if config.MatchWorkerRandom {
serverID, err = findBestServerRandom(workerClients)
} else {
serverID, err = findBestServerFromBrowser(workerClients, client)
}
serverID, err := findBestServerFromBrowser(workerClients, client, zone)
if err != nil {
log.Println(err)
return nil, err
@ -239,27 +244,9 @@ func (o *Server) getWorkerFromAddress(address string) *WorkerClient {
return nil
}
// findBestServer returns the best server for a session
func findBestServerRandom(workerClients map[string]*WorkerClient) (string, error) {
// TODO: Find best Server by latency, currently return by ping
if len(workerClients) == 0 {
return "", errors.New("No server found")
}
r := rand.Intn(len(workerClients))
for k := range workerClients {
if r == 0 {
return k, nil
}
r--
}
return "", errors.New("No server found")
}
// findBestServerFromBrowser returns the best server for a session
// All workers addresses are sent to user and user will ping to get latency
func findBestServerFromBrowser(workerClients map[string]*WorkerClient, client *BrowserClient) (string, error) {
func findBestServerFromBrowser(workerClients map[string]*WorkerClient, client *BrowserClient, zone string) (string, error) {
// TODO: Find best Server by latency, currently return by ping
if len(workerClients) == 0 {
return "", errors.New("No server found")
@ -277,6 +264,11 @@ func findBestServerFromBrowser(workerClients map[string]*WorkerClient, client *B
// get the worker with lowest latency to user
for wc, l := range latencies {
if zone != "" && wc.Zone != zone {
// skip worker not in the zone if zone param is given
continue
}
if l < minLatency {
bestWorker = wc
minLatency = l
@ -328,9 +320,9 @@ func (o *Server) cleanConnection(client *WorkerClient, serverID string) {
// Remove serverID from servers
delete(o.workerClients, serverID)
// Clean all rooms connecting to that server
for roomID, roomServer := range o.roomToServer {
for roomID, roomServer := range o.roomToWorker {
if roomServer == serverID {
delete(o.roomToServer, roomID)
delete(o.roomToWorker, roomID)
}
}

View file

@ -13,6 +13,7 @@ type WorkerClient struct {
Address string
StunTurnServer string
IsAvailable bool
Zone string
}
// RouteWorker are all routes server received from worker
@ -21,8 +22,8 @@ func (o *Server) RouteWorker(workerClient *WorkerClient) {
// RoomID is global so it is managed by overlord.
workerClient.Receive("registerRoom", func(resp cws.WSPacket) cws.WSPacket {
log.Printf("Overlord: Received registerRoom room %s from worker %s", resp.Data, workerClient.ServerID)
o.roomToServer[resp.Data] = workerClient.ServerID
log.Printf("Overlord: Current room list is: %+v", o.roomToServer)
o.roomToWorker[resp.Data] = workerClient.ServerID
log.Printf("Overlord: Current room list is: %+v", o.roomToWorker)
return cws.WSPacket{
ID: "registerRoom",
@ -32,8 +33,8 @@ func (o *Server) RouteWorker(workerClient *WorkerClient) {
// closeRoom event from a worker, when worker close a room
workerClient.Receive("closeRoom", func(resp cws.WSPacket) cws.WSPacket {
log.Printf("Overlord: Received closeRoom room %s from worker %s", resp.Data, workerClient.ServerID)
delete(o.roomToServer, resp.Data)
log.Printf("Overlord: Current room list is: %+v", o.roomToServer)
delete(o.roomToWorker, resp.Data)
log.Printf("Overlord: Current room list is: %+v", o.roomToWorker)
return cws.WSPacket{
ID: "closeRoom",
@ -43,10 +44,10 @@ func (o *Server) RouteWorker(workerClient *WorkerClient) {
// getRoom returns the server ID based on requested roomID.
workerClient.Receive("getRoom", func(resp cws.WSPacket) cws.WSPacket {
log.Println("Overlord: Received a getroom request")
log.Println("Result: ", o.roomToServer[resp.Data])
log.Println("Result: ", o.roomToWorker[resp.Data])
return cws.WSPacket{
ID: "getRoom",
Data: o.roomToServer[resp.Data],
Data: o.roomToWorker[resp.Data],
}
})
@ -56,12 +57,13 @@ func (o *Server) RouteWorker(workerClient *WorkerClient) {
}
// NewWorkerClient returns a client connecting to worker. This connection exchanges information between workers and server
func NewWorkerClient(c *websocket.Conn, serverID string, address string, stunturn string) *WorkerClient {
func NewWorkerClient(c *websocket.Conn, serverID string, address string, stunturn string, zone string) *WorkerClient {
return &WorkerClient{
Client: cws.NewClient(c),
ServerID: serverID,
Address: address,
StunTurnServer: stunturn,
IsAvailable: true,
Zone: zone,
}
}

View file

@ -2,6 +2,7 @@ package worker
import (
"log"
"net/url"
"os"
"path"
"time"
@ -58,7 +59,7 @@ func NewHandler(cfg worker.Config) *Handler {
// Run starts a Handler running logic
func (h *Handler) Run() {
for {
oClient, err := setupOverlordConnection(h.overlordHost)
oClient, err := setupOverlordConnection(h.overlordHost, h.cfg.Zone)
if err != nil {
log.Println("Cannot connect to overlord. Retrying...")
time.Sleep(time.Second)
@ -74,8 +75,16 @@ func (h *Handler) Run() {
}
}
func setupOverlordConnection(ohost string) (*OverlordClient, error) {
conn, err := createOverlordConnection(ohost)
func setupOverlordConnection(ohost string, zone string) (*OverlordClient, error) {
overlordURL := url.URL{
Scheme: "ws",
Host: ohost,
Path: "/wso",
RawQuery: "zone=" + zone,
}
log.Println("Worker connecting to overlord:", overlordURL.String())
conn, err := createOverlordConnection(overlordURL.String())
if err != nil {
return nil, err
}

4
web/js/init.js vendored
View file

@ -7,7 +7,7 @@ $(document).ready(() => {
joystick.init();
touch.init();
const roomId = room.loadMaybe();
[roomId, zone] = room.loadMaybe();
// if from URL -> start game immediately!
socket.init(roomId);
socket.init(roomId, zone);
});

View file

@ -11,8 +11,10 @@ const socket = (() => {
let conn;
let curPacketId = '';
const init = (roomId) => {
conn = new WebSocket(`ws://${location.host}/ws${roomId ? `?room_id=${roomId}` : ''}`);
const init = (roomId, zone) => {
const paramString = new URLSearchParams({roomId: roomId, zone: zone})
conn = new WebSocket(`ws://${location.host}/ws?${paramString.toString()}`);
// Clear old roomID
conn.onopen = () => {

15
web/js/room.js vendored
View file

@ -11,7 +11,11 @@ const room = (() => {
// !to rewrite
const parseURLForRoom = () => {
let queryDict = {};
let regex = /^\/?([A-Za-z]*)\/?/g;
var zone = regex.exec(location.pathname)[1]
var room = null
// get room from URL
location.search.substr(1)
.split('&')
.forEach((item) => {
@ -19,10 +23,10 @@ const room = (() => {
});
if (typeof queryDict.id === 'string') {
return decodeURIComponent(queryDict.id);
room = decodeURIComponent(queryDict.id);
}
return null;
return [room, zone];
};
event.sub(GAME_ROOM_AVAILABLE, data => {
@ -50,12 +54,15 @@ const room = (() => {
//roomID = loadRoomID();
// Shared URL second
const parsedId = parseURLForRoom();
const [parsedId, czone] = parseURLForRoom();
if (parsedId !== null) {
id = parsedId;
}
if (czone !== null) {
zone = czone;
}
return id;
return [id, zone];
},
copyToClipboard: () => {
const el = document.createElement('textarea');