aquabox improvements (#99)

pull/103/head
Azalea 2025-01-05 07:23:16 -05:00 committed by GitHub
commit 574e885da3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 445 additions and 97 deletions

2
AquaNet/.gitignore vendored
View File

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

View File

@ -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 @@
</div>
{:else}
<div class="chuni-userbox-container">
<ChuniUserplateComponent on:click={() => userboxSelected = "nameplateId"} chuniCharacter={userbox.characterId} chuniLevel={userbox.level} chuniRating={userbox.playerRating / 100}
<ChuniUserplateComponent chuniIsUserbox={true} on:click={() => 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}></ChuniUserplateComponent>
<ChuniPenguinComponent classPassthrough="chuni-penguin-float" chuniWear={userbox.avatarWear} chuniHead={userbox.avatarHead} chuniBack={userbox.avatarBack}
<ChuniPenguinComponent chuniWear={userbox.avatarWear} chuniHead={userbox.avatarHead} chuniBack={userbox.avatarBack}
chuniFront={userbox.avatarFront} chuniFace={userbox.avatarFace} chuniItem={userbox.avatarItem}
chuniSkin={userbox.avatarSkin}></ChuniPenguinComponent>
</div>
@ -210,39 +237,28 @@
{/each}
</div>
{/if}
{#if HAS_USERBOX_ASSETS}
{#if USERBOX_INSTALLED}
<!-- god this is a mess but idgaf atp -->
<div class="field boolean" style:margin-top="1em">
<input type="checkbox" bind:checked={USERBOX_ENABLED.value} id="newUserbox">
<label for="newUserbox">
<span class="name">{t("userbox.new.activate")}</span>
<span class="desc">{t(`userbox.new.activate_desc`)}</span>
</label>
</div>
{/if}
{#if USERBOX_SUPPORT}
<p>
<button on:click={() => USERBOX_SETUP_RUN = !USERBOX_SETUP_RUN}>{t(!USERBOX_INSTALLED ? `userbox.new.activate_first` : `userbox.new.activate_update`)}</button>
</p>
{/if}
{#if !USERBOX_SUPPORT || !USERBOX_INSTALLED || !USERBOX_ENABLED.value}
<h2>{t("userbox.header.preview")}</h2>
<p class="notice">{t("userbox.preview.notice")}</p>
<input bind:value={preview} placeholder={t("userbox.preview.url")}/>
{#if preview}
<div class="preview">
{#each userItems.filter(v => v.iKey != 'trophy' && v.iKey != 'systemVoice') as { iKey, ubKey, items }, i}
<div>
<span>{ts(`userbox.${ubKey}`)}</span>
<img src={`${preview}/${iKey}/${userbox[ubKey].toString().padStart(8, '0')}.png`} alt="" on:error={coverNotFound} />
</div>
{/each}
</div>
{/if}
{/if}
{#if USERBOX_INSTALLED}
<!-- god this is a mess but idgaf atp -->
<div class="field boolean" style:margin-top="1em">
<input type="checkbox" bind:checked={USERBOX_ENABLED.value} id="newUserbox">
<label for="newUserbox">
<span class="name">{t("userbox.new.activate")}</span>
<span class="desc">{t(`userbox.new.activate_desc`)}</span>
</label>
</div>
<div class="field boolean" style:margin-top="1em">
<input type="checkbox" bind:checked={USERBOX_PROFILE_ENABLED.value} id="newUserboxProfile">
<label for="newUserboxProfile">
<span class="name">{t("userbox.new.activate_profile")}</span>
<span class="desc">{t(`userbox.new.activate_profile_desc`)}</span>
</label>
</div>
{/if}
{#if USERBOX_SUPPORT && !USERBOX_DEFAULT_URL}
<p>
<button on:click={() => USERBOX_SETUP_RUN = !USERBOX_SETUP_RUN}>{t(!USERBOX_INSTALLED ? `userbox.new.activate_first` : `userbox.new.activate_update`)}</button>
</p>
{/if}
<ChuniMatchingSettings/>
</div>
{/if}
@ -251,20 +267,32 @@
<div class="overlay" transition:fade>
<div>
<h2>{t('userbox.new.name')}</h2>
<span>{USERBOX_SETUP_TEXT}</span>
<span>{USERBOX_SETUP_MODE ? t('userbox.new.url_warning') : USERBOX_SETUP_TEXT}</span>
<div class="actions">
{#if USERBOX_PROGRESS != 0}
<div class="progress">
<div class="progress-bar" style="width: {USERBOX_PROGRESS}%"></div>
</div>
{#if USERBOX_SETUP_MODE}
<input type="text" on:keyup={e => {if (e.key == "Enter") userboxHandleInput((e.target as HTMLInputElement).value)}} class="add-margin" placeholder="Base URL">
{:else}
<button class="drop-btn">
<input type="file" on:input={userboxSafeDrop} on:click={e => e.preventDefault()}>
{t('userbox.new.drop')}
</button>
<button on:click={() => USERBOX_SETUP_RUN = false}>
{t('back')}
</button>
{#if USERBOX_PROGRESS != 0}
<div class="progress">
<div class="progress-bar" style="width: {USERBOX_PROGRESS}%"></div>
</div>
{:else}
<p class="notice add-margin">
{t('userbox.new.setup.notice')}
</p>
<button class="drop-btn">
<input type="file" on:input={userboxSafeDrop} on:click={e => e.preventDefault()}>
{t('userbox.new.drop')}
</button>
{/if}
{/if}
{#if USERBOX_PROGRESS == 0}
<button on:click={() => USERBOX_SETUP_RUN = false}>
{t('back')}
</button>
<button on:click={() => USERBOX_SETUP_MODE = !USERBOX_SETUP_MODE}>
{t(USERBOX_SETUP_MODE ? 'userbox.new.switch.to_drop' : 'userbox.new.switch.to_url')}
</button>
{/if}
</div>
</div>
@ -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

View File

@ -11,7 +11,7 @@
export var chuniItem = 1500001;
export var chuniFront = 1600001;
export var chuniBack = 1700001;
export var classPassthrough: string = ``
export var classPassthrough: string = ``;
</script>
<div class="chuni-penguin {classPassthrough}">
<div class="chuni-penguin-body">
@ -28,13 +28,33 @@
<img class="chuni-penguin-beak chuni-penguin-accessory" src={imageURL} alt="Beak">
{/await}
<!-- Arms (surfboard) -->
{#await DDSreader.getFileFromSheet("surfboard:CHU_UI_Common_Avatar_body_00.dds", 0, 0, 85, 160, 0.75) then imageURL}
<img class="chuni-penguin-arm-left chuni-penguin-arm" src={imageURL} alt="Left Arm">
{/await}
{#await DDSreader.getFileFromSheet("surfboard:CHU_UI_Common_Avatar_body_00.dds", 0, 0, 85, 160, 0.75) then imageURL}
<img class="chuni-penguin-arm-right chuni-penguin-arm" src={imageURL} alt="Right Arm">
{/await}
{#if chuniItem != 1500001}
<!-- Arms (straight) -->
{#await DDSreader.getFileFromSheet("surfboard:CHU_UI_Common_Avatar_body_00.dds", 0, 0, 85, 160, 0.75) then imageURL}
<img class="chuni-penguin-arm-left chuni-penguin-arm" src={imageURL} alt="Left Arm">
<div class="chuni-penguin-arm-left chuni-penguin-arm-type-1 chuni-penguin-arm">
{#await DDSreader.getFileFromSheet(`avatarAccessory:${chuniItem.toString().padStart(8, "0")}`, 0, 0, 200, 544, 0.75) then imageURL}
<img class="chuni-penguin-item chuni-penguin-accessory chuni-penguin-item-left" src={imageURL} alt="Item">
{/await}
</div>
{/await}
{#await DDSreader.getFileFromSheet("surfboard:CHU_UI_Common_Avatar_body_00.dds", 0, 0, 85, 160, 0.75) then imageURL}
<img class="chuni-penguin-arm-right chuni-penguin-arm" src={imageURL} alt="Right Arm">
<div class="chuni-penguin-arm-right chuni-penguin-arm-type-1 chuni-penguin-arm">
{#await DDSreader.getFileFromSheet(`avatarAccessory:${chuniItem.toString().padStart(8, "0")}`, 200, 0, 200, 544, 0.75) then imageURL}
<img class="chuni-penguin-item chuni-penguin-accessory chuni-penguin-item-right" src={imageURL} alt="Item">
{/await}
</div>
{/await}
{:else}
<!-- Arms (bent) -->
{#await DDSreader.getFileFromSheet("surfboard:CHU_UI_Common_Avatar_body_00.dds", 80, 0, 110, 100, 0.75) then imageURL}
<img class="chuni-penguin-arm-left chuni-penguin-arm chuni-penguin-arm-type-2" src={imageURL} alt="Left Arm">
{/await}
{#await DDSreader.getFileFromSheet("surfboard:CHU_UI_Common_Avatar_body_00.dds", 80, 0, 110, 100, 0.75) then imageURL}
<img class="chuni-penguin-arm-right chuni-penguin-arm chuni-penguin-arm-type-2" src={imageURL} alt="Right Arm">
{/await}
{/if}
<!-- Wear -->
{#await DDSreader.getFileScaled(`avatarAccessory:${chuniWear.toString().padStart(8, "0")}`, 0.75, `avatarAccessory:01100001`) then imageURL}
@ -60,11 +80,6 @@
<img class="chuni-penguin-face-accessory chuni-penguin-accessory" src={imageURL} alt="Face (Accessory)">
{/await}
<!-- Item -->
{#await DDSreader.getFileScaled(`avatarAccessory:${chuniItem.toString().padStart(8, "0")}`, 0.75, `avatarAccessory:01500001`) then imageURL}
<img class="chuni-penguin-item chuni-penguin-accessory" src={imageURL} alt="Item">
{/await}
<!-- Front -->
{#await DDSreader.getFileScaled(`avatarAccessory:${chuniFront.toString().padStart(8, "0")}`, 0.75) then imageURL}
<img class="chuni-penguin-front chuni-penguin-accessory" src={imageURL} alt="Front">
@ -77,8 +92,11 @@
</div>
<div class="chuni-penguin-feet">
<!-- Feet -->
{#await DDSreader.getFileFromSheet(`avatarAccessory:${chuniSkin.toString().padStart(8, "0")}`, 0, 410, 167, 80, 0.75) then imageURL}
<img src={imageURL} alt="Feet">
{#await DDSreader.getFileFromSheet(`avatarAccessory:${chuniSkin.toString().padStart(8, "0")}`, 0, 410, 85, 80, 0.75) then imageURL}
<img src={imageURL} alt="Foot">
{/await}
{#await DDSreader.getFileFromSheet(`avatarAccessory:${chuniSkin.toString().padStart(8, "0")}`, 85, 410, 85, 80, 0.75) then imageURL}
<img src={imageURL} alt="Foot">
{/await}
</div>
</div>
@ -86,11 +104,11 @@
<style lang="sass">
@keyframes chuniPenguinBodyBob
0%
transform: translate(-50%, 0%) translate(0%, -50%)
transform: translate(-50%, 5px) translate(0%, -50%)
50%
transform: translate(-50%, 10px) translate(0%, -50%)
100%
transform: translate(-50%, 0%) translate(0%, -50%)
100%
transform: translate(-50%, 5px) translate(0%, -50%)
@keyframes chuniPenguinArmLeft
0%
transform: translate(-50%, 0) rotate(-2deg)
@ -108,11 +126,21 @@
img
-webkit-user-drag: none
user-select: none
.chuni-penguin
height: 512px
aspect-ratio: 1/2
position: relative
pointer-events: none
z-index: 1
&.chuni-penguin-float
position: absolute
top: 50%
left: 50%
transform: translate(-50%, -50%)
.chuni-penguin-body, .chuni-penguin-feet
transform: translate(-50%, -50%)
@ -122,21 +150,43 @@
.chuni-penguin-body
top: 50%
z-index: 1
animation: chuniPenguinBodyBob 2s infinite cubic-bezier(0.45, 0, 0.55, 1)
animation: chuniPenguinBodyBob 1s infinite cubic-bezier(0.45, 0, 0.55, 1)
.chuni-penguin-feet
top: 82.5%
top: 80%
z-index: 0
width: 175px
display: flex
justify-content: center
img
margin-left: auto
margin-right: auto
.chuni-penguin-arm
transform-origin: 95% 10%
transform-origin: 90% 10%
position: absolute
top: 40%
.chuni-penguin-arm-left
left: 0%
animation: chuniPenguinArmLeft 1.5s infinite cubic-bezier(0.45, 0, 0.55, 1)
.chuni-penguin-arm-right
left: 70%
animation: chuniPenguinArmRight 1.5s infinite cubic-bezier(0.45, 0, 0.55, 1)
z-index: 0
&.chuni-penguin-arm-type-1
width: calc(85px * 0.75)
height: calc(160px * 0.75)
z-index: 2
&.chuni-penguin-arm-type-2
transform-origin: 40% 10%
z-index: 2
&.chuni-penguin-arm-left
left: 0%
transform: translate(-50%, 0)
animation: chuniPenguinArmLeft 1s infinite cubic-bezier(0.45, 0, 0.55, 1)
&.chuni-penguin-arm-type-2
left: 15%
&.chuni-penguin-arm-right
left: 72.5%
transform: translate(-50%, 0) scaleX(-1)
animation: chuniPenguinArmRight 1s infinite cubic-bezier(0.45, 0, 0.55, 1)
&.chuni-penguin-arm-type-2
left: 95%
.chuni-penguin-accessory
transform: translate(-50%, -50%)
@ -144,12 +194,22 @@
top: 50%
left: 50%
.chuni-penguin-item
z-index: 1
top: 25%
left: 0
&.chuni-penguin-item-left
transform: translate(-50%, -50%) rotate(-15deg)
&.chuni-penguin-item-right
transform: translate(-50%, -50%) scaleX(-1) rotate(15deg)
.chuni-penguin-eyes
top: 22.5%
.chuni-penguin-beak
top: 29.5%
.chuni-penguin-wear
top: 57.5%
top: 60%
.chuni-penguin-head
top: 7.5%
z-index: 10

View File

@ -4,25 +4,30 @@
const DDSreader = new DDS(ddsDB);
export var chuniLevel: number = 1
export var chuniLevel: string = ""
export var chuniName: string = "AquaDX"
export var chuniRating: number = 1.23
export var chuniNameplate: number = 1
export var chuniCharacter: number = 0
export var chuniTrophyName: string = "NEWCOMER"
export var chuniIsUserbox: boolean = false;
let ratingToString = (rating: number) => {
return rating.toFixed(2)
}
</script>
{#await DDSreader?.getFile(`nameplate:${chuniNameplate.toString().padStart(8, "0")}`) then nameplateURL}
{#await DDSreader?.getFile(`nameplate:${chuniNameplate.toString().padStart(8, "0")}`, `nameplate:00000001`) then nameplateURL}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div on:click class="chuni-nameplate" style:background={`url(${nameplateURL})`}>
{#await DDSreader?.getFile(`characterThumbnail:${chuniCharacter.toString().padStart(6, "0")}`) then characterThumbnailURL}
<div on:click class="chuni-nameplate" class:chuni-nameplate-clickable={chuniIsUserbox} style:background={`url(${nameplateURL})`}>
{#await DDSreader?.getFile(`characterThumbnail:${chuniCharacter.toString().padStart(6, "0")}`, `characterThumbnail:000000`) then characterThumbnailURL}
<img class="chuni-character" src={characterThumbnailURL} alt="Character">
{/await}
{#await DDSreader?.getFileFromSheet("surfboard:CHU_UI_title_rank_00_v10.dds", 5, 5 + (75 * 2), 595, 64) then trophyURL}
<div class="chuni-trophy">
<div class="chuni-trophy" title={chuniTrophyName}>
{chuniTrophyName}
<img src={trophyURL} class="chuni-trophy-bg" alt="Trophy">
</div>
<img src={trophyURL} class="chuni-trophy-bg" alt="Trophy" title={chuniTrophyName}>
{/await}
<div class="chuni-user-info">
<div class="chuni-user-name">
@ -39,7 +44,7 @@
<div class="chuni-user-rating">
RATING
<span class="chuni-user-rating-number">
{chuniRating}
{ratingToString(chuniRating)}
</span>
</div>
</div>
@ -53,11 +58,13 @@
position: relative
font-size: 16px
/* Overlap penguin avatar when put side to side */
z-index: 2
cursor: pointer
z-index: 1
&.chuni-nameplate-clickable
cursor: pointer
.chuni-trophy
width: 410px
width: 390px
height: 45px
background-position: center
background-size: cover
@ -74,14 +81,21 @@
font-family: sans-serif
font-weight: bold
overflow-x: hidden
white-space: nowrap
text-overflow: ellipsis
z-index: 1
text-shadow: 0 1px white
margin: 0 10px
img
width: 100%
height: 100%
position: absolute
z-index: -1
img.chuni-trophy-bg
width: 410px
height: 45px
position: absolute
top: 40px
right: 25px
z-index: -1
.chuni-character
position: absolute
@ -115,9 +129,11 @@
.chuni-user-name
flex: 1 0 65%
box-shadow: 0 1px 0 #ccc
white-space: nowrap
text-overflow: ellipsis
.chuni-user-level
font-size: 2em
font-size: 1.5em
margin-left: 10px
.chuni-user-name-text

View File

@ -16,7 +16,10 @@ 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 = ""
export const HAS_USERBOX_ASSETS = true
// Meow meow meow

View File

@ -182,7 +182,6 @@ export const EN_REF_USERBOX = {
'userbox.avatarItem': 'Avatar Item',
'userbox.avatarFront': 'Avatar Front',
'userbox.avatarBack': 'Avatar Back',
'userbox.preview.notice': 'To honor the copyright, we cannot host the images of the userbox items. However, if someone else is willing to provide the images, you can enter their URL here and it will be displayed.',
'userbox.preview.url': 'Image URL',
'userbox.error.nodata': 'Chuni data not found',
@ -196,14 +195,21 @@ export const EN_REF_USERBOX = {
'userbox.new.name': 'AquaBox',
'userbox.new.setup': 'Drag and drop your Chuni game folder (Lumi or newer) into the box below to display UserBoxes with their nameplate & avatar. All files are handled in-browser.',
'userbox.new.setup.notice': 'This tool assumes your files to be in "bin/option" and "data/A000".',
'userbox.new.setup.processing_file': 'Processing',
'userbox.new.setup.finalizing': 'Saving to internal storage',
'userbox.new.drop': 'Drop game folder here',
'userbox.new.switch.to_url': 'Switch to URL mode',
'userbox.new.switch.to_drop': 'Switch to drop mode',
'userbox.new.url_warning': 'Enter in the path to access Userbox assets. You are responsible for any results in this state. Please read the documentation. Don\'t expect support for this mode.',
'userbox.new.activate_first': 'Enable AquaBox (game files required)',
'userbox.new.activate_update': 'Update AquaBox (game files required)',
'userbox.new.activate': 'Use AquaBox',
'userbox.new.activate_desc': 'Enable displaying UserBoxes with their nameplate & avatar',
'userbox.new.error.invalidFolder': 'The folder you selected is invalid. Ensure that your game\'s version is Lumi or newer and that the "A001" option pack is present.'
'userbox.new.activate_profile': 'Use AquaBox on profiles',
'userbox.new.activate_profile_desc': 'Enable displaying UserBoxes with their nameplate & avatar on profile pages',
'userbox.new.error.invalidFolder': 'The folder you selected is invalid. Ensure that your game\'s version is Lumi or newer and that the "A000" option pack is present.',
'userbox.new.error.invalidUrl': 'The URL you inputted is invalid.'
}
export const EN_REF = { ...EN_REF_USER, ...EN_REF_Welcome, ...EN_REF_GENERAL,

View File

@ -266,6 +266,8 @@ export const USERBOX = {
get('/api/v2/game/chu3/user-box', {}),
setUserBox: (d: { field: string, value: number | string }) =>
post(`/api/v2/game/chu3/user-detail-set`, d),
getUserProfile: (username: string): Promise<UserBox> =>
get(`/api/v2/game/chu3/user-detail`, {username})
}
export const CARD = {

View File

@ -1,3 +1,6 @@
import useLocalStorage from "../hooks/useLocalStorage.svelte";
import { USERBOX_DEFAULT_URL } from "../config";
export default class DDSCache {
constructor(db: IDBDatabase | undefined) {
this.db = db;
@ -43,7 +46,13 @@ export default class DDSCache {
* @param path Image path
*/
getFromDatabase(path: string): Promise<Blob | null> {
return new Promise((resolve, reject) => {
return new Promise(async (resolve, reject) => {
if (this.userboxURL.value != "") {
let targetPath = path.replaceAll(":", "/");
let response = await fetch(`${this.userboxURL.value}/${targetPath}.chu`).then(b => b.blob()).catch(reject);
if (response)
return resolve(response);
};
if (!this.db)
return resolve(null);
let transaction = this.db.transaction(["dds"], "readonly");
@ -61,4 +70,5 @@ export default class DDSCache {
private urlCache: {scale: number, path: string, url: string}[] = [];
private db: IDBDatabase | undefined;
userboxURL = useLocalStorage("userboxURL", USERBOX_DEFAULT_URL);
}

View File

@ -173,7 +173,9 @@ export async function userboxFileProcess(folder: FileSystemEntry, progressUpdate
const dataFolder = await getDirectoryFromPath(folder, "data");
if (dataFolder)
await scanOptionFolder(dataFolder, progressUpdate);
useLocalStorage("userboxURL", "").value = "";
useLocalStorage("userboxNew", false).value = true;
useLocalStorage("userboxNewProfile", false).value = true;
location.reload();
return null

View File

@ -105,6 +105,41 @@
d!.user.rival = isAdd
}).catch(e => error = e.message).finally(() => isLoading = false)
}
/* Aquabox */
import { userboxFileProcess, ddsDB, initializeDb } from "../libs/userbox/userbox"
import ChuniPenguinComponent from "../components/settings/userbox/ChuniPenguin.svelte"
import ChuniUserplateComponent from "../components/settings/userbox/ChuniUserplate.svelte";
import {
type UserBox,
type UserItem,
} from "../libs/generalTypes";
import { USERBOX } from "../libs/sdk";
let USERBOX_ACTIVE = useLocalStorage("userboxNewProfile", false);
let USERBOX_INSTALLED = false;
let userbox: UserBox;
let allItems: Record<string, Record<string, { name: string }>> = {};
if (game == "chu3" && USERBOX_ACTIVE.value) {
indexedDB.databases().then(async (dbi) => {
let databaseExists = dbi.some(db => db.name == "userboxChusanDDS");
if (databaseExists) {
await initializeDb();
const profile = await USERBOX.getUserProfile(username).catch(_ => null)
if (!profile) return;
userbox = profile;
console.log(userbox);
allItems = await DATA.allItems('chu3').catch(_ => {
error = t("userbox.error.nodata")
}) as typeof allItems
USERBOX_INSTALLED = databaseExists;
}
})
}
</script>
<main id="user-home" class="content">
@ -132,6 +167,18 @@
</nav>
</div>
{#if USERBOX_ACTIVE.value && USERBOX_INSTALLED && game == "chu3"}
<div class="chuni-userbox-container">
<ChuniUserplateComponent chuniCharacter={userbox.characterId} chuniRating={d.user.rating / 100} chuniLevel={userbox.level.toString()}
chuniNameplate={userbox.nameplateId} chuniName={userbox.userName} chuniTrophyName={allItems.trophy[userbox.trophyId].name}></ChuniUserplateComponent>
<div class="chuni-penguin-container">
<ChuniPenguinComponent classPassthrough="chuni-penguin-float" chuniWear={userbox.avatarWear} chuniHead={userbox.avatarHead} chuniBack={userbox.avatarBack}
chuniFront={userbox.avatarFront} chuniFace={userbox.avatarFace} chuniItem={userbox.avatarItem}
chuniSkin={userbox.avatarSkin}></ChuniPenguinComponent>
</div>
</div>
{/if}
<div>
<h2>{titleText} {t('UserHome.Statistics')}</h2>
<div class="scoring-info">
@ -576,4 +623,18 @@
&:before
content: "+"
color: vars.$c-good
.chuni-userbox-container
display: flex
align-items: center
justify-content: center
.chuni-penguin-container
height: 256px
aspect-ratio: 1
position: relative
@media (max-width: 1000px)
.chuni-userbox-container
flex-wrap: wrap
</style>

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