// ==UserScript==
// @name More Ore - Enhanced Save
// @namespace https://syns.studio/more-ore/
// @version 1.1.2
// @description Automatically tries to export save to a file every 1+ hours (customizable)
// @author 123HD123
// @match https://syns.studio/more-ore/
// @icon https://syns.studio/more-ore/misc-tinyRock.22ef93dd.ico
// @require https://greasyfork.org/scripts/444840-more-ore-notification/code/More%20Ore%20-%20Notification+.user.js
// @grant none
// ==/UserScript==
(function () {
const MOD_NAME = "Enhanced Save";
const MOD_ID = "enhancedSave";
const MOD_STORAGE_DEFAULT = {
intervals: [],
mutationObservers: [],
settings: [{
id: "askBeforeExport",
text: "Ask before auto export",
value: true,
type: "checkbox"
},
{
id: "autoExportTo",
text: "Automatically export to",
value: "file",
options: ["file", "clipboard"],
type: "select"
},
{
id: "exportInterval",
text: "Export save every x hours",
value: 1,
type: "range"
}
]
};
window.mods = window.mods || {};
const Logger = NotificationPlus?.notify || console.log;
if (Object.keys(window.mods).includes(MOD_NAME))
return Logger(
"Warning: Mod named " + MOD_NAME + " has already been loaded",
3
);
NotificationPlus?.load(MOD_NAME);
window.mods[MOD_NAME] = window.mods[MOD_NAME] || Object.assign({}, MOD_STORAGE_DEFAULT);
const MOD_STORAGE = window.mods[MOD_NAME];
if (MOD_STORAGE.intervals != [])
MOD_STORAGE.intervals.forEach(interval => clearInterval(interval.id));
MOD_STORAGE.intervals = [];
MOD_STORAGE.settings = JSON.parse(localStorage.getItem(`${MOD_ID}_settings`)) ?? MOD_STORAGE_DEFAULT.settings.slice();
let update = false;
for(let setting of MOD_STORAGE_DEFAULT.settings) {
if (MOD_STORAGE_DEFAULT.settings.length != MOD_STORAGE.settings.length) {
update = true;
break;
}
for(let key of Object.keys(setting)) {
if (key == "value") continue;
let index = MOD_STORAGE_DEFAULT.settings.indexOf(setting);
if (setting[key] != MOD_STORAGE.settings[index][key] && typeof setting[key] != "object") update = true;
else if (typeof setting[key] == "object") {
for(let i in setting[key]) {
if (setting[key][i] != MOD_STORAGE.settings[index][key][i]) {
update = true;
break;
}
}
}
if (update) break;
}
if (update) break;
}
if (update) MOD_STORAGE.settings = MOD_STORAGE_DEFAULT.settings.slice();
localStorage.setItem(`${MOD_ID}_settings`, JSON.stringify(MOD_STORAGE.settings));
if (MOD_STORAGE.settings.find(setting => setting.id == "exportInterval").value != -1)
MOD_STORAGE.intervals.push(setInterval(requestExportSave, MOD_STORAGE.settings.find(setting => setting.id == "exportInterval")?.value * 60 * 60 * 1000));
MOD_STORAGE.requestExportSave = requestExportSave;
if (MOD_STORAGE.mutationObservers != [])
MOD_STORAGE.mutationObservers.forEach(mutationObserver => mutationObserver.disconnect());
MOD_STORAGE.mutationObservers = [];
let m = new MutationObserver(onSettings);
m.observe(document.querySelector(".page-container"), {childList: true});
MOD_STORAGE.mutationObservers.push(m);
//window.requestExportSave = requestExportSave;
function requestExportSave() {
if (MOD_STORAGE.settings.find(setting => setting.id == "askBeforeExport")?.value) {
let autoExportTo = MOD_STORAGE.settings.find(setting => setting.id == "autoExportTo")?.value;
utils.confirmModal(
MOD_NAME,
`<p>Do you want to export your save to ${autoExportTo == "file" ? "an external file" : "clipboard"}?<br><span style='font-size: 13px;'>(TIP: the time between auto exports can be modified in the settings)</span></p>`,
() => exportSave(true),
"Export", "Cancel",
410,
true,
true);
} else exportSave(true);
}
function exportSave(auto = false) {
let autoExportTo = MOD_STORAGE.settings.find(setting => setting.id == "autoExportTo")?.value;
document.querySelector(".save-game")?.click();
// Array.from(document.querySelector(".save-row").children).find(child => child.innerText == "save game")?.click(); // Save game
let save = localStorage.getItem("s"); // Get save
if (auto === true) {
if (autoExportTo == "file") {
// Download file
let a = document.createElement("a");
a.download = `MO - Auto Save ${utils.getTimeFormat()}.txt`;
a.href = "data:plain/text;charset=utf-8," + save;
a.click();
} else if (autoExportTo == "clipboard") {
navigator.clipboard.writeText(save);
}
} else {
// Download file
let a = document.createElement("a");
a.download = `MO - Save ${utils.getTimeFormat()}.txt`;
a.href = "data:plain/text;charset=utf-8," + save;
a.click();
}
}
function onSettings(mutationList, observer) {
for (let mutation of mutationList) {
if (mutation.addedNodes.keys().length == 0) return;
let node = mutation.addedNodes.item(0);
if (!node?.className?.toLowerCase() == "modal-wrapper") return;
// Insert elements
if (node?.querySelector(".modal-settings")) {
// Update volume
let volumeBar = Array.from(document.querySelectorAll(".setting-section")).find(setting => setting.children[0]?.innerText == "Volume").children[2];
window.volume = volumeBar.value;
volumeBar.addEventListener("change", (event) => volume = event.target.value);
insertSettings();
modifyExportSave();
} else if (node?.querySelector(".modal-importSave")) {
insertUploadSave(node);
}
}
}
function insertSettings() {
let settings = document.querySelector(".modal-settings");
if (!settings) return;
if (Array.from(document.querySelectorAll(".setting-section")).find(child => child.children[0]?.innerText == MOD_NAME)) return;
let settingSection = document.createElement("div");
settingSection.classList.add("setting-section");
let settingTitle = document.createElement("h3");
settingTitle.innerText = MOD_NAME;
settingSection.append(settingTitle);
for (let setting of MOD_STORAGE.settings) {
let settingRow = document.createElement("div");
settingRow.classList.add("row");
settingRow.innerHTML = `<p>${setting.text}</p><input type='${setting.type}'/>`;
switch(setting.type) {
case "range": {
let text = setting.text.replace(" x ", ` ${setting.value} `);
if (setting.value == -1) text = "Auto export is disabled";
if (setting.value == 1) text = setting.text.split(" ").slice(0, setting.text.split(" ").length-2).join(" ") + " hour";
settingRow.innerHTML = `<p>${text}</p>`;
break;
}
case "checkbox": {
settingRow.children[1].checked = setting.value;
break;
}
case "select": {
settingRow.innerHTML = `<p>${setting.text}</p><select>${
setting.options.map(option => `<option value="${option}">${option}</option>`).join("")
}</select>`;
break;
}
}
if (settingRow.children[1])
settingRow.children[1].onchange = e => {
switch(setting.type) {
case "checkbox":
setting.value = e.target.checked;
break;
default:
setting.value = e.target.value;
};
localStorage.setItem(`${MOD_ID}_settings`, JSON.stringify(MOD_STORAGE.settings));
};
settingSection.append(settingRow);
if (setting.type == "range") {
let settingSlider = document.createElement("input");
settingSlider.type = "range";
settingSlider.min = "-1";
settingSlider.max = "12";
settingSlider.step = "1";
settingSlider.value = setting.value == .5 ? 0 : setting.value;
settingSlider.style.width = "100%";
settingSlider.onchange = _ => {
setting.value = parseInt(settingSlider.value) || .5;
let text = setting.text.replace(" x ", ` ${setting.value} `);
if (setting.value == -1) text = "Auto export is disabled";
if (setting.value == 1) text = setting.text.split(" ").slice(0, setting.text.split(" ").length-2).join(" ") + " hour";
settingRow.innerHTML = `<p>${text}</p>`;
localStorage.setItem(`${MOD_ID}_settings`, JSON.stringify(MOD_STORAGE.settings));
clearInterval(MOD_STORAGE.intervals[0]);
MOD_STORAGE.intervals = [];
if (setting.value == -1) return;
MOD_STORAGE.intervals.push(setInterval(requestExportSave, setting.value * 60 * 60 * 1000));
};
settingSection.append(settingSlider);
}
}
settings.querySelector(".modal-content").insertBefore(settingSection, Array.from(settings.querySelector(".modal-content").children).find(section => section.querySelector("h3")?.innerText == "Saves"));
}
function modifyExportSave() {
let savesSection = Array.from(document.querySelectorAll(".setting-section").values())
.find(section => section.children[0].innerText == "Saves");
let exportFile = savesSection.children[1].children[0];
let exportClipboard = savesSection.children[2].children[0];
exportFile.innerHTML = "export (file)";
exportFile.onclick = exportSave;
exportClipboard.innerHTML = "export (clipboard)";
exportClipboard.onclick = () => {
document.querySelector(".save-game")?.click();
//Array.from(document.querySelector(".save-row").children).find(child => child.innerText == "save game")?.click(); // Save game
navigator.clipboard.writeText(localStorage.getItem("s"));
};
/*a.onclick = () => utils.choiceModal(
MOD_NAME,
"<p>Would you like to export to <b>file</b> or <b>clipboard</b>?</p>",
{text: "Clipboard", callback: () => {
Array.from(document.querySelector(".save-row").children).find(child => child.innerText == "save game")?.click(); // Save game
navigator.clipboard.writeText(localStorage.getItem("s"));
}},
{text: "File", callback: exportSave},
410,
true);*/
}
function insertUploadSave(node) {
if (node.querySelector("#file")) return;
let button = document.createElement("button");
button.className = "modal-action";
button.style.padding = "0";
let label = document.createElement("label");
label.htmlFor = "file";
label.style.cursor = "pointer";
label.style.padding = "2px 8px";
label.innerText = "Import File";
button.append(label);
let file = document.createElement("input");
file.id = "file";
file.type = "file";
file.name = "file";
file.className = "modal-action";
file.style.display = "none";
file.addEventListener('change', function() {
var fr = new FileReader();
fr.onload = function() {
node.querySelector('.modal-content > textarea').innerText = fr.result;
}
fr.readAsText(this.files[0]);
});
document.querySelector(".modal-importSave > .modal-actions").insertBefore(file, document.querySelector(".modal-importSave > .modal-actions").children[1]);
document.querySelector(".modal-importSave > .modal-actions").insertBefore(button, file);
}
const utils = {
confirmModal: function (title, innerHTML, onConfirm) {
//this.closeTopmostModal();
var confirmMessage =
arguments.length > 3 && void 0 !== arguments[3] ?
arguments[3] :
'Confirm',
cancelMessage =
arguments.length > 4 && void 0 !== arguments[4] ? arguments[4] : 'Cancel',
customWidth = arguments.length > 5 ? arguments[5] : void 0,
center = arguments.length > 6 ? arguments[6] : void 0,
nonBlocking = arguments.length > 7 ? arguments[7] : void 0;
let wrapper = this.createEl('div', ['modal-wrapper']);
if (nonBlocking) {
wrapper.style.margin = "auto";
wrapper.style.width = "fit-content";
wrapper.style.height = "fit-content";
wrapper.style.background = "transparent";
}
wrapper.addEventListener('click', this.closeTopmostModal);
var modal = this.createEl('div', [
'modal'
]);
customWidth && (modal.style.width = customWidth + 'px');
center && (modal.style.textAlign = 'center');
var titleElement = this.createEl('h2', ['modal-title'], title);
modal.append(titleElement);
var contentElement = this.createEl('div', ['modal-content']);
contentElement.innerHTML = innerHTML;
modal.append(contentElement);
var actionsContainer = this.createEl('div', ['modal-actions']),
confirmAction = this.createEl('button', ['modal-action'], confirmMessage);
confirmAction.onclick = function () {
this.closeTopmostModal();
onConfirm();
}.bind(this);
var cancelAction = this.createEl('button', ['modal-action'], cancelMessage);
cancelAction.onclick = function () {
this.closeTopmostModal();
}.bind(this);
actionsContainer.append(cancelAction, confirmAction);
modal.append(actionsContainer);
wrapper.append(modal);
document.querySelector(".page-container").append(wrapper);
},
choiceModal: function (title, innerHTML, leftButton, rightButton) {
//this.closeTopmostModal();
var leftMessage =
leftButton.text,
rightMessage =
rightButton.text,
customWidth = arguments.length > 4 ? arguments[4] : void 0,
center = arguments.length > 5 ? arguments[5] : void 0;
let wrapper = this.createEl('div', ['modal-wrapper']);
wrapper.addEventListener('click', this.closeTopmostModal);
var modal = this.createEl('div', [
'modal'
]);
customWidth && (modal.style.width = customWidth + 'px');
center && (modal.style.textAlign = 'center');
var titleElement = this.createEl('h2', ['modal-title'], title);
modal.append(titleElement);
var contentElement = this.createEl('div', ['modal-content']);
contentElement.innerHTML = innerHTML;
modal.append(contentElement);
var actionsContainer = this.createEl('div', ['modal-actions']),
leftAction = this.createEl('button', ['modal-action'], leftMessage);
leftAction.onclick = function () {
this.closeTopmostModal();
leftButton.callback();
}.bind(this);
var rightAction = this.createEl('button', ['modal-action'], rightMessage);
rightAction.onclick = function () {
this.closeTopmostModal();
rightButton.callback();
}.bind(this);
actionsContainer.append(rightAction, leftAction);
modal.append(actionsContainer);
wrapper.append(modal);
document.querySelector(".page-container").append(wrapper);
},
createEl: function (tag) {
var classes =
arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : [],
innerHTML =
arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : '',
element = document.createElement(tag);
return (
classes.forEach(function (clazz) {
return element.classList.add(clazz)
}),
(element.innerHTML = innerHTML + ''),
element
);
},
getCodeName: function (name) {
return name
.replace(/(?:^\w|[A-Z]|\b\w)/g, function (match, p1) {
return 0 === p1 ? match.toLowerCase() : match.toUpperCase()
})
.replace(/\s+/g, '')
.replace(/\./g, '');
},
getTimeFormat: function() {
let d = new Date();
let dateString = d.toLocaleDateString().replaceAll("/", "-");
let hours = d.getHours().toString().padStart(2, '0');
let minutes = d.getMinutes().toString().padStart(2, '0');
let seconds = d.getSeconds().toString().padStart(2, '0');
return `${dateString}-${hours}.${minutes}.${seconds}`;
},
closeTopmostModal: function (event) {
var targetElement;
if (event) {
var targetEl = event.target;
targetElement = targetEl.classList.contains('modal-wrapper') ? targetEl : null;
} else {
var allWrappers = document.querySelectorAll('.modal-wrapper');
allWrappers.length > 0 && (targetElement = allWrappers[allWrappers.length - 1]);
}
if (targetElement) {
targetElement.style.pointerEvents = 'none';
new Howl({
src: ['./sounds/misc.wav'],
volume: .1 * volume
}).play();
var modal = targetElement.children[0];
modal.style.animation = 'fadeOutDown .15s ease-out forwards';
modal.addEventListener('animationend', () => targetElement.parentNode.removeChild(targetElement));
}
}
};
})();