diff --git a/code/modules/tgui/external.dm b/code/modules/tgui/external.dm
index 4820f2d5828..5de54c439cc 100644
--- a/code/modules/tgui/external.dm
+++ b/code/modules/tgui/external.dm
@@ -107,6 +107,14 @@
/datum/proc/ui_host(mob/user)
return src // Default src.
+/**
+ * global
+ *
+ * Associative list of JSON-encoded shared states that were set by
+ * tgui clients.
+ */
+/datum/var/list/tgui_shared_states
+
/**
* global
*
diff --git a/code/modules/tgui/tgui.dm b/code/modules/tgui/tgui.dm
index fd3a5da5c58..3a983bd37ed 100644
--- a/code/modules/tgui/tgui.dm
+++ b/code/modules/tgui/tgui.dm
@@ -42,8 +42,6 @@
var/datum/tgui/master_ui
/// Children of this UI.
var/list/datum/tgui/children = list()
- // TODO: Remove in favor of useGlobal
- var/ui_screen = "home"
/**
* public
@@ -197,7 +195,6 @@
json_data["config"] = list(
"title" = title,
"status" = status,
- "screen" = ui_screen,
"interface" = interface,
"fancy" = user.client.prefs.tgui_fancy,
"locked" = user.client.prefs.tgui_lock,
@@ -213,6 +210,10 @@
if(!isnull(static_data))
json_data["static_data"] = static_data
+ // Send shared states
+ if(src_object.tgui_shared_states)
+ json_data["shared"] = src_object.tgui_shared_states
+
// Generate the JSON.
var/json = json_encode(json_data)
// Strip #255/improper.
@@ -238,9 +239,12 @@
if("tgui:initialize")
user << output(_initial_update, "[window_id].browser:update")
initialized = TRUE
- if("tgui:view")
- if(params["screen"])
- ui_screen = params["screen"]
+ if("tgui:setSharedState")
+ var/key = params["key"]
+ var/value = params["value"]
+ if(!src_object.tgui_shared_states)
+ src_object.tgui_shared_states = list()
+ src_object.tgui_shared_states[key] = value
SStgui.update_uis(src_object)
if("tgui:log")
// Force window to show frills on fatal errors
@@ -249,14 +253,13 @@
log_message(params["log"])
if("tgui:link")
user << link(params["url"])
- if("tgui:fancy")
- user.client.prefs.tgui_fancy = TRUE
- if("tgui:nofrills")
- user.client.prefs.tgui_fancy = FALSE
else
- update_status(push = FALSE) // Update the window state.
- if(src_object.ui_act(action, params, src, state)) // Call ui_act() on the src_object.
- SStgui.update_uis(src_object) // Update if the object requested it.
+ // Update the window state.
+ update_status(push = FALSE)
+ // Call ui_act() on the src_object.
+ if(src_object.ui_act(action, params, src, state))
+ // Update if the object requested it.
+ SStgui.update_uis(src_object)
/**
* private
diff --git a/tgui/packages/tgui/backend.js b/tgui/packages/tgui/backend.js
index 65cef786451..0c354834876 100644
--- a/tgui/packages/tgui/backend.js
+++ b/tgui/packages/tgui/backend.js
@@ -1,6 +1,3 @@
-import { act } from './byond';
-import { UI_DISABLED, UI_INTERACTIVE } from './constants';
-
/**
* This file provides a clear separation layer between backend updates
* and what state our React app sees.
@@ -10,21 +7,23 @@ import { UI_DISABLED, UI_INTERACTIVE } from './constants';
* the response with already existing state.
*/
-/**
- * Creates a backend update action.
- */
+import { UI_DISABLED, UI_INTERACTIVE } from './constants';
+import { callByond } from './byond';
+
export const backendUpdate = state => ({
- type: 'backendUpdate',
+ type: 'backend/update',
payload: state,
});
-/**
- * Precisely defines state changes.
- */
+export const backendSetSharedState = (key, nextState) => ({
+ type: 'backend/setSharedState',
+ payload: { key, nextState },
+});
+
export const backendReducer = (state, action) => {
const { type, payload } = action;
- if (type === 'backendUpdate') {
+ if (type === 'backend/update') {
// Merge config
const config = {
...state.config,
@@ -36,6 +35,19 @@ export const backendReducer = (state, action) => {
...payload.static_data,
...payload.data,
};
+ // Merge shared states
+ const shared = { ...state.shared };
+ if (payload.shared) {
+ for (let key of Object.keys(payload.shared)) {
+ const value = payload.shared[key];
+ if (value === '') {
+ shared[key] = undefined;
+ }
+ else {
+ shared[key] = JSON.parse(value);
+ }
+ }
+ }
// Calculate our own fields
const visible = config.status !== UI_DISABLED;
const interactive = config.status === UI_INTERACTIVE;
@@ -44,11 +56,23 @@ export const backendReducer = (state, action) => {
...state,
config,
data,
+ shared,
visible,
interactive,
};
}
+ if (type === 'backend/setSharedState') {
+ const { key, nextState } = payload;
+ return {
+ ...state,
+ shared: {
+ ...state.shared,
+ [key]: nextState,
+ },
+ };
+ }
+
return state;
};
@@ -84,15 +108,78 @@ export const backendReducer = (state, action) => {
* }}
*/
export const useBackend = context => {
- // TODO: Dispatch "act" calls as Redux actions
const { store } = context;
const state = store.getState();
const ref = state.config.ref;
- const boundAct = (action, params = {}) => {
- act(ref, action, params);
- };
- return {
- ...state,
- act: boundAct,
+ const act = (action, params = {}) => {
+ callByond('', {
+ src: ref,
+ action,
+ ...params,
+ });
};
+ return { ...state, act };
+};
+
+/**
+ * Allocates state on Redux store without sharing it with other clients.
+ *
+ * Use it when you want to have a stateful variable in your component
+ * that persists between renders, but will be forgotten after you close
+ * the UI.
+ *
+ * It is a lot more performant than `setSharedState`.
+ *
+ * @param {any} context React context.
+ * @param {string} key Key which uniquely identifies this state in Redux store.
+ * @param {any} initialState Initializes your global variable with this value.
+ */
+export const useLocalState = (context, key, initialState) => {
+ const { store } = context;
+ const state = store.getState();
+ const sharedStates = state.shared ?? {};
+ const sharedState = (key in sharedStates)
+ ? sharedStates[key]
+ : initialState;
+ return [
+ sharedState,
+ nextState => {
+ store.dispatch(backendSetSharedState(key, nextState));
+ },
+ ];
+};
+
+/**
+ * Allocates state on Redux store, and **shares** it with other clients
+ * in the game.
+ *
+ * Use it when you want to have a stateful variable in your component
+ * that persists not only between renders, but also gets pushed to other
+ * clients that observe this UI.
+ *
+ * This makes creation of observable s
+ *
+ * @param {any} context React context.
+ * @param {string} key Key which uniquely identifies this state in Redux store.
+ * @param {any} initialState Initializes your global variable with this value.
+ */
+export const useSharedState = (context, key, initialState) => {
+ const { store } = context;
+ const state = store.getState();
+ const ref = state.config.ref;
+ const sharedStates = state.shared ?? {};
+ const sharedState = (key in sharedStates)
+ ? sharedStates[key]
+ : initialState;
+ return [
+ sharedState,
+ nextState => {
+ callByond('', {
+ src: ref,
+ action: 'tgui:setSharedState',
+ key,
+ value: JSON.stringify(nextState) || '',
+ });
+ },
+ ];
};
diff --git a/tgui/packages/tgui/byond.js b/tgui/packages/tgui/byond.js
index 56a407e6042..dc4e681bf10 100644
--- a/tgui/packages/tgui/byond.js
+++ b/tgui/packages/tgui/byond.js
@@ -1,12 +1,12 @@
-import { buildQueryString } from 'common/string';
+// Reference a global Byond object
+const { Byond } = window;
/**
* Version of Trident engine used in Internet Explorer.
+ * An integer number or `null` if this is not a trident engine.
*
* - IE 8 - Trident 4.0
* - IE 11 - Trident 7.0
- *
- * @return An integer number or 'null' if this is not a trident engine.
*/
const tridentVersion = (() => {
const groups = navigator.userAgent.match(/Trident\/(\d+).+?;/i);
@@ -27,41 +27,23 @@ export const IS_IE8 = tridentVersion !== null
&& tridentVersion <= 4;
/**
- * True if browser is a BYOND browser.
+ * Makes a BYOND call.
+ *
+ * If path is empty, this will trigger a Topic call.
+ * You can reference a specific object by setting the "src" parameter.
+ *
+ * See: https://secure.byond.com/docs/ref/skinparams.html
*/
-// We're currently just checking, whether we're running on a localhost
-// with a path similar to a BYOND cache. I couldn't find a better/faster
-// non-invasive method of doing this.
-export const IS_BYOND = tridentVersion !== null
- && location.hostname === '127.0.0.1'
- && location.pathname.startsWith('/tmp');
-
-/**
- * Helper to generate a BYOND href given 'params' as an object
- * (with an optional 'url' for eg winset).
- */
-const createByondUrl = (path, params = {}) => {
- return 'byond://' + path + '?' + buildQueryString(params);
-};
-
export const callByond = (path, params = {}) => {
- // Abort BYOND calls when we're running in a normal browser
- if (!IS_BYOND) {
- return;
- }
- location.href = createByondUrl(path, params);
+ Byond.call(path, params);
};
/**
- * A high-level abstraction of BYJAX. Makes a call to BYOND and returns
+ * A high-level abstraction of BYOND calls. Makes a BYOND call and returns
* a promise, which (if endpoint has a callback parameter) resolves
* with the return value of that call.
*/
export const callByondAsync = (path, params = {}) => {
- // Abort BYOND calls when we're running in a normal browser
- if (!IS_BYOND) {
- return new Promise(() => {});
- }
// Create a callback array if it doesn't exist yet
window.__callbacks__ = window.__callbacks__ || [];
// Create a Promise and push its resolve function into callback array
@@ -71,7 +53,7 @@ export const callByondAsync = (path, params = {}) => {
window.__callbacks__.push(resolve);
});
// Call BYOND client
- callByond(path, {
+ Byond.call(path, {
...params,
callback: `__callbacks__[${callbackIndex}]`,
});
@@ -79,18 +61,12 @@ export const callByondAsync = (path, params = {}) => {
};
/**
- * Literally types a command on the client.
+ * Runs a BYOND skin command
+ *
+ * See: https://secure.byond.com/docs/ref/skinparams.html
*/
export const runCommand = command => callByond('winset', { command });
-/**
- * Helper to make a BYOND ui_act() call on the UI 'src' given an 'action'
- * and optional 'params'.
- */
-export const act = (src, action, params = {}) => {
- return callByond('', { src, action, ...params });
-};
-
/**
* Calls 'winget' on a BYOND skin element, retrieving value by the 'key'.
*/
diff --git a/tgui/packages/tgui/index.js b/tgui/packages/tgui/index.js
index 818158f0958..645d8691a0b 100644
--- a/tgui/packages/tgui/index.js
+++ b/tgui/packages/tgui/index.js
@@ -11,15 +11,12 @@ import { setupHotReloading } from 'tgui-dev-server/link/client';
import { backendUpdate } from './backend';
import { IS_IE8 } from './byond';
import { setupDrag } from './drag';
-import { createLogger } from './logging';
+import { logger } from './logging';
import { createStore, StoreProvider } from './store';
const enteredBundleAt = Date.now();
-
-const logger = createLogger();
const store = createStore();
let reactRoot;
-
let initialRender = true;
const renderLayout = () => {
@@ -117,7 +114,7 @@ const setupApp = () => {
// Subscribe for bankend updates
window.update = stateJson => {
// NOTE: stateJson can be an object only if called manually from console.
- // This is useful for debugging tgui in proper browsers, like Chrome.
+ // This is useful for debugging tgui in external browsers, like Chrome.
const state = typeof stateJson === 'string'
? parseStateJson(stateJson)
: stateJson;
diff --git a/tgui/packages/tgui/interfaces/AirAlarm.js b/tgui/packages/tgui/interfaces/AirAlarm.js
index 5d5755ba067..c6f440985aa 100644
--- a/tgui/packages/tgui/interfaces/AirAlarm.js
+++ b/tgui/packages/tgui/interfaces/AirAlarm.js
@@ -1,7 +1,7 @@
import { toFixed } from 'common/math';
import { decodeHtmlEntities } from 'common/string';
import { Fragment } from 'inferno';
-import { useBackend } from '../backend';
+import { useBackend, useLocalState } from '../backend';
import { Box, Button, LabeledList, NumberInput, Section } from '../components';
import { getGasLabel } from '../constants';
import { Window } from '../layouts';
@@ -114,19 +114,17 @@ const AIR_ALARM_ROUTES = {
};
const AirAlarmControl = (props, context) => {
- const { act, config } = useBackend(context);
- const route = AIR_ALARM_ROUTES[config.screen] || AIR_ALARM_ROUTES.home;
+ const [screen, setScreen] = useLocalState(context, 'screen');
+ const route = AIR_ALARM_ROUTES[screen] || AIR_ALARM_ROUTES.home;
const Component = route.component();
return (
act('tgui:view', {
- screen: 'home',
- })} />
+ onClick={() => setScreen()} />
)}>
@@ -139,6 +137,7 @@ const AirAlarmControl = (props, context) => {
const AirAlarmControlHome = (props, context) => {
const { act, data } = useBackend(context);
+ const [screen, setScreen] = useLocalState(context, 'screen');
const {
mode,
atmos_alarm,
@@ -166,30 +165,22 @@ const AirAlarmControlHome = (props, context) => {