@dcl/sdk 7.29.1-35901015152.commit-d1b7b96 → 7.29.1-35917671376.commit-046b268

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/platform/index.js CHANGED
@@ -1,8 +1,40 @@
1
1
  import { getExplorerInformation } from '~system/Runtime';
2
+ import { subscribe } from '~system/EngineApi';
3
+ import { Observable } from '../internal/Observable';
4
+ import { localeChangedEvents } from '../internal/language-events';
2
5
  const VALID_PLATFORMS = new Set(['mobile', 'desktop', 'web']);
6
+ const DEFAULT_LANGUAGE = 'en';
7
+ const BCP47_TAG = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/;
3
8
  let platform = null;
9
+ let language = DEFAULT_LANGUAGE;
10
+ let languageSetByEvent = false;
11
+ /**
12
+ * Triggered when the player switches the client language mid-session.
13
+ * Older clients never emit it.
14
+ * @public
15
+ */
16
+ export const onPlayerLanguageChanged = new Observable();
17
+ function normalizeLocale(locale) {
18
+ const tag = (locale ?? '').trim().replace(/_/g, '-');
19
+ return BCP47_TAG.test(tag) ? tag : DEFAULT_LANGUAGE;
20
+ }
21
+ function setLanguage(next) {
22
+ if (next === language)
23
+ return;
24
+ language = next;
25
+ onPlayerLanguageChanged.notifyObservers({ language });
26
+ }
27
+ localeChangedEvents.add((event) => {
28
+ languageSetByEvent = true;
29
+ setLanguage(normalizeLocale(event.locale));
30
+ });
31
+ subscribe({ eventId: 'localeChanged' }).catch(console.error);
4
32
  void getExplorerInformation({})
