7 Commits

Author SHA1 Message Date
chompstation-ci[bot]
ad7d82f3ae Automatic changelog compile [ci skip] 2025-12-05 01:23:02 +00:00
chompstation-ci[bot]
c4337dc60a Automatic changelog for PR #12068 [ci skip] 2025-12-05 01:19:46 +00:00
chompstation-ci[bot]
90bebd219a Automatic changelog for PR #12067 [ci skip] 2025-12-05 01:19:30 +00:00
CHOMPStation2StaffMirrorBot
067ab7459b [MIRROR] Hair fix things (#12068)
Co-authored-by: FluffMedic <109300046+FluffMedic@users.noreply.github.com>
2025-12-05 02:19:06 +01:00
CHOMPStation2StaffMirrorBot
a42bdce47b [MIRROR] Iframe fix (#12067)
Co-authored-by: Selis <12716288+ItsSelis@users.noreply.github.com>
2025-12-05 02:18:52 +01:00
chompstation-ci[bot]
28a8d23bf6 Automatic changelog for PR #12040 [ci skip] 2025-12-05 00:26:47 +00:00
CHOMPStation2StaffMirrorBot
ba5019bd9d [MIRROR] Port of the iframe storage for settings (#12040)
Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com>
2025-12-05 01:26:10 +01:00
19 changed files with 344 additions and 43 deletions

View File

@@ -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

View File

@@ -58,4 +58,4 @@
#endif //ifdef REFERENCE_TRACKING #endif //ifdef REFERENCE_TRACKING
// Standard flags to use for browser-options // Standard flags to use for browser-options
#define DEFAULT_CLIENT_BROWSER_OPTIONS "byondstorage,find" #define DEFAULT_CLIENT_BROWSER_OPTIONS "find"

View File

@@ -11,3 +11,7 @@
/datum/config_entry/flag/smart_cache_assets /datum/config_entry/flag/smart_cache_assets
/datum/config_entry/flag/save_spritesheets /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"

View File

@@ -15,9 +15,12 @@ SUBSYSTEM_DEF(tgui)
wait = 9 wait = 9
flags = SS_NO_INIT flags = SS_NO_INIT
priority = FIRE_PRIORITY_TGUI priority = FIRE_PRIORITY_TGUI
init_stage = INITSTAGE_EARLY
runlevels = RUNLEVEL_LOBBY | RUNLEVELS_DEFAULT runlevels = RUNLEVEL_LOBBY | RUNLEVELS_DEFAULT
dependencies = list(
/datum/controller/subsystem/assets
)
/// A list of UIs scheduled to process /// A list of UIs scheduled to process
var/list/current_run = list() var/list/current_run = list()
/// A list of all open UIs /// A list of all open UIs
@@ -40,6 +43,25 @@ SUBSYSTEM_DEF(tgui)
basehtml = replacetextEx(basehtml, "<!-- tgui:nt-copyright -->", "Nanotrasen (c) 2284-[text2num(time2text(world.realtime,"YYYY")) + STATION_YEAR_OFFSET]") basehtml = replacetextEx(basehtml, "<!-- tgui:nt-copyright -->", "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 = replacetextEx(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 = replacetextEx(basehtml, "\[tgui:storagecdn]", webroot.get_asset_url("iframe.html", item))
return
if(!storage_iframe)
return
basehtml = replacetextEx(basehtml, "\[tgui:storagecdn]", storage_iframe)
/datum/controller/subsystem/tgui/Shutdown() /datum/controller/subsystem/tgui/Shutdown()
close_all_uis() close_all_uis()

View File

@@ -782,3 +782,8 @@ ADMIN_VERB(quick_nif, R_ADMIN, "Quick NIF", "Spawns a NIF into someone in quick-
log_and_message_admins("Quick NIF'd [H.real_name] with a [input_NIF].", user) log_and_message_admins("Quick NIF'd [H.real_name] with a [input_NIF].", user)
feedback_add_details("admin_verb","QNIF") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc! feedback_add_details("admin_verb","QNIF") //If you are copy-pasting this, ensure the 2nd parameter is unique to the new proc!
ADMIN_VERB(reload_configuration, R_DEBUG, "Reload Configuration", "Reloads the configuration from the default path on the disk, wiping any in-round modifications.", ADMIN_CATEGORY_DEBUG)
if(tgui_alert(user, "Are you absolutely sure you want to reload the configuration from the default path on the disk, wiping any in-round modifications?", "Really reset?", list("No", "Yes")) != "Yes")
return
config.admin_reload()

View File

@@ -269,6 +269,7 @@ GLOBAL_LIST_INIT(blacklisted_builds, list(
if (CONFIG_GET(flag/chatlog_database_backend)) if (CONFIG_GET(flag/chatlog_database_backend))
chatlog_token = vchatlog_generate_token(ckey, GLOB.round_id) chatlog_token = vchatlog_generate_token(ckey, GLOB.round_id)
winset(src, null, list("browser-options" = "find,refresh"))
// Instantiate stat panel // Instantiate stat panel
stat_panel = new(src, "statbrowser") stat_panel = new(src, "statbrowser")
stat_panel.subscribe(src, PROC_REF(on_stat_panel_message)) stat_panel.subscribe(src, PROC_REF(on_stat_panel_message))

View File

@@ -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). ## 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. ## Will only seek to waste disk space if ran on production.
#SAVE_SPRITESHEETS #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

View File

@@ -1,5 +0,0 @@
author: "CHOMPStation2StaffMirrorBot"
delete-after: True
changes:
- bugfix: "sun subsystem overruns"
- bugfix: "nightshift subsystem overruns"

View File

@@ -1,4 +0,0 @@
author: "Will"
delete-after: True
changes:
- bugfix: "Phased shadekin are no longer knocked down or thrown when a shuttle they are inside of moves."

View File

@@ -31,3 +31,14 @@
FluffMedic: FluffMedic:
- bugfix: A certain Tyr boss mechanic functions as intended, and one tyr wrecked - bugfix: A certain Tyr boss mechanic functions as intended, and one tyr wrecked
shuttles is more accurate shuttles is more accurate
2025-12-05:
CHOMPStation2StaffMirrorBot:
- bugfix: windows should no longer be just white from an invalid iframe
- admin: re-added the 'reload configuration' verb to reload config from disk
- bugfix: fixed the hairs I tried to add a bit ago
- bugfix: sun subsystem overruns
- bugfix: nightshift subsystem overruns
- bugfix: some remaining hitching issues
Will:
- bugfix: Phased shadekin are no longer knocked down or thrown when a shuttle they
are inside of moves.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

After

Width:  |  Height:  |  Size: 204 KiB

5
tgui/global.d.ts vendored
View File

@@ -63,6 +63,11 @@ type ByondType = {
*/ */
strictMode: boolean; strictMode: boolean;
/**
* The external URL for the IndexedDB IFrame to use as the origin
*/
storageCdn: string;
/** /**
* Makes a BYOND call. * Makes a BYOND call.
* *

View File

@@ -6,12 +6,13 @@
* @license MIT * @license MIT
*/ */
export const IMPL_MEMORY = 0;
export const IMPL_HUB_STORAGE = 1; export const IMPL_HUB_STORAGE = 1;
export const IMPL_IFRAME_INDEXED_DB = 2;
type StorageImplementation = typeof IMPL_MEMORY | typeof IMPL_HUB_STORAGE;
const KEY_NAME = 'chomp'; // CHOMPEdit - CHOMPStation Localstore const KEY_NAME = 'chomp'; // CHOMPEdit - CHOMPStation Localstore
type StorageImplementation =
| typeof IMPL_HUB_STORAGE
| typeof IMPL_IFRAME_INDEXED_DB;
type StorageBackend = { type StorageBackend = {
impl: StorageImplementation; impl: StorageImplementation;
@@ -61,28 +62,154 @@ 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<boolean | null> {
const iframe = document.createElement('iframe');
const iframeStore = `${Byond.storageCdn}?store=${KEY_NAME}`;
iframe.style.display = 'none';
this.documentElement = document.body.appendChild(iframe);
iframe.src = iframeStore;
const completePromise: Promise<boolean> = new Promise((resolve) => {
fetch(iframeStore, { method: 'HEAD' })
.then((response) => {
if (response.status !== 200) {
resolve(false);
}
})
.catch(() => {
resolve(false);
});
window.addEventListener('message', (message) => {
if (message.data === 'ready') {
resolve(true);
}
});
});
if (!this.documentElement.contentWindow) {
return new Promise((res) => res(false));
}
this.iframeWindow = this.documentElement.contentWindow;
return completePromise;
}
async get(key: string): Promise<any> {
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 }, '*');
return promise;
}
async set(key: string, value: any): Promise<void> {
this.iframeWindow.postMessage({ type: 'set', key: key, value: value }, '*');
}
async remove(key: string): Promise<void> {
this.iframeWindow.postMessage({ type: 'remove', key: key }, '*');
}
async clear(): Promise<void> {
this.iframeWindow.postMessage({ type: 'clear' }, '*');
}
async destroy(): Promise<void> {
document.body.removeChild(this.documentElement);
}
}
/** /**
* Web Storage Proxy object, which selects the best backend available * Web Storage Proxy object, which selects the best backend available
* depending on the environment. * depending on the environment.
*/ */
class StorageProxy implements StorageBackend { class StorageProxy implements StorageBackend {
private backendPromise: Promise<StorageBackend>; private backendPromise: Promise<StorageBackend>;
public impl: StorageImplementation = IMPL_MEMORY; public impl: StorageImplementation = IMPL_IFRAME_INDEXED_DB;
constructor() { constructor() {
this.backendPromise = (async () => { 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
console.log(`testHubStorage ${testHubStorage()}`);
if (!testHubStorage()) { if (!testHubStorage()) {
// If we have an IFrame URL we can use, and we haven't already enabled
// byondstorage, we should use the IFrame backend
console.log(`storageCdn: ${Byond.storageCdn}`);
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<void>((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) => { return new Promise((resolve) => {
const listener = () => { const listener = () => {
document.removeEventListener('byondstorageupdated', 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); document.addEventListener('byondstorageupdated', listener);
}); });
} }
// byondstorage is already enabled, we can use it straight away
return new HubStorageBackend(); return new HubStorageBackend();
})() as Promise<StorageBackend>; })();
} }
async get(key: string): Promise<any> { async get(key: string): Promise<any> {

View File

@@ -334,6 +334,7 @@ export const chatMiddleware = (store) => {
next(action); next(action);
const page = selectCurrentChatPage(store.getState()); const page = selectCurrentChatPage(store.getState());
chatRenderer.changePage(page); chatRenderer.changePage(page);
needsUpdate = true;
return; return;
} }
if (type === rebuildChat.type) { if (type === rebuildChat.type) {

View File

@@ -29,6 +29,7 @@
// Expose inlined metadata // Expose inlined metadata
Byond.windowId = parseMetaTag('tgui:windowId'); Byond.windowId = parseMetaTag('tgui:windowId');
Byond.storageCdn = parseMetaTag('tgui:storagecdn');
// Backwards compatibility // Backwards compatibility
window.__windowId__ = Byond.windowId; window.__windowId__ = Byond.windowId;
@@ -397,32 +398,17 @@
], ],
}; };
try { window
window .showSaveFilePicker(opts)
.showSaveFilePicker(opts) .then(function (file) {
.then((fileHandle) => { return file.createWritable();
fileHandle })
.createWritable() .then(function (file) {
.then((writeableFileHandle) => { return file.write(blob).then(function () {
writeableFileHandle return file.close();
.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); .catch(function () {});
}
} }
}; };

File diff suppressed because one or more lines are too long

105
tgui/public/iframe.html Normal file
View File

@@ -0,0 +1,105 @@
<!doctype html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta charset="utf-8" />
<script type="text/javascript">
const INDEXED_DB_VERSION = 1;
const INDEXED_DB_NAME = 'tgui';
const INDEXED_DB_STORE_NAME = 'storage';
const READ_ONLY = 'readonly';
const READ_WRITE = 'readwrite';
const MAX_MESSAGES = 1000;
const urlParams = new URLSearchParams(window.location.search);
const storeValue = `${urlParams.get('store')}-${INDEXED_DB_NAME}`;
const dbPromise = new Promise((resolve, reject) => {
const indexedDB = window.indexedDB;
const req = indexedDB.open(storeValue, INDEXED_DB_VERSION);
req.onupgradeneeded = (event) => {
try {
if (event.oldVersion < 1) {
req.result.createObjectStore(INDEXED_DB_STORE_NAME);
}
} catch (err) {
reject(new Error('Failed to upgrade IDB: ' + req.error));
}
};
req.onsuccess = () => resolve(req.result);
req.onerror = () => {
reject(new Error('Failed to open IDB: ' + req.error));
};
});
window.addEventListener('message', (messageEvent) => {
switch (messageEvent.data.type) {
case 'ping':
messageEvent.source.postMessage(true, '*');
break;
case 'get':
get(messageEvent.data.key).then((value) => {
messageEvent.source.postMessage(
{ key: messageEvent.data.key, value: value },
'*',
);
});
break;
case 'set':
set(messageEvent.data.key, messageEvent.data.value);
break;
case 'remove':
remove(messageEvent.data.key);
break;
case 'clear':
clear();
break;
default:
break;
}
});
const getStore = async (mode) => {
return dbPromise.then((db) =>
db
.transaction(INDEXED_DB_STORE_NAME, mode)
.objectStore(INDEXED_DB_STORE_NAME),
);
};
const get = async (key) => {
const store = await getStore(READ_ONLY);
return new Promise((resolve, reject) => {
const req = store.get(key);
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
};
const set = async (key, value) => {
const store = await getStore(READ_WRITE);
store.put(value, key);
};
const remove = async (key) => {
const store = await getStore(READ_WRITE);
store.delete(key);
};
const clear = async () => {
const store = await getStore(READ_WRITE);
store.clear();
};
window.onload = () => {
window.parent.postMessage('ready', '*');
};
</script>
</head>
</html>

View File

@@ -6,6 +6,7 @@
<!-- Inlined metadata --> <!-- Inlined metadata -->
<meta id="tgui:windowId" content="[tgui:windowId]" /> <meta id="tgui:windowId" content="[tgui:windowId]" />
<meta id="tgui:strictMode" content="[tgui:strictMode]" /> <meta id="tgui:strictMode" content="[tgui:strictMode]" />
<meta id="tgui:storagecdn" content="[tgui:storagecdn]" />
<!-- Early setup --> <!-- Early setup -->
<!-- tgui:helpers --> <!-- tgui:helpers -->