From 57674e06d6afa1f8554e80bef49fff708aa138b1 Mon Sep 17 00:00:00 2001 From: Aleksej Komarov Date: Thu, 16 Apr 2020 08:59:17 +0300 Subject: [PATCH] Shared tgui states, global Byond API object --- code/modules/tgui/external.dm | 8 ++ code/modules/tgui/tgui.dm | 29 +++-- tgui/packages/tgui/backend.js | 123 +++++++++++++++--- tgui/packages/tgui/byond.js | 54 +++----- tgui/packages/tgui/index.js | 7 +- tgui/packages/tgui/interfaces/AirAlarm.js | 29 ++--- .../packages/tgui/interfaces/CameraConsole.js | 5 +- tgui/packages/tgui/interfaces/Cargo.js | 7 +- tgui/packages/tgui/interfaces/CargoExpress.js | 12 +- tgui/packages/tgui/interfaces/ChemFilter.js | 7 +- tgui/packages/tgui/interfaces/ChemMaster.js | 11 +- .../tgui/interfaces/ChemReactionChamber.js | 7 +- tgui/packages/tgui/interfaces/KitchenSink.js | 7 +- .../interfaces/MalfunctionModulePicker.js | 7 +- tgui/packages/tgui/interfaces/Uplink.js | 7 +- tgui/packages/tgui/logging.js | 6 +- tgui/packages/tgui/public/tgui.html | 64 ++++++++- tgui/packages/tgui/store.js | 45 ------- 18 files changed, 249 insertions(+), 186 deletions(-) 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) => {