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}
{#if USERBOX_SETUP_MODE} - + {if (e.key == "Enter") userboxHandleInput((e.target as HTMLInputElement).value)}} class="add-margin" placeholder="Base URL"> {:else}

{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