backend in typescript, andere manier van tijdsynchronisatie en syncen van state

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 server
master
Florens Douwes 4 years ago
parent b05ee951d5
commit 18aa4fa369
  1. 2
      backend/Dockerfile
  2. 2725
      backend/data/timeline.js
  3. 2733
      backend/data/timelines.js
  4. 134
      backend/package-lock.json
  5. 9
      backend/package.json
  6. 160
      backend/src/Lobby.js
  7. 186
      backend/src/Room.ts
  8. 120
      backend/src/Service.ts
  9. 59
      backend/src/State.js
  10. 5
      backend/src/User.js
  11. 77
      backend/src/User.ts
  12. 22
      backend/src/app.js
  13. 80
      backend/src/index.js
  14. 175
      backend/src/index.ts
  15. 68
      backend/src/service.js
  16. 24
      backend/src/timeline.ts
  17. 15
      backend/src/util.js
  18. 20
      backend/src/util.ts
  19. 8
      backend/tsconfig.json
  20. 730
      frontend/package-lock.json
  21. 3
      frontend/package.json
  22. 5
      frontend/src/components/App.tsx
  23. 66
      frontend/src/components/Centurion.tsx
  24. 51
      frontend/src/components/Feed.tsx
  25. 33
      frontend/src/components/FeedItem.tsx
  26. 229
      frontend/src/components/Lobby.tsx
  27. 40
      frontend/src/components/NextShot.tsx
  28. 32
      frontend/src/components/Player.ts
  29. 20
      frontend/src/components/ShotsTaken.tsx
  30. 2
      frontend/src/css/index.sass
  31. 68
      frontend/src/css/lobby.sass
  32. 321
      frontend/src/lib/Connection.ts
  33. 41
      frontend/src/types/types.ts
  34. 11
      frontend/src/util/hooks.ts
  35. 19
      frontend/src/util/socket.ts
  36. 40
      frontend/src/util/sub.ts

@ -6,4 +6,4 @@ RUN yarn install
COPY src src/
COPY data data/
CMD ["node", "src/index.js"]
CMD ["npm", "run", "app"]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -4,12 +4,81 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@types/body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==",
"dev": true,
"requires": {
"@types/connect": "*",
"@types/node": "*"
}
},
"@types/connect": {
"version": "3.4.33",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz",
"integrity": "sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/express": {
"version": "4.17.6",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.6.tgz",
"integrity": "sha512-n/mr9tZI83kd4azlPG5y997C/M4DNABK9yErhFM6hKdym4kkmd9j0vtsJyjFIwfRBxtrxZtAfGZCNRIBMFLK5w==",
"dev": true,
"requires": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "*",
"@types/qs": "*",
"@types/serve-static": "*"
}
},
"@types/express-serve-static-core": {
"version": "4.17.4",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.4.tgz",
"integrity": "sha512-dPs6CaRWxsfHbYDVU51VjEJaUJEcli4UI0fFMT4oWmgCvHj+j7oIxz5MLHVL0Rv++N004c21ylJNdWQvPkkb5w==",
"dev": true,
"requires": {
"@types/node": "*",
"@types/range-parser": "*"
}
},
"@types/mime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz",
"integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==",
"dev": true
},
"@types/node": {
"version": "13.11.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.11.0.tgz",
"integrity": "sha512-uM4mnmsIIPK/yeO+42F2RQhGUIs39K2RFmugcJANppXe6J1nvH87PvzPZYpza7Xhhs8Yn9yIAVdLZ84z61+0xQ==",
"dev": true
},
"@types/qs": {
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.1.tgz",
"integrity": "sha512-lhbQXx9HKZAPgBkISrBcmAcMpZsmpe/Cd/hY7LGZS5OfkySUBItnPZHgQPssWYUET8elF+yCFBbP1Q0RZPTdaw==",
"dev": true
},
"@types/range-parser": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz",
"integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==",
"dev": true
},
"@types/serve-static": {
"version": "1.13.3",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz",
"integrity": "sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g==",
"dev": true,
"requires": {
"@types/express-serve-static-core": "*",
"@types/mime": "*"
}
},
"@types/socket.io": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-2.1.4.tgz",
@ -33,6 +102,12 @@
"resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
"integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8="
},
"arg": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
"dev": true
},
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@ -93,6 +168,12 @@
"type-is": "~1.6.17"
}
},
"buffer-from": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
"dev": true
},
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
@ -159,6 +240,12 @@
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dev": true
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -379,6 +466,12 @@
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
"integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4="
},
"make-error": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
"dev": true
},
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@ -663,6 +756,22 @@
}
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
},
"source-map-support": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz",
"integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==",
"dev": true,
"requires": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
}
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
@ -678,6 +787,19 @@
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
},
"ts-node": {
"version": "8.8.2",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.8.2.tgz",
"integrity": "sha512-duVj6BpSpUpD/oM4MfhO98ozgkp3Gt9qIp3jGxwU2DFvl/3IRaEAvbLa8G60uS7C77457e/m5TMowjedeRxI1Q==",
"dev": true,
"requires": {
"arg": "^4.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"source-map-support": "^0.5.6",
"yn": "3.1.1"
}
},
"type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@ -687,6 +809,12 @@
"mime-types": "~2.1.24"
}
},
"typescript": {
"version": "3.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz",
"integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==",
"dev": true
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@ -716,6 +844,12 @@
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
"integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
},
"yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"dev": true
}
}
}

