// ==UserScript==
// @name Pavlov mod.io maps integration with pavlovrcon.com
// @namespace https://greasyfork.org/en/users/1103172-underpl
// @version 3.0
// @description Add extra buttons to mod.io pavlov map pages to allow switching to the map or adding it to the rotation of a server by using pavlovrcon.com
// @author UnderPL
// @license CC BY-NC 4.0
// @require https://greasyfork.org/scripts/426194-toast-js/code/toastjs.js?version=971661
// @match https://mod.io/g/pavlov
// @match https://mod.io/g/pavlov/m/*
// @match https://mod.io/g/pavlov?_sort=*
// @match https://mod.io/g/pavlov?tags-in=*
// @match https://mod.io/g/pavlov?platforms*
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript==
(function() {
'use strict';
var currentPage = window.location.href;
var defaultGameMode = "INFECTION";
var storedGameMode = GM_getValue("storedGameMode", defaultGameMode);
var defaultServerId = "0";
var storedServerId = GM_getValue("storedServerId", defaultServerId);
var mapList = {};
var storedMapList = GM_getValue("storedMapList", mapList);
const mapsPageRegex = new RegExp("^https:\/\/mod\.io\/g\/pavlov(?:$|\\?tags-in=|\\?_sort=|\\?platforms)");
//To use with an HTTP server/proxy that can forward the request to the server
var httpProxy = false;
var httpProxyIp = "192.168.0.150"
var httpProxyPort = "3000"
const headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/114.0",
"Accept": "*/*",
"Accept-Language": "en-US,en;q=0.5",
"Content-Type": "application/json"
}
var retrievedMapList = false;
function retrieveMapList() {
GM_xmlhttpRequest({
method: "GET",
url: httpProxy ? `http://${httpProxyIp}:${httpProxyPort}/rcon?command=maps` : `https://pavlovrcon.com/api/maplist/${storedServerId}`,
headers: headers,
onload: function(response) {
if (response.status >= 200 && response.status < 400) {
var data = httpProxy ? JSON.parse(response.responseText) : response.responseText;
var mapListData = httpProxy ? data.MapList : JSON.parse(data);
mapListData.forEach(function(item) {
let mapId = httpProxy ? item.MapId : item.id;
let gameMode = httpProxy ? item.GameMode : item.game_mode;
if (mapId.startsWith("UGC")) {
mapId = mapId.substring(3);
if(!mapList[mapId]) {
mapList[mapId] = [];
}
mapList[mapId].push(gameMode);
}
});
retrievedMapList = true;
} else {
console.log("Command failed with status: " + response.status);
}
},
onerror: function() {
console.log("Request failed");
}
});
}
function createButton(id, color, text, targetButton) {
var newButton = targetButton.cloneNode(true);
newButton.id = id;
newButton.querySelector('span div span').textContent = text;
newButton.style.cssText = 'border-color: ' + color + ';margin-bottom: 10px;--primary-hover:' + color;
newButton.addEventListener('mouseover', function() {
newButton.querySelector('span div span').style.color = color;
});
newButton.addEventListener('mouseout', function() {
newButton.querySelector('span div span').style.color = 'inherit';
});
return newButton;
};
function confirmAction(mapName, mapId, selectedGameMode, action) {
var confirmationMessage = `Are you sure you want to ${action === 'addmaprotation' ? 'add' : 'remove'} "${mapName}" ${action === 'addmaprotation' ? 'to' : 'from'} the map rotation?\n
MapRotation=(MapId="${mapId}",GameMode="${selectedGameMode}")`;
return window.confirm(confirmationMessage);
}
function createSelectsAndLabels() {
var container = document.createElement('div');
container.style.cssText = 'justify-content:center;' + (currentPage.startsWith('https://mod.io/g/pavlov/m/') ? 'display:flex;' : "");
var serverIdDiv = document.createElement('div');
var serverIdLabel = document.createElement('label');
serverIdLabel.innerHTML = 'SERVER ID:';
serverIdLabel.style.cssText = 'font-size: 150%; margin: 0px 10px;';
serverIdDiv.appendChild(serverIdLabel);
var serverIdSelect = document.createElement('select');
serverIdSelect.id = "serverIds";
serverIdSelect.classList.add("form-select");
serverIdSelect.style.cssText = "color: black; font-size: 155%; padding-left: 5px; text-align: center; display: inline-block; margin: 0 auto;";
serverIdDiv.appendChild(serverIdSelect);
serverIdSelect.innerHTML = Array.from({length: 10}, (_, i) => i)
.map(id => `<option value="${id}" ${storedServerId == id ? 'selected' : ''}>${id}</option>`).join('');
serverIdSelect.addEventListener('change', function() {
GM_setValue("storedServerId", serverIdSelect.value);
});
var gameModeDiv = document.createElement('div');
var gameModeLabel = document.createElement('label');
gameModeLabel.innerHTML = 'MODE:';
gameModeLabel.style.cssText = 'font-size: 150%; margin: 0px 10px;';
gameModeDiv.appendChild(gameModeLabel);
var gameModeSelect = document.createElement('select');
gameModeSelect.id = "gameModes";
gameModeSelect.classList.add("form-select");
gameModeSelect.style.cssText = "color: black; font-size: 155%; padding-left: 5px; text-align: center; display: inline-block; margin: 0 auto;";
gameModeDiv.appendChild(gameModeSelect);
gameModeSelect.innerHTML = ['SND', 'TDM', 'DM', 'GUN', 'ZWV', 'WW2GUN', 'TANKTDM', 'KOTH', 'TTT', 'OITC', 'INFECTION', 'HIDE', 'PUSH', 'PH', 'CUSTOM']
.map(mode => `<option value="${mode}" ${storedGameMode === mode ? 'selected' : ''}>${mode}</option>`).join('');
gameModeSelect.addEventListener('change', function() {
storedGameMode = gameModeSelect.value;
GM_setValue("storedGameMode", storedGameMode);
});
container.appendChild(serverIdDiv);
container.appendChild(gameModeDiv);
return container;
};
function mapAction(mapId, selectedGameMode, action, callback) {
var body = JSON.stringify({
data: selectedGameMode,
"Map Name": "UGC" + mapId,
uid: null
});
var requestUrl;
if (httpProxy) {
requestUrl = `http://${httpProxyIp}:${httpProxyPort}/rcon?command=${action} UGC${mapId} ${selectedGameMode}`;
} else {
requestUrl = `https://pavlovrcon.com/api/${action}/${storedServerId}`;
}
GM_xmlhttpRequest({
method: httpProxy ? "GET" : "POST",
url: httpProxy ?
`http://${httpProxyIp}:${httpProxyPort}/rcon?command=${action} UGC${mapId} ${selectedGameMode}` :
`https://pavlovrcon.com/api/${action}/${storedServerId}`,
headers: headers,
data: body,
onload: function(response) {
if (response.status >= 200 && response.status < 400) {
cocoMessage.success(1500, "Command sent successfully", () => {
});
if(callback) {
callback(true);
}
} else {
cocoMessage.error(1500, "Command failed with status: " + response.status, () => {
});
if(callback) {
callback(false);
}
}
}
});
}
function addButtonsWithEventListeners(mapId, subscribeButton, specificMapPage = false) {
var playButton = createButton("playButton", "green", "Play", subscribeButton);
playButton.style.marginBottom = "10px";
playButton.style.marginTop = "8px";
var mapRotationButtonsContainer = document.createElement('div');
mapRotationButtonsContainer.style.cssText = 'display: flex; justify-content: space-between;';
var addToMapRotationButton = createButton("addToMapRotationButton", "blue", "Add to map rotation", subscribeButton);
var removeFromMapRotationButton = createButton("removeFromMapRotationButton", "blue", "Remove from map rotation", addToMapRotationButton);
addToMapRotationButton.style.width = "49%";
addToMapRotationButton.querySelector('span div span').style.fontSize = "75%";
removeFromMapRotationButton.style.width = "49%";
removeFromMapRotationButton.querySelector('span div span').style.fontSize = "75%";
mapRotationButtonsContainer.appendChild(addToMapRotationButton);
mapRotationButtonsContainer.appendChild(removeFromMapRotationButton);
subscribeButton.parentNode.insertBefore(playButton, subscribeButton);~
subscribeButton.parentNode.insertBefore(mapRotationButtonsContainer, playButton.nextElementSibling);
if(specificMapPage){
addSelectElement(playButton, 'before');
}
playButton.addEventListener('click', function(e) {
mapAction(mapId, GM_getValue("storedGameMode", selectElementContainer.querySelectorAll('select')[1].value), "switch_map");
});
var mapName;
addToMapRotationButton.addEventListener('click', function(e) {
var mapName;
var targetDiv;
if(specificMapPage) {
mapName = document.querySelector('.tw-util-truncate-two-lines.tw-font-bold').textContent;
targetDiv = document.querySelector('.tw-util-ratio-16-9.tw-relative.tw-w-full.tw-global--border-radius.tw-bg-theme-1.tw-overflow-hidden');
} else {
mapName = subscribeButton.parentNode.parentNode.parentNode.querySelectorAll('a > div')[1].textContent;
targetDiv = subscribeButton.parentNode.parentNode.parentNode;
}
var gameMode = GM_getValue("storedGameMode", selectElementContainer.querySelectorAll('select')[1].value);
if (confirmAction(mapName, mapId, gameMode, "add")) {
mapAction(mapId, gameMode, "addmaprotation");
addOverlayAndSpan(targetDiv, gameMode, mapId);
}
});
removeFromMapRotationButton.addEventListener('click', function(e) {
var mapName;
var targetDiv
if(specificMapPage) {
mapName = document.querySelector('.tw-util-truncate-two-lines.tw-font-bold').textContent;
targetDiv = document.querySelector('.tw-util-ratio-16-9.tw-relative.tw-w-full.tw-global--border-radius.tw-bg-theme-1.tw-overflow-hidden');
} else {
mapName = subscribeButton.parentNode.parentNode.parentNode.querySelectorAll('a > div')[1].textContent;
targetDiv = subscribeButton.parentNode.parentNode.parentNode;
}
var gameMode = GM_getValue("storedGameMode", selectElementContainer.querySelectorAll('select')[1].value);
if (confirmAction(mapName, mapId, gameMode, "remove")) {
mapAction(mapId, gameMode, "removemaprotation");
var overlayToRemove = targetDiv.querySelector(`#o-${mapId}-${gameMode}`);
if(overlayToRemove) {
overlayToRemove.remove();
}
}
});
}
function extractMapIdFromDiv(divsContainingMapId) {
const mapIds = [];
for (let i = 0; i < divsContainingMapId.length; i++) {
const style = divsContainingMapId[i].getAttribute('style');
const urlMatch = style.match(/url\("([^"]+)"\)/);
if (urlMatch) {
const url = urlMatch[1];
const mapIdMatch = url.match(/\/(\d+)\//);
if (mapIdMatch) {
mapIds.push(mapIdMatch[1]);
}
}
}
return mapIds;
}
let addedMaps = new Set();
function processMapDivs(divsContainingMapId) {
const subscribeButtons = document.querySelectorAll('button.tw-button-transition.tw-outline-none.tw-shrink-0.tw-items-center.tw-justify-center.tw-space-x-2.tw-font-bold.tw-bg-theme-1--hover.tw-text-md[id^="input"]');
const mapIds = extractMapIdFromDiv(divsContainingMapId);
for (let i = 0; i < mapIds.length; i++) {
const mapId = mapIds[i];
if (addedMaps.has(mapId)) {
continue;
}
addButtonsWithEventListeners(mapId, subscribeButtons[i]);
addedMaps.add(mapId);
}
}
function addControlsToSpecificMapPage() {
var subscribeButton = document.querySelector('button.tw-button-transition.tw-outline-none.tw-shrink-0.tw-items-center.tw-justify-center.tw-space-x-2.tw-font-bold.tw-bg-theme-1--hover.tw-text-md[id^="input"]');
if (!subscribeButton || document.getElementById('playButton') || document.getElementById('addToMapRotationButton')) {
return;
}
var mapIdElement = document.querySelector('div.tw-justify-between.tw-items-center.tw-flex:last-child > span.tw-whitespace-nowrap:last-child > span');
var mapId = mapIdElement.textContent;
addButtonsWithEventListeners(mapId, subscribeButton, true);
addGameModeInServerRotationTags(observerForSpecificMapPage, null, mapId);
};
function addOverlayAndSpan(targetDiv, gameMode, mapId) {
var tagsDiv = targetDiv.querySelector('.tags-div');
if (!tagsDiv) {
tagsDiv = document.createElement('div');
tagsDiv.className = 'tags-div tw-flex tw-space-x-1 tw-items-center';
tagsDiv.style.cssText = 'flex-direction:column;align-items: flex-end; position: absolute; right: 0;';
targetDiv.appendChild(tagsDiv);
}
var overlayDiv = document.createElement('div');
overlayDiv.id = `o-${mapId}-${gameMode}`;
overlayDiv.style = 'width: 100%; height: 100%; z-index: 1; text-align: right; margin-bottom:1px;';
overlayDiv.onclick = e => e.preventDefault();
var spanElement = document.createElement('span');
spanElement.id = `t-${mapId}-${gameMode}`;
spanElement.className = `ttw-px-2 tw-z-2 tw-bg-primary tw-rounded-full tw-text-primary-text tw-leading-snug tw-max-w-full`;
spanElement.style.cssText = 'background-color: blue; border:2px solid red; padding:0px 4px';
var pElement = document.createElement('p');
pElement.className = 'tw-util-truncate-one-line tw-w-full';
pElement.style = 'display:inline;';
pElement.textContent = `${gameMode}`;
spanElement.appendChild(pElement);
overlayDiv.appendChild(spanElement);
tagsDiv.appendChild(overlayDiv);
}
function addGameModeInServerRotationTags(observer, divsContainingMapId, mapId = null) {
observer.disconnect();
var mapIds = mapId ? [mapId] : extractMapIdFromDiv(divsContainingMapId);
var targetDivs = mapId ? document.querySelectorAll('.tw-util-ratio-16-9.tw-relative.tw-w-full.tw-global--border-radius.tw-bg-theme-1.tw-overflow-hidden')
: document.querySelectorAll('a .tw-relative.tw-util-ratio-16-9');
let mapGameModeCounters = {};
for(let i = 0; i < mapIds.length; i++) {
var mapId = mapIds[i];
if(mapList[mapId] && targetDivs[i]) {
var tagsDiv = targetDivs[i].querySelector('.tags-div');
if(!tagsDiv) {
tagsDiv = document.createElement('div');
tagsDiv.className = 'tags-div tw-flex tw-space-x-1 tw-items-center';
tagsDiv.style.cssText = 'flex-direction:column;align-items: flex-end; position: absolute; right: 0;';
targetDivs[i].appendChild(tagsDiv);
}
mapList[mapId].forEach((gameMode) => {
let currentMapId = mapId;
let currentGameMode = gameMode;
const mapGameModeKey = `${mapId}-${gameMode}`;
if(!mapGameModeCounters[mapGameModeKey]){
mapGameModeCounters[mapGameModeKey] = 0;
}
var overlayDiv = document.createElement('div');
overlayDiv.id = `o-${mapId}-${gameMode}-${mapGameModeCounters[mapGameModeKey]}`;
overlayDiv.style = 'width: 100%; height: 100%; z-index: 1; text-align: right;';
overlayDiv.onclick = e => e.preventDefault();
const spanId = `t-${mapId}-${gameMode}-${mapGameModeCounters[mapGameModeKey]++}`;
if (!tagsDiv.querySelector(`#${spanId}`)) {
var spanElement = document.createElement('span');
spanElement.id = spanId;
spanElement.className = `ttw-px-2 tw-z-2 tw-bg-primary tw-rounded-full tw-text-primary-text tw-leading-snug tw-max-w-full`;
spanElement.style.cssText = 'background-color: blue; border:2px solid red; padding:0px 4px';
var pElement = document.createElement('p');
pElement.className = 'tw-util-truncate-one-line tw-w-full';
pElement.style = 'display:inline;';
pElement.textContent = `${gameMode}`;
spanElement.appendChild(pElement);
overlayDiv.appendChild(spanElement);
tagsDiv.appendChild(overlayDiv);
spanElement.addEventListener('click', function(e) {
e.preventDefault();
if (confirmAction(currentGameMode, currentMapId, gameMode, "removemaprotation")) {
mapAction(currentMapId, gameMode, "removemaprotation", function(success) {
if (success) {
let index = mapList[currentMapId].indexOf(currentGameMode);
if (index !== -1) {
mapList[currentMapId].splice(index, 1);
}
document.querySelector(`#${overlayDiv.id}`).remove();
}
});
}
});
}
});
}
}
observer.observe(document.body, { childList: true, subtree: true });
}
function adjustElementStylesForMapsPage() {
const elements = document.querySelectorAll('.tw-flex.tw-items-center.tw-flex-col.tw-absolute.tw-bottom-3.lg\\:tw-bottom-4.tw-inset-x-3.lg\\:tw-inset-x-4');
elements.forEach((element) => {
element.classList.remove('tw-absolute');
});
const elementsTwo = document.querySelectorAll('.tw-px-3.lg\\:tw-px-4.tw-pb-14.lg\\:tw-pb-\\[3\\.75rem\\]');
elementsTwo.forEach((element) => {
element.classList.remove('tw-pb-14');
element.classList.remove('lg:tw-pb-[3.75rem]');
element.classList.add('tw-pb-4');
});
}
function addSelectElement(targetElement, position) {
position === "before" ? targetElement.before(selectElementContainer) : targetElement.after(selectElementContainer);
}
function findTargetElementAndAddSelectElement(targetElementSelector, position) {
var checkExist = setInterval(function() {
var targetElement = document.getElementsByClassName(targetElementSelector)[1]
if (targetElement) {
addSelectElement(targetElement, position);
clearInterval(checkExist);
}
}, 500);
}
var selectElementContainer = createSelectsAndLabels();
retrieveMapList();
if (mapsPageRegex.test(currentPage)) {
var targetElementSelector = 'md:tw-rounded-lg md:dark:tw-bg-dark-1 md:tw-bg-light-1 tw-border-opacity-40 tw-border-grey md:tw-border-0 tw-border-b md:tw-mb-2 tw-relative';
var targetElement = document.getElementsByClassName(targetElementSelector)[1]
findTargetElementAndAddSelectElement(targetElementSelector, 'after');
var observerForAllMapsPage = new MutationObserver(function(mutationsList) {
const divsContainingMapId = document.querySelectorAll('div.tw-bg-center.tw-bg-cover.tw-w-full.tw-h-full[role="img"]');
for (var mutation of mutationsList) {
if (mutation.type === 'childList') {
processMapDivs(divsContainingMapId);
adjustElementStylesForMapsPage();
addGameModeInServerRotationTags(observerForAllMapsPage, divsContainingMapId)
break;
}
}
});
observerForAllMapsPage.observe(document.body, { childList: true, subtree: true });
} else {
var observerForSpecificMapPage = new MutationObserver(function(mutationsList) {
for (var mutation of mutationsList) {
if (mutation.type === 'childList') {
addControlsToSpecificMapPage();
break;
}
}
});
observerForSpecificMapPage.observe(document.body, { childList: true, subtree: true });
}
})();