mirror of
https://github.com/giongto35/cloud-game.git
synced 2026-01-23 18:46:11 +00:00
* Allow HTTP access to Raspberry Pi over local network Lower audio buffer maximum theoretical size to get the worker code to compile on Raspberry Pi * Add https port flag to run https worker and coordinator on the same machine Add https chain and key flags to allow to use an existing certificate and bypass letsencrypt Note the ping address resolution is still broken with this configuration * Add option to define a ping server in the coordinator This is useful when it is not predicatable what address and port the worker will be runnning at This only works when there is a single worker * Free temporarily allocated CStrings Store constant CString * Only load core once and unload it when done * Add Nintendo 64 support! Disclaimer: only tested with Mupen64plus and Mupen64plusNext on Raspberry Pi. It probably needs more work to run on every system and with other OpenGL libretro libraries. Input controls are hacked together, it really needs analog stick and remapping support to play in a nicer way. I am worried there might be a memory leak when unloading Mupen64plus but this needs further investigation. * Add analog sticks + R2,L2,R3,L3 support * Add client logic to control left analog stick via keyboard and touch Add client logic to toggle between dpad mode and analog mode (even for joystick) Might need to revisit if and when remapping is implemented Tocuh sensitivity of analog stick is pretty high, might need tweaking * Add cores for Raspberry Pi Add N64 core for linux x86_64 * Reset use OpenGL flag on nanoarch shutdown (line lost in refactoring)
449 lines
15 KiB
JavaScript
Vendored
449 lines
15 KiB
JavaScript
Vendored
/**
|
|
* App controller module.
|
|
* @version 1
|
|
*/
|
|
/* const controller = */
|
|
(() => {
|
|
// current app state
|
|
let state;
|
|
|
|
// flags
|
|
// first user interaction
|
|
// used for mute/unmute
|
|
let interacted = false;
|
|
|
|
const DIR = (() => {
|
|
return {
|
|
IDLE: 'idle',
|
|
UP: 'up',
|
|
DOWN: 'down',
|
|
}
|
|
})();
|
|
let prevDir = DIR.IDLE;
|
|
|
|
// UI elements
|
|
// use $element[0] for DOM element
|
|
const gameScreen = $('#game-screen');
|
|
const menuScreen = $('#menu-screen');
|
|
const helpOverlay = $('#help-overlay');
|
|
const popupBox = $('#noti-box');
|
|
// keymap
|
|
const keyButtons = {};
|
|
Object.keys(KEY).forEach(button => {
|
|
keyButtons[KEY[button]] = $(`#btn-${KEY[button]}`);
|
|
});
|
|
|
|
const setState = (newState) => {
|
|
log.debug(`[control] [s] ${state ? state.name : '???'} -> ${newState.name}`);
|
|
state = newState;
|
|
};
|
|
|
|
const onGameRoomAvailable = () => {
|
|
//keyButtons[KEY.JOIN].html('share');
|
|
popup('Now you can share you game!');
|
|
};
|
|
|
|
const onConnectionReady = () => {
|
|
// start a game right away or show the menu
|
|
if (room.getId()) {
|
|
startGame();
|
|
} else {
|
|
state.menuReady();
|
|
}
|
|
};
|
|
|
|
const onLatencyCheckRequest = (data) => {
|
|
popup('Ping check...');
|
|
const timeoutMs = 2000;
|
|
// TODO: why we use maximum timeout
|
|
const maxTimeoutMs = timeoutMs > ajax.defaultTimeoutMs() ? timeoutMs : ajax.defaultTimeoutMs();
|
|
|
|
Promise.all((data.addresses || []).map(address => {
|
|
let beforeTime = Date.now();
|
|
return ajax.fetch(`${address}?_=${beforeTime}`, {method: "GET", redirect: "follow"}, timeoutMs)
|
|
.then(() => ({[address]: Date.now() - beforeTime}), () => ({[address]: maxTimeoutMs}));
|
|
})).then(results => {
|
|
// const latencies = Object.assign({}, ...results);
|
|
const latencies = {};
|
|
results.map(latency => Object.keys(latency).forEach(address => latencies[address] = latency[address]));
|
|
log.info('[ping] <->', latencies);
|
|
socket.latency(latencies, data.packetId);
|
|
});
|
|
};
|
|
|
|
const helpScreen = {
|
|
// don't call $ if holding the button
|
|
shown: false,
|
|
// undo the state when release the button
|
|
prevState: null,
|
|
// use function () if you need "this"
|
|
show: function (show, event) {
|
|
if (this.shown === show) return;
|
|
|
|
// hack
|
|
if (state === app.state.game || this.prevState === app.state.game) {
|
|
gameScreen.toggle(!show);
|
|
} else {
|
|
keyButtons[KEY.SAVE].toggle(show);
|
|
keyButtons[KEY.LOAD].toggle(show);
|
|
menuScreen.toggle(!show);
|
|
}
|
|
helpOverlay.toggle(show);
|
|
|
|
this.shown = show;
|
|
|
|
if (show) {
|
|
this.prevState = state;
|
|
setState(app.state.help);
|
|
} else {
|
|
setState(this.prevState);
|
|
}
|
|
|
|
if (event) event.pub(HELP_OVERLAY_TOGGLED, {shown: show});
|
|
}
|
|
};
|
|
|
|
const showMenuScreen = () => {
|
|
// clear scenes
|
|
gameScreen.hide();
|
|
menuScreen.hide();
|
|
gameList.hide();
|
|
keyButtons[KEY.SAVE].hide();
|
|
keyButtons[KEY.LOAD].hide();
|
|
//keyButtons[KEY.JOIN].html('play');
|
|
|
|
// show menu scene
|
|
gameScreen.show().delay(0).fadeOut(0, () => {
|
|
log.debug('[control] loading menu screen');
|
|
menuScreen.fadeIn(0, () => {
|
|
gameList.show();
|
|
setState(app.state.menu);
|
|
});
|
|
});
|
|
};
|
|
|
|
const startGame = () => {
|
|
if (!rtcp.isConnected()) {
|
|
popup('Game cannot load. Please refresh');
|
|
return;
|
|
}
|
|
|
|
if (!rtcp.isInputReady()) {
|
|
popup('Game is not ready yet. Please wait');
|
|
return;
|
|
}
|
|
|
|
//const el = document.createElement('textarea');
|
|
const playeridx = parseInt($('#playeridx').val(), 10) - 1
|
|
|
|
log.info('[control] starting game screen');
|
|
|
|
setState(app.state.game);
|
|
|
|
const promise = gameScreen[0].play();
|
|
if (promise !== undefined) {
|
|
promise.then(() => log.info('Media can autoplay'))
|
|
.catch(error => {
|
|
// Usually error happens when we autoplay unmuted video, browser requires manual play.
|
|
// We already muted video and use separate audio encoding so it's fine now
|
|
log.error('Media Failed to autoplay');
|
|
log.error(error)
|
|
// TODO: Consider workaround
|
|
});
|
|
}
|
|
|
|
// TODO get current game from the URL and not from the list?
|
|
// if we are opening a share link it will send the default game name to the server
|
|
// currently it's a game with the index 1
|
|
// on the server this game is ignored and the actual game will be extracted from the share link
|
|
// so there's no point in doing this and this' really confusing
|
|
socket.startGame(gameList.getCurrentGame(), env.isMobileDevice(), room.getId(), playeridx);
|
|
|
|
// clear menu screen
|
|
input.poll().disable();
|
|
menuScreen.hide();
|
|
gameScreen.show();
|
|
keyButtons[KEY.SAVE].show();
|
|
keyButtons[KEY.LOAD].show();
|
|
// end clear
|
|
input.poll().enable();
|
|
};
|
|
|
|
// !to add debounce
|
|
const popup = (msg) => {
|
|
popupBox.html(msg);
|
|
popupBox.fadeIn().delay(0).fadeOut();
|
|
};
|
|
|
|
const onKeyPress = (data) => {
|
|
if (data.key == "up" || data.key == "down" || data.key == "left" || data.key == "right") {
|
|
keyButtons[data.key].addClass('dpad-pressed');
|
|
} else {
|
|
keyButtons[data.key].addClass('pressed');
|
|
}
|
|
|
|
if (KEY.HELP === data.key) {
|
|
helpScreen.show(true, event);
|
|
}
|
|
|
|
state.keyPress(data.key);
|
|
};
|
|
|
|
const onKeyRelease = (data) => {
|
|
if (data.key == "up" || data.key == "down" || data.key == "left" || data.key == "right") {
|
|
keyButtons[data.key].removeClass('dpad-pressed');
|
|
} else {
|
|
keyButtons[data.key].removeClass('pressed');
|
|
}
|
|
|
|
if (KEY.HELP === data.key) {
|
|
helpScreen.show(false, event);
|
|
}
|
|
|
|
// maybe move it somewhere
|
|
if (!interacted) {
|
|
// unmute when there is user interaction
|
|
gameScreen[0].muted = false;
|
|
interacted = true;
|
|
}
|
|
|
|
state.keyRelease(data.key);
|
|
};
|
|
|
|
const onAxisChanged = (data) => {
|
|
// maybe move it somewhere
|
|
if (!interacted) {
|
|
// unmute when there is user interaction
|
|
gameScreen[0].muted = false;
|
|
interacted = true;
|
|
}
|
|
|
|
state.axisChanged(data.id, data.value);
|
|
};
|
|
|
|
const updatePlayerIndex = (idx) => {
|
|
var slider = document.getElementById('playeridx');
|
|
slider.value = idx + 1;
|
|
socket.updatePlayerIndex(idx);
|
|
};
|
|
|
|
const handleToggle = () => {
|
|
var toggle = document.getElementById('dpad-toggle');
|
|
toggle.checked = !toggle.checked;
|
|
event.pub(DPAD_TOGGLE, {checked: toggle.checked});
|
|
};
|
|
|
|
const app = {
|
|
state: {
|
|
eden: {
|
|
name: 'eden',
|
|
axisChanged: () => {
|
|
},
|
|
keyPress: () => {
|
|
},
|
|
keyRelease: () => {
|
|
},
|
|
menuReady: () => {
|
|
showMenuScreen()
|
|
}
|
|
},
|
|
|
|
help: {
|
|
name: 'help',
|
|
axisChanged: () => {
|
|
},
|
|
keyPress: () => {
|
|
},
|
|
keyRelease: () => {
|
|
},
|
|
menuReady: () => {
|
|
// show silently
|
|
gameScreen.hide();
|
|
menuScreen.hide();
|
|
gameList.hide();
|
|
//keyButtons[KEY.JOIN].html('play');
|
|
|
|
gameList.show();
|
|
|
|
helpScreen.prevState = app.state.menu;
|
|
}
|
|
},
|
|
|
|
menu: {
|
|
name: 'menu',
|
|
axisChanged: (id, value) => {
|
|
if (id === 1) { // Left Stick, Y Axis
|
|
let dir = DIR.IDLE;
|
|
if (value < -0.5) dir = DIR.UP;
|
|
if (value > 0.5) dir = DIR.DOWN;
|
|
if (dir !== prevDir) {
|
|
prevDir = dir;
|
|
switch (dir) {
|
|
case DIR.IDLE:
|
|
gameList.stopGamePickerTimer();
|
|
break;
|
|
case DIR.UP:
|
|
gameList.startGamePickerTimer(true);
|
|
break;
|
|
case DIR.DOWN:
|
|
gameList.startGamePickerTimer(false);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
keyPress: (key) => {
|
|
switch (key) {
|
|
case KEY.UP:
|
|
case KEY.DOWN:
|
|
gameList.startGamePickerTimer(key === KEY.UP);
|
|
break;
|
|
}
|
|
},
|
|
keyRelease: (key) => {
|
|
switch (key) {
|
|
case KEY.UP:
|
|
case KEY.DOWN:
|
|
gameList.stopGamePickerTimer();
|
|
break;
|
|
case KEY.JOIN:
|
|
case KEY.A:
|
|
case KEY.B:
|
|
case KEY.X:
|
|
case KEY.Y:
|
|
case KEY.START:
|
|
case KEY.SELECT:
|
|
startGame();
|
|
break;
|
|
case KEY.QUIT:
|
|
popup('You are already in menu screen!');
|
|
break;
|
|
case KEY.LOAD:
|
|
popup('Lets play to load game!');
|
|
break;
|
|
case KEY.SAVE:
|
|
popup('Lets play to save game!');
|
|
break;
|
|
case KEY.STATS:
|
|
event.pub(STATS_TOGGLE);
|
|
break;
|
|
case KEY.DTOGGLE:
|
|
handleToggle();
|
|
break;
|
|
}
|
|
},
|
|
menuReady: () => {
|
|
}
|
|
},
|
|
|
|
game: {
|
|
name: 'game',
|
|
axisChanged: (id, value) => {
|
|
input.setAxisChanged(id, value);
|
|
},
|
|
keyPress: (key) => {
|
|
input.setKeyState(key, true);
|
|
},
|
|
keyRelease: function (key) {
|
|
input.setKeyState(key, false);
|
|
|
|
switch (key) {
|
|
// nani? why join / copy switch, it's confusing. Me: It's because of the original design to update label only :-s.
|
|
case KEY.JOIN: // or SHARE
|
|
// save when click share
|
|
event.pub(KEY_PRESSED, {key: KEY.SAVE})
|
|
room.copyToClipboard();
|
|
popup('Copy link to clipboard!');
|
|
break;
|
|
case KEY.SAVE:
|
|
socket.saveGame();
|
|
break;
|
|
case KEY.LOAD:
|
|
socket.loadGame();
|
|
break;
|
|
case KEY.FULL:
|
|
env.display().toggleFullscreen(gameScreen.height() !== window.innerHeight, gameScreen[0]);
|
|
break;
|
|
|
|
// update player index
|
|
case KEY.PAD1:
|
|
updatePlayerIndex(0);
|
|
break;
|
|
case KEY.PAD2:
|
|
updatePlayerIndex(1);
|
|
break;
|
|
case KEY.PAD3:
|
|
updatePlayerIndex(2);
|
|
break;
|
|
case KEY.PAD4:
|
|
updatePlayerIndex(3);
|
|
break;
|
|
|
|
// quit
|
|
case KEY.QUIT:
|
|
input.poll().disable();
|
|
|
|
// TODO: Stop game
|
|
socket.quitGame(room.getId());
|
|
room.reset();
|
|
|
|
popup('Quit!');
|
|
|
|
window.location = window.location.pathname;
|
|
break;
|
|
|
|
case KEY.STATS:
|
|
event.pub(STATS_TOGGLE);
|
|
break;
|
|
case KEY.DTOGGLE:
|
|
handleToggle();
|
|
break;
|
|
}
|
|
|
|
},
|
|
menuReady: () => {
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// subscriptions
|
|
event.sub(GAME_ROOM_AVAILABLE, onGameRoomAvailable, 2);
|
|
event.sub(GAME_SAVED, () => popup('Saved'));
|
|
event.sub(GAME_LOADED, () => popup('Loaded'));
|
|
event.sub(GAME_PLAYER_IDX, (idx) => popup(parseInt(idx)+1));
|
|
|
|
event.sub(MEDIA_STREAM_INITIALIZED, (data) => {
|
|
rtcp.start(data.stunturn);
|
|
gameList.set(data.games);
|
|
});
|
|
event.sub(MEDIA_STREAM_SDP_AVAILABLE, (data) => rtcp.setRemoteDescription(data.sdp, gameScreen[0]));
|
|
event.sub(MEDIA_STREAM_CANDIDATE_ADD, (data) => rtcp.addCandidate(data.candidate));
|
|
event.sub(MEDIA_STREAM_CANDIDATE_FLUSH, () => rtcp.flushCandidate());
|
|
event.sub(MEDIA_STREAM_READY, () => rtcp.start());
|
|
event.sub(CONNECTION_READY, onConnectionReady);
|
|
event.sub(CONNECTION_CLOSED, () => input.poll().disable());
|
|
event.sub(LATENCY_CHECK_REQUESTED, onLatencyCheckRequest);
|
|
event.sub(GAMEPAD_CONNECTED, () => popup('Gamepad connected'));
|
|
event.sub(GAMEPAD_DISCONNECTED, () => popup('Gamepad disconnected'));
|
|
// touch stuff
|
|
event.sub(MENU_HANDLER_ATTACHED, (data) => {
|
|
menuScreen.on(data.event, data.handler);
|
|
});
|
|
event.sub(KEY_PRESSED, onKeyPress);
|
|
event.sub(KEY_RELEASED, onKeyRelease);
|
|
event.sub(AXIS_CHANGED, onAxisChanged);
|
|
event.sub(CONTROLLER_UPDATED, data => rtcp.input(data));
|
|
|
|
// game screen stuff
|
|
gameScreen.on('loadstart', () => {
|
|
gameScreen[0].volume = 0.5;
|
|
gameScreen[0].poster = '/static/img/screen_loading.gif';
|
|
});
|
|
gameScreen.on('canplay', () => {
|
|
gameScreen[0].poster = '';
|
|
});
|
|
|
|
// initial app state
|
|
setState(app.state.eden);
|
|
})($, document, event, env, gameList, input, KEY, log, room, stats);
|