@ -4,8 +4,8 @@
"description": "",
"main": "./src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"app": "node src/index.js"
"build": "tsc",
"app": "ts-node src/index.ts"
},
"author": "",
"license": "ISC",
@ -14,6 +14,9 @@
"socket.io": "^2.3.0"
},
"devDependencies": {
"@types/socket.io": "^2.1.4"
"@types/express": "^4.17.6",
"@types/socket.io": "^2.1.4",
"ts-node": "^8.8.2",
"typescript": "^3.8.3"
}
}

@ -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,11 +1,19 @@
const timeline = require('../data/timeline.js');
// @ts-ignore
import timeline from '../data/timelines.js';
export function getTimeline(name: string) {
let t = timeline.timelines.find((i: any) => i.name == name);
if (!t) return null;
return t;
}
/**
*
* @param i
* @returns {*}
*/
function getIndex(i) {
export function getIndex(i: number): any {
if (i >= timeline.length) {
return;
}
@ -17,7 +25,7 @@ function getIndex(i) {
* @param {number} i - the index.
* @returns {{count: number, timestamp: number}|undefined}
*/
function getNextShot(i) {
export function getNextShot(i: number) {
for (; i < timeline.length; i++) {
const time = getIndex(i);
@ -34,9 +42,9 @@ function getNextShot(i) {
return undefined;
}
function indexForTime(seconds) {
export function indexForTime(seconds: number): number {
let lastIndex = 0;
for(let i = 0; i < timeline.length; i++) {
for (let i = 0; i < timeline.length; i++) {
const time = timeline[i];
if (time.timestamp >= seconds) {
@ -45,8 +53,6 @@ function indexForTime(seconds) {
lastIndex = i;
}
}
module.exports = {
getIndex, getNextShot, indexForTime
};
return -1;
}

@ -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

@ -11,8 +11,9 @@
"@types/react": "^16.9.19",
"@types/react-dom": "^16.9.5",
"@types/react-transition-group": "^4.2.3",
"antd": "^3.26.9",
"antd": "^4.1.4",
"node-sass": "^4.13.1",
"query-string": "^6.12.1",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-scripts": "^3.4.0",

@ -1,13 +1,10 @@
import React from 'react';
import {SocketIOProvider} from "use-socketio/lib";
import Centurion from "./Centurion";
const App = () => {
return (
<SocketIOProvider url="/">
<Centurion/>
</SocketIOProvider>
<Centurion/>
);
};

@ -1,76 +1,38 @@
import React, {useEffect, useRef, useState} from "react";
import React, {useRef, useState} from "react";
import {Row} from "antd";
import {useSocket} from "use-socketio";
import {NumberParam, useQueryParam} from "use-query-params";
import {Tick} from "../types/types";
import {roomTime, useRoom, useRoomRunningChanged, useTick} from "../lib/Connection";
import NextShot from "./NextShot";
import Feed from "./Feed";
import ShotsTaken from "./ShotsTaken";
import Lobby from "./Lobby";
import logo from "../img/via-logo.svg";
import Player from "./Player";
const Centurion = () => {
const [started, setStarted] = useState(false);
const [wantsToStart, setWantsToStart] = useState(false);
const player = useRef(new Audio("centurion.m4a"));
const [noSound] = useQueryParam('noSound', NumberParam);
function goStart() {
console.log("Starting from wrapper..");
setWantsToStart(true);
}
useSocket("started", async (obj: any) => {
console.log("got started");
setStarted(true);
if (typeof noSound === 'undefined') {
await player.current.play();
}
});
useEffect(() => {
if (noSound === 1) {
// Won't play sound so we don't get DOM Interaction errors.
setWantsToStart(true);
}
}, [noSound]);
useSocket("tick_event", async (tick: Tick) => {
if (!started && wantsToStart) {
if (player.current.paused) {
if (typeof noSound === 'undefined') {
await player.current.play();
}
}
setStarted(true);
player.current.currentTime = tick.current;
}
});
let roomRunning = useRoomRunningChanged()?.running || false;
const feedContent = (
<Row>
<NextShot/>
<Feed/>
<ShotsTaken/>
</Row>
<React.Fragment>
<Player/>
<Row>
<NextShot/>
<Feed/>
<ShotsTaken/>
</Row>
</React.Fragment>
);
const lobbyContent = (
<Lobby start={goStart}/>
<Lobby/>
);
const content = started ? feedContent : lobbyContent;
return (
<>
<section className="content">
{content}
{roomRunning ? feedContent : lobbyContent}
</section>
<footer>
<img src={logo} className="via-logo" alt="logo"/>

@ -1,44 +1,37 @@
import React, {useState} from 'react';
import React, {useRef} from 'react';
import {Col} from "antd"
import {useSocket} from "use-socketio/lib";
import {CSSTransition, TransitionGroup} from 'react-transition-group'
import {Tick, TimestampEvent} from "../types/types";
import {TimelineItem} from "../types/types";
import FeedItem from "./FeedItem"
import connection, {roomTime, useRoom, useRoomRunningChanged, useTimeline} from "../lib/Connection";
import {useUpdateAfterDelay} from "../util/hooks";
import CSSTransition from "react-transition-group/CSSTransition";
import TransitionGroup from "react-transition-group/TransitionGroup";
const Feed = (props: any) => {
const roomRunning = useRoomRunningChanged()?.running || false;
const timeline = useTimeline();
const Feed = () => {
const [feedItems, setFeedItems] = useState<TimestampEvent[]>([]);
useUpdateAfterDelay(500)
useSocket("tick_event", async (tick: Tick) => {
if (!tick.next) {
return;
}
let liveFeed: TimelineItem[] = [];
if (tick.current === tick.next.timestamp) {
// Current tick is a new event.
const newItems: any[] = [];
for (let i = 0; i < feedItems.length; i++) {
newItems.push(feedItems[i]);
}
for (let j = 0; j < tick.next.events.length; j++) {
newItems.push(tick.next.events[j]);
}
// @ts-ignore
setFeedItems(newItems);
}
});
if (roomRunning && timeline != null) {
liveFeed = timeline.feed.filter(item => {
return item.timestamp * 1000 <= roomTime()
});
}
return (
<Col className="time-feed" span={24} md={16}>
<TransitionGroup className="feed-reverse">
{feedItems.map((item, i) =>
<CSSTransition timeout={500} classNames="fade" key={i}>
<FeedItem {...item}/>
</CSSTransition>
{liveFeed.map((item, i) =>
item.events.map((event, j) =>
<CSSTransition timeout={500} classNames="fade" key={`${item.timestamp}.${j}`}>
<FeedItem item={event} key={`${item.timestamp}.${j}f`}/>
</CSSTransition>
)
)}
</TransitionGroup>
</Col>

@ -1,4 +1,4 @@
import React from 'react';
import React, {PureComponent} from 'react';
import {TimestampEvent} from "../types/types";
@ -12,20 +12,23 @@ const images = {
shot, song, talk, time
};
const FeedItem = (item: TimestampEvent) => {
return (
<div className="feed-item">
<div className="feed-item__title">
{item.text[0]}
class FeedItem extends PureComponent<{item: TimestampEvent}> {
render() {
// console.log('feeditem render');
return (
<div className="feed-item">
<div className="feed-item__title">
{this.props.item.text[0]}
</div>
<div className="feed-item__emoji">
<img src={images[this.props.item.type]}/>
</div>
<div className="feed-item__desc">
{this.props.item.text[1]}
</div>
</div>
<div className="feed-item__emoji">
<img src={images[item.type]}/>
</div>
<div className="feed-item__desc">
{item.text[1]}
</div>
</div>
);
};
);
}
}
export default FeedItem;

@ -1,68 +1,90 @@
import React, {useEffect, useRef, useState} from 'react';
import {Card, Col, InputNumber, Row} from "antd"
import {useSocket} from "use-socketio/lib";
import {NumberParam, useQueryParam} from 'use-query-params';
import React, {MouseEvent, useState} from 'react';
import {Input, Button, Card, Col, InputNumber, Row, Space, Divider} from "antd"
import { red } from '@ant-design/colors';
import {emit} from "../util/socket";
import connection, {useIsConnected, useRoom} from "../lib/Connection";
import "../css/lobby.sass";
import beer from "../img/beer.png"
const Lobby = (props: any) => {
const [lobbyId, setLobbyId] = useQueryParam('lobby', NumberParam);
const [isLeader, setIsLeader] = useState(false);
const [seekTime, setSeekTime] = useState(0);
const [userCount, setUserCount] = useState(0);
const [selectedRoomId, setSelectedRoomId] = useState(1);
const {socket} = useSocket("welcome", async (obj: any) => {
if (lobbyId) {
// lobbyId is already defined, this means we have a queryparam set.
await onChangeLobbyInput(lobbyId);
return;
}
console.log("Got welcome", lobbyId);
const [joiningLobby, setJoiningLobby] = useState(false);
const [joinLobbyError, setJoinLobbyError] = useState(false);
setLobbyId(obj.lobby.name);
setIsLeader(socket.id === obj.lobby.leaderId);
setUserCount(obj.lobby.users?.length || 0);
});
const isConnected = useIsConnected();
const room = useRoom();
const socketRef = useRef<SocketIOClient.Socket>(socket);
let isLeader = room?.isLeader || false;
let userCount = room?.userCount || 0;
async function onChangeLobbyInput(i: any) {
setLobbyId(i);
function handleRequestStartClicked(e: MouseEvent) {
e.preventDefault();
const result: any = await emit(socket, 'join_lobby', i);
setIsLeader(socket.id === result.lobby.leaderId);
setUserCount(result.lobby.users?.length || 0);
connection.requestStart();
}
useEffect(() => {
async function wrapper() {
const result: any = await emit(socketRef.current, 'lobby_info');
setIsLeader(socketRef.current.id === result.lobby.leaderId);
setUserCount(result.lobby.users?.length || 0);
}
wrapper();
}, []);
const seekItem = (
<span>
Start bij: <InputNumber size="small" min={0} max={60000} value={seekTime}
onChange={time => setSeekTime(time || 0)}/>
sec.
</span>
);
function handleJoin(e: MouseEvent) {
// connection.requestStart();
}
function applyRoomId(v: number) {
connection.requestJoin(v).then(v => {
setJoiningLobby(false);
setJoinLobbyError(!v);
})
setJoiningLobby(true)
}
function joinRandomLobby() {
connection.requestJoinRandom()
setJoinLobbyError(false);
}
// const {socket} = useSocket("welcome", async (obj: any) => {
// if (lobbyQueryId) {
// // lobbyId is already defined, this means we have a queryparam set.
// await onChangeLobbyInput(lobbyQueryId);
// return;
// }
// console.log("Got welcome", lobbyQueryId);
//
// setLobbyId(obj.room.name);
// setIsLeader(socket.id === obj.room.leaderId);
// setUserCount(obj.room.users?.length || 0);
// });
// const socketRef = useRef<SocketIOClient.Socket>(socket);
async function onChangeRoomInput(i: any) {
// setLobbyId(i);
// const result: any = await emit(connection.socket, 'join_room', i);
// setIsLeader(connection.socket.id === result.room.leaderId);
// setUserCount(result.room.users?.length || 0);
// connection.requestJoin(i);
}
// useEffect(() => {
// async function wrapper() {
// const result: any = await emit(connection.current, 'room_info');
// setIsLeader(connection.current.id === result.room.leaderId);
// setUserCount(result.room.users?.length || 0);
// }
//
// wrapper();
// }, []);
return (
<div className="lobby">
<Row>
<Col span={24}>
<h1 id="centurion-title">
<img src={beer} className="beer-flipped" alt="beer"/>Centurion!
<img src={beer} className="beer" alt="beer"/>
</h1>
<Col className="centurion-title" span={24}>
<div className="beer-flipped">
<img src={beer} className={`beer ${isConnected ? 'connected' : 'connecting'}`} alt="beer"/>
</div>
<span className="text">Centurion!</span>
<img src={beer} className={`beer ${isConnected ? 'connected' : 'connecting'}`} alt="beer"/>
</Col>
</Row>
<Row>
@ -72,26 +94,105 @@ const Lobby = (props: any) => {
<div>Kun jij het aan?</div>
</Col>
</Row>
<Row type="flex" justify="center">
<Col className="control" xs={12} sm={10} md={8} xl={6}>
<Card title={`Lobby setup (${lobbyId}):`} actions={isLeader ? [
<span onClick={async () => {
await emit(socket, 'request_start', seekTime)
}}>Start</span>,
] : [
<span onClick={props.start}>Join</span>
]}>
<span>
Huidige lobby <InputNumber size="small" min={100} max={100000} value={lobbyId}
onChange={onChangeLobbyInput}/>
</span>
<p>Er zijn {userCount} gebruiker(s) aanwezig.</p>
<p> {isLeader ? 'Jij bent de baas.' : 'Wachten op de baas...'} </p>
{isLeader ? seekItem : ''}
<br/>
{!isConnected &&
<Row justify="center">
<Col className="lobby-connecting">
<h2>Verbinden...</h2>
</Col>
</Row>
}
{isConnected &&
<Row justify="center">
<Col xs={12} sm={10} md={10} xl={6}>
<Card>
<h3>Huidige lobby: <b>{room?.id || 'Geen lobby'}</b></h3>
<Row>
<Col>
{userCount === 1 ?
<p>Er is één gebruiker aanwezig.</p>
:
<p>Er zijn {userCount} gebruikers aanwezig.</p>
}
<p>{isLeader ? 'Jij bent de baas van deze lobby.' : 'Wachten tot de baas de mix start.'}</p>
</Col>
</Row>
<Row>
<Col>
{isLeader &&
<span>Start de mix op
<Input
type="number"
min={0}
max={60000}
suffix="sec"
value={seekTime}
onChange={v => setSeekTime(parseInt(v.target.value) || 0)}/>
</span>
}
</Col>
</Row>
{isLeader ?
<Button
block
type="primary"
onClick={handleRequestStartClicked}>Start</Button>
:
<Button
block
type="primary"
disabled={!room}
onClick={handleJoin}>Join</Button>
}
<Divider/>
<Row justify="center">
<Col>
<InputNumber
style={{'width': 'calc(100% - 150px)'}}
min={1}
max={100000}
value={selectedRoomId || room?.id || 0}
onChange={(v) => setSelectedRoomId(v || 0)}/>
<Button
style={{'width': '150px'}}
type="primary"
loading={joiningLobby}
onClick={() => {
applyRoomId(selectedRoomId)
}}>Verander van lobby</Button>
{joinLobbyError &&
<span style={{color: red[4]}}>Die lobby bestaat niet</span>
}
</Col>
</Row>
<Row justify="center">
<span className={'lobby-options-or'}>of</span>
</Row>
<Row justify="center">
<Col>
<Button type="primary"
onClick={() => {
joinRandomLobby()
}}>Join een willekeurige lobby</Button>
</Col>
</Row>
</Card>
</Col>
</Row>
}
</div>
);
};

@ -3,42 +3,38 @@ import {Col, Progress} from "antd"
import {useSocket} from "use-socketio/lib";
import {Tick} from "../types/types";
import connection, {roomTime, useRoom, useTimeline} from "../lib/Connection";
import {useUpdateAfterDelay} from "../util/hooks";
const NextShot = () => {
const [remaining, setRemaining] = useState(60);
const [remainingPercentage, setRemainingPercentage] = useState(100);
const fullTime = useRef(0);
useSocket("tick_event", async (tick: Tick) => {
if (!tick.nextShot || !tick.next) {
setRemaining(0);
return;
}
const room = useRoom()
const timeline = useTimeline()
if (fullTime.current === 0) {
fullTime.current = tick.nextShot.timestamp - tick.current;
}
useUpdateAfterDelay(1000)
const shotEvent = tick.next.events.find(e => e.type === 'shot');
let remainingTime = 0;
let remainingPercentage = 0;
if (shotEvent && tick.current === tick.next.timestamp) {
fullTime.current = 0;
}
if (room?.running && timeline) {
const time = roomTime();
const [current, next] = timeline.itemAtTime(time, 'shot');
const timeRemaining = tick.nextShot.timestamp - tick.current;
if (current && next) {
let currentTime = time - current.timestamp * 1000
let nextTime = next.timestamp * 1000 - current.timestamp * 1000;
setRemaining(timeRemaining);
// Fix divide by zero (.. || 1)
setRemainingPercentage(Math.ceil(timeRemaining / (fullTime.current || 1) * 100));
});
remainingTime = Math.round((nextTime - currentTime) / 1000)
remainingPercentage = 100 - (currentTime / (nextTime || 1)) * 100;
}
}
return (
<Col className="sider" span={24} md={4}>
<h1>Tijd tot volgende shot:</h1>
<Progress type="circle"
percent={remainingPercentage}
format={_ => remaining + ' sec.'}
format={_ => remainingTime + ' sec.'}
strokeColor={"#304ba3"}
strokeWidth={10}
status="normal"/>

@ -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;

@ -4,20 +4,24 @@ import {useSocket} from "use-socketio/lib";
import {Tick} from "../types/types";
import {roomTime, useRoom, useTimeline} from "../lib/Connection";
import {useUpdateAfterDelay} from "../util/hooks";
const ShotsTaken = () => {
const [taken, setTaken] = useState(100);
let room = useRoom();
let timeline = useTimeline();
useSocket("tick_event", async (tick: Tick) => {
if (!tick.nextShot) {
setTaken(100);
return;
}
useUpdateAfterDelay(1000);
setTaken(tick.nextShot.count - 1);
});
let taken = 0;
if (room?.running && timeline) {
let [current, _] = timeline.eventAtTime(roomTime(), 'shot');
if (current) {
taken = current.shotCount!!;
}
}
return (
<Col className="sider" span={24} md={4}>

@ -29,7 +29,7 @@ body
right: 0
bottom: 0
width: auto
height: 7em
height: 5em
padding: 10px

@ -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;
}

@ -10,6 +10,47 @@ export interface Tick {
}
}
export interface Room {
id: number,
userCount: number,
isLeader: boolean,
running: boolean,
startTime: number
}
export class Timeline {
name: string
feed: TimelineItem[]
constructor(obj: any) {
this.name = obj.name;
this.feed = obj.feed;
}
itemAtTime(time: number, type: string = ''): [TimelineItem | null, TimelineItem | null] {
let feedToSearch = type ? this.feed.filter(i => i.events.some(j => j.type == type)) : this.feed;
for (let i = 1; i < feedToSearch.length; i++) {
if (feedToSearch[i].timestamp * 1000 >= time) {
return [feedToSearch[i - 1], feedToSearch[i]];
}
}
return [feedToSearch[feedToSearch.length - 1], null];
}
eventAtTime(time: number, type: string = ''): [TimestampEvent | null, TimestampEvent | null] {
let [current, next] = this.itemAtTime(time, type)
return [current ? (current.events.find(i => i.type == type) || null) : null,
next ? (next.events.find(i => i.type == type) || null) : null]
}
}
export interface TimelineItem {
timestamp: number,
events: TimestampEvent[]
}
export interface TimestampEvent {
type: 'talk' | 'shot' | 'song' | 'time',
text: string[],

@ -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)
})
}

@ -1,22 +1,3 @@
// const socket = io(window.location.protocol + "//" + window.location.hostname + ":3001");
const socket = {
on: (...args: any[]) => {
},
once: (...args: any[]) => {
},
off: (...args: any[]) => {
},
id: '1'
};
export default socket;
/**
* Promisify emit.
* @param event

@ -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…
Cancel
Save