tgui-core 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/{src/components → components}/AnimatedNumber.tsx +185 -185
  2. package/{src/components → components}/BlockQuote.tsx +15 -15
  3. package/{src/components → components}/BodyZoneSelector.tsx +149 -149
  4. package/{src/components → components}/Box.tsx +255 -255
  5. package/{src/components → components}/Button.tsx +415 -415
  6. package/{src/components → components}/ByondUi.jsx +121 -121
  7. package/{src/components → components}/Chart.tsx +160 -160
  8. package/{src/components → components}/ColorBox.tsx +30 -30
  9. package/{src/components → components}/Dimmer.tsx +19 -19
  10. package/{src/components → components}/Divider.tsx +26 -26
  11. package/{src/components → components}/DmIcon.tsx +72 -72
  12. package/{src/components → components}/DraggableControl.jsx +282 -282
  13. package/{src/components → components}/Dropdown.tsx +246 -246
  14. package/{src/components → components}/Flex.tsx +105 -105
  15. package/{src/components → components}/Icon.tsx +91 -91
  16. package/{src/components → components}/Input.tsx +181 -181
  17. package/{src/components → components}/KeyListener.tsx +40 -40
  18. package/{src/components → components}/Knob.tsx +185 -185
  19. package/{src/components → components}/LabeledList.tsx +130 -130
  20. package/{src/components → components}/MenuBar.tsx +233 -238
  21. package/{src/components → components}/Modal.tsx +25 -25
  22. package/{src/components → components}/NoticeBox.tsx +48 -48
  23. package/{src/components → components}/NumberInput.tsx +328 -328
  24. package/{src/components → components}/ProgressBar.tsx +79 -79
  25. package/{src/components → components}/RestrictedInput.jsx +301 -301
  26. package/{src/components → components}/RoundGauge.tsx +189 -189
  27. package/{src/components → components}/Section.tsx +125 -125
  28. package/{src/components → components}/Slider.tsx +173 -173
  29. package/{src/components → components}/Stack.tsx +101 -101
  30. package/{src/components → components}/Table.tsx +90 -90
  31. package/{src/components → components}/Tabs.tsx +90 -90
  32. package/{src/components → components}/TextArea.tsx +198 -198
  33. package/{src/components → components}/TimeDisplay.jsx +64 -64
  34. package/components/index.ts +51 -0
  35. package/{src/debug/KitchenSink.jsx → debug/KitchenSink.tsx} +56 -56
  36. package/{src/debug/actions.js → debug/actions.ts} +11 -11
  37. package/{src/debug/hooks.js → debug/hooks.ts} +10 -10
  38. package/{src/debug/middleware.js → debug/middleware.ts} +86 -86
  39. package/{src/debug/reducer.js → debug/reducer.ts} +27 -22
  40. package/{src/debug/selectors.js → debug/selectors.ts} +7 -7
  41. package/{src/layouts → layouts}/Layout.tsx +75 -75
  42. package/{src/layouts → layouts}/NtosWindow.tsx +162 -162
  43. package/{src/layouts → layouts}/Pane.tsx +56 -56
  44. package/{src/layouts → layouts}/Window.tsx +227 -227
  45. package/layouts/index.ts +10 -0
  46. package/package.json +3 -2
  47. package/src/assets.ts +43 -43
  48. package/src/backend.ts +369 -369
  49. package/src/drag.ts +280 -280
  50. package/src/events.ts +237 -237
  51. package/src/hotkeys.ts +212 -212
  52. package/src/renderer.ts +50 -50
  53. package/stories/Blink.stories.tsx +20 -0
  54. package/stories/BlockQuote.stories.tsx +23 -0
  55. package/stories/Box.stories.tsx +27 -0
  56. package/stories/Button.stories.tsx +68 -0
  57. package/stories/ByondUi.stories.tsx +45 -0
  58. package/stories/Collapsible.stories.tsx +23 -0
  59. package/stories/Flex.stories.tsx +68 -0
  60. package/stories/Input.stories.tsx +124 -0
  61. package/stories/LabeledList.stories.tsx +73 -0
  62. package/stories/Popper.stories.tsx +58 -0
  63. package/stories/ProgressBar.stories.tsx +58 -0
  64. package/stories/Stack.stories.tsx +55 -0
  65. package/stories/Storage.stories.tsx +46 -0
  66. package/stories/Themes.stories.tsx +30 -0
  67. package/stories/Tooltip.stories.tsx +48 -0
  68. package/stories/common.tsx +19 -0
  69. package/tsconfig.json +0 -21
  70. package/src/components/Grid.tsx +0 -44
  71. /package/{src/common → common}/collections.ts +0 -0
  72. /package/{src/common → common}/color.ts +0 -0
  73. /package/{src/common → common}/events.ts +0 -0
  74. /package/{src/common → common}/exhaustive.ts +0 -0
  75. /package/{src/common → common}/fp.ts +0 -0
  76. /package/{src/common → common}/keycodes.ts +0 -0
  77. /package/{src/common → common}/keys.ts +0 -0
  78. /package/{src/common → common}/math.ts +0 -0
  79. /package/{src/common → common}/perf.ts +0 -0
  80. /package/{src/common → common}/random.ts +0 -0
  81. /package/{src/common → common}/react.ts +0 -0
  82. /package/{src/common → common}/redux.ts +0 -0
  83. /package/{src/common → common}/storage.js +0 -0
  84. /package/{src/common → common}/string.ts +0 -0
  85. /package/{src/common → common}/timer.ts +0 -0
  86. /package/{src/common → common}/type-utils.ts +0 -0
  87. /package/{src/common → common}/types.ts +0 -0
  88. /package/{src/common → common}/uuid.ts +0 -0
  89. /package/{src/common → common}/vector.ts +0 -0
  90. /package/{src/components → components}/Autofocus.tsx +0 -0
  91. /package/{src/components → components}/Blink.jsx +0 -0
  92. /package/{src/components → components}/Collapsible.tsx +0 -0
  93. /package/{src/components → components}/Dialog.tsx +0 -0
  94. /package/{src/components → components}/FakeTerminal.jsx +0 -0
  95. /package/{src/components → components}/FitText.tsx +0 -0
  96. /package/{src/components → components}/Image.tsx +0 -0
  97. /package/{src/components → components}/InfinitePlane.jsx +0 -0
  98. /package/{src/components → components}/LabeledControls.tsx +0 -0
  99. /package/{src/components → components}/Popper.tsx +0 -0
  100. /package/{src/components → components}/StyleableSection.tsx +0 -0
  101. /package/{src/components → components}/Tooltip.tsx +0 -0
  102. /package/{src/components → components}/TrackOutsideClicks.tsx +0 -0
  103. /package/{src/components → components}/VirtualList.tsx +0 -0
  104. /package/{src/debug → debug}/index.ts +0 -0
  105. /package/{src/styles → styles}/base.scss +0 -0
  106. /package/{src/styles → styles}/colors.scss +0 -0
  107. /package/{src/styles → styles}/components/BlockQuote.scss +0 -0
  108. /package/{src/styles → styles}/components/Button.scss +0 -0
  109. /package/{src/styles → styles}/components/ColorBox.scss +0 -0
  110. /package/{src/styles → styles}/components/Dialog.scss +0 -0
  111. /package/{src/styles → styles}/components/Dimmer.scss +0 -0
  112. /package/{src/styles → styles}/components/Divider.scss +0 -0
  113. /package/{src/styles → styles}/components/Dropdown.scss +0 -0
  114. /package/{src/styles → styles}/components/Flex.scss +0 -0
  115. /package/{src/styles → styles}/components/Icon.scss +0 -0
  116. /package/{src/styles → styles}/components/Input.scss +0 -0
  117. /package/{src/styles → styles}/components/Knob.scss +0 -0
  118. /package/{src/styles → styles}/components/LabeledList.scss +0 -0
  119. /package/{src/styles → styles}/components/MenuBar.scss +0 -0
  120. /package/{src/styles → styles}/components/Modal.scss +0 -0
  121. /package/{src/styles → styles}/components/NoticeBox.scss +0 -0
  122. /package/{src/styles → styles}/components/NumberInput.scss +0 -0
  123. /package/{src/styles → styles}/components/ProgressBar.scss +0 -0
  124. /package/{src/styles → styles}/components/RoundGauge.scss +0 -0
  125. /package/{src/styles → styles}/components/Section.scss +0 -0
  126. /package/{src/styles → styles}/components/Slider.scss +0 -0
  127. /package/{src/styles → styles}/components/Stack.scss +0 -0
  128. /package/{src/styles → styles}/components/Table.scss +0 -0
  129. /package/{src/styles → styles}/components/Tabs.scss +0 -0
  130. /package/{src/styles → styles}/components/TextArea.scss +0 -0
  131. /package/{src/styles → styles}/components/Tooltip.scss +0 -0
  132. /package/{src/styles → styles}/functions.scss +0 -0
  133. /package/{src/styles → styles}/layouts/Layout.scss +0 -0
  134. /package/{src/styles → styles}/layouts/NtosHeader.scss +0 -0
  135. /package/{src/styles → styles}/layouts/NtosWindow.scss +0 -0
  136. /package/{src/styles → styles}/layouts/TitleBar.scss +0 -0
  137. /package/{src/styles → styles}/layouts/Window.scss +0 -0
  138. /package/{src/styles → styles}/main.scss +0 -0
  139. /package/{src/styles → styles}/reset.scss +0 -0
  140. /package/{src/styles → styles}/themes/abductor.scss +0 -0
  141. /package/{src/styles → styles}/themes/admin.scss +0 -0
  142. /package/{src/styles → styles}/themes/cardtable.scss +0 -0
  143. /package/{src/styles → styles}/themes/hackerman.scss +0 -0
  144. /package/{src/styles → styles}/themes/malfunction.scss +0 -0
  145. /package/{src/styles → styles}/themes/neutral.scss +0 -0
  146. /package/{src/styles → styles}/themes/ntOS95.scss +0 -0
  147. /package/{src/styles → styles}/themes/ntos.scss +0 -0
  148. /package/{src/styles → styles}/themes/ntos_cat.scss +0 -0
  149. /package/{src/styles → styles}/themes/ntos_darkmode.scss +0 -0
  150. /package/{src/styles → styles}/themes/ntos_lightmode.scss +0 -0
  151. /package/{src/styles → styles}/themes/ntos_spooky.scss +0 -0
  152. /package/{src/styles → styles}/themes/ntos_synth.scss +0 -0
  153. /package/{src/styles → styles}/themes/ntos_terminal.scss +0 -0
  154. /package/{src/styles → styles}/themes/paper.scss +0 -0
  155. /package/{src/styles → styles}/themes/retro.scss +0 -0
  156. /package/{src/styles → styles}/themes/spookyconsole.scss +0 -0
  157. /package/{src/styles → styles}/themes/syndicate.scss +0 -0
  158. /package/{src/styles → styles}/themes/wizard.scss +0 -0
