[MIRROR] Add Multishock integration (#11003)

Co-authored-by: ShadowLarkens <shadowlarkens@gmail.com>
This commit is contained in:
CHOMPStation2StaffMirrorBot
2025-06-03 00:35:04 -07:00
committed by GitHub
parent 76c9d5e08a
commit 74d886613b
10 changed files with 455 additions and 1 deletions

4
code/__defines/shock.dm Normal file
View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)]."))

79
html/shock.js Normal file
View File

@@ -0,0 +1,79 @@
let webSocket;
let authKey;
let lastCall;
const reactRoot = document.getElementById("react-root");
if (reactRoot) {
reactRoot.innerHTML = "<h1>You shouldn't see this window, update your skin.</h1>";
}
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,
});
});

View File

@@ -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

View File

@@ -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<MultishockAPIShocker>;
};
type MultishockAPIAvailableDevices = Array<MultishockAPIDevice>;
// 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<Data>();
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 (
<Window width={400} height={600}>
<Window.Content>
<Section title="Status">
<Stack>
<Stack.Item grow>
{connected ? (
<Box inline color="good">
Connected
</Box>
) : (
<Box inline color="bad">
Disconnected
</Box>
)}
</Stack.Item>
<Stack.Item>
<Button onClick={() => act('connect')}>
{connected ? 'Disconnect' : 'Connect'}
</Button>
<Button ml={1} onClick={() => act('estop')}>
E-STOP
</Button>
</Stack.Item>
</Stack>
<LabeledList>
<LabeledList.Item label="MultiShock Port">
<NumberInput
value={port}
minValue={0}
maxValue={25565}
step={1}
onChange={(val) => act('port', { port: val })}
/>
</LabeledList.Item>
<LabeledList.Item label="MultiShock Auth Key">
<Input
value={authKey}
onChange={(val) => {
storage.set('shocker-authkey', val);
setAuthKey(val);
}}
/>
</LabeledList.Item>
<LabeledList.Item label="Shock Intensity">
<NumberInput
value={intensity}
minValue={0}
maxValue={100}
step={1}
onChange={(val) => act('intensity', { intensity: val })}
/>
</LabeledList.Item>
<LabeledList.Item label="Shock Duration (Seconds)">
<NumberInput
value={duration}
minValue={0}
maxValue={15}
step={0.1}
onChange={(val) => act('duration', { duration: val })}
/>
</LabeledList.Item>
</LabeledList>
<Button onClick={() => act('test')}>Test Shocker</Button>
<Box color="bad">
ETIQUITTE NOTE: You must let your partners know you&apos;re using
this!
</Box>
</Section>
<Section title="Shock Sources">
<Box>
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.
</Box>
<Button.Checkbox
checked={enabledFlags & ShockFlags.BurnDamage}
selected={enabledFlags & ShockFlags.BurnDamage}
onClick={() => act('set_flag', { flag: ShockFlags.BurnDamage })}
>
Burn
</Button.Checkbox>
<Button.Checkbox
checked={enabledFlags & ShockFlags.Digestion}
selected={enabledFlags & ShockFlags.Digestion}
onClick={() => act('set_flag', { flag: ShockFlags.Digestion })}
>
Digestion
</Button.Checkbox>
</Section>
<Section
title="Devices"
buttons={
<Button
icon="refresh"
tooltip="Refresh"
onClick={() => act('request_devices')}
/>
}
>
{availableDevices && availableDevices.length ? (
availableDevices.map((device) => (
<Box key={device.id}>
<Box bold>
Device: {device.name} - {device.id}
</Box>
<Box>
{device.shockers.map((shocker) => (
<Box ml={4} key={shocker.identifier}>
Shocker: {shocker.name} - {shocker.identifier}
<Button.Checkbox
ml={1}
checked={shocker.identifier === selectedDevice}
selected={shocker.identifier === selectedDevice}
onClick={() => {
if (shocker.identifier !== selectedDevice) {
act('setSelectedDevice', {
device: shocker.identifier,
});
} else {
act('setSelectedDevice', {
device: -1,
});
}
}}
>
Use This Device
</Button.Checkbox>
</Box>
))}
</Box>
</Box>
))
) : (
<Box color="bad">No Devices Found</Box>
)}
</Section>
</Window.Content>
</Window>
);
};

View File

@@ -54,7 +54,7 @@ export const VorePanelEditToggle = (props: {
</Stack.Item>
<Stack.Item>
<Button
icon={persistEditMode ? 'lock' : 'lock-open'}
icon={persistEditMode ? 'lock-open' : 'lock'}
selected={persistEditMode}
tooltip={
(persistEditMode ? 'Dis' : 'En') +

View File

@@ -144,6 +144,7 @@
#include "code\__defines\SDQL_2.dm"
#include "code\__defines\shadekin.dm"
#include "code\__defines\shields.dm"
#include "code\__defines\shock.dm"
#include "code\__defines\shuttle.dm"
#include "code\__defines\simple_mob.dm"
#include "code\__defines\size.dm"
@@ -2265,6 +2266,7 @@
#include "code\modules\client\preferences_toggle_procs.dm"
#include "code\modules\client\preferences_vr.dm"
#include "code\modules\client\record_updater.dm"
#include "code\modules\client\shock.dm"
#include "code\modules\client\spam_prevention.dm"
#include "code\modules\client\stored_item.dm"
#include "code\modules\client\ui_style.dm"