diff --git a/tgui/packages/tgui-panel/index.jsx b/tgui/packages/tgui-panel/index.jsx index 9f43a3091e25..6a365114cef2 100644 --- a/tgui/packages/tgui-panel/index.jsx +++ b/tgui/packages/tgui-panel/index.jsx @@ -23,6 +23,7 @@ import { pingMiddleware, pingReducer } from './ping'; import { settingsMiddleware, settingsReducer } from './settings'; import { telemetryMiddleware } from './telemetry'; import { setGlobalStore } from 'tgui/backend'; +import { websocketMiddleware } from './websocket'; perf.mark('inception', window.performance?.timing?.navigationStart); perf.mark('init'); @@ -37,6 +38,7 @@ const store = configureStore({ }), middleware: { pre: [ + websocketMiddleware, chatMiddleware, pingMiddleware, telemetryMiddleware, diff --git a/tgui/packages/tgui-panel/settings/SettingsPanel.jsx b/tgui/packages/tgui-panel/settings/SettingsPanel.jsx index 07ef79754dfc..ee3af451764a 100644 --- a/tgui/packages/tgui-panel/settings/SettingsPanel.jsx +++ b/tgui/packages/tgui-panel/settings/SettingsPanel.jsx @@ -40,6 +40,8 @@ import { selectHighlightSettings, selectHighlightSettingById, } from './selectors'; +import { reconnectWebsocket, disconnectWebsocket } from '../websocket'; +import { chatRenderer } from '../chat/renderer'; export const SettingsPanel = (props, context) => { const activeTab = useSelector(context, selectActiveTab); @@ -71,6 +73,7 @@ export const SettingsPanel = (props, context) => { {activeTab === 'general' && } {activeTab === 'chatPage' && } {activeTab === 'textHighlight' && } + {activeTab === 'experimental' && } ); @@ -393,3 +396,91 @@ const TextHighlightSetting = (props, context) => { ); }; + +const ExperimentalSettings = (props, context) => { + const { websocketEnabled, websocketServer } = useSelector( + context, + selectSettings, + ); + const dispatch = useDispatch(context); + + return ( +
+ + + + + + dispatch( + updateSettings({ + websocketEnabled: !websocketEnabled, + }), + ) + } + /> +
+ ); +}; diff --git a/tgui/packages/tgui-panel/settings/constants.js b/tgui/packages/tgui-panel/settings/constants.js index 6c86f4c24618..2479d57b3a51 100644 --- a/tgui/packages/tgui-panel/settings/constants.js +++ b/tgui/packages/tgui-panel/settings/constants.js @@ -18,6 +18,10 @@ export const SETTINGS_TABS = [ id: 'chatPage', name: 'Chat Tabs', }, + { + id: 'experimental', + name: 'Experimental', + }, ]; export const FONTS_DISABLED = 'Default'; diff --git a/tgui/packages/tgui-panel/settings/reducer.js b/tgui/packages/tgui-panel/settings/reducer.js index 9391950e758c..b41a323c051a 100644 --- a/tgui/packages/tgui-panel/settings/reducer.js +++ b/tgui/packages/tgui-panel/settings/reducer.js @@ -38,6 +38,8 @@ const initialState = { visible: false, activeTab: SETTINGS_TABS[0].id, }, + websocketEnabled: false, + websocketServer: '', }; export const settingsReducer = (state = initialState, action) => { diff --git a/tgui/packages/tgui-panel/websocket.ts b/tgui/packages/tgui-panel/websocket.ts new file mode 100644 index 000000000000..48ec466a7bea --- /dev/null +++ b/tgui/packages/tgui-panel/websocket.ts @@ -0,0 +1,135 @@ +import { createAction } from 'common/redux'; +import { chatRenderer } from './chat/renderer'; +import { loadSettings, updateSettings } from './settings/actions'; +import { selectSettings } from './settings/selectors'; + +const sendWSNotice = (message, small = false) => { + chatRenderer.processBatch([ + { + html: small + ? `${message}` + : `
${message}
`, + }, + ]); +}; + +export const reconnectWebsocket = createAction('websocket/reconnect'); +export const disconnectWebsocket = createAction('websocket/disconnect'); + +// Websocket close codes +const WEBSOCKET_DISABLED = 4555; +const WEBSOCKET_REATTEMPT = 4556; + +export const websocketMiddleware = (store) => { + let websocket: WebSocket | null = null; + + const setupWebsocket = (store) => { + const { websocketEnabled, websocketServer } = selectSettings( + store.getState(), + ); + if (!websocketEnabled) { + websocket?.close(WEBSOCKET_REATTEMPT); + return; + } + + websocket?.close(WEBSOCKET_REATTEMPT); + + try { + websocket = new WebSocket(`ws://${websocketServer}`); + } catch (e) { + if (e.name === 'SyntaxError') { + sendWSNotice( + `Error creating websocket: Invalid address! Make sure you're following the placeholder. Example: localhost:1234`, + ); + return; + } + sendWSNotice(`Error creating websocket: ${e.name} - ${e.message}`); + return; + } + + websocket.addEventListener('open', () => { + sendWSNotice('Websocket connected!', true); + }); + + websocket.addEventListener('close', function closeEventThing(ev) { + const { websocketEnabled } = selectSettings(store.getState()); + if (!websocketEnabled) { + // Doing this because eitherwise it 'close' will get called + // thousands of times per second if the connection wasn't closed properly. + // I don't know WHY it does that but it just does. + ev.target?.removeEventListener('close', closeEventThing); + websocket?.removeEventListener('close', closeEventThing); + return; + } + if (ev.code !== WEBSOCKET_DISABLED && ev.code !== WEBSOCKET_REATTEMPT) { + sendWSNotice( + `Websocket disconnected! Code: ${ev.code} Reason: ${ev.reason || 'None provided'}`, + ); + } + }); + + websocket.addEventListener('error', () => { + // Really don't think we should do anything here. + // setTimeout(() => setupWebsocket(store), 2000); + }); + }; + + setTimeout(() => setupWebsocket(store)); + + return (next) => (action) => { + const { type, payload } = action as { + type: string; + payload: { + websocketEnabled: boolean; + websocketServer: string; + }; + }; + if (!payload) return next(action); + if (type === updateSettings.type || type === loadSettings.type) { + if (typeof payload?.websocketEnabled === 'undefined') { + store.dispatch( + updateSettings({ + websocketEnabled: false, + }), + ); + return next(action); + } + if (!payload.websocketEnabled) { + websocket?.close(WEBSOCKET_DISABLED); + websocket = null; + } else if ( + !websocket || + websocket.url !== payload.websocketServer || + (payload.websocketEnabled && + (!websocket || websocket.readyState !== websocket.OPEN)) + ) { + websocket?.close(WEBSOCKET_REATTEMPT, 'Websocket settings changed'); + sendWSNotice('Websocket enabled.', true); + setupWebsocket(store); + } + return next(action); + } + + if (type === reconnectWebsocket.type) { + const settings = selectSettings(store.getState()); + if (settings.websocketEnabled) setupWebsocket(store); + return next(action); + } + + if (type === disconnectWebsocket.type) { + websocket?.close(WEBSOCKET_DISABLED); + websocket = null; + sendWSNotice('Websocket forcefully disconnected.', true); + } + + websocket && + websocket.readyState === websocket.OPEN && + websocket?.send( + JSON.stringify({ + type, + payload, + }), + ); + return next(action); + }; +};