backend is nu in typescript met de classes georganiseerd de manier van synchronisatie is nu gebaseerd op het synchroniseren van de tijd van de server en client, zodat er minder communicatie nodig is vanuit de servermaster
parent
b05ee951d5
commit
18aa4fa369
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,160 +0,0 @@ |
||||
const User = require("./User.js"); |
||||
const timeline = require("./timeline.js"); |
||||
|
||||
module.exports = class Lobby { |
||||
/** |
||||
* @type {User[]} |
||||
*/ |
||||
users = []; |
||||
/** |
||||
* @type {string|undefined} |
||||
*/ |
||||
leaderId = undefined; |
||||
|
||||
running = false; |
||||
startTime = 0; |
||||
currentSeconds = 0; |
||||
timelineIndex = 0; |
||||
|
||||
// For debugging purposes
|
||||
speedFactor = 1; |
||||
|
||||
constructor(name) { |
||||
this.name = name; |
||||
} |
||||
|
||||
run(io) { |
||||
this.running = true; |
||||
this.startTime = Date.now(); |
||||
|
||||
const doTick = () => { |
||||
if (this.users.length === 0) { |
||||
// this lobby is over.
|
||||
return; |
||||
} |
||||
|
||||
const timestamp = timeline.getIndex(this.timelineIndex); |
||||
const nextShot = timeline.getNextShot(this.timelineIndex); |
||||
|
||||
if (!timestamp) { |
||||
// We are done.
|
||||
io.to(this.name + "").emit('tick_event', { |
||||
current: this.currentSeconds |
||||
}); |
||||
console.log("Done"); |
||||
this.running = false; |
||||
return; |
||||
} |
||||
|
||||
console.log("ticking", this.currentSeconds); |
||||
|
||||
io.to(this.name + "").emit('tick_event', { |
||||
current: this.currentSeconds, |
||||
next: timestamp, |
||||
nextShot: nextShot |
||||
}); |
||||
|
||||
if (this.currentSeconds >= timestamp.timestamp) { |
||||
this.timelineIndex += 1; |
||||
} |
||||
|
||||
this.currentSeconds += 1; |
||||
// We spend some time processing, wait a bit less than 1000ms
|
||||
const nextTickTime = this.startTime + (1000 * this.currentSeconds / this.speedFactor); |
||||
const waitTime = nextTickTime - Date.now(); |
||||
console.log("waiting", waitTime); |
||||
setTimeout(doTick, Math.floor(waitTime / this.speedFactor)); |
||||
}; |
||||
|
||||
doTick(); |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* @param io |
||||
* @param {number} time |
||||
*/ |
||||
seek(io, time) { |
||||
this.currentSeconds = time; |
||||
this.startTime = Date.now() - time * 1000; |
||||
this.timelineIndex = timeline.indexForTime(this.currentSeconds); |
||||
io.to(this.name + "").emit('seek', time); |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* @returns {boolean} |
||||
*/ |
||||
hasUsers() { |
||||
return this.users.length !== 0; |
||||
} |
||||
|
||||
|
||||
setRandomLeader() { |
||||
if (this.hasUsers()) { |
||||
this.leaderId = this.users[0].id; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* @param {User} user |
||||
*/ |
||||
addUser(user) { |
||||
this.users.push(user); |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* @param id |
||||
* @returns {User|undefined} |
||||
*/ |
||||
getUser(id) { |
||||
return this.users.find(u => u.id === id); |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* @param {string} id |
||||
*/ |
||||
removeUser(id) { |
||||
this.users = this.users.filter(u => u.id !== id); |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* @returns {boolean} |
||||
*/ |
||||
hasLeader() { |
||||
return !!this.leaderId; |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* @param {string} id |
||||
* @returns {boolean} |
||||
*/ |
||||
isLeader(id) { |
||||
return this.leaderId === id; |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* @param {string} id |
||||
*/ |
||||
setLeader(id) { |
||||
if (!this.getUser(id)) { |
||||
throw new Error('user_not_in_lobby'); |
||||
} |
||||
|
||||
this.leaderId = id; |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* @returns {User|undefined} |
||||
*/ |
||||
getLeader() { |
||||
return this.users.find(u => u.id === this.leaderId) |
||||
} |
||||
}; |
@ -0,0 +1,186 @@ |
||||
import {Socket} from "socket.io"; |
||||
|
||||
import User from "./User"; |
||||
import {getIndex, getNextShot, getTimeline, indexForTime} from "./timeline"; |
||||
import {getCurrentTime} from "./util"; |
||||
|
||||
export default class Room { |
||||
id: number = 0; |
||||
users: User[] = []; |
||||
leader: User | null = null; |
||||
|
||||
running = false; |
||||
startTime = 0; |
||||
currentSeconds = 0; |
||||
timelineIndex: number = 0; |
||||
|
||||
timelineName: string = 'Centurion'; |
||||
|
||||
// For debugging purposes
|
||||
speedFactor = 1; |
||||
|
||||
constructor(name: number) { |
||||
this.id = name; |
||||
} |
||||
|
||||
serialize(user: User) { |
||||
return { |
||||
'id': this.id, |
||||
'userCount': this.users.length, |
||||
'isLeader': this.leader == user, |
||||
'running': this.running, |
||||
'startTime': this.startTime, |
||||
'timelineName': this.timelineName, |
||||
} |
||||
} |
||||
|
||||
serializeTimeline(user: User) { |
||||
return getTimeline(this.timelineName); |
||||
} |
||||
|
||||
sync() { |
||||
this.users.forEach(u => u.sync()); |
||||
} |
||||
|
||||
join(user: User) { |
||||
this.users.push(user); |
||||
user.setRoom(this); |
||||
|
||||
if (!this.hasLeader()) { |
||||
this.setLeader(user); |
||||
} |
||||
|
||||
this.sync(); |
||||
} |
||||
|
||||
leave(user: User) { |
||||
this.users.splice(this.users.indexOf(user), 1); |
||||
user.setRoom(null); |
||||
|
||||
if (this.leader == user) { |
||||
this.setRandomLeader(); |
||||
} |
||||
|
||||
this.sync(); |
||||
} |
||||
|
||||
onBeforeDelete() { |
||||
} |
||||
|
||||
start() { |
||||
this.running = true; |
||||
this.startTime = getCurrentTime() - 1400 * 1000 |
||||
|
||||
this.sync(); |
||||
} |
||||
|
||||
run(io: Socket) { |
||||
this.running = true; |
||||
this.startTime = Date.now(); |
||||
|
||||
// io.to(this.id.toString()).emit('timeline', {
|
||||
// 'timeline': {
|
||||
// }
|
||||
// });
|
||||
|
||||
const doTick = () => { |
||||
if (this.users.length === 0) { |
||||
// this room is over.
|
||||
return; |
||||
} |
||||
|
||||
const timestamp = getIndex(this.timelineIndex); |
||||
const nextShot = getNextShot(this.timelineIndex); |
||||
|
||||
if (!timestamp) { |
||||
// We are done.
|
||||
io.to(this.id.toString()).emit('tick_event', { |
||||
tick: { |
||||
current: this.currentSeconds |
||||
} |
||||
}); |
||||
console.log("Done"); |
||||
this.running = false; |
||||
return; |
||||
} |
||||
|
||||
console.log("ticking", this.currentSeconds); |
||||
|
||||
io.to(this.id.toString()).emit('tick_event', { |
||||
tick: { |
||||
current: this.currentSeconds, |
||||
next: timestamp, |
||||
nextShot: nextShot |
||||
} |
||||
}); |
||||
|
||||
if (this.currentSeconds >= timestamp.timestamp) { |
||||
this.timelineIndex += 1; |
||||
} |
||||
|
||||
this.currentSeconds += 1; |
||||
// We spend some time processing, wait a bit less than 1000ms
|
||||
const nextTickTime = this.startTime + (1000 * this.currentSeconds / this.speedFactor); |
||||
const waitTime = nextTickTime - Date.now(); |
||||
console.log("waiting", waitTime); |
||||
setTimeout(doTick, Math.floor(waitTime / this.speedFactor)); |
||||
}; |
||||
|
||||
doTick(); |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* @param io |
||||
* @param {number} time |
||||
*/ |
||||
seek(io: Socket, time: number) { |
||||
this.currentSeconds = time; |
||||
this.startTime = Date.now() - time * 1000; |
||||
this.timelineIndex = indexForTime(this.currentSeconds); |
||||
io.to(this.id.toString()).emit('seek', time); |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* @returns {boolean} |
||||
*/ |
||||
hasUsers() { |
||||
return this.users.length !== 0; |
||||
} |
||||
|
||||
setRandomLeader() { |
||||
if (this.hasUsers()) { |
||||
this.leader = this.users[0]; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* @param id |
||||
* @returns {User|undefined} |
||||
*/ |
||||
getUser(id: string) { |
||||
return this.users.find(u => u.id === id); |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* @param {string} id |
||||
*/ |
||||
removeUser(id: string) { |
||||
this.users = this.users.filter(u => u.id !== id); |
||||
} |
||||
|
||||
hasLeader(): boolean { |
||||
return this.leader != null; |
||||
} |
||||
|
||||
setLeader(user: User) { |
||||
this.leader = user; |
||||
} |
||||
|
||||
getLeader(): User | null { |
||||
return this.leader; |
||||
} |
||||
}; |
@ -0,0 +1,120 @@ |
||||
import {Socket} from "socket.io"; |
||||
|
||||
import User from './User' |
||||
import Room from './Room' |
||||
import {getCurrentTime, randomInt} from "./util"; |
||||
|
||||
export default class Service { |
||||
private roomIdToRooms = new Map<number, Room>(); |
||||
private socketsToUsers = new Map<string, User>(); |
||||
|
||||
onSocketConnect(socket: Socket) { |
||||
let user = new User(socket); |
||||
this.socketsToUsers.set(socket.id, user); |
||||
} |
||||
|
||||
onSocketDisconnect(socket: Socket) { |
||||
let user = this.getUser(socket); |
||||
|
||||
if (user.room != null) { |
||||
user.room.leave(user); |
||||
} |
||||
|
||||
this.deleteEmptyRooms(); |
||||
|
||||
user.onDisconnect(); |
||||
} |
||||
|
||||
onTimeSync(socket: Socket, requestId: number, clientTime: number) { |
||||
let user = this.getUser(socket); |
||||
|
||||
let now = getCurrentTime(); |
||||
user.emit('time_sync', { |
||||
'requestId': requestId, |
||||
'clientDiff': now - clientTime, |
||||
'serverTime': now |
||||
}) |
||||
} |
||||
|
||||
onRequestStart(socket: Socket) { |
||||
let user = this.getUser(socket); |
||||
|
||||
if (user.room?.getLeader() == user) { |
||||
user.room!!.start(); |
||||
} |
||||
} |
||||
|
||||
onRequestJoin(socket: Socket, roomId: number): boolean { |
||||
let user = this.getUser(socket); |
||||
if (!this.roomIdToRooms.has(roomId)) return false; |
||||
if (user.room && user.room.id == roomId) return false; |
||||
|
||||
if (user.room) { |
||||
user.room.leave(user); |
||||
this.deleteEmptyRooms(); |
||||
} |
||||
|
||||
let room = this.roomIdToRooms.get(roomId)!!; |
||||
room.join(user); |
||||
return true; |
||||
} |
||||
|
||||
onRequestJoinRandom(socket: Socket) { |
||||
let user = this.getUser(socket); |
||||
|
||||
if (user.room) { |
||||
user.room.leave(user); |
||||
this.deleteEmptyRooms(); |
||||
} |
||||
|
||||
const room = this.createRandomRoom(); |
||||
if (!room) throw Error('Too many rooms active'); |
||||
room.join(user); |
||||
} |
||||
|
||||
hasRoomId(roomId: number): boolean { |
||||
return this.roomIdToRooms.has(roomId); |
||||
} |
||||
|
||||
private getUser(socket: Socket): User { |
||||
let user = this.socketsToUsers.get(socket.id); |
||||
if (!user) { |
||||
throw new Error('User not found'); |
||||
} |
||||
return user; |
||||
} |
||||
|
||||
private deleteEmptyRooms() { |
||||
for (let room of this.roomIdToRooms.values()) { |
||||
if (room.users.length == 0) { |
||||
this.deleteRoom(room); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private createRandomRoom(): Room | null { |
||||
let tries = 0; |
||||
while (tries++ < 1000) { |
||||
const randomId = randomInt(100, Math.max(1000, this.roomIdToRooms.size * 2)); |
||||
if (this.roomIdToRooms.has(randomId)) continue; |
||||
|
||||
return this.createRoomWithId(randomId); |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
private createRoomWithId(roomId: number): Room { |
||||
if (this.roomIdToRooms.has(roomId)) { |
||||
throw new Error('A room with the given id already exists'); |
||||
} |
||||
|
||||
let room = new Room(roomId); |
||||
this.roomIdToRooms.set(roomId, room); |
||||
return room; |
||||
} |
||||
|
||||
private deleteRoom(room: Room) { |
||||
this.roomIdToRooms.get(room.id)!!.onBeforeDelete(); |
||||
this.roomIdToRooms.delete(room.id) |
||||
} |
||||
} |
@ -1,59 +0,0 @@ |
||||
const Lobby = require("./Lobby.js"); |
||||
const {getRandomInt} = require("./util.js"); |
||||
|
||||
class State { |
||||
/** |
||||
* @type {Object.<string, Lobby>} |
||||
*/ |
||||
lobbies = {}; |
||||
|
||||
constructor() { |
||||
} |
||||
|
||||
/** |
||||
* @returns {Lobby} |
||||
*/ |
||||
createRandomLobby() { |
||||
let lobby = undefined; |
||||
|
||||
while (!lobby) { |
||||
const lobbyCount = Object.keys(this.lobbies).length; |
||||
const id = getRandomInt(100, Math.max(1000, lobbyCount * 2)); |
||||
lobby = this.createLobby(id); |
||||
} |
||||
|
||||
return lobby; |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* @param lobbyId |
||||
* @returns {Lobby|undefined} |
||||
*/ |
||||
getLobby(lobbyId) { |
||||
if (!lobbyId || !this.lobbies.hasOwnProperty(lobbyId)) { |
||||
return undefined; |
||||
} |
||||
|
||||
return this.lobbies[lobbyId]; |
||||
} |
||||
|
||||
/** |
||||
* Returns undefined when the lobby already exists. |
||||
* @param {number} lobbyId |
||||
* @returns {Lobby|undefined} |
||||
*/ |
||||
createLobby(lobbyId) { |
||||
if (this.lobbies.hasOwnProperty(lobbyId)) { |
||||
return undefined; |
||||
} |
||||
|
||||
return new Lobby(lobbyId); |
||||
} |
||||
|
||||
removeLobby(lobbyId) { |
||||
delete this.lobbies[lobbyId]; |
||||
} |
||||
} |
||||
|
||||
module.exports = new State(); |
@ -1,5 +0,0 @@ |
||||
module.exports = class User { |
||||
constructor(id) { |
||||
this.id = id; |
||||
} |
||||
}; |
@ -0,0 +1,77 @@ |
||||
import {Socket} from "socket.io"; |
||||
|
||||
import Room from "./Room"; |
||||
|
||||
export default class User { |
||||
socket: Socket; |
||||
id: string; |
||||
|
||||
room: Room | null = null; |
||||
|
||||
constructor(socket: Socket) { |
||||
this.socket = socket; |
||||
this.id = socket.id; |
||||
} |
||||
|
||||
onDisconnect() { |
||||
if (this.room != null) { |
||||
|
||||
} |
||||
} |
||||
|
||||
setRoom(room: Room | null) { |
||||
if (this.room === room) return; |
||||
|
||||
if (this.room != null) { |
||||
this.socket.leave(this.room.id.toString()); |
||||
} |
||||
|
||||
this.room = room; |
||||
|
||||
if (this.room != null) { |
||||
this.socket.join(this.room.id.toString()); |
||||
} |
||||
|
||||
this.sync(); |
||||
} |
||||
|
||||
sentRoom: any = null; |
||||
sentTimelineName: string | null = null; |
||||
|
||||
sync() { |
||||
if (!this.shallowEquals(this.sentRoom, this.room?.serialize(this))) { |
||||
this.sentRoom = this.room?.serialize(this); |
||||
this.emit('room', { |
||||
'room': this.sentRoom |
||||
}) |
||||
} |
||||
|
||||
if (!this.shallowEquals(this.sentTimelineName, this.room?.timelineName)) { |
||||
this.sentTimelineName = this.room?.timelineName || null; |
||||
this.emit('timeline', { |
||||
'timeline': this.sentTimelineName == null ? null : this.room!!.serializeTimeline(this) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
emit(eventName: string, obj: any) { |
||||
this.socket.emit(eventName, obj); |
||||
} |
||||
|
||||
shallowEquals(obj1: any, obj2: any) { |
||||
if (obj1 === null && obj2 === null) |
||||
return true; |
||||
|
||||
if ((obj1 === null && obj2 !== null) || (obj1 !== null && obj2 === null)) |
||||
return false; |
||||
|
||||
if (typeof (obj1) != typeof (obj2)) |
||||
return false; |
||||
|
||||
if (Object.keys(obj1).length !== Object.keys(obj2).length) return false |
||||
|
||||
return Object.keys(obj1).every(key => |
||||
obj2.hasOwnProperty(key) && obj1[key] === obj2[key] |
||||
); |
||||
} |
||||
} |
@ -1,22 +0,0 @@ |
||||
const express = require("express"); |
||||
const socketIO = require("socket.io"); |
||||
const state = require("./State.js"); |
||||
const path = require("path"); |
||||
|
||||
const HOST = '0.0.0.0'; |
||||
const PORT = 3001; |
||||
|
||||
const app = express(); |
||||
const server = app.listen(PORT, HOST,() => console.log(`Centurion listening on port ${PORT}!`)); |
||||
app.use(express.static(path.join(__dirname, '../public'))); |
||||
|
||||
const io = socketIO(server); |
||||
|
||||
app.get('/state', (req, res) => res.send('<pre>' + JSON.stringify(state) + '</pre>')); |
||||
|
||||
process.on('SIGINT', () => process.exit()); |
||||
process.on('SIGTERM', () => process.exit()); |
||||
|
||||
module.exports = { |
||||
app, server, io |
||||
}; |
@ -1,80 +0,0 @@ |
||||
const service = require("./service.js"); |
||||
const state = require("./State.js"); |
||||
|
||||
const {io} = require('./app.js'); |
||||
|
||||
io.on('connection', socket => { |
||||
const socketId = socket.id; |
||||
|
||||
console.log('a user connected', socketId); |
||||
|
||||
const lobby = state.createRandomLobby(); |
||||
service.joinLobby(socketId, lobby.name); |
||||
socket.join(lobby.name); |
||||
socket.emit('welcome', {lobby: lobby}); |
||||
|
||||
socket.on('disconnect', (reason) => { |
||||
console.log('Disconnected:', socketId); |
||||
service.leaveLobby(socketId); |
||||
}); |
||||
|
||||
socket.on('join_lobby', (lobbyId, callback) => { |
||||
if (!callback || typeof callback !== 'function') { |
||||
console.error("Join: Callback not a function."); |
||||
return |
||||
} |
||||
|
||||
if (!lobbyId) { |
||||
return callback('no_lobby_id_given'); |
||||
} |
||||
|
||||
if (!Number.isSafeInteger(+lobbyId)) { |
||||
return callback('lobby_id_not_integer'); |
||||
} |
||||
|
||||
console.log(`${socketId} wants to join '${lobbyId}'.`); |
||||
|
||||
// Leave current lobby first
|
||||
service.leaveLobby(socketId); |
||||
const lobby = service.joinLobby(socketId, lobbyId); |
||||
|
||||
socket.join(lobby.name); |
||||
|
||||
callback(null, { |
||||
status: 'ok', |
||||
lobby: lobby |
||||
}); |
||||
}); |
||||
|
||||
socket.on('lobby_info', callback => { |
||||
if (!callback || typeof callback !== 'function') { |
||||
console.error("Lobby info: Callback not a function."); |
||||
return |
||||
} |
||||
|
||||
const lobby = service.getUserLobby(socketId); |
||||
|
||||
callback(null, { |
||||
status: 'ok', |
||||
lobby: lobby |
||||
}); |
||||
}); |
||||
|
||||
socket.on('request_start', (time = null) => { |
||||
console.log('request start', socket.rooms); |
||||
|
||||
const lobby = service.getUserLobby(socketId); |
||||
if (!lobby.isLeader(socketId)) { |
||||
console.warn("Non leader tried to start."); |
||||
return; |
||||
} |
||||
|
||||
io.to(lobby.name + "").emit('started'); |
||||
lobby.run(io); |
||||
|
||||
if (typeof time === 'number' && time) { |
||||
console.log("Starting at", time); |
||||
lobby.seek(io, time); |
||||
} |
||||
}); |
||||
}); |
@ -0,0 +1,175 @@ |
||||
import express from "express"; |
||||
import SocketIO, {Socket} from "socket.io"; |
||||
import path from "path"; |
||||
|
||||
import Service from './Service' |
||||
|
||||
// process.on('SIGINT', () => process.exit());
|
||||
// process.on('SIGTERM', () => process.exit());
|
||||
|
||||
const HOST = '0.0.0.0'; |
||||
const PORT = 3001; |
||||
|
||||
const app = express(); |
||||
const server = app.listen(PORT, HOST, () => console.log(`Centurion listening on port ${PORT}!`)); |
||||
app.use(express.static(path.join(__dirname, '../public'))); |
||||
|
||||
const io = SocketIO(server); |
||||
|
||||
const service = new Service(); |
||||
|
||||
io.on('connection', socket => { |
||||
socket.on('disconnect', (reason) => { |
||||
service.onSocketDisconnect(socket); |
||||
}); |
||||
|
||||
socket.on('ping', () => { |
||||
socket.emit('pong'); |
||||
}) |
||||
|
||||
socket.on('time_sync', (requestId: number, clientTime: number) => { |
||||
if (!Number.isSafeInteger(requestId)) return; |
||||
if (!Number.isSafeInteger(clientTime)) return; |
||||
|
||||
service.onTimeSync(socket, requestId, clientTime); |
||||
}) |
||||
|
||||
socket.on('request_start', () => { |
||||
service.onRequestStart(socket); |
||||
}); |
||||
|
||||
socket.on('request_join', (roomId: number) => { |
||||
if (!Number.isSafeInteger(roomId)) return; |
||||
|
||||
service.onRequestJoin(socket, roomId); |
||||
}); |
||||
|
||||
socket.on('request_join_random', () => { |
||||
service.onRequestJoinRandom(socket); |
||||
}) |
||||
|
||||
socket.on('call', (id: number, name: string, params: any) => { |
||||
if (!Number.isSafeInteger(id)) return; |
||||
// noinspection SuspiciousTypeOfGuard
|
||||
if (!name || typeof (name) !== 'string') return; |
||||
// if (!params) return;
|
||||
|
||||
let call = new Call(socket, id, name, params); |
||||
|
||||
if (name == 'room_exists') { |
||||
let roomId = params && params['roomId']; |
||||
if (!Number.isSafeInteger(roomId)) { |
||||
call.error('Invalid room id'); |
||||
return; |
||||
} |
||||
|
||||
call.respond(service.hasRoomId(roomId)); |
||||
return; |
||||
} |
||||
|
||||
if (name == 'request_join') { |
||||
let roomId = params && params['roomId']; |
||||
if (!Number.isSafeInteger(roomId)) { |
||||
call.error('Invalid room id'); |
||||
return; |
||||
} |
||||
if (!service.hasRoomId(roomId)) { |
||||
call.respond(false); |
||||
return; |
||||
} |
||||
if (service.onRequestJoin(socket, roomId)) { |
||||
call.respond(true); |
||||
} else { |
||||
call.respond(false); |
||||
} |
||||
} |
||||
}) |
||||
|
||||
service.onSocketConnect(socket); |
||||
|
||||
/*socket.on('join_room', (roomId, callback) => { |
||||
if (!callback || typeof callback !== 'function') { |
||||
console.error("Join: Callback not a function."); |
||||
return |
||||
} |
||||
|
||||
if (!roomId) { |
||||
return callback('no_room_id_given'); |
||||
} |
||||
|
||||
if (!Number.isSafeInteger(+roomId)) { |
||||
return callback('room_id_not_integer'); |
||||
} |
||||
|
||||
console.log(`${socketId} wants to join '${roomId}'.`); |
||||
|
||||
// Leave current room first
|
||||
|
||||
let currentRoom = service.getUserRoom(socketId); |
||||
if (currentRoom) { |
||||
socket.leave(currentRoom.name); |
||||
service.leaveRoom(socketId); |
||||
} |
||||
|
||||
const room = service.joinRoom(socketId, roomId); |
||||
|
||||
socket.join(room.name); |
||||
sendRoom(socket, room); |
||||
}); |
||||
|
||||
socket.on('room_info', callback => { |
||||
if (!callback || typeof callback !== 'function') { |
||||
console.error("Room info: Callback not a function."); |
||||
return |
||||
} |
||||
|
||||
const room = service.getUserRoom(socketId); |
||||
sendRoom(socket, room); |
||||
}); |
||||
|
||||
socket.on('request_start', (time = null) => { |
||||
console.log('request start', socket.rooms); |
||||
|
||||
const room = service.getUserRoom(socketId); |
||||
if (!room.isLeader(socketId)) { |
||||
console.warn("Non leader tried to start."); |
||||
return; |
||||
} |
||||
|
||||
room.run(io); |
||||
sendRoom(io.to(room.name.toString()), room); |
||||
|
||||
if (typeof time === 'number' && time) { |
||||
console.log("Starting at", time); |
||||
room.seek(io, time); |
||||
} |
||||
});*/ |
||||
}); |
||||
|
||||
class Call { |
||||
private socket: Socket; |
||||
private id: number; |
||||
private name: string; |
||||
private params: any; |
||||
|
||||
constructor(socket: Socket, id: number, name: string, params: any) { |
||||
this.socket = socket; |
||||
this.id = id; |
||||
this.name = name; |
||||
this.params = params; |
||||
} |
||||
|
||||
error(reason: string) { |
||||
this.socket.emit('call_response', { |
||||
'id': this.id, |
||||
'error': reason |
||||
}) |
||||
} |
||||
|
||||
respond(data: any) { |
||||
this.socket.emit('call_response', { |
||||
'id': this.id, |
||||
'response': data |
||||
}); |
||||
} |
||||
} |
@ -1,68 +0,0 @@ |
||||
const User = require("./User.js"); |
||||
const state = require("./State.js"); |
||||
|
||||
/** |
||||
* |
||||
* @param {string} socketId |
||||
* @param {number} lobbyId |
||||
* @returns {Lobby} |
||||
*/ |
||||
function joinLobby(socketId, lobbyId) { |
||||
let lobby = state.getLobby(lobbyId); |
||||
if (!lobby) { |
||||
lobby = state.createLobby(lobbyId); |
||||
} |
||||
|
||||
lobby.addUser(new User(socketId)); |
||||
|
||||
if (!lobby.hasLeader()) { |
||||
lobby.setLeader(socketId); |
||||
} |
||||
|
||||
state.lobbies[lobby.name] = lobby; |
||||
return lobby; |
||||
} |
||||
|
||||
function leaveLobby(socketId) { |
||||
Object.keys(state.lobbies).forEach(lobbyId => { |
||||
const lobby = state.getLobby(lobbyId); |
||||
|
||||
if (!lobby) { |
||||
return; |
||||
} |
||||
|
||||
lobby.removeUser(socketId); |
||||
|
||||
if (!lobby.hasUsers()) { |
||||
state.removeLobby(lobbyId); |
||||
return; |
||||
} |
||||
|
||||
if (lobby.getLeader() === socketId) { |
||||
lobby.setRandomLeader(); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* |
||||
* @param socketId |
||||
* @returns {Lobby|undefined} |
||||
*/ |
||||
function getUserLobby(socketId) { |
||||
for (let lobbyId of Object.keys(state.lobbies)) { |
||||
const lobby = state.getLobby(lobbyId); |
||||
|
||||
if (!lobby) { |
||||
continue; |
||||
} |
||||
|
||||
if (lobby.getUser(socketId)) { |
||||
return lobby; |
||||
} |
||||
} |
||||
|
||||
return undefined; |
||||
} |
||||
|
||||
module.exports = {joinLobby, leaveLobby, getUserLobby}; |
@ -1,15 +0,0 @@ |
||||
/** |
||||
* Generates random int |
||||
* @param {number} min, inclusive |
||||
* @param {number} max, exclusive |
||||
* @returns {number} |
||||
*/ |
||||
function getRandomInt(min, max) { |
||||
min = Math.ceil(min); |
||||
max = Math.floor(max); |
||||
return Math.floor(Math.random() * (max - min)) + min; |
||||
} |
||||
|
||||
module.exports = { |
||||
getRandomInt |
||||
}; |
@ -0,0 +1,20 @@ |
||||
/** |
||||
* Generates random int |
||||
* @param {number} min, inclusive |
||||
* @param {number} max, exclusive |
||||
* @returns {number} |
||||
*/ |
||||
export function randomInt(min: number, max: number): number { |
||||
min = Math.ceil(min); |
||||
max = Math.floor(max); |
||||
return Math.floor(Math.random() * (max - min)) + min; |
||||
} |
||||
|
||||
let _randomTimeOffsetForDebug = randomInt(-10000, 10000); |
||||
_randomTimeOffsetForDebug = 0; |
||||
|
||||
console.log('random time offset', _randomTimeOffsetForDebug); |
||||
|
||||
export function getCurrentTime() { |
||||
return Date.now() + _randomTimeOffsetForDebug; |
||||
} |
@ -0,0 +1,8 @@ |
||||
{ |
||||
"compilerOptions": { |
||||
"target": "es6", |
||||
"module": "commonjs", |
||||
"strict": true, |
||||
"esModuleInterop": true |
||||
} |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,32 @@ |
||||
import {roomTime, useRoom, useRoomRunningChanged, useRoomTime, useTick} from "../lib/Connection"; |
||||
import {useRef} from "react"; |
||||
|
||||
|
||||
const Player = () => { |
||||
const roomRunning = useRoomRunningChanged(); |
||||
const _ = useRoomTime() |
||||
console.log('PLAYER RENDER', roomTime) |
||||
|
||||
const player = useRef(new Audio("centurion.m4a")); |
||||
|
||||
if (roomRunning?.running) { |
||||
let targetTime = roomTime() / 1000; |
||||
let diff = player.current.currentTime - targetTime; |
||||
console.log('PLAYER DIFF', diff); |
||||
if (Math.abs(diff) > 0.1) { |
||||
player.current.currentTime = targetTime; |
||||
} |
||||
|
||||
if (player.current.paused) { |
||||
player.current.play().catch(e => { |
||||
console.error('Error playing', e); |
||||
}) |
||||
} |
||||
} else { |
||||
player.current.pause(); |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
export default Player; |
@ -1,17 +1,59 @@ |
||||
#centurion-title |
||||
font-size: 3.5rem |
||||
text-align: center |
||||
min-height: inherit |
||||
.lobby |
||||
.centurion-title |
||||
text-align: center |
||||
|
||||
.hints |
||||
font-size: 2rem |
||||
text-align: center |
||||
font-size: 3.0rem |
||||
min-height: inherit |
||||
|
||||
.text |
||||
padding: 0 2rem |
||||
|
||||
.control |
||||
font-size: 1.3rem |
||||
margin-top: 5em |
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24) |
||||
.beer |
||||
animation-fill-mode: forwards |
||||
|
||||
.beer-flipped |
||||
transform: scaleX(-1) |
||||
&.connecting |
||||
animation: spin 0.4s ease-in-out 25 |
||||
|
||||
@keyframes spin |
||||
100% |
||||
transform: rotate(-360deg) |
||||
|
||||
.beer-flipped |
||||
transform: scaleX(-1) |
||||
display: inline-block |
||||
|
||||
.lobby-connecting |
||||
margin: 2em 0 0 0 |
||||
text-align: center |
||||
|
||||
.hints |
||||
margin: 1rem 0 0 0 |
||||
font-size: 2rem |
||||
text-align: center |
||||
|
||||
.control |
||||
font-size: 1.3rem |
||||
margin: 2em 1em 2em 1em |
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24) |
||||
|
||||
.lobby-options-or |
||||
display: inline-block |
||||
position: relative |
||||
|
||||
margin: 16px 0 |
||||
color: #888 |
||||
|
||||
&:before, &:after |
||||
content: '' |
||||
width: 1px |
||||
height: 6px |
||||
position: absolute |
||||
left: 50% |
||||
margin-left: 0.1px |
||||
background: #bbb |
||||
|
||||
&:before |
||||
bottom: 100% |
||||
|
||||
&:after |
||||
top: 100% |
||||
|
@ -0,0 +1,321 @@ |
||||
import io from "socket.io-client"; |
||||
import {useEffect, useState} from "react"; |
||||
import {parse as parseQueryString, stringify as stringifyQueryString} from 'query-string'; |
||||
|
||||
import {Room, Tick, Timeline} from "../types/types"; |
||||
import {Sub, useSub} from "../util/sub"; |
||||
|
||||
class Connection { |
||||
url = '/'; |
||||
|
||||
socket: SocketIOClient.Socket; |
||||
|
||||
room = new Sub<Room | null>(); |
||||
tick = new Sub<Tick | null>(); |
||||
timeline = new Sub<Timeline | null>(); |
||||
|
||||
timeSyncIntervals = [500, 1000, 3000, 5000, 10000, 30000]; |
||||
timeSyncs: { [requestId: number]: TimeSyncRequest } = {}; |
||||
timeSyncTimeoutIds: number[] = []; |
||||
timeSyncTooOld = 120000; |
||||
roomTime = new Sub<number>(); |
||||
|
||||
calls: { [id: number]: Call } = {}; |
||||
|
||||
constructor() { |
||||
this.socket = io(this.url, { |
||||
autoConnect: false |
||||
}); |
||||
|
||||
this.setupSocketListeners(); |
||||
|
||||
this.connect(); |
||||
|
||||
this.roomTime.set(0); |
||||
} |
||||
|
||||
connect() { |
||||
this.socket.connect(); |
||||
} |
||||
|
||||
setupSocketListeners() { |
||||
this.socket.on('connect', () => { |
||||
this.onConnect(); |
||||
}) |
||||
|
||||
this.socket.on('disconnect', () => { |
||||
this.onDisconnect(); |
||||
}) |
||||
|
||||
this.socket.on('time_sync', (data: any) => { |
||||
this.timeSyncResponse(data.requestId, data.clientDiff, data.serverTime); |
||||
}) |
||||
|
||||
this.socket.on('room', (data: any) => { |
||||
console.log('ROOM', data.room); |
||||
|
||||
if (data.room) { |
||||
this.setQueryLobbyId(data.room.id); |
||||
} |
||||
|
||||
this.room.set(data.room); |
||||
}); |
||||
|
||||
this.socket.on('tick_event', (data: any) => { |
||||
this.tick.set(data.tick); |
||||
}); |
||||
|
||||
this.socket.on('timeline', (data: any) => { |
||||
if (data.timeline) { |
||||
this.timeline.set(new Timeline(data.timeline)); |
||||
} else { |
||||
this.timeline.set(null); |
||||
} |
||||
}); |
||||
|
||||
this.socket.on('call_response', (data: any) => { |
||||
let call = this.calls[data.id]; |
||||
if (!call) return; |
||||
|
||||
if (data.error) { |
||||
call.callback(data.error, null); |
||||
} else { |
||||
call.callback(null, data.response); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
onConnect() { |
||||
this.startTimeSync(); |
||||
|
||||
let lobbyId = this.getQueryLobbyId(); |
||||
if (lobbyId) { |
||||
this.requestJoin(lobbyId).then(v => { |
||||
if (!v) { |
||||
this.setQueryLobbyId(null); |
||||
} |
||||
}) |
||||
} else { |
||||
this.requestJoinRandom(); |
||||
} |
||||
} |
||||
|
||||
onDisconnect() { |
||||
this.stopTimeSync(); |
||||
} |
||||
|
||||
private getQueryLobbyId(): number | null { |
||||
let query = parseQueryString(window.location.search); |
||||
if (query.lobby) { |
||||
let lobbyId = Number.parseInt(query.lobby.toString()); |
||||
if (Number.isSafeInteger(lobbyId) && lobbyId > 0) { |
||||
return lobbyId |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
private setQueryLobbyId(lobbyId: number | null) { |
||||
let query = parseQueryString(window.location.search); |
||||
if (lobbyId) { |
||||
query.lobby = lobbyId.toString(); |
||||
} else { |
||||
delete query.lobby; |
||||
} |
||||
console.log('QUERY', query); |
||||
let newUrl = window.location.protocol + "//" + window.location.host + |
||||
window.location.pathname + (Object.keys(query).length ? ('?' + stringifyQueryString(query)) : ''); |
||||
window.history.pushState({}, '', newUrl); |
||||
} |
||||
|
||||
async call(name: string, params: any) { |
||||
return new Promise<any>((resolve, reject) => { |
||||
let callback = (err: any, res: any) => { |
||||
if (err) { |
||||
return reject(err); |
||||
} |
||||
|
||||
resolve(res); |
||||
}; |
||||
|
||||
let call = new Call(name, params, callback); |
||||
this.calls[call.id] = call; |
||||
this.socket.emit('call', call.id, name, params); |
||||
}); |
||||
} |
||||
|
||||
requestStart() { |
||||
this.socket.emit('request_start'); |
||||
} |
||||
|
||||
async requestJoin(roomId: number): Promise<boolean> { |
||||
return this.call('room_exists', {roomId: roomId}).then(v => { |
||||
if (v) { |
||||
this.socket.emit('request_join', roomId); |
||||
return true; |
||||
} else { |
||||
return false; |
||||
} |
||||
}) |
||||
} |
||||
|
||||
requestJoinRandom() { |
||||
this.socket.emit('request_join_random'); |
||||
} |
||||
|
||||
startTimeSync() { |
||||
for (let i = 0; i < this.timeSyncIntervals.length; i++) { |
||||
let timeoutId = setTimeout(() => { |
||||
this.sendTimeSync(i === this.timeSyncIntervals.length - 1); |
||||
}, this.timeSyncIntervals[i]); |
||||
// @ts-ignore
|
||||
this.timeSyncTimeoutIds.push(timeoutId); |
||||
} |
||||
} |
||||
|
||||
stopTimeSync() { |
||||
for (let i = 0; i < this.timeSyncTimeoutIds.length; i++) { |
||||
clearTimeout(this.timeSyncTimeoutIds[i]); |
||||
} |
||||
} |
||||
|
||||
sendTimeSync(alsoSchedule: boolean) { |
||||
let sync = new TimeSyncRequest(); |
||||
this.socket.emit('time_sync', sync.requestId, Date.now()); |
||||
this.timeSyncs[sync.requestId] = sync; |
||||
|
||||
if (alsoSchedule) { |
||||
setTimeout(() => { |
||||
this.sendTimeSync(true); |
||||
}, this.timeSyncIntervals[this.timeSyncIntervals.length - 1]); |
||||
} |
||||
} |
||||
|
||||
timeSyncResponse(requestId: number, clientDiff: number, serverTime: number) { |
||||
let syncReq = this.timeSyncs[requestId]; |
||||
if (!syncReq) return; |
||||
syncReq.response(clientDiff, serverTime); |
||||
|
||||
for (let i in this.timeSyncs) { |
||||
if (this.timeSyncs[i].start < Date.now() - this.timeSyncTooOld) { |
||||
delete this.timeSyncs[i]; |
||||
break; |
||||
} |
||||
} |
||||
// console.log(this.timeSyncs);
|
||||
// console.log('SERVER TIME', this.serverTimeOffset());
|
||||
|
||||
this.roomTime.set(roomTime()); |
||||
} |
||||
|
||||
serverTime(): number { |
||||
return Date.now() + this.serverTimeOffset(); |
||||
} |
||||
|
||||
serverTimeOffset(): number { |
||||
let num = 0; |
||||
let sum = 0; |
||||
for (let i in this.timeSyncs) { |
||||
let sync = this.timeSyncs[i]; |
||||
if (!sync.ready) continue; |
||||
sum += sync.offset; |
||||
num += 1; |
||||
} |
||||
|
||||
if (num === 0) { |
||||
return 0; |
||||
} |
||||
|
||||
return Math.round(sum / num); |
||||
} |
||||
} |
||||
|
||||
let _callId = 0; |
||||
|
||||
class Call { |
||||
id: number; |
||||
name: string; |
||||
params: any; |
||||
callback: (err: any, res: any) => any; |
||||
|
||||
constructor(name: string, params: any, callback: (err: any, res: any) => void) { |
||||
this.name = name; |
||||
this.params = params; |
||||
this.id = _callId++; |
||||
this.callback = callback; |
||||
} |
||||
} |
||||
|
||||
let _timeSyncId = 0; |
||||
|
||||
class TimeSyncRequest { |
||||
requestId: number; |
||||
start: number; |
||||
offset: number = 0; |
||||
ready = false; |
||||
|
||||
constructor() { |
||||
this.requestId = _timeSyncId++; |
||||
this.start = Date.now(); |
||||
} |
||||
|
||||
response(clientDiff: number, serverTime: number) { |
||||
this.ready = true; |
||||
let now = Date.now(); |
||||
|
||||
let lag = now - this.start; |
||||
this.offset = serverTime - now + lag / 2; |
||||
// console.log('TIME SYNC', 'cdiff:', clientDiff, 'lag:',
|
||||
// lag, 'diff:', serverTime - now, 'offset:', this.offset);
|
||||
} |
||||
} |
||||
|
||||
let connection: Connection = new Connection(); |
||||
// @ts-ignore
|
||||
window['connection'] = connection; |
||||
export default connection; |
||||
|
||||
export function useRoom(): Room | null { |
||||
return useSub(connection.room); |
||||
} |
||||
|
||||
export function useRoomRunningChanged(): Room | null { |
||||
return useSub(connection.room, (v) => [v && v.running]); |
||||
} |
||||
|
||||
export function useTimeline(): Timeline | null { |
||||
return useSub(connection.timeline); |
||||
} |
||||
|
||||
export function useTick(): Tick | null { |
||||
return useSub(connection.tick); |
||||
} |
||||
|
||||
export function useRoomTime(): number { |
||||
return useSub(connection.roomTime); |
||||
} |
||||
|
||||
export function roomTime(): number { |
||||
let room = connection.room.get(); |
||||
if (!room) return 0; |
||||
return connection.serverTime() - room.startTime; |
||||
} |
||||
|
||||
export function useIsConnected() { |
||||
const [isConnected, setIsConnected] = useState(connection.socket.connected); |
||||
|
||||
useEffect(() => { |
||||
let connectListener = () => setIsConnected(true); |
||||
let disconnectListener = () => setIsConnected(false); |
||||
|
||||
connection.socket.on('connect', connectListener); |
||||
connection.socket.on('disconnect', disconnectListener); |
||||
|
||||
return () => { |
||||
connection.socket.off('connect', connectListener); |
||||
connection.socket.off('disconnect', disconnectListener); |
||||
} |
||||
}, [isConnected]); |
||||
|
||||
return isConnected; |
||||
} |
@ -0,0 +1,11 @@ |
||||
import {useEffect, useState} from "react"; |
||||
|
||||
export function useUpdateAfterDelay(delay: number) { |
||||
const [_, timedUpdateSet] = useState(0); |
||||
useEffect(() => { |
||||
let timeoutId = setTimeout(() => { |
||||
timedUpdateSet(v => v + 1); |
||||
}, delay); |
||||
return () => clearTimeout(timeoutId) |
||||
}) |
||||
} |
@ -0,0 +1,40 @@ |
||||
import {useEffect, useState} from "react"; |
||||
|
||||
export class Sub<T> { |
||||
_listeners: ((obj: T) => void)[] = []; |
||||
_current: any = null; |
||||
|
||||
subscribe(listener: any) { |
||||
if (this._listeners.indexOf(listener) < 0) { |
||||
this._listeners.push(listener); |
||||
} |
||||
} |
||||
|
||||
unsubscribe(listener: any) { |
||||
let index = this._listeners.indexOf(listener); |
||||
if (index >= 0) { |
||||
this._listeners.splice(index, 1); |
||||
} |
||||
} |
||||
|
||||
get() { |
||||
return this._current; |
||||
} |
||||
|
||||
set(obj: T) { |
||||
this._current = obj; |
||||
this._listeners.forEach(cb => cb(obj)); |
||||
} |
||||
} |
||||
|
||||
export function useSub<T>(sub: Sub<T>, effectChanges: ((v: T) => any[]) | null = null): T { |
||||
const [currentState, stateSetter] = useState(sub.get()); |
||||
|
||||
useEffect(() => { |
||||
let listener = (obj: T) => stateSetter(obj); |
||||
sub.subscribe(listener); |
||||
return () => sub.unsubscribe(listener); |
||||
}, effectChanges ? effectChanges(currentState) : []); |
||||
|
||||
return currentState; |
||||
} |
Loading…
Reference in new issue