From 8b9236ae43c6597814cf0574f03bc2c85284227f Mon Sep 17 00:00:00 2001 From: Raymond <101374892+raymonable@users.noreply.github.com> Date: Sat, 4 Jan 2025 22:04:09 -0500 Subject: [PATCH] feat: :art: finalize server url mode --- AquaNet/.gitignore | 2 + .../components/settings/ChuniSettings.svelte | 25 ++-- AquaNet/src/libs/config.ts | 2 +- AquaNet/src/libs/userbox/ddsCache.ts | 2 +- docs/aquabox-url-mode.md | 30 +++++ tools/extract-chusan.js | 126 ++++++++++++++++++ 6 files changed, 172 insertions(+), 15 deletions(-) create mode 100644 docs/aquabox-url-mode.md create mode 100644 tools/extract-chusan.js diff --git a/AquaNet/.gitignore b/AquaNet/.gitignore index 3a9471ba..20ffbea5 100644 --- a/AquaNet/.gitignore +++ b/AquaNet/.gitignore @@ -31,3 +31,5 @@ dist-ssr !.yarn/releases !.yarn/sdks !.yarn/versions + +public/chu3 \ No newline at end of file diff --git a/AquaNet/src/components/settings/ChuniSettings.svelte b/AquaNet/src/components/settings/ChuniSettings.svelte index c6e1f739..bebf6799 100644 --- a/AquaNet/src/components/settings/ChuniSettings.svelte +++ b/AquaNet/src/components/settings/ChuniSettings.svelte @@ -120,15 +120,14 @@ } let USERBOX_URL_STATE = useLocalStorage("userboxURL", USERBOX_DEFAULT_URL); - function userboxHandleInput(e: KeyboardEvent) { - if (e.key != "Enter") - return; - let baseURL = (e.target as HTMLInputElement).value; + function userboxHandleInput(baseURL: string, isSetByServer: boolean = false) { if (baseURL != "") try { // validate url - new URL(baseURL); + new URL(baseURL, location.href); } catch(err) { + if (isSetByServer) + return; return error = t("userbox.new.error.invalidUrl") } USERBOX_URL_STATE.value = baseURL; @@ -137,17 +136,17 @@ location.reload(); } - if (USERBOX_DEFAULT_URL) - USERBOX_URL_STATE.value = USERBOX_DEFAULT_URL; + if (USERBOX_DEFAULT_URL && !USERBOX_URL_STATE.value) + userboxHandleInput(USERBOX_DEFAULT_URL, true); indexedDB.databases().then(async (dbi) => { let databaseExists = dbi.some(db => db.name == "userboxChusanDDS"); - if (databaseExists) { + if (databaseExists) await initializeDb(); + if (databaseExists || USERBOX_URL_STATE.value) { DDSreader = new DDS(ddsDB); - USERBOX_INSTALLED = databaseExists; - } else if (USERBOX_URL_STATE.value) - USERBOX_INSTALLED = true; + USERBOX_INSTALLED = databaseExists || USERBOX_URL_STATE.value != ""; + } }) @@ -251,7 +250,7 @@ {/if} - {#if USERBOX_SUPPORT} + {#if USERBOX_SUPPORT && !USERBOX_DEFAULT_URL}
@@ -282,7 +281,7 @@ {USERBOX_SETUP_MODE ? t('userbox.new.url_warning') : USERBOX_SETUP_TEXT}
{t('userbox.new.setup.notice')}
diff --git a/AquaNet/src/libs/config.ts b/AquaNet/src/libs/config.ts
index ee86f68b..fb9cbde6 100644
--- a/AquaNet/src/libs/config.ts
+++ b/AquaNet/src/libs/config.ts
@@ -16,7 +16,7 @@ export const FADE_OUT = { duration: 200 }
export const FADE_IN = { delay: 400 }
export const DEFAULT_PFP = '/assets/imgs/no_profile.png'
-// USERBOX_ASSETS
+// Documentation for Userbox mode can be found in `docs/aquabox-url-mode.md`
// Please note that if this is set, it must be manually unset by users in Chuni Settings -> Update Userbox -> Switch to URL mode -> (empty value) -> Enter key
export const USERBOX_DEFAULT_URL = ""
diff --git a/AquaNet/src/libs/userbox/ddsCache.ts b/AquaNet/src/libs/userbox/ddsCache.ts
index 8bdfcfa1..e6bbb212 100644
--- a/AquaNet/src/libs/userbox/ddsCache.ts
+++ b/AquaNet/src/libs/userbox/ddsCache.ts
@@ -49,7 +49,7 @@ export default class DDSCache {
return new Promise(async (resolve, reject) => {
if (this.userboxURL.value) {
let targetPath = path.replaceAll(":", "/");
- let response = await fetch(`${this.userboxURL.value}/${targetPath}.dds`).then(b => b.blob()).catch(reject);
+ let response = await fetch(`${this.userboxURL.value}/${targetPath}.chu`).then(b => b.blob()).catch(reject);
if (response)
return resolve(response);
};
diff --git a/docs/aquabox-url-mode.md b/docs/aquabox-url-mode.md
new file mode 100644
index 00000000..2b236529
--- /dev/null
+++ b/docs/aquabox-url-mode.md
@@ -0,0 +1,30 @@
+# AquaBox URL Mode Setup Guide
+
+## For users
+
+1. Go to your Chuni game settings
+2. Go down to "Enable AquaBox" or "Upgrade AquaBox"
+3. Click on "Switch to URL mode"
+4. Enter the base URL for your AquaBox
+
+## For server owners / asset hosters
+
+> :warning: Assets are already not hosted on AquaDX for legal reasons.
+> Hosting SEGA's assets may put you at higher risk of DMCA.
+
+1. Extract your Chunithm Luminous game files.
+
+ It is recommend you have the latest version of the game and all of the options your users may use.
+
+ The script to generate the proper paths can be found in [/tools/chusan-extractor.js](/tools/chusan-extractor.js). Node.js or Bun is required.
+ Please read the comments at the top of the script for usage instructions.
+
+2. Copy the new `chu3` folder where you need it to be (read #3 if you're hosting AquaNet and want to host on the same endpoints).
+3. (Optional) Update `src/lib/config.ts`.
+```ts
+// Change this to the base url of where your assets are stored.
+// If you are hosting on AquaNet, you can put the files @ /public/chu3 & use '/chu3' for your base url.
+// This will work the same way as setting it on the UI does. TEST IT ON THE UI BEFORE YOU APPLY THIS CONFIG!!!
+export const USERBOX_DEFAULT_URL = "/chu3";
+```
+4. Enjoy!
\ No newline at end of file
diff --git a/tools/extract-chusan.js b/tools/extract-chusan.js
new file mode 100644
index 00000000..3ec4c87a
--- /dev/null
+++ b/tools/extract-chusan.js
@@ -0,0 +1,126 @@
+/*
+
+Chusan asset extractor for AquaBox URL mode.
+ Place your "option" (or "bin/option") and "data" folders in the same directory as this script as they're named.
+
+ Data will be placed into the "chu3" folder.
+ Place the contents into a public directory that can be accessed by users.
+
+Know Python or another common scripting language?
+ Feel free to rewrite this tool and submit it to MewoLab/AquaDX!
+ Or rewrite it in JavaScript again! Anything is better than this hot pile of garbage!
+
+*/
+
+// Allows this to be a single-file script
+const fs = require("fs");
+
+const verifyDirectoryExistant = (name) => {
+ return fs.existsSync(name);
+}
+const mkdir = (name) => {
+ if (!fs.existsSync(name))
+ fs.mkdirSync(name);
+};
+const outputTarget = "chu3";
+
+const directoryPaths = [
+ {
+ folder: "ddsImage",
+ processName: "Characters",
+ path: "characterThumbnail",
+ filter: (name) => name.substring(name.length - 6, name.length) == "02.dds",
+ id: (name) => `0${name.substring(17, 21)}${name.substring(23, 24)}`
+ },
+ {
+ folder: "namePlate",
+ processName: "Nameplates",
+ path: "nameplate",
+ filter: (name) => name.substring(0, 17) == "CHU_UI_NamePlate_",
+ id: (name) => name.substring(17, 25)
+ },
+ {
+ folder: "avatarAccessory",
+ processName: "Avatar Accessory Thumbnails",
+ path: "avatarAccessoryThumbnail",
+ filter: (name) => name.substring(14, 18) == "Icon",
+ id: (name) => name.substring(19, 27)
+ },
+ {
+ folder: "avatarAccessory",
+ processName: "Avatar Accessories",
+ path: "avatarAccessory",
+ filter: (name) => name.substring(14, 17) == "Tex",
+ id: (name) => name.substring(18, 26)
+ },
+ {
+ folder: "texture",
+ processName: "Surfboard Textures",
+ useFileName: true,
+ path: "surfboard",
+ filter: (name) =>
+ ([
+ "CHU_UI_Common_Avatar_body_00.dds",
+ "CHU_UI_Common_Avatar_face_00.dds",
+ "CHU_UI_title_rank_00_v10.dds"
+ ]).includes(name),
+ id: (name) => name
+ }
+];
+
+const processFile = (fileName, path, subFolder) => {
+ let localReference = directoryPaths.find(p => p.folder == subFolder && p.filter(fileName));
+ if (!localReference) return;
+ files.push({
+ path: `${path}/${fileName}`,
+ target: `${localReference.id(fileName)}.chu`,
+ targetFolder: `${localReference.path}`,
+ name: fileName
+ });
+}
+
+let files = [];
+const processFolder = (path) => {
+ for (const folder of fs.readdirSync(path)) {
+ let folderData = fs.statSync(`${path}/${folder}`);
+ if (!folderData.isDirectory()) continue;
+ for (const subFolder of fs.readdirSync(`${path}/${folder}`)) {
+ let folderData = fs.statSync(`${path}/${folder}/${subFolder}`);
+ let reference = directoryPaths.find(p => p.folder == subFolder);
+ if (!reference || !folderData.isDirectory()) continue;
+ // what a mess
+ for (const subSubFolder of fs.readdirSync(`${path}/${folder}/${subFolder}`))
+ if (fs.statSync(`${path}/${folder}/${subFolder}/${subSubFolder}`).isDirectory()) {
+ for (const subSubSubFile of fs.readdirSync(`${path}/${folder}/${subFolder}/${subSubFolder}`))
+ processFile(subSubSubFile, `${path}/${folder}/${subFolder}/${subSubFolder}`, subFolder)
+ } else
+ processFile(subSubFolder, `${path}/${folder}/${subFolder}`, subFolder)
+ }
+ }
+}
+
+if (!verifyDirectoryExistant("data"))
+ return console.log("Data folder non-existant.")
+if (!verifyDirectoryExistant("bin"))
+ if (!verifyDirectoryExistant("option"))
+ return console.log("Option folder non-existant.")
+
+processFolder("data");
+if (verifyDirectoryExistant("bin")) {
+ processFolder("bin/option");
+} else
+ processFolder("option");
+
+
+console.log(`Found ${files.length} files.`);
+console.log(`Copying now, please wait.`)
+
+if (verifyDirectoryExistant(outputTarget))
+ return console.log("Output folder exists.");
+mkdir(outputTarget);
+
+files.forEach(fileData => {
+ console.log(`Copying ${fileData.name}`)
+ mkdir(`${outputTarget}/${fileData.targetFolder}`)
+ fs.copyFileSync(fileData.path, `${outputTarget}/${fileData.targetFolder}/${fileData.target}`)
+})
\ No newline at end of file