ButtEOS

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

  1. // ==UserScript==
  2. // @name ButtEOS
  3. // @namespace Violentmonkey Scripts
  4. // @grant none
  5. // @match *://milovana.com/webteases/showtease.php
  6. // @match *://milovana.com/eos/editor/*
  7. // @match *://eosscript.com/*
  8. // @license BSD
  9. // @version 1.1
  10. // @author cfs6t08p
  11. // @description 2/21/2022, 9:26:31 PM
  12. // ==/UserScript==
  13.  
  14. /* jshint esversion: 8 */
  15.  
  16. function mod(a, b) {
  17. return ((a % b) + b) % b;
  18. }
  19.  
  20. function actionIndex(pattern, time) {
  21. for(let a = 0; a < pattern.numActions; a++) {
  22. if(pattern.actions[a].at > time) {
  23. return a;
  24. }
  25. }
  26. }
  27.  
  28. function positionAt(pattern, time, index) {
  29. if(pattern.actions[index].at > time) {
  30. let a1 = pattern.actions[mod((index - 1), pattern.numActions)];
  31. let a2 = pattern.actions[index];
  32.  
  33. let a1Wrap = mod(a1.at, pattern.patternLength);
  34.  
  35. let dp = a2.pos - a1.pos;
  36. let dt = a2.at - a1Wrap;
  37.  
  38. let alpha = (time - a1Wrap) / dt;
  39.  
  40. return 99 - (a1.pos + alpha * dp);
  41. }
  42. }
  43.  
  44. function vibe(level) {
  45. window.parent.postMessage({buttEOS: true, vib: {level: level}}, "https://milovana.com/webteases/*");
  46. }
  47.  
  48. function linear(position, duration) {
  49. window.parent.postMessage({buttEOS: true, linear: {position: position, duration: duration }}, "https://milovana.com/webteases/*");
  50. }
  51.  
  52. if(document.getElementById("eosContainer")) {
  53. let eos = document.getElementById("eosContainer");
  54. let bod = document.body;
  55.  
  56. let div = document.createElement("div");
  57.  
  58. div.style = "position: absolute; left: 20px; top: 40px; width: 160px; z-index: 100000";
  59.  
  60. bod.append(div);
  61.  
  62. let bar = document.createElement("div");
  63. let fill = document.createElement("div");
  64. let arrow = document.createElement("div");
  65. let line = document.createElement("div");
  66. let text = document.createElement("div");
  67.  
  68. 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;";
  69. fill.style = "position: absolute; left: 7px; width: 36px; bottom: 0px; background-color: #bb55bbcc;";
  70. arrow.style = "position: absolute; left: 7px; width: 36px; height: 18px; border-top-left-radius: 18px; border-top-right-radius: 18px; background-color: #bb55bbcc;";
  71. line.style = "position: absolute; width: 100%; height: 4px; bottom: 15%; background-color: #ffff00cc;";
  72. text.style = "position: absolute; width: 100%; height: 10%; bottom: 0px; color: white; padding-top: 5px;";
  73.  
  74. bar.append(fill);
  75. bar.append(arrow);
  76. bar.append(line);
  77.  
  78. div.append(bar);
  79. div.append(text);
  80.  
  81. let currentPattern = {};
  82. let lastPatternName;
  83. let lastBPM;
  84. let bpmPattern;
  85. let patternStart;
  86. let prevActionIndex;
  87.  
  88. let newPatterns = 0;
  89.  
  90. let vibeLevel;
  91.  
  92. let patterns = {};
  93.  
  94. setInterval(() => {
  95. let xpath = ".//p[contains(text(),'Load pattern:')]";
  96. let result = document.evaluate(xpath, eos, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
  97.  
  98. if(result.snapshotLength == 0) {
  99. newPatterns = 0;
  100. }
  101.  
  102. for(let i = 0; i < result.snapshotLength; i++) {
  103. let node = result.snapshotItem(i);
  104. let name = node.textContent.slice(13).trim();
  105.  
  106. let data = node.parentNode.childNodes;
  107.  
  108. if(data.length >= 3) {
  109. let text = data[1].textContent;
  110. let funscript = "";
  111.  
  112. for(let l = 2; l < data.length; l++) {
  113. funscript = funscript + data[l].textContent;
  114. }
  115.  
  116. if(patterns[name] === undefined) {
  117. patterns[name] = {};
  118.  
  119. try {
  120. let pattern = JSON.parse(funscript);
  121.  
  122. pattern.valid = true;
  123. pattern.text = text;
  124. pattern.numActions = pattern.actions.length;
  125. pattern.patternLength = pattern.actions[pattern.numActions - 1].at;
  126.  
  127. let time = 0;
  128.  
  129. for(let a = 0; a < pattern.numActions; a++) {
  130. let at = pattern.actions[a].at;
  131.  
  132. pattern.actions[a].dur = at - time;
  133.  
  134. time = at;
  135. }
  136.  
  137. patterns[name] = pattern;
  138.  
  139. newPatterns++;
  140.  
  141. console.log(pattern);
  142. } catch(error) {
  143. console.error("Failed to load pattern \"" + name + "\"");
  144. console.error(error);
  145. }
  146. }
  147. }
  148. }
  149.  
  150. if(newPatterns > 0) {
  151. text.innerText = "Loaded " + newPatterns + " pattern(s)";
  152. }
  153. }, 100);
  154.  
  155. setInterval(() => {
  156. let now = Date.now();
  157. let h = (eos.clientHeight - 40) * 0.7;
  158.  
  159. div.style.height = h + "px";
  160.  
  161. let vibePath = ".//div[contains(text(),'Vibrator:')]";
  162. let vibeNotification = document.evaluate(vibePath, eos, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
  163.  
  164. let newVibe = vibeLevel;
  165.  
  166. if(vibeNotification) {
  167. newVibe = parseInt(vibeNotification.textContent.slice(9));
  168. } else {
  169. newVibe = 0;
  170. }
  171.  
  172. if((newVibe != vibeLevel) && !(Number.isNaN(newVibe) && Number.isNaN(vibeLevel))) {
  173. vibeLevel = newVibe;
  174.  
  175. if(Number.isNaN(vibeLevel) || vibeLevel > 100 || vibeLevel < 0) {
  176. console.error("Invalid vibrator level: \"" + vibeLevel + "\"");
  177.  
  178. vibe(0);
  179. } else {
  180. vibe(vibeLevel);
  181. }
  182. }
  183.  
  184. let pattern;
  185.  
  186. let patternPath = ".//div[contains(text(),'Pattern:')]";
  187. let patternNotification = document.evaluate(patternPath, eos, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
  188.  
  189. if(patternNotification) {
  190. let name = patternNotification.textContent.slice(8).trim();
  191. pattern = patterns[name];
  192.  
  193. if(lastPatternName != name) {
  194. lastPatternName = name;
  195.  
  196. if(!pattern) {
  197. console.error("Pattern \"" + name + "\" not found");
  198. }
  199. }
  200. }
  201.  
  202. let bpmPath = ".//div[contains(text(),'BPM:')]";
  203. let bpmNotification = document.evaluate(bpmPath, eos, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
  204.  
  205. if(bpmNotification) {
  206. let bpm = parseInt(bpmNotification.textContent.slice(4));
  207.  
  208. if((bpm != lastBPM) && !(Number.isNaN(bpm) && Number.isNaN(lastBPM))) {
  209. lastBPM = bpm;
  210.  
  211. if(Number.isNaN(bpm) || bpm <= 0 || bpm > 600) {
  212. console.error("Invalid BPM: \"" + bpm + "\"");
  213.  
  214. bpmPattern = undefined;
  215. } else {
  216. let period = (60 * 1000) / bpm;
  217.  
  218. pattern = {valid: true, numActions: 2, patternLength: period, text: "", actions: [{at: period / 2, pos: 0, dur: period / 2},{at: period, pos: 100, dur: period / 2}]};
  219.  
  220. bpmPattern = pattern;
  221. }
  222. } else {
  223. pattern = bpmPattern;
  224. }
  225. }
  226.  
  227. if(pattern != currentPattern) {
  228. currentPattern = pattern;
  229. patternStart = now;
  230. prevActionIndex = -1;
  231.  
  232. if(pattern) {
  233. text.innerText = pattern.text;
  234.  
  235. bar.style.visibility = "visible";
  236. } else {
  237. text.innerText = "";
  238.  
  239. bar.style.visibility = "hidden";
  240. }
  241.  
  242. newPatterns = 0;
  243. }
  244.  
  245. if(currentPattern !== undefined && currentPattern.valid) {
  246. let patternTime = mod(now - patternStart, currentPattern.patternLength);
  247. let index = actionIndex(currentPattern, patternTime);
  248.  
  249. if(index != prevActionIndex) {
  250. linear(currentPattern.actions[index].pos, currentPattern.actions[index].dur);
  251.  
  252. prevActionIndex = index;
  253. }
  254.  
  255. let fillHeight = ((bar.clientHeight - 25) * positionAt(currentPattern, patternTime, index)) / 100;
  256.  
  257. fill.style.height = fillHeight + "px";
  258. arrow.style.bottom = fillHeight + "px";
  259. }
  260. }, 10);
  261. }
  262.  
  263. if(document.querySelector(".eosTopBody")) {
  264. window.addEventListener("message", (event) => {
  265. if(event.data.buttEOS) {
  266. if(window.buttplug_devices) {
  267. if(event.data.vib) {
  268. window.buttplug_devices.forEach((device) => {
  269. if(device.messageAttributes(Buttplug.ButtplugDeviceMessageType.VibrateCmd)) {
  270. device.vibrate(event.data.vib.level / 100);
  271. }
  272. });
  273. }
  274.  
  275. if(event.data.linear) {
  276. window.buttplug_devices.forEach((device) => {
  277. if(device.messageAttributes(Buttplug.ButtplugDeviceMessageType.LinearCmd)) {
  278. device.linear(event.data.linear.position / 100, event.data.linear.duration);
  279. }
  280. });
  281. }
  282. }
  283.  
  284. event.stopImmediatePropagation();
  285. }
  286. });
  287.  
  288. let bpscript= document.createElement("script");
  289. bpscript.src = "https://cdn.jsdelivr.net/npm/buttplug@1.0.17/dist/web/buttplug.min.js";
  290. document.body.append(bpscript);
  291.  
  292. window.addEventListener("load", function (e) {
  293. let style = document.createElement("style");
  294. style.innerHTML = `
  295. #buttplug-top-container h3, li {
  296. font-family:Arial;
  297. font-size:15px;
  298. }
  299. #buttplug-top-container ul {
  300. list-style-type: none;
  301. column-count: 2;
  302. }
  303. .buttplug-button {
  304. box-shadow:inset 0px 1px 3px 0px #91b8b3;
  305. background:linear-gradient(to bottom, #768d87 5%, #6c7c7c 100%);
  306. background-color:#768d87;
  307. border-radius:5px;
  308. border:1px solid #566963;
  309. display:inline-block;
  310. cursor:pointer;
  311. color:#ffffff;
  312. font-family:Arial;
  313. font-size:15px;
  314. font-weight:bold;
  315. padding:11px 23px;
  316. text-decoration:none;
  317. text-shadow:0px -1px 0px #2b665e;
  318. margin: 5px;
  319. }
  320. .buttplug-button:hover {
  321. background:linear-gradient(to bottom, #6c7c7c 5%, #768d87 100%);
  322. background-color:#6c7c7c;
  323. }
  324. .buttplug-button:active {
  325. position:relative;
  326. top:1px;
  327. }
  328.  
  329. #buttplug-top-container {
  330. position: fixed;
  331. top: 0;
  332. right: 0;
  333. width: 100%;
  334. height: 100%;
  335. overflow: hidden;
  336. background: rgba(0, 0, 0, 0.7);
  337. display: none;
  338. }
  339.  
  340. #buttplug-dialog {
  341. width: 50%;
  342. min-height: 200px;
  343. position: absolute;
  344. top: 10%;
  345. left: 0;
  346. left: 0;
  347. right: 0;
  348. margin: auto;
  349. background: #888888cc;
  350. border-radius: 5px;
  351. padding: 20px;
  352. }
  353.  
  354. .close {
  355. background: #000;
  356. cursor: pointer;
  357. width: 20px;
  358. height: 20px;
  359. border-radius: 2px;
  360. text-align: center;
  361. color: white;
  362. }
  363.  
  364. #close-bottom-right {
  365. position: absolute;
  366. bottom: 0;
  367. right: 0;
  368. }
  369.  
  370. body {
  371. width: 100%;
  372. height: 100%;
  373. }
  374.  
  375. .open {
  376. width: 50px;
  377. height: 50px;
  378. 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");
  379. display: none;
  380. z-index:999;
  381. }
  382.  
  383. #open-bottom-right {
  384. position: fixed;
  385. bottom: 0;
  386. right: 0;
  387. display: block;
  388. }
  389. `;
  390.  
  391. document.body.append(style);
  392.  
  393. let open_element = document.createElement('div');
  394. open_element.id = `open-bottom-right`;
  395. open_element.className = "open";
  396. document.body.append(open_element);
  397.  
  398. let container_div = document.createElement('div');
  399. container_div.innerHTML = `
  400. <div id="buttplug-dialog">
  401. <div id="close-bottom-right" class="close">V</div>
  402. <div id="buttplug-container" style="margin: 10px; display: flex;">
  403. <div id="buttplug-connector" style="display: block;">
  404. <a href="#" class="buttplug-button" id="buttplug-connect-browser">Connect in Browser</a>
  405. <br/>
  406. <a href="#" class="buttplug-button" id="buttplug-connect-intiface">Connect to Intiface Desktop</a>
  407. <br/>
  408. </div>
  409. <div id="buttplug-enumeration" style="display: none;">
  410. <a href="#" class="buttplug-button" id="buttplug-scanning">Start Scanning</a>
  411. <a href="#" class="buttplug-button" id="buttplug-disconnect">Disconnect</a>
  412. <br/>
  413. <h3>Devices</h3>
  414. <ul id="buttplug-device-list">
  415. <li>
  416. </li>
  417. </ul>
  418. </div>
  419. </div>
  420. </div>`;
  421. container_div.id = "buttplug-top-container";
  422. document.body.append(container_div);
  423.  
  424. // We need the buttplug_devices to be global, so that tampermonkey user
  425. // scripts can work with it. Hang it off window.
  426. window.buttplug_devices = [];
  427.  
  428. setTimeout(() =>
  429. (async function () {
  430. // Set up Buttplug
  431. await Buttplug.buttplugInit();
  432.  
  433. const buttplug_client = new Buttplug.ButtplugClient("ButtEOS Client");
  434. const dialog_div = document.getElementById("buttplug-dialog");
  435. const connector_div = document.getElementById("buttplug-connector");
  436. const enumeration_div = document.getElementById("buttplug-enumeration");
  437. const scanning_button = document.getElementById("buttplug-scanning");
  438. const connect_browser_button = document.getElementById("buttplug-connect-browser");
  439. const connect_intiface_button = document.getElementById("buttplug-connect-intiface");
  440. const disconnect_button = document.getElementById("buttplug-disconnect");
  441. const device_list = document.getElementById("buttplug-device-list");
  442. buttplug_client.addListener('deviceadded', async (device) => {
  443. const element_id = `buttplug-device-${device.Index}`;
  444. const input = document.createElement("li");
  445. input.id = element_id;
  446. const checkbox = document.createElement("input");
  447. const checkbox_id = `${element_id}-checkbox`;
  448. checkbox.type = "checkbox";
  449. checkbox.id = checkbox_id;
  450. input.addEventListener("click", async (event) => {
  451. const index = window.buttplug_devices.indexOf(device);
  452.  
  453. if (index > -1) {
  454. await device.stop();
  455. window.buttplug_devices.splice(index, 1);
  456. checkbox.checked = false;
  457. } else {
  458. window.buttplug_devices.push(device);
  459. checkbox.checked = true;
  460. }
  461. });
  462. let label = document.createElement("label");
  463. label.for = `${element_id}-checkbox`;
  464. label.innerHTML = device.Name;
  465. input.appendChild(checkbox);
  466. input.appendChild(label);
  467. device_list.appendChild(input);
  468. });
  469.  
  470. buttplug_client.addListener('deviceremoved', async (device) => {
  471. const element_id = `buttplug-device-${device.Index}`;
  472. var element = document.getElementById(element_id);
  473. element.parentNode.removeChild(element);
  474. });
  475.  
  476. connect_browser_button.addEventListener("click", async (event) => {
  477. const connector = new Buttplug.ButtplugEmbeddedConnectorOptions();
  478. await buttplug_client.connect(connector);
  479. connector_div.style.display = "none";
  480. enumeration_div.style.display = "block";
  481. }, false);
  482.  
  483. connect_intiface_button.addEventListener("click", async (event) => {
  484. const connector = new Buttplug.ButtplugWebsocketConnectorOptions("ws://localhost:12345/");
  485. await buttplug_client.connect(connector);
  486. connector_div.style.display = "none";
  487. enumeration_div.style.display = "block";
  488. }, false);
  489.  
  490. disconnect_button.addEventListener("click", async (event) => {
  491. await buttplug_client.disconnect();
  492. enumeration_div.style.display = "none";
  493. connector_div.style.display = "block";
  494. }, false);
  495.  
  496. scanning_button.addEventListener('click', async () => {
  497. await buttplug_client.startScanning();
  498. });
  499.  
  500. let container = document.querySelector("#buttplug-top-container");
  501.  
  502. let close = document.getElementById(`close-bottom-right`);
  503. let open = document.getElementById(`open-bottom-right`);
  504. close.addEventListener("click", () => {
  505. container.style.display = "none";
  506. open.style.display = "block";
  507. }, false);
  508.  
  509. container_div.addEventListener("click", () => {
  510. container.style.display = "none";
  511. open.style.display = "block";
  512. }, false);
  513.  
  514. dialog_div.addEventListener("click", (ev) => {
  515. ev.stopPropagation();
  516. }, false);
  517.  
  518. open.addEventListener("click", () => {
  519. open.style.display = "none";
  520. container.style.display = "block";
  521. }, false);
  522. })(), 0);
  523.  
  524. }, false);
  525. }