Add Room logics

This commit is contained in:
giongto35 2019-04-07 18:54:34 +08:00
commit 7b0738afc6
23 changed files with 7595 additions and 22 deletions

48
main.go
View file

@ -1,6 +1,7 @@
package main
import (
"html/template"
"math/rand"
"os"
"strconv"
@ -18,7 +19,6 @@ import (
"github.com/giongto35/cloud-game/util"
"github.com/giongto35/cloud-game/webrtc"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"encoding/json"
@ -27,7 +27,12 @@ import (
// var webRTC *webrtc.WebRTC
var width = 256
var height = 240
var index string = "./index_http.html"
var indexFN string = "./static/gameboy.html"
var service string = "http"
type IndexPageData struct {
Service string
}
var upgrader = websocket.Upgrader{}
@ -37,6 +42,12 @@ type WSPacket struct {
RoomID string `json:"room_id"`
}
type SessionPacket struct {
Game string `json:"game"`
RoomID string `json:"room_id"`
SDP string `json:"sdp"`
}
type Room struct {
imageChannel chan *image.RGBA
inputChannel chan int
@ -57,30 +68,35 @@ func main() {
fmt.Printf("Usage: %s [ws]\n", os.Args[0])
fmt.Println("http://localhost:8000")
rooms = map[string]*Room{}
if len(os.Args) > 1 {
service = "ws"
log.Println("Using websocket")
index = "./index_ws.html"
// ignore origin
upgrader.CheckOrigin = func(r *http.Request) bool { return true }
http.HandleFunc("/", getWeb)
http.HandleFunc("/ws", ws)
http.ListenAndServe(":8000", nil)
} else {
log.Println("Using http")
router := mux.NewRouter()
router.HandleFunc("/", getWeb).Methods("GET")
router.HandleFunc("/session", postSession).Methods("POST")
http.ListenAndServe(":8000", router)
http.HandleFunc("/session", postSession)
}
http.HandleFunc("/", getWeb)
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))
http.ListenAndServe(":8000", nil)
}
func getWeb(w http.ResponseWriter, r *http.Request) {
bs, err := ioutil.ReadFile(index)
if err != nil {
log.Fatal(err)
tmpl := template.Must(template.ParseFiles(indexFN))
data := IndexPageData{
Service: service,
}
w.Write(bs)
tmpl.Execute(w, data)
// bs, err := ioutil.ReadFile(index)
// if err != nil {
// log.Fatal(err)
// }
// w.Write(bs)
}
// initRoom initilize room returns roomID
@ -195,12 +211,6 @@ func ws(w http.ResponseWriter, r *http.Request) {
}
}
type SessionPacket struct {
Game string `json:"game"`
RoomID string `json:"room_id"`
SDP string `json:"sdp"`
}
func postSession(w http.ResponseWriter, r *http.Request) {
bs, err := ioutil.ReadAll(r.Body)
if err != nil {

6922
static/css/gameboy.css Normal file

File diff suppressed because it is too large Load diff

Binary file not shown.

161
static/gameboy.html Normal file
View file

@ -0,0 +1,161 @@
<head>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous">
<link type="text/css" rel="stylesheet" href="/static/css/gameboy.css">
</head>
<body>
<div id="gameboy" class="green">
<div id="canvas"></div>
<div id="border"></div>
<div id="border-top"></div>
<div id="border-left"></div>
<div id="border-bottom"></div>
<div id="border-right"></div>
<div id="screw-small-right" class="screw small"></div>
<div id="screw-small-left" class="screw small"></div>
<div id="screw-large-right" class="screw large"></div>
<div id="screw-large-left" class="screw large"></div>
<div id="backboard"></div>
<div id="motherboard"></div>
<div id="motherboard-capacitors" class="capacitors"></div>
<div id="chip-short" class="chip"></div>
<div id="chip-diagonal" class="chip"></div>
<div id="chip-tall" class="chip"></div>
<div id="chip-capacitors" class="capacitors"></div>
<div id="contrast-knob"></div>
<div id="link-port"></div>
<div id="circuit-bottom" class="circuit"></div>
<div id="circuit-top" class="circuit"></div>
<div id="transistors"></div>
<div id="processor"></div>
<div id="component"></div>
<div id="controller"></div>
<div id="speaker"></div>
<div id="whitescreen"></div>
<div id="glass"></div>
<div id="glass-gameboy-text">GAME BOY</div>
<div id="glass-color-text">C</div>
<div id="screen">
<video id="loading-screen" autoplay=true poster="https://orig00.deviantart.net/cdcd/f/2017/276/a/a/october_2nd___gameboy_poltergeist_by_wanyo-dbpdmnd.gif">
</video>
<div id="menu-screen">
<img id="box-art" src="/static/img/boxarts/Contra (USA).png">
<div id="title">
<p>Contra</p>
</div>
<img class="arrow left" src="/static/img/arrow2_e.gif">
<img class="arrow right" src="/static/img/arrow2_e.gif">
</div>
</div>
<div id="screen-gameboy-text">GAME BOY</div>
<div id="screen-nintendo-text">Nintendo</div>
<div id="joystick-pad"></div>
<div id="joystick">.</div>
<div id="control"></div>
<div id="control-b" class="control-button">B</div>
<div id="control-a" class="control-button">A</div>
<div id="start-select-box"></div>
<div id="start-select-button"></div>
<div id="cover-vertical"></div>
<div id="cover-horizontal"></div>
<div id="gloss"></div>
<div id="speaker-holes"></div>
<div id="power"></div>
</div>
<div id="colors">
<div class="color" data-color="red"></div>
<div class="color" data-color="purple"></div>
<div class="color" data-color="green"></div>
<div class="color" data-color="yellow"></div>
<div class="color" data-color="teal"></div>
<div class="color" data-color="transparent"></div><br><br>
<div>Animation powered by bchanx <a href="http://bchanx.com/animated-gameboy-in-css-blog"><i class="fas fa-blog"></i></a> <a href="https://github.com/bchanx/animated-gameboy-in-css"><i class="fab fa-github"></i></a></div><br>
<div>Rocked by <a href="https://github.com/giongto35">giongto35</a> <i class="far fa-handshake"></i> <a href="https://trich.im">trichimtrich</a></div>
</div>
<div style="position:absolute; top:30%; left: 5%;">
<h3>Instruction</h3>
<div>
Use Up, Down, Left, Right to Move <br />
Z to jump (A) <br />
X to sprint (B) <br />
C is start (in game) <br />
V is select game <br />
Q is super quit <br />
Room ID: <input type="text" id="roomID"><br>
<!--Fullscreen media for better gaming experience<br /-->
</div><br>
<h3>Log:</h3>
<pre id="div"></pre>
<div>
<i class="fas fa-gamepad"></i> <u><i>Refresh to retry when checking is too long</i></u>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="/static/js/const.js"></script>
<script>
/*
GLOBAL VARS
*/
// Game state
screenState = "loader";
gameIdx = 0;
// Input vars
keyState = {
// controllers
a: false,
b: false,
start: false,
select: false,
// navigators
up: false,
down: false,
left: false,
right: false,
// unofficial
quit: false
}
stateUnchange = true;
unchangePacket = INPUT_STATE_PACKET;
inputTimer = null;
// Connection vars
var pc, inputChannel;
var localSessionDescription = "";
var remoteSessionDescription = "";
var conn;
/*
FUNCTIONS
*/
// miscs
function log(msg) {
if (DEBUG) {
document.getElementById('div').innerHTML += msg + '<br>'
console.log(msg);
}
}
</script>
<script src="/static/js/gameboy_loader.js"></script>
<script src="/static/js/gameboy_controller.js"></script>
<script src="/static/js/{{.Service}}.js"></script>
</body>

BIN
static/img/arrow2_e.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

0
static/index.html Normal file
View file

View file

@ -109,9 +109,7 @@ window.startGame = () => {
// webrtc
pc = new RTCPeerConnection({iceServers: [{urls: 'stun:stun.l.google.com:19302'}]})
// input channel
let r = Math.random().toString(36).substring(7)
console.log("data chan name", r)
inputChannel = pc.createDataChannel(r)
inputChannel = pc.createDataChannel('foo')
inputChannel.onclose = () => {
log('inputChannel has closed');
}
@ -173,6 +171,10 @@ var localSessionDescription = "";
var remoteSessionDescription = "";
var conn;
// Input handler
keyState = {
// controllers

100
static/js/const.js Normal file
View file

@ -0,0 +1,100 @@
// miscs
DEBUG = true;
// list game
GAME_LIST = [
{
name: "Contra",
nes: "Contra.nes",
art: "Contra (USA).png"
},
{
name: "Kirby's Adventure",
nes: "Kirby's Adventure.nes",
art: "Kirby's Adventure (Canada).png"
},
{
name: "Mega Man 2",
nes: "Mega Man 2.nes",
art: "Mega Man 2 (USA).png"
},
{
name: "Metal Gear",
nes: "Metal Gear.nes",
art: "Metal Gear (USA).png"
},
{
name: "Mortal Kombat 4",
nes: "Mortal Kombat 4.nes",
art: "mortal-kombat-x-box-art-revealed-as-pre-orders-ope_5pxd.jpg"
},
{
name: "Super Mario Bros 2",
nes: "Super Mario Bros 2.nes",
art: "Super Mario Bros. 2 (USA).png"
},
{
name: "Super Mario Bros 3",
nes: "Super Mario Bros 3.nes",
art: "Super Mario Bros. 3 (USA).png"
},
{
name: "Super Mario Bros",
nes: "Super Mario Bros.nes",
art: "Super Mario Bros. (World).png"
},
{
name: "TMNT 3",
nes: "Teenage Mutant Ninja Turtles 3.nes",
art: "Teenage Mutant Ninja Turtles III - The Manhattan Project (USA).png"
},
{
name: "Zelda",
nes: "zelda.nes",
art: "Zelda II - The Adventure of Link (Europe) (Rev A).png"
}
];
// Keyboard stuffs
KEY_MAP = {
37: "left",
38: "up",
39: "right",
40: "down",
90: "a", // z
88: "b", // x
67: "start", // c
86: "select", // v
81: "quit" // q
}
/*
const (
ButtonA = iota
ButtonB
ButtonSelect
ButtonStart
ButtonUp
ButtonDown
ButtonLeft
ButtonRight
)
*/
KEY_BIT = ["a", "b", "select", "start", "up", "down", "left", "right"];
INPUT_FPS = 100;
INPUT_STATE_PACKET = 5;

View file

@ -0,0 +1,135 @@
// menu screen
function showMenuScreen() {
log("Clean up connection / frame");
// clean up before / after menu
try {
inputChannel.close();
} catch (err) {
log(`> [Warning] input channel: ${err}`);
}
try {
pc.close();
} catch (err) {
log(`> [Warning] peer connection: ${err}`);
}
try {
conn.close();
} catch (err) {
log(`> [Warning] Websocket connection: ${err}`);
}
$("#loading-screen").hide();
$("#menu-screen").hide();
// show
$("#loading-screen").show().delay(1000).fadeOut(400, () => {
log("Loading menu screen");
$("#menu-screen").fadeIn(400, () => {
chooseGame(7);
screenState = "menu";
});
});
}
// game menu
function chooseGame(idx) {
if (idx < 0 || idx == gameIdx || idx >= GAME_LIST.length) return false;
$("#menu-screen #box-art").fadeOut(400, function () {
$(this).attr("src", `/static/img/boxarts/${GAME_LIST[idx].art}`);
$(this).fadeIn(400, function () {
$("#menu-screen #title p").html(GAME_LIST[idx].name);
});
});
if (idx == 0) {
$("#menu-screen .left").hide();
} else {
$("#menu-screen .left").show();
}
if (idx == GAME_LIST.length - 1) {
$("#menu-screen .right").hide();
} else {
$("#menu-screen .right").show();
}
gameIdx = idx;
log(`> [Pick] game ${gameIdx + 1}/${GAME_LIST.length} - ${GAME_LIST[gameIdx].name}`);
}
function setState(e, bo) {
if (e.keyCode in KEY_MAP) {
keyState[KEY_MAP[e.keyCode]] = bo;
stateUnchange = false;
unchangePacket = INPUT_STATE_PACKET;
}
}
document.body.onkeyup = function (e) {
if (screenState === "menu") {
switch (KEY_MAP[e.keyCode]) {
case "left":
chooseGame(gameIdx - 1);
break;
case "right":
chooseGame(gameIdx + 1);
break;
case "select":
startGame();
}
} else if (screenState === "game") {
setState(e, false);
}
// global reset
if (KEY_MAP[e.keyCode] == "quit") {
endInput();
showMenuScreen();
}
}
document.body.onkeydown = function (e) {
if (screenState === "game")
setState(e, true);
};
function sendInput() {
// prepare key
if (stateUnchange || unchangePacket > 0) {
st = "";
KEY_BIT.forEach(elem => {
st += keyState[elem] ? 1 : 0;
});
ss = parseInt(st, 2);
console.log(`Key state string: ${st} ==> ${ss}`);
// send
inputChannel.send(ss);
stateUnchange = false;
unchangePacket--;
}
}
function startInput() {
if (inputTimer == null) {
inputTimer = setInterval(sendInput, 1000 / INPUT_FPS)
}
}
function endInput() {
clearInterval(inputTimer);
inputTimer = null;
}

View file

@ -0,0 +1,35 @@
// A tiny bit of javascript code for color selection
window.onload = function() {
var colors = ['red', 'purple', 'green', 'yellow', 'teal', 'transparent'];
var last = null;
Array.prototype.slice.call(document.querySelectorAll('.color')).forEach(function(el) {
el.addEventListener('click', function() {
log("Starting gameboy");
screenState = "loader";
if (last) {
last.classList.remove('active');
}
var color = el.getAttribute('data-color');
var gameboy = document.querySelector('#gameboy');
gameboy.style.opacity = 0;
gameboy.classList.remove(gameboy.classList[0]);
var clone = gameboy.cloneNode(true);
gameboy.remove();
clone.classList.add(color);
clone.style.opacity = 1;
var colors = document.querySelector('#colors');
colors.parentNode.insertBefore(clone, colors);
el.classList.add('active');
last = el;
});
});
document.querySelector('.color[data-color="green"]').dispatchEvent(new MouseEvent('click', {
'view': window,
'bubbles': true
}));
$("#screen-gameboy-text").on("webkitAnimationEnd", showMenuScreen);
$("#screen-gameboy-text").on("animationend", showMenuScreen);
}

94
static/js/http.js Normal file
View file

@ -0,0 +1,94 @@
// http signal server
function startGame() {
log("Starting game screen")
// clear
endInput();
document.getElementById('div').innerHTML = "";
$("#loading-screen").show();
$("#menu-screen").fadeOut();
// end clear
// Register with server the session description
function postSession(session) {
if (session == "") {
return;
}
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200) {
remoteSessionDescription = this.responseText;
// document.getElementById('playGame').disabled = false;
// by original design, we would click to start.
startSession();
}
};
xhttp.open("POST", "/session", true);
xhttp.setRequestHeader("Content-type", "application/json");
xhttp.send(JSON.stringify({ "game": GAME_LIST[gameIdx].nes, "sdp": session }));
}
let pc = new RTCPeerConnection({
iceServers: [
{
urls: 'stun:stun.l.google.com:19302'
}
]
})
inputChannel = pc.createDataChannel('foo')
inputChannel.onclose = () => log('inputChannel has closed')
inputChannel.onopen = () => log('inputChannel has opened')
inputChannel.onmessage = e => log(`Message from DataChannel '${inputChannel.label}' payload '${e.data}'`)
pc.ontrack = function (event) {
log("New stream, yay!");
document.getElementById("loading-screen").srcObject = event.streams[0];
// var el = document.createElement(event.track.kind)
// el.srcObject = event.streams[0]
// el.autoplay = true
// el.width = 800;
// el.height = 600;
// el.poster = new URL("https://orig00.deviantart.net/cdcd/f/2017/276/a/a/october_2nd___gameboy_poltergeist_by_wanyo-dbpdmnd.gif");
// document.getElementById('remoteVideos').appendChild(el)
}
pc.onicecandidate = event => {
if (event.candidate === null) {
var session = btoa(JSON.stringify(pc.localDescription));
localSessionDescription = session;
postSession(session)
}
}
pc.createOffer({ offerToReceiveVideo: true, offerToReceiveAudio: true }).then(d => pc.setLocalDescription(d)).catch(log)
window.startSession = () => {
let sd = remoteSessionDescription
if (sd === '') {
return alert('Session Description must not be empty')
}
try {
pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(atob(sd))));
} catch (e) {
alert(e);
}
}
pc.oniceconnectionstatechange = e => {
log(`iceConnectionState: ${pc.iceConnectionState}`);
if (pc.iceConnectionState === "connected") {
startInput();
screenState = "game";
}
else if (pc.iceConnectionState === "disconnected") {
endInput();
}
}
}

114
static/js/ws.js Normal file
View file

@ -0,0 +1,114 @@
// web socket
function startGame() {
log("Starting game screen");
// clear
endInput();
document.getElementById('div').innerHTML = "";
$("#loading-screen").show();
$("#menu-screen").fadeOut();
// end clear
conn = new WebSocket(`ws://${location.host}/ws`);
conn.onopen = () => {
log("WebSocket is opened. Send ping");
roomID = document.getElementById('roomID').value
conn.send(JSON.stringify({"id": "ping", "data": GAME_LIST[gameIdx].nes, "room_id": roomID}));
}
conn.onerror = error => {
log(`Websocket error: ${error}`);
}
conn.onclose = () => {
log("Websocket closed");
}
conn.onmessage = e => {
d = JSON.parse(e.data);
switch (d["id"]) {
case "pong":
log("Recv pong. Start webrtc");
startWebRTC();
break;
case "sdp":
log("Got remote sdp");
pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(atob(d["data"]))));
break;
case "start":
log("Got start");
document.getElementById('roomID').value = d["room_id"]
break;
}
}
// webrtc
pc = new RTCPeerConnection({iceServers: [{urls: 'stun:stun.l.google.com:19302'}]})
// input channel
inputChannel = pc.createDataChannel('foo')
inputChannel.onclose = () => {
log('inputChannel has closed');
}
inputChannel.onopen = () => {
log('inputChannel has opened');
}
inputChannel.onmessage = e => {
log(`Message from DataChannel '${inputChannel.label}' payload '${e.data}'`);
}
pc.oniceconnectionstatechange = e => {
log(`iceConnectionState: ${pc.iceConnectionState}`);
if (pc.iceConnectionState === "connected") {
conn.send(JSON.stringify({"id": "start", "data": ""}));
startInput();
screenState = "game";
}
else if (pc.iceConnectionState === "disconnected") {
endInput();
}
}
// stream channel
pc.ontrack = function (event) {
log("New stream, yay!");
document.getElementById("loading-screen").srcObject = event.streams[0];
// document.getElementById("loading-screen").width = 270;
// document.getElementById("loading-screen").height = 240;
}
// candidate packet from STUN
pc.onicecandidate = event => {
if (event.candidate === null) {
} else {
console.log(JSON.stringify(event.candidate));
// conn.send(JSON.stringify({"id": "candidate", "data": JSON.stringify(event.candidate)}));
}
}
function startWebRTC() {
// receiver only tracks
pc.addTransceiver('video', {'direction': 'recvonly'});
// create SDP
pc.createOffer({offerToReceiveVideo: true, offerToReceiveAudio: false}).then(d => {
pc.setLocalDescription(d, () => {
// send to ws
session = btoa(JSON.stringify(pc.localDescription));
localSessionDescription = session;
log("Send SDP to remote peer");
conn.send(JSON.stringify({"id": "sdp", "data": session}));
});
}).catch(log);
}
}