ButtEOS

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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/[email protected]/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);
}