package/src/backend.ts CHANGED
@@ -1,369 +1,369 @@
1
- /**
2
- * This file provides a clear separation layer between backend updates
3
- * and what state our React app sees.
4
- *
5
- * Sometimes backend can response without a "data" field, but our final
6
- * state will still contain previous "data" because we are merging
7
- * the response with already existing state.
8
- *
9
- * @file
10
- * @copyright 2020 Aleksej Komarov
11
- * @license MIT
12
- */
13
-
14
- import { perf } from './common/perf';
15
- import { createAction } from './common/redux';
16
-
17
- import { setupDrag } from './drag';
18
- import { globalEvents } from './events';
19
- import { focusMap } from './focus';
20
- import { resumeRenderer, suspendRenderer } from './renderer';
21
-
22
- export let globalStore;
23
-
24
- export const setGlobalStore = (store) => {
25
- globalStore = store;
26
- };
27
-
28
- export const backendUpdate = createAction('backend/update');
29
- export const backendSetSharedState = createAction('backend/setSharedState');
30
- export const backendSuspendStart = createAction('backend/suspendStart');
31
-
32
- export const backendSuspendSuccess = () => ({
33
- type: 'backend/suspendSuccess',
34
- payload: {
35
- timestamp: Date.now(),
36
- },
37
- });
38
-
39
- const initialState = {
40
- config: {},
41
- data: {},
42
- shared: {},
43
- // Start as suspended
44
- suspended: Date.now(),
45
- suspending: false,
46
- };
47
-
48
- export const backendReducer = (state = initialState, action) => {
49
- const { type, payload } = action;
50
-
51
- if (type === 'backend/update') {
52
- // Merge config
53
- const config = {
54
- ...state.config,
55
- ...payload.config,
56
- };
57
- // Merge data
58
- const data = {
59
- ...state.data,
60
- ...payload.static_data,
61
- ...payload.data,
62
- };
63
- // Merge shared states
64
- const shared = { ...state.shared };
65
- if (payload.shared) {
66
- for (let key of Object.keys(payload.shared)) {
67
- const value = payload.shared[key];
68
- if (value === '') {
69
- shared[key] = undefined;
70
- } else {
71
- shared[key] = JSON.parse(value);
72
- }
73
- }
74
- }
75
- // Return new state
76
- return {
77
- ...state,
78
- config,
79
- data,
80
- shared,
81
- suspended: false,
82
- };
83
- }
84
-
85
- if (type === 'backend/setSharedState') {
86
- const { key, nextState } = payload;
87
- return {
88
- ...state,
89
- shared: {
90
- ...state.shared,
91
- [key]: nextState,
92
- },
93
- };
94
- }
95
-
96
- if (type === 'backend/suspendStart') {
97
- return {
98
- ...state,
99
- suspending: true,
100
- };
101
- }
102
-
103
- if (type === 'backend/suspendSuccess') {
104
- const { timestamp } = payload;
105
- return {
106
- ...state,
107
- data: {},
108
- shared: {},
109
- config: {
110
- ...state.config,
111
- title: '',
112
- status: 1,
113
- },
114
- suspending: false,
115
- suspended: timestamp,
116
- };
117
- }
118
-
119
- return state;
120
- };
121
-
122
- export const backendMiddleware = (store) => {
123
- let fancyState;
124
- let suspendInterval;
125
-
126
- return (next) => (action) => {
127
- const { suspended } = selectBackend(store.getState());
128
- const { type, payload } = action;
129
-
130
- if (type === 'update') {
131
- store.dispatch(backendUpdate(payload));
132
- return;
133
- }
134
-
135
- if (type === 'suspend') {
136
- store.dispatch(backendSuspendSuccess());
137
- return;
138
- }
139
-
140
- if (type === 'ping') {
141
- Byond.sendMessage('ping/reply');
142
- return;
143
- }
144
-
145
- if (type === 'byond/mousedown') {
146
- globalEvents.emit('byond/mousedown');
147
- }
148
-
149
- if (type === 'byond/mouseup') {
150
- globalEvents.emit('byond/mouseup');
151
- }
152
-
153
- if (type === 'byond/ctrldown') {
154
- globalEvents.emit('byond/ctrldown');
155
- }
156
-
157
- if (type === 'byond/ctrlup') {
158
- globalEvents.emit('byond/ctrlup');
159
- }
160
-
161
- if (type === 'backend/suspendStart' && !suspendInterval) {
162
- // Keep sending suspend messages until it succeeds.
163
- // It may fail multiple times due to topic rate limiting.
164
- const suspendFn = () => Byond.sendMessage('suspend');
165
- suspendFn();
166
- suspendInterval = setInterval(suspendFn, 2000);
167
- }
168
-
169
- if (type === 'backend/suspendSuccess') {
170
- suspendRenderer();
171
- clearInterval(suspendInterval);
172
- suspendInterval = undefined;
173
- Byond.winset(Byond.windowId, {
174
- 'is-visible': false,
175
- });
176
- setTimeout(() => focusMap());
177
- }
178
-
179
- if (type === 'backend/update') {
180
- const fancy = payload.config?.window?.fancy;
181
- // Initialize fancy state
182
- if (fancyState === undefined) {
183
- fancyState = fancy;
184
- }
185
- // React to changes in fancy
186
- else if (fancyState !== fancy) {
187
- fancyState = fancy;
188
- Byond.winset(Byond.windowId, {
189
- titlebar: !fancy,
190
- 'can-resize': !fancy,
191
- });
192
- }
193
- }
194
-
195
- // Resume on incoming update
196
- if (type === 'backend/update' && suspended) {
197
- // Signal renderer that we have resumed
198
- resumeRenderer();
199
- // Setup drag
200
- setupDrag();
201
- // We schedule this for the next tick here because resizing and unhiding
202
- // during the same tick will flash with a white background.
203
- setTimeout(() => {
204
- perf.mark('resume/start');
205
- // Doublecheck if we are not re-suspended.
206
- const { suspended } = selectBackend(store.getState());
207
- if (suspended) {
208
- return;
209
- }
210
- Byond.winset(Byond.windowId, {
211
- 'is-visible': true,
212
- });
213
- perf.mark('resume/finish');
214
- });
215
- }
216
-
217
- return next(action);
218
- };
219
- };
220
-
221
- /**
222
- * Sends an action to `ui_act` on `src_object` that this tgui window
223
- * is associated with.
224
- */
225
- export const sendAct = (action: string, payload: object = {}) => {
226
- // Validate that payload is an object
227
- // prettier-ignore
228
- const isObject = typeof payload === 'object'
229
- && payload !== null
230
- && !Array.isArray(payload);
231
- if (!isObject) {
232
- return;
233
- }
234
- Byond.sendMessage('act/' + action, payload);
235
- };
236
-
237
- type BackendState<TData> = {
238
- config: {
239
- title: string;
240
- status: number;
241
- interface: string;
242
- refreshing: boolean;
243
- window: {
244
- key: string;
245
- size: [number, number];
246
- fancy: boolean;
247
- locked: boolean;
248
- };
249
- client: {
250
- ckey: string;
251
- address: string;
252
- computer_id: string;
253
- };
254
- user: {
255
- name: string;
256
- observer: number;
257
- };
258
- };
259
- data: TData;
260
- shared: Record<string, any>;
261
- suspending: boolean;
262
- suspended: boolean;
263
- };
264
-
265
- /**
266
- * Selects a backend-related slice of Redux state
267
- */
268
- export const selectBackend = <TData>(state: any): BackendState<TData> =>
269
- state.backend || {};
270
-
271
- /**
272
- * Get data from tgui backend.
273
- *
274
- * Includes the `act` function for performing DM actions.
275
- */
276
- export const useBackend = <TData>() => {
277
- const state: BackendState<TData> = globalStore?.getState()?.backend;
278
-
279
- return {
280
- ...state,
281
- act: sendAct,
282
- };
283
- };
284
-
285
- /**
286
- * A tuple that contains the state and a setter function for it.
287
- */
288
- type StateWithSetter<T> = [T, (nextState: T) => void];
289
-
290
- /**
291
- * Allocates state on Redux store without sharing it with other clients.
292
- *
293
- * Use it when you want to have a stateful variable in your component
294
- * that persists between renders, but will be forgotten after you close
295
- * the UI.
296
- *
297
- * It is a lot more performant than `setSharedState`.
298
- *
299
- * @param context React context.
300
- * @param key Key which uniquely identifies this state in Redux store.
301
- * @param initialState Initializes your global variable with this value.
302
- * @deprecated Use useState and useEffect when you can. Pass the state as a prop.
303
- */
304
- export const useLocalState = <T>(
305
- key: string,
306
- initialState: T
307
- ): StateWithSetter<T> => {
308
- const state = globalStore?.getState()?.backend;
309
- const sharedStates = state?.shared ?? {};
310
- const sharedState = key in sharedStates ? sharedStates[key] : initialState;
311
- return [
312
- sharedState,
313
- (nextState) => {
314
- globalStore.dispatch(
315
- backendSetSharedState({
316
- key,
317
- nextState:
318
- typeof nextState === 'function'
319
- ? nextState(sharedState)
320
- : nextState,
321
- })
322
- );
323
- },
324
- ];
325
- };
326
-
327
- /**
328
- * Allocates state on Redux store, and **shares** it with other clients
329
- * in the game.
330
- *
331
- * Use it when you want to have a stateful variable in your component
332
- * that persists not only between renders, but also gets pushed to other
333
- * clients that observe this UI.
334
- *
335
- * This makes creation of observable s
336
- *
337
- * @param context React context.
338
- * @param key Key which uniquely identifies this state in Redux store.
339
- * @param initialState Initializes your global variable with this value.
340
- */
341
- export const useSharedState = <T>(
342
- key: string,
343
- initialState: T
344
- ): StateWithSetter<T> => {
345
- const state = globalStore?.getState()?.backend;
346
- const sharedStates = state?.shared ?? {};
347
- const sharedState = key in sharedStates ? sharedStates[key] : initialState;
348
- return [
349
- sharedState,
350
- (nextState) => {
351
- Byond.sendMessage({
352
- type: 'setSharedState',
353
- key,
354
- value:
355
- JSON.stringify(
356
- typeof nextState === 'function' ? nextState(sharedState) : nextState
357
- ) || '',
358
- });
359
- },
360
- ];
361
- };
362
-
363
- export const useDispatch = () => {
364
- return globalStore.dispatch;
365
- };
366
-
367
- export const useSelector = (selector: (state: any) => any) => {
368
- return selector(globalStore?.getState());
369
- };
1
+ /**
2
+ * This file provides a clear separation layer between backend updates
3
+ * and what state our React app sees.
4
+ *
5
+ * Sometimes backend can response without a "data" field, but our final
6
+ * state will still contain previous "data" because we are merging
7
+ * the response with already existing state.
8
+ *
9
+ * @file
10
+ * @copyright 2020 Aleksej Komarov
11
+ * @license MIT
12
+ */
13
+
14
+ import { perf } from '../common/perf';
15
+ import { createAction } from '../common/redux';
16
+
17
+ import { setupDrag } from './drag';
18
+ import { globalEvents } from './events';
19
+ import { focusMap } from './focus';
20
+ import { resumeRenderer, suspendRenderer } from './renderer';
21
+
22
+ export let globalStore;
23
+
24
+ export const setGlobalStore = (store) => {
25
+ globalStore = store;
26
+ };
27
+
28
+ export const backendUpdate = createAction('backend/update');
29
+ export const backendSetSharedState = createAction('backend/setSharedState');
30
+ export const backendSuspendStart = createAction('backend/suspendStart');
31
+
32
+ export const backendSuspendSuccess = () => ({
33
+ type: 'backend/suspendSuccess',
34
+ payload: {
35
+ timestamp: Date.now(),
36
+ },
37
+ });
38
+
39
+ const initialState = {
40
+ config: {},
41
+ data: {},
42
+ shared: {},
43
+ // Start as suspended
44
+ suspended: Date.now(),
45
+ suspending: false,
46
+ };
47
+
48
+ export const backendReducer = (state = initialState, action) => {
49
+ const { type, payload } = action;
50
+
51
+ if (type === 'backend/update') {
52
+ // Merge config
53
+ const config = {
54
+ ...state.config,
55
+ ...payload.config,
56
+ };
57
+ // Merge data
58
+ const data = {
59
+ ...state.data,
60
+ ...payload.static_data,
61
+ ...payload.data,
62
+ };
63
+ // Merge shared states
64
+ const shared = { ...state.shared };
65
+ if (payload.shared) {
66
+ for (let key of Object.keys(payload.shared)) {
67
+ const value = payload.shared[key];
68
+ if (value === '') {
69
+ shared[key] = undefined;
70
+ } else {
71
+ shared[key] = JSON.parse(value);
72
+ }
73
+ }
74
+ }
75
+ // Return new state
76
+ return {
77
+ ...state,
78
+ config,
79
+ data,
80
+ shared,
81
+ suspended: false,
82
+ };
83
+ }
84
+
85
+ if (type === 'backend/setSharedState') {
86
+ const { key, nextState } = payload;
87
+ return {
88
+ ...state,
89
+ shared: {
90
+ ...state.shared,
91
+ [key]: nextState,
92
+ },
93
+ };
94
+ }
95
+
96
+ if (type === 'backend/suspendStart') {
97
+ return {
98
+ ...state,
99
+ suspending: true,
100
+ };
101
+ }
102
+
103
+ if (type === 'backend/suspendSuccess') {
104
+ const { timestamp } = payload;
105
+ return {
106
+ ...state,
107
+ data: {},
108
+ shared: {},
109
+ config: {
110
+ ...state.config,
111
+ title: '',
112
+ status: 1,
113
+ },
114
+ suspending: false,
115
+ suspended: timestamp,
116
+ };
117
+ }
118
+
119
+ return state;
120
+ };
121
+
122
+ export const backendMiddleware = (store) => {
123
+ let fancyState;
124
+ let suspendInterval;
125
+
126
+ return (next) => (action) => {
127
+ const { suspended } = selectBackend(store.getState());
128
+ const { type, payload } = action;
129
+
130
+ if (type === 'update') {
131
+ store.dispatch(backendUpdate(payload));
132
+ return;
133
+ }
134
+
135
+ if (type === 'suspend') {
136
+ store.dispatch(backendSuspendSuccess());
137
+ return;
138
+ }
139
+
140
+ if (type === 'ping') {
141
+ Byond.sendMessage('ping/reply');
142
+ return;
143
+ }
144
+
145
+ if (type === 'byond/mousedown') {
146
+ globalEvents.emit('byond/mousedown');
147
+ }
148
+
149
+ if (type === 'byond/mouseup') {
150
+ globalEvents.emit('byond/mouseup');
151
+ }
152
+
153
+ if (type === 'byond/ctrldown') {
154
+ globalEvents.emit('byond/ctrldown');
155
+ }
156
+
157
+ if (type === 'byond/ctrlup') {
158
+ globalEvents.emit('byond/ctrlup');
159
+ }
160
+
161
+ if (type === 'backend/suspendStart' && !suspendInterval) {
162
+ // Keep sending suspend messages until it succeeds.
163
+ // It may fail multiple times due to topic rate limiting.
164
+ const suspendFn = () => Byond.sendMessage('suspend');
165
+ suspendFn();
166
+ suspendInterval = setInterval(suspendFn, 2000);
167
+ }
168
+
169
+ if (type === 'backend/suspendSuccess') {
170
+ suspendRenderer();
171
+ clearInterval(suspendInterval);
172
+ suspendInterval = undefined;
173
+ Byond.winset(Byond.windowId, {
174
+ 'is-visible': false,
175
+ });
176
+ setTimeout(() => focusMap());
177
+ }
178
+
179
+ if (type === 'backend/update') {
180
+ const fancy = payload.config?.window?.fancy;
181
+ // Initialize fancy state
182
+ if (fancyState === undefined) {
183
+ fancyState = fancy;
184
+ }
185
+ // React to changes in fancy
186
+ else if (fancyState !== fancy) {
187
+ fancyState = fancy;
188
+ Byond.winset(Byond.windowId, {
189
+ titlebar: !fancy,
190
+ 'can-resize': !fancy,
191
+ });
192
+ }
193
+ }
194
+
195
+ // Resume on incoming update
196
+ if (type === 'backend/update' && suspended) {
197
+ // Signal renderer that we have resumed
198
+ resumeRenderer();
199
+ // Setup drag
200
+ setupDrag();
201
+ // We schedule this for the next tick here because resizing and unhiding
202
+ // during the same tick will flash with a white background.
203
+ setTimeout(() => {
204
+ perf.mark('resume/start');
205
+ // Doublecheck if we are not re-suspended.
206
+ const { suspended } = selectBackend(store.getState());
207
+ if (suspended) {
208
+ return;
209
+ }
210
+ Byond.winset(Byond.windowId, {
211
+ 'is-visible': true,
212
+ });
213
+ perf.mark('resume/finish');
214
+ });
215
+ }
216
+
217
+ return next(action);
218
+ };
219
+ };
220
+
221
+ /**
222
+ * Sends an action to `ui_act` on `src_object` that this tgui window
223
+ * is associated with.
224
+ */
225
+ export const sendAct = (action: string, payload: object = {}) => {
226
+ // Validate that payload is an object
227
+ // prettier-ignore
228
+ const isObject = typeof payload === 'object'
229
+ && payload !== null
230
+ && !Array.isArray(payload);
231
+ if (!isObject) {
232
+ return;
233
+ }
234
+ Byond.sendMessage('act/' + action, payload);
235
+ };
236
+
237
+ type BackendState<TData> = {
238
+ config: {
239
+ title: string;
240
+ status: number;
241
+ interface: string;
242
+ refreshing: boolean;
243
+ window: {
244
+ key: string;
245
+ size: [number, number];
246
+ fancy: boolean;
247
+ locked: boolean;
248
+ };
249
+ client: {
250
+ ckey: string;
251
+ address: string;
252
+ computer_id: string;
253
+ };
254
+ user: {
255
+ name: string;
256
+ observer: number;
257
+ };
258
+ };
259
+ data: TData;
260
+ shared: Record<string, any>;
261
+ suspending: boolean;
262
+ suspended: boolean;
263
+ };
264
+
265
+ /**
266
+ * Selects a backend-related slice of Redux state
267
+ */
268
+ export const selectBackend = <TData>(state: any): BackendState<TData> =>
269
+ state.backend || {};
270
+
271
+ /**
272
+ * Get data from tgui backend.
273
+ *
274
+ * Includes the `act` function for performing DM actions.
275
+ */
276
+ export const useBackend = <TData>() => {
277
+ const state: BackendState<TData> = globalStore?.getState()?.backend;
278
+
279
+ return {
280
+ ...state,
281
+ act: sendAct,
282
+ };
283
+ };
284
+
285
+ /**
286
+ * A tuple that contains the state and a setter function for it.
287
+ */
288
+ type StateWithSetter<T> = [T, (nextState: T) => void];
289
+
290
+ /**
291
+ * Allocates state on Redux store without sharing it with other clients.
292
+ *
293
+ * Use it when you want to have a stateful variable in your component
294
+ * that persists between renders, but will be forgotten after you close
295
+ * the UI.
296
+ *
297
+ * It is a lot more performant than `setSharedState`.
298
+ *
299
+ * @param context React context.
300
+ * @param key Key which uniquely identifies this state in Redux store.
301
+ * @param initialState Initializes your global variable with this value.
302
+ * @deprecated Use useState and useEffect when you can. Pass the state as a prop.
303
+ */
304
+ export const useLocalState = <T>(
305
+ key: string,
306
+ initialState: T
307
+ ): StateWithSetter<T> => {
308
+ const state = globalStore?.getState()?.backend;
309
+ const sharedStates = state?.shared ?? {};
310
+ const sharedState = key in sharedStates ? sharedStates[key] : initialState;
311
+ return [
312
+ sharedState,
313
+ (nextState) => {
314
+ globalStore.dispatch(
315
+ backendSetSharedState({
316
+ key,
317
+ nextState:
318
+ typeof nextState === 'function'
319
+ ? nextState(sharedState)
320
+ : nextState,
321
+ })
322
+ );
323
+ },
324
+ ];
325
+ };
326
+
327
+ /**
328
+ * Allocates state on Redux store, and **shares** it with other clients
329
+ * in the game.
330
+ *
331
+ * Use it when you want to have a stateful variable in your component
332
+ * that persists not only between renders, but also gets pushed to other
333
+ * clients that observe this UI.
334
+ *
335
+ * This makes creation of observable s
336
+ *
337
+ * @param context React context.
338
+ * @param key Key which uniquely identifies this state in Redux store.
339
+ * @param initialState Initializes your global variable with this value.
340
+ */
341
+ export const useSharedState = <T>(
342
+ key: string,
343
+ initialState: T
344
+ ): StateWithSetter<T> => {
345
+ const state = globalStore?.getState()?.backend;
346
+ const sharedStates = state?.shared ?? {};
347
+ const sharedState = key in sharedStates ? sharedStates[key] : initialState;
348
+ return [
349
+ sharedState,
350
+ (nextState) => {
351
+ Byond.sendMessage({
352
+ type: 'setSharedState',
353
+ key,
354
+ value:
355
+ JSON.stringify(
356
+ typeof nextState === 'function' ? nextState(sharedState) : nextState
357
+ ) || '',
358
+ });
359
+ },
360
+ ];
361
+ };
362
+
363
+ export const useDispatch = () => {
364
+ return globalStore.dispatch;
365
+ };
366
+
367
+ export const useSelector = (selector: (state: any) => any) => {
368
+ return selector(globalStore?.getState());
369
+ };