From 74d886613bacbc8f4e39c3db1d1937c9a71d9c5f Mon Sep 17 00:00:00 2001 From: CHOMPStation2StaffMirrorBot <94713762+CHOMPStation2StaffMirrorBot@users.noreply.github.com> Date: Tue, 3 Jun 2025 00:35:04 -0700 Subject: [PATCH] [MIRROR] Add Multishock integration (#11003) Co-authored-by: ShadowLarkens --- code/__defines/shock.dm | 4 + code/modules/client/client procs.dm | 2 + code/modules/client/shock.dm | 149 +++++++++++++ code/modules/mob/living/damage_procs.dm | 1 + .../vore/eating/bellymodes_datum_vr.dm | 1 + html/shock.js | 79 +++++++ interface/skin.dmf | 8 + .../tgui/interfaces/ShockConfigurator.tsx | 208 ++++++++++++++++++ .../VorePanelCommonElements.tsx | 2 +- vorestation.dme | 2 + 10 files changed, 455 insertions(+), 1 deletion(-) create mode 100644 code/__defines/shock.dm create mode 100644 code/modules/client/shock.dm create mode 100644 html/shock.js create mode 100644 tgui/packages/tgui/interfaces/ShockConfigurator.tsx diff --git a/code/__defines/shock.dm b/code/__defines/shock.dm new file mode 100644 index 0000000000..d877cc3ef7 --- /dev/null +++ b/code/__defines/shock.dm @@ -0,0 +1,4 @@ +// KEEP THIS UP TO DATE WITH tgui/packages/tgui/interfaces/ShockConfigurator.tsx +#define SHOCKFLAG_BURNDAMAGE 0x1 +#define SHOCKFLAG_DIGESTION 0x2 +#define SHOCKFLAG_TEST "test" diff --git a/code/modules/client/client procs.dm b/code/modules/client/client procs.dm index ed440457cb..aa09565613 100644 --- a/code/modules/client/client procs.dm +++ b/code/modules/client/client procs.dm @@ -256,6 +256,7 @@ // Instantiate tgui panel tgui_say = new(src, "tgui_say") + tgui_shocker = new(src, "tgui_shock") initialize_commandbar_spy() tgui_panel = new(src, "browseroutput") @@ -304,6 +305,7 @@ // Initialize tgui panel tgui_say.initialize() + tgui_shocker.initialize() connection_time = world.time connection_realtime = world.realtime diff --git a/code/modules/client/shock.dm b/code/modules/client/shock.dm new file mode 100644 index 0000000000..21152d10f4 --- /dev/null +++ b/code/modules/client/shock.dm @@ -0,0 +1,149 @@ +/client/var/datum/tgui_shock/tgui_shocker + +/client/verb/configure_shocker() + set name = "Configure MultiShock Integration" + set category = "OOC.Game Settings" + + if(tgui_shocker) + tgui_shocker.tgui_interact(mob) + +/mob/proc/attempt_multishock(flag) + client?.attempt_multishock(flag) + +/client/proc/attempt_multishock(flag) + tgui_shocker?.shock(flag) + +// NOTE: This datum controls TWO UIs, `window` is hidden and provides all of the WebSocket shit, `tgui_interact` is a +// normal configuration UI! +/datum/tgui_shock + /// The user who opened the window + var/client/client + /// The modal window + var/datum/tgui_window/window + + var/port = 8765 + var/enabled_flags = 0 + var/intensity = 15 + var/duration = 1 + + var/connected = FALSE + var/selected_device = -1 + var/list/available_devices + +////////////////////////////////////////// +// SHOCK.JS UI // +////////////////////////////////////////// +/datum/tgui_shock/New(client/client, id) + src.client = client + window = new(client, id) + window.subscribe(src, PROC_REF(on_message)) + window.is_browser = TRUE + +/datum/tgui_shock/proc/initialize() + set waitfor = FALSE + window.initialize( + inline_js = file2text('html/shock.js') + ) + +/datum/tgui_shock/proc/connect() + window.send_message("connect", list( + "port" = port, + )) + +/datum/tgui_shock/proc/request_devices() + if(!connected) + return + window.send_message("enumerateShockers") + +/datum/tgui_shock/proc/shock(flag) + if(!connected || !selected_device) + return + + if(flag != SHOCKFLAG_TEST) + if(!(enabled_flags & flag)) + return + + window.send_message("shock", list( + "intensity" = intensity, + "duration" = duration, + "shocker_ids" = list( + selected_device + ), + "warning" = FALSE, + )) + +/datum/tgui_shock/proc/estop() + window.send_message("estop") + +/datum/tgui_shock/proc/on_message(type, payload, href_list) + if(type == "connected") + connected = TRUE + else if(type == "disconnected") + connected = FALSE + else if(type == "error") + connected = FALSE + log_debug("WebSocket Error [json_encode(payload)]") + else if(type == "incomingMessage") + if(payload["lastCall"] == "get_devices") + available_devices = json_decode(payload["data"]) + +////////////////////////////////////////// +// TGUI // +////////////////////////////////////////// +/datum/tgui_shock/tgui_state(mob/user) + return GLOB.tgui_always_state + +/datum/tgui_shock/tgui_interact(mob/user, datum/tgui/ui, datum/tgui/parent_ui, custom_state) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "ShockConfigurator", "Shock Configurator") + ui.open() + +/datum/tgui_shock/tgui_data(mob/user, datum/tgui/ui, datum/tgui_state/state) + var/list/data = ..() + + data["port"] = port + data["connected"] = connected + data["intensity"] = intensity + data["duration"] = duration + data["selectedDevice"] = selected_device + data["availableDevices"] = available_devices + data["enabledFlags"] = enabled_flags + + return data + +/datum/tgui_shock/tgui_act(action, list/params, datum/tgui/ui, datum/tgui_state/state) + . = ..() + + switch(action) + if("connect") + if(connected) + estop() + else + // TODO: preferences + connect() + . = TRUE + if("request_devices") + request_devices() + . = TRUE + if("estop") + estop() + . = TRUE + if("setSelectedDevice") + selected_device = text2num(params["device"]) + . = TRUE + if("test") + shock(SHOCKFLAG_TEST) + . = TRUE + if("set_flag") + enabled_flags ^= text2num(params["flag"]) + . = TRUE + if("port") + port = text2num(params["port"]) + . = TRUE + if("intensity") + intensity = text2num(params["intensity"]) + . = TRUE + if("duration") + duration = text2num(params["duration"]) + . = TRUE diff --git a/code/modules/mob/living/damage_procs.dm b/code/modules/mob/living/damage_procs.dm index 1922f97392..50e1dd16c7 100644 --- a/code/modules/mob/living/damage_procs.dm +++ b/code/modules/mob/living/damage_procs.dm @@ -76,6 +76,7 @@ if(COLD_RESISTANCE in mutations) damage = 0 adjustFireLoss(damage * blocked) + attempt_multishock(SHOCKFLAG_BURNDAMAGE) if(SEARING) apply_damage(round(damage / 3), BURN, def_zone, initial_blocked, soaked, sharp, edge, used_weapon) apply_damage(round(damage / 3 * 2), BRUTE, def_zone, initial_blocked, soaked, sharp, edge, used_weapon) diff --git a/code/modules/vore/eating/bellymodes_datum_vr.dm b/code/modules/vore/eating/bellymodes_datum_vr.dm index 828b70ea54..78a046a446 100644 --- a/code/modules/vore/eating/bellymodes_datum_vr.dm +++ b/code/modules/vore/eating/bellymodes_datum_vr.dm @@ -71,6 +71,7 @@ GLOBAL_LIST_INIT(digest_modes, list()) L.adjustOxyLoss(B.digest_oxy) L.adjustToxLoss(B.digest_tox) L.adjustCloneLoss(B.digest_clone) + L.attempt_multishock(SHOCKFLAG_DIGESTION) // Send a message when a prey-thing enters hard crit. if(iscarbon(L) && old_health > 0 && L.health <= 0) to_chat(B.owner, span_notice("You feel [L] go still within your [lowertext(B.name)].")) diff --git a/html/shock.js b/html/shock.js new file mode 100644 index 0000000000..68ad8b61a4 --- /dev/null +++ b/html/shock.js @@ -0,0 +1,79 @@ +let webSocket; +let authKey; +let lastCall; + +const reactRoot = document.getElementById("react-root"); +if (reactRoot) { + reactRoot.innerHTML = "

You shouldn't see this window, update your skin.

"; +} + +Byond.subscribeTo("estop", function () { + if (webSocket) { + webSocket.close(); + } else { + Byond.sendMessage("disconnected"); + } +}) + +Byond.subscribeTo("connect", function (data) { + if (webSocket) { + webSocket.close(); + } + webSocket = new WebSocket(`ws://localhost:${data.port}`); + webSocket.sendJson = (data) => { + webSocket.send(JSON.stringify(data)); + }; + authKey = JSON.parse(window.hubStorage.getItem("virgo-shocker-authkey")); + webSocket.onopen = (ev) => { + Byond.sendMessage("connected"); + }; + webSocket.onerror = (ev) => { + Byond.sendMessage("error", ev); + }; + webSocket.onclose = (ev) => { + Byond.sendMessage("disconnected"); + } + webSocket.onmessage = (ev) => { + Byond.sendMessage("incomingMessage", { data: ev.data, lastCall }); + }; +}); + +Byond.subscribeTo("enumerateShockers", function () { + if (!webSocket) { + return; + } + + lastCall = "get_devices"; + webSocket.sendJson({ + cmd: "get_devices", + auth_key: authKey, + }); +}); + +// data: { +// "intensity": 10, // 1 - 100 - int +// "duration": 1, // 0.1 - 15 - float +// "shocker_ids": [], // [] - List of shocker ids +// "warning": false, // true, false - will send a vibrate with the same intensity and duration +// }, +Byond.subscribeTo("shock", function (data) { + if (!webSocket) { + return; + } + + lastCall = "operate"; + webSocket.sendJson({ + "cmd": "operate", + "value": { + "intensity": data.intensity, // 1 - 100 - int + "duration": data.duration, // 0.1 - 15 - float + "shocker_option": "all", // all, random + "action": "shock", // shock, vibrate, beep, end + "shocker_ids": data.shocker_ids, // [] - List of shocker ids + "device_ids": [], // [] - list of pishock client ids, if one of these is provided it will activate all shockers associated with it + "warning": data.warning, // true, false - will send a vibrate with the same intensity and duration + "held": false, // true, false - for continuous commands + }, + "auth_key": authKey, + }); +}); diff --git a/interface/skin.dmf b/interface/skin.dmf index 982f4b94e6..2b65417108 100644 --- a/interface/skin.dmf +++ b/interface/skin.dmf @@ -1192,6 +1192,14 @@ window "mainwindow" anchor2 = -1,-1 is-visible = false saved-params = "" + elem "tgui_shock" + type = BROWSER + pos = 0,0 + size = 200x200 + anchor1 = -1,-1 + anchor2 = -1,-1 + is-visible = false + saved-params = "" elem "hotkey_toggle" type = BUTTON pos = 560,400 diff --git a/tgui/packages/tgui/interfaces/ShockConfigurator.tsx b/tgui/packages/tgui/interfaces/ShockConfigurator.tsx new file mode 100644 index 0000000000..dfbe2c7268 --- /dev/null +++ b/tgui/packages/tgui/interfaces/ShockConfigurator.tsx @@ -0,0 +1,208 @@ +import { storage } from 'common/storage'; +import { useEffect, useState } from 'react'; +import { useBackend } from 'tgui/backend'; +import { Window } from 'tgui/layouts'; +import { + Box, + Button, + Input, + LabeledList, + NumberInput, + Section, + Stack, +} from 'tgui-core/components'; +import { type BooleanLike } from 'tgui-core/react'; + +// These are straight from https://docs.pishock.com/multishock/multishock-websocket-api.html +type MultishockAPIShocker = { + name: string; + identifier: number; +}; + +type MultishockAPIDevice = { + name: string; + id: number; + shockers: Array; +}; + +type MultishockAPIAvailableDevices = Array; + +// KEEP THIS UP TO DATE WITH code/__defines/shock.dm +enum ShockFlags { + BurnDamage = 1, + Digestion = 2, +} + +type Data = { + port: number; + authKey: string; + connected: BooleanLike; + intensity: number; + duration: number; + selectedDevice: number; + availableDevices: MultishockAPIAvailableDevices; + enabledFlags: ShockFlags; +}; + +export const ShockConfigurator = (props) => { + const { act, data } = useBackend(); + const { + port, + connected, + intensity, + duration, + selectedDevice, + availableDevices, + enabledFlags, + } = data; + + const [authKey, setAuthKey] = useState(''); + + useEffect(() => { + const async_get = async () => { + setAuthKey(await storage.get('shocker-authkey')); + }; + async_get(); + }); + + return ( + + +
+ + + {connected ? ( + + Connected + + ) : ( + + Disconnected + + )} + + + + + + + + + act('port', { port: val })} + /> + + + { + storage.set('shocker-authkey', val); + setAuthKey(val); + }} + /> + + + act('intensity', { intensity: val })} + /> + + + act('duration', { duration: val })} + /> + + + + + ETIQUITTE NOTE: You must let your partners know you're using + this! + +
+
+ + This section indicates when you would like to be shocked. Note: Burn + + Digestion is not recommended, as it will shock you twice per + digest tick. + + act('set_flag', { flag: ShockFlags.BurnDamage })} + > + Burn + + act('set_flag', { flag: ShockFlags.Digestion })} + > + Digestion + +
+
act('request_devices')} + /> + } + > + {availableDevices && availableDevices.length ? ( + availableDevices.map((device) => ( + + + Device: {device.name} - {device.id} + + + {device.shockers.map((shocker) => ( + + Shocker: {shocker.name} - {shocker.identifier} + { + if (shocker.identifier !== selectedDevice) { + act('setSelectedDevice', { + device: shocker.identifier, + }); + } else { + act('setSelectedDevice', { + device: -1, + }); + } + }} + > + Use This Device + + + ))} + + + )) + ) : ( + No Devices Found + )} +
+
+
+ ); +}; diff --git a/tgui/packages/tgui/interfaces/VorePanel/VorePanelElements/VorePanelCommonElements.tsx b/tgui/packages/tgui/interfaces/VorePanel/VorePanelElements/VorePanelCommonElements.tsx index d0f3847a59..5c9e9ada5b 100644 --- a/tgui/packages/tgui/interfaces/VorePanel/VorePanelElements/VorePanelCommonElements.tsx +++ b/tgui/packages/tgui/interfaces/VorePanel/VorePanelElements/VorePanelCommonElements.tsx @@ -54,7 +54,7 @@ export const VorePanelEditToggle = (props: {