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 b57a0c8b..f1e84f3f 100644 --- a/AquaNet/src/components/settings/ChuniSettings.svelte +++ b/AquaNet/src/components/settings/ChuniSettings.svelte @@ -8,7 +8,7 @@ } from "../../libs/generalTypes"; import { DATA, USER, USERBOX } from "../../libs/sdk"; import { t, ts } from "../../libs/i18n"; - import { DATA_HOST, FADE_IN, FADE_OUT, HAS_USERBOX_ASSETS } from "../../libs/config"; + import { DATA_HOST, FADE_IN, FADE_OUT, USERBOX_DEFAULT_URL } from "../../libs/config"; import { fade, slide } from "svelte/transition"; import StatusOverlays from "../StatusOverlays.svelte"; import Icon from "@iconify/svelte"; @@ -97,9 +97,11 @@ let USERBOX_PROGRESS = 0; let USERBOX_SETUP_RUN = false; + let USERBOX_SETUP_MODE = false; let USERBOX_SETUP_TEXT = t("userbox.new.setup"); let USERBOX_ENABLED = useLocalStorage("userboxNew", false); + let USERBOX_PROFILE_ENABLED = useLocalStorage("userboxNewProfile", false); let USERBOX_INSTALLED = false; let USERBOX_SUPPORT = "webkitGetAsEntry" in DataTransferItem.prototype; @@ -117,12 +119,37 @@ }) ?? ""; } + let USERBOX_URL_STATE = useLocalStorage("userboxURL", USERBOX_DEFAULT_URL); + function userboxHandleInput(baseURL: string, isSetByServer: boolean = false) { + if (baseURL != "") + try { + // validate url + new URL(baseURL, location.href); + } catch(err) { + if (isSetByServer) + return; + return error = t("userbox.new.error.invalidUrl") + } + USERBOX_URL_STATE.value = baseURL; + USERBOX_ENABLED.value = true; + USERBOX_PROFILE_ENABLED.value = true; + location.reload(); + } + + 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 (USERBOX_URL_STATE.value && databaseExists) { + indexedDB.deleteDatabase("userboxChusanDDS") + } if (databaseExists) { await initializeDb(); + } + if (databaseExists || USERBOX_URL_STATE.value) { DDSreader = new DDS(ddsDB); - USERBOX_INSTALLED = databaseExists; + USERBOX_INSTALLED = databaseExists || USERBOX_URL_STATE.value != ""; } }) @@ -156,9 +183,9 @@ {:else}
- userboxSelected = "nameplateId"} chuniCharacter={userbox.characterId} chuniLevel={userbox.level} chuniRating={userbox.playerRating / 100} + userboxSelected = "nameplateId"} chuniCharacter={userbox.characterId} chuniLevel={userbox.level.toString()} chuniRating={userbox.playerRating / 100} chuniNameplate={userbox.nameplateId} chuniName={userbox.userName} chuniTrophyName={allItems.trophy[userbox.trophyId].name}> -
@@ -210,39 +237,28 @@ {/each} {/if} - {#if HAS_USERBOX_ASSETS} - {#if USERBOX_INSTALLED} - -
- - -
- {/if} - {#if USERBOX_SUPPORT} -

- -

- {/if} - {#if !USERBOX_SUPPORT || !USERBOX_INSTALLED || !USERBOX_ENABLED.value} -

{t("userbox.header.preview")}

-

{t("userbox.preview.notice")}

- - {#if preview} -
- {#each userItems.filter(v => v.iKey != 'trophy' && v.iKey != 'systemVoice') as { iKey, ubKey, items }, i} -
- {ts(`userbox.${ubKey}`)} - -
- {/each} -
- {/if} - {/if} + {#if USERBOX_INSTALLED} + +
+ + +
+
+ + +
+ {/if} + {#if USERBOX_SUPPORT && !USERBOX_DEFAULT_URL} +

+ +

{/if} - {/if} @@ -251,20 +267,32 @@

{t('userbox.new.name')}

- {USERBOX_SETUP_TEXT} + {USERBOX_SETUP_MODE ? t('userbox.new.url_warning') : USERBOX_SETUP_TEXT}
- {#if USERBOX_PROGRESS != 0} -
-
-
+ {#if USERBOX_SETUP_MODE} + {if (e.key == "Enter") userboxHandleInput((e.target as HTMLInputElement).value)}} class="add-margin" placeholder="Base URL"> {:else} - - + {#if USERBOX_PROGRESS != 0} +
+
+
+ {:else} +

+ {t('userbox.new.setup.notice')} +

+ + {/if} + {/if} + {#if USERBOX_PROGRESS == 0} + + {/if}
@@ -299,13 +327,15 @@ p.notice border-radius: 25px +.add-margin, .drop-btn + margin-bottom: 1em + .drop-btn position: relative width: 100% aspect-ratio: 3 background: transparent box-shadow: 0 0 1px 1px vars.$ov-lighter - margin-bottom: 1em > input position: absolute diff --git a/AquaNet/src/components/settings/userbox/ChuniPenguin.svelte b/AquaNet/src/components/settings/userbox/ChuniPenguin.svelte index ed9730c6..061feb6e 100644 --- a/AquaNet/src/components/settings/userbox/ChuniPenguin.svelte +++ b/AquaNet/src/components/settings/userbox/ChuniPenguin.svelte @@ -11,7 +11,7 @@ export var chuniItem = 1500001; export var chuniFront = 1600001; export var chuniBack = 1700001; - export var classPassthrough: string = `` + export var classPassthrough: string = ``;
@@ -28,13 +28,33 @@ Beak {/await} - - {#await DDSreader.getFileFromSheet("surfboard:CHU_UI_Common_Avatar_body_00.dds", 0, 0, 85, 160, 0.75) then imageURL} - Left Arm - {/await} - {#await DDSreader.getFileFromSheet("surfboard:CHU_UI_Common_Avatar_body_00.dds", 0, 0, 85, 160, 0.75) then imageURL} - Right Arm - {/await} + {#if chuniItem != 1500001} + + {#await DDSreader.getFileFromSheet("surfboard:CHU_UI_Common_Avatar_body_00.dds", 0, 0, 85, 160, 0.75) then imageURL} + Left Arm +
+ {#await DDSreader.getFileFromSheet(`avatarAccessory:${chuniItem.toString().padStart(8, "0")}`, 0, 0, 200, 544, 0.75) then imageURL} + Item + {/await} +
+ {/await} + {#await DDSreader.getFileFromSheet("surfboard:CHU_UI_Common_Avatar_body_00.dds", 0, 0, 85, 160, 0.75) then imageURL} + Right Arm +
+ {#await DDSreader.getFileFromSheet(`avatarAccessory:${chuniItem.toString().padStart(8, "0")}`, 200, 0, 200, 544, 0.75) then imageURL} + Item + {/await} +
+ {/await} + {:else} + + {#await DDSreader.getFileFromSheet("surfboard:CHU_UI_Common_Avatar_body_00.dds", 80, 0, 110, 100, 0.75) then imageURL} + Left Arm + {/await} + {#await DDSreader.getFileFromSheet("surfboard:CHU_UI_Common_Avatar_body_00.dds", 80, 0, 110, 100, 0.75) then imageURL} + Right Arm + {/await} + {/if} {#await DDSreader.getFileScaled(`avatarAccessory:${chuniWear.toString().padStart(8, "0")}`, 0.75, `avatarAccessory:01100001`) then imageURL} @@ -60,11 +80,6 @@ Face (Accessory) {/await} - - {#await DDSreader.getFileScaled(`avatarAccessory:${chuniItem.toString().padStart(8, "0")}`, 0.75, `avatarAccessory:01500001`) then imageURL} - Item - {/await} - {#await DDSreader.getFileScaled(`avatarAccessory:${chuniFront.toString().padStart(8, "0")}`, 0.75) then imageURL} Front @@ -77,8 +92,11 @@
- {#await DDSreader.getFileFromSheet(`avatarAccessory:${chuniSkin.toString().padStart(8, "0")}`, 0, 410, 167, 80, 0.75) then imageURL} - Feet + {#await DDSreader.getFileFromSheet(`avatarAccessory:${chuniSkin.toString().padStart(8, "0")}`, 0, 410, 85, 80, 0.75) then imageURL} + Foot + {/await} + {#await DDSreader.getFileFromSheet(`avatarAccessory:${chuniSkin.toString().padStart(8, "0")}`, 85, 410, 85, 80, 0.75) then imageURL} + Foot {/await}
@@ -86,11 +104,11 @@ diff --git a/docs/aquabox-url-mode.md b/docs/aquabox-url-mode.md new file mode 100644 index 00000000..8d7c85df --- /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 recommended 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