104 Commits
Author SHA1 Message Date
asterisk727 c1d3f61b00 feat: no longer stub maimai magical 2026-09-25 12:49:14 -05:00
RandomEwok 55b47a3c02 fix: various bug fixes for wacca (#245) 2026-09-20 15:55:59 -04:00
Azalea 97743f55ef [+] Add pagination to maimai photo gallery (#244) 2026-09-16 09:35:07 +08:00
Azalea 11c5617abc [F] Fix 101% appearing as 100.9% (#243) 2026-09-11 01:55:59 +08:00
Raymond cf89e59af8 fix: don't set a timer on email confirmation tokens 2026-09-09 21:44:57 -04:00
TechnicalVessel 587557177e fix: assign ap+ lamp correctly for batch manual export 2026-09-09 21:06:01 -04:00
Azalea 6285bd3fd4 [-] Remove SDGA 1.65 nonce dedup 2026-08-12 05:43:59 +00:00
Raymond 7ba95f0b90 fix: do it correctly and not like a bum 2026-08-06 13:55:21 -04:00
Raymond 13aa48982f fix: shift ini down to lowercase for compat
i don't care anymore
2026-08-06 13:54:41 -04:00
Raymond 0a5c97d985 fix: shit 2026-08-05 20:28:32 -04:00
Raymond c1b6c3e414 fix: add trim 2026-08-05 19:03:52 -04:00
Azalea 5e6aaace8c [F] Fix SDGA 1.65 / SDGB 1.53 playlog, add nonce to UpsertUserAll (#238) 2026-08-06 04:44:41 +08:00
Azalea 3be81520ce [F] Fix import/export disparity, fix jackson3 export isSomething serialization bug, add tests (#237) 2026-08-06 03:21:20 +08:00
Azalea 30139cae8d [F] Fix export import missing fields and database errors (#236) 2026-08-04 12:29:03 +08:00
Azalea 96570a97ff [O] Ranking optimizations (#235) 2026-08-04 11:45:47 +08:00
Raymond 7eab79e48d fix: return fc+ on batch manual correctly 2026-08-03 16:08:38 -04:00
Azalea 05e613d667 [+] Delete account (#234) 2026-08-02 22:16:42 +08:00
Azalea d34aab37b2 [U] Java 21 -> Java 25 (#232) 2026-08-02 04:57:41 +00:00
Raymond 4aba8d8642 chore: 🐈‍⬛ 2026-07-28 23:24:15 -04:00
Raymond 0d70471bd6 revert: generate keychip on /keychip api
fixes regression causing 3,000+ users to not be assigned a keychip
2026-07-08 19:42:49 -04:00
Raymond 7ea5add407 fix: import/export fixes for chu3 & mai2 2026-07-08 12:00:02 -04:00
Raymond 41edddcaec fix: username insensitivity 2026-07-07 23:25:41 -04:00
Teio 1d36998967 [+] Mate (#231) 2026-07-07 01:58:52 -04:00
Raymond 3178c4c957 chore: revamp CONTRIBUTING.md 2026-07-04 19:59:18 -04:00
Raymond 48eb8a9c07 chore: update game-specific notes
Updated notes for Chunithm, Maimai DX, O.N.G.E.K.I, and Card Maker, including required patches and additional notes.
2026-07-04 19:36:38 -04:00
Raymond fcee9fb278 feat: chusan bs 2026-06-28 19:43:50 -04:00
Raymond 853abc8867 feat: use mai/chuni/ongeki-encoding when available (#230) 2026-06-20 15:57:20 -04:00
Raymond c099a8954e chore: 🐈‍⬛ 2026-06-18 14:06:39 -04:00
Raymond 96a894aa70 fix: add tls config prop (#228) 2026-06-16 19:54:56 -04:00
Raymond 0b3d52d31d fix: 60fps label 2026-06-15 22:10:07 -04:00
Azalea 27685087be Reapply "[F] Fix aimedb memory leak"
This reverts commit fec3480963.
2026-06-16 01:08:58 +00:00
Azalea fec3480963 Revert "[F] Fix aimedb memory leak"
This reverts commit 536734746b.
2026-06-16 00:38:25 +00:00
Azalea 536734746b [F] Fix aimedb memory leak 2026-06-15 21:15:25 +00:00
Raymond b352576358 fix: sort by correct rating value 2026-06-13 23:44:04 -04:00
Raymond f2ea015ef3 fix: don't not show non-aquanet users 2026-06-13 23:37:28 -04:00
Raymond 253c7a73d6 fix: don't show rank section at all if not available 2026-06-13 23:22:09 -04:00
Raymond e62115e6ac fix: improve ongeki ux, improve ranking backend (#227) 2026-06-14 11:12:37 +08:00
Raymond 08bf69c723 fix: display S+ and SS+ for chu3 on AquaNet 2026-06-13 21:20:31 -04:00
Raymond 3e2c198204 feat: reject invalid icf / v0.00 2026-06-11 22:54:44 -04:00
Raymond c3f75e6428 chore; UPDATE THE README 2026-06-08 22:46:56 -04:00
Raymond 4a6003ded3 chore: update chinese tls (ML) 2026-06-08 12:30:24 -04:00
Raymond b114adf940 fix: mistake in import 2026-06-08 12:15:40 -04:00
Raymond 7c31dfd4ab feat: add candidate display option to global game options 2026-06-08 12:11:30 -04:00
Raymond faabd45b7a fix: use correct class for exports 2026-06-08 12:07:35 -04:00
Raymond d7254913cd fix: [ongeki] use decimal instead of comma 2026-06-08 11:39:22 -04:00
RaymondandAzalea 237ba1464a feat: display ongeki refresh data on web ui
Co-authored-by: Azalea <noreply@aza.moe>
2026-06-07 21:05:09 -04:00
Raymond d5d50d687c feat: add general data list to chu3 2026-06-07 19:23:17 -04:00
crxmsxn 2c28758ae0 fix: allow more rfc5322 special characters in email (#225) 2026-06-07 00:29:05 -04:00
Raymond f025465f40 fix: mistake 2026-06-07 00:21:34 -04:00
Raymond 4b5eeadc2b feat: hide country toggle 2026-06-07 00:18:20 -04:00
Raymond 898939485c fix: enforce fullwidth 2026-06-07 00:00:11 -04:00
Raymond 593783866a fix: [ongeki] update constraints to cascade 2026-06-06 23:40:39 -04:00
Raymond 9b3871706f fix: [ongeki] more fixes, should work now 2026-06-06 23:30:19 -04:00
Raymond 5be6d8c80b fix: [ongeki] fix field matching in importer 2026-06-06 23:07:27 -04:00
Raymond f750cc6596 fix: [ongeki] mistake in importer 2026-06-06 22:54:06 -04:00
Raymond f67bbb822d fix: settings tab shouldn't show unplayed games 2026-06-06 22:49:26 -04:00
Raymond 819c058517 feat: [ongeki] username setting from webui 2026-06-06 22:43:41 -04:00
Raymond b2feb9a130 feat: [ongeki] data export/import 2026-06-06 22:19:12 -04:00
crxmsxn 6a10570fa6 feat: add chusan force unlock 2967 2999 ultima option (#224) 2026-05-31 14:23:49 -04:00
Yonle 6243ee7205 chore: [AquaNet] update README, move userbox config to .env, and add check before build (#223)
Signed-off-by: Yonle <yonle@proton.me>
2026-05-31 12:15:10 -04:00
Raymond 72c1f48bdb fix: rounding issue breaking encryption 2026-05-31 00:11:51 -04:00
raymonable 06eec89dfe fix: make minor rom versions always use the major rom key 2026-05-28 14:51:41 -04:00
Yonle 367a26c109 fix: adjust path example to avoid confusion (#222) 2026-05-28 14:43:12 -04:00
Yonle 2552568ac1 feat: improved announcement management, custom qr access code handler, fixes 2026-05-22 21:01:50 -04:00
Raymond 512a791aa4 chore: [🐈‍⬛] meow 2026-05-17 20:52:30 -04:00
Raymond 9bdc84418a fix: [mai2] change hash from md5 to sha256 2026-05-16 14:02:14 -04:00
Raymond 243a9b2ddc fix: 🛡️ don't leak extid & token in mai photos 2026-05-16 00:30:42 -04:00
Raymond f313f090f8 fix: default to user's last played game for rankings in navbar 2026-05-15 23:57:36 -04:00
Raymond 2e55247de0 fix: [chu3] import functional, improve error response for importing 2026-05-15 23:47:09 -04:00
Yonle 4895456bb0 fix: display error in a friendlier way on rankings
migrate from status overlays to displaying within container
2026-05-15 22:39:54 -04:00
Raymond aec0fdac8b fix: add new option for lv and bump correctly 2026-05-12 18:27:59 -04:00
Azalea eb600f1715 [F] Fix kaledx 2026-05-12 03:59:09 +00:00
Raymond bd6a641336 chore: bump data 2026-05-11 19:28:52 -04:00
Raymond aeebf069c2 fix: 🚑 fix CM title servers 2026-05-04 17:08:52 -04:00
Azalea a77111b2ad [F] Fix diva null? 2026-04-28 18:47:11 -04:00
Azalea 031f9e4663 Merge branch 'v1-dev' of https://github.com/MewoLab/AquaDX into v1-dev 2026-04-28 18:46:56 -04:00
Azalea 1181707b43 [WTF] Fix firefox favicon wtf 2026-04-24 12:36:44 +08:00
Azalea 927fa56862 [F] Fix svelte warnings & errors (#218) 2026-04-24 12:27:23 +08:00
Raymond d17073297c fix: 🚑 typo 2026-04-23 19:47:15 -04:00
Raymond 34a3de1209 fix: add new lv 2026-04-23 19:00:45 -04:00
raymonable 3a42435dc5 fix: increase maximum linked verses 2026-04-23 14:15:30 -04:00
Azalea 7865f7cfa7 [F] Fix svelte warnings & errors 2026-04-21 03:49:04 -04:00
Raymond e711921725 fix: 🚑 make data version fallback to v0 instead of v1 2026-04-16 21:29:49 -04:00
asterisk727 d0110d7f47 fix: 🚑 bad migration 2026-04-15 23:06:16 +09:00
asterisk727 26748719a0 chore: remove duplicate migration 2026-04-14 22:31:03 -04:00
asterisk727 b20fe6fa62 Merge branch 'v1-dev' of https://github.com/MewoLab/AquaDX into v1-dev 2026-04-14 22:28:03 -04:00
asterisk727 9aec50d901 chore: update chusan event db 2026-04-14 22:27:56 -04:00
asterisk727 c31c1a0524 chore: update chusan event db 2026-04-14 22:20:23 -04:00
Azalea c989c273c4 [F] Fix wacca: Path variable 2026-04-14 02:34:41 -04:00
Azalea c4c550b239 [F] Fix wacca: Should not compress 2026-04-14 02:27:41 -04:00
Azalea 22334de8a7 [F] Fix wacca trailing slash 2026-04-14 01:59:11 -04:00
Raymond 50cb970828 fix: logging typo 2026-04-13 20:56:53 -04:00
Azalea 80c088626e fix: 🚑 ketchup pattern 2026-04-13 20:42:22 -04:00
Raymond 2d5fb11dc0 fix: 🚑 illiteracy at it's finest 2026-04-13 18:45:49 -04:00
Raymond 708532d18b fix: 🚑 chusan json fix 2026-04-13 18:42:02 -04:00
Raymond dad66401cc fix: 🚑 aimedb hotfix 2026-04-13 17:33:57 -04:00
Raymond 1bc89c46f1 chore: make readme a little prettier 2026-04-13 14:56:35 -04:00
Raymond 1d3ddfc06c feat: Title Server Encryption (#216)
i still have a few things i'd like to do but i want to get this merged in and deployed asap
2026-04-13 14:54:07 -04:00
34cfa279dc feat: support multiple keychips per user (#215)
Co-authored-by: hykilpikonna <22280294+hykilpikonna@users.noreply.github.com>
Co-authored-by: asterisk727 <59166650+asterisk727@users.noreply.github.com>
Co-authored-by: Raymond <101374892+raymonable@users.noreply.github.com>
2026-04-13 00:21:50 -04:00
Raymond 2e21926189 User Options Web UI (#214) 2026-04-06 10:04:03 +08:00
Azalea 7a946708c3 [F] Fix data 2026-03-31 17:16:24 +00:00
Azalea fcf7c46cad [F] 2099 > 2029??? 2026-03-29 19:30:16 -04:00
Azalea 28063912ce Reapply "Remove events from db (#213)"
This reverts commit beacca12c3.
2026-03-29 19:29:12 -04:00
Azalea beacca12c3 Revert "Remove events from db (#213)"
This reverts commit fe0a2e738d.
2026-03-29 19:16:38 -04:00
635 changed files with 24042 additions and 82185 deletions
-5
View File
@@ -1,5 +0,0 @@
---
trigger: always_on
---
Make sure `./gradlew build` compiles before you say you're done.
+8
View File
@@ -0,0 +1,8 @@
*
!gradlew
!gradle/
!gradle/**
!build.gradle.kts
!settings.gradle.kts
!src/
!src/**
+2 -2
View File
@@ -12,9 +12,9 @@ jobs:
- uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: '21'
java-version: '25'
distribution: 'temurin'
- name: Build with Gradle
+2 -2
View File
@@ -17,9 +17,9 @@ jobs:
fetch-depth: '10'
- name: Set up JDK
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: '17'
java-version: '25'
distribution: 'temurin'
server-id: github
+3
View File
@@ -84,3 +84,6 @@ src/main/resources/meta/*/*.json
test-diff
htmlReport
docs/logs
# Sensitive keys
game_encryption.json
-10
View File
@@ -1,10 +0,0 @@
VITE_AQUA_HOST=https://aquadx.net/aqua
VITE_DATA_HOST=https://aquadx.net
VITE_AQUA_CONNECTION=aquadx.hydev.org
VITE_TURNSTILE_SITE_KEY=0x4AAAAAAASGA2KQEIelo9P9
VITE_DISCORD_INVITE=https://discord.gg/FNgveqFF7s
VITE_TELEGRAM_INVITE=https://t.me/+zBL4RZdyfvUzZGU1
VITE_QQ_INVITE=https://qm.qq.com/q/dpYmGoVHnG
VITE_GITHUB_REPOSITORY=https://github.com/MewoLab/AquaDX
+26
View File
@@ -0,0 +1,26 @@
# This should reference your AquaDX instance's :80 port.
# Sometimes issues with CORS may appear, the easiest fix is to use a reverse proxy like Caddy to put AquaDX on the /aqua path,
# and set this to match the /aqua path (see example below)
VITE_AQUA_HOST=https://aquadx.net/aqua
# Link to converted game data (ex: all_music.json, all_items.json, etc.)
# Profile pictures are also served through this path, you may want to disable profile pictures if using the primary instance for data fetching
VITE_DATA_HOST=https://aquadx.net
VITE_AQUA_CONNECTION=aquadx.hydev.org
VITE_TURNSTILE_SITE_KEY=0x4AAAAAAASGA2KQEIelo9P9
VITE_DISCORD_INVITE=https://discord.gg/FNgveqFF7s
VITE_TELEGRAM_INVITE=https://t.me/+zBL4RZdyfvUzZGU1
VITE_QQ_INVITE=https://qm.qq.com/q/dpYmGoVHnG
VITE_GITHUB_REPOSITORY=https://github.com/MewoLab/AquaDX
# 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
VITE_USERBOX_DEFAULT_URL=
# the following is used at some case where custom access code QR code URL has been done.
# example:
# https://aquadx.net/cards/#accesskey=....
VITE_ACCESSCODE_QKEY=accesskey
VITE_DEFAULT_GAME=mai2
+3 -1
View File
@@ -35,4 +35,6 @@ dist-ssr
public/chu3
# local env file
*.local
*.local
.env
+18 -5
View File
@@ -9,12 +9,25 @@ This project uses Svelte (NOT SvelteKit) + TypeScript + Sass, built using Vite.
### Running locally
First, you would need to install Node.js and bun.
Then, you would need to start your testing AquaDX server and configure the `aqua_host` in `src/libs/config.ts` to use your URL.
Please leave `data_host` unchanged if you're not sure what it is.
Finally, run:
You will need to have Node.js (or equivalent) and `pnpm` installed, Start by installing dependencies by running:
```
pnpm install
```
```shell
Then, you would need to start your testing AquaDX server and configure the `VITE_AQUA_HOST` in `.env` (copy `.env.example` file to `.env`) to use your URL.
Please leave `VITE_DATA_HOST` unchanged if you're not sure what it is.
Finally, run:
```
pnpm run dev
```
### Running locally (with bun)
Alternatively, If you prefer [Bun](https://bun.sh), You can do the same as above.
Start by configuring the `VITE_AQUA_HOST` in `.env` (copy `.env.example` file to `.env`) to use your URL.
and finally:
```
bun install
bun run dev
```
+9
View File
@@ -0,0 +1,9 @@
import { existsSync } from "fs"
if (!existsSync(".env")) {
console.error(" I did not see .env being here. You sure it was there?")
console.error(" Copy .env.example to .env, edit .env, and try again.\n")
process.exit(1)
}
process.exit(0)
Regular → Executable
View File
-1
View File
@@ -2,7 +2,6 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AquaNet</title>
+2 -2
View File
@@ -4,8 +4,8 @@
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"dev": "node buildcheck.js && vite",
"build": "node buildcheck.js && vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json",
"lint": "eslint . --ext ts,tsx,svelte --max-warnings 0 --fix"
+13 -4
View File
@@ -9,7 +9,8 @@
import Settings from "./pages/User/Settings.svelte";
import MaiPhoto from "./pages/MaiPhoto.svelte";
import { pfp, tooltip } from "./libs/ui"
import { ANNOUNCEMENT } from "./libs/config";
import { ANNOUNCEMENT } from "./libs/announcement";
import { DEFAULT_GAME } from "./libs/config"
import { t } from "./libs/i18n";
import Transfer from "./pages/Transfer/Transfer.svelte";
import { link } from "d3";
@@ -34,12 +35,19 @@
let me: AquaNetUser
let playedMai = false
let recentGame: string = DEFAULT_GAME;
if (USER.isLoggedIn())
{
USER.me().then(m => {
me = m
CARD.userGames(me.username).then(game => {
playedMai = !!game.mai2
recentGame = Object.keys(game)
.filter(k => !!game[k])
.sort((a, b) => {
return (new Date(game[b].lastLogin)) - (new Date(game[a].lastLogin))
})[0] ?? "mai2"
})
}).catch(e => console.error(e))
}
@@ -53,15 +61,16 @@
<span>AquaNet</span>
</a>
{/if}
{#if ANNOUNCEMENT}
{#if $ANNOUNCEMENT}
<div class="announcement">
<strong>{t('navigation.notice')}</strong>: {ANNOUNCEMENT}
<strong>{t('navigation.notice')}</strong>: {$ANNOUNCEMENT}
</div>
{/if}
<a href="/home">{t('navigation.home').toLowerCase()}</a>
<!-- <div on:click={() => alert("Coming soon™")} on:keydown={e => e.key === "Enter" && alert("Coming soon™")}
role="button" tabindex="0">{t('navigation.maps').toLowerCase()}</div> -->
<a href="/ranking">{t('navigation.rankings').toLowerCase()}</a>
<!-- kill me -->
<a href={`/ranking/${recentGame}`}>{t('navigation.rankings').toLowerCase()}</a>
{#if playedMai}
<a href="/pictures">photo</a>
{/if}
+1 -1
View File
@@ -27,7 +27,7 @@
display: flex
gap: 1rem
div
a
&.active
color: vars.$c-main
</style>
+7 -2
View File
@@ -9,8 +9,13 @@
let editing = false
let inputPage: number
function focus(node: HTMLInputElement) {
node.focus()
}
function updatePage(newPage: number) {
if (newPage > 0 && newPage <= totalPages) dispatch('updatePage', newPage)
const p = Math.floor(Number(newPage))
if (!isNaN(p) && p > 0 && p <= totalPages) dispatch('updatePage', p)
}
function startEditing() {
@@ -33,7 +38,7 @@
<button on:click={() => updatePage(page - 1)} disabled={page <= 1}>Previous</button>
{#if editing}
<input bind:value={inputPage} on:blur={finishEditing} on:keydown={handleKeydown} min="1" max={totalPages} autofocus/>
<input type="number" bind:value={inputPage} on:blur={finishEditing} on:keydown={handleKeydown} min="1" max={totalPages} use:focus/>
{:else}
<span on:click={startEditing} role="button" tabindex="0" on:keydown={(e) => e.key === 'Enter' && startEditing()}>
Page {page} of {totalPages}
+55 -10
View File
@@ -16,19 +16,25 @@
if (top) split = split?.toSorted((a, b) => b.score - a.score).slice(0, top)
if (split) console.log("Split", split)
let mustResize = split && split.length > 12
</script>
{#if split && comp}
<div>
<h2>{title}</h2>
<div class="rating-composition">
{#each split as p}
<div>
<RatingCompSong {p} {game}/>
</div>
{/each}
{#if split && split.length > 0 && comp}
<div class="rating-composition-container" class:must-resize={mustResize}>
{#if mustResize}
<input type="checkbox">
{/if}
<h2>{title}</h2>
<div class="rating-composition">
{#each split as p}
<div>
<RatingCompSong {p} {game}/>
</div>
{/each}
</div>
</div>
</div>
{/if}
<style lang="sass">
@@ -39,4 +45,43 @@
// 3 columns
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr))
gap: vars.$gap
overflow: hidden
transition: max-height 250ms
.rating-composition-container
position: relative
input
width: 100%
height: 100%
position: absolute
z-index: 10
border: none
border-radius: 0
opacity: 0
transition: opacity 250ms
.rating-composition-container.must-resize
input
& ~ .rating-composition
max-height: 250px
& ~ h2::after
content: "▾"
margin-left: 0.5em
&:checked ~ h2::after
content: "▴"
&:checked
opacity: 0
&:checked ~ .rating-composition
max-height: 3000px
opacity: 1
background: linear-gradient(#0000 250px, rgb(28.35, 28.35, 28.35) 100%) /* for some reason the color.adjust doesn't work here so whatever */
</style>
+4 -4
View File
@@ -27,15 +27,15 @@
})
})
function mouseOver(event: MouseEvent) {
function mouseOver(event: Event) {
console.log('over')
isHovered = true
updatePosition(event)
updatePosition(event as MouseEvent)
}
function mouseMove(event: MouseEvent) {
function mouseMove(event: Event) {
console.log('move')
updatePosition(event)
updatePosition(event as MouseEvent)
}
function updatePosition(event: MouseEvent) {
+1 -1
View File
@@ -14,7 +14,7 @@
let error = ""
setLoading(true)
GAME.userSummary(username, game).then(d => data = d).catch(e => error = e).finally(_ => setLoading(false))
GAME.userSummary(username, game).then(d => data = d).catch(e => error = e).finally(() => setLoading(false))
</script>
{#if !data}
@@ -16,7 +16,7 @@
let changed: string[] = [];
let symbols: Record<number, number> = {};
let allItems: Record<string, Record<string, { name: string }>> = {}
let submitting: string | undefined | null;
let submitting: string | undefined | null;
let settings: Record<string, GameOption> = {};
let existingUrl = "";
@@ -101,12 +101,13 @@
{#if linkedVerseAvailable}
<GameSettingFields game="chu3-linked-verse" />
{#if settings["chusanLvDifficulty"]}
<a target="_blank" href="https://silentblue.remywiki.com/CHUNITHM:Linked_VERSE">{t(`userbox.lv.help`)}</a>
<div class="field">
<label for={`chusanLvDifficulty`}>{ts(`userbox.lv.difficulty`)}</label>
<div>
<select bind:value={settings["chusanLvDifficulty"].value} id={`chusanLvDifficulty`} on:change={() => {changed = [...changed, `chusanLvDifficulty`];}}>
{#each {length: 5}, i}
<option value={i + 1}>{t(`userbox.lv.difficulty.${i + 1}` as keyof typeof EN_REF)}</option>
{#each {length: 6}, i}
<option value={i}>{t(`userbox.lv.difficulty.${i}` as keyof typeof EN_REF)}</option>
{/each}
</select>
{#if changed.includes(`chusanLvDifficulty`)}
@@ -3,10 +3,11 @@
<script lang="ts">
import {
type AquaNetUser,
type GameUserOption,
type UserBox,
type UserItem,
} from "../../libs/generalTypes";
import { DATA, USER, USERBOX, GAME } from "../../libs/sdk";
import { DATA, USER, USERBOX, GAME, SETTING } from "../../libs/sdk";
import { t, ts } from "../../libs/i18n";
import { FADE_IN, FADE_OUT, USERBOX_DEFAULT_URL } from "../../libs/config";
import { fade, slide } from "svelte/transition";
@@ -23,11 +24,15 @@
import { DDS } from "../../libs/userbox/dds";
import ChuniMatchingSettings from "./ChuniMatchingSettings.svelte";
import InputField from "../ui/InputField.svelte";
import UserOptionSlider from "./UserOptionSlider.svelte";
let user: AquaNetUser
let [loading, error, submitting, preview] = [true, "", "", ""]
let changed: string[] = [];
let userOptions: GameUserOption = {};
let userIsUsingPreset = false;
// Available (unlocked) options for each kind of item
// In allItems: 'namePlate', 'frame', 'trophy', 'mapIcon', 'systemVoice', 'avatarAccessory'
let allItems: Record<string, Record<string, { name: string }>> = {}
@@ -89,6 +94,13 @@
console.log("All items", allItems)
console.log("Userbox", userbox)
userOptions = await SETTING.optionGet('chu3').catch(_ => {
loading = false
error = t("userbox.error.nodata")
}) as GameUserOption
if (userOptions["optionSet"] != 3)
userIsUsingPreset = true;
loading = false
}
@@ -167,7 +179,7 @@
try {
data = await GAME.export('chu3')
}
catch (e) {
catch (e: any) {
error = e.message
submitting = ""
return
@@ -312,6 +324,39 @@
<InputField bind:field={userNameField}
callback={() => USERBOX.setUserBox({ field: "userName", value: userNameField.value })}/>
<!-- User Options -->
{#if userIsUsingPreset}
<blockquote class="info">
{t('settings.options.all.preset-warning')}
</blockquote>
{/if}
<div class="fields-ranges">
<UserOptionSlider
game="chu3" type="headphone"
minValue={0} maxValue={60}
defaultValue={userOptions["headphone"]}
getTextFunction={(value: number) => `${((value / 60) * 100).toFixed(0)}%`}
/>
<UserOptionSlider
game="chu3" type="trackSkip"
minValue={0} maxValue={7}
defaultValue={userOptions["trackSkip"]}
getTextFunction={(value: number) => [t('settings.options.all.none'), `S`, `S+`, `SS`, `SS+`, `SSS`, `SSS+`, t('settings.options.all.personal-best')][value]}
/>
<UserOptionSlider
game="chu3" type="speed"
minValue={0} maxValue={61}
defaultValue={userOptions["speed"]}
getTextFunction={(value: number) => value >= 61 ? "SONIC" : value < 55 ? `${(value / 4) + 1}` : `${value - 40}`}
/>
<UserOptionSlider
game="chu3" type="speed_120"
minValue={0} maxValue={61}
defaultValue={userOptions["speed_120"]}
getTextFunction={(value: number) => value >= 61 ? "SONIC" : value < 55 ? `${(value / 4) + 1}` : `${value - 40}`}
/>
</div>
</div>
<h2>{t("userbox.header.userbox")}</h2>
{#if !USERBOX_ENABLED.value || !USERBOX_INSTALLED}
@@ -394,6 +439,7 @@
{/each}
</div>
{/if}
{#if USERBOX_INSTALLED}
<!-- god this is a mess but idgaf atp -->
<div class="field boolean" style:margin-top="1em">
@@ -435,7 +481,7 @@
<span>{USERBOX_SETUP_MODE ? t('userbox.new.url_warning') : USERBOX_SETUP_TEXT}</span>
<div class="actions">
{#if USERBOX_SETUP_MODE}
<input type="text" on:keyup={e => {if (e.key == "Enter") { userboxHandleInput((e.target as HTMLInputElement).value) } else g(e.currentTarget.value)}} 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}
{#if USERBOX_PROGRESS != 0}
<div class="progress">
@@ -512,36 +558,6 @@ p.notice
height: 100%
opacity: 0
.preview
margin-top: 32px
display: flex
flex-wrap: wrap
justify-content: space-between
gap: 32px
> div
position: relative
width: 100px
height: 100px
overflow: hidden
background: vars.$ov-lighter
border-radius: vars.$border-radius
span
position: absolute
bottom: 0
width: 100%
text-align: center
z-index: 10
background: rgba(0, 0, 0, 0.2)
backdrop-filter: blur(2px)
img
position: absolute
inset: 0
width: 100%
height: 100%
object-fit: contain
.fields
display: flex
@@ -594,6 +610,18 @@ p.notice
.desc
opacity: 0.6
.fields-ranges
display: flex
flex-wrap: wrap
margin: 0.5rem 0
gap: 0 1rem
justify-content: center
:global(.field)
flex: calc(50% - 1rem)
width: 50%
/* AquaBox */
.chuni-userbox-row
@@ -1,32 +1,67 @@
<script>
<script lang="ts">
import { fade } from "svelte/transition";
import { FADE_IN, FADE_OUT } from "../../libs/config";
import { t, ts } from "../../libs/i18n";
import useLocalStorage from "../../libs/hooks/useLocalStorage.svelte";
import RegionSelector from "./RegionSelector.svelte";
import { USER } from "../../libs/sdk";
import type { AquaNetUser } from "../../libs/generalTypes";
import StatusOverlays from "../StatusOverlays.svelte";
const rounding = useLocalStorage("rounding", true);
let me: AquaNetUser;
let submitting = "";
let error = "";
let loading = true;
USER.me().then((m) => {
me = m;
loading = false;
}).catch(e => error = e.message)
function submit(field: string, value: string) {
if (submitting) return
submitting = field
USER.setting(field, value).then(() => {
}).catch(e => error = e.message).finally(() => submitting = "")
}
</script>
<div class="fields">
<blockquote class="info">
{ts("settings.siteNotice")}
</blockquote>
<div class="field">
<div class="bool">
<input id="rounding" type="checkbox" bind:checked={rounding.value}/>
<label for="rounding">
<span class="name">{ts(`settings.fields.rounding.name`)}</span>
<span class="desc">{ts(`settings.fields.rounding.desc`)}</span>
</label>
{#if !loading}
<div class="fields">
<blockquote class="info">
{ts("settings.siteNotice")}
</blockquote>
<div class="field">
<div class="bool">
<input id="rounding" type="checkbox" bind:checked={rounding.value}/>
<label for="rounding">
<span class="name">{ts(`settings.fields.rounding.name`)}</span>
<span class="desc">{ts(`settings.fields.rounding.desc`)}</span>
</label>
</div>
</div>
<div class="field m-t">
<div class="bool">
<input id="displayCandidates" type="checkbox" bind:checked={me.displayCandidates}
on:change={() => submit('displayCandidates', me.displayCandidates.toString())}/>
<label for="displayCandidates">
<span class="name">{ts(`settings.fields.displayCandidates.name`)}</span>
<span class="desc">{ts(`settings.fields.displayCandidates.desc`)}</span>
</label>
</div>
</div>
<div class="divider"></div>
<blockquote class="info">
{ts("settings.regionNotice")}
</blockquote>
<RegionSelector/>
</div>
<div class="divider"></div>
<blockquote class="info">
{ts("settings.regionNotice")}
</blockquote>
<RegionSelector/>
</div>
{/if}
<StatusOverlays {error} {loading} />
<style lang="sass">
@use "../../vars"
@@ -4,37 +4,36 @@
import { t } from "../../libs/i18n.js";
import Icon from "@iconify/svelte";
import StatusOverlays from "../StatusOverlays.svelte";
import { GAME, USER } from "../../libs/sdk";
import { GAME, SETTING, USER } from "../../libs/sdk";
import GameSettingFields from "./GameSettingFields.svelte";
import { download } from "../../libs/ui";
import UserOptionSlider from "./UserOptionSlider.svelte";
import type { GameUserOption } from "../../libs/generalTypes";
import InputField from "../ui/InputField.svelte";
const profileFields = [
['name', t('settings.mai2.name')],
]
let userNameField: any
let error: string
let submitting = ""
let values = Array(profileFields.length).fill('')
let loading = true
let changed: string[] = []
USER.me().then(me =>
GAME.userSummary(me.username, 'mai2').then(({name}) => {
values = [name]
let userOptions: GameUserOption = {}
let userIsUsingPreset = false
USER.me().then(me =>
GAME.userSummary(me.username, 'mai2').then(async ({name}) => {
userNameField = {key: "gameUsername", value: name, type: "String"}
userOptions = await SETTING.optionGet('mai2').catch(_ => {
error = t("userbox.error.nodata")
loading = true
}) as GameUserOption
if (userOptions["optionKind"] != 3)
userIsUsingPreset = true
loading = false
}).catch(e => error = e.message));
function submit(field: string, value: string) {
if (submitting) return
submitting = field
switch (field) {
case 'name':
GAME.changeName('mai2', value).then(({newName}) => {
changed = changed.filter(c => c !== field)
values = [newName]
}).catch(e => error = e.message).finally(() => submitting = "")
break
}
}
async function exportBatchManual() {
submitting = "batchExport"
const DIFFICULTY_MAP: Record<number, string> = {
@@ -156,12 +155,12 @@
let lamp = null;
if (score.isAllPerfect) {
lamp = "ALL PERFECT";
if (score.percent == 101.0) {
if (percent >= 101.0) {
lamp = "ALL PERFECT+";
}
} else if (score.isFullCombo) {
lamp = "FULL COMBO";
if (good == 0 && great == 0) {
if (!good) {
lamp = "FULL COMBO+";
}
} else if (score.isClear) {
@@ -195,39 +194,72 @@
if(data.userData.classRank in CLASS_MAP){
output.classes["matchingClass"] = CLASS_MAP[data.userData.classRank]
}
download(JSON.stringify(output), `AquaDX_maimai2_BatchManualExport_${values[0]}.json`)
download(JSON.stringify(output), `AquaDX_maimai2_BatchManualExport_${userNameField.value}.json`)
submitting = ""
}
function exportData() {
submitting = "export"
GAME.export('mai2')
.then(data => download(JSON.stringify(data), `AquaDX_maimai2_export_${values[0]}.json`))
.then(data => download(JSON.stringify(data), `AquaDX_maimai2_export_${userNameField.value}.json`))
.catch(e => error = e.message)
.finally(() => submitting = "")
}
</script>
{#if !loading}
<div class="fields">
{#each profileFields as [field, name], i (field)}
<div class="field">
<label for={field}>{name}</label>
<div>
<input id={field} type="text"
bind:value={values[i]} on:input={() => changed = [...changed, field]}
placeholder={field === 'password' ? t('settings.profile.unchanged') : t('settings.profile.unset')}/>
{#if changed.includes(field) && values[i]}
<button transition:slide={{axis: 'x'}} on:click={() => submit(field, values[i])}>
{#if submitting === field}
<Icon icon="line-md:loading-twotone-loop"/>
{:else}
{t('settings.profile.save')}
{/if}
</button>
{/if}
</div>
</div>
{/each}
<InputField bind:field={userNameField}
callback={() => GAME.changeName('mai2', userNameField.value)}/>
{#if userIsUsingPreset}
<blockquote class="info">
{t('settings.options.all.preset-warning')}
</blockquote>
{/if}
<div class="fields-ranges">
<!-- TODO: determine min & max -->
<UserOptionSlider
game="mai2" type="noteSpeed"
minValue={0} maxValue={37}
defaultValue={userOptions["noteSpeed"]}
getTextFunction={(value: number) => value >= 37 ? "SONIC" : `${(value / 4) + 1}`}
/>
<UserOptionSlider
game="mai2" type="touchSpeed"
minValue={0} maxValue={37}
defaultValue={userOptions["touchSpeed"]}
getTextFunction={(value: number) => value >= 37 ? "SONIC" : `${(value / 4) + 1}`}
/>
<UserOptionSlider
game="mai2" type="slideSpeed"
minValue={0} maxValue={20}
defaultValue={userOptions["slideSpeed"]}
getTextFunction={(value: number) => value == 10 ? "Normal" : `${value > 10 ? "Late" : "Fast"} ${(Math.abs(value - 10) / 10).toFixed(1)}`}
/>
<UserOptionSlider
game="mai2" type="headPhoneVolume"
minValue={0} maxValue={19}
defaultValue={userOptions["headPhoneVolume"]}
getTextFunction={(value: number) => `${((value / 19) * 100).toFixed(0)}%`}
/>
<UserOptionSlider
game="mai2" type="trackSkip"
minValue={0} maxValue={11}
defaultValue={userOptions["trackSkip"]}
getTextFunction={(value: number) => [
t('settings.options.all.none'),
t('settings.options.all.push'),
`S`, `SS`, `SSS`,
t('settings.options.all.personal-best'),
t('settings.options.all.rival-score'),
`${t('settings.options.all.life')} 300`,
`${t('settings.options.all.life')} 100`,
`${t('settings.options.all.life')} 50`,
`${t('settings.options.all.life')} 10`,
`${t('settings.options.all.life')} 1`
][value]}
/>
</div>
<GameSettingFields game="mai2"/>
<button class="exportButton" on:click={exportData}>
<Icon icon="bxs:file-export"/>
@@ -238,8 +270,9 @@
{t('settings.batchManualExport')}
</button>
</div>
{/if}
<StatusOverlays {error} loading={!values[0] || !!submitting}/>
<StatusOverlays {error} loading={!userNameField || !!submitting || loading}/>
<style lang="sass">
.fields
@@ -263,6 +296,17 @@
> input
flex: 1
.fields-ranges
display: flex
flex-wrap: wrap
margin: 0.5rem 0
gap: 0 1rem
justify-content: center
:global(.field)
flex: calc(50% - 1rem)
width: 50%
.exportButton
display: flex
justify-content: center
@@ -1,9 +1,55 @@
<script>
<script lang="ts">
import { fade } from "svelte/transition";
import { FADE_IN, FADE_OUT } from "../../libs/config";
import GameSettingFields from "./GameSettingFields.svelte";
import { GAME, SETTING, USER } from "../../libs/sdk";
import { download } from "../../libs/ui";
import InputField from "../ui/InputField.svelte";
import Icon from "@iconify/svelte";
import { t } from "../../libs/i18n";
import StatusOverlays from "../StatusOverlays.svelte";
let userNameField: any
let submitting = "";
let error: string
let loading = true;
USER.me().then(me =>
GAME.userSummary(me.username, 'ongeki').then(async ({name}) => {
userNameField = {key: "gameUsername", value: name, type: "String"}
loading = false;
}))
function exportData() {
submitting = "export"
GAME.export('ongeki')
.then(data => download(JSON.stringify(data), `AquaDX_ongeki_export_${userNameField.value}.json`))
.catch(e => error = e.message)
.finally(() => submitting = "")
}
</script>
<div>
<GameSettingFields game="ongeki"/>
</div>
{#if !loading}
<div>
<div class="fields">
<InputField bind:field={userNameField}
callback={() => SETTING.detailSet('ongeki', 'userName', userNameField.value)}/>
<GameSettingFields game="ongeki"/>
<button class="exportButton" on:click={exportData}>
<Icon icon="bxs:file-export"/>
{t('settings.export')}
</button>
</div>
</div>
{/if}
<StatusOverlays {error} loading={!userNameField || !!submitting || loading}/>
<style lang="sass">
.fields
display: flex
flex-direction: column
gap: 12px
</style>
@@ -3,8 +3,8 @@
import { ts } from "../../libs/i18n";
import StatusOverlays from "../StatusOverlays.svelte";
let regionId = $state(0);
let submitting = ""
let error: string;
let submitting = $state("")
let error: string = $state("");
const prefectures = ["None","Aichi","Aomori","Akita","Ishikawa","Ibaraki","Iwate","Ehime","Oita","Osaka","Okayama","Okinawa","Kagawa","Kagoshima","Kanagawa","Gifu","Kyoto","Kumamoto","Gunma","Kochi","Saitama","Saga","Shiga","Shizuoka","Shimane","Chiba","Tokyo","Tokushima","Tochigi","Tottori","Toyama","Nagasaki","Nagano","Nara","Niigata","Hyogo","Hiroshima","Fukui","Fukuoka","Fukushima","Hokkaido","Mie","Miyagi","Miyazaki","Yamagata","Yamaguchi","Yamanashi","Wakayama"]
@@ -31,7 +31,7 @@
<span class="name">{ts(`settings.regionSelector.title`)}</span>
<span class="desc">{ts(`settings.regionSelector.desc`)}</span>
</label>
<select bind:value={regionId} on:change={saveNewRegion}>
<select bind:value={regionId} onchange={saveNewRegion}>
<option value={0} disabled selected>{ts("settings.regionSelector.select")}</option>
{#each prefectures.slice(1) as prefecture, index}
<option value={index}>{prefecture}</option>
@@ -0,0 +1,86 @@
<script lang="ts">
import { t } from "../../libs/i18n";
import { EN_REF } from "../../libs/i18n/en_ref";
import type { GameName } from "../../libs/scoring";
import { SETTING } from "../../libs/sdk";
export let getTextFunction: (value: number) => string
export let defaultValue: number = 0
export let minValue: number = 0
export let maxValue: number = 0
export let type: string = "speed";
export let game: GameName;
export let disabled: boolean = false;
let value = defaultValue;
let rationalizedValue = defaultValue / maxValue;
async function submit() {
await SETTING.optionSet(game, type, value);
}
</script>
<div class="field range">
<div class="range-label">
{t(`settings.options.${game}.${type}` as keyof typeof EN_REF)} <!-- TODO: i18n -->
</div>
<div class="range-value">
<input
type="range" min={minValue} {disabled}
on:change={submit} max={maxValue} bind:value={value}
>
<div>
{getTextFunction(value)}
</div>
</div>
</div>
<style lang="sass">
@use "../../vars"
.field
width: 100%
.range-value
display: flex
align-items: center
div
min-width: 4rem
white-space: nowrap
display: flex
justify-content: center
input
flex: 100%
input
-webkit-appearance: none
width: 100%
height: 5px !important
outline: none
transition: opacity 200ms
padding: 4px
border-radius: 4px
opacity: 80%
background: linear-gradient(to right, transparent 0%, #caf1 10%, #caf1 90%, transparent 100%)
margin: 16px 0
&:hover
opacity: 100%
*:focus
outline: none
input::-webkit-slider-thumb
-webkit-appearance: none
appearance: none
width: 8px
height: 30px
background: vars.$c-main
border-radius: 8px
cursor: pointer
box-shadow: 0 0 2px 0 vars.$c-main
</style>
@@ -1,10 +1,10 @@
<script lang="ts">
import { slide, fade } from "svelte/transition";
import type { AquaNetUser } from "../../libs/generalTypes";
import { CARD, USER } from "../../libs/sdk";
import type { AquaNetUser, ConfirmProps } from "../../libs/generalTypes";
import { CARD, GAME, USER, type ExportGameName } from "../../libs/sdk";
import StatusOverlays from "../../components/StatusOverlays.svelte";
import Icon from "@iconify/svelte";
import { pfp } from "../../libs/ui";
import { download, pfp } from "../../libs/ui";
import { t, ts } from "../../libs/i18n";
import Cropper from "svelte-easy-crop";
@@ -12,6 +12,7 @@
let error: string;
let submitting = ""
let loading = false;
let confirm: ConfirmProps | null = null;
const profileFields = [
[ 'displayName', t('settings.profile.name') ],
@@ -97,11 +98,48 @@
location.href = "/";
}
function requestAccountDeletion() {
if (submitting) return;
confirm = {
title: t('settings.profile.delete-confirm-title'),
message: t('settings.profile.delete-confirm-message'),
dangerous: true,
cancel: () => confirm = null,
confirm: deleteAccount,
};
}
async function deleteAccount() {
if (submitting) return;
submitting = "deleteAccount";
try {
const gameSummary = await CARD.userGames(me.username);
const games = (['mai2', 'chu3', 'ongeki', 'wacca', 'diva'] as ExportGameName[])
.filter(game => gameSummary[game] !== null);
const exports = await Promise.all(games.map(async game => ({
game,
data: await GAME.export(game),
})));
exports.forEach(({game, data}) => download(
JSON.stringify(data, null, 2),
`AquaDX_${game}_export_${me.username}.json`,
));
await USER.deleteAccount();
localStorage.removeItem("token");
location.href = "/";
} catch (e) {
error = (e as Error).message;
submitting = "";
}
}
const passwordAction = (node: HTMLInputElement, whether: boolean) => {
if (whether) node.type = 'password'
}
</script>
<StatusOverlays {error} loading={!!submitting || loading} />
<StatusOverlays bind:confirm={confirm} {error} loading={!!submitting || loading} />
{#if !submitting && !error && me}
<div>
<div class="fields">
@@ -156,11 +194,31 @@
</label>
</div>
</div>
<div class="field m-t">
<div class="bool">
<input id="hideCountry" type="checkbox" bind:checked={me.hideCountry}
on:change={() => submit('hideCountry', me.hideCountry.toString())}/>
<label for="hideCountry">
<span class="name">{ts(`settings.fields.hideCountry.name`)}</span>
<span class="desc">{ts(`settings.fields.hideCountry.desc`)}</span>
</label>
</div>
</div>
<div class="field m-t">
<div>
<button on:click={logOut}>{ts(`settings.profile.logout`)}</button>
</div>
</div>
<div class="field danger-zone m-t">
<span class="name">{t('settings.profile.delete-title')}</span>
<span class="desc">{t('settings.profile.delete-description')}</span>
<div>
<button class="error" on:click={requestAccountDeletion}>
<Icon icon="tabler:trash-x-filled" />
{t('settings.profile.delete')}
</button>
</div>
</div>
</div>
</div>
{/if}
@@ -224,6 +282,19 @@
object-fit: cover
aspect-ratio: 1
.danger-zone
margin-top: 1.5rem
padding-top: 1.5rem
border-top: 1px solid vars.$ov-lighter
.desc
opacity: 0.6
button
display: flex
align-items: center
gap: 0.5rem
.cropper-container
-1
View File
@@ -4,7 +4,6 @@
import { DISCORD_INVITE } from "../../libs/config";
export let error: string;
export let expected: boolean = false;
</script>
<div class="overlay" transition:fade>
+35
View File
@@ -0,0 +1,35 @@
// card handler. for dealing user QR scan.
// this will assumes that the link of the generated QR code in [access code]
// page has been altered by modder to link to custom
import { ACCESSCODE_QKEY } from "./config";
const query = new URLSearchParams(location.hash.slice(1));
const lsKey = "acurl"
export const url_has = (): boolean => query.has(ACCESSCODE_QKEY);
export const get = (): string => localStorage.getItem(lsKey) || "";
export const has = (): boolean => !!localStorage.getItem(lsKey);
export const clear = (): void => localStorage.removeItem(lsKey);
export function check() {
if (!url_has()) return
console.log("Found access code in URL. Saving.")
localStorage.setItem(lsKey, query.get(ACCESSCODE_QKEY) || "")
}
export function preview() {
const card = get()
const masked =
card.slice(0, 4) +
"x".repeat(card.length - 4);
return masked.match(/.{1,4}/g)?.join(" ");
}
export function previewR() {
const card = get()
return card.match(/.{1,4}/g)?.join(" ");
}
+11
View File
@@ -0,0 +1,11 @@
import { writable } from "svelte/store"
export const ANNOUNCEMENT = writable('')
// This task must not bother the existing one. Keep it async in the background instead
void (async () => {
let r = await fetch("/announcement.txt")
if (!r.ok) return;
ANNOUNCEMENT.set(await r.text())
})()
+5 -4
View File
@@ -17,14 +17,15 @@ export const FADE_OUT = { duration: 200 }
export const FADE_IN = { delay: 400 }
export const DEFAULT_PFP = '/assets/imgs/no_profile.png'
export const ANNOUNCEMENT = '' // If set, will add an announcement to the top bar. Keep it short.
// Key name for access key scanned from QR code
export const ACCESSCODE_QKEY = import.meta.env.VITE_ACCESSCODE_QKEY || "accesskey"
// 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 USERBOX_DEFAULT_URL = import.meta.env.VITE_USERBOX_DEFAULT_URL || ""
export const HAS_USERBOX_ASSETS = true
export const DEFAULT_GAME = import.meta.env.VITE_DEFAULT_GAME || "mai2"
// Meow meow meow
// Matching servers
+8 -2
View File
@@ -31,6 +31,9 @@ export interface AquaNetUser {
computedName: string,
password: string,
optOutOfLeaderboard: boolean,
canModifyKeychips: boolean,
hideCountry: boolean,
displayCandidates: boolean
}
export interface CardSummaryGame {
@@ -90,10 +93,11 @@ export interface GenericGameSummary {
name: string
iconId: number
aquaUser?: AquaNetUser
serverRank: number
serverRank: string
accuracy: number
rating: number
ratingHighest: number
ratingHighest: number,
ratingNotGeneric: boolean,
ranks: RankCount[]
detailedRanks: { [key: number]: { [key: string]: number } }
maxCombo: number
@@ -137,6 +141,8 @@ export interface GameOption {
changed?: boolean
}
export type GameUserOption = Record<string, number>
export interface UserItem { itemKind: number, itemId: number, stock: number }
export interface UserBox {
userName: string,
+71 -15
View File
@@ -2,6 +2,8 @@ export const EN_REF_USER = {
'UserHome.ServerRank': 'Server Rank',
'UserHome.DXRating': 'DX Rating',
'UserHome.Rating': 'Rating',
'UserHome.HighestDXRating': 'Peak DX Rating',
'UserHome.HighestRating': 'Peak Rating',
'UserHome.Statistics': 'Statistics',
'UserHome.Accuracy': 'Accuracy',
'UserHome.MaxCombo': 'Max Combo',
@@ -24,6 +26,13 @@ export const EN_REF_USER = {
'UserHome.RankDetail.Title': 'Achievement Details',
'UserHome.RankDetail.Level': 'Level',
'UserHome.B50': 'B50',
'UserHome.RatingComposition.Best': 'Best ${n}',
'UserHome.RatingComposition.New': 'New ${n}',
'UserHome.RatingComposition.BestCandidates': 'Best ${n} Candidates',
'UserHome.RatingComposition.NewCandidates': 'New ${n} Candidates',
'UserHome.RatingComposition.Platinum': 'Platinum ${n}',
'UserHome.RatingComposition.PlatinumCandidates': 'Platinum ${n} Candidates',
'UserHome.RatingComposition.Recent': 'Recent ${n}',
'UserHome.AddRival': "Add to Rival",
'UserHome.RemoveRival': "Remove from Rival",
'UserHome.InvalidGame': "Game ${game} is not supported on the web UI yet. We only support maimai, chunithm, wacca, and ongeki for now.",
@@ -60,12 +69,15 @@ export const EN_REF_Welcome = {
'welcome.verified': 'Your email has been verified! You can now log in now.',
'welcome.verification-failed': 'Verification failed: ${message}. Please try again.',
'welcome.password-reset-done': 'Your password has been updated! Please log back in.',
'welcome.login_link': 'Login / register to link your card.'
}
export const EN_REF_LEADERBOARD = {
'Leaderboard.Title': 'Server Leaderboard',
'Leaderboard.Rank': 'Rank',
'Leaderboard.Rating': 'Rating',
'Leaderboard.LegacyRating': 'Legacy Rating',
'Leaderboard.ModernRating': 'Modern Rating',
'Leaderboard.Accuracy': 'Accuracy',
'Leaderboard.FC': 'FC',
'Leaderboard.AP': 'AP',
@@ -119,6 +131,7 @@ export const EN_REF_HOME = {
'home.linkcard.enter-info': 'Please enter either an Access Code or a Serial Number in the following boxes. Dragging and dropping aime.txt or felica.txt is also supported.',
'home.linkcard.access-code': 'If you have an Access Code, enter it below. Access Codes can be obtained from the back of your card or from the title screen of your game (press the Access Code button and card in). If you do not have an Access Code, you may generate one.',
'home.linkcard.enter-sn': 'If you have a Serial Number, enter it below. Serial Numbers can be obtained via most smartphones (NFC Tools: <a href="https://play.google.com/store/apps/details?id=com.wakdev.wdnfc">Android</a> / <a href="https://apps.apple.com/us/app/nfc-tools/id1252962749">Apple</a>) or a card reader.',
'home.linkcard.kdx-notice': 'If you are playing KanadeDX, please enter the virtual card number here (can be found in Settings > Card Number).',
'home.linkcard.link': 'Link',
'home.linkcard.data-conflict': 'Data Conflict',
'home.linkcard.name': 'Name',
@@ -142,12 +155,11 @@ export const EN_REF_HOME = {
export const EN_REF_SETUP = {
'setup.welcome': `Welcome! If you have a game set up, please follow the instructions below to set up the connection with AquaDX.`,
'setup.keychip-warning': `Your keychip is linked to your account and should be kept secure. Do not give others your keychip.`,
'setup.keychip-warning': `Your keychip(s) are linked to your account and should be kept secure.`,
'setup.steps.one': `Pick a method of setting up network communications. Some browsers may not be able to do automatic setup.`,
'setup.steps.two': `Ensure your game has encryption disabled as AquaDX does not support encryption. This may be via a patch or setting.`,
'setup.steps.three': `Link your Aime card to your AquaDX account using the <a href="/cards">Cards</a> page via it's access code or serial number.`,
'setup.steps.four': `Start the game. Upon reaching the title screen, the network icon in the corner should now show green instead of grey.`,
'setup.support-info': `If you need assistance, feel free to make an inquiry in a <a href="/support">support channel</a>.`,
'setup.steps.two': `Link your Aime card to your AquaDX account using the <a href="/cards">Cards</a> page via it's access code or serial number.`,
'setup.steps.three': `Start the game. Upon reaching the title screen, the network icon in the corner should now show green instead of grey.`,
'setup.support-info': `If you need additional assistance, feel free to make an inquiry in a <a href="/support">support channel</a>.`,
'setup.reveal-keychip': `Reveal keychip`,
'setup.type.automatic': `Automatic Setup`,
'setup.type.manual': `Manual Setup`,
@@ -155,7 +167,21 @@ export const EN_REF_SETUP = {
'setup.automatic': `Select your segatools.ini below to autofill the network information.`,
'setup.automatic.success': `Success, data has been updated`,
'setup.automatic.failure': `Failed to update information, please ensure access is not blocked and try again.`,
'setup.automatic.select': `Pick file`
'setup.automatic.select': `Pick file`,
'setup.troubleshooting.header': 'Setup Troubleshooting (FAQ)',
'setup.troubleshooting.items.one': `» It displays DNS BAD on Network Test<br>
You likely misconfigured your segatools.ini. Ensure it's pointing to the DNS shown above (under Manual Setup).`,
'setup.troubleshooting.items.two': `» It displays タイトル サーバー BAD (Title Server BAD) on Network Test<br>
You should check your segatools.ini to ensure your keychip is set correctly.<br>
Additionally, Card Maker users and players using a game version higher than what's listed may need to disable encryption
<a target="_blank" href="https://gitea.tendokyu.moe/Dniel97/SEGAguide/wiki/SDED#patching-assemblycsharpdll">(instructions for Card Maker)</a>`,
'setup.troubleshooting.items.three': `» I can't scan my card<br>
The built-in emulated card reader, by default, is configured to use the Enter / Return key to scan in (hold the key).`,
'setup.keychip': 'Keychip Management',
'setup.keychip.warning': 'Keychips can be manually set here for cabinet owners with physical keychips trying to connect to AquaDX. Selecting a keychip will autofill it for the setup below.',
'setup.keychip.add': `Add keychip`,
'setup.keychip.delete': `Delete`,
'setup.keychip.placeholder': `New Keychip ID`
}
export const EN_REF_SETTINGS = {
@@ -167,6 +193,24 @@ export const EN_REF_SETTINGS = {
'settings.tabs.mai2': 'Mai',
'settings.tabs.ongeki': 'Ongeki',
'settings.tabs.wacca': 'Wacca',
'settings.options.all.preset-warning': 'You are using a preset option set. Options may be reflected incorrectly.',
'settings.options.all.personal-best': 'Record',
'settings.options.all.rival-score': 'Rival',
'settings.options.all.life': 'Life',
'settings.options.all.push': 'Push',
'settings.options.all.none': 'None',
// Chuni User Options
'settings.options.chu3.speed': 'Lane Speed (60fps)',
'settings.options.chu3.speed_120': 'Lane Speed (120fps)',
'settings.options.chu3.trackSkip': 'Track Skip Type',
'settings.options.chu3.headphone': 'Headphone Volume',
// Maimai User Options
'settings.options.mai2.noteSpeed': 'Note Speed',
'settings.options.mai2.touchSpeed': 'Touch Speed',
'settings.options.mai2.slideSpeed': 'Slide Speed',
'settings.options.mai2.headPhoneVolume': 'Headphone Volume',
'settings.options.mai2.trackSkip': 'Track Skip Type',
'settings.fields.mai2UnlockMusic.name': 'Unlock All Music',
'settings.fields.mai2UnlockMusic.desc': 'Unlock all music and master difficulty.',
'settings.fields.mai2UnlockChara.name': 'Unlock All Characters',
@@ -195,6 +239,8 @@ export const EN_REF_SETTINGS = {
'settings.fields.chusanTeamName.desc': 'Customize the text displayed on the top of your profile.',
'settings.fields.chusanInfinitePenguins.name': 'Infinite Penguins',
'settings.fields.chusanInfinitePenguins.desc': 'Set penguin statues for character level prompting to 999.',
'settings.fields.chusanUnlock29672999Ultima.name': 'Unlock 2967, 2999 Ultima',
'settings.fields.chusanUnlock29672999Ultima.desc': 'Unlocks Ultima difficulty ONLY for musicIds 2967, 2999. Do not use if you don\'t know why you need this.',
'settings.fields.chusanLvUnlockAll.name': 'Unlock Linked Gates',
'settings.fields.chusanLvUnlockAll.desc': 'Incurs a long animated sequence, cannot be undone',
'settings.fields.chusanMatchingReflector.name': 'Matching Server Reflector',
@@ -205,13 +251,16 @@ export const EN_REF_SETTINGS = {
'settings.fields.ongekiInfiniteKaika.desc': 'Set Kaika to 999',
'settings.fields.rounding.name': 'Score Rounding',
'settings.fields.rounding.desc': 'Round the score to one decimal place',
'settings.fields.gameUsername.name': 'In-Game Username',
'settings.fields.gameUsername.name': 'Player Name',
'settings.fields.gameUsername.desc': 'Your name shown in game',
'settings.fields.optOutOfLeaderboard.name': 'Opt Out of Leaderboard',
'settings.fields.optOutOfLeaderboard.desc': 'You will still be able to see yourself on the leaderboard after logging in',
'settings.fields.optOutOfLeaderboard.desc': 'It may take some time for this to take effect',
'settings.fields.hideCountry.name': 'Hide Country',
'settings.fields.hideCountry.desc': 'Your country of registration will not be visible on your profile',
'settings.fields.displayCandidates.name': 'Display Candidates',
'settings.fields.displayCandidates.desc': 'Shows upcoming songs that could improve your rating on your profile (when available)',
'settings.fields.enableMusicRank.name': 'Enable Recommended Music Rank on Your Machine',
'settings.fields.enableMusicRank.desc': 'If you have your own ranking, you can turn this off. It only affects your own machine',
'settings.mai2.name': 'Player Name',
'settings.profile.picture': 'Profile Picture',
'settings.profile.upload-new': 'Upload New',
'settings.profile.bad-format': 'Invalid image format. Supported types are PNG, JPG, JPEG, WEBP & GIF.',
@@ -225,6 +274,11 @@ export const EN_REF_SETTINGS = {
'settings.profile.unset': 'Unset',
'settings.profile.logout': 'Log out',
'settings.profile.unchanged': 'Unchanged',
'settings.profile.delete-title': 'Delete account',
'settings.profile.delete-description': 'Download a backup, then permanently delete your account and all associated game data.',
'settings.profile.delete': 'Delete account',
'settings.profile.delete-confirm-title': 'Permanently delete your account?',
'settings.profile.delete-confirm-message': 'A backup of all game data will be downloaded first. After that, your account and all associated game data will be permanently deleted. This cannot be undone.',
'settings.export': 'Export Player Data',
'settings.batchManualExport': "Export in Batch Manual (for Tachi)",
'settings.cabNotice': "These settings only apply for your keychip. If you're playing on someone else's setup, please ask them to change these settings.",
@@ -270,13 +324,15 @@ export const EN_REF_USERBOX = {
'userbox.matching.symbolChat': 'Message Choice',
'userbox.matching.symbolChat.default': 'Default',
'userbox.lv.diffnotice': 'In Linked Verse, you will be matched separately based on your difficulty.',
'userbox.lv.help': 'Information about Linked Verse Difficulty',
'userbox.lv.diffnotice': 'In Linked Verse, you will be matched separately based on your difficulty.',
'userbox.lv.difficulty': 'Linked Verse Difficulty',
'userbox.lv.difficulty.1': 'LEVEL V (1000 life, MASTER only)',
'userbox.lv.difficulty.2': 'LEVEL IV (3000 life, MASTER only)',
'userbox.lv.difficulty.3': 'LEVEL III (5000 life, MASTER only)',
'userbox.lv.difficulty.4': 'LEVEL II (5000 life, MASTER & EXPERT only)',
'userbox.lv.difficulty.5': 'LEVEL I (5000 life, ALL difficulties)',
'userbox.lv.difficulty.0': 'LEVEL ∞ (Only for supported gates)',
'userbox.lv.difficulty.1': 'LEVEL V',
'userbox.lv.difficulty.2': 'LEVEL IV',
'userbox.lv.difficulty.3': 'LEVEL III',
'userbox.lv.difficulty.4': 'LEVEL II',
'userbox.lv.difficulty.5': 'LEVEL I',
'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.',
+128 -42
View File
@@ -5,6 +5,7 @@ import {
EN_REF_LEADERBOARD,
EN_REF_MAI_PHOTO,
EN_REF_SETTINGS,
EN_REF_SETUP,
EN_REF_USER,
EN_REF_USERBOX,
type EN_REF_Welcome
@@ -13,7 +14,9 @@ import {
const zhUser: typeof EN_REF_USER = {
'UserHome.ServerRank': '服务器排名',
'UserHome.DXRating': 'DX B50',
'UserHome.Rating': '评分',
'UserHome.Rating': '最高评分',
'UserHome.HighestDXRating': 'DX B50',
'UserHome.HighestRating': '最高评分',
'UserHome.Statistics': '统计数据',
'UserHome.Accuracy': '准确率',
'UserHome.MaxCombo': '最大连击',
@@ -36,6 +39,13 @@ const zhUser: typeof EN_REF_USER = {
'UserHome.RankDetail.Title': '评分详细',
'UserHome.RankDetail.Level': "等级",
'UserHome.B50': "B50",
'UserHome.RatingComposition.Best': '最佳 ${n}',
'UserHome.RatingComposition.New': '新曲 ${n}',
'UserHome.RatingComposition.BestCandidates': '最佳 ${n} 候选 ',
'UserHome.RatingComposition.NewCandidates': '新曲 ${n} 候选 ',
'UserHome.RatingComposition.PlatinumCandidates': '白金分 ${n} 候选 ',
'UserHome.RatingComposition.Platinum': '白金分 ${n}',
'UserHome.RatingComposition.Recent': '最近 ${n}',
'UserHome.AddRival': "添加劲敌",
'UserHome.RemoveRival': "移除劲敌",
'UserHome.InvalidGame': "游戏 ${game} 还不支持网页端查看。我们目前只支持舞萌、中二、华卡和音击。",
@@ -59,25 +69,28 @@ const zhWelcome: typeof EN_REF_Welcome = {
'welcome.username-missing': '用户名/邮箱必须填哦',
'welcome.email-password-missing': '邮箱和密码必须填哦',
'welcome.waiting-turnstile': '正在验证网络环境…',
'welcome.turnstile-error': '验证网络环境出错了,请关闭 VPN 后重试',
'welcome.turnstile-timeout': '验证网络环境超时了,请重试',
'welcome.verification-sent': '验证邮件已发送至 ${email},请翻翻收件箱',
'welcome.reset-password-sent': '重置邮件已发送至 ${email},请翻翻收件箱',
'welcome.verify-state-0': '您还没有验证邮箱哦!验证邮件一分钟内刚刚发到您的邮箱,请翻翻收件箱',
'welcome.verify-state-1': '您还没有验证邮箱哦!我们在过去的 24 小时内已经发送了 3 封验证邮件,所以我们不会再发送了,请翻翻收件箱',
'welcome.verify-state-2': '您还没有验证邮箱哦!我们刚刚又发送了一封验证邮件,请翻翻收件箱',
'welcome.reset-state-0': '重置邮件刚刚发送到你的邮箱啦,请翻翻收件箱!',
'welcome.reset-state-1': '邮件发送次数过多,暂时不会再发送新的重置邮件了',
'welcome.turnstile-error': '验证网络环境出错了,请关闭 VPN 后重试',
'welcome.turnstile-timeout': '验证网络环境超时了,请重试',
'welcome.verification-sent': '验证邮件已发送至 ${email},请翻翻收件箱',
'welcome.reset-password-sent': '重置邮件已发送至 ${email},请翻翻收件箱',
'welcome.verify-state-0': '您还没有验证邮箱哦!验证邮件一分钟内刚刚发到您的邮箱,请翻翻收件箱',
'welcome.verify-state-1': '您还没有验证邮箱哦!我们在过去的 24 小时内已经发送了 3 封验证邮件,所以我们不会再发送了,请翻翻收件箱',
'welcome.verify-state-2': '您还没有验证邮箱哦!我们刚刚又发送了一封验证邮件,请翻翻收件箱',
'welcome.reset-state-0': '重置邮件刚刚发送到你的邮箱啦,请翻翻收件箱!',
'welcome.reset-state-1': '邮件发送次数过多,暂时不会再发送新的重置邮件了',
'welcome.verifying': '正在验证邮箱…请稍等',
'welcome.verified': '您的邮箱已经验证成功!您现在可以登录了',
'welcome.verification-failed': '验证失败:${message}。请重试',
'welcome.password-reset-done': '您的密码已更新!请重新登录',
'welcome.login_link': '登录或注册以绑定卡片。',
}
const zhLeaderboard: typeof EN_REF_LEADERBOARD = {
'Leaderboard.Title': '排行榜',
'Leaderboard.Rank': '排名',
'Leaderboard.Rating': '评分',
'Leaderboard.LegacyRating': '旧评分',
'Leaderboard.ModernRating': '新评分',
'Leaderboard.Accuracy': '准确率',
'Leaderboard.FC': 'FC',
'Leaderboard.AP': 'AP',
@@ -90,43 +103,48 @@ const zhGeneral: typeof EN_REF_GENERAL = {
'game.ongeki': "音击",
'game.wacca': "华卡",
"status.error": "发生错误",
"status.error.hint": "出了一些问题,请稍后刷新重试或者",
"status.error.hint.link": "加我们的 Discord 群问一问",
"status.error.hint": "支持 (Discord)",
"status.detail": "详细信息:${detail}",
"action.refresh": "刷新",
"action.cancel": "取消",
"action.confirm": "确认",
"action.home": "主页",
'navigation.profile': '个人资料',
'navigation.maps': '地图',
'navigation.home': '首页',
'navigation.rankings': '排行榜',
'navigation.notice': '公告'
'navigation.notice': '公告',
'loading': '正在加载...',
'404': '找不到页面 (${pathname})。如果您认为这是个错误,请通过适当的支持渠道报告。'
}
const zhHome: typeof EN_REF_HOME = {
'home.nav.portal': "主页",
'home.nav.link-card': "绑卡",
'home.nav.game-setup': "连接设置",
'home.nav.support': "支持",
'home.user-profile': "个人资料",
'home.rankings': "排行榜",
'home.settings': "设置",
'home.manage-cards': '管理游戏卡',
'home.manage-cards-description': '绑定、解绑、管理游戏数据卡',
'home.link-card': '绑定游戏卡',
'home.link-cards-description':'绑定游戏数据卡(Amusement IC 或 Aime 卡)后才可以访问游戏存档哦',
'home.join-community': '加入群组',
'home.join-community-description': '加入我们的聊天群组,与其他玩家聊天、获取帮助',
'home.join-community-description': '加入我们的聊天群组,与其他玩家聊天、获取帮助',
'home.setup': '连接到 AquaDX',
'home.setup-description': '如果您有街机框体或者手台,点击这里设置服务器的连接',
'home.setup-description': '如果您有街机框体或者手台,点击这里设置服务器的连接',
'home.import': '导入玩家数据',
'home.import-description': '如果你来自其他在线服,可以点击这里导入从其他服务器导出的数据',
'home.import-description': '如果你来自其他在线服,可以点击这里导入从其他服务器导出的数据',
'home.linkcard.cards': "已绑卡片",
'home.linkcard.description': "这些是您绑定到帐户的卡",
'home.linkcard.account-card': "账户卡",
'home.linkcard.registered': "注册于",
'home.linkcard.lastused': "上次使用",
'home.linkcard.enter-info': "请输入以下信息,或将 aime.txt / felica.txt 文件拖放到此区域",
'home.linkcard.access-code': "卡背面的 20 位卡号(如果提示找不到卡,请尝试使用游戏内置的显示卡号功能,输入游戏读取到的卡号)",
'home.linkcard.enter-sn1': "在您的手机",
'home.linkcard.enter-sn2': "上下载 NFC Tools 并扫描您的卡。然后输入显示的 SN 号。",
'home.linkcard.kdx-notice': "如果你在玩 KanadeDX,请在这里输入虚拟卡号(可以在设置 > 卡号中找到卡号)",
'home.linkcard.enter-info': "请输入以下信息,或将 aime.txt / felica.txt 文件拖放到此区域",
'home.linkcard.access-code': "卡背面的 20 位卡号(如果提示找不到卡,请尝试使用游戏内置的显示卡号功能,输入游戏读取到的卡号)",
'home.linkcard.enter-sn': "在您的手机上下载 NFC Tools 并扫描您的卡。然后输入显示的 SN 号。",
'home.linkcard.kdx-notice': "如果你在玩 KanadeDX,请在这里输入虚拟卡号(可以在设置 > 卡号中找到卡号)",
'home.linkcard.link': "绑定",
'home.linkcard.data-conflict': "卡号冲突",
'home.linkcard.name': "名称",
@@ -137,31 +155,75 @@ const zhHome: typeof EN_REF_HOME = {
'home.linkcard.notfound': "找不到卡",
'home.linkcard.unlink': "取消链接",
'home.linkcard.unlink-notice': "你确定要取消此卡的链接吗?",
'home.linkcard.felica-ac-warning': "该 Access Code 是一张 FeliCa AIC 卡。\n如果你使用实体卡(而非 aime.txt 模拟)刷卡登录游戏,与官方服务器不同,你需要绑定该卡的 FeliCa SN(或与之对应的,游戏界面中查看得到的 00 开头的卡号)而非此号码。\n如果你使用 aime.txt 模拟登录,请忽略本警告继续绑定。",
'home.setup.welcome': "欢迎! 如果您有街机框体或者手台,请按照以下说明设置以连接到 AquaDX。",
'home.setup.blockquote': "我们假设您已经拥有所需的文件,并且可以启动机台或手台附带的游戏(例如 ROM 和 segatools)。如果没有,请联系您设备的卖家以获取所需的文件,因为出于版权原因,我们不会提供这些文件。",
'home.setup.get': "开始",
'home.setup.edit': "请打开您的 segatools.ini 文件并修改以下行",
'home.setup.test': "在您重新启动游戏后,应该能够连接到 AquaDX。可以验证测试菜单中的网络测试测试连接是否全部良好。",
'home.setup.ask': "如果您有任何问题,请加入我们的",
'home.setup.support': "以获取支持",
'home.setup.keychip-tips': "这是你的狗号,不要与任何人分享",
'home.linkcard.felica-ac-warning': "该 Access Code 是一张 FeliCa AIC 卡。\n如果你使用实体卡(而非 aime.txt 模拟)刷卡登录游戏,与官方服务器不同,你需要绑定该卡的 FeliCa SN(或与之对应的,游戏界面中查看得到的 00 开头的卡号)而非此号码。\n如果你使用 aime.txt 模拟登录,请忽略本警告继续绑定。",
'home.community.discord': 'Discord',
'home.community.telegram': 'Telegram (中文)',
'home.community.qq': 'QQ (中文)',
'home.community.github': 'GitHub 仓库',
'home.linkcard.card-security-warning': "卡片信息决定了您可以访问的所有玩家数据。请不要与任何人分享您的卡号。",
'home.import.unknown-game': '未知游戏类型 (目前导入只支持舞萌和中二)',
'home.import.new-data': '要导入的数据',
'home.import.data-conflict': '继续导入将覆盖现有数据',
}
const zhSetup: typeof EN_REF_SETUP = {
'setup.welcome': `欢迎!如果您已准备好游戏,请按照以下说明设置与 AquaDX 的连接。`,
'setup.keychip-warning': `您的密钥芯片已与您的账户绑定,请妥善保管。`,
'setup.steps.one': `选择一种网络通信设置方法。某些浏览器可能无法进行自动设置。`,
'setup.steps.two': `请通过 <a href="/cards">卡片</a> 页面,使用访问码或序列号将您的 Aime 卡与 AquaDX 账户绑定。`,
'setup.steps.three': `启动游戏。进入标题画面后,角落处的网络图标应变为绿色,而非灰色。`,
'setup.support-info': `如需进一步协助,欢迎在 <a href="/support">支持频道</a> 中咨询。`,
'setup.reveal-keychip': `显示密钥芯片`,
'setup.type.automatic': `自动设置`,
'setup.type.manual': `手动设置`,
'setup.manual': `请使用以下信息修改您的 segatools.ini 文件。`,
'setup.automatic': `请在下方的 segatools.ini 文件中选择,以自动填写网络信息。`,
'setup.automatic.select': `选择文件`,
'setup.automatic.success': `成功,数据已更新`,
'setup.automatic.failure': `更新信息失败,请确保访问未被阻止,然后重试。`,
'setup.troubleshooting.header': '设置故障排除(常见问题解答)',
'setup.troubleshooting.items.one': `» 网络测试显示 DNS BAD<br>
您可能配置了错误的 segatools.ini。请确保其指向上述(手动设置部分)显示的 DNS。`,
'setup.troubleshooting.items.two': `» 网络测试显示 タイトル サーバー BAD (Title Server BAD)<br>
请检查您的 segatools.ini 文件,确保密钥芯片设置正确。<br>
此外, Card Maker 用户以及使用高于列表中版本的游戏版本的玩家,可能需要禁用加密
<a target="_blank" href="https://gitea.tendokyu.moe/Dniel97/SEGAguide/wiki/SDED#patching-assemblycsharpdll">(Card Maker 操作指南)</a>`,
'setup.troubleshooting.items.three': `» 无法扫描卡片<br>
内置的模拟读卡器默认配置为使用回车键进行扫描(长按该键)。`,
'setup.keychip': '钥匙芯片管理',
'setup.keychip.warning': '对于拥有实体钥匙芯片并试图连接到 AquaDX 的机柜所有者,可在此手动设置钥匙芯片。选择钥匙芯片后,下方设置项将自动填充。',
'setup.keychip.add': `添加钥匙芯片`,
'setup.keychip.delete': `删除`,
'setup.keychip.placeholder': `新钥匙芯片 ID`
}
const zhSettings: typeof EN_REF_SETTINGS = {
'settings.title': '用户设置',
'settings.page-title': '${page} 设置',
'settings.tabs.profile': '个人资料',
'settings.tabs.global': '全局',
'settings.tabs.chu3': '中二',
'settings.tabs.mai2': '舞萌',
'settings.tabs.ongeki': '音击',
'settings.tabs.wacca': '华卡',
"settings.options.all.preset-warning": "您正在使用预设选项配置,部分选项可能显示不正确。",
"settings.options.all.personal-best": "记录",
"settings.options.all.rival-score": "对手",
"settings.options.all.life": "生命值",
"settings.options.all.push": "按压",
"settings.options.all.none": "无",
"settings.options.chu3.speed": "轨道速度",
"settings.options.chu3.speed_120": "轨道速度(120帧)",
"settings.options.chu3.trackSkip": "跳过曲目类型",
"settings.options.chu3.headphone": "耳机音量",
"settings.options.mai2.noteSpeed": "音符速度",
"settings.options.mai2.touchSpeed": "触控速度",
"settings.options.mai2.slideSpeed": "滑动速度",
"settings.options.mai2.headPhoneVolume": "耳机音量",
"settings.options.mai2.trackSkip": "跳过曲目类型",
'settings.fields.mai2UnlockMusic.name': '解锁谱面',
'settings.fields.mai2UnlockMusic.desc': '解锁所有曲目和大师难度谱面。',
'settings.fields.mai2UnlockChara.name': '解锁角色',
@@ -190,6 +252,10 @@ const zhSettings: typeof EN_REF_SETTINGS = {
'settings.fields.chusanTeamName.desc': '自定义显示在个人资料顶部的文本。',
'settings.fields.chusanInfinitePenguins.name': '我是桐谷遥',
'settings.fields.chusanInfinitePenguins.desc': '将角色界限突破的企鹅雕像数量设置为 999。',
'settings.fields.chusanUnlock29672999Ultima.name': '解锁 2967、2999 Ultima',
'settings.fields.chusanUnlock29672999Ultima.desc': '仅解锁曲目 2967、2999 的 Ultima 难度。不清楚用途请勿使用。',
'settings.fields.chusanLvUnlockAll.name': '解锁所有 Linked Gate',
'settings.fields.chusanLvUnlockAll.desc': '会触发一个很长的动画序列,且无法撤销',
'settings.fields.chusanMatchingReflector.name': '全国对战 Reflector',
'settings.fields.chusanMatchingReflector.desc': '全国对战服务器的 UDP 反射服务器的 URL',
'settings.fields.chusanMatchingServer.name': '全国对战服务器',
@@ -202,12 +268,15 @@ const zhSettings: typeof EN_REF_SETTINGS = {
'settings.fields.gameUsername.desc': '在游戏中显示的用户名',
'settings.fields.optOutOfLeaderboard.name': '不参与排行榜',
'settings.fields.optOutOfLeaderboard.desc': '登录之后还是可以在排行榜上看到自己',
"settings.fields.hideCountry.name": "隐藏注册国家",
"settings.fields.hideCountry.desc": "您的注册国家不会在个人资料中显示",
"settings.fields.displayCandidates.name": "显示推荐曲目",
"settings.fields.displayCandidates.desc": "在您的个人资料中显示可能提升您评分的曲目(如有)",
'settings.fields.enableMusicRank.name': '在你的机台上启用「推荐乐曲排行榜」',
'settings.fields.enableMusicRank.desc': '如果你自己设计了排行榜的话,可以关闭这个(会影响你自己的机器)。',
'settings.mai2.name': '玩家名字',
'settings.fields.enableMusicRank.desc': '如果你自己设计了排行榜的话,可以关闭这个(会影响你自己的机器)。',
'settings.profile.picture': '头像',
'settings.profile.upload-new': '上传',
'settings.profile.bad-format': '无效的图片格式,支持的格式有 PNG、JPG、JPEG、WEBP 和 GIF。',
'settings.profile.bad-format': '无效的图片格式,支持的格式有 PNG、JPG、JPEG、WEBP 和 GIF。',
'settings.profile.save': '保存',
'settings.profile.name': '昵称',
'settings.profile.username': '用户名',
@@ -218,9 +287,14 @@ const zhSettings: typeof EN_REF_SETTINGS = {
'settings.profile.unset': '未设置',
'settings.profile.logout': '登出',
'settings.profile.unchanged': '未更改',
'settings.profile.delete-title': '删除账户',
'settings.profile.delete-description': '下载备份后,永久删除您的账户及所有关联游戏数据。',
'settings.profile.delete': '删除账户',
'settings.profile.delete-confirm-title': '永久删除您的账户?',
'settings.profile.delete-confirm-message': '系统会先下载所有游戏数据的备份,然后永久删除您的账户及所有关联游戏数据。此操作无法撤销。',
'settings.export': '导出玩家数据',
'settings.batchManualExport': "导出 Batch Manual 格式(用于 Tachi)",
'settings.cabNotice': '注意:下面这些设置只会影响你自己的机器,如果你是在其他人的机器上玩的话,请联系机主来改设置',
'settings.cabNotice': '注意:下面这些设置只会影响你自己的机器,如果你是在其他人的机器上玩的话,请联系机主来改设置',
// AI
'settings.siteNotice': "这些设置仅适用于网站。",
// AI
@@ -237,6 +311,7 @@ export const zhUserbox: typeof EN_REF_USERBOX = {
'userbox.header.general': '游戏设置',
'userbox.header.matching': '全国对战',
'userbox.header.matching.symbolChat': '全国对战聊天表情',
'userbox.header.linkedVerse': 'Linked Verse',
'userbox.header.userbox': 'UserBox 设置',
'userbox.header.preview': 'UserBox 预览',
'userbox.nameplateId': '名牌',
@@ -246,6 +321,7 @@ export const zhUserbox: typeof EN_REF_USERBOX = {
'userbox.trophyIdSub2': '称号3',
'userbox.mapIconId': '地图图标',
'userbox.voiceId': '系统语音',
'userbox.stageId': '舞台',
'userbox.avatarWear': '企鹅服饰',
'userbox.avatarHead': '企鹅头饰',
'userbox.avatarFace': '企鹅面部',
@@ -266,8 +342,18 @@ export const zhUserbox: typeof EN_REF_USERBOX = {
'userbox.matching.symbolChat': '表情选择',
'userbox.matching.symbolChat.default': '默认',
'userbox.lv.help': '关于 Linked Verse Difficulty 的信息',
'userbox.lv.diffnotice': '在 Linked Verse 中,你将根据难度分别进行匹配。',
'userbox.lv.difficulty': 'Linked Verse 难度',
'userbox.lv.difficulty.0': 'LEVEL ∞ (并非总是可用)',
'userbox.lv.difficulty.1': 'LEVEL V',
'userbox.lv.difficulty.2': 'LEVEL IV',
'userbox.lv.difficulty.3': 'LEVEL III',
'userbox.lv.difficulty.4': 'LEVEL II',
'userbox.lv.difficulty.5': 'LEVEL I',
'userbox.new.name': 'AquaBox',
'userbox.new.setup': '将中二(Lumi 或更高版本)的游戏文件夹拖放到下方区域,以显示带有名牌和头像的 UserBox。所有文件都在浏览器中处理。',
'userbox.new.setup': '将中二(Lumi 或更高版本)的游戏文件夹拖放到下方区域,以显示带有名牌和头像的 UserBox。所有文件都在浏览器中处理。',
'userbox.new.setup.notice': '选择包含游戏数据的最外层文件夹。',
'userbox.new.setup.processing_file': '正在处理文件',
'userbox.new.setup.finalizing': '正在保存到内部存储',
@@ -281,20 +367,20 @@ export const zhUserbox: typeof EN_REF_USERBOX = {
'userbox.new.activate_desc': '启用后可显示带有名牌和头像的 UserBox',
'userbox.new.activate_profile': '在用户页面启用 AquaBox',
'userbox.new.activate_profile_desc': '启用后可在个人页面显示带有名牌和头像的 UserBox',
'userbox.new.error.invalidFolder': '所选文件夹无效。请确认游戏版本为 Lumi 或更新,并且包含 “A000” 选项包。',
'userbox.new.error.invalidFolder': '所选文件夹无效。请确认游戏版本为 Lumi 或更新,并且包含 “A000” 选项包。',
'userbox.new.error.invalidUrl': '输入的 URL 无效。'
};
export const zhMaiPhoto: typeof EN_REF_MAI_PHOTO = {
'maiphoto.title': 'Mai 纪念照片库',
'maiphoto.url_warning': '注意:如果想与朋友分享图片的话,请先保存照片再发出去。不要复制图片 URL,因为 URL 中包含 AquaDX 账号信息。',
'maiphoto.url_warning': '注意:如果想与朋友分享图片的话,请先保存照片再发出去。不要复制图片 URL,因为 URL 中包含 AquaDX 账号信息。',
'maiphoto.none': '还没有图片哦~ 可以在每次游戏结束的时候点击上传来上传照片。',
}
export const zhAquaTrans: typeof EN_REF_AQUATRANS = {
'trans.title': '🏳️‍⚧️ AquaTrans™ 数据迁移工具',
'trans.confirm.unbackuped.title': '确认迁移',
'trans.confirm.unbackuped.msg': '似乎还没有备份目标服务器的数据,真的要继续吗?(推荐先备份一下,因为迁移的时候会覆盖数据)',
'trans.confirm.unbackuped.msg': '似乎还没有备份目标服务器的数据,真的要继续吗?(推荐先备份一下,因为迁移的时候会覆盖数据)',
'trans.confirm.untested.title': '不太聪明喵',
'trans.confirm.untested.msg': '在两个服务器上都测试完连接之后才能进行数据迁移哦!',
'trans.confirm.done.title': '完成!',
@@ -302,8 +388,8 @@ export const zhAquaTrans: typeof EN_REF_AQUATRANS = {
'trans.alert.in-progress': '在迁移了在迁移了',
'trans.prompt-html': `
<p>👋 欢迎使用 AquaTrans™ 服务器游玩数据迁移工具!</p>
<p>这个工具可以导出任意服务器的数据,并使用连接凭证(卡号、服务器地址和 Keychip ID)将数据导入任何其他服务器。</p>
<p>我将模拟游戏客户端,从源服务器拉取游戏数据并推送到目标服务器。</p>
<p>这个工具可以导出任意服务器的数据,并使用连接凭证(卡号、服务器地址和 Keychip ID)将数据导入任何其他服务器。</p>
<p>我将模拟游戏客户端,从源服务器拉取游戏数据并推送到目标服务器。</p>
<p>填写下面的表格开始迁移吧!</p>
`,
'trans.error.empty': '请填写所有字段。',
@@ -319,11 +405,11 @@ export const zhAquaTrans: typeof EN_REF_AQUATRANS = {
'trans.btn.test': '测试连接',
'trans.btn.export': '导出数据',
'trans.btn.import': '导入数据',
'trans.blacklist': "这个服务器的服主把这个导出工具 ban 了,所以不能从这里导出",
'trans.blacklist': "这个服务器的服主把这个导出工具 ban 了,所以不能从这里导出",
}
export const ZH = { ...zhUser, ...zhWelcome, ...zhGeneral,
...zhLeaderboard, ...zhHome, ...zhSettings, ...zhUserbox, ...zhMaiPhoto,
...zhAquaTrans
...zhAquaTrans, ...zhSetup
}
+6 -4
View File
@@ -29,7 +29,9 @@ const multTable = {
'chu3': [
[ 100.9, 215, 'SSS+' ],
[ 100.75, 200, 'SSS' ],
[ 100.50, 0, 'SS+' ],
[ 100.0, 0, 'SS' ],
[ 99.0, 0, 'S+' ],
[ 97.5, 0, 'S' ],
[ 95.0, 0, 'AAA' ],
[ 92.5, 0, 'AA' ],
@@ -80,11 +82,11 @@ export function getMult(achievement: number, game: GameName) {
}
export function roundFloor(achievement: number, game: GameName, digits = 2) {
// Round, but if the rounded number reaches the next rank, use floor instead
// Round, but if the rounded number reaches the next rank (or fake 101% AP+), use floor instead
const mult = getMult(achievement, game);
achievement /= 10000
const rounded = achievement.toFixed(digits);
if (getMult(+rounded * 10000, game)[2] === mult[2] && rounded !== '101.0') return rounded;
const original = achievement / 10000;
const rounded = original.toFixed(digits);
if (getMult(+rounded * 10000, game)[2] === mult[2] && (+rounded < 101 || original >= 101)) return rounded;
return (+rounded - Math.pow(10, -digits)).toFixed(digits);
}
+30 -6
View File
@@ -9,10 +9,21 @@ import type {
AquaNetUser, GameOption,
UserBox,
UserItem,
Dict
Dict,
GameUserOption
} from './generalTypes'
import type { GameName } from './scoring'
export type ExportGameName = GameName | 'diva'
export interface PhotoPage {
photos: string[]
page: number
pageSize: number
total: number
totalPages: number
}
interface ExtReqInit extends RequestInit {
params?: { [index: string]: string }
json?: any
@@ -25,7 +36,8 @@ interface ExtReqInit extends RequestInit {
* @param callback Callback for modification
*/
export function reconstructUrl(input: URL | RequestInfo, callback: (url: URL) => URL | void): RequestInfo | URL {
let u = new URL((input instanceof Request) ? input.url : input)
const base = typeof window !== 'undefined' ? window.location.origin : 'http://localhost'
let u = new URL((input instanceof Request) ? input.url : input, base)
const result = callback(u)
if (result) u = result
if (input instanceof Request) {
@@ -185,8 +197,12 @@ export const USER = {
ensureLoggedIn()
return post('/api/v2/user/me', {})
},
keychip: (): Promise<string> =>
post('/api/v2/user/keychip', {}).then(it => it.keychip),
keychips: (): Promise<string[]> =>
post('/api/v2/user/keychip', {}).then(it => it.keychips),
addKeychip: (keychipId: string): Promise<string> =>
post('/api/v2/user/keychip/add', { keychipId }).then(it => it.keychipId),
deleteKeychip: (keychipId: string) =>
post('/api/v2/user/keychip/delete', { keychipId }),
setting: (key: string, value: string) =>
post('/api/v2/user/setting', { key: key === 'password' ? 'pwHash' : key, value }),
uploadPfp: (file: File) => {
@@ -198,6 +214,8 @@ export const USER = {
ensureLoggedIn,
changeRegion: (regionId: number) =>
post('/api/v2/user/change-region', { regionId }),
deleteAccount: () =>
post('/api/v2/user/delete-account'),
}
export const USERBOX = {
@@ -223,7 +241,9 @@ export const CARD = {
export const GAME = {
trend: (username: string, game: GameName): Promise<TrendEntry[]> =>
post(`/api/v2/game/${game}/trend`, { username }),
photos: (): Promise<string[]> =>
photos: (page: number = 1, size: number = 12): Promise<PhotoPage> =>
post(`/api/v2/game/mai2/my-photo`, { page, size }),
allPhotos: (): Promise<string[]> =>
post(`/api/v2/game/mai2/my-photo`, { }),
userSummary: (username: string, game: GameName): Promise<GenericGameSummary> =>
post(`/api/v2/game/${game}/user-summary`, { username }),
@@ -231,7 +251,7 @@ export const GAME = {
post(`/api/v2/game/${game}/ranking`, typeof page === "number" ? { page } : {}),
changeName: (game: GameName, newName: string): Promise<{ newName: string }> =>
post(`/api/v2/game/${game}/change-name`, { newName }),
export: (game: GameName): Promise<Record<string, any>> =>
export: (game: ExportGameName): Promise<Record<string, any>> =>
post(`/api/v2/game/${game}/export`),
import: (game: GameName, data: any): Promise<Record<string, any>> =>
post(`/api/v2/game/${game}/import`, {}, { json: data }),
@@ -255,6 +275,10 @@ export const SETTING = {
post('/api/v2/settings/set', { key, value: `${value}` }),
detailSet: (game: string, field: string, value: any) =>
post(`/api/v2/game/${game}/user-detail-set`, { field, value }),
optionGet: (game: string): Promise<GameUserOption> =>
post(`/api/v2/game/${game}/user-option`),
optionSet: (game: string, field: string, value: number): Promise<void> =>
post(`/api/v2/game/${game}/user-option-set`, { field, value }),
}
export const TRANSFER = {
+4 -4
View File
@@ -24,10 +24,10 @@ function addOrUpdateItem(ini: string[], section: string, key: string, value: str
idx, keys: {}
};
} else {
let key = item.split("=")[0];
let key = item.split("=")[0].toLowerCase();
let value = item.split("=")[1];
if (!key || !value)
if (!key)
continue;
if (activeSection && sections[activeSection])
@@ -48,7 +48,7 @@ function addOrUpdateItem(ini: string[], section: string, key: string, value: str
}
export function injectNetworkData(baseIni: string, networkData: NetworkData): string {
let ini: string[] = baseIni.split("\n").map(i => i.replaceAll("\r", ""));
let ini: string[] = baseIni.split("\n").map(i => i.trim().replaceAll("\r", ""));
if (networkData.dns)
addOrUpdateItem(ini, "dns", "default", networkData.dns);
@@ -80,4 +80,4 @@ export async function patchUserSegatools(networkData: NetworkData): Promise<bool
} catch(err) {
return false;
}
}
}
+5 -5
View File
@@ -35,7 +35,7 @@ export function registerChart() {
)
}
const dayTemplate = (DateHelper) => {
const dayTemplate = (DateHelper: any) => {
const ROWS_COUNT = 7
const ALLOWED_DOMAIN_TYPE = ['month']
@@ -43,7 +43,7 @@ const dayTemplate = (DateHelper) => {
name: 'ghDayFix',
allowedDomainType: ALLOWED_DOMAIN_TYPE,
rowsCount: () => ROWS_COUNT,
columnsCount: (ts) => {
columnsCount: (ts: any) => {
let count = DateHelper.getWeeksCountInMonth(ts)
const endOfMonth = moment().endOf('month').toDate()
const clampEnd = DateHelper.getFirstWeekOfMonth(endOfMonth).toDate()
@@ -53,7 +53,7 @@ const dayTemplate = (DateHelper) => {
}
return count
},
mapping: (startTimestamp, endTimestamp) => {
mapping: (startTimestamp: any, endTimestamp: any) => {
const clampStart = DateHelper.getFirstWeekOfMonth(startTimestamp)
let clampEnd = DateHelper.getFirstWeekOfMonth(endTimestamp)
@@ -64,7 +64,7 @@ const dayTemplate = (DateHelper) => {
let x = -1
const pivotDay = clampStart.weekday()
return DateHelper.intervals('day', clampStart, clampEnd).map((ts) => {
return DateHelper.intervals('day', clampStart, clampEnd).map((ts: any) => {
const weekday = DateHelper.date(ts).weekday()
if (weekday === pivotDay) {
x += 1
@@ -77,7 +77,7 @@ const dayTemplate = (DateHelper) => {
}
})
},
extractUnit: (ts) => DateHelper.date(ts).startOf('day').valueOf(),
extractUnit: (ts: any) => DateHelper.date(ts).startOf('day').valueOf(),
}
}
+5 -7
View File
@@ -6,6 +6,7 @@
import { t } from "../libs/i18n";
import ImportDataAction from "./Home/ImportDataAction.svelte";
import DashboardTabs from "../components/DashboardTabs.svelte";
import * as acUrl from "../libs/acUrl";
USER.ensureLoggedIn();
@@ -13,6 +14,10 @@
let error = "";
USER.me().then((m) => me = m).catch(e => error = e.message)
if (acUrl.has()) {
location.href = "/cards"
}
</script>
<main class="content">
@@ -63,13 +68,6 @@
<style lang="sass">
@use "../vars"
.tabs
display: flex
gap: 1rem
div
&.active
color: vars.$c-main
h3
font-size: 1.3rem
@@ -65,10 +65,13 @@
const getGameByCode = (code: string) => {
switch (code?.toUpperCase()) {
case 'SDGA':
case 'SDEZ':
return 'mai2';
case 'SDHD':
return 'chu3';
case 'SDDT':
return 'ongeki';
default:
throw new Error(t('home.import.unknown-game'));
}
+19 -4
View File
@@ -9,6 +9,8 @@
import StatusOverlays from "../../components/StatusOverlays.svelte"
import { t } from "../../libs/i18n"
import DashboardTabs from "../../components/DashboardTabs.svelte";
import * as acUrl from "../../libs/acUrl"
import { ACCESSCODE_QKEY } from "../../libs/config";
// State
let state: 'ready' | 'linking-AC' | 'linking-SN' | 'loading' = "loading"
@@ -27,7 +29,10 @@
// Always put the ghost card at the top
m.cards.sort((a, b) => a.isGhost ? -1 : 1)
state = "ready"
linkQR()
}).catch(e => error = e.message)
acUrl.check()
updateMe()
// Data conflict overlay
@@ -49,12 +54,12 @@
if (linkingType === 'AC') inputAC = ""
if (linkingType === 'SN') inputSN = ""
} catch (e) {
setError(e.message, linkingType)
setError((e as any).message, linkingType!)
}
state = "ready"
}
let linkingType: 'AC' | 'SN' = null
let linkingType: 'AC' | 'SN' | null = null
async function link(type: 'AC' | 'SN') {
if (state !== 'ready' || accountCardSummary === null) return
state = "linking-" + type
@@ -192,7 +197,7 @@
function inputACChange() {
// Add spaces to the input
const cursorIndex = cursorPositionToCursorIndex(inputAC, elemInputAC.selectionStart, /\d/)
const cursorIndex = cursorPositionToCursorIndex(inputAC, elemInputAC.selectionStart ?? 0, /\d/)
inputAC = inputAC.replace(/\D/g, '').replace(/(.{4})/g, '$1 ').replace(/ $/, '')
const cursorPosition = cursorIndexToCursorPosition(inputAC, cursorIndex, /\d/)
setTimeout(() => elemInputAC.selectionStart = elemInputAC.selectionEnd = cursorPosition, 0)
@@ -212,7 +217,7 @@
function inputSNChange() {
// Add colons to the input
inputSN = inputSN.toUpperCase()
const cursorIndex = cursorPositionToCursorIndex(inputSN, inputElemSN.selectionStart, /[0-9A-F]/)
const cursorIndex = cursorPositionToCursorIndex(inputSN, inputElemSN.selectionStart ?? 0, /[0-9A-F]/)
inputSN = inputSN.replace(/[^0-9A-F]/g, '').replace(/(.{2})/g, '$1:').replace(/:$/, '')
const cursorPosition = cursorIndexToCursorPosition(inputSN, cursorIndex, /[0-9A-F]/)
setTimeout(() => inputElemSN.selectionStart = inputElemSN.selectionEnd = cursorPosition, 0)
@@ -273,6 +278,16 @@
};
inputACChange();
}
function linkQR() {
if (!acUrl.has()) return;
inputAC = acUrl.get()
if (inputAC.length !== 20) return inputAC = "";
inputACChange()
acUrl.clear()
}
</script>
<main class="content">
+220 -48
View File
@@ -11,34 +11,100 @@
import { patchUserSegatools } from "../../libs/setup";
let user: AquaNetUser
let keychip: string;
let keychips: string[] = [];
let selectedKeychip: string = "";
let keychipCode: string;
let exposeKeychip = false;
let automaticSetupStatus: "none" | "success" | "failure" = "none";
let isLoading = true;
let isAdding = false;
let newKeychip = "";
let addKeychipError = "";
USER.me().then((u) => {
user = u;
USER.keychip().then(k => {
keychip = `${k.slice(0, 4)}-${k.slice(4)}1337`;
codeToHtml(`
function formatKeychipDisplay(k: string): string {
return `${k.slice(0, 4)}-${k.slice(4)}`;
}
function buildManualKeychipLines(): string {
return `id=${formatKeychipDisplay(selectedKeychip)}`;
}
async function buildKeychipCode() {
exposeKeychip = false;
const keychipLines = buildManualKeychipLines();
keychipCode = await codeToHtml(`
[dns]
default=${AQUA_CONNECTION}
[keychip]
enable=1
id=${keychip}`.trim(), {
lang: 'ini',
theme: 'rose-pine',
transformers: []
}).then((html) => {
keychipCode = html;
});
${keychipLines}`.trim(), {
lang: 'ini',
theme: 'rose-pine',
transformers: []
});
}
async function loadKeychips() {
isLoading = true;
keychips = await USER.keychips();
if (keychips.length > 0) {
selectedKeychip = keychips[0];
await buildKeychipCode();
} else {
selectedKeychip = "";
await buildKeychipCode();
}
isLoading = false;
}
USER.me().then((u) => {
user = u;
loadKeychips();
});
async function selectKeychip(k: string) {
selectedKeychip = k;
await buildKeychipCode();
}
async function addKeychip() {
const rawKeychipId = newKeychip.trim().toUpperCase();
const validRawFormat = /^([A-Z\d]{4}-[A-Z\d]{11}|[A-Z\d]{15})$/.test(rawKeychipId);
if (!validRawFormat) {
addKeychipError = "Invalid keychip format. Use 15 characters (with optional dash): A12345678901234 or A123-12345678901.";
return;
}
const keychipId = rawKeychipId.replace("-", "");
addKeychipError = "";
isAdding = true;
try {
const newId = await USER.addKeychip(keychipId);
keychips = [...keychips, newId];
selectedKeychip = newId;
newKeychip = "";
await buildKeychipCode();
} catch (error) {
addKeychipError = error instanceof Error ? error.message : "Failed to add keychip.";
} finally {
isAdding = false;
}
}
async function deleteKeychip(k: string) {
await USER.deleteKeychip(k);
keychips = keychips.filter(id => id !== k);
if (selectedKeychip === k) {
selectedKeychip = keychips[0] ?? "";
await buildKeychipCode();
}
}
async function patchSegatools() {
automaticSetupStatus = await patchUserSegatools({ keychip, dns: AQUA_CONNECTION }) ? "success" : "failure";
automaticSetupStatus = await patchUserSegatools({ keychip: formatKeychipDisplay(selectedKeychip), dns: AQUA_CONNECTION }) ? "success" : "failure";
}
</script>
@@ -47,45 +113,87 @@ id=${keychip}`.trim(), {
<div class="setup-instructions">
<h2>{t('home.setup')}</h2>
{#if keychip}
{#if isLoading}
<p>{t('loading')}</p>
{:else}
<div class="setup-step">
1. <div>{@html t('setup.steps.one')}</div>
</div>
<blockquote class="info">
{t('setup.keychip-warning')}
</blockquote>
{#if !!window.showOpenFilePicker}
{#if user.canModifyKeychips}
<details>
<summary>{t('setup.type.automatic')}</summary>
{@html t('setup.automatic')}
{#if automaticSetupStatus != "none"}
<blockquote class={`keychip-status ${automaticSetupStatus}`}>
{t(`setup.automatic.${automaticSetupStatus}`)}
</blockquote>
{/if}
<div class="setup-btn">
<button on:click={patchSegatools}>{t('setup.automatic.select')}</button>
<summary>{t('setup.keychip')}</summary>
<p>
{t('setup.keychip.warning')}
</p>
<div class="keychip-list">
{#each keychips as k}
<div class="keychip-item" class:selected={k === selectedKeychip}>
<button class="keychip-select" on:click={() => selectKeychip(k)}>
{formatKeychipDisplay(k)}
</button>
<button class="keychip-delete danger" on:click={() => deleteKeychip(k)}>
{t('setup.keychip.delete')}
</button>
</div>
{/each}
<form class="add-keychip-form" on:submit|preventDefault={addKeychip}>
<input
type="text"
placeholder={t('setup.keychip.placeholder')}
maxlength="16"
bind:value={newKeychip}
required
/>
<button class="add-keychip" type="submit" disabled={isAdding}>
{isAdding ? t('loading') : t('setup.keychip.add')}
</button>
</form>
{#if addKeychipError}
<p class="danger">{addKeychipError}</p>
{/if}
</div>
</details>
<div class="divider"></div>
{/if}
<details>
<summary>{t('setup.type.manual')}</summary>
{@html t('setup.manual')}
<div class="code-container">
<div class="code" class:revealed={exposeKeychip}>
{@html keychipCode}
{#if selectedKeychip}
{#if !!window.showOpenFilePicker}
<details>
<summary>{t('setup.type.automatic')}</summary>
{@html t('setup.automatic')}
{#if automaticSetupStatus != "none"}
<blockquote class={`keychip-status ${automaticSetupStatus}`}>
{t(`setup.automatic.${automaticSetupStatus}`)}
</blockquote>
{/if}
<div class="setup-btn">
<button on:click={patchSegatools}>{t('setup.automatic.select')}</button>
</div>
</details>
{/if}
<details>
<summary>{t('setup.type.manual')}</summary>
{@html t('setup.manual')}
<div class="code-container">
<div class="code" class:revealed={exposeKeychip}>
{@html keychipCode}
</div>
{#if !exposeKeychip}
<button class="reveal-btn" on:click={() => exposeKeychip = true}>
{t('setup.reveal-keychip')}
</button>
{/if}
</div>
{#if !exposeKeychip}
<button class="reveal-btn" on:click={() => exposeKeychip = true}>
{t('setup.reveal-keychip')}
</button>
{/if}
</div>
</details>
<br>
</details>
<br>
{/if}
<div class="setup-step">
2. <div>{@html t('setup.steps.two')}</div>
@@ -93,15 +201,21 @@ id=${keychip}`.trim(), {
<div class="setup-step">
3. <div>{@html t('setup.steps.three')}</div>
</div>
<div class="setup-step">
4. <div>{@html t('setup.steps.four')}</div>
</div>
<h2>{t('setup.troubleshooting.header')}</h2>
<ul>
<li>
{@html t('setup.troubleshooting.items.one')}
</li>
<li>
{@html t('setup.troubleshooting.items.two')}
</li>
<li>
{@html t('setup.troubleshooting.items.three')}
</li>
</ul>
<p>
{@html t('setup.support-info')}
</p>
{:else}
<p>{t('loading')}</p>
{/if}
</div>
</main>
@@ -111,6 +225,21 @@ id=${keychip}`.trim(), {
.code
overflow-x: auto
ul
li
margin: 0.75em 0
.divider
width: 90%
height: 1px
background: #fff3
margin: 1em 0
position: relative
left: 50%
transform: translate(-50%, 0)
:global(pre.shiki)
background-color: transparent !important
@@ -167,5 +296,48 @@ id=${keychip}`.trim(), {
top: 50%
left: 50%
transform: translate(-50%, -50%)
.keychip-list
display: flex
flex-direction: column
gap: 0.5em
margin: 1em 0
.keychip-item
display: flex
align-items: center
gap: 0.5em
padding: 0.25em 0.5em
border-radius: 4px
&.selected
background: vars.$c-shadow
.keychip-select
font-family: monospace
flex: 1
text-align: left
.add-keychip
align-self: flex-start
margin-top: 0.25em
.add-keychip-form
display: flex
flex-wrap: wrap
gap: 0.5em
align-items: center
margin-top: 0.25em
input
flex: 1 1 16rem
max-width: 16rem
button
flex: 1 1 10rem
max-width: 8rem
.danger
color: vars.$c-error
</style>
+98 -18
View File
@@ -1,11 +1,80 @@
<script lang="ts">
import {GAME} from "../libs/sdk";
import {AQUA_HOST} from "../libs/config";
<script lang="ts">
import { onMount } from "svelte";
import { GAME } from "../libs/sdk";
import type { PhotoPage } from "../libs/sdk";
import { AQUA_HOST } from "../libs/config";
import Loading from "../components/ui/Loading.svelte";
import Error from "../components/ui/Error.svelte";
import Pagination from "../components/Pagination.svelte";
import { t } from "../libs/i18n";
const token = localStorage.getItem("token")
let page = 1;
const pageSize = 12;
let photoData: PhotoPage | null = null;
let loading = true;
let error: any = null;
// Monotonic token so a slow response can never overwrite a newer one
let requestSeq = 0;
function readPageFromUrl(): number {
const raw = new URL(window.location.toString()).searchParams.get("page");
return raw ? parseInt(raw, 10) || 1 : 1;
}
/**
* Keep the url in sync with the page the server actually served, since
* out-of-range pages are clamped server-side.
*/
function syncUrl(effectivePage: number) {
const url = new URL(window.location.toString());
const current = url.searchParams.get("page");
const target = effectivePage > 1 ? effectivePage.toString() : null;
if (current === target) return;
if (target === null) url.searchParams.delete("page");
else url.searchParams.set("page", target);
history.replaceState({}, "", url.toString());
}
async function loadPhotos(targetPage: number) {
const seq = ++requestSeq;
// Apply the target immediately so the pager and further clicks act on it
// instead of on the page that is still being fetched
page = targetPage;
loading = true;
error = null;
try {
const data = await GAME.photos(targetPage, pageSize);
if (seq !== requestSeq) return;
photoData = data;
page = data.page;
syncUrl(data.page);
} catch (e) {
if (seq !== requestSeq) return;
error = e;
} finally {
if (seq === requestSeq) loading = false;
}
}
function handleUpdatePage(event: CustomEvent<number>) {
const newPage = event.detail;
if (newPage === page) return;
const url = new URL(window.location.toString());
url.searchParams.set("page", newPage.toString());
history.pushState({}, "", url.toString());
loadPhotos(newPage);
window.scrollTo({ top: 0, behavior: "smooth" });
}
onMount(() => {
loadPhotos(readPageFromUrl());
const onPopState = () => loadPhotos(readPageFromUrl());
window.addEventListener("popstate", onPopState);
return () => window.removeEventListener("popstate", onPopState);
});
</script>
<main class="content">
@@ -13,24 +82,31 @@
<h2>{t("maiphoto.title")}</h2>
</div>
{#await GAME.photos()}
{#if loading && !photoData}
<Loading/>
{:then photos}
{#if photos.length === 0}
{:else if error}
<Error {error}/>
{:else if photoData}
{#if photoData.total === 0}
<blockquote class="info">{t('maiphoto.none')}</blockquote>
{:else}
<blockquote class="info">{t('maiphoto.url_warning')}</blockquote>
{#if photoData.totalPages > 1}
<Pagination {page} totalPages={photoData.totalPages} on:updatePage={handleUpdatePage} />
{/if}
<div class="pictures" class:loading>
{#each photoData.photos as photo (photo)}
<div class="photo-container">
<img class="rounded-2xl" loading="lazy" src="{AQUA_HOST}/api/v2/game/mai2/my-photo/{photo}" alt="Mai Memorial Photo" />
</div>
{/each}
</div>
{#if photoData.totalPages > 1}
<Pagination {page} totalPages={photoData.totalPages} on:updatePage={handleUpdatePage} />
{/if}
{/if}
<div class="pictures">
{#each photos as photo}
<div class="photo-container">
<img class="rounded-2xl" src="{AQUA_HOST}/api/v2/game/mai2/my-photo/{photo}?token={token}" alt="Memorial" />
</div>
{/each}
</div>
{:catch error}
<Error {error}/>
{/await}
{/if}
</main>
<style lang="sass">
@@ -42,6 +118,10 @@
justify-content: center
row-gap: 1rem
gap: 1rem
transition: opacity 0.2s ease-in-out
&.loading
opacity: 0.5
.photo-container
flex: 1 1 300px
+24 -9
View File
@@ -3,15 +3,15 @@
import { title } from "../libs/ui";
import { GAME } from "../libs/sdk";
import type { GenericRanking } from "../libs/generalTypes";
import StatusOverlays from "../components/StatusOverlays.svelte";
import type { GameName } from "../libs/scoring";
import { GAME_TITLE } from "../libs/i18n";
import { t } from "../libs/i18n";
import UserCard from "../components/UserCard.svelte";
import Tooltip from "../components/Tooltip.svelte";
import Cap from "./Ranking/Cap.svelte";
import { DEFAULT_GAME } from "../libs/config";
export let game: GameName = 'mai2';
export let game: GameName = DEFAULT_GAME;
title(`Ranking`);
@@ -70,11 +70,18 @@
<span>{user.name}</span>
{/if}
</span>
<span class="rating">{
game === 'chu3' || game === 'ongeki' ?
(user.rating / 100).toFixed(2) :
user.rating.toLocaleString()
}</span>
{#if game == 'ongeki'}
<span class="rating">{
(user.rating / 1000).toFixed(3)
}</span>
{:else}
<span class="rating">{
game === 'chu3' ?
(user.rating / 100).toFixed(2) :
user.rating.toLocaleString()
}</span>
{/if}
<span class="accuracy">{(+user.accuracy).toFixed(2)}%</span>
<span class="fc">{user.fullCombo}</span>
<span class="ap">{user.allPerfect}</span>
@@ -86,9 +93,9 @@
<Tooltip triggeredBy=".name" loading={hoverLoading}>
<UserCard username={hoveringUser} {game} setLoading={l => hoverLoading = l} />
</Tooltip>
{:else}
<p>{error}</p>
{/if}
<StatusOverlays error={error} loading={!loadedPages} />
</main>
<style lang="sass">
@@ -141,6 +148,14 @@
.accuracy
display: none
.modern-rating
display: none
.legacy-rating
/* this is officially the worst css i've ever written dear god */
white-space: nowrap
overflow: hidden
text-overflow: clip
min-width: 3.5em !important
&.alternate
background-color: vars.$ov-light
+12 -1
View File
@@ -95,7 +95,9 @@
bind:tested={srcTested} bind:this={srcEl} bind:exportedData={srcExportedData} />
<div class="arrow" class:disabled={!(srcTested && dstTested)}>
<img src="{DATA_HOST}/d/DownArrow.png" alt="arrow" on:click={startTransfer}>
<button class="arrow-btn" on:click={startTransfer}>
<img src="{DATA_HOST}/d/DownArrow.png" alt="arrow">
</button>
</div>
<TransferServer bind:src={dst} bind:gameInfo={gameInfo} on:change={onChange}
@@ -115,6 +117,15 @@
&.disabled
filter: grayscale(1)
.arrow-btn
background: none
border: none
padding: 0
cursor: pointer
&:disabled
cursor: not-allowed
// CSS animation to let the image opacity breathe
img
animation: breathe 1s infinite alternate
@@ -107,7 +107,7 @@
validate={v => /^https?:\/\/[a-z0-9.-]+(:\d+)?$/i.test(v)} />
<InputTextShort desc={t('trans.field.keychip')} placeholder="e.g. A0299792458"
bind:value={src.keychip} on:change disabled={tested}
validate={v => /^([A-Z0-9]{11}|[A-Z0-9]{4}-[A-Z0-9]{11})$/.test(v)} />
validate={v => /^([A-Z\d]{15}|[A-Z\d]{4}-[A-Z\d]{11})$/.test(v)} />
</div>
<!-- Second input line -->
+10 -2
View File
@@ -14,6 +14,7 @@
import UserSettings from "../../components/settings/UserSettings.svelte";
import type { Component } from "svelte";
import { EN_REF } from "../../libs/i18n/en_ref";
import type { GameName } from "../../libs/scoring";
USER.ensureLoggedIn()
@@ -30,15 +31,22 @@
if (!pages[page] && page)
error = t("404", {pathname: new URL(location.href).pathname});
USER.me().then(m => me = m)
let userGames: GameName[] = [];
USER.me().then(m => {
me = m
CARD.userGames(m.username).then(cards => userGames = Object.keys(cards).filter(k => !!cards[k as GameName]) as GameName[])
})
.catch(e => error = e.message)
</script>
<main class="content">
<div class="outer-title-options">
<h2>{t('settings.title')}</h2>
<nav>
{#each Object.entries(pages) as tab}
{#each Object.entries(pages).filter(v => v[0] == "profile" || v[0] == "global" || userGames.includes(v[0] as GameName)) as tab}
<a href={`/settings/${tab[0] != "profile" ? tab[0] : ""}`} transition:slide={{axis: 'x'}}
class:active={tab[0] == page || (tab[0] == "profile" && !page)} role="button" tabindex="0">
{ts(`settings.tabs.${tab[0]}`)}
+65 -34
View File
@@ -29,12 +29,11 @@
registerChart()
export let username: string;
export let game: GameName;
export let username: string = "";
export let game: GameName = "" as GameName;
let calElement: HTMLElement
let error: string;
let me: AquaNetUser
title(`User ${username}`)
const rounding = useLocalStorage("rounding", true);
interface MusicAndPlay extends MusicMeta, GenericGamePlaylog {}
@@ -51,6 +50,7 @@
let isLoading = false
let showMoreRecent = false
function init() {
USER.isLoggedIn() && USER.me().then(u => me = u)
@@ -77,11 +77,13 @@
GAME.userSummary(username, game),
GAME.trend(username, game),
DATA.allMusic(game),
]).then(([user, trend, music]) => {
]).then(async ([user, trend, music]) => {
console.log(user)
console.log(trend)
console.log(games)
title(` ${user.name} (@${username}) ・ ${GAME_TITLE[game]}`)
// If game is wacca, divide all ratings by 10
if (game === 'wacca') {
user.rating /= 10
@@ -161,7 +163,7 @@
<a href={`/u/${username}/${g}`} class:active={game === g}>{name}</a>
{/each}
{#if me && me.username === username}
{#if me && me.username.toLowerCase() === username.toLowerCase()}
<a class="setting-icon clickable" use:tooltip={t("UserHome.Settings")} href={`/settings/${game}`}>
<Icon icon="eos-icons:rotating-gear"/>
</a>
@@ -187,19 +189,31 @@
<div class="scoring-info">
<div class="chart">
<div class="info-top">
<div class="rating">
<span>{game === 'mai2' ? t("UserHome.DXRating"): t("UserHome.Rating")}</span>
<span>{
game === 'chu3' || game === 'ongeki' ?
(d.user.rating / 100).toFixed(2) :
d.user.rating.toLocaleString()
}</span>
</div>
<div class="rank">
<span>{t('UserHome.ServerRank')}</span>
<span>#{(d.user.serverRank + 1).toLocaleString()}</span>
</div>
{#if game == "ongeki" && d.user.ratingNotGeneric}
<div class="rating">
<span>{t("UserHome.Rating")}</span>
<span>{(d.user.rating / 1000).toFixed(3)}</span>
</div>
{:else}
<div class="rating">
<span>{game === 'mai2' ? t("UserHome.DXRating"): t("UserHome.Rating")}</span>
<!-- NOTE: this is for legacy ongeki display, so 1.45 profiles are at least usable -->
<span>{
game === 'chu3' || game === 'ongeki' ?
(d.user.rating / 100).toFixed(2) :
d.user.rating.toLocaleString()
}</span>
</div>
{/if}
{#if !isNaN(parseInt(d.user.serverRank))}
<div class="rank">
<span>{t('UserHome.ServerRank')}</span>
<span>#{(d.user.serverRank).toLocaleString()}</span>
</div>
{/if}
</div>
<div class="trend">
@@ -247,6 +261,23 @@
<span>{(d.user.accuracy).toFixed(2)}%</span>
</div>
{#if game == "ongeki" && d.user.ratingNotGeneric}
<div class="rating">
<span>{t("UserHome.HighestRating")}</span>
<span>{(d.user.ratingHighest / 1000).toFixed(3)}</span>
</div>
{:else}
<div class="rating">
<span>{game === 'mai2' ? t("UserHome.HighestDXRating"): t("UserHome.HighestRating")}</span>
<!-- NOTE: this is for legacy ongeki display, so 1.45 profiles are at least usable -->
<span>{
game === 'chu3' || game === 'ongeki' ?
(d.user.ratingHighest / 100).toFixed(2) :
d.user.ratingHighest.toLocaleString()
}</span>
</div>
{/if}
<div class="max-combo">
<span>{t("UserHome.MaxCombo")}</span>
<span>{d.user.maxCombo}</span>
@@ -306,18 +337,25 @@
</div>
</div>
<!-- I don't like doing this but it may be preferable to gaslighting the types -->
<RatingComposition title={t("UserHome.RatingComposition.Best", {n: 50})} comp={d.user.ratingComposition.best50} {allMusics} {game}/>
<RatingComposition title={t("UserHome.RatingComposition.Best", {n: 35})} comp={d.user.ratingComposition.best35} {allMusics} {game}/>
<RatingComposition title={t("UserHome.RatingComposition.Best", {n: 30})} comp={d.user.ratingComposition.best30} {allMusics} {game}/>
<RatingComposition title={t("UserHome.RatingComposition.Best", {n: 15})} comp={d.user.ratingComposition.best15} {allMusics} {game}/>
<RatingComposition title={t("UserHome.RatingComposition.New", {n : 15})} comp={d.user.ratingComposition.new15} {allMusics} {game}/>
<RatingComposition title={t("UserHome.RatingComposition.New", {n: 10})} comp={d.user.ratingComposition.new10} {allMusics} {game}/>
<RatingComposition title={t("UserHome.RatingComposition.Platinum", {n: 50})} comp={d.user.ratingComposition.pscore50} {allMusics} {game}/>
{#if me && me.displayCandidates && d.user.aquaUser && me.username == d.user.aquaUser.username}
<RatingComposition title={t("UserHome.RatingComposition.BestCandidates", {n: 20})} comp={d.user.ratingComposition.best20_candidates} {allMusics} {game}/>
<RatingComposition title={t("UserHome.RatingComposition.NewCandidates", {n: 10})} comp={d.user.ratingComposition.new10_candidates} {allMusics} {game}/>
<RatingComposition title={t("UserHome.RatingComposition.PlatinumCandidates", {n: 20})} comp={d.user.ratingComposition.pscore20_candidates} {allMusics} {game}/>
{/if}
<RatingComposition title="B30" comp={d.user.ratingComposition.best30} {allMusics} {game}/>
<RatingComposition title="B35" comp={d.user.ratingComposition.best35} {allMusics} {game}/>
<RatingComposition title="B15" comp={d.user.ratingComposition.best15} {allMusics} {game}/>
<!-- <RatingComposition title="Hot 10" comp={d.user.ratingComposition.hot10} {allMusics} {game}/> -->
<!-- <RatingComposition title="N10" comp={d.user.ratingComposition.next10} {allMusics} {game}/> -->
<!-- Chuni -->
{#if d.user.ratingComposition.new}
<RatingComposition title="New 20" comp={d.user.ratingComposition.new} {allMusics} game="chu3"/>
<RatingComposition title={t("UserHome.RatingComposition.New", {n: 20})} comp={d.user.ratingComposition.new} {allMusics} game="chu3"/>
{:else}
<RatingComposition title="Recent 10" comp={d.user.ratingComposition.recent10} {allMusics} {game} top={10}/>
<RatingComposition title={t("UserHome.RatingComposition.Recent", {n: 10})} comp={d.user.ratingComposition.recent10} {allMusics} {game} top={10}/>
{/if}
<div class="recent">
@@ -494,7 +532,7 @@
.scoring-info
display: flex
gap: vars.$gap
max-height: 250px
max-height: 300px
.chart
flex: 0 1 790px
@@ -575,7 +613,7 @@
display: flex
align-items: center
gap: 20px
background-color: rgba(white, 0.03)
border-radius: vars.$border-radius
@@ -593,10 +631,6 @@
overflow: hidden
flex-direction: column
.first-line
display: flex
flex-direction: row
// Limit song name to one line
.song-title
max-width: 90%
@@ -612,9 +646,6 @@
flex-direction: column
gap: 0
.rank-text
text-align: left
// Recent Scores section
.recent
.scores
+18
View File
@@ -6,6 +6,14 @@
import { USER } from "../libs/sdk";
import { t } from "../libs/i18n"
import * as acUrl from "../libs/acUrl"
const hasAc = acUrl.has()
let previewAc = ""
if (hasAc) {
previewAc = acUrl.preview() || ""
}
let params = new URLSearchParams(window.location.search)
let state = "home"
@@ -178,6 +186,13 @@
<main id="home" class="no-margin">
<div>
<h1 id="title">AquaNet</h1>
<div id="welcome">
{#if hasAc}
<span>{t('welcome.login_link')}:</span><br>
<small>{previewAc}</small>
{/if}
</div>
{#if state === "home"}
<div class="btn-group" transition:slide>
<button on:click={() => state = 'login'}>{t('welcome.btn-login')}</button>
@@ -329,6 +344,9 @@
margin-bottom: 32px
opacity: 0.9
#welcome
margin-bottom: 32px
.btn-group
display: flex
gap: 8px
+14 -32
View File
@@ -1,37 +1,19 @@
# CONTRIBUTING
Thanks for your interest in improving Aqua server, and huge welcome! This document will guide you how to get started.
# Contribution
# Before you start
Thank you for your interest in contributing to the AquaDX server and/or AquaNet web interface implementation.
Please look over the following guidelines before creating an issue or pull request.
* We (or at least I, the fork maintainer) will not care where you come from or what kind of scene you involve with as long as you are in good faith.
* This fork might have n-0 or even n+1 support from time to time. If you can't agree with this, please refrain from get involve with this project.
* We **will not help you to get game itself**. This includes sharing a patched or original binary. Please don't ask or give guidance about it within this repository. This is bare minimum for our safety and long run possibility of this project.
- We try to support game versions that are n-0 (and occasionally n+1).<br>
By using this server and/or contributing source, you understand some features may be incomplete due to restrictions (whether it's technical or due to it being n-0).
# Getting started
- Issues should describe a problem or feature request for either AquaDX or AquaNet. <br>
Please do not ask for game dumps, ask for assistance with setting up a game dump or send links to game dumps.
* Please make sure you read README.md and documents in /docs folder.
* Make sure you have account for this gitlab instance.
- Pull requests should have meaningful descriptions and changes should match the rest of our codebase to be accepted.<br>
Commits can be formatted in any way preferable as long as they're easily digestable and meaningful.<br>
All pull requests must be tested and ensured functional.<br>
If contributing to the server (AquaDX), the server should be capable of booting to the colorful logo & affected clients should be able to connect and make use of your contribution(s).<br>
If contributing to the web interface (AquaNet), it should be capable of building, shouldn't report any errors and should make use of your contribution(s).
# Steps to follow
* Open a issue: if you found a bug or want to add or suggest new feature, try open a issue first so we can know about the problem and start discussion.
* Include some kind of information (for example: error log if a bug; what you trying to achieve if a new feature)
* This step is not essential but will make thing much easier later on. (PR)
* If you are end user (not a developer), it ends in here. Thanks!
* Fork it and make a new branch where you start working on it.
* Please make a branch per PR if you are thinking about making multiple PRs. Still, we can cherry-pick even it's not but..
* Start working
* We don't have any code formatting guide or rule. As long as code runs fine and readable enough, good to go.
* Make commits of logical and atomic units. Please DO NOT make a commit message too general like "WIP - 1" or "Work".
* Test if it works
* Database changes: default database testing is sqlite only. If you are not sure or can't test with MariaDB or MySQL, just leave a message about it in the PR.
* Game endpoint changes: Although aqua have testing process while building and packaging, this probably won't do much for the game so our recommendation is run through a game at least once (from entry to saving).
* Server API / feature / bug fix: Just test at least once the way you want.
* Make a PR
* Thanks! We will get you know when it's ready.
We appreciate your time. Contribution is what makes open source project interesting and keep working.
- AquaDX makes use of the [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en) license.<br>
By contributing, you agree your contributions can be placed under this license.
+7 -4
View File
@@ -1,7 +1,6 @@
# Use a multi-stage build to keep the image size small
# Start with a Gradle image for building the project
#FROM gradle:jdk21-alpine as builder
FROM gradle:8.8.0-jdk21 as builder
# Pin the builder so ARM64 deployments cannot reuse an older floating jdk25 image.
FROM gradle:9.6.1-jdk25 AS builder
# Copy the Gradle wrapper and configuration files separately to leverage Docker cache
COPY --chown=gradle:gradle gradlew /home/gradle/
@@ -16,16 +15,20 @@ RUN sed -i 's/\r$//' ./gradlew
# Download dependencies - cached if build.gradle.kts and settings.gradle.kts are unchanged
RUN chmod +x ./gradlew
RUN java -version
RUN ./gradlew dependencies
# Copy the project source, this layer is rebuilt whenever a file has changed
COPY --chown=gradle:gradle src /home/gradle/src
# Download game metadata (music/items lists)
RUN bash /home/gradle/src/main/resources/meta/update.sh
# Build the application
RUN ./gradlew build -x test
# Start with a fresh image for the runtime
FROM eclipse-temurin:21-jre-alpine
FROM eclipse-temurin:25-jre-alpine
# Set the deployment directory
WORKDIR /app
+10 -9
View File
@@ -32,21 +32,22 @@ Multipurpose game server for ALL.Net games.
> [!WARNING]
> CHUNITHM pre-NEW!! and maimai pre-DX are no longer supported after February 6th, 2026 and all associated data will be removed.
| Game | Latest Ver. | Initial Ver. | Notes | Web UI | Data Import |
|------------------------|---------------------|---------------------|-------------------------------------------------------------|--------|-------------|
| SDHD: CHUNITHM | 2.45 (X-VERSE-X) | 2.00 (NEW) | | ✅ | ❗ |
| SDEZ: maimai DX | 1.65 (CiRCLE PLUS) | 1.00 (DX) | Missing circle (teams) support | ✅ | ✅ |
| SDGA: maimai DX (Intl) | 1.60 (CiRCLE) | 1.00 (DX) | Thanks [@Clansty](https://github.com/clansty) | ✅ | ✅ |
| SDED: Card Maker | 1.39 | N/A | Thanks [@Becods](https://github.com/Becods) | ❌ | ❌ |
| SDDT: O.N.G.E.K.I. | 1.50 (Re:Fresh) | N/A | Thanks [@PenguinCaptain](https://github.com/PenguinCaptain) | ✅ | ❌ |
| SBZV: Project DIVA | 7.10 | N/A | Minimal support | ❌ | ❌ |
| SDFE: Wacca | 3.07 (Reverse) | N/A | Later versions are EOS patches, network will not work | ✅ | ❌ |
| Game | Latest Ver. | Initial Ver. | Notes | Web UI | Import | AES |
|------------------------|--------------------|---------------------|-------------------------------------------------------------|--------|-------------|---------|
| SDHD: CHUNITHM | 2.50 (MATE) | 2.00 (NEW) | | ✅ | ✅ | ✅ |
| SDEZ: maimai DX | 1.65 (CiRCLE PLUS) | 1.00 (DX) | Thanks [@Menci](https://github.com/Menci) | ✅ | ✅ | ✅ |
| SDGA: maimai DX (Intl) | 1.60 (CiRCLE) | 1.00 (DX) | Thanks [@Clansty](https://github.com/clansty) | ✅ | ✅ | ✅ |
| SDED: Card Maker | 1.39 | N/A | Thanks [@Becods](https://github.com/Becods) | ❌ | ❌ | ❌ |
| SDDT: O.N.G.E.K.I. | 1.50 (Re:Fresh) | N/A | Thanks [@PenguinCaptain](https://github.com/PenguinCaptain) | ✅ | ✅ | ✅ |
| SBZV: Project DIVA | 7.10 | N/A | Minimal support | ❌ | ❌ | 🔓 |
| SDFE: Wacca | 3.07 (REVERSE) | N/A | Later versions are EOS patches | ✅ | ❌ | 🔓 |
<!-- A majority of them have been left as N/A for initial version as they do not appear to have restrictions -->
Check out these docs for more information.
* [Game specific notes](docs/game_specific_notes.md)
* [Frequently asked questions](docs/frequently_asked_questions.md)
* [Encryption notes](docs/encryption.md)
## Usage
+192 -27
View File
@@ -1,36 +1,201 @@
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
kotlin("jvm") version "2.1.10" apply false
kotlin("plugin.spring") version "2.1.10" apply false
kotlin("plugin.jpa") version "2.1.10" apply false
kotlin("plugin.serialization") version "2.1.10" apply false
kotlin("plugin.allopen") version "2.1.10" apply false
id("org.springframework.boot") version "3.2.3" apply false
id("io.spring.dependency-management") version "1.1.4" apply false
id("com.github.ben-manes.versions") version "0.51.0"
val ktVer = "2.4.0"
java
kotlin("jvm") version ktVer
kotlin("plugin.spring") version ktVer
kotlin("plugin.jpa") version ktVer
kotlin("plugin.serialization") version ktVer
kotlin("plugin.allopen") version ktVer
kotlin("kapt") version ktVer
id("org.springframework.boot") version "4.1.0"
id("com.github.ben-manes.versions") version "0.54.0"
id("org.hibernate.orm") version "7.4.4.Final"
application
}
allprojects {
group = "icu.samnyan.aqua"
version = "1.0.0"
apply(plugin = "io.spring.dependency-management")
repositories {
mavenLocal()
mavenCentral()
repositories {
mavenLocal()
mavenCentral()
}
extra["netty.version"] = "4.2.16.Final"
extra["kotlin-coroutines.version"] = "1.11.0"
dependencies {
// Spring boot
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-web") {
exclude(group = "org.springframework.boot", module = "spring-boot-starter-tomcat")
}
implementation("org.springframework.boot:spring-boot-starter-jetty")
implementation(enforcedPlatform("io.netty:netty-bom:4.2.16.Final"))
implementation("io.netty:netty-all:4.2.16.Final")
implementation("org.apache.commons:commons-lang3:3.20.0")
implementation("org.apache.httpcomponents.client5:httpclient5:5.6.2")
implementation("org.springframework.boot:spring-boot-starter-flyway")
implementation("org.flywaydb:flyway-core:12.10.0")
implementation("org.flywaydb:flyway-mysql:12.10.0")
testImplementation("org.springframework.boot:spring-boot-starter-test") {
exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
}
testImplementation("org.springframework.security:spring-security-test")
implementation("net.logstash.logback:logstash-logback-encoder:9.0")
// Metrics
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("io.micrometer:micrometer-registry-prometheus")
// Database
runtimeOnly("org.mariadb.jdbc:mariadb-java-client:3.5.9")
runtimeOnly("org.xerial:sqlite-jdbc:3.53.2.0")
implementation("org.hibernate.orm:hibernate-core:7.4.4.Final")
implementation("org.hibernate.orm:hibernate-community-dialects:7.4.4.Final")
implementation("io.github.openfeign.querydsl:querydsl-jpa:7.4.0")
kapt("io.github.openfeign.querydsl:querydsl-apt:7.4.0:jpa")
// JSR305 for nullable
implementation("com.google.code.findbugs:jsr305:3.0.2")
// =============================
// AquaNet Specific Dependencies
// =============================
// Network
implementation("io.ktor:ktor-client-core:3.5.1")
implementation("io.ktor:ktor-client-cio:3.5.1")
implementation("io.ktor:ktor-client-content-negotiation:3.5.1")
implementation("io.ktor:ktor-client-encoding:3.5.1")
implementation("io.ktor:ktor-serialization-kotlinx-json:3.5.1")
implementation("org.jetbrains.kotlin:kotlin-reflect")
// Somehow these are needed for ktor even though they're not in the documentation
runtimeOnly("org.reactivestreams:reactive-streams:1.0.4")
runtimeOnly("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.11.0")
// Email
implementation("org.simplejavamail:simple-java-mail:9.0.1")
implementation("org.simplejavamail:spring-module:9.0.1")
// GeoIP
implementation("com.maxmind.geoip2:geoip2:5.1.0")
// JWT Authentication
implementation("io.jsonwebtoken:jjwt-api:0.13.0")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.13.0")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.13.0")
// Content validation
implementation("org.apache.tika:tika-core:3.3.1")
// Serialization (Jackson 3; JSR-310 support is built into jackson-databind 3.x)
implementation("tools.jackson.module:jackson-module-kotlin:3.2.1")
// Testing
testImplementation("io.kotest:kotest-runner-junit5-jvm:6.2.2")
testImplementation("io.kotest:kotest-assertions-core:6.2.2")
}
group = "icu.samnya"
version = "1.0.0"
description = "AquaDX Arcade Server"
java {
sourceCompatibility = JavaVersion.VERSION_25
targetCompatibility = JavaVersion.VERSION_25
toolchain {
languageVersion.set(JavaLanguageVersion.of(25))
}
}
subprojects {
apply(plugin = "org.jetbrains.kotlin.jvm")
// Use configure to access the extension safely in subprojects
configure<org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension> {
jvmToolchain(21)
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21)
freeCompilerArgs.add("-Xjsr305=strict")
}
kotlin {
jvmToolchain(25)
compilerOptions {
jvmTarget.set(JvmTarget.JVM_25)
}
}
springBoot {
mainClass.set("icu.samnyan.aqua.EntryKt")
}
application {
mainClass = "icu.samnyan.aqua.EntryKt"
}
kapt {
includeCompileClasspath = false
keepJavacAnnotationProcessors = true
}
allOpen {
annotation("jakarta.persistence.Entity")
annotation("jakarta.persistence.MappedSuperclass")
annotation("jakarta.persistence.Embeddable")
}
val buildTime = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z").withZone(ZoneId.of("UTC")).format(Instant.now())
val projectVersion = version.toString()
extra["buildTime"] = buildTime
tasks.processResources {
filesMatching("**/application.properties") {
expand(
mapOf(
"version" to projectVersion,
"ext" to mapOf("buildTime" to buildTime),
)
)
}
}
tasks.test {
enabled = providers.gradleProperty("runTests").isPresent
useJUnitPlatform()
jvmArgs("-Dkotest.assertions.collection.print.size=100")
}
tasks.withType<JavaCompile>().configureEach {
options.encoding = "UTF-8"
}
tasks.withType<Javadoc>().configureEach {
options.encoding = "UTF-8"
}
tasks.named<Jar>("jar") {
enabled = false
}
sourceSets {
main {
java.srcDir("${layout.buildDirectory.get()}/generated/source/kapt/main")
}
}
val copyDependencies = tasks.register<Copy>("copyDependencies") {
from(configurations.runtimeClasspath)
into("${layout.buildDirectory.get()}/libs/lib")
}
tasks.register<Jar>("packageThin") {
group = "build"
from(sourceSets.main.get().output)
manifest {
attributes(
"Main-Class" to "icu.samnyan.aqua.EntryKt",
"Class-Path" to configurations.runtimeClasspath.get().files.joinToString(" ") { "lib/${it.name}" }
)
}
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
dependsOn(copyDependencies)
}
-26
View File
@@ -1,26 +0,0 @@
plugins {
kotlin("jvm")
kotlin("plugin.serialization")
id("application")
}
dependencies {
implementation(project(":shared"))
// Network
implementation("io.ktor:ktor-client-core:3.0.3")
implementation("io.ktor:ktor-client-cio:3.0.3")
implementation("io.ktor:ktor-client-content-negotiation:3.0.3")
implementation("io.ktor:ktor-serialization-kotlinx-json:3.0.3")
// Serialization
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.0")
// Testing
testImplementation("io.kotest:kotest-runner-junit5-jvm:5.8.1")
}
application {
mainClass = "icu.samnyan.aqua.comet.EntryKt"
}
+2 -1
View File
@@ -97,6 +97,7 @@ spring.datasource.hikari.maximum-pool-size=10
## Link card limit
aqua-net.link-card-limit=10
aqua-net.keychip-limit=20
## CloudFlare Turnstile Captcha
## This enables captcha for user registration.
@@ -141,4 +142,4 @@ aqua-net.bot.enabled=true
aqua-net.bot.secret=hunter2
## OpenAI Settings (For content moderation)
aqua-net.openai.api-key=sk-1234567890abcdef1234567890abcdef
aqua-net.openai.api-key=sk-1234567890abcdef1234567890abcdef
+1 -1
View File
@@ -1,7 +1,7 @@
FROM archlinux:latest
RUN pacman -Syu --noconfirm \
&& pacman -S --noconfirm openssh sudo jdk21-openjdk wget which procps-ng zsh git curlie micro ripgrep python3 exa diffutils \
&& pacman -S --noconfirm openssh sudo jdk25-openjdk wget which procps-ng zsh git curlie micro ripgrep python3 exa \
&& rm -rf /var/cache/pacman/pkg/* \
&& mkdir -p /var/run/sshd \
&& chsh -s /bin/zsh root \
+1
View File
@@ -1,5 +1,6 @@
services:
app:
build: .
image: hykilpikonna/aquadx:latest
ports:
- "80:80"
+40 -4
View File
@@ -17,10 +17,46 @@
| 11 | Frame |
| 12 | Tickets |
| 13 | Mile |
| 14 | Intimate Item |
| 15 | Kaleidx Scope Key |
## Multiplayer
| 14 | Intimate Item |
| 15 | Kaleidx Scope Key |
## Kaleidx Scope gates
Kaleidx Scope gate visibility is controlled by three server responses:
* `GetGameEventApi` must return open event rows for the gate/course event IDs used by the game's static data.
* `GetGameKaleidxScopeApi` must return a `gameKaleidxScopeList` row for each gate the user can see. Current known gate IDs are `1..6` for the original gates, `7` for Prism/Center Tower, `8` for the post-Prism boss gate, `9` for Hope Gate, and `10` for the final challenge.
* `GetUserKaleidxScopeApi` must return a `userKaleidxScopeList` row for the same `gateId`.
The client hides a gate when the user row is missing, the event is closed, or the row has `isGateFound = false`. A visible selectable gate needs at least:
```json
{
"gateId": 8,
"isGateFound": true,
"isKeyFound": true,
"isClear": false
}
```
The unlock announcement can still appear when `isGateFound = true`, `isKeyFound = true`, and `isInfoWatched = false`; that announcement alone does not prove that the gate will be listed in the menu on the next download.
For progression past Prism Tower, `UpsertUserAllApi` must persist `upsertUserAll.userKaleidxScopeList` by `(userId, gateId)`. AquaDX unlocks Prism/Center Tower gate `7` after gate `6` is clear. After gate `7` is clear, the next `GetUserKaleidxScopeApi` response must include gate `7` as clear and a gate `8` row with both found/key true. The same pattern applies for gates `9` and `10` after their preceding gates are clear.
Fixed AquaDX.v1 issue: older code advertised gates `1..10` from `GetGameKaleidxScopeApi` and saved uploaded Kaleidx rows in `UpsertUserAllApi`, but `GetUserKaleidxScopeApi` only backfilled missing rows for gates `1..6`. It also forced every persisted row to `isKeyFound = true` before returning it. Existing users without usable rows for gate `7` or later could therefore receive an unlock-style message but still have no visible Prism Tower or post-Prism gate in the Kaleidx Scope menu.
To confirm the failure for a user, inspect their rows:
```sql
SELECT gate_id, is_gate_found, is_key_found, is_clear, is_info_watched
FROM maimai2_user_kaleidx
WHERE user_id = <internal_user_id>
ORDER BY gate_id;
```
If gate `7` is clear and gate `8` is missing, or if any target gate row has `is_gate_found = 0`, the menu will not show the next gate.
## Multiplayer
### Party Host/Client/Member
+35
View File
@@ -0,0 +1,35 @@
# Encryption Support
AquaDX supports encryption for Chunithm, Maimai and Ongeki.<br>
It is not required to connect and keys are NOT provided by default.
Card Maker has encryption features in the client, but they are not supported due to it's complexity.
## Adding new keys to AquaDX
> [!IMPORTANT]
> Ensure you're using AquaDX from source. Docker will work, as long as you use `docker-compose up --build`
1. Create a new file at `src/main/resources/data/game/<game name>/game_encryption.json`
2. Fill it out using the following schema:
```json
{
"code": "SDHD",
"versions": [215],
"key": "DECAFBADDECAFBADDECAFBADDECAFBADDECAFBADDECAFBADDECAFBADDECAFBAD",
"iv": "DECAFBADDECAFBADDECAFBADDECAFBAD",
"salt": "DECAFBADDECAFBAD",
"iterations": 36
},
```
**`key`, `salt`, and `iv` MUST be hex strings. Do NOT insert raw ASCII strings.**<br>
Replace `code` with the correct code for your game (`SDHD` for Chunithm JP, `SDEZ` for Maimai JP, etc.).<br>
Set `versions` to be an array of game versions without the decimal (215 for 2.15, 150 for 1.50, etc.).<br>
You must include the rom major version. Minor versions will automatically round down to use the major key.
- Maimai DX does NOT use iterations, you do not need to include it
- Ongeki always uses 64 iterations (you must include it)
- If using a specific Python script to rip Chunithm keys, focus on the `=>` entries.
- `key` is 32 bytes long. `iv` is 16 bytes long. `salt` is 8 bytes long. (hex strings should be twice as long as they are in bytes)
+16 -21
View File
@@ -3,34 +3,35 @@
## Chunithm (Chusan)
### Required patches
* No encryption & TLS
* None
<sub>※ No encryption & No TLS is required if you do not have encryption keys set for your game version (supported on public instance)</sub>
### Additional notes
* Class/Dan and National Matching modes will work after playing the first game
* Class/Dan, National Matching and LINKED VERSE modes will work after playing the first game
(both when you first set up the game and when you update the game's rom or options).
* National Matching requires [additional setup](chu3-national-matching.md).
* For user box customization, use the AquaNet website.
* National Matching is supported out of the box. Services provided by [Yukiotoko](https://gitea.tendokyu.moe/CharaLol/Yukiotoko)
* For user box and LINKED VERSE customization, use the AquaNet website.
* All LINKED VERSE gates are available by default and can be played sequentially. Original condition flags are disregarded due to implementation complexity (and low overall interest).
* Many aspects of the game may not work in freeplay mode, this is not a server-side restriction.
## Maimai DX
### Required patches
* No TLS
* No certificate pinning
* No URI obfuscation
* No encryption
### Non-working features
* KOP related
* Tournament mode
<sub>※ No encryption & No TLS is required if you do not have encryption keys set for your game version (supported on public instance)</sub>
### Additional notes
* KOP related features and tournament mode do not work
* All KALEID×SCOPE gates are intended to be open but may be broken
## O.N.G.E.K.I
### Required patches
* No TLS
* No certificate pinning
* No URI obfuscation
* No encryption
<sub>※ No encryption & No TLS is required if you do not have encryption keys set for your game version (supported on public instance)</sub>
### Non-working features
* KOP related
@@ -39,16 +40,10 @@
## Card Maker
### Required patches
* No TLS
* No encryption
### Support status
* Chunithm New: Yes, New (2.00)
* Maimai DX: Yes, Universe (1.20)
* O.N.G.E.K.I: No
* No TLS & Encryption (Not implemented as it has three (four?) sets of keys across several games, not worth the effort)
### Additional notes
* Only stated version above are supported.
* Server does not consider gacha rarity and probability weight during card draw.
* Server returns same hard-coded serial for each cards. This is intentional behavior.
* Due to its high correlation with every game endpoints, this may cease to work after major game version up.
* Not priority and may be out of date, broken or even removed in the future due to low overall usage (by player count on public instance)
+2 -2
View File
@@ -41,10 +41,10 @@ docker compose up
```
### Building
You need to install JDK 21 on your system, then run `./gradlew clean build`. The jar file will be built into the `build/libs` folder.
You need to install JDK 25 on your system, then run `./gradlew clean build`. The jar file will be built into the `build/libs` folder.
## Why drop SQLite support?
If you wonder why I dropped SQLite support, ask SQLite devs why they still haven't supported adding a single constraint to a table without all the hassle of creating a new one and migrating all data over and finally deleting the original.
![](sqlite-sucks.png)
![](sqlite-sucks.png)
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
-168
View File
@@ -1,168 +0,0 @@
import java.time.ZonedDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
plugins {
val ktVer = "2.1.10"
java
kotlin("jvm") version ktVer
kotlin("plugin.spring") version ktVer
kotlin("plugin.jpa") version ktVer
kotlin("plugin.serialization") version ktVer
kotlin("plugin.allopen") version ktVer
id("org.springframework.boot") version "3.2.3"
id("io.spring.dependency-management") version "1.1.4"
id("com.github.ben-manes.versions") version "0.51.0"
id("org.hibernate.orm") version "6.4.4.Final"
application
}
dependencies {
implementation(project(":shared"))
// Spring boot
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-web") {
exclude(group = "org.springframework.boot", module = "spring-boot-starter-tomcat")
}
implementation("org.springframework.boot:spring-boot-starter-jetty")
implementation("io.netty:netty-all")
implementation("org.apache.commons:commons-lang3:3.14.0")
implementation("org.apache.httpcomponents.client5:httpclient5")
implementation("org.flywaydb:flyway-core:10.10.0")
implementation("org.flywaydb:flyway-mysql:10.10.0")
testImplementation("org.springframework.boot:spring-boot-starter-test") {
exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
}
testImplementation("org.springframework.security:spring-security-test")
implementation("net.logstash.logback:logstash-logback-encoder:7.4")
// Metrics
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("io.micrometer:micrometer-registry-prometheus")
// Database
runtimeOnly("org.mariadb.jdbc:mariadb-java-client:3.3.3")
runtimeOnly("org.xerial:sqlite-jdbc:3.45.2.0")
implementation("org.hibernate.orm:hibernate-core:6.4.4.Final")
implementation("org.hibernate.orm:hibernate-community-dialects:6.4.4.Final")
// JSR305 for nullable
implementation("com.google.code.findbugs:jsr305:3.0.2")
// Network
implementation("io.ktor:ktor-client-core:3.0.3")
implementation("io.ktor:ktor-client-cio:3.0.3")
implementation("io.ktor:ktor-client-content-negotiation:3.0.3")
implementation("io.ktor:ktor-client-encoding:3.0.3")
implementation("io.ktor:ktor-serialization-kotlinx-json:3.0.3")
implementation("org.jetbrains.kotlin:kotlin-reflect")
runtimeOnly("org.reactivestreams:reactive-streams:1.0.4")
runtimeOnly("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.8.0")
// Email
implementation("org.simplejavamail:simple-java-mail:8.6.3")
implementation("org.simplejavamail:spring-module:8.6.3")
// GeoIP
implementation("com.maxmind.geoip2:geoip2:4.2.0")
// JWT Authentication
implementation("io.jsonwebtoken:jjwt-api:0.12.5")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.5")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.5")
// Content validation
implementation("org.apache.tika:tika-core:2.9.1")
// Import: DateTime Parsing
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.0")
// Serialization
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
// Testing
testImplementation("io.kotest:kotest-runner-junit5-jvm:5.8.1")
testImplementation("io.kotest:kotest-assertions-core")
}
group = "icu.samnyan.aqua"
version = "1.0.0"
description = "AquaDX Planet Server"
java.sourceCompatibility = JavaVersion.VERSION_21
kotlin {
jvmToolchain(21)
}
springBoot {
mainClass.set("icu.samnyan.aqua.EntryKt")
}
application {
mainClass = "icu.samnyan.aqua.EntryKt"
}
hibernate {
enhancement {
enableLazyInitialization = true
enableAssociationManagement = false
enableExtendedEnhancement = false
}
}
allOpen {
annotation("jakarta.persistence.Entity")
annotation("jakarta.persistence.MappedSuperclass")
annotation("jakarta.persistence.Embeddable")
}
tasks.processResources {
val buildTime = ZonedDateTime.now(ZoneId.of("UTC"))
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z"))
inputs.property("buildTime", buildTime)
inputs.property("version", project.version)
filesMatching("**/application.properties") {
expand(mapOf("project" to project, "buildTime" to buildTime, "version" to project.version, "ext" to mapOf("buildTime" to buildTime)))
}
}
tasks.test {
enabled = false
useJUnitPlatform()
jvmArgs("-Dkotest.assertions.collection.print.size=100")
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21)
freeCompilerArgs.add("-Xjsr305=strict")
}
}
tasks.getByName<Jar>("jar") {
enabled = false
}
val copyDependencies by tasks.registering(Copy::class) {
from(configurations.runtimeClasspath)
into("${layout.buildDirectory.get()}/libs/lib")
}
val packageThin by tasks.registering(Jar::class) {
group = "build"
from(sourceSets.main.get().output)
manifest {
attributes(
"Main-Class" to "icu.samnyan.aqua.EntryKt",
"Class-Path" to configurations.runtimeClasspath.get().files.joinToString(" ") { "lib/${it.name}" }
)
}
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
dependsOn(copyDependencies)
}
@@ -1,22 +0,0 @@
package aquadx.net
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
import org.springframework.stereotype.Service
@Repository
interface SafetyRepo : JpaRepository<Safety, Long> {
fun findBySafetyId(safetyId: String): Safety?
}
@Service
class AquaNetSafetyService(val safetyRepo: SafetyRepo) {
fun isSafe(safetyId: String): Boolean {
val safety = safetyRepo.findBySafetyId(safetyId)
return safety == null || safety.status == 0
}
fun isSafeBatch(safetyIds: List<String>): List<Boolean> {
return safetyIds.map { isSafe(it) }
}
}
@@ -1,10 +0,0 @@
package aquadx.net.db
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface EmailConfirmationRepo : JpaRepository<EmailConfirmation, Long> {
fun findByToken(token: String): EmailConfirmation?
fun findByAquaNetUserAuId(auId: Long): List<EmailConfirmation>
}
@@ -1,10 +0,0 @@
package aquadx.net.db
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface ResetPasswordRepo : JpaRepository<ResetPassword, Long> {
fun findByToken(token: String): ResetPassword?
fun findByAquaNetUserAuId(auId: Long): List<ResetPassword>
}
@@ -1,7 +0,0 @@
package aquadx.net.db
import org.springframework.data.jpa.repository.JpaRepository
// Entity AquaGameOptions is now in :shared
interface AquaGameOptionsRepo : JpaRepository<AquaGameOptions, Long>
@@ -1,10 +0,0 @@
package aquadx.net.db
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface SessionTokenRepo : JpaRepository<SessionToken, String> {
fun findByToken(token: String): SessionToken?
fun findByAquaNetUserAuId(auId: Long): List<SessionToken>
}
@@ -1,29 +0,0 @@
package aquadx.net.games
import aquadx.sega.general.model.Card
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.NoRepositoryBean
@NoRepositoryBean
interface GenericUserDataRepo<T : IUserData> : JpaRepository<T, Long> {
fun findByCard(card: Card): T?
fun findByCard_ExtId(extId: Long): T?
@Query("select e from #{'#'}{#entityName} e where e.card.rankingBanned = false")
fun findAllNonBanned(): List<T>
}
@NoRepositoryBean
interface GenericPlaylogRepo<T: IGenericGamePlaylog> : JpaRepository<T, Long> {
fun findByUserCardExtId(extId: Long): List<T>
fun findByUserCardExtId(extId: Long, page: Pageable): Page<T>
}
@NoRepositoryBean
interface GenericUserMusicRepo<T: IGenericUserMusic> : JpaRepository<T, Long> {
fun findByUserCardExtId(extId: Long): List<T>
fun findByUser_Card_ExtIdAndMusicIdIn(userId: Long, musicId: List<Int>): List<T>
}
@@ -1,44 +0,0 @@
package aquadx.net.games.wacca
import ext.API
import ext.RP
import ext.isoDate
import ext.utc
import aquadx.net.db.AquaUserServices
import aquadx.net.games.*
import aquadx.net.utils.waccaScores
import aquadx.sega.wacca.model.db.WaccaUser
import aquadx.sega.wacca.model.db.WcUserBestScoreRepo
import aquadx.sega.wacca.model.db.WcUserPlayLogRepo
import aquadx.sega.wacca.model.db.WcUserRepo
import org.springframework.web.bind.annotation.RestController
@RestController
@API("api/v2/game/wacca")
class Wacca(
override val us: AquaUserServices,
override val playlogRepo: WcUserPlayLogRepo,
override val userDataRepo: WcUserRepo,
override val userMusicRepo: WcUserBestScoreRepo,
): GameApiController<WaccaUser>("wacca", WaccaUser::class) {
override val settableFields: Map<String, (WaccaUser, String) -> Unit> by lazy { mapOf(
"userName" to usernameCheck(WACCA_USERNAME_CHARS),
"lastRomVersion" to { u, v -> u.lastRomVersion = v },
) }
override suspend fun trend(@RP username: String) = us.cardByName(username) { card ->
findTrend(playlogRepo.findByUserCardExtId(card.extId)
.map { TrendLog(it.userPlayDate.utc().isoDate(), it.afterRating) })
}
override suspend fun userSummary(@RP username: String, @RP token: String?) = us.cardByName(username) { card ->
// TODO: Rating composition
val data = userDataRepo.findByCard_ExtId(card.extId)
genericUserSummary(card, mapOf(), null, data?.favoriteSongs)
}
override val shownRanks: List<Pair<Int, String>> = waccaScores.filter { it.first > 85 * 10000 }
}
@@ -1,10 +0,0 @@
package aquadx.sega.allnet
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface KeyChipRepo : JpaRepository<Keychip, Long> {
fun findByKeychipId(keychipId: String): Keychip?
fun existsByKeychipId(keychipId: String): Boolean
}
@@ -1,38 +0,0 @@
package aquadx.sega.diva.util
import com.fasterxml.jackson.core.json.JsonWriteFeature
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer
import ext.JDict
import aquadx.sega.util.BooleanNumberDeserializer
import aquadx.sega.util.BooleanNumberSerializer
import aquadx.sega.util.ZonedDateTimeDeserializer
import org.springframework.stereotype.Component
import java.time.LocalDateTime
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
@Component
class DivaMapper {
private val mapper: ObjectMapper = JsonMapper.builder().enable(JsonWriteFeature.WRITE_NUMBERS_AS_STRINGS)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.build().apply {
registerModule(SimpleModule().apply {
addSerializer(LocalDateTime::class.java, DivaDateTimeSerializer())
addDeserializer(LocalDateTime::class.java, LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.0")))
addDeserializer(ZonedDateTime::class.java, ZonedDateTimeDeserializer())
addSerializer(Boolean::class.java, BooleanNumberSerializer())
addSerializer(Boolean::class.javaPrimitiveType, BooleanNumberSerializer())
addDeserializer(Boolean::class.java, BooleanNumberDeserializer())
addDeserializer(Boolean::class.javaPrimitiveType, BooleanNumberDeserializer())
})
}
fun write(o: Any) = mapper.writeValueAsString(o)
fun <T> convert(map: JDict, toClass: Class<T>) = mapper.convertValue<T>(map, toClass)
fun toMap(obj: Any) = mapper.convertValue(obj, object : TypeReference<LinkedHashMap<String, Any>>() {})
}
@@ -1,24 +0,0 @@
package aquadx.sega.diva.util
import aquadx.sega.diva.PlayerPvRecordRepository
import aquadx.sega.diva.model.common.Edition
import aquadx.sega.diva.model.common.LevelInfo
import aquadx.sega.diva.model.db.userdata.PlayerProfile
import org.springframework.stereotype.Component
import kotlin.math.roundToInt
@Component
class DivaCalculator(private val playerPvRecordRepository: PlayerPvRecordRepository) {
fun getLevelInfo(profile: PlayerProfile): LevelInfo {
val recordList = playerPvRecordRepository.findByPdIdAndEdition(profile, Edition.ORIGINAL)
var totalAttain = 0
for (record in recordList) {
totalAttain += record.maxAttain
}
val level = totalAttain / 13979
val exp = ((totalAttain % 13979) / 13979.0f * 100.0f).roundToInt()
return LevelInfo(level + 1, exp)
}
}
@@ -1,86 +0,0 @@
package aquadx.sega.general.filter
import ext.details
import ext.logger
import ext.toJson
import aquadx.net.components.GeoIP
import aquadx.sega.allnet.TokenChecker
import aquadx.sega.util.ZLib
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.eclipse.jetty.io.EofException
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter
import org.springframework.web.util.ContentCachingResponseWrapper
import java.util.*
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Component
class CompressionFilter(
val geoip: GeoIP
) : OncePerRequestFilter() {
companion object {
val log = logger()
val b64d = Base64.getMimeDecoder()
val b64e = Base64.getMimeEncoder()
}
override fun doFilterInternal(req: HttpServletRequest, resp: HttpServletResponse, chain: FilterChain) {
val isDeflate = req.getHeader("content-encoding") == "deflate"
val isDfi = req.getHeader("pragma") == "DFI"
// Decode input
val reqSrc = try {
req.inputStream.readAllBytes().let {
if (isDeflate) ZLib.decompress(it)
else if (isDfi) ZLib.decompress(b64d.decode(it))
else it
}
} catch (e: Exception) {
log.error("Failed to decode request from ip ${geoip.getIP(req)}")
resp.sendError(400, "Failed to decode request")
return
}
// Handle request
val respW = ContentCachingResponseWrapper(resp)
val result = try {
chain.doFilter(CompressRequestWrapper(req, reqSrc), respW)
ZLib.compress(respW.contentAsByteArray).let { if (isDfi) b64e.encode(it) else it }
} finally {
if (respW.status != 200) {
val details = mapOf(
"req" to req.details(),
"resp" to respW.details(),
"body" to reqSrc.toString(Charsets.UTF_8),
"result" to respW.contentAsByteArray.toString(Charsets.UTF_8),
"token" to TokenChecker.getCurrentSession()?.token
).toJson()
log.error("HTTP ${respW.status}: $details")
}
}
// Write response
resp.setContentLength(result.size)
if (isDfi) resp.setHeader("pragma", "DFI")
else {
resp.contentType = "application/json; charset=utf-8"
resp.setHeader("content-encoding", "deflate")
}
try {
resp.outputStream.use { it.write(result); it.flush() }
} catch (e: EofException) {
log.warn("- EOF: Client closed connection when writing result")
}
}
/** Only games (other than WACCA) require response compression */
override fun shouldNotFilter(req: HttpServletRequest) =
!(req.servletPath.startsWith("/g/") && !req.servletPath.startsWith("/g/wacca"))
}
@@ -1,10 +0,0 @@
package aquadx.sega.general.model
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
import java.util.*
@Repository
interface CardTimestampRepo : JpaRepository<CardTimestamp, Long> {
fun findByCardIdAndGameId(cardId: Long, gameId: String): Optional<CardTimestamp>
}
@@ -1,70 +0,0 @@
package aquadx.sega.ongeki
import aquadx.net.db.AquaUserServices
import aquadx.net.utils.simpleDescribe
import aquadx.sega.allnet.TokenChecker
import ext.*
import aquadx.sega.general.GameMusicPopularity
import aquadx.sega.general.MeowApi
import aquadx.sega.general.RequestContext
import aquadx.sega.util.BasicMapper
import aquadx.spring.Metrics
import aquadx.sega.general.service.CardService
import jakarta.servlet.http.HttpServletRequest
import org.springframework.web.bind.annotation.RestController
@Suppress("unused")
@RestController
@API("/g/ongeki/{version}", "/g/ongeki")
class OngekiController(
val mapper: BasicMapper,
val db: OngekiUserRepos,
val gdb: OngekiGameRepos,
val us: AquaUserServices,
val pop: GameMusicPopularity,
val cardService: CardService,
): MeowApi({ _, resp -> if (resp is String) resp else mapper.write(resp) }) {
val log = logger()
val noopEndpoint = setOf("ExtendLockTimeApi", "GameLogoutApi", "RegisterPromotionCardApi",
"UpsertClientBookkeepingApi", "UpsertClientDevelopApi", "UpsertClientErrorApi", "UpsertClientSettingApi",
"UpsertClientTestmodeApi", "UpsertUserGplogApi", "Ping")
init { ongekiInit() }
val handlers = initH
@API("/{api}")
fun handle(@PV api: Str, @RB data: MutableMap<Str, Any>, @PV version: Str?, req: HttpServletRequest): Any {
val ctx = RequestContext(req, data)
version?.let { data["version"] = it }
val token = TokenChecker.tokenShort()
log.info("$token : $api < ${data.toJson()}")
val noop = """{"returnCode":1,"apiName":"${api.substringBefore("Api").firstCharLower()}"}"""
if (api !in noopEndpoint && !handlers.containsKey(api)) {
log.warn("$token : $api > not found")
return noop
}
// Only record the counter metrics if the API is known.
Metrics.counter("aquadx_ongeki_api_call", "api" to api).increment()
if (api in noopEndpoint) {
log.info("$token : $api > no-op")
return noop
}
return try {
Metrics.timer("aquadx_ongeki_api_latency", "api" to api).recordCallable {
serialize(api, handlers[api]!!(ctx) ?: noop).also {
if (api !in setOf("GetUserItemApi", "GetGameEventApi"))
log.info("$token : $api > ${it.truncate(500)}")
}
}!!
} catch (e: Exception) {
Metrics.counter("aquadx_ongeki_api_error", "api" to api, "error" to e.simpleDescribe()).increment()
throw e
}
}
}
@@ -1,26 +0,0 @@
package aquadx.sega.util
import java.lang.reflect.Field
import java.util.*
open class StaticRepo<T : Any, ID>(val data: List<T>, val idGetter: (T) -> ID) {
fun findAll(): List<T> = data
private val idMap by lazy { data.associateBy { idGetter(it) } }
fun findById(id: ID): Optional<T> = Optional.ofNullable(idMap[id])
private val enableField by lazy {
data.firstOrNull()?.let { obj ->
var cls: Class<*>? = obj::class.java
var f: Field? = null
while (cls != null && f == null) {
f = cls.declaredFields.find { it.name == "enable" || it.name == "isEnabled" }
cls = cls.superclass
}
f?.apply { isAccessible = true }
}
}
private val enableMap by lazy { data.groupBy { enableField?.get(it) as? Boolean } }
fun findByEnable(enable: Boolean): List<T> = enableMap[enable] ?: emptyList()
}
-12
View File
@@ -1,12 +0,0 @@
package ext
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
val HTTP = HttpClient(CIO) {
install(ContentNegotiation) {
json(JSON)
}
}
-45
View File
@@ -1,45 +0,0 @@
package ext
import aquadx.net.utils.ApiException
import jakarta.persistence.Query
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.context.ApplicationContext
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity.BodyBuilder
import org.springframework.web.bind.annotation.*
typealias RP = RequestParam
typealias RB = RequestBody
typealias RT = RequestPart
typealias RH = RequestHeader
typealias PV = PathVariable
typealias API = RequestMapping
fun HttpServletRequest.details() = mapOf(
"method" to method,
"uri" to requestURI,
"query" to queryString,
"remote" to remoteAddr,
"headers" to headerNames.asSequence().associateWith { getHeader(it) }
)
fun HttpServletResponse.details() = mapOf(
"status" to status,
"headers" to headerNames.asSequence().associateWith { getHeader(it) },
)
// HTTP
operator fun HttpStatus.invoke(message: String? = null): Nothing = throw ApiException(value(), message ?: this.reasonPhrase)
fun <R> parsing(block: () -> R) = try { block() }
catch (e: ApiException) { throw e }
catch (e: Exception) { 400 - e.message.toString() }
fun BodyBuilder.headers(vararg pairs: Pair<String, String>) = headers(HttpHeaders().apply { pairs.forEach { (k, v) -> set(k, v) } })
// Database
val Query.exec get() = resultList.map { (it as Array<*>).toList() }
// DI
inline fun <reified T> ApplicationContext.lazy() = lazy { getBean(T::class.java) }
-7
View File
@@ -1,7 +0,0 @@
package ext
import org.apache.tika.Tika
import org.apache.tika.mime.MimeTypes
val TIKA = Tika()
val MIMES = MimeTypes.getDefaultMimeTypes()
@@ -1,39 +0,0 @@
# This configuration file is for define default values
# User should use application.properties in the /config folder instead, not this
aimedb.server.enable=true
aimedb.server.address=0.0.0.0
aimedb.server.port=22345
billing.server.enable=true
billing.server.port=8443
aquaviewer.server.enable=true
server.port=80
build.version=${version}
build.timestamp=${ext.buildTime}
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=20MB
spring.flyway.clean-disabled=false
spring.flyway.baseline-on-migrate=true
spring.flyway.locations=classpath:db
spring.flyway.out-of-order=true
spring.flyway.validate-on-migrate=false
spring.jpa.hibernate.ddl-auto=none
# Force color output even in non-tty mode
# Note: If your terminal doesn't support colors, uninstall your terminal and use one that does
# https://github.com/termstandard/colors#truecolor-support-in-output-devices
# If you want to read the logs using a script, read the json logs in `logs` folder instead.
spring.output.ansi.enabled=always
# Metrics
management.server.port=8081
management.endpoint.prometheus.enabled=true
management.endpoints.web.exposure.include=prometheus
management.metrics.distribution.percentiles.http.server.requests=0.5, 0.75, 0.9, 0.95, 0.99
management.metrics.distribution.percentiles.tasks.scheduled.execution=0.5, 0.75, 0.9, 0.95, 0.99
management.metrics.distribution.percentiles.spring.data.repository.invocations=0.5, 0.75, 0.9, 0.95, 0.99
@@ -1,32 +0,0 @@
[
{
"charge_id": 2020,
"order_id": 0,
"price": 1,
"sale_price": 1
},
{
"charge_id": 2040,
"order_id": 1,
"price": 1,
"sale_price": 1
},
{
"charge_id": 2060,
"order_id": 2,
"price": 1,
"sale_price": 1
},
{
"charge_id": 2070,
"order_id": 3,
"price": 1,
"sale_price": 1
},
{
"charge_id": 2080,
"order_id": 4,
"price": 1,
"sale_price": 1
}
]
File diff suppressed because it is too large Load Diff
@@ -1,52 +0,0 @@
[
{
"gacha_id": 370,
"gacha_name": "お風呂上がりの一発でわからせる!",
"type": 0,
"kind": 0,
"is_ceiling": 0,
"ceiling_cnt": 10,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
},
{
"gacha_id": 380,
"gacha_name": "優しいキャロルが流れる頃には",
"type": 0,
"kind": 0,
"is_ceiling": 0,
"ceiling_cnt": 10,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
},
{
"gacha_id": 390,
"gacha_name": "音闘気鍛練の行",
"type": 0,
"kind": 0,
"is_ceiling": 0,
"ceiling_cnt": 10,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
},
{
"gacha_id": 400,
"gacha_name": "最後の物語。約束はあなたと共に",
"type": 0,
"kind": 0,
"is_ceiling": 0,
"ceiling_cnt": 10,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
},
{
"gacha_id": 410,
"gacha_name": "可愛さ指数は3000倍!?",
"type": 0,
"kind": 0,
"is_ceiling": 0,
"ceiling_cnt": 10,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
}
]
File diff suppressed because it is too large Load Diff
@@ -1,42 +0,0 @@
[
{
"id": 10001,
"name": "Linked GATE ORIGIN",
"music_id": 2838
},
{
"id": 10002,
"name": "Linked GATE AIR",
"music_id": 2846
},
{
"id": 10003,
"name": "Linked GATE STAR",
"music_id": 2858
},
{
"id": 10004,
"name": "Linked GATE AMAZON",
"music_id": 2869
},
{
"id": 10005,
"name": "Linked GATE CRYSTAL",
"music_id": 2880
},
{
"id": 10006,
"name": "Linked GATE PARADISE",
"music_id": 2891
},
{
"id": 10007,
"name": "Linked GATE NEW",
"music_id": 2919
},
{
"id": 10008,
"name": "Linked GATE SUN",
"music_id": 2938
}
]
@@ -1 +0,0 @@
[]
@@ -1,27 +0,0 @@
[
{
"order_id": 1,
"charge_id": 2,
"price": 1
},
{
"order_id": 2,
"charge_id": 3,
"price": 2
},
{
"order_id": 3,
"charge_id": 4,
"price": 3
},
{
"order_id": 4,
"charge_id": 5,
"price": 4
},
{
"order_id": 5,
"charge_id": 6,
"price": 5
}
]
File diff suppressed because it is too large Load Diff
@@ -1,818 +0,0 @@
[
{
"card_id": 2000014
},
{
"card_id": 2000016
},
{
"card_id": 2001014
},
{
"card_id": 2001016
},
{
"card_id": 2002014
},
{
"card_id": 2002016
},
{
"card_id": 2003014
},
{
"card_id": 2003016
},
{
"card_id": 2004014
},
{
"card_id": 2004016
},
{
"card_id": 2005014
},
{
"card_id": 2005016
},
{
"card_id": 1024
},
{
"card_id": 4502016
},
{
"card_id": 1003012
},
{
"card_id": 1003013
},
{
"card_id": 1003014
},
{
"card_id": 1003016
},
{
"card_id": 512012
},
{
"card_id": 512013
},
{
"card_id": 512014
},
{
"card_id": 4116
},
{
"card_id": 4501014
},
{
"card_id": 4501016
},
{
"card_id": 1002012
},
{
"card_id": 1002013
},
{
"card_id": 1002014
},
{
"card_id": 1002016
},
{
"card_id": 511012
},
{
"card_id": 511013
},
{
"card_id": 511014
},
{
"card_id": 3116
},
{
"card_id": 4500014
},
{
"card_id": 4500016
},
{
"card_id": 1001012
},
{
"card_id": 1001013
},
{
"card_id": 1001014
},
{
"card_id": 1001016
},
{
"card_id": 510012
},
{
"card_id": 510013
},
{
"card_id": 510014
},
{
"card_id": 2116
},
{
"card_id": 1000012
},
{
"card_id": 1000013
},
{
"card_id": 1000014
},
{
"card_id": 1000016
},
{
"card_id": 509012
},
{
"card_id": 509013
},
{
"card_id": 509014
},
{
"card_id": 3005014
},
{
"card_id": 3005016
},
{
"card_id": 1116
},
{
"card_id": 508012
},
{
"card_id": 508013
},
{
"card_id": 508014
},
{
"card_id": 3004014
},
{
"card_id": 3004016
},
{
"card_id": 512116
},
{
"card_id": 507012
},
{
"card_id": 507013
},
{
"card_id": 507014
},
{
"card_id": 3003014
},
{
"card_id": 3003016
},
{
"card_id": 511116
},
{
"card_id": 4005014
},
{
"card_id": 4005016
},
{
"card_id": 506012
},
{
"card_id": 506013
},
{
"card_id": 506014
},
{
"card_id": 3002014
},
{
"card_id": 3002016
},
{
"card_id": 15012
},
{
"card_id": 15013
},
{
"card_id": 15014
},
{
"card_id": 510116
},
{
"card_id": 15022
},
{
"card_id": 15023
},
{
"card_id": 15024
},
{
"card_id": 4004014
},
{
"card_id": 4004016
},
{
"card_id": 505012
},
{
"card_id": 505013
},
{
"card_id": 505014
},
{
"card_id": 3001014
},
{
"card_id": 3001016
},
{
"card_id": 14012
},
{
"card_id": 14013
},
{
"card_id": 14014
},
{
"card_id": 509116
},
{
"card_id": 14022
},
{
"card_id": 14023
},
{
"card_id": 14024
},
{
"card_id": 4003014
},
{
"card_id": 4003016
},
{
"card_id": 504012
},
{
"card_id": 504013
},
{
"card_id": 504014
},
{
"card_id": 3000014
},
{
"card_id": 3000016
},
{
"card_id": 13012
},
{
"card_id": 13013
},
{
"card_id": 13014
},
{
"card_id": 508116
},
{
"card_id": 13022
},
{
"card_id": 13023
},
{
"card_id": 13024
},
{
"card_id": 4002014
},
{
"card_id": 4002016
},
{
"card_id": 503012
},
{
"card_id": 503013
},
{
"card_id": 503014
},
{
"card_id": 12012
},
{
"card_id": 12013
},
{
"card_id": 12014
},
{
"card_id": 507116
},
{
"card_id": 1505014
},
{
"card_id": 4001014
},
{
"card_id": 1505016
},
{
"card_id": 4001016
},
{
"card_id": 502012
},
{
"card_id": 502013
},
{
"card_id": 502014
},
{
"card_id": 11012
},
{
"card_id": 11013
},
{
"card_id": 11014
},
{
"card_id": 506116
},
{
"card_id": 15116
},
{
"card_id": 1504014
},
{
"card_id": 4000014
},
{
"card_id": 1504016
},
{
"card_id": 4000016
},
{
"card_id": 501012
},
{
"card_id": 501013
},
{
"card_id": 501014
},
{
"card_id": 10012
},
{
"card_id": 10013
},
{
"card_id": 10014
},
{
"card_id": 505116
},
{
"card_id": 14116
},
{
"card_id": 10022
},
{
"card_id": 10023
},
{
"card_id": 10024
},
{
"card_id": 1503014
},
{
"card_id": 1503016
},
{
"card_id": 500012
},
{
"card_id": 500013
},
{
"card_id": 500014
},
{
"card_id": 9012
},
{
"card_id": 9013
},
{
"card_id": 9014
},
{
"card_id": 504116
},
{
"card_id": 2505014
},
{
"card_id": 2505016
},
{
"card_id": 13116
},
{
"card_id": 9022
},
{
"card_id": 9023
},
{
"card_id": 9024
},
{
"card_id": 1502014
},
{
"card_id": 1502016
},
{
"card_id": 8012
},
{
"card_id": 8013
},
{
"card_id": 8014
},
{
"card_id": 503116
},
{
"card_id": 2504014
},
{
"card_id": 2504016
},
{
"card_id": 12116
},
{
"card_id": 8022
},
{
"card_id": 8023
},
{
"card_id": 8024
},
{
"card_id": 1501014
},
{
"card_id": 1501016
},
{
"card_id": 7012
},
{
"card_id": 7013
},
{
"card_id": 7014
},
{
"card_id": 502116
},
{
"card_id": 2503014
},
{
"card_id": 2503016
},
{
"card_id": 11116
},
{
"card_id": 7022
},
{
"card_id": 7023
},
{
"card_id": 7024
},
{
"card_id": 1500014
},
{
"card_id": 1500016
},
{
"card_id": 6012
},
{
"card_id": 6013
},
{
"card_id": 6014
},
{
"card_id": 501116
},
{
"card_id": 2502014
},
{
"card_id": 2502016
},
{
"card_id": 10116
},
{
"card_id": 4502014
},
{
"card_id": 3504014
},
{
"card_id": 3504016
},
{
"card_id": 5012
},
{
"card_id": 5013
},
{
"card_id": 5014
},
{
"card_id": 500116
},
{
"card_id": 2501014
},
{
"card_id": 2501016
},
{
"card_id": 9116
},
{
"card_id": 5022
},
{
"card_id": 5023
},
{
"card_id": 5024
},
{
"card_id": 3503014
},
{
"card_id": 3503016
},
{
"card_id": 4012
},
{
"card_id": 4013
},
{
"card_id": 4014
},
{
"card_id": 2500014
},
{
"card_id": 2500016
},
{
"card_id": 8116
},
{
"card_id": 4022
},
{
"card_id": 4023
},
{
"card_id": 4024
},
{
"card_id": 3502014
},
{
"card_id": 3502016
},
{
"card_id": 3012
},
{
"card_id": 3013
},
{
"card_id": 3014
},
{
"card_id": 7116
},
{
"card_id": 3022
},
{
"card_id": 3023
},
{
"card_id": 3024
},
{
"card_id": 4504014
},
{
"card_id": 4504016
},
{
"card_id": 1005012
},
{
"card_id": 1005013
},
{
"card_id": 1005014
},
{
"card_id": 3501014
},
{
"card_id": 1005016
},
{
"card_id": 3501016
},
{
"card_id": 2012
},
{
"card_id": 2013
},
{
"card_id": 2014
},
{
"card_id": 6116
},
{
"card_id": 2022
},
{
"card_id": 2023
},
{
"card_id": 2024
},
{
"card_id": 4503014
},
{
"card_id": 4503016
},
{
"card_id": 1004012
},
{
"card_id": 1004013
},
{
"card_id": 1004014
},
{
"card_id": 3500014
},
{
"card_id": 1004016
},
{
"card_id": 3500016
},
{
"card_id": 1012
},
{
"card_id": 1013
},
{
"card_id": 1014
},
{
"card_id": 5116
},
{
"card_id": 1022
},
{
"card_id": 1023
},
{
"card_id": 5504014
},
{
"card_id": 5504016
},
{
"card_id": 5503014
},
{
"card_id": 5503016
},
{
"card_id": 5502014
},
{
"card_id": 5502016
},
{
"card_id": 5501014
},
{
"card_id": 5501016
},
{
"card_id": 5500014
},
{
"card_id": 5500016
},
{
"card_id": 5003014
},
{
"card_id": 5003016
},
{
"card_id": 5002014
},
{
"card_id": 5002016
},
{
"card_id": 5001014
},
{
"card_id": 5001016
},
{
"card_id": 5000014
},
{
"card_id": 5000016
},
{
"card_id": 5505014
},
{
"card_id": 5505016
}
]
@@ -1,242 +0,0 @@
[
{
"id": 0,
"name": "使用しない",
"credits": 0,
"kind": "None",
"max": 0,
"detail": "チケットを使用しません",
"event_id": 1,
"event_name": "無期限常時解放"
},
{
"id": 2,
"name": "2倍チケット",
"credits": 1,
"kind": "Paid",
"max": 0,
"detail": "2倍チケット",
"event_id": 1,
"event_name": "無期限常時解放"
},
{
"id": 3,
"name": "3倍チケット",
"credits": 2,
"kind": "Paid",
"max": 0,
"detail": "3倍チケット",
"event_id": 1,
"event_name": "無期限常時解放"
},
{
"id": 4,
"name": "4倍チケット",
"credits": 3,
"kind": "Paid",
"max": 0,
"detail": "4倍チケット",
"event_id": 0,
"event_name": "解放なし"
},
{
"id": 5,
"name": "5倍チケット",
"credits": 4,
"kind": "Paid",
"max": 0,
"detail": "5倍チケット",
"event_id": 24021661,
"event_name": "240216_06_1:有料5倍チケット解放"
},
{
"id": 6,
"name": "6倍チケット",
"credits": 5,
"kind": "Paid",
"max": 0,
"detail": "6倍チケット",
"event_id": 0,
"event_name": "解放なし"
},
{
"id": 10001,
"name": "イベント1.5倍チケット_1",
"credits": 0,
"kind": "Event",
"max": 1,
"detail": "期限付き1.5倍チケット",
"event_id": 0,
"event_name": "解放なし"
},
{
"id": 10002,
"name": "イベント2倍チケット_1",
"credits": 0,
"kind": "Event",
"max": 1,
"detail": "期限付き2倍チケット",
"event_id": 0,
"event_name": "解放なし"
},
{
"id": 10003,
"name": "イベント3倍チケット_1",
"credits": 0,
"kind": "Event",
"max": 1,
"detail": "期限付き3倍チケット",
"event_id": 0,
"event_name": "解放なし"
},
{
"id": 10005,
"name": "イベント5倍チケット_1",
"credits": 0,
"kind": "Event",
"max": 1,
"detail": "期限付き5倍チケット",
"event_id": 23091461,
"event_name": "230914_06_1:イベント5倍チケット配布_1(1.40予備)"
},
{
"id": 10101,
"name": "イベント1.5倍チケット_2",
"credits": 0,
"kind": "Event",
"max": 1,
"detail": "期限付き1.5倍チケット",
"event_id": 0,
"event_name": "解放なし"
},
{
"id": 10102,
"name": "イベント2倍チケット_2",
"credits": 0,
"kind": "Event",
"max": 1,
"detail": "期限付き2倍チケット",
"event_id": 0,
"event_name": "解放なし"
},
{
"id": 10103,
"name": "イベント3倍チケット_2",
"credits": 0,
"kind": "Event",
"max": 1,
"detail": "期限付き3倍チケット",
"event_id": 0,
"event_name": "解放なし"
},
{
"id": 10105,
"name": "イベント5倍チケット_2",
"credits": 0,
"kind": "Event",
"max": 1,
"detail": "期限付き5倍チケット",
"event_id": 23091462,
"event_name": "230914_06_2:イベント5倍チケット配布_2(1.40予備)"
},
{
"id": 10201,
"name": "イベント1.5倍チケット_3",
"credits": 0,
"kind": "Event",
"max": 1,
"detail": "期限付き1.5倍チケット",
"event_id": 0,
"event_name": "解放なし"
},
{
"id": 10202,
"name": "イベント2倍チケット_3",
"credits": 0,
"kind": "Event",
"max": 1,
"detail": "期限付き2倍チケット",
"event_id": 0,
"event_name": "解放なし"
},
{
"id": 10203,
"name": "イベント3倍チケット_3",
"credits": 0,
"kind": "Event",
"max": 1,
"detail": "期限付き3倍チケット",
"event_id": 0,
"event_name": "解放なし"
},
{
"id": 10205,
"name": "イベント5倍チケット_3",
"credits": 0,
"kind": "Event",
"max": 1,
"detail": "期限付き5倍チケット",
"event_id": 23032362,
"event_name": "230323_06_2:イベント5倍チケット配布_3(予備)"
},
{
"id": 11001,
"name": "無料1.5倍チケット",
"credits": 0,
"kind": "Free",
"max": 5,
"detail": "ちほー進行1.5倍チケット",
"event_id": 1,
"event_name": "無期限常時解放"
},
{
"id": 11002,
"name": "無料2倍チケット",
"credits": 0,
"kind": "Free",
"max": 5,
"detail": "ちほー進行2倍チケット",
"event_id": 0,
"event_name": "解放なし"
},
{
"id": 11003,
"name": "無料3倍チケット",
"credits": 0,
"kind": "Free",
"max": 5,
"detail": "ちほー進行3倍チケット",
"event_id": 0,
"event_name": "解放なし"
},
{
"id": 11005,
"name": "無料5倍チケット",
"credits": 0,
"kind": "Free",
"max": 5,
"detail": "ちほー進行5倍チケット",
"event_id": 0,
"event_name": "解放なし"
},
{
"id": 20010,
"name": "無料10倍チケット",
"credits": 0,
"kind": "Free",
"max": 99,
"detail": "ちほー進行10倍チケット",
"event_id": 0,
"event_name": "解放なし"
},
{
"id": 20020,
"name": "無料20倍チケット",
"credits": 0,
"kind": "Free",
"max": 99,
"detail": "ちほー進行20倍チケット",
"event_id": 0,
"event_name": "解放なし"
}
]
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,299 +0,0 @@
[
{
"gacha_id": 1011,
"gacha_name": "無料ガチャ",
"type": 0,
"kind": 3,
"is_ceiling": 0,
"max_select_point": 0,
"ceiling_cnt": 0,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
},
{
"gacha_id": 1012,
"gacha_name": "無料ガチャ(SR確定)",
"type": 0,
"kind": 3,
"is_ceiling": 0,
"max_select_point": 0,
"ceiling_cnt": 0,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
},
{
"gacha_id": 1112,
"gacha_name": "レギュラーガチャ",
"type": 0,
"kind": 0,
"is_ceiling": 0,
"max_select_point": 0,
"ceiling_cnt": 0,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
},
{
"gacha_id": 1123,
"gacha_name": "AQUA属性オンリーガチャ",
"type": 0,
"kind": 1,
"is_ceiling": 0,
"max_select_point": 0,
"ceiling_cnt": 0,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
},
{
"gacha_id": 1128,
"gacha_name": "bitter chocolate kiss ガチャ",
"type": 0,
"kind": 4,
"is_ceiling": 0,
"max_select_point": 0,
"ceiling_cnt": 0,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
},
{
"gacha_id": 1135,
"gacha_name": "オンゲキ bright 大感謝祭ガチャ",
"type": 0,
"kind": 1,
"is_ceiling": 0,
"max_select_point": 0,
"ceiling_cnt": 0,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
},
{
"gacha_id": 1137,
"gacha_name": "テアトル展開!プロジェクト奏坂ドールズリミテッドガチャ",
"type": 0,
"kind": 1,
"is_ceiling": 0,
"max_select_point": 0,
"ceiling_cnt": 0,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
},
{
"gacha_id": 1138,
"gacha_name": "WONDER SHOOTER EASTER ガチャ",
"type": 0,
"kind": 4,
"is_ceiling": 0,
"max_select_point": 0,
"ceiling_cnt": 0,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
},
{
"gacha_id": 1139,
"gacha_name": "ラスボス登場!?悪の侵略者ガチャ",
"type": 0,
"kind": 4,
"is_ceiling": 0,
"max_select_point": 0,
"ceiling_cnt": 0,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
},
{
"gacha_id": 1140,
"gacha_name": "カラフルアンブレラガチャ",
"type": 0,
"kind": 4,
"is_ceiling": 0,
"max_select_point": 0,
"ceiling_cnt": 0,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
},
{
"gacha_id": 1141,
"gacha_name": "It's Showtime!ワンダフルサーカスガチャ",
"type": 0,
"kind": 4,
"is_ceiling": 0,
"max_select_point": 0,
"ceiling_cnt": 0,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
},
{
"gacha_id": 1143,
"gacha_name": "マーチングポケッツ ピックアップガチャ",
"type": 0,
"kind": 4,
"is_ceiling": 0,
"max_select_point": 0,
"ceiling_cnt": 0,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
},
{
"gacha_id": 1144,
"gacha_name": "bitter flavor ピックアップガチャ",
"type": 0,
"kind": 4,
"is_ceiling": 0,
"max_select_point": 0,
"ceiling_cnt": 0,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
},
{
"gacha_id": 1145,
"gacha_name": "7EVENDAYS⇔HOLIDAYS ピックアップガチャ",
"type": 0,
"kind": 1,
"is_ceiling": 0,
"max_select_point": 0,
"ceiling_cnt": 0,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
},
{
"gacha_id": 1146,
"gacha_name": "⊿TRiEDGE ピックアップガチャ",
"type": 0,
"kind": 1,
"is_ceiling": 0,
"max_select_point": 0,
"ceiling_cnt": 0,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
},
{
"gacha_id": 1147,
"gacha_name": "R.B.P. ピックアップガチャ",
"type": 0,
"kind": 1,
"is_ceiling": 0,
"max_select_point": 0,
"ceiling_cnt": 0,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
},
{
"gacha_id": 1148,
"gacha_name": "皇城 セツナ ピックアップガチャ",
"type": 0,
"kind": 1,
"is_ceiling": 0,
"max_select_point": 0,
"ceiling_cnt": 0,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
},
{
"gacha_id": 1149,
"gacha_name": "ASTERISM ピックアップガチャ",
"type": 0,
"kind": 1,
"is_ceiling": 0,
"max_select_point": 0,
"ceiling_cnt": 0,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
},
{
"gacha_id": 1151,
"gacha_name": "スラッシュスキル ピックアップガチャ",
"type": 0,
"kind": 1,
"is_ceiling": 0,
"max_select_point": 0,
"ceiling_cnt": 0,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
},
{
"gacha_id": 1152,
"gacha_name": "高瀬 梨緒オンリーガチャ",
"type": 0,
"kind": 1,
"is_ceiling": 0,
"max_select_point": 0,
"ceiling_cnt": 0,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
},
{
"gacha_id": 1154,
"gacha_name": "トラストスキル ピックアップガチャ",
"type": 0,
"kind": 1,
"is_ceiling": 0,
"max_select_point": 0,
"ceiling_cnt": 0,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
},
{
"gacha_id": 1156,
"gacha_name": "bright memory振り返りガチャ",
"type": 0,
"kind": 2,
"is_ceiling": 0,
"max_select_point": 0,
"ceiling_cnt": 0,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
},
{
"gacha_id": 1159,
"gacha_name": "オンゲキ&オンゲキ PLUS ピックアップガチャ",
"type": 0,
"kind": 2,
"is_ceiling": 0,
"max_select_point": 0,
"ceiling_cnt": 0,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
},
{
"gacha_id": 1160,
"gacha_name": "SUMMER & SUMMER PLUS ピックアップガチャ",
"type": 0,
"kind": 2,
"is_ceiling": 0,
"max_select_point": 0,
"ceiling_cnt": 0,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
},
{
"gacha_id": 1161,
"gacha_name": "R.E.D. & R.E.D. PLUS ピックアップガチャ",
"type": 0,
"kind": 2,
"is_ceiling": 0,
"max_select_point": 0,
"ceiling_cnt": 0,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
},
{
"gacha_id": 1162,
"gacha_name": "bright & bright MEMORY ピックアップガチャ",
"type": 0,
"kind": 2,
"is_ceiling": 0,
"max_select_point": 0,
"ceiling_cnt": 0,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
},
{
"gacha_id": 1167,
"gacha_name": "6周年記念!! 6rd Anniversaryセレクトガチャ",
"type": 0,
"kind": 2,
"is_ceiling": 1,
"max_select_point": 11,
"ceiling_cnt": 11,
"change_rate_cnt1": 0,
"change_rate_cnt2": 0
}
]
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More