ButtEOS

2/21/2022, 9:26:31 PM

// ==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);
}