others
This commit is contained in:
File diff suppressed because one or more lines are too long
55
tgui/.yarn/releases/yarn-2.4.0.cjs
vendored
55
tgui/.yarn/releases/yarn-2.4.0.cjs
vendored
File diff suppressed because one or more lines are too long
@@ -1,271 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Converts a given collection to an array.
|
||||
*
|
||||
* - Arrays are returned unmodified;
|
||||
* - If object was provided, keys will be discarded;
|
||||
* - Everything else will result in an empty array.
|
||||
*
|
||||
* @returns {any[]}
|
||||
*/
|
||||
export const toArray = collection => {
|
||||
if (Array.isArray(collection)) {
|
||||
return collection;
|
||||
}
|
||||
if (typeof collection === 'object') {
|
||||
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
const result = [];
|
||||
for (let i in collection) {
|
||||
if (hasOwnProperty.call(collection, i)) {
|
||||
result.push(collection[i]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a given object to an array, and appends a key to every
|
||||
* object inside of that array.
|
||||
*
|
||||
* Example input (object):
|
||||
* ```
|
||||
* {
|
||||
* 'Foo': { info: 'Hello world!' },
|
||||
* 'Bar': { info: 'Hello world!' },
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Example output (array):
|
||||
* ```
|
||||
* [
|
||||
* { key: 'Foo', info: 'Hello world!' },
|
||||
* { key: 'Bar', info: 'Hello world!' },
|
||||
* ]
|
||||
* ```
|
||||
*
|
||||
* @template T
|
||||
* @param {{ [key: string]: T }} obj Object, or in DM terms, an assoc array
|
||||
* @param {string} keyProp Property, to which key will be assigned
|
||||
* @returns {T[]} Array of keyed objects
|
||||
*/
|
||||
export const toKeyedArray = (obj, keyProp = 'key') => {
|
||||
return map((item, key) => ({
|
||||
[keyProp]: key,
|
||||
...item,
|
||||
}))(obj);
|
||||
};
|
||||
|
||||
/**
|
||||
* Iterates over elements of collection, returning an array of all elements
|
||||
* iteratee returns truthy for. The predicate is invoked with three
|
||||
* arguments: (value, index|key, collection).
|
||||
*
|
||||
* If collection is 'null' or 'undefined', it will be returned "as is"
|
||||
* without emitting any errors (which can be useful in some cases).
|
||||
*
|
||||
* @returns {any[]}
|
||||
*/
|
||||
export const filter = iterateeFn => collection => {
|
||||
if (collection === null || collection === undefined) {
|
||||
return collection;
|
||||
}
|
||||
if (Array.isArray(collection)) {
|
||||
const result = [];
|
||||
for (let i = 0; i < collection.length; i++) {
|
||||
const item = collection[i];
|
||||
if (iterateeFn(item, i, collection)) {
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
throw new Error(`filter() can't iterate on type ${typeof collection}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an array of values by running each element in collection
|
||||
* thru an iteratee function. The iteratee is invoked with three
|
||||
* arguments: (value, index|key, collection).
|
||||
*
|
||||
* If collection is 'null' or 'undefined', it will be returned "as is"
|
||||
* without emitting any errors (which can be useful in some cases).
|
||||
*
|
||||
* @returns {any[]}
|
||||
*/
|
||||
export const map = iterateeFn => collection => {
|
||||
if (collection === null || collection === undefined) {
|
||||
return collection;
|
||||
}
|
||||
if (Array.isArray(collection)) {
|
||||
const result = [];
|
||||
for (let i = 0; i < collection.length; i++) {
|
||||
result.push(iterateeFn(collection[i], i, collection));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
if (typeof collection === 'object') {
|
||||
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
const result = [];
|
||||
for (let i in collection) {
|
||||
if (hasOwnProperty.call(collection, i)) {
|
||||
result.push(iterateeFn(collection[i], i, collection));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
throw new Error(`map() can't iterate on type ${typeof collection}`);
|
||||
};
|
||||
|
||||
const COMPARATOR = (objA, objB) => {
|
||||
const criteriaA = objA.criteria;
|
||||
const criteriaB = objB.criteria;
|
||||
const length = criteriaA.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
const a = criteriaA[i];
|
||||
const b = criteriaB[i];
|
||||
if (a < b) {
|
||||
return -1;
|
||||
}
|
||||
if (a > b) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an array of elements, sorted in ascending order by the results
|
||||
* of running each element in a collection thru each iteratee.
|
||||
*
|
||||
* Iteratees are called with one argument (value).
|
||||
*
|
||||
* @returns {any[]}
|
||||
*/
|
||||
export const sortBy = (...iterateeFns) => array => {
|
||||
if (!Array.isArray(array)) {
|
||||
return array;
|
||||
}
|
||||
let length = array.length;
|
||||
// Iterate over the array to collect criteria to sort it by
|
||||
let mappedArray = [];
|
||||
for (let i = 0; i < length; i++) {
|
||||
const value = array[i];
|
||||
mappedArray.push({
|
||||
criteria: iterateeFns.map(fn => fn(value)),
|
||||
value,
|
||||
});
|
||||
}
|
||||
// Sort criteria using the base comparator
|
||||
mappedArray.sort(COMPARATOR);
|
||||
// Unwrap values
|
||||
while (length--) {
|
||||
mappedArray[length] = mappedArray[length].value;
|
||||
}
|
||||
return mappedArray;
|
||||
};
|
||||
|
||||
/**
|
||||
* A fast implementation of reduce.
|
||||
*/
|
||||
export const reduce = (reducerFn, initialValue) => array => {
|
||||
const length = array.length;
|
||||
let i;
|
||||
let result;
|
||||
if (initialValue === undefined) {
|
||||
i = 1;
|
||||
result = array[0];
|
||||
}
|
||||
else {
|
||||
i = 0;
|
||||
result = initialValue;
|
||||
}
|
||||
for (; i < length; i++) {
|
||||
result = reducerFn(result, array[i], i, array);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a duplicate-free version of an array, using SameValueZero for
|
||||
* equality comparisons, in which only the first occurrence of each element
|
||||
* is kept. The order of result values is determined by the order they occur
|
||||
* in the array.
|
||||
*
|
||||
* It accepts iteratee which is invoked for each element in array to generate
|
||||
* the criterion by which uniqueness is computed. The order of result values
|
||||
* is determined by the order they occur in the array. The iteratee is
|
||||
* invoked with one argument: value.
|
||||
*/
|
||||
export const uniqBy = iterateeFn => array => {
|
||||
const { length } = array;
|
||||
const result = [];
|
||||
const seen = iterateeFn ? [] : result;
|
||||
let index = -1;
|
||||
outer:
|
||||
while (++index < length) {
|
||||
let value = array[index];
|
||||
const computed = iterateeFn ? iterateeFn(value) : value;
|
||||
value = value !== 0 ? value : 0;
|
||||
if (computed === computed) {
|
||||
let seenIndex = seen.length;
|
||||
while (seenIndex--) {
|
||||
if (seen[seenIndex] === computed) {
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
if (iterateeFn) {
|
||||
seen.push(computed);
|
||||
}
|
||||
result.push(value);
|
||||
}
|
||||
else if (!seen.includes(computed)) {
|
||||
if (seen !== result) {
|
||||
seen.push(computed);
|
||||
}
|
||||
result.push(value);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an array of grouped elements, the first of which contains
|
||||
* the first elements of the given arrays, the second of which contains
|
||||
* the second elements of the given arrays, and so on.
|
||||
*
|
||||
* @returns {any[]}
|
||||
*/
|
||||
export const zip = (...arrays) => {
|
||||
if (arrays.length === 0) {
|
||||
return;
|
||||
}
|
||||
const numArrays = arrays.length;
|
||||
const numValues = arrays[0].length;
|
||||
const result = [];
|
||||
for (let valueIndex = 0; valueIndex < numValues; valueIndex++) {
|
||||
const entry = [];
|
||||
for (let arrayIndex = 0; arrayIndex < numArrays; arrayIndex++) {
|
||||
entry.push(arrays[arrayIndex][valueIndex]);
|
||||
}
|
||||
result.push(entry);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* This method is like "zip" except that it accepts iteratee to
|
||||
* specify how grouped values should be combined. The iteratee is
|
||||
* invoked with the elements of each group.
|
||||
*
|
||||
* @returns {any[]}
|
||||
*/
|
||||
export const zipWith = iterateeFn => (...arrays) => {
|
||||
return map(values => iterateeFn(...values))(zip(...arrays));
|
||||
};
|
||||
@@ -1,100 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Limits a number to the range between 'min' and 'max'.
|
||||
*/
|
||||
export const clamp = (value, min, max) => {
|
||||
return value < min ? min : value > max ? max : value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Limits a number between 0 and 1.
|
||||
*/
|
||||
export const clamp01 = value => {
|
||||
return value < 0 ? 0 : value > 1 ? 1 : value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Scales a number to fit into the range between min and max.
|
||||
*/
|
||||
export const scale = (value, min, max) => {
|
||||
return (value - min) / (max - min);
|
||||
};
|
||||
|
||||
/**
|
||||
* Robust number rounding.
|
||||
*
|
||||
* Adapted from Locutus, see: http://locutus.io/php/math/round/
|
||||
*
|
||||
* @param {number} value
|
||||
* @param {number} precision
|
||||
* @return {number}
|
||||
*/
|
||||
export const round = (value, precision) => {
|
||||
if (!value || isNaN(value)) {
|
||||
return value;
|
||||
}
|
||||
// helper variables
|
||||
let m, f, isHalf, sgn;
|
||||
// making sure precision is integer
|
||||
precision |= 0;
|
||||
m = Math.pow(10, precision);
|
||||
value *= m;
|
||||
// sign of the number
|
||||
sgn = (value > 0) | -(value < 0);
|
||||
// isHalf = value % 1 === 0.5 * sgn;
|
||||
isHalf = Math.abs(value % 1) >= 0.4999999999854481;
|
||||
f = Math.floor(value);
|
||||
if (isHalf) {
|
||||
// rounds .5 away from zero
|
||||
value = f + (sgn > 0);
|
||||
}
|
||||
return (isHalf ? value : Math.round(value)) / m;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a string representing a number in fixed point notation.
|
||||
*/
|
||||
export const toFixed = (value, fractionDigits = 0) => {
|
||||
return Number(value).toFixed(Math.max(fractionDigits, 0));
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks whether a value is within the provided range.
|
||||
*
|
||||
* Range is an array of two numbers, for example: [0, 15].
|
||||
*/
|
||||
export const inRange = (value, range) => {
|
||||
return range
|
||||
&& value >= range[0]
|
||||
&& value <= range[1];
|
||||
};
|
||||
|
||||
/**
|
||||
* Walks over the object with ranges, comparing value against every range,
|
||||
* and returns the key of the first matching range.
|
||||
*
|
||||
* Range is an array of two numbers, for example: [0, 15].
|
||||
*/
|
||||
export const keyOfMatchingRange = (value, ranges) => {
|
||||
for (let rangeName of Object.keys(ranges)) {
|
||||
const range = ranges[rangeName];
|
||||
if (inRange(value, range)) {
|
||||
return rangeName;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get number of digits following the decimal point in a number
|
||||
*/
|
||||
export const numberOfDecimalDigits = value => {
|
||||
if (Math.floor(value) !== value) {
|
||||
return value.toString().split('.')[1].length || 0;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
@@ -1,383 +0,0 @@
|
||||
/**
|
||||
* This file provides a clear separation layer between backend updates
|
||||
* and what state our React app sees.
|
||||
*
|
||||
* Sometimes backend can response without a "data" field, but our final
|
||||
* state will still contain previous "data" because we are merging
|
||||
* the response with already existing state.
|
||||
*
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { perf } from 'common/perf';
|
||||
import { setupDrag } from './drag';
|
||||
import { focusMap } from './focus';
|
||||
import { createLogger } from './logging';
|
||||
import { resumeRenderer, suspendRenderer } from './renderer';
|
||||
|
||||
const logger = createLogger('backend');
|
||||
|
||||
export const backendUpdate = state => ({
|
||||
type: 'backend/update',
|
||||
payload: state,
|
||||
});
|
||||
|
||||
export const backendSetSharedState = (key, nextState) => ({
|
||||
type: 'backend/setSharedState',
|
||||
payload: { key, nextState },
|
||||
});
|
||||
|
||||
export const backendSuspendStart = () => ({
|
||||
type: 'backend/suspendStart',
|
||||
});
|
||||
|
||||
export const backendSuspendSuccess = () => ({
|
||||
type: 'backend/suspendSuccess',
|
||||
payload: {
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
const initialState = {
|
||||
config: {},
|
||||
data: {},
|
||||
shared: {},
|
||||
// Start as suspended
|
||||
suspended: Date.now(),
|
||||
suspending: false,
|
||||
};
|
||||
|
||||
export const backendReducer = (state = initialState, action) => {
|
||||
const { type, payload } = action;
|
||||
|
||||
if (type === 'backend/update') {
|
||||
// Merge config
|
||||
const config = {
|
||||
...state.config,
|
||||
...payload.config,
|
||||
};
|
||||
// Merge data
|
||||
const data = {
|
||||
...state.data,
|
||||
...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);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Return new state
|
||||
return {
|
||||
...state,
|
||||
config,
|
||||
data,
|
||||
shared,
|
||||
suspended: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (type === 'backend/setSharedState') {
|
||||
const { key, nextState } = payload;
|
||||
return {
|
||||
...state,
|
||||
shared: {
|
||||
...state.shared,
|
||||
[key]: nextState,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (type === 'backend/suspendStart') {
|
||||
return {
|
||||
...state,
|
||||
suspending: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (type === 'backend/suspendSuccess') {
|
||||
const { timestamp } = payload;
|
||||
return {
|
||||
...state,
|
||||
data: {},
|
||||
shared: {},
|
||||
config: {
|
||||
...state.config,
|
||||
title: '',
|
||||
status: 1,
|
||||
},
|
||||
suspending: false,
|
||||
suspended: timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
export const backendMiddleware = store => {
|
||||
let fancyState;
|
||||
let suspendInterval;
|
||||
|
||||
return next => action => {
|
||||
const { suspended } = selectBackend(store.getState());
|
||||
const { type, payload } = action;
|
||||
|
||||
if (type === 'update') {
|
||||
store.dispatch(backendUpdate(payload));
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'suspend') {
|
||||
store.dispatch(backendSuspendSuccess());
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'ping') {
|
||||
sendMessage({
|
||||
type: 'pingReply',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'backend/suspendStart' && !suspendInterval) {
|
||||
logger.log(`suspending (${window.__windowId__})`);
|
||||
// Keep sending suspend messages until it succeeds.
|
||||
// It may fail multiple times due to topic rate limiting.
|
||||
const suspendFn = () => sendMessage({
|
||||
type: 'suspend',
|
||||
});
|
||||
suspendFn();
|
||||
suspendInterval = setInterval(suspendFn, 2000);
|
||||
}
|
||||
|
||||
if (type === 'backend/suspendSuccess') {
|
||||
suspendRenderer();
|
||||
clearInterval(suspendInterval);
|
||||
suspendInterval = undefined;
|
||||
Byond.winset(window.__windowId__, {
|
||||
'is-visible': false,
|
||||
});
|
||||
setImmediate(() => focusMap());
|
||||
}
|
||||
|
||||
if (type === 'backend/update') {
|
||||
const fancy = payload.config?.window?.fancy;
|
||||
// Initialize fancy state
|
||||
if (fancyState === undefined) {
|
||||
fancyState = fancy;
|
||||
}
|
||||
// React to changes in fancy
|
||||
else if (fancyState !== fancy) {
|
||||
logger.log('changing fancy mode to', fancy);
|
||||
fancyState = fancy;
|
||||
Byond.winset(window.__windowId__, {
|
||||
titlebar: !fancy,
|
||||
'can-resize': !fancy,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Resume on incoming update
|
||||
if (type === 'backend/update' && suspended) {
|
||||
// Show the payload
|
||||
logger.log('backend/update', payload);
|
||||
// Signal renderer that we have resumed
|
||||
resumeRenderer();
|
||||
// Setup drag
|
||||
setupDrag();
|
||||
// We schedule this for the next tick here because resizing and unhiding
|
||||
// during the same tick will flash with a white background.
|
||||
setImmediate(() => {
|
||||
perf.mark('resume/start');
|
||||
// Doublecheck if we are not re-suspended.
|
||||
const { suspended } = selectBackend(store.getState());
|
||||
if (suspended) {
|
||||
return;
|
||||
}
|
||||
Byond.winset(window.__windowId__, {
|
||||
'is-visible': true,
|
||||
});
|
||||
perf.mark('resume/finish');
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
logger.log('visible in',
|
||||
perf.measure('render/finish', 'resume/finish'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return next(action);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends a message to /datum/tgui_window.
|
||||
*/
|
||||
export const sendMessage = (message = {}) => {
|
||||
const { payload, ...rest } = message;
|
||||
const data = {
|
||||
// Message identifying header
|
||||
tgui: 1,
|
||||
window_id: window.__windowId__,
|
||||
// Message body
|
||||
...rest,
|
||||
};
|
||||
// JSON-encode the payload
|
||||
if (payload !== null && payload !== undefined) {
|
||||
data.payload = JSON.stringify(payload);
|
||||
}
|
||||
Byond.topic(data);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sends an action to `ui_act` on `src_object` that this tgui window
|
||||
* is associated with.
|
||||
*/
|
||||
export const sendAct = (action, payload = {}) => {
|
||||
// Validate that payload is an object
|
||||
const isObject = typeof payload === 'object'
|
||||
&& payload !== null
|
||||
&& !Array.isArray(payload);
|
||||
if (!isObject) {
|
||||
logger.error(`Payload for act() must be an object, got this:`, payload);
|
||||
return;
|
||||
}
|
||||
sendMessage({
|
||||
type: 'act/' + action,
|
||||
payload,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef BackendState
|
||||
* @type {{
|
||||
* config: {
|
||||
* title: string,
|
||||
* status: number,
|
||||
* interface: string,
|
||||
* window: {
|
||||
* key: string,
|
||||
* size: [number, number],
|
||||
* fancy: boolean,
|
||||
* locked: boolean,
|
||||
* },
|
||||
* client: {
|
||||
* ckey: string,
|
||||
* address: string,
|
||||
* computer_id: string,
|
||||
* },
|
||||
* user: {
|
||||
* name: string,
|
||||
* observer: number,
|
||||
* },
|
||||
* },
|
||||
* data: any,
|
||||
* shared: any,
|
||||
* suspending: boolean,
|
||||
* suspended: boolean,
|
||||
* }}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Selects a backend-related slice of Redux state
|
||||
*
|
||||
* @return {BackendState}
|
||||
*/
|
||||
export const selectBackend = state => state.backend || {};
|
||||
|
||||
/**
|
||||
* A React hook (sort of) for getting tgui state and related functions.
|
||||
*
|
||||
* This is supposed to be replaced with a real React Hook, which can only
|
||||
* be used in functional components.
|
||||
*
|
||||
* @return {BackendState & {
|
||||
* act: sendAct,
|
||||
* }}
|
||||
*/
|
||||
export const useBackend = context => {
|
||||
const { store } = context;
|
||||
const state = selectBackend(store.getState());
|
||||
return {
|
||||
...state,
|
||||
act: sendAct,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 = selectBackend(store.getState());
|
||||
const sharedStates = state.shared ?? {};
|
||||
const sharedState = (key in sharedStates)
|
||||
? sharedStates[key]
|
||||
: initialState;
|
||||
return [
|
||||
sharedState,
|
||||
nextState => {
|
||||
store.dispatch(backendSetSharedState(key, (
|
||||
typeof nextState === 'function'
|
||||
? nextState(sharedState)
|
||||
: 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 = selectBackend(store.getState());
|
||||
const sharedStates = state.shared ?? {};
|
||||
const sharedState = (key in sharedStates)
|
||||
? sharedStates[key]
|
||||
: initialState;
|
||||
return [
|
||||
sharedState,
|
||||
nextState => {
|
||||
sendMessage({
|
||||
type: 'setSharedState',
|
||||
key,
|
||||
value: JSON.stringify(
|
||||
typeof nextState === 'function'
|
||||
? nextState(sharedState)
|
||||
: nextState
|
||||
) || '',
|
||||
});
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -1,171 +0,0 @@
|
||||
/**
|
||||
* @file
|
||||
* @copyright 2020 Aleksej Komarov
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { KEY_CTRL, KEY_ENTER, KEY_ESCAPE, KEY_F, KEY_F5, KEY_R, KEY_SHIFT, KEY_SPACE, KEY_TAB } from 'common/keycodes';
|
||||
import { globalEvents } from './events';
|
||||
import { createLogger } from './logging';
|
||||
|
||||
const logger = createLogger('hotkeys');
|
||||
|
||||
// BYOND macros, in `key: command` format.
|
||||
const byondMacros = {};
|
||||
|
||||
// Array of acquired keys, which will not be sent to BYOND.
|
||||
const hotKeysAcquired = [
|
||||
// Default set of acquired keys
|
||||
KEY_ESCAPE,
|
||||
KEY_ENTER,
|
||||
KEY_SPACE,
|
||||
KEY_TAB,
|
||||
KEY_CTRL,
|
||||
KEY_SHIFT,
|
||||
KEY_F5,
|
||||
];
|
||||
|
||||
// State of passed-through keys.
|
||||
const keyState = {};
|
||||
|
||||
/**
|
||||
* Converts a browser keycode to BYOND keycode.
|
||||
*/
|
||||
const keyCodeToByond = keyCode => {
|
||||
if (keyCode === 16) return 'Shift';
|
||||
if (keyCode === 17) return 'Ctrl';
|
||||
if (keyCode === 18) return 'Alt';
|
||||
if (keyCode === 33) return 'Northeast';
|
||||
if (keyCode === 34) return 'Southeast';
|
||||
if (keyCode === 35) return 'Southwest';
|
||||
if (keyCode === 36) return 'Northwest';
|
||||
if (keyCode === 37) return 'West';
|
||||
if (keyCode === 38) return 'North';
|
||||
if (keyCode === 39) return 'East';
|
||||
if (keyCode === 40) return 'South';
|
||||
if (keyCode === 45) return 'Insert';
|
||||
if (keyCode === 46) return 'Delete';
|
||||
if (keyCode >= 48 && keyCode <= 57 || keyCode >= 65 && keyCode <= 90) {
|
||||
return String.fromCharCode(keyCode);
|
||||
}
|
||||
if (keyCode >= 96 && keyCode <= 105) {
|
||||
return 'Numpad' + (keyCode - 96);
|
||||
}
|
||||
if (keyCode >= 112 && keyCode <= 123) {
|
||||
return 'F' + (keyCode - 111);
|
||||
}
|
||||
if (keyCode === 188) return ',';
|
||||
if (keyCode === 189) return '-';
|
||||
if (keyCode === 190) return '.';
|
||||
};
|
||||
|
||||
/**
|
||||
* Keyboard passthrough logic. This allows you to keep doing things
|
||||
* in game while the browser window is focused.
|
||||
*/
|
||||
const handlePassthrough = key => {
|
||||
// In addition to F5, support reloading with Ctrl+R and Ctrl+F5
|
||||
if (key.ctrl && (key.code === KEY_F5 || key.code === KEY_R)) {
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
// Prevent passthrough on Ctrl+F
|
||||
if (key.ctrl && key.code === KEY_F) {
|
||||
return;
|
||||
}
|
||||
// NOTE: Alt modifier is pretty bad and sticky in IE11.
|
||||
if (key.event.defaultPrevented
|
||||
|| key.isModifierKey()
|
||||
|| hotKeysAcquired.includes(key.code)) {
|
||||
return;
|
||||
}
|
||||
const byondKeyCode = keyCodeToByond(key.code);
|
||||
if (!byondKeyCode) {
|
||||
return;
|
||||
}
|
||||
// Macro
|
||||
const macro = byondMacros[byondKeyCode];
|
||||
if (macro) {
|
||||
logger.debug('macro', macro);
|
||||
return Byond.command(macro);
|
||||
}
|
||||
// KeyDown
|
||||
if (key.isDown() && !keyState[byondKeyCode]) {
|
||||
keyState[byondKeyCode] = true;
|
||||
const command = `KeyDown "${byondKeyCode}"`;
|
||||
logger.debug(command);
|
||||
return Byond.command(command);
|
||||
}
|
||||
// KeyUp
|
||||
if (key.isUp() && keyState[byondKeyCode]) {
|
||||
keyState[byondKeyCode] = false;
|
||||
const command = `KeyUp "${byondKeyCode}"`;
|
||||
logger.debug(command);
|
||||
return Byond.command(command);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Acquires a lock on the hotkey, which prevents it from being
|
||||
* passed through to BYOND.
|
||||
*/
|
||||
export const acquireHotKey = keyCode => {
|
||||
hotKeysAcquired.push(keyCode);
|
||||
};
|
||||
|
||||
/**
|
||||
* Makes the hotkey available to BYOND again.
|
||||
*/
|
||||
export const releaseHotKey = keyCode => {
|
||||
const index = hotKeysAcquired.indexOf(keyCode);
|
||||
if (index >= 0) {
|
||||
hotKeysAcquired.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
export const releaseHeldKeys = () => {
|
||||
for (let byondKeyCode of Object.keys(keyState)) {
|
||||
if (keyState[byondKeyCode]) {
|
||||
keyState[byondKeyCode] = false;
|
||||
logger.log(`releasing key "${byondKeyCode}"`);
|
||||
Byond.command(`KeyUp "${byondKeyCode}"`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const setupHotKeys = () => {
|
||||
// Read macros
|
||||
Byond.winget('default.*').then(data => {
|
||||
// Group each macro by ref
|
||||
const groupedByRef = {};
|
||||
for (let key of Object.keys(data)) {
|
||||
const keyPath = key.split('.');
|
||||
const ref = keyPath[1];
|
||||
const prop = keyPath[2];
|
||||
if (ref && prop) {
|
||||
if (!groupedByRef[ref]) {
|
||||
groupedByRef[ref] = {};
|
||||
}
|
||||
groupedByRef[ref][prop] = data[key];
|
||||
}
|
||||
}
|
||||
// Insert macros
|
||||
const escapedQuotRegex = /\\"/g;
|
||||
const unescape = str => str
|
||||
.substring(1, str.length - 1)
|
||||
.replace(escapedQuotRegex, '"');
|
||||
for (let ref of Object.keys(groupedByRef)) {
|
||||
const macro = groupedByRef[ref];
|
||||
const byondKeyName = unescape(macro.name);
|
||||
byondMacros[byondKeyName] = unescape(macro.command);
|
||||
}
|
||||
logger.debug('loaded macros', byondMacros);
|
||||
});
|
||||
// Setup event handlers
|
||||
globalEvents.on('window-blur', () => {
|
||||
releaseHeldKeys();
|
||||
});
|
||||
globalEvents.on('key', key => {
|
||||
handlePassthrough(key);
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user