diff --git a/code/_experiments.dm b/code/_experiments.dm index ef2240406ed2..c7fdad2f7887 100644 --- a/code/_experiments.dm +++ b/code/_experiments.dm @@ -3,11 +3,8 @@ // Any flag you see here can be flipped with the `-D` CLI argument. // For example, if you want to enable EXPERIMENT_MY_COOL_FEATURE, compile with -DEXPERIMENT_MY_COOL_FEATURE -// EXPERIMENT_515_QDEL_HARD_REFERENCE -// - Hold a hard reference for qdeleted items, and check ref_count, rather than using refs. Requires 515+. - -// EXPERIMENT_515_DONT_CACHE_REF -// - Avoids `text_ref` caching, aided by improvements to ref() speed in 515. +// EXPERIMENT_MY_COOL_FEATURE +// - Does something really cool, just so neat, absolutely banging, gaming and chill #if DM_VERSION < 515 @@ -20,6 +17,6 @@ #define EXPERIMENT_MY_COOL_FEATURE #endif -#if DM_VERSION >= 516 - #error "Remove all 515 experiments" +#if DM_VERSION >= 517 + #error "Remove all 516 experiments" #endif diff --git a/code/controllers/subsystem/statpanel.dm b/code/controllers/subsystem/statpanel.dm index 08a26d9a79cf..3dba3b00faab 100644 --- a/code/controllers/subsystem/statpanel.dm +++ b/code/controllers/subsystem/statpanel.dm @@ -99,11 +99,12 @@ SUBSYSTEM_DEF(statpanels) return /datum/controller/subsystem/statpanels/proc/set_status_tab(client/target) + var/static/list/beta_notice = list("", "You are on the BYOND 516 beta, various UIs and such may be broken!", "Please report issues, and switch back to BYOND 515 if things are causing too many issues for you.") if(!global_data)//statbrowser hasnt fired yet and we were called from immediate_send_stat_data() return target.stat_panel.send_message("update_stat", list( - "global_data" = global_data, + "global_data" = (target.byond_version < 516) ? global_data : (global_data + beta_notice), "ping_str" = "Ping: [round(target.lastping, 1)]ms (Average: [round(target.avgping, 1)]ms)", "other_str" = target.mob?.get_status_tab_items(), )) diff --git a/code/modules/admin/holder2.dm b/code/modules/admin/holder2.dm index 57179bd89bfc..94fde875336c 100644 --- a/code/modules/admin/holder2.dm +++ b/code/modules/admin/holder2.dm @@ -407,7 +407,7 @@ GLOBAL_PROTECT(href_token) /datum/admins/proc/try_give_devtools() if(!(rank_flags() & R_DEBUG) || owner.byond_version < 516) return - winset(owner, null, "browser-options=byondstorage,find,devtools") + winset(owner, null, "browser-options=byondstorage,find,refresh,devtools") /datum/admins/proc/try_give_profiling() if (CONFIG_GET(flag/forbid_admin_profiling)) diff --git a/code/modules/asset_cache/asset_cache_client.dm b/code/modules/asset_cache/asset_cache_client.dm index 3cff8cb41f3e..cf786de09e8b 100644 --- a/code/modules/asset_cache/asset_cache_client.dm +++ b/code/modules/asset_cache/asset_cache_client.dm @@ -36,7 +36,10 @@ var/job = ++last_asset_job var/t = 0 var/timeout_time = timeout - src << browse({""}, "window=asset_cache_browser&file=asset_cache_send_verify.htm") + if(byond_version < 516) + src << browse({""}, "window=asset_cache_browser&file=asset_cache_send_verify.htm") + else + src << browse({""}, "window=asset_cache_browser&file=asset_cache_send_verify.htm") while(!completed_asset_jobs["[job]"] && t < timeout_time) // Reception is handled in Topic() stoplag(1) // Lock up the caller until this is received. diff --git a/code/modules/asset_cache/validate_assets.html b/code/modules/asset_cache/validate_assets.html index 78bcbb92a1ab..70fdca8a9d77 100644 --- a/code/modules/asset_cache/validate_assets.html +++ b/code/modules/asset_cache/validate_assets.html @@ -8,7 +8,7 @@ //this is used over window.location because window.location has a character limit in IE. function sendbyond(text) { var xhr = new XMLHttpRequest(); - xhr.open('GET', '?'+text, true); + xhr.open('GET', 'byond://?' + text, true); xhr.send(null); } var xhr = new XMLHttpRequest(); @@ -24,6 +24,6 @@ }; xhr.send(null); - + diff --git a/code/modules/client/client_procs.dm b/code/modules/client/client_procs.dm index be80860d23e1..f5afc598e84d 100644 --- a/code/modules/client/client_procs.dm +++ b/code/modules/client/client_procs.dm @@ -225,9 +225,6 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( /////////// /client/New(TopicData) - if(byond_version >= 516) // Enable 516 compat browser storage mechanisms - winset(src, "", "browser-options=byondstorage,find") - var/tdata = TopicData //save this for later use TopicData = null //Prevent calls to client.Topic from connect @@ -237,6 +234,9 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( GLOB.clients += src GLOB.directory[ckey] = src + if(byond_version >= 516) + winset(src, null, list("browser-options" = "find,refresh,byondstorage")) + // Instantiate stat panel stat_panel = new(src, "statbrowser") stat_panel.subscribe(src, PROC_REF(on_stat_panel_message)) diff --git a/code/modules/tgui_panel/external.dm b/code/modules/tgui_panel/external.dm index 692244a6e80d..e48ddeb22390 100644 --- a/code/modules/tgui_panel/external.dm +++ b/code/modules/tgui_panel/external.dm @@ -19,27 +19,19 @@ // Failed to fix, using tgalert as fallback action = tgalert(src, "Did that work?", "", "Yes", "No, switch to old ui") if (action == "No, switch to old ui") - winset(src, "output", "on-show=&is-disabled=0&is-visible=1") - winset(src, "browseroutput", "is-disabled=1;is-visible=0") + winset(src, "legacy_output_selector", "left=output_legacy") log_tgui(src, "Failed to fix.", context = "verb/fix_tgui_panel") /client/proc/nuke_chat() // Catch all solution (kick the whole thing in the pants) - winset(src, "output", "on-show=&is-disabled=0&is-visible=1") - winset(src, "browseroutput", "is-disabled=1;is-visible=0") + winset(src, "legacy_output_selector", "left=output_legacy") if(!tgui_panel || !istype(tgui_panel)) log_tgui(src, "tgui_panel datum is missing", context = "verb/fix_tgui_panel") tgui_panel = new(src) tgui_panel.initialize(force = TRUE) // Force show the panel to see if there are any errors - winset(src, "output", "is-disabled=1&is-visible=0") - winset(src, "browseroutput", "is-disabled=0;is-visible=1") - if(byond_version >= 516) - var/list/options = list("byondstorage", "find") - if(check_rights_for(src, R_DEBUG)) - options += "devtools" - winset(src, null, "browser-options=[options.Join(",")]") + winset(src, "legacy_output_selector", "left=output_browser") /client/verb/refresh_tgui() set name = "Refresh TGUI" diff --git a/interface/skin.dmf b/interface/skin.dmf index 9a4704e95120..d46f5c55b184 100644 --- a/interface/skin.dmf +++ b/interface/skin.dmf @@ -226,7 +226,7 @@ window "infowindow" window "outputwindow" elem "outputwindow" type = MAIN - pos = 281,0 + pos = 0,0 size = 640x480 anchor1 = -1,-1 anchor2 = -1,-1 @@ -285,15 +285,26 @@ window "outputwindow" anchor2 = -1,-1 is-visible = false saved-params = "" - elem "browseroutput" - type = BROWSER + elem "legacy_output_selector" + type = CHILD pos = 0,0 size = 640x456 anchor1 = 0,0 anchor2 = 100,100 - is-visible = false - is-disabled = true - saved-params = "" + saved-params = "splitter" + left = "output_legacy" + is-vert = false + +window "output_legacy" + elem "output_legacy" + type = MAIN + pos = 0,0 + size = 640x456 + anchor1 = -1,-1 + anchor2 = -1,-1 + background-color = none + saved-params = "pos;size;is-minimized;is-maximized" + is-pane = true elem "output" type = OUTPUT pos = 0,0 @@ -301,6 +312,25 @@ window "outputwindow" anchor1 = 0,0 anchor2 = 100,100 is-default = true + saved-params = "max-lines" + +window "output_browser" + elem "output_browser" + type = MAIN + pos = 0,0 + size = 640x456 + anchor1 = -1,-1 + anchor2 = -1,-1 + background-color = none + saved-params = "pos;size;is-minimized;is-maximized" + is-pane = true + elem "browseroutput" + type = BROWSER + pos = 0,0 + size = 640x456 + anchor1 = 0,0 + anchor2 = 100,100 + background-color = none saved-params = "" window "popupwindow" diff --git a/tgui/global.d.ts b/tgui/global.d.ts index b1697d5fcdbc..b64bd4006dbe 100644 --- a/tgui/global.d.ts +++ b/tgui/global.d.ts @@ -51,6 +51,11 @@ type ByondType = { */ TRIDENT: number | null; + /** + * Version of Blink engine of WebView2. Null if N/A. + */ + BLINK: number | null; + /** * True if browser is IE8 or lower. */ @@ -162,6 +167,11 @@ type ByondType = { */ parseJson(text: string): any; + /** + * Downloads a blob, platform-agnostic + */ + saveBlob(blob: Blob, filename: string, ext: string): void; + /** * Sends a message to `/datum/tgui_window` which hosts this window instance. */ @@ -205,4 +215,13 @@ interface Window { Byond: ByondType; __store__: Store; __augmentStack__: (store: Store) => StackAugmentor; + + // IE IndexedDB stuff. + msIndexedDB: IDBFactory; + msIDBTransaction: IDBTransaction; + + // 516 byondstorage API. + hubStorage: Storage; + domainStorage: Storage; + serverStorage: Storage; } diff --git a/tgui/packages/common/keys.ts b/tgui/packages/common/keys.ts index 61b79992b486..abbd6b49bb70 100644 --- a/tgui/packages/common/keys.ts +++ b/tgui/packages/common/keys.ts @@ -25,7 +25,8 @@ export enum KEY { Down = 'Down', End = 'End', Enter = 'Enter', - Escape = 'Esc', + Esc = 'Esc', + Escape = 'Escape', Home = 'Home', Insert = 'Insert', Left = 'Left', @@ -37,3 +38,18 @@ export enum KEY { Tab = 'Tab', Up = 'Up', } + +/** + * ### isEscape + * + * Checks if the user has hit the 'ESC' key on their keyboard. + * There's a weirdness in BYOND where this could be either the string + * 'Escape' or 'Esc' depending on the browser. This function handles + * both cases. + * + * @param key - the key to check, typically from event.key + * @returns true if key is Escape or Esc, false otherwise + */ +export const isEscape = (key: string): boolean => { + return key === KEY.Esc || key === KEY.Escape; +}; diff --git a/tgui/packages/common/storage.js b/tgui/packages/common/storage.ts similarity index 62% rename from tgui/packages/common/storage.js rename to tgui/packages/common/storage.ts index 9a47ecc2b54c..b2564acf36dc 100644 --- a/tgui/packages/common/storage.js +++ b/tgui/packages/common/storage.ts @@ -10,6 +10,11 @@ export const IMPL_MEMORY = 0; export const IMPL_HUB_STORAGE = 1; export const IMPL_INDEXED_DB = 2; +type StorageImplementation = + | typeof IMPL_MEMORY + | typeof IMPL_HUB_STORAGE + | typeof IMPL_INDEXED_DB; + const INDEXED_DB_VERSION = 1; const INDEXED_DB_NAME = 'tgui'; const INDEXED_DB_STORE_NAME = 'storage-v1'; @@ -17,7 +22,15 @@ const INDEXED_DB_STORE_NAME = 'storage-v1'; const READ_ONLY = 'readonly'; const READ_WRITE = 'readwrite'; -const testGeneric = (testFn) => () => { +type StorageBackend = { + impl: StorageImplementation; + get(key: string): Promise; + set(key: string, value: any): Promise; + remove(key: string): Promise; + clear(): Promise; +}; + +const testGeneric = (testFn: () => boolean) => (): boolean => { try { return Boolean(testFn()); } catch { @@ -26,69 +39,76 @@ const testGeneric = (testFn) => () => { }; const testHubStorage = testGeneric( - () => window.hubStorage && window.hubStorage.getItem, + () => window.hubStorage && !!window.hubStorage.getItem, ); // TODO: Remove with 516 // prettier-ignore const testIndexedDb = testGeneric(() => ( (window.indexedDB || window.msIndexedDB) - && (window.IDBTransaction || window.msIDBTransaction) + && !!(window.IDBTransaction || window.msIDBTransaction) )); -class MemoryBackend { +class MemoryBackend implements StorageBackend { + private store: Record; + public impl: StorageImplementation; + constructor() { this.impl = IMPL_MEMORY; this.store = {}; } - async get(key) { + async get(key: string): Promise { return this.store[key]; } - async set(key, value) { + async set(key: string, value: any): Promise { this.store[key] = value; } - async remove(key) { + async remove(key: string): Promise { this.store[key] = undefined; } - async clear() { + async clear(): Promise { this.store = {}; } } -class HubStorageBackend { +class HubStorageBackend implements StorageBackend { + public impl: StorageImplementation; + constructor() { this.impl = IMPL_HUB_STORAGE; } - async get(key) { + async get(key: string): Promise { const value = await window.hubStorage.getItem(key); if (typeof value === 'string') { return JSON.parse(value); } + return undefined; } - set(key, value) { + async set(key: string, value: any): Promise { window.hubStorage.setItem(key, JSON.stringify(value)); } - remove(key) { + async remove(key: string): Promise { window.hubStorage.removeItem(key); } - clear() { + async clear(): Promise { window.hubStorage.clear(); } } -class IndexedDbBackend { - // TODO: Remove with 516 +class IndexedDbBackend implements StorageBackend { + public impl: StorageImplementation; + public dbPromise: Promise; + constructor() { this.impl = IMPL_INDEXED_DB; - /** @type {Promise} */ this.dbPromise = new Promise((resolve, reject) => { const indexedDB = window.indexedDB || window.msIndexedDB; const req = indexedDB.open(INDEXED_DB_NAME, INDEXED_DB_VERSION); @@ -96,7 +116,12 @@ class IndexedDbBackend { try { req.result.createObjectStore(INDEXED_DB_STORE_NAME); } catch (err) { - reject(new Error('Failed to upgrade IDB: ' + req.error)); + reject( + new Error( + 'Failed to upgrade IDB: ' + + (err instanceof Error ? err.message : String(err)), + ), + ); } }; req.onsuccess = () => resolve(req.result); @@ -106,14 +131,14 @@ class IndexedDbBackend { }); } - async getStore(mode) { - // prettier-ignore - return this.dbPromise.then((db) => db + private async getStore(mode: IDBTransactionMode): Promise { + const db = await this.dbPromise; + return db .transaction(INDEXED_DB_STORE_NAME, mode) - .objectStore(INDEXED_DB_STORE_NAME)); + .objectStore(INDEXED_DB_STORE_NAME); } - async get(key) { + async get(key: string): Promise { const store = await this.getStore(READ_ONLY); return new Promise((resolve, reject) => { const req = store.get(key); @@ -122,19 +147,19 @@ class IndexedDbBackend { }); } - async set(key, value) { + async set(key: string, value: any): Promise { // NOTE: We deliberately make this operation transactionless const store = await this.getStore(READ_WRITE); store.put(value, key); } - async remove(key) { + async remove(key: string): Promise { // NOTE: We deliberately make this operation transactionless const store = await this.getStore(READ_WRITE); store.delete(key); } - async clear() { + async clear(): Promise { // NOTE: We deliberately make this operation transactionless const store = await this.getStore(READ_WRITE); store.clear(); @@ -145,7 +170,10 @@ class IndexedDbBackend { * Web Storage Proxy object, which selects the best backend available * depending on the environment. */ -class StorageProxy { +class StorageProxy implements StorageBackend { + private backendPromise: Promise; + public impl: StorageImplementation = IMPL_MEMORY; + constructor() { this.backendPromise = (async () => { if (!Byond.TRIDENT && testHubStorage()) { @@ -166,22 +194,22 @@ class StorageProxy { })(); } - async get(key) { + async get(key: string): Promise { const backend = await this.backendPromise; return backend.get(key); } - async set(key, value) { + async set(key: string, value: any): Promise { const backend = await this.backendPromise; return backend.set(key, value); } - async remove(key) { + async remove(key: string): Promise { const backend = await this.backendPromise; return backend.remove(key); } - async clear() { + async clear(): Promise { const backend = await this.backendPromise; return backend.clear(); } diff --git a/tgui/packages/tgui-panel/chat/renderer.jsx b/tgui/packages/tgui-panel/chat/renderer.jsx index fa04d94f38c6..e19c7471888c 100644 --- a/tgui/packages/tgui-panel/chat/renderer.jsx +++ b/tgui/packages/tgui-panel/chat/renderer.jsx @@ -80,7 +80,7 @@ const createReconnectedNode = () => { const handleImageError = (e) => { setTimeout(() => { /** @type {HTMLImageElement} */ - const node = Byond.BLINK !== null ? e : e.target; + const node = e.target; const attempts = parseInt(node.getAttribute('data-reload-n'), 10) || 0; if (attempts >= IMAGE_RETRY_LIMIT) { logger.error(`failed to load an image after ${attempts} attempts`); @@ -612,13 +612,13 @@ class ChatRenderer { + '\n' + '\n'; // Create and send a nice blob - const blob = new Blob([pageHtml]); + const blob = new Blob([pageHtml], { type: 'text/plain' }); const timestamp = new Date() .toISOString() .substring(0, 19) .replace(/[-:]/g, '') .replace('T', '-'); - window.navigator.msSaveBlob(blob, `ss13-chatlog-${timestamp}.html`); + Byond.saveBlob(blob, `ss13-chatlog-${timestamp}.html`, '.html'); } } diff --git a/tgui/packages/tgui-panel/index.jsx b/tgui/packages/tgui-panel/index.jsx index 95559291ee2d..9f43a3091e25 100644 --- a/tgui/packages/tgui-panel/index.jsx +++ b/tgui/packages/tgui-panel/index.jsx @@ -78,14 +78,8 @@ const setupApp = () => { Byond.subscribe((type, payload) => store.dispatch({ type, payload })); // Unhide the panel - Byond.winset('output', { - 'is-visible': false, - }); - Byond.winset('browseroutput', { - 'is-visible': true, - 'is-disabled': false, - pos: '0x0', - size: '0x0', + Byond.winset('legacy_output_selector', { + left: 'output_browser', }); // Resize the panel to match the non-browser output diff --git a/tgui/packages/tgui-say/TguiSay.tsx b/tgui/packages/tgui-say/TguiSay.tsx index 0a1c9c7e4efa..aee0d12a5cf5 100644 --- a/tgui/packages/tgui-say/TguiSay.tsx +++ b/tgui/packages/tgui-say/TguiSay.tsx @@ -6,7 +6,7 @@ import { byondMessages } from './timers'; import { dragStartHandler } from 'tgui/drag'; import { windowOpen, windowClose, windowSet } from './helpers'; import { BooleanLike } from 'common/react'; -import { KEY } from 'common/keys'; +import { isEscape, KEY } from 'common/keys'; type ByondOpen = { channel: Channel; @@ -252,9 +252,10 @@ export class TguiSay extends Component<{}, State> { this.handleIncrementChannel(); break; - case KEY.Escape: - this.handleClose(); - break; + default: + if (isEscape(event.key)) { + this.handleClose(); + } } } diff --git a/tgui/packages/tgui/interfaces/KeyComboModal.tsx b/tgui/packages/tgui/interfaces/KeyComboModal.tsx index 6d618fe01214..515f7162fb3f 100644 --- a/tgui/packages/tgui/interfaces/KeyComboModal.tsx +++ b/tgui/packages/tgui/interfaces/KeyComboModal.tsx @@ -1,4 +1,4 @@ -import { KEY } from 'common/keys'; +import { KEY, isEscape } from 'common/keys'; import { useBackend, useLocalState } from '../backend'; import { Autofocus, Box, Button, Section, Stack } from '../components'; import { Window } from '../layouts'; @@ -18,7 +18,7 @@ const isStandardKey = (event: KeyboardEvent): boolean => { event.key !== KEY.Alt && event.key !== KEY.Control && event.key !== KEY.Shift && - event.key !== KEY.Escape + !isEscape(event.key) ); }; @@ -93,7 +93,7 @@ export const KeyComboModal = (props) => { if (event.key === KEY.Enter) { act('submit', { entry: input }); } - if (event.key === KEY.Escape) { + if (isEscape(event.key)) { act('cancel'); } return; @@ -105,7 +105,7 @@ export const KeyComboModal = (props) => { setValue(formatKeyboardEvent(event)); setBinding(false); return; - } else if (event.key === KEY.Escape) { + } else if (isEscape(event.key)) { setValue(init_value); setBinding(false); return; diff --git a/tgui/public/tgui.html b/tgui/public/tgui.html index 7dded63eee27..8937a9443bc4 100644 --- a/tgui/public/tgui.html +++ b/tgui/public/tgui.html @@ -1,673 +1,730 @@ + - - - - - - - - - + + - - + + + + - - - - - - - - -
- - -
- - - A fatal exception has occurred at 002B:C562F1B7 in TGUI. The current - application will be terminated. Please remain calm. Get to the nearest - NTNet workstation and send the copy of the following stack trace to: - https://github.com/Monkestation/Monkestation2.0/. Thank you for your cooperation. - -
- -
- -