5
33
  .then((response) => {
34
+ // a mid-session change that arrived first is more recent than this response
35
+ if (!languageSetByEvent) {
36
+ setLanguage(normalizeLocale(response.configurations?.['locale']));
37
+ }
6
38
  const normalized = response.platform.toLowerCase();
7
39
  if (VALID_PLATFORMS.has(normalized)) {
8
40
  platform = normalized;
@@ -26,4 +58,13 @@ export function isDesktop() {
26
58
  export function isWeb() {
27
59
  return getPlatform() === 'web';
28
60
  }
29
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvcGxhdGZvcm0vaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLHNCQUFzQixFQUFFLE1BQU0saUJBQWlCLENBQUE7QUFJeEQsTUFBTSxlQUFlLEdBQWdCLElBQUksR0FBRyxDQUFTLENBQUMsUUFBUSxFQUFFLFNBQVMsRUFBRSxLQUFLLENBQUMsQ0FBQyxDQUFBO0FBRWxGLElBQUksUUFBUSxHQUFvQixJQUFJLENBQUE7QUFDcEMsS0FBSyxzQkFBc0IsQ0FBQyxFQUFFLENBQUM7S0FDNUIsSUFBSSxDQUFDLENBQUMsUUFBUSxFQUFFLEVBQUU7SUFDakIsTUFBTSxVQUFVLEdBQUcsUUFBUSxDQUFDLFFBQVEsQ0FBQyxXQUFXLEVBQUUsQ0FBQTtJQUNsRCxJQUFJLGVBQWUsQ0FBQyxHQUFHLENBQUMsVUFBVSxDQUFDLEVBQUU7UUFDbkMsUUFBUSxHQUFHLFVBQXNCLENBQUE7S0FDbEM7U0FBTTtRQUNMLE9BQU8sQ0FBQyxLQUFLLENBQUMsNEJBQTRCLFFBQVEsQ0FBQyxRQUFRLEdBQUcsQ0FBQyxDQUFBO0tBQ2hFO0FBQ0gsQ0FBQyxDQUFDO0tBQ0QsS0FBSyxDQUFDLENBQUMsS0FBSyxFQUFFLEVBQUU7SUFDZixPQUFPLENBQUMsS0FBSyxDQUFDLHFDQUFxQyxFQUFFLEtBQUssQ0FBQyxDQUFBO0FBQzdELENBQUMsQ0FBQyxDQUFBO0FBRUosTUFBTSxVQUFVLFdBQVc7SUFDekIsT0FBTyxRQUFRLENBQUE7QUFDakIsQ0FBQztBQUVELE1BQU0sVUFBVSxRQUFRO0lBQ3RCLE9BQU8sV0FBVyxFQUFFLEtBQUssUUFBUSxDQUFBO0FBQ25DLENBQUM7QUFFRCxNQUFNLFVBQVUsU0FBUztJQUN2QixPQUFPLFdBQVcsRUFBRSxLQUFLLFNBQVMsQ0FBQTtBQUNwQyxDQUFDO0FBRUQsTUFBTSxVQUFVLEtBQUs7SUFDbkIsT0FBTyxXQUFXLEVBQUUsS0FBSyxLQUFLLENBQUE7QUFDaEMsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IGdldEV4cGxvcmVySW5mb3JtYXRpb24gfSBmcm9tICd+c3lzdGVtL1J1bnRpbWUnXG5cbmV4cG9ydCB0eXBlIFBsYXRmb3JtID0gJ21vYmlsZScgfCAnZGVza3RvcCcgfCAnd2ViJ1xuXG5jb25zdCBWQUxJRF9QTEFURk9STVM6IFNldDxzdHJpbmc+ID0gbmV3IFNldDxzdHJpbmc+KFsnbW9iaWxlJywgJ2Rlc2t0b3AnLCAnd2ViJ10pXG5cbmxldCBwbGF0Zm9ybTogUGxhdGZvcm0gfCBudWxsID0gbnVsbFxudm9pZCBnZXRFeHBsb3JlckluZm9ybWF0aW9uKHt9KVxuICAudGhlbigocmVzcG9uc2UpID0+IHtcbiAgICBjb25zdCBub3JtYWxpemVkID0gcmVzcG9uc2UucGxhdGZvcm0udG9Mb3dlckNhc2UoKVxuICAgIGlmIChWQUxJRF9QTEFURk9STVMuaGFzKG5vcm1hbGl6ZWQpKSB7XG4gICAgICBwbGF0Zm9ybSA9IG5vcm1hbGl6ZWQgYXMgUGxhdGZvcm1cbiAgICB9IGVsc2Uge1xuICAgICAgY29uc29sZS5lcnJvcihgVW5rbm93biBwbGF0Zm9ybSB2YWx1ZTogXCIke3Jlc3BvbnNlLnBsYXRmb3JtfVwiYClcbiAgICB9XG4gIH0pXG4gIC5jYXRjaCgoZXJyb3IpID0+IHtcbiAgICBjb25zb2xlLmVycm9yKCdGYWlsZWQgdG8gZ2V0IGV4cGxvcmVyIGluZm9ybWF0aW9uOicsIGVycm9yKVxuICB9KVxuXG5leHBvcnQgZnVuY3Rpb24gZ2V0UGxhdGZvcm0oKTogUGxhdGZvcm0gfCBudWxsIHtcbiAgcmV0dXJuIHBsYXRmb3JtXG59XG5cbmV4cG9ydCBmdW5jdGlvbiBpc01vYmlsZSgpOiBib29sZWFuIHtcbiAgcmV0dXJuIGdldFBsYXRmb3JtKCkgPT09ICdtb2JpbGUnXG59XG5cbmV4cG9ydCBmdW5jdGlvbiBpc0Rlc2t0b3AoKTogYm9vbGVhbiB7XG4gIHJldHVybiBnZXRQbGF0Zm9ybSgpID09PSAnZGVza3RvcCdcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIGlzV2ViKCk6IGJvb2xlYW4ge1xuICByZXR1cm4gZ2V0UGxhdGZvcm0oKSA9PT0gJ3dlYidcbn1cbiJdfQ==
61
+ /**
62
+ * Player's client language as a BCP-47 tag (e.g. `'es'`, `'pt-BR'`).
63
+ * Returns `'en'` until the explorer responds, and on clients that don't report a locale.
64
+ * Use `onPlayerLanguageChanged` to react to mid-session changes.
65
+ * @public
66
+ */
67
+ export function getPlayerLanguage() {
68
+ return language;
69
+ }
70
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvcGxhdGZvcm0vaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLHNCQUFzQixFQUFFLE1BQU0saUJBQWlCLENBQUE7QUFDeEQsT0FBTyxFQUFFLFNBQVMsRUFBRSxNQUFNLG1CQUFtQixDQUFBO0FBQzdDLE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQTtBQUNuRCxPQUFPLEVBQUUsbUJBQW1CLEVBQUUsTUFBTSw2QkFBNkIsQ0FBQTtBQUlqRSxNQUFNLGVBQWUsR0FBZ0IsSUFBSSxHQUFHLENBQVMsQ0FBQyxRQUFRLEVBQUUsU0FBUyxFQUFFLEtBQUssQ0FBQyxDQUFDLENBQUE7QUFFbEYsTUFBTSxnQkFBZ0IsR0FBRyxJQUFJLENBQUE7QUFDN0IsTUFBTSxTQUFTLEdBQUcscUNBQXFDLENBQUE7QUFFdkQsSUFBSSxRQUFRLEdBQW9CLElBQUksQ0FBQTtBQUNwQyxJQUFJLFFBQVEsR0FBVyxnQkFBZ0IsQ0FBQTtBQUN2QyxJQUFJLGtCQUFrQixHQUFHLEtBQUssQ0FBQTtBQUU5Qjs7OztHQUlHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sdUJBQXVCLEdBQUcsSUFBSSxVQUFVLEVBQXdCLENBQUE7QUFFN0UsU0FBUyxlQUFlLENBQUMsTUFBMEI7SUFDakQsTUFBTSxHQUFHLEdBQUcsQ0FBQyxNQUFNLElBQUksRUFBRSxDQUFDLENBQUMsSUFBSSxFQUFFLENBQUMsT0FBTyxDQUFDLElBQUksRUFBRSxHQUFHLENBQUMsQ0FBQTtJQUNwRCxPQUFPLFNBQVMsQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsZ0JBQWdCLENBQUE7QUFDckQsQ0FBQztBQUVELFNBQVMsV0FBVyxDQUFDLElBQVk7SUFDL0IsSUFBSSxJQUFJLEtBQUssUUFBUTtRQUFFLE9BQU07SUFDN0IsUUFBUSxHQUFHLElBQUksQ0FBQTtJQUNmLHVCQUF1QixDQUFDLGVBQWUsQ0FBQyxFQUFFLFFBQVEsRUFBRSxDQUFDLENBQUE7QUFDdkQsQ0FBQztBQUVELG1CQUFtQixDQUFDLEdBQUcsQ0FBQyxDQUFDLEtBQUssRUFBRSxFQUFFO0lBQ2hDLGtCQUFrQixHQUFHLElBQUksQ0FBQTtJQUN6QixXQUFXLENBQUMsZUFBZSxDQUFDLEtBQUssQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFBO0FBQzVDLENBQUMsQ0FBQyxDQUFBO0FBQ0YsU0FBUyxDQUFDLEVBQUUsT0FBTyxFQUFFLGVBQWUsRUFBRSxDQUFDLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUMsQ0FBQTtBQUU1RCxLQUFLLHNCQUFzQixDQUFDLEVBQUUsQ0FBQztLQUM1QixJQUFJLENBQUMsQ0FBQyxRQUFRLEVBQUUsRUFBRTtJQUNqQiw0RUFBNEU7SUFDNUUsSUFBSSxDQUFDLGtCQUFrQixFQUFFO1FBQ3ZCLFdBQVcsQ0FBQyxlQUFlLENBQUMsUUFBUSxDQUFDLGNBQWMsRUFBRSxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsQ0FBQTtLQUNsRTtJQUNELE1BQU0sVUFBVSxHQUFHLFFBQVEsQ0FBQyxRQUFRLENBQUMsV0FBVyxFQUFFLENBQUE7SUFDbEQsSUFBSSxlQUFlLENBQUMsR0FBRyxDQUFDLFVBQVUsQ0FBQyxFQUFFO1FBQ25DLFFBQVEsR0FBRyxVQUFzQixDQUFBO0tBQ2xDO1NBQU07UUFDTCxPQUFPLENBQUMsS0FBSyxDQUFDLDRCQUE0QixRQUFRLENBQUMsUUFBUSxHQUFHLENBQUMsQ0FBQTtLQUNoRTtBQUNILENBQUMsQ0FBQztLQUNELEtBQUssQ0FBQyxDQUFDLEtBQUssRUFBRSxFQUFFO0lBQ2YsT0FBTyxDQUFDLEtBQUssQ0FBQyxxQ0FBcUMsRUFBRSxLQUFLLENBQUMsQ0FBQTtBQUM3RCxDQUFDLENBQUMsQ0FBQTtBQUVKLE1BQU0sVUFBVSxXQUFXO0lBQ3pCLE9BQU8sUUFBUSxDQUFBO0FBQ2pCLENBQUM7QUFFRCxNQUFNLFVBQVUsUUFBUTtJQUN0QixPQUFPLFdBQVcsRUFBRSxLQUFLLFFBQVEsQ0FBQTtBQUNuQyxDQUFDO0FBRUQsTUFBTSxVQUFVLFNBQVM7SUFDdkIsT0FBTyxXQUFXLEVBQUUsS0FBSyxTQUFTLENBQUE7QUFDcEMsQ0FBQztBQUVELE1BQU0sVUFBVSxLQUFLO0lBQ25CLE9BQU8sV0FBVyxFQUFFLEtBQUssS0FBSyxDQUFBO0FBQ2hDLENBQUM7QUFFRDs7Ozs7R0FLRztBQUNILE1BQU0sVUFBVSxpQkFBaUI7SUFDL0IsT0FBTyxRQUFRLENBQUE7QUFDakIsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IGdldEV4cGxvcmVySW5mb3JtYXRpb24gfSBmcm9tICd+c3lzdGVtL1J1bnRpbWUnXG5pbXBvcnQgeyBzdWJzY3JpYmUgfSBmcm9tICd+c3lzdGVtL0VuZ2luZUFwaSdcbmltcG9ydCB7IE9ic2VydmFibGUgfSBmcm9tICcuLi9pbnRlcm5hbC9PYnNlcnZhYmxlJ1xuaW1wb3J0IHsgbG9jYWxlQ2hhbmdlZEV2ZW50cyB9IGZyb20gJy4uL2ludGVybmFsL2xhbmd1YWdlLWV2ZW50cydcblxuZXhwb3J0IHR5cGUgUGxhdGZvcm0gPSAnbW9iaWxlJyB8ICdkZXNrdG9wJyB8ICd3ZWInXG5cbmNvbnN0IFZBTElEX1BMQVRGT1JNUzogU2V0PHN0cmluZz4gPSBuZXcgU2V0PHN0cmluZz4oWydtb2JpbGUnLCAnZGVza3RvcCcsICd3ZWInXSlcblxuY29uc3QgREVGQVVMVF9MQU5HVUFHRSA9ICdlbidcbmNvbnN0IEJDUDQ3X1RBRyA9IC9eW2EtekEtWl17MiwzfSgtW2EtekEtWjAtOV17Miw4fSkqJC9cblxubGV0IHBsYXRmb3JtOiBQbGF0Zm9ybSB8IG51bGwgPSBudWxsXG5sZXQgbGFuZ3VhZ2U6IHN0cmluZyA9IERFRkFVTFRfTEFOR1VBR0VcbmxldCBsYW5ndWFnZVNldEJ5RXZlbnQgPSBmYWxzZVxuXG4vKipcbiAqIFRyaWdnZXJlZCB3aGVuIHRoZSBwbGF5ZXIgc3dpdGNoZXMgdGhlIGNsaWVudCBsYW5ndWFnZSBtaWQtc2Vzc2lvbi5cbiAqIE9sZGVyIGNsaWVudHMgbmV2ZXIgZW1pdCBpdC5cbiAqIEBwdWJsaWNcbiAqL1xuZXhwb3J0IGNvbnN0IG9uUGxheWVyTGFuZ3VhZ2VDaGFuZ2VkID0gbmV3IE9ic2VydmFibGU8eyBsYW5ndWFnZTogc3RyaW5nIH0+KClcblxuZnVuY3Rpb24gbm9ybWFsaXplTG9jYWxlKGxvY2FsZTogc3RyaW5nIHwgdW5kZWZpbmVkKTogc3RyaW5nIHtcbiAgY29uc3QgdGFnID0gKGxvY2FsZSA/PyAnJykudHJpbSgpLnJlcGxhY2UoL18vZywgJy0nKVxuICByZXR1cm4gQkNQNDdfVEFHLnRlc3QodGFnKSA/IHRhZyA6IERFRkFVTFRfTEFOR1VBR0Vcbn1cblxuZnVuY3Rpb24gc2V0TGFuZ3VhZ2UobmV4dDogc3RyaW5nKSB7XG4gIGlmIChuZXh0ID09PSBsYW5ndWFnZSkgcmV0dXJuXG4gIGxhbmd1YWdlID0gbmV4dFxuICBvblBsYXllckxhbmd1YWdlQ2hhbmdlZC5ub3RpZnlPYnNlcnZlcnMoeyBsYW5ndWFnZSB9KVxufVxuXG5sb2NhbGVDaGFuZ2VkRXZlbnRzLmFkZCgoZXZlbnQpID0+IHtcbiAgbGFuZ3VhZ2VTZXRCeUV2ZW50ID0gdHJ1ZVxuICBzZXRMYW5ndWFnZShub3JtYWxpemVMb2NhbGUoZXZlbnQubG9jYWxlKSlcbn0pXG5zdWJzY3JpYmUoeyBldmVudElkOiAnbG9jYWxlQ2hhbmdlZCcgfSkuY2F0Y2goY29uc29sZS5lcnJvcilcblxudm9pZCBnZXRFeHBsb3JlckluZm9ybWF0aW9uKHt9KVxuICAudGhlbigocmVzcG9uc2UpID0+IHtcbiAgICAvLyBhIG1pZC1zZXNzaW9uIGNoYW5nZSB0aGF0IGFycml2ZWQgZmlyc3QgaXMgbW9yZSByZWNlbnQgdGhhbiB0aGlzIHJlc3BvbnNlXG4gICAgaWYgKCFsYW5ndWFnZVNldEJ5RXZlbnQpIHtcbiAgICAgIHNldExhbmd1YWdlKG5vcm1hbGl6ZUxvY2FsZShyZXNwb25zZS5jb25maWd1cmF0aW9ucz8uWydsb2NhbGUnXSkpXG4gICAgfVxuICAgIGNvbnN0IG5vcm1hbGl6ZWQgPSByZXNwb25zZS5wbGF0Zm9ybS50b0xvd2VyQ2FzZSgpXG4gICAgaWYgKFZBTElEX1BMQVRGT1JNUy5oYXMobm9ybWFsaXplZCkpIHtcbiAgICAgIHBsYXRmb3JtID0gbm9ybWFsaXplZCBhcyBQbGF0Zm9ybVxuICAgIH0gZWxzZSB7XG4gICAgICBjb25zb2xlLmVycm9yKGBVbmtub3duIHBsYXRmb3JtIHZhbHVlOiBcIiR7cmVzcG9uc2UucGxhdGZvcm19XCJgKVxuICAgIH1cbiAgfSlcbiAgLmNhdGNoKChlcnJvcikgPT4ge1xuICAgIGNvbnNvbGUuZXJyb3IoJ0ZhaWxlZCB0byBnZXQgZXhwbG9yZXIgaW5mb3JtYXRpb246JywgZXJyb3IpXG4gIH0pXG5cbmV4cG9ydCBmdW5jdGlvbiBnZXRQbGF0Zm9ybSgpOiBQbGF0Zm9ybSB8IG51bGwge1xuICByZXR1cm4gcGxhdGZvcm1cbn1cblxuZXhwb3J0IGZ1bmN0aW9uIGlzTW9iaWxlKCk6IGJvb2xlYW4ge1xuICByZXR1cm4gZ2V0UGxhdGZvcm0oKSA9PT0gJ21vYmlsZSdcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIGlzRGVza3RvcCgpOiBib29sZWFuIHtcbiAgcmV0dXJuIGdldFBsYXRmb3JtKCkgPT09ICdkZXNrdG9wJ1xufVxuXG5leHBvcnQgZnVuY3Rpb24gaXNXZWIoKTogYm9vbGVhbiB7XG4gIHJldHVybiBnZXRQbGF0Zm9ybSgpID09PSAnd2ViJ1xufVxuXG4vKipcbiAqIFBsYXllcidzIGNsaWVudCBsYW5ndWFnZSBhcyBhIEJDUC00NyB0YWcgKGUuZy4gYCdlcydgLCBgJ3B0LUJSJ2ApLlxuICogUmV0dXJucyBgJ2VuJ2AgdW50aWwgdGhlIGV4cGxvcmVyIHJlc3BvbmRzLCBhbmQgb24gY2xpZW50cyB0aGF0IGRvbid0IHJlcG9ydCBhIGxvY2FsZS5cbiAqIFVzZSBgb25QbGF5ZXJMYW5ndWFnZUNoYW5nZWRgIHRvIHJlYWN0IHRvIG1pZC1zZXNzaW9uIGNoYW5nZXMuXG4gKiBAcHVibGljXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBnZXRQbGF5ZXJMYW5ndWFnZSgpOiBzdHJpbmcge1xuICByZXR1cm4gbGFuZ3VhZ2Vcbn1cbiJdfQ==
@@ -11,7 +11,6 @@ type GetPlayerDataRes = {
11
11
  avatar?: PBAvatarBase;
12
12
  wearables: PBAvatarEquippedData['wearableUrns'];
13
13
  emotes: PBAvatarEquippedData['emoteUrns'];
14
- forceRender: PBAvatarEquippedData['forceRender'];
15
14
  position: TransformType['position'] | undefined;
16
15
  };
17
16
  export declare function definePlayerHelper(engine: IEngine): {
package/players/index.js CHANGED
@@ -64,9 +64,11 @@ export function definePlayerHelper(engine) {
64
64
  isGuest: !!playerData?.isGuest,
65
65
  userId: playerData?.address ?? '',
66
66
  avatar: avatarData ?? undefined,
67
- wearables: wearablesData?.wearableUrns ?? [],
68
- emotes: wearablesData?.emoteUrns ?? [],
69
- forceRender: wearablesData?.forceRender ?? [],
67
+ // Component reads come back deeply readonly, so these are copied rather than
68
+ // aliased: the public Player type promises plain mutable arrays, and handing
69
+ // out the component's own frozen array would throw on any caller that writes.
70
+ wearables: [...(wearablesData?.wearableUrns ?? [])],
71
+ emotes: [...(wearablesData?.emoteUrns ?? [])],
70
72
  position: Transform.getOrNull(userEntity)?.position
71
73
  };
72
74
  }
@@ -76,4 +78,4 @@ const players = definePlayerHelper(engine);
76
78
  const { getPlayer, onEnterScene, onLeaveScene } = players;
77
79
  export { getPlayer, onEnterScene, onLeaveScene };
78
80
  export default players;
79
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvcGxheWVycy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQWtDLE1BQU0sRUFBRSxNQUFNLFVBQVUsQ0FBQTtBQUNqRSxPQUFPLEVBQ0wsa0JBQWtCLElBQUksdUJBQXVCLEVBQzdDLFVBQVUsSUFBSSxnQkFBZ0IsRUFDOUIsa0JBQWtCLElBQUksd0JBQXdCLEVBRzlDLFNBQVMsSUFBSSxlQUFlLEVBQzdCLE1BQU0sMEJBQTBCLENBQUE7QUFpQmpDLE1BQU0sVUFBVSxrQkFBa0IsQ0FBQyxNQUFlO0lBQ2hELE1BQU0sU0FBUyxHQUFHLGVBQWUsQ0FBQyxNQUFNLENBQUMsQ0FBQTtJQUN6QyxNQUFNLGtCQUFrQixHQUFHLHVCQUF1QixDQUFDLE1BQU0sQ0FBQyxDQUFBO0lBQzFELE1BQU0sa0JBQWtCLEdBQUcsd0JBQXdCLENBQUMsTUFBTSxDQUFDLENBQUE7SUFDM0QsTUFBTSxVQUFVLEdBQUcsZ0JBQWdCLENBQUMsTUFBTSxDQUFDLENBQUE7SUFDM0MsTUFBTSxjQUFjLEdBQUcsSUFBSSxHQUFHLEVBQWtCLENBQUE7SUFFaEQsTUFBTSxjQUFjLEdBQTJDLEVBQUUsQ0FBQTtJQUNqRSxNQUFNLGNBQWMsR0FBaUMsRUFBRSxDQUFBO0lBRXZELE1BQU0sQ0FBQyxTQUFTLENBQUMsR0FBRyxFQUFFO1FBQ3BCLE1BQU0sT0FBTyxHQUFHLEtBQUssQ0FBQyxJQUFJLENBQUMsTUFBTSxDQUFDLGVBQWUsQ0FBQyxrQkFBa0IsRUFBRSxVQUFVLENBQUMsQ0FBQyxDQUFBO1FBQ2xGLElBQUksT0FBTyxDQUFDLE1BQU0sS0FBSyxjQUFjLENBQUMsSUFBSTtZQUFFLE9BQU07UUFFbEQsS0FBSyxNQUFNLENBQUMsTUFBTSxFQUFFLFFBQVEsQ0FBQyxJQUFJLE9BQU8sRUFBRTtZQUN4QyxJQUFJLENBQUMsY0FBYyxDQUFDLEdBQUcsQ0FBQyxNQUFNLENBQUMsRUFB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TyxHQUFHLGtCQUFrQixDQUFDLE1BQU0sQ0FBQyxDQUFBO0FBQzFDLE1BQU0sRUFBRSxTQUFTLEVBQUUsWUFBWSxFQUFFLFlBQVksRUFBRSxHQUFHLE9BQU8sQ0FBQTtBQUV6RCxPQUFPLEVBQUUsU0FBUyxFQUFFLFlBQVksRUFBRSxZQUFZLEVBQUUsQ0FBQTtBQUNoRCxlQUFlLE9BQU8sQ0FBQSIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IEVudGl0eSwgSUVuZ2luZSwgVHJhbnNmb3JtVHlwZSwgZW5naW5lIH0gZnJvbSAnQGRjbC9lY3MnXG5pbXBvcnQge1xuICBQbGF5ZXJJZGVudGl0eURhdGEgYXMgZGVmaW5lUGxheWVySWRlbml0eURhdGEsXG4gIEF2YXRhckJhc2UgYXMgZGVmaW5lQXZhdGFyQmFzZSxcbiAgQXZhdGFyRXF1aXBwZWREYXRhIGFzIGRlZmluZUF2YXRhckVxdWlwcGVkRGF0YSxcbiAgUEJBdmF0YXJCYXNlLFxuICBQQkF2YXRhckVxdWlwcGVkRGF0YSxcbiAgVHJhbnNmb3JtIGFzIGRlZmluZVRyYW5zZm9ybVxufSBmcm9tICdAZGNsL2Vjcy9kaXN0L2NvbXBvbmVudHMnXG5cbnR5cGUgR2V0UGxheWVyRGF0YVJlcSA9IHtcbiAgdXNlcklkOiBzdHJpbmdcbn1cbnR5cGUgR2V0UGxheWVyRGF0YVJlcyA9IHtcbiAgZW50aXR5OiBFbnRpdHlcbiAgbmFtZTogc3RyaW5nXG4gIGlzR3Vlc3Q6IGJvb2xlYW5cbiAgdXNlcklkOiBzdHJpbmdcbiAgYXZhdGFyPzogUEJBdmF0YXJCYXNlXG4gIHdlYXJhYmxlczogUEJBdmF0YXJFcXVpcHBlZERhdGFbJ3dlYXJhYmxlVXJucyddXG4gIGVtb3RlczogUEJBdmF0YXJFcXVpcHBlZERhdGFbJ2Vtb3RlVXJucyddXG4gIGZvcmNlUmVuZGVyOiBQQkF2YXRhckVxdWlwcGVkRGF0YVsnZm9yY2VSZW5kZXInXVxuICBwb3NpdGlvbjogVHJhbnNmb3JtVHlwZVsncG9zaXRpb24nXSB8IHVuZGVmaW5lZFxufVxuXG5leHBvcnQgZnVuY3Rpb24gZGVmaW5lUGxheWVySGVscGVyKGVuZ2luZTogSUVuZ2luZSkge1xuICBjb25zdCBUcmFuc2Zvcm0gPSBkZWZpbmVUcmFuc2Zvcm0oZW5naW5lKVxuICBjb25zdCBQbGF5ZXJJZGVudGl0eURhdGEgPSBkZWZpbmVQbGF5ZXJJZGVuaXR5RGF0YShlbmdpbmUpXG4gIGNvbnN0IEF2YXRhckVxdWlwcGVkRGF0YSA9IGRlZmluZUF2YXRhckVxdWlwcGVkRGF0YShlbmdpbmUpXG4gIGNvbnN0IEF2YXRhckJhc2UgPSBkZWZpbmVBdmF0YXJCYXNlKGVuZ2luZSlcbiAgY29uc3QgcGxheWVyRW50aXRpZXMgPSBuZXcgTWFwPEVudGl0eSwgc3RyaW5nPigpXG5cbiAgY29uc3Qgb25FbnRlclNjZW5lQ2I6ICgocGxheWVyOiBHZXRQbGF5ZXJEYXRhUmVzKSA9PiB2b2lkKVtdID0gW11cbiAgY29uc3Qgb25MZWF2ZVNjZW5lQ2I6ICgodXNlcklkOiBzdHJpbmcpID0+IHZvaWQpW10gPSBbXVxuXG4gIGVuZ2luZS5hZGRTeXN0ZW0oKCkgPT4ge1xuICAgIGNvbnN0IHBsYXllcnMgPSBBcnJheS5mcm9tKGVuZ2luZS5nZXRFbnRpdGllc1dpdGgoUGxheWVySWRlbnRpdHlEYXRhLCBBdmF0YXJCYXNlKSlcbiAgICBpZiAocGxheWVycy5sZW5ndGggPT09IHBsYXllckVudGl0aWVzLnNpemUpIHJldHVyblxuXG4gICAgZm9yIChjb25zdCBbZW50aXR5LCBpZGVudGl0eV0gb2YgcGxheWVycykge1xuICAgICAgaWYgKCFwbGF5ZXJFbnRpdGllcy5oYXMoZW50aXR5KSkge1xuICAgICAgICBwbGF5ZXJFbnRpdGllcy5zZXQoZW50aXR5LCBpZGVudGl0eS5hZGRyZXNzKVxuXG4gICAgICAgIC8vIENhbGwgb25FbnRlciBjYWxsYmFja1xuICAgICAgICBpZiAob25FbnRlclNjZW5lQ2IubGVuZ3RoKSB7XG4gICAgICAgICAgb25FbnRlclNjZW5lQ2IuZm9yRWFjaCgoY2IpID0+IGNiKGdldFBsYXllcih7IHVzZXJJZDogaWRlbnRpdHkuYWRkcmVzcyB9KSEpKVxuICAgICAgICB9XG5cbiAgICAgICAgLy8gQ2hlY2sgZm9yIGNoYW5nZXMvcmVtb3ZlIGNhbGxiYWNrc1xuICAgICAgICBBdmF0YXJCYXNlLm9uQ2hhbmdlKGVudGl0eSwgKHZhbHVlKSA9PiB7XG4gICAgICAgICAgaWYgKCF2YWx1ZSAmJiBwbGF5ZXJFbnRpdGllcy5nZXQoZW50aXR5KSkge1xuICAgICAgICAgICAgb25MZWF2ZVNjZW5lQ2IuZm9yRWFjaCgoY2IpID0+IGNiKHBsYXllckVudGl0aWVzLmdldChlbnRpdHkpISkpXG4gICAgICAgICAgICBwbGF5ZXJFbnRpdGllcy5kZWxldGUoZW50aXR5KVxuICAgICAgICAgIH1cbiAgICAgICAgfSlcbiAgICAgIH1cbiAgICB9XG4gIH0pXG5cbiAgcmV0dXJuIHtcbiAgICBvbkVudGVyU2NlbmUoY2I6IChwbGF5ZXI6IEdldFBsYXllckRhdGFSZXMpID0+IHZvaWQpIHtcbiAgICAgIG9uRW50ZXJTY2VuZUNiLnB1c2goY2IpXG4gICAgfSxcbiAgICBvbkxlYXZlU2NlbmUoY2I6ICh1c2VySWQ6IHN0cmluZykgPT4gdm9pZCkge1xuICAgICAgb25MZWF2ZVNjZW5lQ2IucHVzaChjYilcbiAgICB9LFxuICAgIC8qKlxuICAgICAqIFJldHVybnMgdGhlIGluZm8gb2YgdGhlIHBsYXllciBpZiBpdCdzIGluIHRoZSBzY2VuZS5cbiAgICAgKi9cbiAgICBnZXRQbGF5ZXIodXNlcj86IEdldFBsYXllckRhdGFSZXEpOiBHZXRQbGF5ZXJEYXRhUmVzIHwgbnVsbCB7XG4gICAgICBmdW5jdGlvbiBnZXRFbnRpdHkoKSB7XG4gICAgICAgIGlmICghdXNlcj8udXNlcklkKSByZXR1cm4gZW5naW5lLlBsYXllckVudGl0eVxuICAgICAgICBmb3IgKGNvbnN0IFtlbnRpdHksIGRhdGFdIG9mIGVuZ2luZS5nZXRFbnRpdGllc1dpdGgoUGxheWVySWRlbnRpdHlEYXRhKSkge1xuICAgICAgICAgIGlmIChkYXRhLmFkZHJlc3MgPT09IHVzZXIudXNlcklkKSB7XG4gICAgICAgICAgICByZXR1cm4gZW50aXR5XG4gICAgICAgICAgfVxuICAgICAgICB9XG4gICAgICAgIHJldHVybiB1bmRlZmluZWRcbiAgICAgIH1cblxuICAgICAgY29uc3QgdXNlckVudGl0eSA9IGdldEVudGl0eSgpXG4gICAgICBpZiAoIXVzZXJFbnRpdHkpIHJldHVybiBudWxsXG5cbiAgICAgIGNvbnN0IHBsYXllckRhdGEgPSBQbGF5ZXJJZGVudGl0eURhdGEuZ2V0T3JOdWxsKHVzZXJFbnRpdHkpXG4gICAgICBjb25zdCBhdmF0YXJEYXRhID0gQXZhdGFyQmFzZS5nZXRPck51bGwodXNlckVudGl0eSlcbiAgICAgIGNvbnN0IHdlYXJhYmxlc0RhdGEgPSBBdmF0YXJFcXVpcHBlZERhdGEuZ2V0T3JOdWxsKHVzZXJFbnRpdHkpXG5cbiAgICAgIGlmICghcGxheWVyRGF0YSAmJiAhYXZhdGFyRGF0YSAmJiAhd2VhcmFibGVzRGF0YSkgcmV0dXJuIG51bGxcblxuICAgICAgcmV0dXJuIHtcbiAgICAgICAgZW50aXR5OiB1c2VyRW50aXR5LFxuICAgICAgICBuYW1lOiBhdmF0YXJEYXRhPy5uYW1lID8/ICcnLFxuICAgICAgICBpc0d1ZXN0OiAhIXBsYXllckRhdGE/LmlzR3Vlc3QsXG4gICAgICAgIHVzZXJJZDogcGxheWVyRGF0YT8uYWRkcmVzcyA/PyAnJyxcbiAgICAgICAgYXZhdGFyOiBhdmF0YXJEYXRhID8/IHVuZGVmaW5lZCxcbiAgICAgICAgd2VhcmFibGVzOiB3ZWFyYWJsZXNEYXRhPy53ZWFyYWJsZVVybnMgPz8gW10sXG4gICAgICAgIGVtb3Rlczogd2VhcmFibGVzRGF0YT8uZW1vdGVVcm5zID8/IFtdLFxuICAgICAgICBmb3JjZVJlbmRlcjogd2VhcmFibGVzRGF0YT8uZm9yY2VSZW5kZXIgPz8gW10sXG4gICAgICAgIHBvc2l0aW9uOiBUcmFuc2Zvcm0uZ2V0T3JOdWxsKHVzZXJFbnRpdHkpPy5wb3NpdGlvblxuICAgICAgfVxuICAgIH1cbiAgfVxufVxuXG5jb25zdCBwbGF5ZXJzID0gZGVmaW5lUGxheWVySGVscGVyKGVuZ2luZSlcbmNvbnN0IHsgZ2V0UGxheWVyLCBvbkVudGVyU2NlbmUsIG9uTGVhdmVTY2VuZSB9ID0gcGxheWVyc1xuXG5leHBvcnQgeyBnZXRQbGF5ZXIsIG9uRW50ZXJTY2VuZSwgb25MZWF2ZVNjZW5lIH1cbmV4cG9ydCBkZWZhdWx0IHBsYXllcnNcbiJdfQ==
81
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvcGxheWVycy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQWtDLE1BQU0sRUFBRSxNQUFNLFVBQVUsQ0FBQTtBQUNqRSxPQUFPLEVBQ0wsa0JBQWtCLElBQUksdUJBQXVCLEVBQzdDLFVBQVUsSUFBSSxnQkFBZ0IsRUFDOUIsa0JBQWtCLElBQUksd0JBQXdCLEVBRzlDLFNBQVMsSUFBSSxlQUFlLEVBQzdCLE1BQU0sMEJBQTBCLENBQUE7QUFnQmpDLE1BQU0sVUFBVSxrQkFBa0IsQ0FBQyxNQUFlO0lBQ2hELE1BQU0sU0FBUyxHQUFHLGVBQWUsQ0FBQyxNQUFNLENBQUMsQ0FBQTtJQUN6QyxNQUFNLGtCQUFrQixHQUFHLHVCQUF1QixDQUFDLE1BQU0sQ0FBQyxDQUFBO0lBQzFELE1BQU0sa0JBQWtCLEdBQUcsd0JBQXdCLENBQUMsTUFBTSxDQUFDLENBQUE7SUFDM0QsTUFBTSxVQUFVLEdBQUcsZ0JBQWdCLENBQUMsTUFBTSxDQUFDLENBQUE7SUFDM0MsTUFBTSxjQUFjLEdBQUcsSUFBSSxHQUFHLEVBQWtCLENBQUE7SUFFaEQsTUFBTSxjQUFjLEdBQTJDLEVBQUUsQ0FBQTtJQUNqRSxNQUFNLGNBQWMsR0FBaUMsRUFBRSxDQUFBO0lBRXZELE1BQU0sQ0FBQyxTQUFTLENBQUMsR0FBRyxFQUFFO1FBQ3BCLE1BQU0sT0FBTyxHQUFHLEtBQUssQ0FBQyxJQUFJLENBQUMsTUFBTSxDQUFDLGVBQWUsQ0FBQyxrQkFBa0IsRUFBRSxVQUFVLENBQUMsQ0FBQyxDQUFBO1FBQ2xGLElBQUksT0FBTyxDQUFDLE1BQU0sS0FBSyxjQUFjLENBQUMsSUFBSTtZQUFFLE9BQU07UUFFbEQsS0FBSyxNQUFNLENBQUMsTUFBTSxFQUFFLFFBQVEsQ0FBQyxJQUFJLE9BQU8sRUFBRTtZQUN4QyxJQUFJLENBQUMsY0FBYyxDQUFDLEdBQUcsQ0FBQyxNQUFNLENBQUMsRUFB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RCxDQUFBO1FBQ0gsQ0FBQztLQUNGLENBQUE7QUFDSCxDQUFDO0FBRUQsTUFBTSxPQUFPLEdBQUcsa0JBQWtCLENBQUMsTUFBTSxDQUFDLENBQUE7QUFDMUMsTUFBTSxFQUFFLFNBQVMsRUFBRSxZQUFZLEVBQUUsWUFBWSxFQUFFLEdBQUcsT0FBTyxDQUFBO0FBRXpELE9BQU8sRUFBRSxTQUFTLEVBQUUsWUFBWSxFQUFFLFlBQVksRUFBRSxDQUFBO0FBQ2hELGVBQWUsT0FBTyxDQUFBIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgRW50aXR5LCBJRW5naW5lLCBUcmFuc2Zvcm1UeXBlLCBlbmdpbmUgfSBmcm9tICdAZGNsL2VjcydcbmltcG9ydCB7XG4gIFBsYXllcklkZW50aXR5RGF0YSBhcyBkZWZpbmVQbGF5ZXJJZGVuaXR5RGF0YSxcbiAgQXZhdGFyQmFzZSBhcyBkZWZpbmVBdmF0YXJCYXNlLFxuICBBdmF0YXJFcXVpcHBlZERhdGEgYXMgZGVmaW5lQXZhdGFyRXF1aXBwZWREYXRhLFxuICBQQkF2YXRhckJhc2UsXG4gIFBCQXZhdGFyRXF1aXBwZWREYXRhLFxuICBUcmFuc2Zvcm0gYXMgZGVmaW5lVHJhbnNmb3JtXG59IGZyb20gJ0BkY2wvZWNzL2Rpc3QvY29tcG9uZW50cydcblxudHlwZSBHZXRQbGF5ZXJEYXRhUmVxID0ge1xuICB1c2VySWQ6IHN0cmluZ1xufVxudHlwZSBHZXRQbGF5ZXJEYXRhUmVzID0ge1xuICBlbnRpdHk6IEVudGl0eVxuICBuYW1lOiBzdHJpbmdcbiAgaXNHdWVzdDogYm9vbGVhblxuICB1c2VySWQ6IHN0cmluZ1xuICBhdmF0YXI/OiBQQkF2YXRhckJhc2VcbiAgd2VhcmFibGVzOiBQQkF2YXRhckVxdWlwcGVkRGF0YVsnd2VhcmFibGVVcm5zJ11cbiAgZW1vdGVzOiBQQkF2YXRhckVxdWlwcGVkRGF0YVsnZW1vdGVVcm5zJ11cbiAgcG9zaXRpb246IFRyYW5zZm9ybVR5cGVbJ3Bvc2l0aW9uJ10gfCB1bmRlZmluZWRcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIGRlZmluZVBsYXllckhlbHBlcihlbmdpbmU6IElFbmdpbmUpIHtcbiAgY29uc3QgVHJhbnNmb3JtID0gZGVmaW5lVHJhbnNmb3JtKGVuZ2luZSlcbiAgY29uc3QgUGxheWVySWRlbnRpdHlEYXRhID0gZGVmaW5lUGxheWVySWRlbml0eURhdGEoZW5naW5lKVxuICBjb25zdCBBdmF0YXJFcXVpcHBlZERhdGEgPSBkZWZpbmVBdmF0YXJFcXVpcHBlZERhdGEoZW5naW5lKVxuICBjb25zdCBBdmF0YXJCYXNlID0gZGVmaW5lQXZhdGFyQmFzZShlbmdpbmUpXG4gIGNvbnN0IHBsYXllckVudGl0aWVzID0gbmV3IE1hcDxFbnRpdHksIHN0cmluZz4oKVxuXG4gIGNvbnN0IG9uRW50ZXJTY2VuZUNiOiAoKHBsYXllcjogR2V0UGxheWVyRGF0YVJlcykgPT4gdm9pZClbXSA9IFtdXG4gIGNvbnN0IG9uTGVhdmVTY2VuZUNiOiAoKHVzZXJJZDogc3RyaW5nKSA9PiB2b2lkKVtdID0gW11cblxuICBlbmdpbmUuYWRkU3lzdGVtKCgpID0+IHtcbiAgICBjb25zdCBwbGF5ZXJzID0gQXJyYXkuZnJvbShlbmdpbmUuZ2V0RW50aXRpZXNXaXRoKFBsYXllcklkZW50aXR5RGF0YSwgQXZhdGFyQmFzZSkpXG4gICAgaWYgKHBsYXllcnMubGVuZ3RoID09PSBwbGF5ZXJFbnRpdGllcy5zaXplKSByZXR1cm5cblxuICAgIGZvciAoY29uc3QgW2VudGl0eSwgaWRlbnRpdHldIG9mIHBsYXllcnMpIHtcbiAgICAgIGlmICghcGxheWVyRW50aXRpZXMuaGFzKGVudGl0eSkpIHtcbiAgICAgICAgcGxheWVyRW50aXRpZXMuc2V0KGVudGl0eSwgaWRlbnRpdHkuYWRkcmVzcylcblxuICAgICAgICAvLyBDYWxsIG9uRW50ZXIgY2FsbGJhY2tcbiAgICAgICAgaWYgKG9uRW50ZXJTY2VuZUNiLmxlbmd0aCkge1xuICAgICAgICAgIG9uRW50ZXJTY2VuZUNiLmZvckVhY2goKGNiKSA9PiBjYihnZXRQbGF5ZXIoeyB1c2VySWQ6IGlkZW50aXR5LmFkZHJlc3MgfSkhKSlcbiAgICAgICAgfVxuXG4gICAgICAgIC8vIENoZWNrIGZvciBjaGFuZ2VzL3JlbW92ZSBjYWxsYmFja3NcbiAgICAgICAgQXZhdGFyQmFzZS5vbkNoYW5nZShlbnRpdHksICh2YWx1ZSkgPT4ge1xuICAgICAgICAgIGlmICghdmFsdWUgJiYgcGxheWVyRW50aXRpZXMuZ2V0KGVudGl0eSkpIHtcbiAgICAgICAgICAgIG9uTGVhdmVTY2VuZUNiLmZvckVhY2goKGNiKSA9PiBjYihwbGF5ZXJFbnRpdGllcy5nZXQoZW50aXR5KSEpKVxuICAgICAgICAgICAgcGxheWVyRW50aXRpZXMuZGVsZXRlKGVudGl0eSlcbiAgICAgICAgICB9XG4gICAgICAgIH0pXG4gICAgICB9XG4gICAgfVxuICB9KVxuXG4gIHJldHVybiB7XG4gICAgb25FbnRlclNjZW5lKGNiOiAocGxheWVyOiBHZXRQbGF5ZXJEYXRhUmVzKSA9PiB2b2lkKSB7XG4gICAgICBvbkVudGVyU2NlbmVDYi5wdXNoKGNiKVxuICAgIH0sXG4gICAgb25MZWF2ZVNjZW5lKGNiOiAodXNlcklkOiBzdHJpbmcpID0+IHZvaWQpIHtcbiAgICAgIG9uTGVhdmVTY2VuZUNiLnB1c2goY2IpXG4gICAgfSxcbiAgICAvKipcbiAgICAgKiBSZXR1cm5zIHRoZSBpbmZvIG9mIHRoZSBwbGF5ZXIgaWYgaXQncyBpbiB0aGUgc2NlbmUuXG4gICAgICovXG4gICAgZ2V0UGxheWVyKHVzZXI/OiBHZXRQbGF5ZXJEYXRhUmVxKTogR2V0UGxheWVyRGF0YVJlcyB8IG51bGwge1xuICAgICAgZnVuY3Rpb24gZ2V0RW50aXR5KCkge1xuICAgICAgICBpZiAoIXVzZXI/LnVzZXJJZCkgcmV0dXJuIGVuZ2luZS5QbGF5ZXJFbnRpdHlcbiAgICAgICAgZm9yIChjb25zdCBbZW50aXR5LCBkYXRhXSBvZiBlbmdpbmUuZ2V0RW50aXRpZXNXaXRoKFBsYXllcklkZW50aXR5RGF0YSkpIHtcbiAgICAgICAgICBpZiAoZGF0YS5hZGRyZXNzID09PSB1c2VyLnVzZXJJZCkge1xuICAgICAgICAgICAgcmV0dXJuIGVudGl0eVxuICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgICByZXR1cm4gdW5kZWZpbmVkXG4gICAgICB9XG5cbiAgICAgIGNvbnN0IHVzZXJFbnRpdHkgPSBnZXRFbnRpdHkoKVxuICAgICAgaWYgKCF1c2VyRW50aXR5KSByZXR1cm4gbnVsbFxuXG4gICAgICBjb25zdCBwbGF5ZXJEYXRhID0gUGxheWVySWRlbnRpdHlEYXRhLmdldE9yTnVsbCh1c2VyRW50aXR5KVxuICAgICAgY29uc3QgYXZhdGFyRGF0YSA9IEF2YXRhckJhc2UuZ2V0T3JOdWxsKHVzZXJFbnRpdHkpXG4gICAgICBjb25zdCB3ZWFyYWJsZXNEYXRhID0gQXZhdGFyRXF1aXBwZWREYXRhLmdldE9yTnVsbCh1c2VyRW50aXR5KVxuXG4gICAgICBpZiAoIXBsYXllckRhdGEgJiYgIWF2YXRhckRhdGEgJiYgIXdlYXJhYmxlc0RhdGEpIHJldHVybiBudWxsXG5cbiAgICAgIHJldHVybiB7XG4gICAgICAgIGVudGl0eTogdXNlckVudGl0eSxcbiAgICAgICAgbmFtZTogYXZhdGFyRGF0YT8ubmFtZSA/PyAnJyxcbiAgICAgICAgaXNHdWVzdDogISFwbGF5ZXJEYXRhPy5pc0d1ZXN0LFxuICAgICAgICB1c2VySWQ6IHBsYXllckRhdGE/LmFkZHJlc3MgPz8gJycsXG4gICAgICAgIGF2YXRhcjogYXZhdGFyRGF0YSA/PyB1bmRlZmluZWQsXG4gICAgICAgIC8vIENvbXBvbmVudCByZWFkcyBjb21lIGJhY2sgZGVlcGx5IHJlYWRvbmx5LCBzbyB0aGVzZSBhcmUgY29waWVkIHJhdGhlciB0aGFuXG4gICAgICAgIC8vIGFsaWFzZWQ6IHRoZSBwdWJsaWMgUGxheWVyIHR5cGUgcHJvbWlzZXMgcGxhaW4gbXV0YWJsZSBhcnJheXMsIGFuZCBoYW5kaW5nXG4gICAgICAgIC8vIG91dCB0aGUgY29tcG9uZW50J3Mgb3duIGZyb3plbiBhcnJheSB3b3VsZCB0aHJvdyBvbiBhbnkgY2FsbGVyIHRoYXQgd3JpdGVzLlxuICAgICAgICB3ZWFyYWJsZXM6IFsuLi4od2VhcmFibGVzRGF0YT8ud2VhcmFibGVVcm5zID8/IFtdKV0sXG4gICAgICAgIGVtb3RlczogWy4uLih3ZWFyYWJsZXNEYXRhPy5lbW90ZVVybnMgPz8gW10pXSxcbiAgICAgICAgcG9zaXRpb246IFRyYW5zZm9ybS5nZXRPck51bGwodXNlckVudGl0eSk/LnBvc2l0aW9uXG4gICAgICB9XG4gICAgfVxuICB9XG59XG5cbmNvbnN0IHBsYXllcnMgPSBkZWZpbmVQbGF5ZXJIZWxwZXIoZW5naW5lKVxuY29uc3QgeyBnZXRQbGF5ZXIsIG9uRW50ZXJTY2VuZSwgb25MZWF2ZVNjZW5lIH0gPSBwbGF5ZXJzXG5cbmV4cG9ydCB7IGdldFBsYXllciwgb25FbnRlclNjZW5lLCBvbkxlYXZlU2NlbmUgfVxuZXhwb3J0IGRlZmF1bHQgcGxheWVyc1xuIl19
@@ -0,0 +1,398 @@
1
+ import {
2
+ engine,
3
+ ExplorerItemPurchaseResult,
4
+ ExplorerUi,
5
+ ExplorerUiEventsResult,
6
+ GrowOnlyValueSetComponentDefinition,
7
+ IEngine,
8
+ PBExplorerItemPurchaseResult,
9
+ PBExplorerUiEventsResult
10
+ } from '@dcl/ecs'
11
+ import { openExplorerUi, OpenExplorerUiResult } from '~system/RestrictedActions'
12
+
13
+ export { ExplorerUi, OpenExplorerUiResult }
14
+
15
+ // What an explorer that does not echo request_id, or a user-initiated action, leaves on the wire.
16
+ const UNCORRELATED = 0
17
+
18
+ // Panels that can be on screen next to another panel. The `ui` field alone cannot attribute
19
+ // their events, so an event on one of these panels must carry a request_id.
20
+ const CONCURRENT_PANELS: readonly ExplorerUi[] = [ExplorerUi.EU_ITEM_PURCHASE]
21
+
22
+ /** @public */
23
+ export type CorrelatedEvent = {
24
+ timestamp: number
25
+ requestId: number
26
+ }
27
+
28
+ /**
29
+ * A named stream of events that this helper can collect and stop on.
30
+ * @public
31
+ */
32
+ export type Channel<N extends string = string, T extends CorrelatedEvent = CorrelatedEvent> = {
33
+ readonly name: N
34
+ readonly component: GrowOnlyValueSetComponentDefinition<T>
35
+ }
36
+
37
+ /** @public */
38
+ export type AnyChannel = Channel<string, any>
39
+
40
+ /**
41
+ * Wraps a grow-only result component as a {@link Channel}. The name must be unique within a scene.
42
+ * @public
43
+ */
44
+ export function channel<N extends string, T extends CorrelatedEvent>(
45
+ name: N,
46
+ component: GrowOnlyValueSetComponentDefinition<T>
47
+ ): Channel<N, T> {
48
+ return { name, component }
49
+ }
50
+
51
+ /** Panel lifecycle events: opened and closed. @public */
52
+ export const ExplorerUiEvents: Channel<'explorerUi', PBExplorerUiEventsResult> = /* @__PURE__ */ channel(
53
+ 'explorerUi',
54
+ ExplorerUiEventsResult
55
+ )
56
+
57
+ /** Outcome of an `EU_ITEM_PURCHASE` flow: purchased, dismissed or failed. @public */
58
+ export const ItemPurchase: Channel<'itemPurchase', PBExplorerItemPurchaseResult> = /* @__PURE__ */ channel(
59
+ 'itemPurchase',
60
+ ExplorerItemPurchaseResult
61
+ )
62
+
63
+ /**
64
+ * The `$case` names of a channel's event union.
65
+ * @public
66
+ */
67
+ export type CaseOf<C extends AnyChannel> =
68
+ C extends Channel<any, infer T> ? Extract<T[keyof T], { $case: string }>['$case'] : never
69
+
70
+ /** @public */
71
+ export type Variant<C extends AnyChannel = AnyChannel, K extends string = string> = {
72
+ readonly channel: C
73
+ readonly $case: K
74
+ }
75
+
76
+ /** @public */
77
+ export function variant<C extends AnyChannel, K extends CaseOf<C>>(ch: C, $case: K): Variant<C, K> {
78
+ return { channel: ch, $case }
79
+ }
80
+
81
+ /**
82
+ * What ends the wait before the panel closes: any event on a channel, or one `$case` of it.
83
+ * @public
84
+ */
85
+ export type StopCondition = AnyChannel | Variant
86
+
87
+ /** @public */
88
+ export type Collected<C> = C extends Channel<infer N, infer T> ? { channel: N; event: T } : never
89
+
90
+ /**
91
+ * How the wait ended.
92
+ * - `closed`: the panel this call opened was closed.
93
+ * - `matched`: `until` fired first, so the panel may still be on screen.
94
+ * - `notOpened`: this call did not open anything. `openResult` says why.
95
+ * - `timedOut`: `timeoutMs` elapsed first.
96
+ *
97
+ * `events` always holds everything collected, the event that stopped the wait included.
98
+ * @public
99
+ */
100
+ export type WaitOutcome<C extends readonly AnyChannel[]> =
101
+ | { $case: 'closed'; events: readonly Collected<C[number]>[] }
102
+ | { $case: 'matched'; events: readonly Collected<C[number]>[] }
103
+ | { $case: 'notOpened'; openResult: OpenExplorerUiResult }
104
+ | { $case: 'timedOut'; events: readonly Collected<C[number]>[] }
105
+
106
+ /** @public */
107
+ export type WaitOptions<C extends readonly AnyChannel[]> = {
108
+ /** Channels whose events land in `events`. Defaults to none. */
109
+ collect?: C
110
+ /** Stops the wait early. The panel closing always stops it regardless. */
111
+ until?: StopCondition
112
+ /**
113
+ * Bounds the wait, in milliseconds. On expiry the promise resolves with `$case: 'timedOut'`.
114
+ * No default, because a panel can stay open for minutes. Without it, an explorer that opens
115
+ * a panel and then emits no `closed` leaves the promise pending for the scene's whole life.
116
+ */
117
+ timeoutMs?: number
118
+ }
119
+
120
+ /** @public */
121
+ export type OpenExplorerUiRequest =
122
+ | { ui: Exclude<ExplorerUi, ExplorerUi.EU_ITEM_PURCHASE>; itemPurchase?: undefined }
123
+ | { ui: ExplorerUi.EU_ITEM_PURCHASE; itemPurchase: { urn: string } }
124
+
125
+ /**
126
+ * Signature of {@link openExplorerUiAndWait}. The second overload takes a bare `ui`, so a
127
+ * computed panel value needs no narrowing.
128
+ * @public
129
+ */
130
+ export interface OpenExplorerUiAndWait {
131
+ <const C extends readonly AnyChannel[] = []>(
132
+ request: OpenExplorerUiRequest,
133
+ options?: WaitOptions<C>
134
+ ): Promise<WaitOutcome<C>>
135
+ (ui: Exclude<ExplorerUi, ExplorerUi.EU_ITEM_PURCHASE>): Promise<WaitOutcome<[]>>
136
+ }
137
+
138
+ /** @internal exposed for tests */
139
+ export const EXPLORER_UI_WAIT_TIMEOUT_SYSTEM = 'explorer-ui-wait-timeout'
140
+
141
+ // Lowest priority sorts this system last, so when it removes itself mid-tick it splices the
142
+ // last element of the array the engine is iterating and skips no other system.
143
+ const TIMEOUT_SYSTEM_PRIORITY = Number.MIN_SAFE_INTEGER
144
+
145
+ type OpenExplorerUiFn = (body: {
146
+ ui: ExplorerUi
147
+ requestId?: number
148
+ itemPurchase?: { urn: string }
149
+ }) => Promise<{ openResult: OpenExplorerUiResult }>
150
+
151
+ type CollectedEvent = { channel: string; event: CorrelatedEvent }
152
+
153
+ type Session = {
154
+ requestId: number
155
+ ui: ExplorerUi
156
+ correlationRequired: boolean
157
+ collect: ReadonlySet<string>
158
+ watched: ReadonlySet<string>
159
+ until: { channel: string; $case: string | undefined } | undefined
160
+ events: CollectedEvent[]
161
+ timeoutMs: number | undefined
162
+ elapsedMs: number
163
+ settled: boolean
164
+ settle: (outcome: WaitOutcome<never[]>) => void
165
+ fail: (error: Error) => void
166
+ }
167
+
168
+ type Attribution = 'yes' | 'no' | 'ambiguous'
169
+
170
+ // ts-proto renders a `oneof` as one wrapper property `{ $case, [$case]: value }`. Finding that
171
+ // property by shape keeps the helper independent of the name each message gave it: `event` on
172
+ // one message, `status` on another.
173
+ function caseOf(event: CorrelatedEvent): string | undefined {
174
+ for (const key of Object.keys(event)) {
175
+ const value = (event as unknown as Record<string, unknown>)[key]
176
+ if (value && typeof value === 'object' && typeof (value as { $case?: unknown }).$case === 'string') {
177
+ return (value as { $case: string }).$case
178
+ }
179
+ }
180
+ return undefined
181
+ }
182
+
183
+ function normalizeStop(stop: StopCondition): { channel: AnyChannel; $case: string | undefined } {
184
+ return '$case' in stop ? { channel: stop.channel, $case: stop.$case } : { channel: stop, $case: undefined }
185
+ }
186
+
187
+ /** @internal test seam. The production instance is {@link openExplorerUiAndWait}. */
188
+ export function create(deps: {
189
+ engine: IEngine
190
+ openExplorerUi: OpenExplorerUiFn
191
+ explorerUiEvents: Channel<'explorerUi', PBExplorerUiEventsResult>
192
+ }) {
193
+ const { engine: engineInstance, openExplorerUi: openExplorerUiFn, explorerUiEvents } = deps
194
+ const root = engineInstance.RootEntity
195
+
196
+ const sessions = new Map<number, Session>()
197
+ // Keyed by name, so two wrappers around the same component arm one listener, not two.
198
+ const armed = new Map<string, GrowOnlyValueSetComponentDefinition<any>>()
199
+ // Starts at 1 so a minted id never collides with UNCORRELATED.
200
+ let nextRequestId = 1
201
+ let timeoutSystemAdded = false
202
+
203
+ function arm(ch: AnyChannel) {
204
+ const bound = armed.get(ch.name)
205
+ if (bound) {
206
+ if (bound !== ch.component) {
207
+ throw new Error(
208
+ `openExplorerUiAndWait: channel name '${ch.name}' is already bound to a different component. ` +
209
+ `Channel names identify a stream, so they have to be unique within a scene.`
210
+ )
211
+ }
212
+ return
213
+ }
214
+ armed.set(ch.name, ch.component)
215
+ const name = ch.name
216
+ // onChange has no unsubscribe, so one listener per channel lives for the engine's lifetime.
217
+ ch.component.onChange(root, (value: CorrelatedEvent | undefined) => {
218
+ // `undefined` arrives on DELETE_ENTITY. Otherwise `value` is one appended element,
219
+ // not the whole set.
220
+ if (value) deliver(name, value)
221
+ })
222
+ }
223
+
224
+ function attribute(session: Session, name: string, event: CorrelatedEvent): Attribution {
225
+ if (event.requestId === session.requestId) return 'yes'
226
+ if (event.requestId !== UNCORRELATED) return 'no'
227
+ // Only the lifecycle channel carries a second discriminator, `ui`, and it identifies a
228
+ // session only while panels cannot coexist. A second call for the same panel is answered
229
+ // WAS_ALREADY_OPEN, so at most one session per non-concurrent panel is ever live.
230
+ if (session.correlationRequired || name !== explorerUiEvents.name) return 'ambiguous'
231
+ return (event as PBExplorerUiEventsResult).ui === session.ui ? 'yes' : 'no'
232
+ }
233
+
234
+ function deliver(name: string, event: CorrelatedEvent) {
235
+ // Settling deletes from `sessions` mid-iteration, which a Map tolerates. Nothing adds.
236
+ for (const session of sessions.values()) {
237
+ if (!session.watched.has(name)) continue
238
+ const attribution = attribute(session, name, event)
239
+ if (attribution === 'no') continue
240
+ if (attribution === 'ambiguous') {
241
+ failSession(
242
+ session,
243
+ new Error(
244
+ `openExplorerUiAndWait: got a '${name}' event with no request_id, and nothing else identifies ` +
245
+ `the call it belongs to. This explorer does not echo OpenExplorerUiRequest.request_id.`
246
+ )
247
+ )
248
+ continue
249
+ }
250
+ accept(session, name, event)
251
+ }
252
+ }
253
+
254
+ function accept(session: Session, name: string, event: CorrelatedEvent) {
255
+ if (session.collect.has(name)) session.events.push({ channel: name, event })
256
+
257
+ // The close is terminal and wins over `until`, because `matched` would tell the caller
258
+ // that the panel is still on screen.
259
+ if (name === explorerUiEvents.name && caseOf(event) === 'closed') {
260
+ settleSession(session, { $case: 'closed', events: session.events as never[] })
261
+ return
262
+ }
263
+
264
+ const until = session.until
265
+ if (!until || until.channel !== name) return
266
+ if (until.$case === undefined || until.$case === caseOf(event)) {
267
+ settleSession(session, { $case: 'matched', events: session.events as never[] })
268
+ }
269
+ }
270
+
271
+ function settleSession(session: Session, outcome: WaitOutcome<never[]>) {
272
+ if (session.settled) return
273
+ session.settled = true
274
+ sessions.delete(session.requestId)
275
+ session.settle(outcome)
276
+ maybeRemoveTimeoutSystem()
277
+ }
278
+
279
+ function failSession(session: Session, error: Error) {
280
+ if (session.settled) return
281
+ session.settled = true
282
+ sessions.delete(session.requestId)
283
+ session.fail(error)
284
+ maybeRemoveTimeoutSystem()
285
+ }
286
+
287
+ function hasTimedSession(): boolean {
288
+ for (const session of sessions.values()) {
289
+ if (session.timeoutMs !== undefined) return true
290
+ }
291
+ return false
292
+ }
293
+
294
+ function maybeRemoveTimeoutSystem() {
295
+ if (!timeoutSystemAdded || hasTimedSession()) return
296
+ timeoutSystemAdded = false
297
+ engineInstance.removeSystem(EXPLORER_UI_WAIT_TIMEOUT_SYSTEM)
298
+ }
299
+
300
+ function ensureTimeoutSystem() {
301
+ if (timeoutSystemAdded) return
302
+ timeoutSystemAdded = true
303
+ // The scene runtime has no timers, so elapsed time is accumulated from the engine's dt.
304
+ engineInstance.addSystem(
305
+ (dt: number) => {
306
+ for (const session of sessions.values()) {
307
+ if (session.timeoutMs === undefined) continue
308
+ session.elapsedMs += dt * 1000
309
+ if (session.elapsedMs >= session.timeoutMs) {
310
+ settleSession(session, { $case: 'timedOut', events: session.events as never[] })
311
+ }
312
+ }
313
+ maybeRemoveTimeoutSystem()
314
+ },
315
+ TIMEOUT_SYSTEM_PRIORITY,
316
+ EXPLORER_UI_WAIT_TIMEOUT_SYSTEM
317
+ )
318
+ }
319
+
320
+ async function openExplorerUiAndWait(
321
+ request: OpenExplorerUiRequest | Exclude<ExplorerUi, ExplorerUi.EU_ITEM_PURCHASE>,
322
+ options?: WaitOptions<readonly AnyChannel[]>
323
+ ): Promise<WaitOutcome<never[]>> {
324
+ const { ui, itemPurchase } = typeof request === 'number' ? { ui: request, itemPurchase: undefined } : request
325
+ const requestId = nextRequestId++
326
+
327
+ const collect = options?.collect ?? []
328
+ const collectNames = new Set<string>()
329
+ for (const ch of collect) collectNames.add(ch.name)
330
+
331
+ const stop = options?.until ? normalizeStop(options.until) : undefined
332
+ const watched = new Set<string>(collectNames)
333
+ watched.add(explorerUiEvents.name)
334
+ if (stop) watched.add(stop.channel.name)
335
+
336
+ // Armed before the RPC, because events can land while the RPC is in flight. A listener
337
+ // that is already up is what makes replaying the accumulated set unnecessary.
338
+ arm(explorerUiEvents)
339
+ for (const ch of collect) arm(ch)
340
+ if (stop) arm(stop.channel)
341
+
342
+ let settle!: (outcome: WaitOutcome<never[]>) => void
343
+ let fail!: (error: Error) => void
344
+ const outcome = new Promise<WaitOutcome<never[]>>((resolve, reject) => {
345
+ settle = resolve
346
+ fail = reject
347
+ })
348
+
349
+ const session: Session = {
350
+ requestId,
351
+ ui,
352
+ correlationRequired: CONCURRENT_PANELS.includes(ui),
353
+ collect: collectNames,
354
+ watched,
355
+ until: stop ? { channel: stop.channel.name, $case: stop.$case } : undefined,
356
+ events: [],
357
+ timeoutMs: options?.timeoutMs,
358
+ elapsedMs: 0,
359
+ settled: false,
360
+ settle,
361
+ fail
362
+ }
363
+ sessions.set(requestId, session)
364
+ if (session.timeoutMs !== undefined) ensureTimeoutSystem()
365
+
366
+ try {
367
+ const { openResult } = await openExplorerUiFn({ ui, requestId, itemPurchase })
368
+ // This call opened nothing, so no event carrying this request id is coming.
369
+ if (openResult !== OpenExplorerUiResult.OPENED) {
370
+ settleSession(session, { $case: 'notOpened', openResult })
371
+ }
372
+ } catch (error) {
373
+ // Routed through the session rather than rethrown, so that `outcome` is never left
374
+ // pending and unobserved.
375
+ failSession(session, error as Error)
376
+ }
377
+
378
+ return outcome
379
+ }
380
+
381
+ // The implementation takes both argument forms and is blind to `collect`. The cast is what
382
+ // gives callers the two overloads that relate them.
383
+ return { openExplorerUiAndWait: openExplorerUiAndWait as OpenExplorerUiAndWait }
384
+ }
385
+
386
+ const helper = /* @__PURE__ */ create({ engine, openExplorerUi, explorerUiEvents: ExplorerUiEvents })
387
+
388
+ /**
389
+ * Opens an explorer panel and waits for the session it started. The wait ends when the panel
390
+ * closes. See {@link WaitOptions} to collect events, to stop earlier or to bound the wait.
391
+ *
392
+ * Every lifecycle outcome resolves, see {@link WaitOutcome}. The promise rejects only when an
393
+ * event cannot be attributed to a call at all: an explorer too old to echo `request_id`
394
+ * paired with a panel that can coexist with another one.
395
+ *
396
+ * @public
397
+ */
398
+ export const openExplorerUiAndWait: OpenExplorerUiAndWait = helper.openExplorerUiAndWait
@@ -0,0 +1,8 @@
1
+ import { Observable } from './Observable'
2
+
3
+ /**
4
+ * @internal
5
+ * Raw `localeChanged` events drained from the renderer by `pollEvents`.
6
+ * Kept side-effect free so `observables` doesn't load `platform` for every scene.
7
+ */
8
+ export const localeChangedEvents = new Observable<{ locale: string }>()
@@ -12,6 +12,7 @@ import {
12
12
  } from '@dcl/ecs'
13
13
  import { ManyEntityAction, SendBatchResponse, subscribe } from '~system/EngineApi'
14
14
  import players from './players'
15
+ import { localeChangedEvents } from './internal/language-events'
15
16
 
16
17
  /// --- EVENTS ---
17
18
 
@@ -49,6 +50,11 @@ export interface IEvents {
49
50
  message: string
50
51
  }
51
52
 
53
+ /** Triggered when the player switches the client language. `locale` is a BCP-47 tag */
54
+ localeChanged: {
55
+ locale: string
56
+ }
57
+
52
58
  /**
53
59
  * This is triggered once the scene should start.
54
60
  */
@@ -215,6 +221,10 @@ export async function pollEvents(sendBatch: (body: ManyEntityAction) => Promise<
215
221
  onCommsMessage.notifyObservers(data as IEvents['comms'])
216
222
  break
217
223
  }
224
+ case 'localeChanged': {
225
+ localeChangedEvents.notifyObservers(data as IEvents['localeChanged'])
226
+ break
227
+ }
218
228
  }
219
229
  }
220
230
  }
@@ -1,12 +1,49 @@
1
1
  import { getExplorerInformation } from '~system/Runtime'
2
+ import { subscribe } from '~system/EngineApi'
3
+ import { Observable } from '../internal/Observable'
4
+ import { localeChangedEvents } from '../internal/language-events'
2
5
 
3
6
  export type Platform = 'mobile' | 'desktop' | 'web'
4
7
 
5
8
  const VALID_PLATFORMS: Set<string> = new Set<string>(['mobile', 'desktop', 'web'])
6
9
 
10
+ const DEFAULT_LANGUAGE = 'en'
11
+ const BCP47_TAG = /^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/
12
+
7
13
  let platform: Platform | null = null
14
+ let language: string = DEFAULT_LANGUAGE
15
+ let languageSetByEvent = false
16
+
17
+ /**
18
+ * Triggered when the player switches the client language mid-session.
19
+ * Older clients never emit it.
20
+ * @public
21
+ */
22
+ export const onPlayerLanguageChanged = new Observable<{ language: string }>()
23
+
24
+ function normalizeLocale(locale: string | undefined): string {
25
+ const tag = (locale ?? '').trim().replace(/_/g, '-')
26
+ return BCP47_TAG.test(tag) ? tag : DEFAULT_LANGUAGE
27
+ }
28
+
29
+ function setLanguage(next: string) {
30
+ if (next === language) return
31
+ language = next
32
+ onPlayerLanguageChanged.notifyObservers({ language })
33
+ }
34
+
35
+ localeChangedEvents.add((event) => {
36
+ languageSetByEvent = true
37
+ setLanguage(normalizeLocale(event.locale))
38
+ })
39
+ subscribe({ eventId: 'localeChanged' }).catch(console.error)
40
+
8
41
  void getExplorerInformation({})
9
42
  .then((response) => {
43
+ // a mid-session change that arrived first is more recent than this response
44
+ if (!languageSetByEvent) {
45
+ setLanguage(normalizeLocale(response.configurations?.['locale']))
46
+ }
10
47
  const normalized = response.platform.toLowerCase()
11
48
  if (VALID_PLATFORMS.has(normalized)) {
12
49
  platform = normalized as Platform
@@ -33,3 +70,13 @@ export function isDesktop(): boolean {
33
70
  export function isWeb(): boolean {
34
71
  return getPlatform() === 'web'
35
72
  }
73
+
74
+ /**
75
+ * Player's client language as a BCP-47 tag (e.g. `'es'`, `'pt-BR'`).
76
+ * Returns `'en'` until the explorer responds, and on clients that don't report a locale.
77
+ * Use `onPlayerLanguageChanged` to react to mid-session changes.
78
+ * @public
79
+ */
80
+ export function getPlayerLanguage(): string {
81
+ return language
82
+ }
@@ -19,7 +19,6 @@ type GetPlayerDataRes = {
19
19
  avatar?: PBAvatarBase
20
20
  wearables: PBAvatarEquippedData['wearableUrns']
21
21
  emotes: PBAvatarEquippedData['emoteUrns']
22
- forceRender: PBAvatarEquippedData['forceRender']
23
22
  position: TransformType['position'] | undefined
24
23
  }
25
24
 
@@ -93,9 +92,11 @@ export function definePlayerHelper(engine: IEngine) {
93
92
  isGuest: !!playerData?.isGuest,
94
93
  userId: playerData?.address ?? '',
95
94
  avatar: avatarData ?? undefined,
96
- wearables: wearablesData?.wearableUrns ?? [],
97
- emotes: wearablesData?.emoteUrns ?? [],
98
- forceRender: wearablesData?.forceRender ?? [],
95
+ // Component reads come back deeply readonly, so these are copied rather than
96
+ // aliased: the public Player type promises plain mutable arrays, and handing
97
+ // out the component's own frozen array would throw on any caller that writes.
98
+ wearables: [...(wearablesData?.wearableUrns ?? [])],
99
+ emotes: [...(wearablesData?.emoteUrns ?? [])],
99
100
  position: Transform.getOrNull(userEntity)?.position
100
101
  }
101
102
  }