mirror of
https://github.com/MewoLab/AquaDX.git
synced 2026-10-07 06:08:03 +03:00
Compare commits
104
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1d3f61b00 | ||
|
|
55b47a3c02 | ||
|
|
97743f55ef | ||
|
|
11c5617abc | ||
|
|
cf89e59af8 | ||
|
|
587557177e | ||
|
|
6285bd3fd4 | ||
|
|
7ba95f0b90 | ||
|
|
13aa48982f | ||
|
|
0a5c97d985 | ||
|
|
c1b6c3e414 | ||
|
|
5e6aaace8c | ||
|
|
3be81520ce | ||
|
|
30139cae8d | ||
|
|
96570a97ff | ||
|
|
7eab79e48d | ||
|
|
05e613d667 | ||
|
|
d34aab37b2 | ||
|
|
4aba8d8642 | ||
|
|
0d70471bd6 | ||
|
|
7ea5add407 | ||
|
|
41edddcaec | ||
|
|
1d36998967 | ||
|
|
3178c4c957 | ||
|
|
48eb8a9c07 | ||
|
|
fcee9fb278 | ||
|
|
853abc8867 | ||
|
|
c099a8954e | ||
|
|
96a894aa70 | ||
|
|
0b3d52d31d | ||
|
|
27685087be | ||
|
|
fec3480963 | ||
|
|
536734746b | ||
|
|
b352576358 | ||
|
|
f2ea015ef3 | ||
|
|
253c7a73d6 | ||
|
|
e62115e6ac | ||
|
|
08bf69c723 | ||
|
|
3e2c198204 | ||
|
|
c3f75e6428 | ||
|
|
4a6003ded3 | ||
|
|
b114adf940 | ||
|
|
7c31dfd4ab | ||
|
|
faabd45b7a | ||
|
|
d7254913cd | ||
|
|
237ba1464a | ||
|
|
d5d50d687c | ||
|
|
2c28758ae0 | ||
|
|
f025465f40 | ||
|
|
4b5eeadc2b | ||
|
|
898939485c | ||
|
|
593783866a | ||
|
|
9b3871706f | ||
|
|
5be6d8c80b | ||
|
|
f750cc6596 | ||
|
|
f67bbb822d | ||
|
|
819c058517 | ||
|
|
b2feb9a130 | ||
|
|
6a10570fa6 | ||
|
|
6243ee7205 | ||
|
|
72c1f48bdb | ||
|
|
06eec89dfe | ||
|
|
367a26c109 | ||
|
|
2552568ac1 | ||
|
|
512a791aa4 | ||
|
|
9bdc84418a | ||
|
|
243a9b2ddc | ||
|
|
f313f090f8 | ||
|
|
2e55247de0 | ||
|
|
4895456bb0 | ||
|
|
aec0fdac8b | ||
|
|
eb600f1715 | ||
|
|
bd6a641336 | ||
|
|
aeebf069c2 | ||
|
|
a77111b2ad | ||
|
|
031f9e4663 | ||
|
|
1181707b43 | ||
|
|
927fa56862 | ||
|
|
d17073297c | ||
|
|
34a3de1209 | ||
|
|
3a42435dc5 | ||
|
|
7865f7cfa7 | ||
|
|
e711921725 | ||
|
|
d0110d7f47 | ||
|
|
26748719a0 | ||
|
|
b20fe6fa62 | ||
|
|
9aec50d901 | ||
|
|
c31c1a0524 | ||
|
|
c989c273c4 | ||
|
|
c4c550b239 | ||
|
|
22334de8a7 | ||
|
|
50cb970828 | ||
|
|
80c088626e | ||
|
|
2d5fb11dc0 | ||
|
|
708532d18b | ||
|
|
dad66401cc | ||
|
|
1bc89c46f1 | ||
|
|
1d3ddfc06c | ||
|
|
34cfa279dc | ||
|
|
2e21926189 | ||
|
|
7a946708c3 | ||
|
|
fcf7c46cad | ||
|
|
28063912ce | ||
|
|
beacca12c3 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
Make sure `./gradlew build` compiles before you say you're done.
|
||||
@@ -0,0 +1,8 @@
|
||||
*
|
||||
!gradlew
|
||||
!gradle/
|
||||
!gradle/**
|
||||
!build.gradle.kts
|
||||
!settings.gradle.kts
|
||||
!src/
|
||||
!src/**
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -84,3 +84,6 @@ src/main/resources/meta/*/*.json
|
||||
test-diff
|
||||
htmlReport
|
||||
docs/logs
|
||||
|
||||
# Sensitive keys
|
||||
game_encryption.json
|
||||
@@ -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
|
||||
@@ -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
@@ -35,4 +35,6 @@ dist-ssr
|
||||
public/chu3
|
||||
|
||||
# local env file
|
||||
*.local
|
||||
*.local
|
||||
|
||||
.env
|
||||
|
||||
+18
-5
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
|
||||
@@ -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
@@ -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}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
display: flex
|
||||
gap: 1rem
|
||||
|
||||
div
|
||||
a
|
||||
&.active
|
||||
color: vars.$c-main
|
||||
</style>
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(" ");
|
||||
}
|
||||
@@ -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())
|
||||
})()
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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]}`)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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,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,5 +1,6 @@
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
image: hykilpikonna/aquadx:latest
|
||||
ports:
|
||||
- "80:80"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||

|
||||
|
||||
Vendored
BIN
Binary file not shown.
+1
-1
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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) }
|
||||
@@ -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 +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
Reference in New Issue
Block a user