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.
- package/{src/components → components}/AnimatedNumber.tsx +185 -185
- package/{src/components → components}/BlockQuote.tsx +15 -15
- package/{src/components → components}/BodyZoneSelector.tsx +149 -149
- package/{src/components → components}/Box.tsx +255 -255
- package/{src/components → components}/Button.tsx +415 -415
- package/{src/components → components}/ByondUi.jsx +121 -121
- package/{src/components → components}/Chart.tsx +160 -160
- package/{src/components → components}/ColorBox.tsx +30 -30
- package/{src/components → components}/Dimmer.tsx +19 -19
- package/{src/components → components}/Divider.tsx +26 -26
- package/{src/components → components}/DmIcon.tsx +72 -72
- package/{src/components → components}/DraggableControl.jsx +282 -282
- package/{src/components → components}/Dropdown.tsx +246 -246
- package/{src/components → components}/Flex.tsx +105 -105
- package/{src/components → components}/Icon.tsx +91 -91
- package/{src/components → components}/Input.tsx +181 -181
- package/{src/components → components}/KeyListener.tsx +40 -40
- package/{src/components → components}/Knob.tsx +185 -185
- package/{src/components → components}/LabeledList.tsx +130 -130
- package/{src/components → components}/MenuBar.tsx +233 -238
- package/{src/components → components}/Modal.tsx +25 -25
- package/{src/components → components}/NoticeBox.tsx +48 -48
- package/{src/components → components}/NumberInput.tsx +328 -328
- package/{src/components → components}/ProgressBar.tsx +79 -79
- package/{src/components → components}/RestrictedInput.jsx +301 -301
- package/{src/components → components}/RoundGauge.tsx +189 -189
- package/{src/components → components}/Section.tsx +125 -125
- package/{src/components → components}/Slider.tsx +173 -173
- package/{src/components → components}/Stack.tsx +101 -101
- package/{src/components → components}/Table.tsx +90 -90
- package/{src/components → components}/Tabs.tsx +90 -90
- package/{src/components → components}/TextArea.tsx +198 -198
- package/{src/components → components}/TimeDisplay.jsx +64 -64
- package/components/index.ts +51 -0
- package/{src/debug/KitchenSink.jsx → debug/KitchenSink.tsx} +56 -56
- package/{src/debug/actions.js → debug/actions.ts} +11 -11
- package/{src/debug/hooks.js → debug/hooks.ts} +10 -10
- package/{src/debug/middleware.js → debug/middleware.ts} +86 -86
- package/{src/debug/reducer.js → debug/reducer.ts} +27 -22
- package/{src/debug/selectors.js → debug/selectors.ts} +7 -7
- package/{src/layouts → layouts}/Layout.tsx +75 -75
- package/{src/layouts → layouts}/NtosWindow.tsx +162 -162
- package/{src/layouts → layouts}/Pane.tsx +56 -56
- package/{src/layouts → layouts}/Window.tsx +227 -227
- package/layouts/index.ts +10 -0
- package/package.json +3 -2
- package/src/assets.ts +43 -43
- package/src/backend.ts +369 -369
- package/src/drag.ts +280 -280
- package/src/events.ts +237 -237
- package/src/hotkeys.ts +212 -212
- package/src/renderer.ts +50 -50
- package/stories/Blink.stories.tsx +20 -0
- package/stories/BlockQuote.stories.tsx +23 -0
- package/stories/Box.stories.tsx +27 -0
- package/stories/Button.stories.tsx +68 -0
- package/stories/ByondUi.stories.tsx +45 -0
- package/stories/Collapsible.stories.tsx +23 -0
- package/stories/Flex.stories.tsx +68 -0
- package/stories/Input.stories.tsx +124 -0
- package/stories/LabeledList.stories.tsx +73 -0
- package/stories/Popper.stories.tsx +58 -0
- package/stories/ProgressBar.stories.tsx +58 -0
- package/stories/Stack.stories.tsx +55 -0
- package/stories/Storage.stories.tsx +46 -0
- package/stories/Themes.stories.tsx +30 -0
- package/stories/Tooltip.stories.tsx +48 -0
- package/stories/common.tsx +19 -0
- package/tsconfig.json +0 -21
- package/src/components/Grid.tsx +0 -44
- /package/{src/common → common}/collections.ts +0 -0
- /package/{src/common → common}/color.ts +0 -0
- /package/{src/common → common}/events.ts +0 -0
- /package/{src/common → common}/exhaustive.ts +0 -0
- /package/{src/common → common}/fp.ts +0 -0
- /package/{src/common → common}/keycodes.ts +0 -0
- /package/{src/common → common}/keys.ts +0 -0
- /package/{src/common → common}/math.ts +0 -0
- /package/{src/common → common}/perf.ts +0 -0
- /package/{src/common → common}/random.ts +0 -0
- /package/{src/common → common}/react.ts +0 -0
- /package/{src/common → common}/redux.ts +0 -0
- /package/{src/common → common}/storage.js +0 -0
- /package/{src/common → common}/string.ts +0 -0
- /package/{src/common → common}/timer.ts +0 -0
- /package/{src/common → common}/type-utils.ts +0 -0
- /package/{src/common → common}/types.ts +0 -0
- /package/{src/common → common}/uuid.ts +0 -0
- /package/{src/common → common}/vector.ts +0 -0
- /package/{src/components → components}/Autofocus.tsx +0 -0
- /package/{src/components → components}/Blink.jsx +0 -0
- /package/{src/components → components}/Collapsible.tsx +0 -0
- /package/{src/components → components}/Dialog.tsx +0 -0
- /package/{src/components → components}/FakeTerminal.jsx +0 -0
- /package/{src/components → components}/FitText.tsx +0 -0
- /package/{src/components → components}/Image.tsx +0 -0
- /package/{src/components → components}/InfinitePlane.jsx +0 -0
- /package/{src/components → components}/LabeledControls.tsx +0 -0
- /package/{src/components → components}/Popper.tsx +0 -0
- /package/{src/components → components}/StyleableSection.tsx +0 -0
- /package/{src/components → components}/Tooltip.tsx +0 -0
- /package/{src/components → components}/TrackOutsideClicks.tsx +0 -0
- /package/{src/components → components}/VirtualList.tsx +0 -0
- /package/{src/debug → debug}/index.ts +0 -0
- /package/{src/styles → styles}/base.scss +0 -0
- /package/{src/styles → styles}/colors.scss +0 -0
- /package/{src/styles → styles}/components/BlockQuote.scss +0 -0
- /package/{src/styles → styles}/components/Button.scss +0 -0
- /package/{src/styles → styles}/components/ColorBox.scss +0 -0
- /package/{src/styles → styles}/components/Dialog.scss +0 -0
- /package/{src/styles → styles}/components/Dimmer.scss +0 -0
- /package/{src/styles → styles}/components/Divider.scss +0 -0
- /package/{src/styles → styles}/components/Dropdown.scss +0 -0
- /package/{src/styles → styles}/components/Flex.scss +0 -0
- /package/{src/styles → styles}/components/Icon.scss +0 -0
- /package/{src/styles → styles}/components/Input.scss +0 -0
- /package/{src/styles → styles}/components/Knob.scss +0 -0
- /package/{src/styles → styles}/components/LabeledList.scss +0 -0
- /package/{src/styles → styles}/components/MenuBar.scss +0 -0
- /package/{src/styles → styles}/components/Modal.scss +0 -0
- /package/{src/styles → styles}/components/NoticeBox.scss +0 -0
- /package/{src/styles → styles}/components/NumberInput.scss +0 -0
- /package/{src/styles → styles}/components/ProgressBar.scss +0 -0
- /package/{src/styles → styles}/components/RoundGauge.scss +0 -0
- /package/{src/styles → styles}/components/Section.scss +0 -0
- /package/{src/styles → styles}/components/Slider.scss +0 -0
- /package/{src/styles → styles}/components/Stack.scss +0 -0
- /package/{src/styles → styles}/components/Table.scss +0 -0
- /package/{src/styles → styles}/components/Tabs.scss +0 -0
- /package/{src/styles → styles}/components/TextArea.scss +0 -0
- /package/{src/styles → styles}/components/Tooltip.scss +0 -0
- /package/{src/styles → styles}/functions.scss +0 -0
- /package/{src/styles → styles}/layouts/Layout.scss +0 -0
- /package/{src/styles → styles}/layouts/NtosHeader.scss +0 -0
- /package/{src/styles → styles}/layouts/NtosWindow.scss +0 -0
- /package/{src/styles → styles}/layouts/TitleBar.scss +0 -0
- /package/{src/styles → styles}/layouts/Window.scss +0 -0
- /package/{src/styles → styles}/main.scss +0 -0
- /package/{src/styles → styles}/reset.scss +0 -0
- /package/{src/styles → styles}/themes/abductor.scss +0 -0
- /package/{src/styles → styles}/themes/admin.scss +0 -0
- /package/{src/styles → styles}/themes/cardtable.scss +0 -0
- /package/{src/styles → styles}/themes/hackerman.scss +0 -0
- /package/{src/styles → styles}/themes/malfunction.scss +0 -0
- /package/{src/styles → styles}/themes/neutral.scss +0 -0
- /package/{src/styles → styles}/themes/ntOS95.scss +0 -0
- /package/{src/styles → styles}/themes/ntos.scss +0 -0
- /package/{src/styles → styles}/themes/ntos_cat.scss +0 -0
- /package/{src/styles → styles}/themes/ntos_darkmode.scss +0 -0
- /package/{src/styles → styles}/themes/ntos_lightmode.scss +0 -0
- /package/{src/styles → styles}/themes/ntos_spooky.scss +0 -0
- /package/{src/styles → styles}/themes/ntos_synth.scss +0 -0
- /package/{src/styles → styles}/themes/ntos_terminal.scss +0 -0
- /package/{src/styles → styles}/themes/paper.scss +0 -0
- /package/{src/styles → styles}/themes/retro.scss +0 -0
- /package/{src/styles → styles}/themes/spookyconsole.scss +0 -0
- /package/{src/styles → styles}/themes/syndicate.scss +0 -0
- /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 '
|
|
15
|
-
import { createAction } from '
|
|
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
|
+
};
|