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 src src/
COPY data data/ 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, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "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": { "@types/node": {
"version": "13.11.0", "version": "13.11.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.11.0.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-13.11.0.tgz",
"integrity": "sha512-uM4mnmsIIPK/yeO+42F2RQhGUIs39K2RFmugcJANppXe6J1nvH87PvzPZYpza7Xhhs8Yn9yIAVdLZ84z61+0xQ==", "integrity": "sha512-uM4mnmsIIPK/yeO+42F2RQhGUIs39K2RFmugcJANppXe6J1nvH87PvzPZYpza7Xhhs8Yn9yIAVdLZ84z61+0xQ==",
"dev": true "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": { "@types/socket.io": {
"version": "2.1.4", "version": "2.1.4",
"resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-2.1.4.tgz", "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", "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
"integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" "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": { "array-flatten": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@ -93,6 +168,12 @@
"type-is": "~1.6.17" "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": { "bytes": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", "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", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" "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": { "ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "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", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz",
"integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" "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": { "media-typer": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "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": { "statuses": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "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", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" "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": { "type-is": {
"version": "1.6.18", "version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@ -687,6 +809,12 @@
"mime-types": "~2.1.24" "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": { "unpipe": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@ -716,6 +844,12 @@
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
"integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" "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": "", "description": "",
"main": "./src/index.js", "main": "./src/index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "build": "tsc",
"app": "node src/index.js" "app": "ts-node src/index.ts"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
@ -14,6 +14,9 @@
"socket.io": "^2.3.0" "socket.io": "^2.3.0"
}, },
"devDependencies": { "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 * @param i
* @returns {*} * @returns {*}
*/ */
function getIndex(i) { export function getIndex(i: number): any {
if (i >= timeline.length) { if (i >= timeline.length) {
return; return;
} }
@ -17,7 +25,7 @@ function getIndex(i) {
* @param {number} i - the index. * @param {number} i - the index.
* @returns {{count: number, timestamp: number}|undefined} * @returns {{count: number, timestamp: number}|undefined}
*/ */
function getNextShot(i) { export function getNextShot(i: number) {
for (; i < timeline.length; i++) { for (; i < timeline.length; i++) {
const time = getIndex(i); const time = getIndex(i);
@ -34,9 +42,9 @@ function getNextShot(i) {
return undefined; return undefined;
} }
function indexForTime(seconds) { export function indexForTime(seconds: number): number {
let lastIndex = 0; let lastIndex = 0;
for(let i = 0; i < timeline.length; i++) { for (let i = 0; i < timeline.length; i++) {
const time = timeline[i]; const time = timeline[i];
if (time.timestamp >= seconds) { if (time.timestamp >= seconds) {
@ -45,8 +53,6 @@ function indexForTime(seconds) {
lastIndex = i; lastIndex = i;
} }
}
module.exports = { return -1;
getIndex, getNextShot, indexForTime }
};

@ -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": "^16.9.19",
"@types/react-dom": "^16.9.5", "@types/react-dom": "^16.9.5",
"@types/react-transition-group": "^4.2.3", "@types/react-transition-group": "^4.2.3",
"antd": "^3.26.9", "antd": "^4.1.4",
"node-sass": "^4.13.1", "node-sass": "^4.13.1",
"query-string": "^6.12.1",
"react": "^16.12.0", "react": "^16.12.0",
"react-dom": "^16.12.0", "react-dom": "^16.12.0",
"react-scripts": "^3.4.0", "react-scripts": "^3.4.0",

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

@ -1,76 +1,38 @@
import React, {useEffect, useRef, useState} from "react"; import React, {useRef, useState} from "react";
import {Row} from "antd"; import {Row} from "antd";
import {useSocket} from "use-socketio";
import {NumberParam, useQueryParam} from "use-query-params"; 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 NextShot from "./NextShot";
import Feed from "./Feed"; import Feed from "./Feed";
import ShotsTaken from "./ShotsTaken"; import ShotsTaken from "./ShotsTaken";
import Lobby from "./Lobby"; import Lobby from "./Lobby";
import logo from "../img/via-logo.svg"; import logo from "../img/via-logo.svg";
import Player from "./Player";
const Centurion = () => { const Centurion = () => {
const [started, setStarted] = useState(false); let roomRunning = useRoomRunningChanged()?.running || 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;
}
});
const feedContent = ( const feedContent = (
<Row> <React.Fragment>
<NextShot/> <Player/>
<Feed/> <Row>
<ShotsTaken/> <NextShot/>
</Row> <Feed/>
<ShotsTaken/>
</Row>
</React.Fragment>
); );
const lobbyContent = ( const lobbyContent = (
<Lobby start={goStart}/> <Lobby/>
); );
const content = started ? feedContent : lobbyContent;
return ( return (
<> <>
<section className="content"> <section className="content">
{content} {roomRunning ? feedContent : lobbyContent}
</section> </section>
<footer> <footer>
<img src={logo} className="via-logo" alt="logo"/> <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 {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 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 = () => { useUpdateAfterDelay(500)
const [feedItems, setFeedItems] = useState<TimestampEvent[]>([]);
useSocket("tick_event", async (tick: Tick) => { let liveFeed: TimelineItem[] = [];
if (!tick.next) {
return;
}
if (tick.current === tick.next.timestamp) { if (roomRunning && timeline != null) {
// Current tick is a new event. liveFeed = timeline.feed.filter(item => {
return item.timestamp * 1000 <= roomTime()
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);
}
});
return ( return (
<Col className="time-feed" span={24} md={16}> <Col className="time-feed" span={24} md={16}>
<TransitionGroup className="feed-reverse"> <TransitionGroup className="feed-reverse">
{feedItems.map((item, i) => {liveFeed.map((item, i) =>
<CSSTransition timeout={500} classNames="fade" key={i}> item.events.map((event, j) =>
<FeedItem {...item}/> <CSSTransition timeout={500} classNames="fade" key={`${item.timestamp}.${j}`}>
</CSSTransition> <FeedItem item={event} key={`${item.timestamp}.${j}f`}/>
</CSSTransition>
)
)} )}
</TransitionGroup> </TransitionGroup>
</Col> </Col>

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

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

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

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

@ -1,17 +1,59 @@
#centurion-title .lobby
font-size: 3.5rem .centurion-title
text-align: center text-align: center
min-height: inherit
.hints font-size: 3.0rem
font-size: 2rem min-height: inherit
text-align: center
.text
padding: 0 2rem
.control .beer
font-size: 1.3rem animation-fill-mode: forwards
margin-top: 5em
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24)
.beer-flipped &.connecting
transform: scaleX(-1) 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 { export interface TimestampEvent {
type: 'talk' | 'shot' | 'song' | 'time', type: 'talk' | 'shot' | 'song' | 'time',
text: string[], 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. * Promisify emit.
* @param event * @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