// ==UserScript==
// @name ButtEOS
// @namespace Violentmonkey Scripts
// @grant none
// @match *://milovana.com/webteases/showtease.php
// @match *://milovana.com/eos/editor/*
// @match *://eosscript.com/*
// @license BSD
// @version 1.1
// @author cfs6t08p
// @description 2/21/2022, 9:26:31 PM
// ==/UserScript==
/* jshint esversion: 8 */
function mod(a, b) {
return ((a % b) + b) % b;
}
function actionIndex(pattern, time) {
for(let a = 0; a < pattern.numActions; a++) {
if(pattern.actions[a].at > time) {
return a;
}
}
}
function positionAt(pattern, time, index) {
if(pattern.actions[index].at > time) {
let a1 = pattern.actions[mod((index - 1), pattern.numActions)];
let a2 = pattern.actions[index];
let a1Wrap = mod(a1.at, pattern.patternLength);
let dp = a2.pos - a1.pos;
let dt = a2.at - a1Wrap;
let alpha = (time - a1Wrap) / dt;
return 99 - (a1.pos + alpha * dp);
}
}
function vibe(level) {
window.parent.postMessage({buttEOS: true, vib: {level: level}}, "https://milovana.com/webteases/*");
}
function linear(position, duration) {
window.parent.postMessage({buttEOS: true, linear: {position: position, duration: duration }}, "https://milovana.com/webteases/*");
}
if(document.getElementById("eosContainer")) {
let eos = document.getElementById("eosContainer");
let bod = document.body;
let div = document.createElement("div");
div.style = "position: absolute; left: 20px; top: 40px; width: 160px; z-index: 100000";
bod.append(div);
let bar = document.createElement("div");
let fill = document.createElement("div");
let arrow = document.createElement("div");
let line = document.createElement("div");
let text = document.createElement("div");
bar.style = "position: absolute; left: 20px; height: 80%; bottom: 10%; width: 50px; border-top-left-radius: 25px; border-top-right-radius: 25px; background-color: #ffffff20; visibility: hidden;";
fill.style = "position: absolute; left: 7px; width: 36px; bottom: 0px; background-color: #bb55bbcc;";
arrow.style = "position: absolute; left: 7px; width: 36px; height: 18px; border-top-left-radius: 18px; border-top-right-radius: 18px; background-color: #bb55bbcc;";
line.style = "position: absolute; width: 100%; height: 4px; bottom: 15%; background-color: #ffff00cc;";
text.style = "position: absolute; width: 100%; height: 10%; bottom: 0px; color: white; padding-top: 5px;";
bar.append(fill);
bar.append(arrow);
bar.append(line);
div.append(bar);
div.append(text);
let currentPattern = {};
let lastPatternName;
let lastBPM;
let bpmPattern;
let patternStart;
let prevActionIndex;
let newPatterns = 0;
let vibeLevel;
let patterns = {};
setInterval(() => {
let xpath = ".//p[contains(text(),'Load pattern:')]";
let result = document.evaluate(xpath, eos, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
if(result.snapshotLength == 0) {
newPatterns = 0;
}
for(let i = 0; i < result.snapshotLength; i++) {
let node = result.snapshotItem(i);
let name = node.textContent.slice(13).trim();
let data = node.parentNode.childNodes;
if(data.length >= 3) {
let text = data[1].textContent;
let funscript = "";
for(let l = 2; l < data.length; l++) {
funscript = funscript + data[l].textContent;
}
if(patterns[name] === undefined) {
patterns[name] = {};
try {
let pattern = JSON.parse(funscript);
pattern.valid = true;
pattern.text = text;
pattern.numActions = pattern.actions.length;
pattern.patternLength = pattern.actions[pattern.numActions - 1].at;
let time = 0;
for(let a = 0; a < pattern.numActions; a++) {
let at = pattern.actions[a].at;
pattern.actions[a].dur = at - time;
time = at;
}
patterns[name] = pattern;
newPatterns++;
console.log(pattern);
} catch(error) {
console.error("Failed to load pattern \"" + name + "\"");
console.error(error);
}
}
}
}
if(newPatterns > 0) {
text.innerText = "Loaded " + newPatterns + " pattern(s)";
}
}, 100);
setInterval(() => {
let now = Date.now();
let h = (eos.clientHeight - 40) * 0.7;
div.style.height = h + "px";
let vibePath = ".//div[contains(text(),'Vibrator:')]";
let vibeNotification = document.evaluate(vibePath, eos, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
let newVibe = vibeLevel;
if(vibeNotification) {
newVibe = parseInt(vibeNotification.textContent.slice(9));
} else {
newVibe = 0;
}
if((newVibe != vibeLevel) && !(Number.isNaN(newVibe) && Number.isNaN(vibeLevel))) {
vibeLevel = newVibe;
if(Number.isNaN(vibeLevel) || vibeLevel > 100 || vibeLevel < 0) {
console.error("Invalid vibrator level: \"" + vibeLevel + "\"");
vibe(0);
} else {
vibe(vibeLevel);
}
}
let pattern;
let patternPath = ".//div[contains(text(),'Pattern:')]";
let patternNotification = document.evaluate(patternPath, eos, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
if(patternNotification) {
let name = patternNotification.textContent.slice(8).trim();
pattern = patterns[name];
if(lastPatternName != name) {
lastPatternName = name;
if(!pattern) {
console.error("Pattern \"" + name + "\" not found");
}
}
}
let bpmPath = ".//div[contains(text(),'BPM:')]";
let bpmNotification = document.evaluate(bpmPath, eos, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
if(bpmNotification) {
let bpm = parseInt(bpmNotification.textContent.slice(4));
if((bpm != lastBPM) && !(Number.isNaN(bpm) && Number.isNaN(lastBPM))) {
lastBPM = bpm;
if(Number.isNaN(bpm) || bpm <= 0 || bpm > 600) {
console.error("Invalid BPM: \"" + bpm + "\"");
bpmPattern = undefined;
} else {
let period = (60 * 1000) / bpm;
pattern = {valid: true, numActions: 2, patternLength: period, text: "", actions: [{at: period / 2, pos: 0, dur: period / 2},{at: period, pos: 100, dur: period / 2}]};
bpmPattern = pattern;
}
} else {
pattern = bpmPattern;
}
}
if(pattern != currentPattern) {
currentPattern = pattern;
patternStart = now;
prevActionIndex = -1;
if(pattern) {
text.innerText = pattern.text;
bar.style.visibility = "visible";
} else {
text.innerText = "";
bar.style.visibility = "hidden";
}
newPatterns = 0;
}
if(currentPattern !== undefined && currentPattern.valid) {
let patternTime = mod(now - patternStart, currentPattern.patternLength);
let index = actionIndex(currentPattern, patternTime);
if(index != prevActionIndex) {
linear(currentPattern.actions[index].pos, currentPattern.actions[index].dur);
prevActionIndex = index;
}
let fillHeight = ((bar.clientHeight - 25) * positionAt(currentPattern, patternTime, index)) / 100;
fill.style.height = fillHeight + "px";
arrow.style.bottom = fillHeight + "px";
}
}, 10);
}
if(document.querySelector(".eosTopBody")) {
window.addEventListener("message", (event) => {
if(event.data.buttEOS) {
if(window.buttplug_devices) {
if(event.data.vib) {
window.buttplug_devices.forEach((device) => {
if(device.messageAttributes(Buttplug.ButtplugDeviceMessageType.VibrateCmd)) {
device.vibrate(event.data.vib.level / 100);
}
});
}
if(event.data.linear) {
window.buttplug_devices.forEach((device) => {
if(device.messageAttributes(Buttplug.ButtplugDeviceMessageType.LinearCmd)) {
device.linear(event.data.linear.position / 100, event.data.linear.duration);
}
});
}
}
event.stopImmediatePropagation();
}
});
let bpscript= document.createElement("script");
bpscript.src = "https://cdn.jsdelivr.net/npm/buttplug@1.0.17/dist/web/buttplug.min.js";
document.body.append(bpscript);
window.addEventListener("load", function (e) {
let style = document.createElement("style");
style.innerHTML = `
#buttplug-top-container h3, li {
font-family:Arial;
font-size:15px;
}
#buttplug-top-container ul {
list-style-type: none;
column-count: 2;
}
.buttplug-button {
box-shadow:inset 0px 1px 3px 0px #91b8b3;
background:linear-gradient(to bottom, #768d87 5%, #6c7c7c 100%);
background-color:#768d87;
border-radius:5px;
border:1px solid #566963;
display:inline-block;
cursor:pointer;
color:#ffffff;
font-family:Arial;
font-size:15px;
font-weight:bold;
padding:11px 23px;
text-decoration:none;
text-shadow:0px -1px 0px #2b665e;
margin: 5px;
}
.buttplug-button:hover {
background:linear-gradient(to bottom, #6c7c7c 5%, #768d87 100%);
background-color:#6c7c7c;
}
.buttplug-button:active {
position:relative;
top:1px;
}
#buttplug-top-container {
position: fixed;
top: 0;
right: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: rgba(0, 0, 0, 0.7);
display: none;
}
#buttplug-dialog {
width: 50%;
min-height: 200px;
position: absolute;
top: 10%;
left: 0;
left: 0;
right: 0;
margin: auto;
background: #888888cc;
border-radius: 5px;
padding: 20px;
}
.close {
background: #000;
cursor: pointer;
width: 20px;
height: 20px;
border-radius: 2px;
text-align: center;
color: white;
}
#close-bottom-right {
position: absolute;
bottom: 0;
right: 0;
}
body {
width: 100%;
height: 100%;
}
.open {
width: 50px;
height: 50px;
background-image: url("data:image/svg+xml,%3Csvg id='Layer_1' data-name='Layer 1' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 290.56 293.08'%3E%3Cdefs%3E%3Cstyle%3E.cls-1,.cls-3%7Bfill:none;%7D.cls-1%7Bstroke:%23fff;stroke-miterlimit:10;%7D.cls-2%7Bfill:%23fff;%7D%3C/style%3E%3C/defs%3E%3Ctitle%3Ebuttplug-logo-1%3C/title%3E%3Crect x='0.5' y='0.5' width='289.56' height='292.08' rx='32' ry='32'/%3E%3Crect class='cls-1' x='0.5' y='0.5' width='289.56' height='292.08' rx='32' ry='32'/%3E%3Crect class='cls-2' x='10.63' y='10.72' width='269.29' height='271.63' rx='25' ry='25'/%3E%3Crect class='cls-1' x='10.63' y='10.72' width='269.29' height='271.63' rx='25' ry='25'/%3E%3Crect x='17.37' y='17.51' width='255.83' height='258.05' rx='20' ry='20'/%3E%3Crect class='cls-1' x='17.37' y='17.51' width='255.83' height='258.05' rx='20' ry='20'/%3E%3Cline class='cls-3' x1='156.1' y1='152.66' x2='142.44' y2='162.32'/%3E%3Cpath class='cls-2' d='M325.32,383.36a3.07,3.07,0,0,1-1.71-5.64,107.76,107.76,0,0,1,14.2-9.47l2.32-1.36c2.57-1.54,5.24-3,7.83-4.36a95,95,0,0,0,13.73-8.38c1.9-1.49,2.33-6.94,2.59-10.2v-.12c.86-10.76,1-22.09-7.83-32-9.93-11.24-8.63-25.63-6.06-38.22,3-14.72,5.94-29.72,8.78-44.22,3.34-17.09,6.8-34.76,10.41-52.11,1.82-8.76,6.31-14.55,12.3-15.88a20.85,20.85,0,0,1,6.58,0c6,1.33,10.48,7.12,12.3,15.88,3.61,17.35,7.07,35,10.41,52.12,2.83,14.5,5.77,29.49,8.78,44.21,2.58,12.59,3.87,27-6.06,38.22-8.79,10-8.69,21.29-7.83,32v.12c.26,3.26.69,8.71,2.6,10.2a95.08,95.08,0,0,0,13.73,8.38c2.58,1.39,5.26,2.82,7.83,4.36l2.32,1.36a108,108,0,0,1,14.2,9.47,3.07,3.07,0,0,1-1.81,5.64H325.32Zm2.69-4H442.34a109.85,109.85,0,0,0-11.81-7.65l-2.37-1.39c-2.48-1.49-5.11-2.9-7.66-4.26a98.21,98.21,0,0,1-14.31-8.76c-3.28-2.57-3.75-8.37-4.12-13v-.12c-.93-11.61-1-23.88,8.83-35,8.74-9.9,7.63-22.56,5.14-34.77-3-14.73-5.95-29.74-8.79-44.25-3.34-17.08-6.79-34.75-10.4-52.07-1.49-7.15-4.86-11.81-9.25-12.79a9.39,9.39,0,0,0-2.27-.17H385a9.32,9.32,0,0,0-2.27.17c-4.39,1-7.76,5.64-9.25,12.79-3.61,17.32-7.06,35-10.4,52.06-2.84,14.51-5.77,29.52-8.79,44.25-2.5,12.21-3.61,24.87,5.14,34.77,9.83,11.13,9.75,23.4,8.82,35v.12c-.37,4.66-.83,10.46-4.12,13a98.14,98.14,0,0,1-14.31,8.76c-2.54,1.36-5.17,2.77-7.66,4.26l-2.37,1.39A109.88,109.88,0,0,0,328,379.35Z' transform='translate(-239.9 -125.68)'/%3E%3C/svg%3E%0A");
display: none;
z-index:999;
}
#open-bottom-right {
position: fixed;
bottom: 0;
right: 0;
display: block;
}
`;
document.body.append(style);
let open_element = document.createElement('div');
open_element.id = `open-bottom-right`;
open_element.className = "open";
document.body.append(open_element);
let container_div = document.createElement('div');
container_div.innerHTML = `
<div id="buttplug-dialog">
<div id="close-bottom-right" class="close">V</div>
<div id="buttplug-container" style="margin: 10px; display: flex;">
<div id="buttplug-connector" style="display: block;">
<a href="#" class="buttplug-button" id="buttplug-connect-browser">Connect in Browser</a>
<br/>
<a href="#" class="buttplug-button" id="buttplug-connect-intiface">Connect to Intiface Desktop</a>
<br/>
</div>
<div id="buttplug-enumeration" style="display: none;">
<a href="#" class="buttplug-button" id="buttplug-scanning">Start Scanning</a>
<a href="#" class="buttplug-button" id="buttplug-disconnect">Disconnect</a>
<br/>
<h3>Devices</h3>
<ul id="buttplug-device-list">
<li>
</li>
</ul>
</div>
</div>
</div>`;
container_div.id = "buttplug-top-container";
document.body.append(container_div);
// We need the buttplug_devices to be global, so that tampermonkey user
// scripts can work with it. Hang it off window.
window.buttplug_devices = [];
setTimeout(() =>
(async function () {
// Set up Buttplug
await Buttplug.buttplugInit();
const buttplug_client = new Buttplug.ButtplugClient("ButtEOS Client");
const dialog_div = document.getElementById("buttplug-dialog");
const connector_div = document.getElementById("buttplug-connector");
const enumeration_div = document.getElementById("buttplug-enumeration");
const scanning_button = document.getElementById("buttplug-scanning");
const connect_browser_button = document.getElementById("buttplug-connect-browser");
const connect_intiface_button = document.getElementById("buttplug-connect-intiface");
const disconnect_button = document.getElementById("buttplug-disconnect");
const device_list = document.getElementById("buttplug-device-list");
buttplug_client.addListener('deviceadded', async (device) => {
const element_id = `buttplug-device-${device.Index}`;
const input = document.createElement("li");
input.id = element_id;
const checkbox = document.createElement("input");
const checkbox_id = `${element_id}-checkbox`;
checkbox.type = "checkbox";
checkbox.id = checkbox_id;
input.addEventListener("click", async (event) => {
const index = window.buttplug_devices.indexOf(device);
if (index > -1) {
await device.stop();
window.buttplug_devices.splice(index, 1);
checkbox.checked = false;
} else {
window.buttplug_devices.push(device);
checkbox.checked = true;
}
});
let label = document.createElement("label");
label.for = `${element_id}-checkbox`;
label.innerHTML = device.Name;
input.appendChild(checkbox);
input.appendChild(label);
device_list.appendChild(input);
});
buttplug_client.addListener('deviceremoved', async (device) => {
const element_id = `buttplug-device-${device.Index}`;
var element = document.getElementById(element_id);
element.parentNode.removeChild(element);
});
connect_browser_button.addEventListener("click", async (event) => {
const connector = new Buttplug.ButtplugEmbeddedConnectorOptions();
await buttplug_client.connect(connector);
connector_div.style.display = "none";
enumeration_div.style.display = "block";
}, false);
connect_intiface_button.addEventListener("click", async (event) => {
const connector = new Buttplug.ButtplugWebsocketConnectorOptions("ws://localhost:12345/");
await buttplug_client.connect(connector);
connector_div.style.display = "none";
enumeration_div.style.display = "block";
}, false);
disconnect_button.addEventListener("click", async (event) => {
await buttplug_client.disconnect();
enumeration_div.style.display = "none";
connector_div.style.display = "block";
}, false);
scanning_button.addEventListener('click', async () => {
await buttplug_client.startScanning();
});
let container = document.querySelector("#buttplug-top-container");
let close = document.getElementById(`close-bottom-right`);
let open = document.getElementById(`open-bottom-right`);
close.addEventListener("click", () => {
container.style.display = "none";
open.style.display = "block";
}, false);
container_div.addEventListener("click", () => {
container.style.display = "none";
open.style.display = "block";
}, false);
dialog_div.addEventListener("click", (ev) => {
ev.stopPropagation();
}, false);
open.addEventListener("click", () => {
open.style.display = "none";
container.style.display = "block";
}, false);
})(), 0);
}, false);
}