From ba5019bd9d68690b05d8612e5aef63fbd90ceac5 Mon Sep 17 00:00:00 2001 From: CHOMPStation2StaffMirrorBot <94713762+CHOMPStation2StaffMirrorBot@users.noreply.github.com> Date: Thu, 4 Dec 2025 17:26:10 -0700 Subject: [PATCH] [MIRROR] Port of the iframe storage for settings (#12040) Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com> --- .github/workflows/generate_client_storage.yml | 33 ++++ .../configuration/entries/resources.dm | 4 + code/controllers/subsystems/tgui.dm | 19 +++ code/modules/client/client procs.dm | 1 + config/example/resources.txt | 9 ++ tgui/global.d.ts | 5 + tgui/packages/common/storage.ts | 145 +++++++++++++++++- tgui/packages/tgui-panel/chat/middleware.ts | 1 + tgui/packages/tgui-setup/helpers.js | 36 ++--- tgui/public/helpers.min.js | 2 +- tgui/public/iframe.html | 102 ++++++++++++ tgui/public/tgui.html | 1 + 12 files changed, 326 insertions(+), 32 deletions(-) create mode 100644 .github/workflows/generate_client_storage.yml create mode 100644 tgui/public/iframe.html diff --git a/.github/workflows/generate_client_storage.yml b/.github/workflows/generate_client_storage.yml new file mode 100644 index 0000000000..f03bbcc405 --- /dev/null +++ b/.github/workflows/generate_client_storage.yml @@ -0,0 +1,33 @@ +name: "Generate Client Storage" +on: + push: + branches: + - master + paths: + - tgui/public/* + +jobs: + dispatch_repo: + if: ( !contains(github.event.head_commit.message, '[ci skip]') ) + name: Repository Dispatch + runs-on: ubuntu-latest + steps: + - name: Generate App Token + id: app-token-generation + uses: actions/create-github-app-token@v2 + if: env.APP_PRIVATE_KEY != '' && env.APP_ID != '' + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + owner: vorestation + env: + APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }} + APP_ID: ${{ secrets.APP_ID }} + + - name: Send Repository Dispatch + if: success() + uses: peter-evans/repository-dispatch@v4 + with: + token: ${{ steps.app-token-generation.outputs.token }} + repository: vorestation/byond-client-storage + event-type: on_master_push diff --git a/code/controllers/configuration/entries/resources.dm b/code/controllers/configuration/entries/resources.dm index 8623f4d75c..d20f259303 100644 --- a/code/controllers/configuration/entries/resources.dm +++ b/code/controllers/configuration/entries/resources.dm @@ -11,3 +11,7 @@ /datum/config_entry/flag/smart_cache_assets /datum/config_entry/flag/save_spritesheets + +/datum/config_entry/string/storage_cdn_iframe + protection = CONFIG_ENTRY_LOCKED + default = "https://vorestation.github.io/byond-client-storage/iframe.html" diff --git a/code/controllers/subsystems/tgui.dm b/code/controllers/subsystems/tgui.dm index 2872eb2824..fbf9cd0ba8 100644 --- a/code/controllers/subsystems/tgui.dm +++ b/code/controllers/subsystems/tgui.dm @@ -40,6 +40,25 @@ SUBSYSTEM_DEF(tgui) basehtml = replacetextEx(basehtml, "", "Nanotrasen (c) 2284-[text2num(time2text(world.realtime,"YYYY")) + STATION_YEAR_OFFSET]") +/datum/controller/subsystem/tgui/OnConfigLoad() + var/storage_iframe = CONFIG_GET(string/storage_cdn_iframe) + + if(storage_iframe && storage_iframe != /datum/config_entry/string/storage_cdn_iframe::default) + basehtml = replacetext(basehtml, "\[tgui:storagecdn\]", storage_iframe) + return + + if(CONFIG_GET(string/asset_transport) == "webroot") + var/datum/asset_transport/webroot/webroot = SSassets.transport + + var/datum/asset_cache_item/item = webroot.register_asset("iframe.html", file("tgui/public/iframe.html")) + basehtml = replacetext(basehtml, "\[tgui:storagecdn\]", webroot.get_asset_url("iframe.html", item)) + return + + if(!storage_iframe) + return + + basehtml = replacetext(basehtml, "\[tgui:storagecdn\]", storage_iframe) + /datum/controller/subsystem/tgui/Shutdown() close_all_uis() diff --git a/code/modules/client/client procs.dm b/code/modules/client/client procs.dm index bc031430d1..d488789d6d 100644 --- a/code/modules/client/client procs.dm +++ b/code/modules/client/client procs.dm @@ -269,6 +269,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list( if (CONFIG_GET(flag/chatlog_database_backend)) chatlog_token = vchatlog_generate_token(ckey, GLOB.round_id) + winset(src, null, list("browser-options" = "find,refresh")) // Instantiate stat panel stat_panel = new(src, "statbrowser") stat_panel.subscribe(src, PROC_REF(on_stat_panel_message)) diff --git a/config/example/resources.txt b/config/example/resources.txt index 338cbcd408..084c440e39 100644 --- a/config/example/resources.txt +++ b/config/example/resources.txt @@ -42,3 +42,12 @@ SMART_CACHE_ASSETS ## Useful for developers to debug potential spritesheet issues to determine where the issue is cropping up (either in DM-side sprite generation or in the TGUI-side display of said spritesheet). ## Will only seek to waste disk space if ran on production. #SAVE_SPRITESHEETS + +# If configured, this allows server operators to define the persistent origin used for clientside +# storage. This must host the same file as available in tgui/public/iframe.html. This is also hosted +# on the GitHub Pages site for the /tg/station repository, so does not need to be configured. +# If multiple servers use the same domain name, clientside features such as message saving +# and chat tabs will be persistent across both. +# If this setting is not configured, but the webroot CDN is, that will be used instead of GitHub Pages. +# If this setting is mpty, and the webroot CDN is disabled, byondstorage will be used. +# STORAGE_CDN_IFRAME https://vorestation.github.io/byond-client-storage/iframe.html diff --git a/tgui/global.d.ts b/tgui/global.d.ts index cfc7920e82..0579f0d58b 100644 --- a/tgui/global.d.ts +++ b/tgui/global.d.ts @@ -63,6 +63,11 @@ type ByondType = { */ strictMode: boolean; + /** + * The external URL for the IndexedDB IFrame to use as the origin + */ + storageCdn: string; + /** * Makes a BYOND call. * diff --git a/tgui/packages/common/storage.ts b/tgui/packages/common/storage.ts index 3096d7e47a..c842a4c732 100644 --- a/tgui/packages/common/storage.ts +++ b/tgui/packages/common/storage.ts @@ -6,12 +6,13 @@ * @license MIT */ -export const IMPL_MEMORY = 0; export const IMPL_HUB_STORAGE = 1; - -type StorageImplementation = typeof IMPL_MEMORY | typeof IMPL_HUB_STORAGE; +export const IMPL_IFRAME_INDEXED_DB = 2; const KEY_NAME = 'chomp'; // CHOMPEdit - CHOMPStation Localstore +type StorageImplementation = + | typeof IMPL_HUB_STORAGE + | typeof IMPL_IFRAME_INDEXED_DB; type StorageBackend = { impl: StorageImplementation; @@ -61,28 +62,160 @@ class HubStorageBackend implements StorageBackend { } } +class IFrameIndexedDbBackend implements StorageBackend { + public impl: StorageImplementation; + + private documentElement: HTMLIFrameElement; + private iframeWindow: Window; + + constructor() { + this.impl = IMPL_IFRAME_INDEXED_DB; + } + + async ready(): Promise { + const iframe = document.createElement('iframe'); + iframe.style.display = 'none'; + iframe.src = Byond.storageCdn; + + const completePromise: Promise = new Promise((resolve) => { + fetch(Byond.storageCdn, { method: 'HEAD' }) + .then((response) => { + if (response.status !== 200) { + resolve(false); + } + }) + .catch(() => { + resolve(false); + }); + + window.addEventListener('message', (message) => { + if (message.data === 'ready') { + resolve(true); + } + }); + }); + + this.documentElement = document.body.appendChild(iframe); + if (!this.documentElement.contentWindow) { + return new Promise((res) => res(false)); + } + + this.iframeWindow = this.documentElement.contentWindow; + + return completePromise; + } + + async get(key: string): Promise { + const promise = new Promise((resolve) => { + window.addEventListener('message', (message) => { + if (message.data.key && message.data.key === key) { + resolve(message.data.value); + } + }); + }); + + this.iframeWindow.postMessage( + { type: 'get', key: `${KEY_NAME}-${key}` }, + '*', + ); + return promise; + } + + async set(key: string, value: any): Promise { + this.iframeWindow.postMessage( + { type: 'set', key: `${KEY_NAME}-${key}`, value: value }, + '*', + ); + } + + async remove(key: string): Promise { + this.iframeWindow.postMessage( + { type: 'remove', key: `${KEY_NAME}-${key}` }, + '*', + ); + } + + async clear(): Promise { + this.iframeWindow.postMessage({ type: 'clear' }, '*'); + } + + async destroy(): Promise { + document.body.removeChild(this.documentElement); + } +} + /** * Web Storage Proxy object, which selects the best backend available * depending on the environment. */ class StorageProxy implements StorageBackend { private backendPromise: Promise; - public impl: StorageImplementation = IMPL_MEMORY; + public impl: StorageImplementation = IMPL_IFRAME_INDEXED_DB; constructor() { this.backendPromise = (async () => { + // If we have not enabled byondstorage yet, we need to check + // if we can use the IFrame, or if we need to enable byondstorage if (!testHubStorage()) { + // If we have an IFrame URL we can use, and we haven't already enabled + // byondstorage, we should use the IFrame backend + if (Byond.storageCdn) { + const iframe = new IFrameIndexedDbBackend(); + + if ((await iframe.ready()) === true) { + if (await iframe.get('byondstorage-migrated')) return iframe; + + Byond.winset(null, 'browser-options', '+byondstorage'); + + await new Promise((resolve) => { + document.addEventListener('byondstorageupdated', async () => { + setTimeout(() => { + const hub = new HubStorageBackend(); + + // Migrate these existing settings from byondstorage to the IFrame + for (const setting of [ + 'panel-settings', + 'chat-state', + 'chat-messages', + ]) { + hub + .get(setting) + .then((settings) => iframe.set(setting, settings)); + } + + iframe.set('byondstorage-migrated', true); + Byond.winset(null, 'browser-options', '-byondstorage'); + + resolve(); + }, 1); + }); + }); + + return iframe; + } + + iframe.destroy(); + } + + // IFrame hasn't worked out for us, we'll need to enable byondstorage + Byond.winset(null, 'browser-options', '+byondstorage'); + return new Promise((resolve) => { const listener = () => { document.removeEventListener('byondstorageupdated', listener); - resolve(new HubStorageBackend()); + + // This event is emitted *before* byondstorage is actually created + // so we have to wait a little bit before we can use it + setTimeout(() => resolve(new HubStorageBackend()), 1); }; document.addEventListener('byondstorageupdated', listener); }); } + + // byondstorage is already enabled, we can use it straight away return new HubStorageBackend(); - })() as Promise; + })(); } async get(key: string): Promise { diff --git a/tgui/packages/tgui-panel/chat/middleware.ts b/tgui/packages/tgui-panel/chat/middleware.ts index 5d7f627ee8..2293ebf65a 100644 --- a/tgui/packages/tgui-panel/chat/middleware.ts +++ b/tgui/packages/tgui-panel/chat/middleware.ts @@ -334,6 +334,7 @@ export const chatMiddleware = (store) => { next(action); const page = selectCurrentChatPage(store.getState()); chatRenderer.changePage(page); + needsUpdate = true; return; } if (type === rebuildChat.type) { diff --git a/tgui/packages/tgui-setup/helpers.js b/tgui/packages/tgui-setup/helpers.js index d3eb103c24..e964edf6b2 100644 --- a/tgui/packages/tgui-setup/helpers.js +++ b/tgui/packages/tgui-setup/helpers.js @@ -29,6 +29,7 @@ // Expose inlined metadata Byond.windowId = parseMetaTag('tgui:windowId'); + Byond.storageCdn = parseMetaTag('tgui:storagecdn'); // Backwards compatibility window.__windowId__ = Byond.windowId; @@ -397,32 +398,17 @@ ], }; - try { - window - .showSaveFilePicker(opts) - .then((fileHandle) => { - fileHandle - .createWritable() - .then((writeableFileHandle) => { - writeableFileHandle - .write(blob) - .then(() => { - writeableFileHandle.close(); - }) - .catch((e) => { - console.error(e); - }); - }) - .catch((e) => { - console.error(e); - }); - }) - .catch((e) => { - console.error(e); + window + .showSaveFilePicker(opts) + .then(function (file) { + return file.createWritable(); + }) + .then(function (file) { + return file.write(blob).then(function () { + return file.close(); }); - } catch (e) { - console.error(e); - } + }) + .catch(function () {}); } }; diff --git a/tgui/public/helpers.min.js b/tgui/public/helpers.min.js index b58673d0b9..0e910db511 100644 --- a/tgui/public/helpers.min.js +++ b/tgui/public/helpers.min.js @@ -1 +1 @@ -(function(){let hasOwn=Object.prototype.hasOwnProperty;let assign=function(target){for(let i=1;i0){url+="&"}let value=params[key];if(value===null||value===undefined){value=""}url+=encodeURIComponent(key)+"="+encodeURIComponent(value)}}}if(window.cef_to_byond){cef_to_byond("byond://"+url);return}if(url.length<2048){location.href="byond://"+url;return}let xhr=new XMLHttpRequest;xhr.open("GET",url);xhr.send()};Byond.callAsync=function(path,params){if(!window.Promise){throw new Error("Async calls require API level of ES2015 or later.")}let index=Byond.__callbacks__.length;let promise=new window.Promise((resolve=>{Byond.__callbacks__.push(resolve)}));Byond.call(path,assign({},params,{callback:"Byond.__callbacks__["+index+"]"}));return promise};Byond.topic=function(params){return Byond.call("",params)};Byond.command=function(command){return Byond.call("winset",{command:command})};Byond.winget=function(id,propName){if(id===null){id=""}let isArray=propName instanceof Array;let isSpecific=propName&&propName!=="*"&&!isArray;let promise=Byond.callAsync("winget",{id:id,property:isArray&&propName.join(",")||propName||"*"});if(isSpecific){promise=promise.then((props=>props[propName]))}return promise};Byond.winset=function(id,propName,propValue){if(id===null){id=""}else if(typeof id==="object"){return Byond.call("winset",id)}let props={};if(typeof propName==="string"){props[propName]=propValue}else{assign(props,propName)}props.id=id;return Byond.call("winset",props)};Byond.parseJson=function(json){try{return JSON.parse(json,byondJsonReviver)}catch(err){throw new Error("JSON parsing error: "+(err&&err.message))}};let MAX_PACKET_SIZE=1024;Byond.sendMessage=function(type,payload){let message=typeof type==="string"?{type:type,payload:payload}:type;if(message.payload!==null&&message.payload!==undefined){message.payload=JSON.stringify(message.payload);if(!Byond.TRIDENT&&message.payload.length>MAX_PACKET_SIZE){let chunks=[];for(let i=0,charsLength=message.payload.length;i0}return false};let injectNode=function(node){if(!document.body){setTimeout((()=>{injectNode(node)}));return}let refs=document.body.childNodes;let ref=refs[refs.length-1];ref.parentNode.insertBefore(node,ref.nextSibling)};let loadAsset=function(options){let url=options.url;let type=options.type;let sync=options.sync;let attempt=options.attempt||0;if(loadedAssetByUrl[url]){return}loadedAssetByUrl[url]=options;let retry=function(){if(attempt>=RETRY_ATTEMPTS){let errorMessage="Error: Failed to load the asset "+"'"+url+"' after several attempts.";if(type==="css"){errorMessage+=+"\nStylesheet was either not found, "+"or you're trying to load an empty stylesheet "+"that has no CSS rules in it."}throw new Error(errorMessage)}setTimeout((()=>{loadedAssetByUrl[url]=null;options.attempt+=1;loadAsset(options)}),RETRY_WAIT_INITIAL+attempt*RETRY_WAIT_INCREMENT)};if(type==="js"){let node=document.createElement("script");node.type="text/javascript";node.crossOrigin="anonymous";node.src=url;if(sync){node.defer=true}else{node.async=true}node.onerror=function(){node.onerror=null;node.parentNode.removeChild(node);node=null;retry()};injectNode(node);return}if(type==="css"){let node=document.createElement("link");node.type="text/css";node.rel="stylesheet";node.crossOrigin="anonymous";node.href=url;if(!sync){node.media="only x"}let removeNodeAndRetry=function(){node.parentNode.removeChild(node);node=null;retry()};node.onerror=function(){node.onerror=null;removeNodeAndRetry()};node.onload=function(){node.onload=null;if(isStyleSheetLoaded(node,url)){node.media="all";return}removeNodeAndRetry()};injectNode(node);return}};Byond.loadJs=function(url,sync){loadAsset({url:url,sync:sync,type:"js"})};Byond.loadCss=function(url,sync){loadAsset({url:url,sync:sync,type:"css"})};Byond.saveBlob=function(blob,filename,ext){if(window.navigator.msSaveBlob){window.navigator.msSaveBlob(blob,filename)}else if(window.showSaveFilePicker){let accept={};accept[blob.type]=[ext];let opts={suggestedName:filename,types:[{description:"SS13 file",accept:accept}]};try{window.showSaveFilePicker(opts).then((fileHandle=>{fileHandle.createWritable().then((writeableFileHandle=>{writeableFileHandle.write(blob).then((()=>{writeableFileHandle.close()})).catch((e=>{console.error(e)}))})).catch((e=>{console.error(e)}))})).catch((e=>{console.error(e)}))}catch(e){console.error(e)}}};Byond.iconRefMap={}})();window.onerror=function(msg,url,line,col,error){window.onerror.errorCount=(window.onerror.errorCount||0)+1;let stack=error&&error.stack;if(!stack){stack=msg+"\n at "+url+":"+line;if(col){stack+=":"+col}}stack=window.__augmentStack__(stack,error);if(Byond.strictMode){let errorRoot=document.getElementById("FatalError");let errorStack=document.getElementById("FatalError__stack");if(errorRoot){errorRoot.className="FatalError FatalError--visible";if(window.onerror.__stack__){window.onerror.__stack__+="\n\n"+stack}else{window.onerror.__stack__=stack}let textProp="textContent";errorStack[textProp]=window.onerror.__stack__}let setFatalErrorGeometry=function(){Byond.winset(Byond.windowId,{titlebar:true,"is-visible":true,"can-resize":true})};setFatalErrorGeometry();setInterval(setFatalErrorGeometry,1e3)}if(Byond.strictMode){Byond.sendMessage({type:"log",fatal:1,message:stack})}else if(window.onerror.errorCount<=1){stack+="\nWindow is in non-strict mode, future errors are suppressed.";Byond.sendMessage({type:"log",message:stack})}if(Byond.strictMode){window.update=function(){};window.update.queue=[]}return true};window.onunhandledrejection=function(e){let msg="UnhandledRejection";if(e.reason){msg+=": "+(e.reason.message||e.reason.description||e.reason);if(e.reason.stack){e.reason.stack="UnhandledRejection: "+e.reason.stack}}window.onerror(msg,null,null,null,e.reason)};window.__augmentStack__=function(stack,error){return stack+"\nUser Agent: "+navigator.userAgent};window.update=function(rawMessage){if(window.update.queueActive){window.update.queue.push(rawMessage);return}let message=Byond.parseJson(rawMessage);let listeners=window.update.listeners;for(let i=0;i{window.update.queue=[]}),0)}}let queue=window.update.queue;for(let i=0;i0){url+="&"}let value=params[key];if(value===null||value===undefined){value=""}url+=encodeURIComponent(key)+"="+encodeURIComponent(value)}}}if(window.cef_to_byond){cef_to_byond("byond://"+url);return}if(url.length<2048){location.href="byond://"+url;return}const xhr=new XMLHttpRequest;xhr.open("GET",url);xhr.send()};Byond.callAsync=function(path,params){if(!window.Promise){throw new Error("Async calls require API level of ES2015 or later.")}const index=Byond.__callbacks__.length;const promise=new window.Promise(resolve=>{Byond.__callbacks__.push(resolve)});Byond.call(path,assign({},params,{callback:"Byond.__callbacks__["+index+"]"}));return promise};Byond.topic=function(params){return Byond.call("",params)};Byond.command=function(command){return Byond.call("winset",{command:command})};Byond.winget=function(id,propName){if(id===null){id=""}const isArray=propName instanceof Array;const isSpecific=propName&&propName!=="*"&&!isArray;let promise=Byond.callAsync("winget",{id:id,property:isArray&&propName.join(",")||propName||"*"});if(isSpecific){promise=promise.then(props=>props[propName])}return promise};Byond.winset=function(id,propName,propValue){if(id===null){id=""}else if(typeof id==="object"){return Byond.call("winset",id)}const props={};if(typeof propName==="string"){props[propName]=propValue}else{assign(props,propName)}props.id=id;return Byond.call("winset",props)};Byond.parseJson=function(json){try{return JSON.parse(json,byondJsonReviver)}catch(err){throw new Error("JSON parsing error: "+(err&&err.message))}};const MAX_PACKET_SIZE=1024;Byond.sendMessage=function(type,payload){let message=typeof type==="string"?{type:type,payload:payload}:type;if(message.payload!==null&&message.payload!==undefined){message.payload=JSON.stringify(message.payload);if(!Byond.TRIDENT&&message.payload.length>MAX_PACKET_SIZE){const chunks=[];for(let i=0,charsLength=message.payload.length;i0}return false};const injectNode=function(node){if(!document.body){setTimeout(()=>{injectNode(node)});return}const refs=document.body.childNodes;const ref=refs[refs.length-1];ref.parentNode.insertBefore(node,ref.nextSibling)};const loadAsset=function(options){const url=options.url;const type=options.type;const sync=options.sync;const attempt=options.attempt||0;if(loadedAssetByUrl[url]){return}loadedAssetByUrl[url]=options;const retry=function(){if(attempt>=RETRY_ATTEMPTS){let errorMessage="Error: Failed to load the asset "+"'"+url+"' after several attempts.";if(type==="css"){errorMessage+=+"\nStylesheet was either not found, "+"or you're trying to load an empty stylesheet "+"that has no CSS rules in it."}throw new Error(errorMessage)}setTimeout(()=>{loadedAssetByUrl[url]=null;options.attempt+=1;loadAsset(options)},RETRY_WAIT_INITIAL+attempt*RETRY_WAIT_INCREMENT)};if(type==="js"){let node=document.createElement("script");node.type="text/javascript";node.crossOrigin="anonymous";node.src=url;if(sync){node.defer=true}else{node.async=true}node.onerror=function(){node.onerror=null;node.parentNode.removeChild(node);node=null;retry()};injectNode(node);return}if(type==="css"){let node=document.createElement("link");node.type="text/css";node.rel="stylesheet";node.crossOrigin="anonymous";node.href=url;if(!sync){node.media="only x"}const removeNodeAndRetry=function(){node.parentNode.removeChild(node);node=null;retry()};node.onerror=function(){node.onerror=null;removeNodeAndRetry()};node.onload=function(){node.onload=null;if(isStyleSheetLoaded(node,url)){node.media="all";return}removeNodeAndRetry()};injectNode(node);return}};Byond.loadJs=function(url,sync){loadAsset({url:url,sync:sync,type:"js"})};Byond.loadCss=function(url,sync){loadAsset({url:url,sync:sync,type:"css"})};Byond.saveBlob=function(blob,filename,ext){if(window.navigator.msSaveBlob){window.navigator.msSaveBlob(blob,filename)}else if(window.showSaveFilePicker){const accept={};accept[blob.type]=[ext];const opts={suggestedName:filename,types:[{description:"SS13 file",accept:accept}]};window.showSaveFilePicker(opts).then(function(file){return file.createWritable()}).then(function(file){return file.write(blob).then(function(){return file.close()})}).catch(function(){})}};Byond.iconRefMap={}})();window.onerror=function(msg,url,line,col,error){window.onerror.errorCount=(window.onerror.errorCount||0)+1;let stack=error&&error.stack;if(!stack){stack=msg+"\n at "+url+":"+line;if(col){stack+=":"+col}}stack=window.__augmentStack__(stack,error);if(Byond.strictMode){const errorRoot=document.getElementById("FatalError");const errorStack=document.getElementById("FatalError__stack");if(errorRoot){errorRoot.className="FatalError FatalError--visible";if(window.onerror.__stack__){window.onerror.__stack__+="\n\n"+stack}else{window.onerror.__stack__=stack}const textProp="textContent";errorStack[textProp]=window.onerror.__stack__}const setFatalErrorGeometry=function(){Byond.winset(Byond.windowId,{titlebar:true,"is-visible":true,"can-resize":true})};setFatalErrorGeometry();setInterval(setFatalErrorGeometry,1e3)}if(Byond.strictMode){Byond.sendMessage({type:"log",fatal:1,message:stack})}else if(window.onerror.errorCount<=1){stack+="\nWindow is in non-strict mode, future errors are suppressed.";Byond.sendMessage({type:"log",message:stack})}if(Byond.strictMode){window.update=function(){};window.update.queue=[]}return true};window.onunhandledrejection=function(e){let msg="UnhandledRejection";if(e.reason){msg+=": "+(e.reason.message||e.reason.description||e.reason);if(e.reason.stack){e.reason.stack="UnhandledRejection: "+e.reason.stack}}window.onerror(msg,null,null,null,e.reason)};window.__augmentStack__=function(stack,error){return stack+"\nUser Agent: "+navigator.userAgent};window.update=function(rawMessage){if(window.update.queueActive){window.update.queue.push(rawMessage);return}const message=Byond.parseJson(rawMessage);const listeners=window.update.listeners;for(let i=0;i{window.update.queue=[]},0)}}const queue=window.update.queue;for(let i=0;i + + + + + + + + diff --git a/tgui/public/tgui.html b/tgui/public/tgui.html index ff60d88cec..82ccf9b933 100644 --- a/tgui/public/tgui.html +++ b/tgui/public/tgui.html @@ -6,6 +6,7 @@ +