diff --git a/src/html/index.html b/src/html/index.html
index 10d0202..88a73f2 100644
--- a/src/html/index.html
+++ b/src/html/index.html
@@ -257,6 +257,7 @@
+
diff --git a/src/ts/fallbackLanguage.ts b/src/ts/fallbackLanguage.ts
index 01dcff3..f1586f8 100644
--- a/src/ts/fallbackLanguage.ts
+++ b/src/ts/fallbackLanguage.ts
@@ -61,6 +61,8 @@ const fallbackLanguage : Language = {
"kAdminVMButtons_ClearTurnQueue": "Clear Turn Queue",
"kAdminVMButtons_BypassTurn": "Bypass Turn",
"kAdminVMButtons_IndefiniteTurn": "Indefinite Turn",
+ "kAdminVMButtons_GhostTurnOn": "Ghost Turn (On)",
+ "kAdminVMButtons_GhostTurnOff": "Ghost Turn (Off)",
"kAdminVMButtons_Ban": "Ban",
"kAdminVMButtons_Kick": "Kick",
diff --git a/src/ts/i18n.ts b/src/ts/i18n.ts
index 48db09d..0a2c2ab 100644
--- a/src/ts/i18n.ts
+++ b/src/ts/i18n.ts
@@ -62,6 +62,8 @@ export enum I18nStringKey {
kAdminVMButtons_ClearTurnQueue = 'kAdminVMButtons_ClearTurnQueue',
kAdminVMButtons_BypassTurn = 'kAdminVMButtons_BypassTurn',
kAdminVMButtons_IndefiniteTurn = 'kAdminVMButtons_IndefiniteTurn',
+ kAdminVMButtons_GhostTurnOn = 'kAdminVMButtons_GhostTurnOn',
+ kAdminVMButtons_GhostTurnOff = 'kAdminVMButtons_GhostTurnOff',
kAdminVMButtons_Ban = 'kAdminVMButtons_Ban',
kAdminVMButtons_Kick = 'kAdminVMButtons_Kick',
@@ -272,6 +274,7 @@ export class I18n {
clearQueueBtnText: I18nStringKey.kAdminVMButtons_ClearTurnQueue,
bypassTurnBtnText: I18nStringKey.kAdminVMButtons_BypassTurn,
indefTurnBtnText: I18nStringKey.kAdminVMButtons_IndefiniteTurn,
+ ghostTurnBtnText: I18nStringKey.kAdminVMButtons_GhostTurnOff,
// Account modal
accountLoginUsernameLabel: I18nStringKey.kGeneric_Username,
diff --git a/src/ts/main.ts b/src/ts/main.ts
index 1e4e566..d216f0e 100644
--- a/src/ts/main.ts
+++ b/src/ts/main.ts
@@ -70,6 +70,8 @@ const elements = {
forceVoteYesBtn: document.getElementById('forceVoteYesBtn') as HTMLButtonElement,
forceVoteNoBtn: document.getElementById('forceVoteNoBtn') as HTMLButtonElement,
indefTurnBtn: document.getElementById('indefTurnBtn') as HTMLButtonElement,
+ ghostTurnBtn: document.getElementById('ghostTurnBtn') as HTMLButtonElement,
+ ghostTurnBtnText: document.getElementById('ghostTurnBtnText') as HTMLSpanElement,
qemuMonitorInput: document.getElementById('qemuMonitorInput') as HTMLInputElement,
qemuMonitorSendBtn: document.getElementById('qemuMonitorSendBtn') as HTMLButtonElement,
qemuMonitorOutput: document.getElementById('qemuMonitorOutput') as HTMLTextAreaElement,
@@ -482,6 +484,7 @@ function closeVM() {
elements.clearQueueBtn.style.display = 'none';
elements.qemuMonitorBtn.style.display = 'none';
elements.indefTurnBtn.style.display = 'none';
+ elements.ghostTurnBtn.style.display = 'none';
elements.xssCheckboxContainer.style.display = 'none';
elements.forceVotePanel.style.display = 'none';
elements.voteResetPanel.style.display = 'none';
@@ -813,6 +816,7 @@ function onLogin(_rank: Rank, _perms: Permissions) {
if (_rank === Rank.Admin) {
elements.qemuMonitorBtn.style.display = 'inline-block';
elements.indefTurnBtn.style.display = 'inline-block';
+ elements.ghostTurnBtn.style.display = 'inline-block';
}
if (_perms.xss) elements.xssCheckboxContainer.style.display = 'inline-block';
if (_perms.forcevote) elements.forceVotePanel.style.display = 'block';
@@ -876,6 +880,15 @@ elements.forceVoteNoBtn.addEventListener('click', () => VM?.forceVote(false));
elements.forceVoteYesBtn.addEventListener('click', () => VM?.forceVote(true));
elements.indefTurnBtn.addEventListener('click', () => VM?.indefiniteTurn());
+
+elements.ghostTurnBtn.addEventListener('click', () => {
+ w.collabvm.ghostTurn = !w.collabvm.ghostTurn;
+ if (w.collabvm.ghostTurn)
+ elements.ghostTurnBtnText.innerText = TheI18n.GetString(I18nStringKey.kAdminVMButtons_GhostTurnOn);
+ else
+ elements.ghostTurnBtnText.innerText = TheI18n.GetString(I18nStringKey.kAdminVMButtons_GhostTurnOff);
+});
+
async function sendQEMUCommand() {
if (!elements.qemuMonitorInput.value) return;
let cmd = elements.qemuMonitorInput.value;
@@ -1212,7 +1225,8 @@ w.collabvm = {
closeVM: closeVM,
loadList: loadList,
multicollab: multicollab,
- getVM: () => VM
+ getVM: () => VM,
+ ghostTurn: false,
};
// Multicollab will stay in the global scope for backwards compatibility
w.multicollab = multicollab;
@@ -1276,6 +1290,10 @@ document.addEventListener('DOMContentLoaded', async () => {
if (darkTheme) elements.toggleThemeBtnText.innerHTML = TheI18n.GetString(I18nStringKey.kSiteButtons_LightMode);
else elements.toggleThemeBtnText.innerHTML = TheI18n.GetString(I18nStringKey.kSiteButtons_DarkMode);
+ if (w.collabvm.ghostTurn)
+ elements.ghostTurnBtnText.innerText = TheI18n.GetString(I18nStringKey.kAdminVMButtons_GhostTurnOn);
+ else
+ elements.ghostTurnBtnText.innerText = TheI18n.GetString(I18nStringKey.kAdminVMButtons_GhostTurnOff);
});
// Load theme
var _darktheme : boolean;
diff --git a/src/ts/protocol/CollabVMClient.ts b/src/ts/protocol/CollabVMClient.ts
index dbc5200..bffed35 100644
--- a/src/ts/protocol/CollabVMClient.ts
+++ b/src/ts/protocol/CollabVMClient.ts
@@ -9,6 +9,7 @@ import GetKeysym from '../keyboard.js';
import VoteStatus from './VoteStatus.js';
import MuteState from './MuteState.js';
import { StringLike } from '../StringLike.js';
+const w = window as any;
export interface CollabVMClientEvents {
//open: () => void;
@@ -98,7 +99,7 @@ export default class CollabVMClient {
this.canvas.addEventListener(
'mousedown',
(e: MouseEvent) => {
- if (this.users.find((u) => u.username === this.username)?.turn === -1 && this.rank !== Rank.Admin) return;
+ if (!this.shouldSendInput()) return;
this.mouse.initFromMouseEvent(e);
this.sendmouse(this.mouse.x, this.mouse.y, this.mouse.makeMask());
},
@@ -110,7 +111,7 @@ export default class CollabVMClient {
this.canvas.addEventListener(
'mouseup',
(e: MouseEvent) => {
- if (this.users.find((u) => u.username === this.username)?.turn === -1 && this.rank !== Rank.Admin) return;
+ if (!this.shouldSendInput()) return;
this.mouse.initFromMouseEvent(e);
this.sendmouse(this.mouse.x, this.mouse.y, this.mouse.makeMask());
},
@@ -122,7 +123,7 @@ export default class CollabVMClient {
this.canvas.addEventListener(
'mousemove',
(e: MouseEvent) => {
- if (this.users.find((u) => u.username === this.username)?.turn === -1 && this.rank !== Rank.Admin) return;
+ if (!this.shouldSendInput()) return;
this.mouse.initFromMouseEvent(e);
this.sendmouse(this.mouse.x, this.mouse.y, this.mouse.makeMask());
},
@@ -135,7 +136,7 @@ export default class CollabVMClient {
'keydown',
(e: KeyboardEvent) => {
e.preventDefault();
- if (this.users.find((u) => u.username === this.username)?.turn === -1 && this.rank !== Rank.Admin) return;
+ if (!this.shouldSendInput()) return;
let keysym = GetKeysym(e.keyCode, e.key, e.location);
if (keysym === null) return;
this.key(keysym, true);
@@ -149,7 +150,7 @@ export default class CollabVMClient {
'keyup',
(e: KeyboardEvent) => {
e.preventDefault();
- if (this.users.find((u) => u.username === this.username)?.turn === -1 && this.rank !== Rank.Admin) return;
+ if (!this.shouldSendInput()) return;
let keysym = GetKeysym(e.keyCode, e.key, e.location);
if (keysym === null) return;
this.key(keysym, false);
@@ -163,7 +164,7 @@ export default class CollabVMClient {
'wheel',
(ev: WheelEvent) => {
ev.preventDefault();
- if (this.users.find((u) => u.username === this.username)?.turn === -1 && this.rank !== Rank.Admin) return;
+ if (!this.shouldSendInput()) return;
this.mouse.initFromWheelEvent(ev);
this.sendmouse(this.mouse.x, this.mouse.y, this.mouse.makeMask());
@@ -677,6 +678,10 @@ export default class CollabVMClient {
return this.internalEmitter.on(event, callback);
}
+ private shouldSendInput() {
+ return this.users.find(u => u.username === this.username)?.turn === 0 || (w.collabvm.ghostTurn && this.rank === Rank.Admin);
+ }
+
on(event: E, callback: CollabVMClientEvents[E]): Unsubscribe {
let unsub = this.publicEmitter.on(event, callback);
this.unsubscribeCallbacks.push(unsub);
diff --git a/static/lang/en-us.json b/static/lang/en-us.json
index 3405815..860e50e 100644
--- a/static/lang/en-us.json
+++ b/static/lang/en-us.json
@@ -60,6 +60,8 @@
"kAdminVMButtons_ClearTurnQueue": "Clear Turn Queue",
"kAdminVMButtons_BypassTurn": "Bypass Turn",
"kAdminVMButtons_IndefiniteTurn": "Indefinite Turn",
+ "kAdminVMButtons_GhostTurnOn": "Ghost Turn (On)",
+ "kAdminVMButtons_GhostTurnOff": "Ghost Turn (Off)",
"kAdminVMButtons_Ban": "Ban",
"kAdminVMButtons_Kick": "Kick",