Files
MaterialDeck/MaterialDeck.js
Cristian Deenen 9242cf4978 v1.5.2_pre
2023-10-05 23:17:56 +02:00

756 lines
25 KiB
JavaScript

import { registerSettings } from "./src/settings.js";
import { StreamDeck } from "./src/streamDeck.js";
import { TokenControl } from "./src/actions/token.js";
import { MacroControl } from "./src/actions/macro.js";
import { CombatTracker } from "./src/actions/combattracker.js";
import { PlaylistControl } from "./src/actions/playlist.js";
import { SoundboardControl } from "./src/actions/soundboard.js";
import { OtherControls } from "./src/actions/othercontrols.js";
import { ExternalModules } from "./src/actions/external.js";
import { SceneControl } from "./src/actions/scene.js";
import { downloadUtility, compareVersions } from "./src/misc.js";
import { TokenHelper } from "./src/systems/tokenHelper.js";
export const releaseURLs = {
module: {
api: "https://api.github.com/repos/MaterialFoundry/MaterialDeck/releases",
url: "https://github.com/MaterialFoundry/MaterialDeck/releases"
},
plugin: {
api: "https://api.github.com/repos/MaterialFoundry/MaterialDeck_SD/releases",
url: "https://github.com/MaterialFoundry/MaterialDeck_SD/releases"
},
materialCompanion: {
api: "https://api.github.com/repos/MaterialFoundry/MaterialCompanion/releases",
url: "https://github.com/MaterialFoundry/MaterialCompanion/releases"
}
}
export let versions = {
materialCompanion: {
current: 'Unknown',
minimum: 'Unknown'
},
plugin: {
current: 'Unknown',
minimum: 'Unknown'
},
module: {
current: 'Unknown'
}
}
export var streamDeck;
export var tokenControl;
export var macroControl;
export var combatTracker;
export var playlistControl;
export var soundboard;
export var otherControls;
export var externalModules;
export var sceneControl;
export var tokenHelper;
export const moduleName = "MaterialDeck";
export let gamingSystem = "dnd5e";
export let hotbarUses = false;
export let calculateHotbarUses;
let ready = false;
let controlTokenTimer;
export var enableModule;
//Websocket variables
var ws; //Websocket variable
let wsOpen = false; //Bool for checking if websocket has ever been opened => changes the warning message if there's no connection
let wsInterval; //Interval timer to detect disconnections
let WSconnected = false;
//CONFIG.debug.hooks = true;
/*
* Analyzes the message received
*
* @param {*} msg Message received
*/
async function analyzeWSmessage(msg){
if (enableModule == false) return;
const data = JSON.parse(msg);
//console.log("Received",data);
if (data.type == 'connected') {
console.log('rec',data)
}
if (data.type == "connected" && data.source == "MaterialCompanion"){
//console.log('data',data)
//transmitInitData();
let sdNok = false;
let msNok = false;
if (data.materialCompanionVersion) {
versions.materialCompanion.current = data.materialCompanionVersion;
if (!compareVersions(versions.materialCompanion.minimum, versions.materialCompanion.current)) {
msNok = true;
}
}
if (data.pluginVersion) {
versions.plugin.current = data.pluginVersion;
if (!compareVersions(versions.plugin.minimum, versions.plugin.current)) {
sdNok = true;
}
}
if (msNok || sdNok) {
let content = '';
if (sdNok && msNok) content += `${game.i18n.localize("MaterialDeck.UpdateRequired.Both")}<br><br>`;
else if (sdNok) content += `${game.i18n.localize("MaterialDeck.UpdateRequired.SD")}<br><br>`;
else if (msNok) content += `${game.i18n.localize("MaterialDeck.UpdateRequired.MC")}<br><br>`;
content += `${game.i18n.localize("MaterialDeck.UpdateRequired.Update")}<br><br>`;
if (sdNok) content += `<a href="${releaseURLs.plugin.url}">${game.i18n.localize("MaterialDeck.UpdateRequired.SDdownload")}</a><br>`;
if (msNok) content += `<a href="${releaseURLs.materialCompanion.url}">Material Companion</a><br>`;
content += "<br>"
new Dialog({
title: game.i18n.localize("MaterialDeck.UpdateRequired.Title"),
content,
buttons: {
download: {
icon: '<i class="fas fa-download"></i>',
label: "Download Utility",
callback: () => new downloadUtility()
},
ignore: {
icon: '<i class="fas fa-times"></i>',
label: "Ignore"
}
},
default: "download"
}).render(true);
}
console.log("streamdeck connected to server", versions.materialCompanion.current);
streamDeck.resetImageBuffer();
}
if (data.type == 'newDevice') {
streamDeck.newDevice(data.iteration,data.device);
return;
}
if (data == undefined || data.payload == undefined) return;
const action = data.action;
const event = data.event;
const context = data.context;
const coordinates = data.payload.coordinates;
const settings = data.payload.settings;
const device = data.device;
const name = data.deviceName;
const type = data.deviceType;
if (data.data == 'init'){
}
if (event == 'willAppear' || event == 'didReceiveSettings'){
if (coordinates == undefined) return;
streamDeck.setScreen(action);
await streamDeck.setContext(device,data.size,data.deviceIteration,action,context,coordinates,settings,name,type);
if (action == 'token'){
tokenControl.active = true;
tokenControl.pushData(canvas.tokens.controlled[0]?.id,settings,context,device);
}
else if (action == 'macro')
macroControl.update(settings,context,device);
else if (action == 'combattracker')
combatTracker.update(settings,context,device);
else if (action == 'playlist')
playlistControl.update(settings,context,device);
else if (action == 'soundboard')
soundboard.update(settings,context,device);
else if (action == 'other')
otherControls.update(settings,context,device);
else if (action == 'external')
externalModules.update(settings,context,device);
else if (action == 'scene')
sceneControl.update(settings,context,device);
}
else if (event == 'willDisappear'){
if (coordinates == undefined) return;
streamDeck.clearContext(device,action,coordinates,context);
}
else if (event == 'keyDown'){
if (action == 'token')
tokenControl.keyPress(settings);
else if (action == 'macro')
macroControl.keyPress(settings);
else if (action == 'combattracker')
combatTracker.keyPress(settings,context,device);
else if (action == 'playlist')
playlistControl.keyPress(settings,context,device);
else if (action == 'soundboard')
soundboard.keyPressDown(settings);
else if (action == 'other')
otherControls.keyPress(settings,context,device);
else if (action == 'external')
externalModules.keyPress(settings,context,device);
else if (action == 'scene')
sceneControl.keyPress(settings);
}
else if (event == 'keyUp'){
if (action == 'soundboard'){
soundboard.keyPressUp(settings);
}
}
};
/**
* Start a new websocket
* Start a 10s interval, if no connection is made, run resetWS()
* If connection is made, set interval to 1.5s to check for disconnects
* If message is received, reset the interval, and send the message to analyzeWSmessage()
*/
function startWebsocket() {
const address = game.settings.get(moduleName,'address');
const url = address.startsWith('wss://') ? address : ('ws://'+address+'/');
ws = new WebSocket(url);
ws.onmessage = function(msg){
//console.log(msg);
analyzeWSmessage(msg.data);
clearInterval(wsInterval);
wsInterval = setInterval(resetWS, 5000);
}
ws.onopen = function() {
messageCount = 0;
WSconnected = true;
ui.notifications.info("Material Deck "+game.i18n.localize("MaterialDeck.Notifications.Connected") +": "+address);
wsOpen = true;
const msg = {
target: "MaterialCompanion",
source: "MaterialDeck_Foundry",
sourceTarget: "MaterialDeck_Device",
type: "connected",
userId: game.userId,
userName: game.user.name,
version: game.modules.get(moduleName).version
}
ws.send(JSON.stringify(msg));
transmitInitData();
clearInterval(wsInterval);
wsInterval = setInterval(resetWS, 5000);
}
clearInterval(wsInterval);
wsInterval = setInterval(resetWS, 10000);
}
let messageCount = 0;
function transmitInitData() {
const msg = {
target: "MaterialDeck_Device",
type: "init",
userId: game.userId,
system: getGamingSystem(),
systemData: {
conditions: tokenHelper.getConditionList(),
abilities: tokenHelper.getAbilityList(),
saves: tokenHelper.getSavesList(),
skills: tokenHelper.getSkillList(),
itemTypes: tokenHelper.getItemTypes(),
weaponRollModes: tokenHelper.getWeaponRollModes(),
featureTypes: tokenHelper.getFeatureTypes(),
spellLevels: tokenHelper.getSpellLevels(),
spellTypes: tokenHelper.getSpellTypes(),
stats: tokenHelper.getStatsList(),
onClick: tokenHelper.getOnClickList(),
rollTypes: tokenHelper.getRollTypes(),
attackModes: tokenHelper.getAttackModes()
},
coreVersion: game.version.split('.')[0]
}
ws.send(JSON.stringify(msg));
}
/**
* Try to reset the websocket if a connection is lost
*/
function resetWS(){
const maxMessages = game.settings.get(moduleName, 'nrOfConnMessages');
if (wsOpen) {
ui.notifications.warn("Material Deck: "+game.i18n.localize("MaterialDeck.Notifications.Disconnected"));
wsOpen = false;
messageCount = 0;
WSconnected = false;
startWebsocket();
}
else if (ws.readyState == 3){
if (maxMessages == 0 || maxMessages > messageCount) {
messageCount++;
const countString = maxMessages == 0 ? "" : " (" + messageCount + "/" + maxMessages + ")";
ui.notifications.warn("Material Deck: "+game.i18n.localize("MaterialDeck.Notifications.ConnectFail") + countString);
}
WSconnected = false;
startWebsocket();
}
}
export function sendWS(txt){
if (WSconnected)
ws.send(txt);
}
export function isEmpty(obj) {
for(var key in obj) {
if(obj.hasOwnProperty(key))
return false;
}
return true;
}
export function getPermission(action,func) {
const role = game.user.role-1;
const settings = game.settings.get(moduleName,'userPermission');
if (action == 'ENABLE') return settings.enable[role];
else return settings.permissions?.[action]?.[func]?.[role];
}
function getGamingSystem() {
const systemOverride = game.settings.get(moduleName,'systemOverride');
gamingSystem = (systemOverride == undefined || systemOverride == null || systemOverride == '') ? game.system.id : systemOverride;
return gamingSystem;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Hooks
//
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
async function enableSettingDialog() {
return new Promise((resolve, reject) => {
const dialog = new Dialog({
title: game.i18n.localize("MaterialDeck.EnableDialog.Title"),
content: game.i18n.localize("MaterialDeck.EnableDialog.Content"),
buttons: {
yes: { label: game.i18n.localize("MaterialDeck.EnableDialog.Yes"), callback: () => { resolve('yes') } },
no: { label: game.i18n.localize("MaterialDeck.EnableDialog.No"), callback: () => { resolve('no') } },
},
default: 'no',
close: () => { reject() }
});
dialog.render(true);
});
}
/**
* Ready hook
* Attempt to open the websocket
*/
Hooks.once('ready', async()=>{
await registerSettings();
if (game.settings.get(moduleName, 'Enable')) game.settings.set(moduleName,'EnableDialogShown',true);
else if (!game.settings.get(moduleName,'EnableDialogShown')) {
const response = await enableSettingDialog();
await game.settings.set(moduleName,'EnableDialogShown',true);
if (response == "yes") await game.settings.set(moduleName, 'Enable', true);
else await game.settings.set(moduleName, 'Enable', false);
}
enableModule = (game.settings.get(moduleName,'Enable')) ? true : false;
getGamingSystem();
versions.module.current = game.modules.get('MaterialDeck').version;
versions.materialCompanion.minimum = game.modules.get(moduleName).flags.minimumMaterialCompanionVersion;
versions.plugin.minimum = game.modules.get(moduleName).flags.minimumPluginVersion;
soundboard = new SoundboardControl();
streamDeck = new StreamDeck();
tokenControl = new TokenControl();
macroControl = new MacroControl();
combatTracker = new CombatTracker();
playlistControl = new PlaylistControl();
otherControls = new OtherControls();
externalModules = new ExternalModules();
sceneControl = new SceneControl();
tokenHelper = new TokenHelper();
game.socket.on(`module.MaterialDeck`, async(payload) =>{
//console.log(payload);
if (payload.msgType == "playSound") soundboard.playSound(payload.trackNr,payload.src,payload.play,payload.repeat,payload.volume);
else if (game.user.isGM && payload.msgType == "playPlaylist") {
const playlist = playlistControl.getPlaylist(payload.playlistNr);
playlistControl.playPlaylist(playlist,payload.playlistNr);
}
else if (game.user.isGM && payload.msgType == "playTrack") {
const playlist = playlistControl.getPlaylist(payload.playlistNr);
const sounds = playlist.data.sounds;
for (let track of sounds)
if (track._id == payload.trackId)
playlistControl.playTrack(track,playlist,payload.playlistNr)
}
else if (game.user.isGM && payload.msgType == "stopAllPlaylists")
playlistControl.stopAll(payload.force);
else if (game.user.isGM && payload.msgType == "soundboardUpdate") {
await game.settings.set(moduleName,'soundboardSettings',payload.settings);
const payloadNew = {
"msgType": "soundboardRefresh"
};
game.socket.emit(`module.MaterialDeck`, payloadNew);
}
else if (game.user.isGM == false && payload.msgType == "soundboardRefresh" && enableModule)
soundboard.updateAll();
else if (game.user.isGM && payload.msgType == "macroboardUpdate") {
await game.settings.set(moduleName,'macroSettings',payload.settings);
const payloadNew = {
"msgType": "macroboardRefresh"
};
game.socket.emit(`module.MaterialDeck`, payloadNew);
}
else if (game.user.isGM == false && payload.msgType == "macroboardRefresh" && enableModule)
macroControl.updateAll();
else if (game.user.isGM && payload.msgType == "playlistUpdate") {
await game.settings.set(moduleName,'playlists',payload.settings);
const payloadNew = {
"msgType": "playlistRefresh"
};
game.socket.emit(`module.MaterialDeck`, payloadNew);
}
else if (game.user.isGM == false && payload.msgType == "playlistRefresh" && enableModule)
playlistControl.updateAll();
});
if (game.user.isGM) {
let soundBoardSettings = game.settings.get(moduleName,'soundboardSettings');
let macroSettings = game.settings.get(moduleName, 'macroSettings');
let array = [];
for (let i=0; i<64; i++) array[i] = "";
let arrayVolume = [];
for (let i=0; i<64; i++) arrayVolume[i] = "50";
let arrayZero = [];
for (let i=0; i<64; i++) arrayZero[i] = "0";
if (macroSettings.color == undefined){
game.settings.set(moduleName,'macroSettings',{
macros: array,
color: arrayZero
});
}
const settings = {
playlist: "",
sounds: array,
colorOn: arrayZero,
colorOff: arrayZero,
mode: arrayZero,
toggle: arrayZero,
volume: arrayVolume
};
if (soundBoardSettings.colorOff == undefined){
game.settings.set(moduleName,'soundboardSettings',settings);
}
}
if (enableModule == false) return;
if (getPermission('ENABLE') == false) {
ready = true;
return;
}
startWebsocket();
const hotbarUsesTemp = game.modules.get("illandril-hotbar-uses");
if (hotbarUsesTemp != undefined) hotbarUses = true;
});
function updateActor(id) {
const token = tokenHelper.getTokenFromActorId(id);
if (token == undefined) return;
tokenControl.update(token.id);
}
Hooks.on('updateToken',(document,changes)=>{
if (enableModule == false || ready == false) return;
let tokenId = changes._id;
if (tokenId == canvas.tokens.controlled[0]?.id) tokenControl.update(canvas.tokens.controlled[0]?.id);
if (macroControl != undefined) macroControl.updateAll();
if (changes.hidden != undefined && combatTracker != undefined) combatTracker.updateAll();
});
Hooks.on('updateActor',(actor)=>{
if (enableModule == false || ready == false) return;
updateActor(actor.id);
if (macroControl != undefined) macroControl.updateAll();
});
Hooks.on('createActiveEffect',(data)=>{
if (enableModule == false || ready == false) return;
updateActor(data.parent.id);
return;
});
Hooks.on('deleteActiveEffect',(data)=>{
if (enableModule == false || ready == false) return;
updateActor(data.parent.id);
return;
});
Hooks.on('onActorSetCondition',(data)=>{
if (enableModule == false || ready == false) return;
updateActor(data.actor.id);
return;
});
Hooks.on('controlToken',(token,controlled)=>{
if (enableModule == false || ready == false) return;
if (controlled) {
tokenControl.update(token.id);
if (controlTokenTimer != undefined) {
clearTimeout(controlTokenTimer);
controlTokenTimer = undefined;
}
}
else {
controlTokenTimer = setTimeout(function(){tokenControl.update(canvas.tokens.controlled[0]?.id);},10)
}
if (macroControl != undefined) macroControl.updateAll();
});
Hooks.on('updateOwnedItem',()=>{
if (macroControl != undefined) macroControl.updateAll();
})
Hooks.on('renderHotbar', (hotbar)=>{
if (enableModule == false || ready == false) return;
if (macroControl != undefined) macroControl.hotbar(hotbar.macros);
});
Hooks.on('render', (app)=>{
if (enableModule == false || ready == false) return;
if (app.id == "hotbar" && macroControl != undefined) macroControl.hotbar(app.macros);
});
Hooks.on('renderCombatTracker',()=>{
if (enableModule == false || ready == false) return;
if (combatTracker != undefined) {
combatTracker.updateAll();
}
if (tokenControl != undefined) tokenControl.update(canvas.tokens.controlled[0]?.id);
});
Hooks.on('renderActorSheet',()=>{
if (enableModule == false || ready == false) return;
if (tokenControl != undefined) tokenControl.update();
});
Hooks.on('renderPlaylistDirectory', (playlistDirectory)=>{
if (enableModule == false || ready == false) return;
if (playlistControl != undefined) playlistControl.updateAll();
});
Hooks.on('closeplaylistConfigForm', (form)=>{
if (enableModule == false || ready == false) return;
if (form.template == "./modules/MaterialDeck/templates/playlistConfig.html")
playlistControl.updateAll();
});
Hooks.on('updatePlaylistSound', ()=>{
if (enableModule == false || ready == false) return;
if (playlistControl != undefined) playlistControl.updateAll();
});
Hooks.on('lightingRefresh',()=>{
if (enableModule == false || ready == false) return;
if (tokenControl != undefined) tokenControl.update();
});
Hooks.on('pauseGame',()=>{
if (enableModule == false || ready == false) return;
otherControls.updateAll();
});
Hooks.on('renderSidebarTab',(app)=>{
const options = {
sidebarTab: app.tabName,
renderPopout: app.popOut
}
if (enableModule == false || ready == false) return;
if (otherControls != undefined) otherControls.updateAll(options);
if (sceneControl != undefined) sceneControl.updateAll();
if (document.getElementsByClassName("roll-type-select")[0] != undefined)
document.getElementsByClassName("roll-type-select")[0].addEventListener('change',function(){
if (otherControls != undefined) otherControls.updateAll(options);
})
});
Hooks.on('closeSidebarTab',(app)=>{
const options = {
sidebarTab: app.tabName,
renderPopout: false
}
if (otherControls != undefined) otherControls.updateAll(options);
});
Hooks.on('changeSidebarTab',()=>{
if (enableModule == false || ready == false) return;
if (otherControls != undefined) otherControls.updateAll();
});
Hooks.on('updateScene',()=>{
if (enableModule == false || ready == false) return;
sceneControl.updateAll();
externalModules.updateAll();
otherControls.updateAll();
});
Hooks.on('renderSceneControls',()=>{
if (enableModule == false || ready == false || otherControls == undefined) return;
otherControls.updateAll();
externalModules.updateAll();
});
Hooks.on('targetToken',(user,token,targeted)=>{
if (enableModule == false || ready == false) return;
if (token.id == canvas.tokens.controlled[0]?.id) tokenControl.update(canvas.tokens.controlled[0]?.id);
});
Hooks.on('sidebarCollapse',()=>{
if (enableModule == false || ready == false) return;
otherControls.updateAll();
});
Hooks.on('renderCompendium',()=>{
if (enableModule == false || ready == false) return;
otherControls.updateAll();
});
Hooks.on('closeCompendium',()=>{
if (enableModule == false || ready == false) return;
otherControls.updateAll();
});
Hooks.on('renderCompendiumBrowser',()=>{
if (enableModule == false || ready == false) return;
otherControls.updateAll({renderCompendiumBrowser:true});
});
Hooks.on('closeCompendiumBrowser',()=>{
if (enableModule == false || ready == false) return;
otherControls.updateAll({renderCompendiumBrowser:false});
});
Hooks.on('renderJournalSheet',(sheet)=>{
if (enableModule == false || ready == false) return;
otherControls.updateAll({
hook:'renderJournalSheet',
sheet:sheet
});
});
Hooks.on('closeJournalSheet',(sheet)=>{
if (enableModule == false || ready == false) return;
otherControls.updateAll({
hook:'closeJournalSheet',
sheet:sheet
});
});
Hooks.on('gmScreenOpenClose',(html,isOpen)=>{
if (enableModule == false || ready == false) return;
externalModules.updateAll({gmScreen:isOpen});
});
Hooks.on('ShareVision', ()=>{
if (enableModule == false || ready == false) return;
externalModules.updateAll();
})
Hooks.on('NotYourTurn', ()=>{
if (enableModule == false || ready == false) return;
externalModules.updateAll();
})
Hooks.on('simple-calendar-date-time-change', ()=>{
if (enableModule == false || ready == false) return;
externalModules.updateAll();
})
Hooks.on('simple-calendar-clock-start-stop', ()=>{
if (enableModule == false || ready == false) return;
externalModules.updateAll();
})
Hooks.on('updateTile',()=>{
if (enableModule == false || ready == false) return;
externalModules.updateAll();
});
Hooks.once('init', ()=>{
//CONFIG.debug.hooks = true;
//registerSettings(); //in ./src/settings.js
});
Hooks.once('canvasReady',()=>{
ready = true;
});
Hooks.on("soundscape", (data) => {
externalModules.newSoundscapeData(data);
});
Hooks.on("globalAmbientVolumeChanged", (volume) => {
soundboard.ambientVolumeChanged(volume);
})
Hooks.on('updateMacro', () => {
if (enableModule == false || ready == false || macroControl == undefined) return;
macroControl.updateAll();
})
Hooks.on('globalPlaylistVolumeChanged', () => {
if (enableModule == false || ready == false || otherControls == undefined) return;
otherControls.updateAll();
})
Hooks.on('globalAmbientVolumeChanged', () => {
if (enableModule == false || ready == false || otherControls == undefined) return;
otherControls.updateAll();
})
Hooks.on('globalInterfaceVolumeChanged', () => {
if (enableModule == false || ready == false || otherControls == undefined) return;
otherControls.updateAll();
})
Hooks.on('MaterialDeck', (data) => {
if (data.type == 'setButton') {
//Get the buttonContext and device from streamDeck.buttonContext using data.buttonId
const buttonContext = streamDeck.buttonContext[0]?.buttons[data.button]?.context;
const device = streamDeck.buttonContext[0]?.device;
//Set icon on SD
streamDeck.setIcon(buttonContext, device, data.icon, data.options);
//Set text on SD
streamDeck.setTitle(data.text, buttonContext);
}
else {
//reserved for future MD hooks
}
});