feat: 🎨 finalize server url mode

pull/99/head
Raymond 2025-01-04 22:04:09 -05:00
parent f6efd392b9
commit 8b9236ae43
6 changed files with 172 additions and 15 deletions

2
AquaNet/.gitignore vendored
View File

@ -31,3 +31,5 @@ dist-ssr
!.yarn/releases !.yarn/releases
!.yarn/sdks !.yarn/sdks
!.yarn/versions !.yarn/versions
public/chu3

View File

@ -120,15 +120,14 @@
} }
let USERBOX_URL_STATE = useLocalStorage("userboxURL", USERBOX_DEFAULT_URL); let USERBOX_URL_STATE = useLocalStorage("userboxURL", USERBOX_DEFAULT_URL);
function userboxHandleInput(e: KeyboardEvent) { function userboxHandleInput(baseURL: string, isSetByServer: boolean = false) {
if (e.key != "Enter")
return;
let baseURL = (e.target as HTMLInputElement).value;
if (baseURL != "") if (baseURL != "")
try { try {
// validate url // validate url
new URL(baseURL); new URL(baseURL, location.href);
} catch(err) { } catch(err) {
if (isSetByServer)
return;
return error = t("userbox.new.error.invalidUrl") return error = t("userbox.new.error.invalidUrl")
} }
USERBOX_URL_STATE.value = baseURL; USERBOX_URL_STATE.value = baseURL;
@ -137,17 +136,17 @@
location.reload(); location.reload();
} }
if (USERBOX_DEFAULT_URL) if (USERBOX_DEFAULT_URL && !USERBOX_URL_STATE.value)
USERBOX_URL_STATE.value = USERBOX_DEFAULT_URL; userboxHandleInput(USERBOX_DEFAULT_URL, true);
indexedDB.databases().then(async (dbi) => { indexedDB.databases().then(async (dbi) => {
let databaseExists = dbi.some(db => db.name == "userboxChusanDDS"); let databaseExists = dbi.some(db => db.name == "userboxChusanDDS");
if (databaseExists) { if (databaseExists)
await initializeDb(); await initializeDb();
if (databaseExists || USERBOX_URL_STATE.value) {
DDSreader = new DDS(ddsDB); DDSreader = new DDS(ddsDB);
USERBOX_INSTALLED = databaseExists; USERBOX_INSTALLED = databaseExists || USERBOX_URL_STATE.value != "";
} else if (USERBOX_URL_STATE.value) }
USERBOX_INSTALLED = true;
}) })
</script> </script>
@ -251,7 +250,7 @@
</label> </label>
</div> </div>
{/if} {/if}
{#if USERBOX_SUPPORT} {#if USERBOX_SUPPORT && !USERBOX_DEFAULT_URL}
<p> <p>
<button on:click={() => USERBOX_SETUP_RUN = !USERBOX_SETUP_RUN}>{t(!USERBOX_INSTALLED ? `userbox.new.activate_first` : `userbox.new.activate_update`)}</button> <button on:click={() => USERBOX_SETUP_RUN = !USERBOX_SETUP_RUN}>{t(!USERBOX_INSTALLED ? `userbox.new.activate_first` : `userbox.new.activate_update`)}</button>
</p> </p>
@ -282,7 +281,7 @@
<span>{USERBOX_SETUP_MODE ? t('userbox.new.url_warning') : USERBOX_SETUP_TEXT}</span> <span>{USERBOX_SETUP_MODE ? t('userbox.new.url_warning') : USERBOX_SETUP_TEXT}</span>
<div class="actions"> <div class="actions">
{#if USERBOX_SETUP_MODE} {#if USERBOX_SETUP_MODE}
<input type="text" on:keyup={userboxHandleInput} class="add-margin" placeholder="Base URL"> <input type="text" on:keyup={e => {if (e.key == "Enter") userboxHandleInput((e.target as HTMLInputElement).value)}} class="add-margin" placeholder="Base URL">
{:else} {:else}
<p class="notice add-margin"> <p class="notice add-margin">
{t('userbox.new.setup.notice')} {t('userbox.new.setup.notice')}

View File

@ -16,7 +16,7 @@ export const FADE_OUT = { duration: 200 }
export const FADE_IN = { delay: 400 } export const FADE_IN = { delay: 400 }
export const DEFAULT_PFP = '/assets/imgs/no_profile.png' 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 // 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 = "" export const USERBOX_DEFAULT_URL = ""

View File

@ -49,7 +49,7 @@ export default class DDSCache {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
if (this.userboxURL.value) { if (this.userboxURL.value) {
let targetPath = path.replaceAll(":", "/"); 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) if (response)
return resolve(response); return resolve(response);
}; };

View File

@ -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.<br>
> 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.<br>
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!

View File

@ -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}`)
})