1 Commits

Author SHA1 Message Date
Afonso
38bddf1763 add prettier formatter 2024-02-12 18:04:56 +01:00
45 changed files with 1186 additions and 2114 deletions

3
.gitignore vendored
View File

@@ -76,3 +76,6 @@ gradle-app.setting
### Gradle Patch ### ### Gradle Patch ###
# Java heap dump # Java heap dump
*.hprof *.hprof
### Docker ###
/db/*

8
.prettierrc Normal file
View File

@@ -0,0 +1,8 @@
{
"trailingComma": "es5",
"semi": false,
"singleQuote": true,
"bracketSpacing": false,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

View File

@@ -11,6 +11,6 @@ TicketUnlock=true
# Skip the warning screen and logo shown after the POST sequence # Skip the warning screen and logo shown after the POST sequence
SkipWarningScreen=true SkipWarningScreen=true
# Single player: Show 1P only, at the center of the screen # Single player: Show 1P only, at the center of the screen
SinglePlayer=true SinglePlayer=false
# !!EXPERIMENTAL!! Skip from the card-scanning screen directly to music selection screen # !!EXPERIMENTAL!! Skip from the card-scanning screen directly to music selection screen
SkipToMusicSelection=false SkipToMusicSelection=true

View File

@@ -1,41 +0,0 @@
// ..eslintrc.cjs example
module.exports = {
root: true,
env: {
browser: true,
es2023: true
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended',
],
ignorePatterns: ['dist', '..eslintrc.cjs'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
},
rules: {
// Custom styling rules
'comma-dangle': ['warn', 'only-multiline'],
'indent': ['warn', 2],
'semi': ['warn', 'never'],
'quotes': ['warn', 'single'],
'arrow-parens': ['warn', 'as-needed'],
'linebreak-style': ['warn', 'unix'],
'object-curly-spacing': ['warn', 'always'],
'array-bracket-spacing': ["error", "always", {
"singleValue": false,
"objectsInArrays": false,
"arraysInArrays": false
}],
// Disabled rules
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'no-constant-condition': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
}

View File

@@ -12,8 +12,7 @@ Please check out [SVELTE.md](SVELTE.md) for more details on the technical aspect
### Running locally ### Running locally
First, you would need to install Node.js and yarn. First, you would need to install Node.js and yarn.
Then, you would need to start your testing AquaDX server and configure the `aqua_host` in `src/libs/config.ts` to use your URL. Then, you would need to start your testing AquaDX server and configure the `src/libs/config.ts` to use your URL.
Please leave `data_host` unchanged if you're not sure what it is.
Finally, run: Finally, run:
```shell ```shell

View File

@@ -30,6 +30,6 @@ If you have state that's important to retain within a component, consider creati
```ts ```ts
// store.ts // store.ts
// An extremely simple external store // An extremely simple external store
import { writable } from 'svelte/store' import {writable} from 'svelte/store'
export default writable(0) export default writable(0)
``` ```

View File

@@ -6,15 +6,36 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AquaNet</title> <title>AquaNet</title>
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png"> <link
<link rel="icon" type="image/png" sizes="32x32" href="/assets/icons/favicon-32x32.png"> rel="apple-touch-icon"
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png"> sizes="180x180"
<link rel="manifest" href="/assets/icons/site.webmanifest"> href="/assets/icons/apple-touch-icon.png"
<link rel="mask-icon" href="/assets/icons/safari-pinned-tab.svg" color="#b3c6ff"> />
<link rel="shortcut icon" href="/assets/icons/favicon.ico"> <link
<meta name="msapplication-TileColor" content="#ffffff"> rel="icon"
<meta name="msapplication-config" content="/assets/icons/browserconfig.xml"> type="image/png"
<meta name="theme-color" content="#ffffff"> sizes="32x32"
href="/assets/icons/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/assets/icons/favicon-16x16.png"
/>
<link rel="manifest" href="/assets/icons/site.webmanifest" />
<link
rel="mask-icon"
href="/assets/icons/safari-pinned-tab.svg"
color="#b3c6ff"
/>
<link rel="shortcut icon" href="/assets/icons/favicon.ico" />
<meta name="msapplication-TileColor" content="#ffffff" />
<meta
name="msapplication-config"
content="/assets/icons/browserconfig.xml"
/>
<meta name="theme-color" content="#ffffff" />
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

View File

@@ -8,22 +8,21 @@
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json", "check": "svelte-check --tsconfig ./tsconfig.json",
"lint": "eslint . --ext ts,tsx,svelte --max-warnings 0 --fix" "format": "prettier --write ."
}, },
"devDependencies": { "devDependencies": {
"@iconify/svelte": "^3.1.6", "@iconify/svelte": "^3.1.6",
"@sveltejs/vite-plugin-svelte": "^3.0.1", "@sveltejs/vite-plugin-svelte": "^3.0.1",
"@tsconfig/svelte": "^5.0.2", "@tsconfig/svelte": "^5.0.2",
"chartjs-adapter-moment": "^1.0.1", "chartjs-adapter-moment": "^1.0.1",
"eslint": "^8.56.0", "prettier": "^3.2.5",
"eslint-plugin-svelte": "^2.35.1", "prettier-plugin-svelte": "^3.1.2",
"sass": "^1.70.0", "sass": "^1.70.0",
"svelte": "^4.2.10", "svelte": "^4.2.10",
"svelte-check": "^3.6.4", "svelte-check": "^3.6.4",
"svelte-routing": "^2.12.0", "svelte-routing": "^2.12.0",
"tslib": "^2.6.2", "tslib": "^2.6.2",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"typescript-eslint": "^7.0.1",
"vite": "^5.1.1" "vite": "^5.1.1"
}, },
"dependencies": { "dependencies": {

View File

@@ -1,19 +1,19 @@
{ {
"name": "", "name": "",
"short_name": "", "short_name": "",
"icons": [ "icons": [
{ {
"src": "/assets/icons/android-chrome-192x192.png", "src": "/assets/icons/android-chrome-192x192.png",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "/assets/icons/android-chrome-512x512.png", "src": "/assets/icons/android-chrome-512x512.png",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png" "type": "image/png"
} }
], ],
"theme_color": "#ffffff", "theme_color": "#ffffff",
"background_color": "#ffffff", "background_color": "#ffffff",
"display": "standalone" "display": "standalone"
} }

View File

@@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import { Router, Route } from "svelte-routing"; import {Router, Route} from 'svelte-routing'
import Home from "./pages/Home.svelte"; import Home from './pages/Home.svelte'
import MaimaiRating from "./pages/MaimaiRating.svelte"; import MaimaiRating from './pages/MaimaiRating.svelte'
import UserHome from "./pages/UserHome.svelte"; import UserHome from './pages/UserHome.svelte'
import Icon from '@iconify/svelte'; import Icon from '@iconify/svelte'
export let url = ""; export let url = ''
</script> </script>
<nav> <nav>

View File

@@ -1,51 +1,54 @@
import { aqua_host, data_host } from './config' import {aqua_host, data_host} from './config'
import type { TrendEntry } from './generalTypes' import type {TrendEntry} from './generalTypes'
import type { MaimaiUserSummaryEntry } from './maimaiTypes' import type {MaimaiUserSummaryEntry} from './maimaiTypes'
const multTable = [ const multTable = [
[ 100.5, 22.4, 'SSSp' ], [100.5, 22.4, 'SSSp'],
[ 100, 21.6, 'SSS' ], [100, 21.6, 'SSS'],
[ 99.5, 21.1, 'SSp' ], [99.5, 21.1, 'SSp'],
[ 99, 20.8, 'SS' ], [99, 20.8, 'SS'],
[ 98, 20.3, 'Sp' ], [98, 20.3, 'Sp'],
[ 97, 20, 'S' ], [97, 20, 'S'],
[ 94, 16.8, 'AAA' ], [94, 16.8, 'AAA'],
[ 90, 15.2, 'AA' ], [90, 15.2, 'AA'],
[ 80, 13.6, 'A' ] [80, 13.6, 'A'],
] ]
export function getMult(achievement: number) { export function getMult(achievement: number) {
achievement /= 10000 achievement /= 10000
for (let i = 0; i < multTable.length; i++) { for (let i = 0; i < multTable.length; i++) {
if (achievement >= (multTable[i][0] as number)) return multTable[i] if (achievement >= (multTable[i][0] as number)) return multTable[i]
} }
return [ 0, 0, 0 ] return [0, 0, 0]
} }
export async function getMaimai(endpoint: string, params: any) { export async function getMaimai(endpoint: string, params: any) {
return await fetch(`${aqua_host}/Maimai2Servlet/${endpoint}`, { return await fetch(`${aqua_host}/Maimai2Servlet/${endpoint}`, {
method: 'POST', method: 'POST',
body: JSON.stringify(params) body: JSON.stringify(params),
}).then(res => res.json()) }).then((res) => res.json())
} }
export async function getMaimaiAllMusic(): Promise<{ [key: string]: any }> { export async function getMaimaiAllMusic(): Promise<{[key: string]: any}> {
return fetch(`${data_host}/maimai/meta/00/all-music.json`).then(it => it.json()) return fetch(`${data_host}/maimai/meta/00/all-music.json`).then((it) =>
it.json()
)
} }
export async function getMaimaiApi(endpoint: string, params: any) { export async function getMaimaiApi(endpoint: string, params: any) {
const url = new URL(`${aqua_host}/api/game/maimai2new/${endpoint}`) let url = new URL(`${aqua_host}/api/game/maimai2new/${endpoint}`)
Object.keys(params).forEach(key => url.searchParams.append(key, params[key])) Object.keys(params).forEach((key) =>
return await fetch(url).then(res => res.json()) url.searchParams.append(key, params[key])
)
return await fetch(url).then((res) => res.json())
} }
export async function getMaimaiTrend(userId: number): Promise<TrendEntry[]> { export async function getMaimaiTrend(userId: number): Promise<TrendEntry[]> {
return await getMaimaiApi('trend', { userId }) return await getMaimaiApi('trend', {userId})
} }
export async function getMaimaiUser(userId: number): Promise<MaimaiUserSummaryEntry> { export async function getMaimaiUser(
return await getMaimaiApi('user-summary', { userId }) userId: number
): Promise<MaimaiUserSummaryEntry> {
return await getMaimaiApi('user-summary', {userId})
} }

View File

@@ -1,118 +1,115 @@
export interface Rating { export interface Rating {
musicId: number musicId: number
level: number level: number
achievement: number achievement: number
} }
export interface ParsedRating extends Rating { export interface ParsedRating extends Rating {
music: MaimaiMusic, music: MaimaiMusic
calc: number, calc: number
rank: string rank: string
} }
export interface MaimaiMusic { export interface MaimaiMusic {
name: string, name: string
composer: string, composer: string
bpm: number, bpm: number
ver: number, ver: number
note: { note: {
lv: number lv: number
designer: string designer: string
lv_id: number lv_id: number
notes: number notes: number
} }
} }
export interface MaimaiUserSummaryEntry { export interface MaimaiUserSummaryEntry {
name: string name: string
iconId: number iconId: number
serverRank: number serverRank: number
accuracy: number accuracy: number
rating: number rating: number
ratingHighest: number ratingHighest: number
ranks: { name: string, count: number }[] ranks: {name: string; count: number}[]
maxCombo: number maxCombo: number
fullCombo: number fullCombo: number
allPerfect: number allPerfect: number
totalDxScore: number totalDxScore: number
plays: number plays: number
totalPlayTime: number totalPlayTime: number
joined: string joined: string
lastSeen: string lastSeen: string
lastVersion: string lastVersion: string
best35: string best35: string
best15: string best15: string
recent: MaimaiUserPlaylog[] recent: MaimaiUserPlaylog[]
} }
export interface MaimaiUserPlaylog { export interface MaimaiUserPlaylog {
id: number; id: number
musicId: number; musicId: number
level: number; level: number
userPlayDate: string; trackNo: number
trackNo: number; vsRank: number
vsRank: number; achievement: number
achievement: number; deluxscore: number
deluxscore: number; scoreRank: number
scoreRank: number; maxCombo: number
maxCombo: number; totalCombo: number
totalCombo: number; maxSync: number
maxSync: number; totalSync: number
totalSync: number; tapCriticalPerfect: number
tapCriticalPerfect: number; tapPerfect: number
tapPerfect: number; tapGreat: number
tapGreat: number; tapGood: number
tapGood: number; tapMiss: number
tapMiss: number; holdCriticalPerfect: number
holdCriticalPerfect: number; holdPerfect: number
holdPerfect: number; holdGreat: number
holdGreat: number; holdGood: number
holdGood: number; holdMiss: number
holdMiss: number; slideCriticalPerfect: number
slideCriticalPerfect: number; slidePerfect: number
slidePerfect: number; slideGreat: number
slideGreat: number; slideGood: number
slideGood: number; slideMiss: number
slideMiss: number; touchCriticalPerfect: number
touchCriticalPerfect: number; touchPerfect: number
touchPerfect: number; touchGreat: number
touchGreat: number; touchGood: number
touchGood: number; touchMiss: number
touchMiss: number; breakCriticalPerfect: number
breakCriticalPerfect: number; breakPerfect: number
breakPerfect: number; breakGreat: number
breakGreat: number; breakGood: number
breakGood: number; breakMiss: number
breakMiss: number; isTap: boolean
isTap: boolean; isHold: boolean
isHold: boolean; isSlide: boolean
isSlide: boolean; isTouch: boolean
isTouch: boolean; isBreak: boolean
isBreak: boolean; isCriticalDisp: boolean
isCriticalDisp: boolean; isFastLateDisp: boolean
isFastLateDisp: boolean; fastCount: number
fastCount: number; lateCount: number
lateCount: number; isAchieveNewRecord: boolean
isAchieveNewRecord: boolean; isDeluxscoreNewRecord: boolean
isDeluxscoreNewRecord: boolean; comboStatus: number
comboStatus: number; syncStatus: number
syncStatus: number; isClear: boolean
isClear: boolean; beforeRating: number
beforeRating: number; afterRating: number
afterRating: number; beforeGrade: number
beforeGrade: number; afterGrade: number
afterGrade: number; afterGradeRank: number
afterGradeRank: number; beforeDeluxRating: number
beforeDeluxRating: number; afterDeluxRating: number
afterDeluxRating: number; isPlayTutorial: boolean
isPlayTutorial: boolean; isEventMode: boolean
isEventMode: boolean; isFreedomMode: boolean
isFreedomMode: boolean; playMode: number
playMode: number; isNewFree: boolean
isNewFree: boolean; trialPlayAchievement: number
trialPlayAchievement: number; extNum1: number
extNum1: number; extNum2: number
extNum2: number;
extNum4: number;
extBool1: boolean;
} }

View File

@@ -6,13 +6,17 @@ import {
LineElement, LineElement,
LinearScale, LinearScale,
PointElement, PointElement,
CategoryScale, TimeScale, type ChartOptions, type LineOptions, CategoryScale,
TimeScale,
type ChartOptions,
type LineOptions,
} from 'chart.js' } from 'chart.js'
import moment from 'moment/moment' import moment from 'moment/moment'
// @ts-expect-error Cal-heatmap does not have proper types // @ts-ignore
import CalHeatmap from 'cal-heatmap' import CalHeatmap from 'cal-heatmap'
// @ts-expect-error Cal-heatmap does not have proper types // @ts-ignore
import CalTooltip from 'cal-heatmap/plugins/Tooltip' import CalTooltip from 'cal-heatmap/plugins/Tooltip'
import type {Line} from 'svelte-chartjs'
export function title(t: string) { export function title(t: string) {
document.title = `AquaNet - ${t}` document.title = `AquaNet - ${t}`
@@ -31,62 +35,72 @@ export function registerChart() {
) )
} }
export function renderCal(el: HTMLElement, d: {date: any, value: any}[]) { export function renderCal(el: HTMLElement, d: {date: any; value: any}[]) {
const cal = new CalHeatmap() const cal = new CalHeatmap()
return cal.paint({ return cal.paint(
itemSelector: el, {
domain: { itemSelector: el,
type: 'month', domain: {
label: { text: 'MMM', textAlign: 'start', position: 'top' }, type: 'month',
}, label: {text: 'MMM', textAlign: 'start', position: 'top'},
subDomain: {
type: 'ghDay',
radius: 2, width: 11, height: 11, gutter: 4
},
range: 12,
data: { source: d, x: 'date', y: 'value' },
scale: {
color: {
type: 'linear',
range: [ '#14432a', '#4dd05a' ],
domain: [ 0, d.reduce((a, b) => Math.max(a, b.value), 0) ]
}, },
subDomain: {
type: 'ghDay',
radius: 2,
width: 11,
height: 11,
gutter: 4,
},
range: 12,
data: {source: d, x: 'date', y: 'value'},
scale: {
color: {
type: 'linear',
range: ['#14432a', '#4dd05a'],
domain: [0, d.reduce((a, b) => Math.max(a, b.value), 0)],
},
},
date: {start: moment().subtract(1, 'year').add(1, 'month').toDate()},
theme: 'dark',
}, },
date: { start: moment().subtract(1, 'year').add(1, 'month').toDate() }, [
theme: 'dark', [
}, [ CalTooltip,
[ CalTooltip, { text: (_: Date, v: number, d: any) => {
`${v ?? 'No'} songs played on ${d.format('MMMM D, YYYY')}` }] text: (_: Date, v: number, d: any) =>
]) `${v ?? 'No'} songs played on ${d.format('MMMM D, YYYY')}`,
},
],
]
)
} }
export const CHARTJS_OPT: ChartOptions<'line'> = { export const CHARTJS_OPT: ChartOptions<'line'> = {
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
// TODO: Show point on hover // TODO: Show point on hover
elements: { elements: {
point: { point: {
radius: 0 radius: 0,
} },
}, },
scales: { scales: {
xAxis: { xAxis: {
type: 'time', type: 'time',
display: false display: false,
}, },
y: { y: {
display: false, display: false,
} },
}, },
plugins: { plugins: {
legend: { legend: {
display: false display: false,
}, },
tooltip: { tooltip: {
mode: 'index', mode: 'index',
intersect: false intersect: false,
} },
}, },
} }
@@ -95,6 +109,8 @@ export const CHARTJS_OPT: ChartOptions<'line'> = {
* *
* @param obj HashMap<string, boolean> * @param obj HashMap<string, boolean>
*/ */
export function clazz(obj: { [key: string]: boolean }) { export function clazz(obj: {[key: string]: boolean}) {
return Object.keys(obj).filter(k => obj[k]).join(' ') return Object.keys(obj)
.filter((k) => obj[k])
.join(' ')
} }

View File

@@ -1,6 +1,7 @@
import './app.sass' import './app.sass'
import App from './App.svelte' import App from './App.svelte'
const app = new App({ target: document.getElementById('app')! }) // @ts-ignore
const app = new App({target: document.getElementById('app')})
export default app export default app

View File

@@ -1,60 +1,62 @@
<script lang="ts"> <script lang="ts">
import {data_host} from "../libs/config"; import {data_host} from '../libs/config'
import {getMaimaiAllMusic, getMaimai, getMult} from "../libs/maimai"; import {getMaimaiAllMusic, getMaimai, getMult} from '../libs/maimai'
import type {ParsedRating, Rating} from "../libs/maimaiTypes"; import type {ParsedRating, Rating} from '../libs/maimaiTypes'
export let userId: any export let userId: any
userId = +userId userId = +userId
if (!userId) console.error("No user ID provided") if (!userId) console.error('No user ID provided')
Promise.all([ Promise.all([
getMaimai("GetUserRatingApi", {userId}), getMaimai('GetUserRatingApi', {userId}),
getMaimaiAllMusic() getMaimaiAllMusic(),
]).then(([rating, music]) => { ]).then(([rating, music]) => {
data = rating data = rating
musicInfo = music musicInfo = music
if (!data || !musicInfo) { if (!data || !musicInfo) {
console.error("Failed to fetch data") console.error('Failed to fetch data')
return return
} }
parsedRatings = { parsedRatings = {
old: parseRating(data.userRating.ratingList), old: parseRating(data.userRating.ratingList),
new: parseRating(data.userRating.newRatingList) new: parseRating(data.userRating.newRatingList),
} }
}) })
function parseRating(arr: Rating[]) { function parseRating(arr: Rating[]) {
return arr.map(x => { return arr
const music = musicInfo[x.musicId] .map((x) => {
const music = musicInfo[x.musicId]
if (!music) { if (!music) {
console.error(`Music not found: ${x.musicId}`) console.error(`Music not found: ${x.musicId}`)
return null return null
} }
music.note = music.notes[x.level] music.note = music.notes[x.level]
const mult = getMult(x.achievement) const mult = getMult(x.achievement)
return { return {
...x, ...x,
music: music, music: music,
calc: (mult[1] as number) * music.note.lv, calc: (mult[1] as number) * music.note.lv,
rank: mult[2] rank: mult[2],
} }
}).filter(x => x != null) as ParsedRating[] })
.filter((x) => x != null) as ParsedRating[]
} }
let parsedRatings: { let parsedRatings: {
old: ParsedRating[], old: ParsedRating[]
new: ParsedRating[] new: ParsedRating[]
} | null = null } | null = null
let data: { let data: {
userRating: { userRating: {
rating: number, rating: number
ratingList: Rating[], ratingList: Rating[]
newRatingList: Rating[] newRatingList: Rating[]
} }
} | null = null } | null = null
@@ -65,26 +67,39 @@
<main> <main>
<!-- Display all parsed ratings --> <!-- Display all parsed ratings -->
{#if parsedRatings} {#if parsedRatings}
{#each [{title: "Old", data: parsedRatings.old}, {title: "New", data: parsedRatings.new}] as section} {#each [{title: 'Old', data: parsedRatings.old}, {title: 'New', data: parsedRatings.new}] as section}
<h2>{section.title}</h2> <h2>{section.title}</h2>
<div class="rating-cards"> <div class="rating-cards">
{#each section.data as rating} {#each section.data as rating}
<div class="level-{rating.level}"> <div class="level-{rating.level}">
<img class="cover" <img
src={`${data_host}/maimai/assetbundle/jacket_s/00${rating.musicId.toString().padStart(6, '0').substring(2)}.png`} class="cover"
alt=""> src={`${data_host}/maimai/assetbundle/jacket_s/00${rating.musicId
.toString()
.padStart(6, '0')
.substring(2)}.png`}
alt=""
/>
<div class="detail"> <div class="detail">
<span class="name">{rating.music.name}</span> <span class="name">{rating.music.name}</span>
<span class="rating"> <span class="rating">
<span>{(rating.achievement / 10000).toFixed(2)}%</span> <span>{(rating.achievement / 10000).toFixed(2)}%</span>
<img class="rank" src={`${data_host}/maimai/sprites/rankimage/UI_GAM_Rank_${rating.rank}.png`} alt=""> <img
</span> class="rank"
src={`${data_host}/maimai/sprites/rankimage/UI_GAM_Rank_${rating.rank}.png`}
alt=""
/>
</span>
<span>{rating.calc.toFixed(1)}</span> <span>{rating.calc.toFixed(1)}</span>
</div> </div>
<img class="ver" <img
src={`${data_host}/maimai/sprites/tab/title/UI_CMN_TabTitle_MaimaiTitle_Ver${rating.music.ver.toString().substring(0, 3)}.png`} class="ver"
alt=""> src={`${data_host}/maimai/sprites/tab/title/UI_CMN_TabTitle_MaimaiTitle_Ver${rating.music.ver
.toString()
.substring(0, 3)}.png`}
alt=""
/>
<div class="lv">{rating.music.note.lv}</div> <div class="lv">{rating.music.note.lv}</div>
</div> </div>
{/each} {/each}
@@ -94,106 +109,111 @@
</main> </main>
<style lang="sass"> <style lang="sass">
.rating-cards .rating-cards
display: grid display: grid
gap: 2rem gap: 2rem
width: 100% width: 100%
// Fill as many columns as possible // Fill as many columns as possible
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)) grid-template-columns: repeat(auto-fill, minmax(200px, 1fr))
// Center the cards // Center the cards
justify-items: center justify-items: center
align-items: center align-items: center
// Style each card // Style each card
> div > div
$border-radius: 20px $border-radius: 20px
width: 200px width: 200px
height: 200px height: 200px
border-radius: $border-radius border-radius: $border-radius
display: flex
position: relative
// Difficulty border
border: 5px solid var(--lv-color, #60aaff)
&.level-1
--lv-color: #aaff60
&.level-2
--lv-color: #f25353
&.level-3
--lv-color: #e881ff
img
object-fit: cover
pointer-events: none
img.cover
width: 100%
height: 100%
border-radius: calc($border-radius - 3px)
img.ver
position: absolute
top: -20px
left: -30px
height: 50px
// Information
.detail
position: absolute
bottom: 0
left: 0
right: 0
padding: 10px
background: rgba(0, 0, 0, 0.5)
border-radius: 0 0 calc($border-radius - 3px) calc($border-radius - 3px)
// Blur
backdrop-filter: blur(3px)
display: flex display: flex
position: relative flex-direction: column
text-align: left
// Difficulty border > span
border: 5px solid var(--lv-color) // Disable text wrapping, max 2 lines
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
img .name
object-fit: cover font-size: 1.2em
pointer-events: none font-weight: bold
img.cover .rating
width: 100% display: flex
height: 100% img
border-radius: calc($border-radius - 3px) height: 1.5em
.lv
position: absolute
bottom: 0
right: 0
padding: 5px 10px
background: var(--lv-color)
// Top left border radius
border-radius: 10px 0
font-size: 1.3em
&:before
content: "Lv"
font-size: 0.8em
// Mobile
@media (max-width: 500px)
margin-left: -1rem
margin-right: -1rem
width: calc(100% + 2rem)
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr))
font-size: 0.8em
> div
width: 150px
height: 150px
img.ver img.ver
position: absolute height: 45px
top: -20px left: -20px
left: -30px
height: 50px
// Information
.detail
position: absolute
bottom: 0
left: 0
right: 0
padding: 10px
background: rgba(0, 0, 0, 0.5)
border-radius: 0 0 calc($border-radius - 3px) calc($border-radius - 3px)
// Blur
backdrop-filter: blur(3px)
display: flex
flex-direction: column
text-align: left
> span
// Disable text wrapping, max 2 lines
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
.name
font-size: 1.2em
font-weight: bold
.rating
display: flex
img
height: 1.5em
.lv
position: absolute
bottom: 0
right: 0
padding: 5px 10px
background: var(--lv-color)
// Top left border radius
border-radius: 10px 0
font-size: 1.3em
&:before
content: "Lv"
font-size: 0.8em
// Mobile
@media (max-width: 500px)
margin-left: -1rem
margin-right: -1rem
width: calc(100% + 2rem)
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr))
font-size: 0.8em
> div
width: 150px
height: 150px
img.ver
height: 45px
left: -20px
</style> </style>

View File

@@ -1,17 +1,26 @@
<script lang="ts"> <script lang="ts">
import {CHARTJS_OPT, clazz, registerChart, renderCal, title} from "../libs/ui"; import {CHARTJS_OPT, clazz, registerChart, renderCal, title} from '../libs/ui'
import {getMaimaiAllMusic, getMaimaiTrend, getMaimaiUser, getMult} from "../libs/maimai"; import {
import type {MaimaiMusic, MaimaiUserPlaylog, MaimaiUserSummaryEntry} from "../libs/maimaiTypes"; getMaimaiAllMusic,
import type {TrendEntry} from "../libs/generalTypes"; getMaimaiTrend,
import {data_host} from "../libs/config"; getMaimaiUser,
import 'cal-heatmap/cal-heatmap.css'; getMult,
import { Line } from 'svelte-chartjs'; } from '../libs/maimai'
import moment from "moment"; import type {
import 'chartjs-adapter-moment'; MaimaiMusic,
MaimaiUserPlaylog,
MaimaiUserSummaryEntry,
} from '../libs/maimaiTypes'
import type {TrendEntry} from '../libs/generalTypes'
import {data_host} from '../libs/config'
import 'cal-heatmap/cal-heatmap.css'
import {Line} from 'svelte-chartjs'
import moment from 'moment'
import 'chartjs-adapter-moment'
registerChart() registerChart()
export let userId: any; export let userId: any
userId = +userId userId = +userId
let calElement: HTMLElement let calElement: HTMLElement
@@ -20,7 +29,7 @@
interface MusicAndPlay extends MaimaiMusic, MaimaiUserPlaylog {} interface MusicAndPlay extends MaimaiMusic, MaimaiUserPlaylog {}
let d: { let d: {
user: MaimaiUserSummaryEntry, user: MaimaiUserSummaryEntry
trend: TrendEntry[] trend: TrendEntry[]
recent: MusicAndPlay[] recent: MusicAndPlay[]
} | null = null } | null = null
@@ -28,25 +37,37 @@
Promise.all([ Promise.all([
getMaimaiUser(userId), getMaimaiUser(userId),
getMaimaiTrend(userId), getMaimaiTrend(userId),
getMaimaiAllMusic() getMaimaiAllMusic(),
]).then(([user, trend, music]) => { ]).then(([user, trend, music]) => {
console.log(user) console.log(user)
console.log(trend) console.log(trend)
console.log(music) console.log(music)
// Sort recent by date d = {
user.recent.sort((a, b) => b.userPlayDate < a.userPlayDate ? -1 : 1) user,
trend,
d = {user, trend, recent: user.recent.map(it => {return {...music[it.musicId], ...it}})} recent: user.recent.map((it) => {
localStorage.setItem("tmp-user-details", JSON.stringify(d)) return {...music[it.musicId], ...it}
renderCal(calElement, trend.map(it => {return {date: it.date, value: it.plays}})) }),
}
localStorage.setItem('tmp-user-details', JSON.stringify(d))
renderCal(
calElement,
trend.map((it) => {
return {date: it.date, value: it.plays}
})
)
}) })
</script> </script>
<main id="user-home"> <main id="user-home">
{#if d !== null} {#if d !== null}
<div class="user-pfp"> <div class="user-pfp">
<img src={`${data_host}/maimai/assetbundle/icon/${d.user.iconId.toString().padStart(6, "0")}.png`} alt="" class="pfp"> <img
src={`${data_host}/maimai/assetbundle/icon/${d.user.iconId.toString().padStart(6, '0')}.png`}
alt=""
class="pfp"
/>
<h2>{d.user.name}</h2> <h2>{d.user.name}</h2>
</div> </div>
@@ -69,18 +90,23 @@
<div class="trend"> <div class="trend">
<!-- ChartJS cannot be fully responsive unless there is a parent div that's independent from its size and helps it determine its size --> <!-- ChartJS cannot be fully responsive unless there is a parent div that's independent from its size and helps it determine its size -->
<div class="chartjs-box-reference"> <div class="chartjs-box-reference">
<Line data={{ <Line
datasets: [ data={{
{ datasets: [
label: 'Rating', {
data: d.trend.map(it => {return {x: Date.parse(it.date), y: it.rating}}), label: 'Rating',
borderColor: '#646cff', data: d.trend.map((it) => {
tension: 0.1, return {x: Date.parse(it.date), y: it.rating}
}),
borderColor: '#646cff',
tension: 0.1,
// TODO: Set X axis span to 3 months // TODO: Set X axis span to 3 months
} },
] ],
}} options={CHARTJS_OPT} /> }}
options={CHARTJS_OPT}
/>
</div> </div>
</div> </div>
@@ -141,12 +167,12 @@
<div class="first-play"> <div class="first-play">
<span>First Seen</span> <span>First Seen</span>
<span>{moment(d.user.joined).format("YYYY-MM-DD")}</span> <span>{moment(d.user.joined).format('YYYY-MM-DD')}</span>
</div> </div>
<div class="last-play"> <div class="last-play">
<span>Last Seen</span> <span>Last Seen</span>
<span>{moment(d.user.lastSeen).format("YYYY-MM-DD")}</span> <span>{moment(d.user.lastSeen).format('YYYY-MM-DD')}</span>
</div> </div>
<div class="last-version"> <div class="last-version">
@@ -162,18 +188,27 @@
<div class="scores"> <div class="scores">
{#each d.recent as r, i} {#each d.recent as r, i}
<div class={clazz({alt: i % 2 === 0})}> <div class={clazz({alt: i % 2 === 0})}>
<img src={`${data_host}/maimai/assetbundle/jacket_s/00${r.musicId.toString().padStart(6, '0').substring(2)}.png`} alt=""> <img
src={`${data_host}/maimai/assetbundle/jacket_s/00${r.musicId.toString().padStart(6, '0').substring(2)}.png`}
alt=""
/>
<div class="info"> <div class="info">
<span class="name">{r.name}</span>
<div> <div>
<span class="name">{r.name}</span> <span class={'rank-' + ('' + getMult(r.achievement)[2])[0]}>
</div> <span class="rank-text"
<div> >{('' + getMult(r.achievement)[2]).replace('p', '+')}</span
<span class={`lv level-${r.level}`}>{r.notes[r.level].lv}</span> >
<span class={"rank-" + ("" + getMult(r.achievement)[2])[0]}> <span class="rank-num"
<span class="rank-text">{("" + getMult(r.achievement)[2]).replace("p", "+")}</span> >{(r.achievement / 10000).toFixed(2)}%</span
<span class="rank-num">{(r.achievement / 10000).toFixed(2)}%</span> >
</span> </span>
<span class={"dx-change " + clazz({increased: r.afterDeluxRating - r.beforeDeluxRating > 0})}> <span
class={'dx-change ' +
clazz({
increased: r.afterDeluxRating - r.beforeDeluxRating > 0,
})}
>
{r.afterDeluxRating - r.beforeDeluxRating} {r.afterDeluxRating - r.beforeDeluxRating}
</span> </span>
</div> </div>
@@ -357,7 +392,7 @@ $gap: 20px
flex-direction: column flex-direction: column
gap: 0 gap: 0
.rank-text span
text-align: left text-align: left
.rank-S .rank-S
@@ -372,13 +407,6 @@ $gap: 20px
.rank-B .rank-B
color: #6ba6ff color: #6ba6ff
.lv
background: var(--lv-color)
padding: 0 6px
border-radius: 10px
opacity: 0.8
margin-right: 10px
span span
display: inline-block display: inline-block
text-align: right text-align: right

View File

@@ -7,12 +7,3 @@ $c-bg: #242424
$nav-height: 4rem $nav-height: 4rem
$w-mobile: 560px $w-mobile: 560px
$w-max: 900px $w-max: 900px
.level-0
--lv-color: #6ED43E
.level-1
--lv-color: #F7B807
.level-2
--lv-color: #FF828D
.level-3
--lv-color: #A051DC

View File

@@ -1,4 +1,4 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' import {vitePreprocess} from '@sveltejs/vite-plugin-svelte'
export default { export default {
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess // Consult https://svelte.dev/docs#compile-time-svelte-preprocess

View File

@@ -16,5 +16,5 @@
"isolatedModules": true "isolatedModules": true
}, },
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{"path": "./tsconfig.node.json"}]
} }

View File

@@ -1,5 +1,5 @@
import { defineConfig } from 'vite' import {defineConfig} from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte' import {svelte} from '@sveltejs/vite-plugin-svelte'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
# AquaDX # AquaDX
Multipurpose game server powered by Spring Boot, for ALL.Net-based games Multipurpose game server powered by Spring Boot, for ALL.Net based games
This is an attempt to rebuild the [original Aqua server](https://dev.s-ul.net/NeumPhis/aqua) This is an attempt to rebuild the [original Aqua server](https://dev.s-ul.net/NeumPhis/aqua)
@@ -26,9 +26,9 @@ Check out these docs for more information.
* [Frequently asked questions](docs/frequently_asked_questions.md) * [Frequently asked questions](docs/frequently_asked_questions.md)
### Notes ### Notes
* Some games may require additional patches and these will not provided in this project and repository. You already found this, so you know where to find related resources too. * Some game may require additional patches and these will not provided in this project and repository. You already found this, so you know where to find related resources too.
* This repository may contain untested, experimental implementations for a few games which I can't test properly. If you couldn't find your wanted game in the above list, do not expect support. * This repository may contain untested, experimental implementation for few games which I can't test properly. If you couldn't find your wanted game in the above list, do not expect support.
* This server also provides a simple API for viewing play records and editing settings for some games. * This server also provides a simple API for viewing play records and edit settings for some games.
### Usage ### Usage
@@ -37,28 +37,28 @@ Check out these docs for more information.
3. Extract the zip file to a folder. 3. Extract the zip file to a folder.
4. Run `java -jar aqua.jar` in the folder. 4. Run `java -jar aqua.jar` in the folder.
By default, Aqua will use SQLite and save user data in `data/db.sqlite`. By default, Aqua will use sqlite and save user data in data/db.sqlite.
If you want to use optional databases, please edit the configuration file then it will auto-create the table and import some initial data. If you want to use optional databases, edit configuration file then it will auto create the table and import some initial data.
### Configuration ### Configuration
Configuration is saved in `config/application.properties`, spring loads this file automatically. Configuration is saved in `config/application.properties`, spring loads this file automatically.
* The host and port of game title servers can be overwritten in `allnet.server.host` and `allnet.server.port`. By default it will send the same host and port the client used the request this information. * The host and port of game title servers can be overritten in `allnet.server.host` and `allnet.server.port`. By default it will send the same host and port the client used the request this information.
This will be sent to the game at booting and being used by the following request. This will be sent to the game at booting and being used by following request.
* You can switch to the MariaDB (or MySQL) database by commenting the Sqlite part. * You can switch to MariaDB (or MySQL) database by commenting the Sqlite part.
* For some games, you might need to change some game-specific config entries. * For some game, you might need to change some game specific config entries.
### Building ### Building
You need to install JDK on your system. However, you don't need to install Gradle separately, as the `gradlew` wrapper script is included. You need to install JDK on your system. However, you don't need to care about Gradle, as wrapper script is included.
``` ```
gradlew clean build gradlew clean build
``` ```
The `build/libs` folder will contain a jar file. The `build/libs` folder will contain an jar file.
### Credit ### Credit
* **samnyan**: The creator and developer of the original Aqua server * **samnyan**: The creator and developer of the original Aqua server
* **Akasaka Ryuunosuke**: providing all the DIVA protocol information * **Akasaka Ryuunosuke** : providing all the DIVA protocol information
* Dom Eori: Developer of forked Aqua server, from v0.0.17 and up * Dom Eori : Developer of forked Aqua server, from v0.0.17 and up
* All devs who contribute to the [MiniMe server](https://dev.s-ul.net/djhackers/minime) * All devs who contribute to the [MiniMe server](https://dev.s-ul.net/djhackers/minime)
* All contributors by merge requests, issues and other channels * All contributors by merge request, issues and other channels

View File

@@ -36,9 +36,9 @@ dependencies {
testImplementation("org.springframework.security:spring-security-test") testImplementation("org.springframework.security:spring-security-test")
// Database // Database
runtimeOnly("com.mysql:mysql-connector-j:8.3.0") runtimeOnly("com.mysql:mysql-connector-j:8.0.33")
runtimeOnly("org.mariadb.jdbc:mariadb-java-client:3.3.2") runtimeOnly("org.mariadb.jdbc:mariadb-java-client:3.1.3")
runtimeOnly("org.xerial:sqlite-jdbc:3.45.1.0") runtimeOnly("org.xerial:sqlite-jdbc:3.41.2.1")
implementation("com.github.gwenn:sqlite-dialect:0.1.4") implementation("com.github.gwenn:sqlite-dialect:0.1.4")
// JSR305 for nullable // JSR305 for nullable

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
networkTimeout=10000 networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@@ -153,6 +153,4 @@ public class UserDetail implements Serializable {
@Transient @Transient
private int cmLastEmoneyCredit = 69; private int cmLastEmoneyCredit = 69;
private int mapStock; private int mapStock;
private int currentPlayCount;
private int renameCredit;
} }

View File

@@ -255,9 +255,4 @@ public class UserPlaylog implements Serializable {
private int extNum2; private int extNum2;
private int extNum4;
@JsonProperty("extBool1")
private boolean extBool1;
} }

View File

@@ -1,14 +0,0 @@
INSERT INTO `maimai2_game_event` (`id`, `end_date`, `start_date`, `type`, `enable`) VALUES
(23120811, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120821, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120822, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120823, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120824, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120825, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120841, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120842, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120843, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120844, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120851, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120852, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23122271, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1');

View File

@@ -1,5 +0,0 @@
INSERT INTO maimai2_game_charge (id, order_id, charge_id, price, start_date, end_date) VALUES (1, 1, 2, 1, '2019-01-01 00:00:00.000000', '2099-01-01 00:00:00.000000');
INSERT INTO maimai2_game_charge (id, order_id, charge_id, price, start_date, end_date) VALUES (2, 2, 3, 2, '2019-01-01 00:00:00.000000', '2099-01-01 00:00:00.000000');
INSERT INTO maimai2_game_charge (id, order_id, charge_id, price, start_date, end_date) VALUES (3, 3, 4, 3, '2019-01-01 00:00:00.000000', '2099-01-01 00:00:00.000000');
INSERT INTO maimai2_game_charge (id, order_id, charge_id, price, start_date, end_date) VALUES (4, 4, 5, 4, '2019-01-01 00:00:00.000000', '2099-01-01 00:00:00.000000');
INSERT INTO maimai2_game_charge (id, order_id, charge_id, price, start_date, end_date) VALUES (5, 5, 6, 5, '2019-01-01 00:00:00.000000', '2099-01-01 00:00:00.000000');

View File

@@ -1,4 +0,0 @@
INSERT INTO `maimai2_game_event` (`id`, `end_date`, `start_date`, `type`, `enable`) VALUES
(24011111, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(24011121, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(24011141, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1');

View File

@@ -1,17 +0,0 @@
-- maimai2_user_playlog
-- Set ext_bool1 as NOT NULL and give it a default value (e.g., FALSE)
UPDATE maimai2_user_playlog SET ext_bool1 = FALSE WHERE ext_bool1 IS NULL;
ALTER TABLE maimai2_user_playlog MODIFY COLUMN ext_bool1 BOOLEAN NOT NULL DEFAULT FALSE;
-- Set ext_num4 as NOT NULL (assuming it already has a default value of 0)
UPDATE maimai2_user_playlog SET ext_num4 = 0 WHERE ext_num4 IS NULL;
ALTER TABLE maimai2_user_playlog MODIFY COLUMN ext_num4 INTEGER NOT NULL DEFAULT 0;
-- maimai2_user_detail
-- Add default value for current_play_count and set it as NOT NULL
UPDATE maimai2_user_detail SET current_play_count = 0 WHERE current_play_count IS NULL;
ALTER TABLE maimai2_user_detail MODIFY COLUMN current_play_count INTEGER NOT NULL DEFAULT 0;
-- Add default value for rename_credit and set it as NOT NULL
UPDATE maimai2_user_detail SET rename_credit = 0 WHERE rename_credit IS NULL;
ALTER TABLE maimai2_user_detail MODIFY COLUMN rename_credit INTEGER NOT NULL DEFAULT 0;

View File

@@ -1,8 +0,0 @@
-- maimai2_user_playlog
ALTER TABLE maimai2_user_playlog ADD COLUMN ext_bool1 BOOLEAN;
ALTER TABLE maimai2_user_playlog ADD COLUMN ext_num4 INTEGER;
UPDATE maimai2_user_playlog SET ext_num4=0;
-- maimai2_user_detail
ALTER TABLE maimai2_user_detail ADD COLUMN current_play_count INTEGER;
ALTER TABLE maimai2_user_detail ADD COLUMN rename_credit INTEGER;

View File

@@ -1,14 +0,0 @@
INSERT INTO `maimai2_game_event` (`id`, `end_date`, `start_date`, `type`, `enable`) VALUES
(23120811, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120821, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120822, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120823, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120824, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120825, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120841, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120842, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120843, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120844, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120851, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120852, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23122271, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1');

View File

@@ -1,5 +0,0 @@
INSERT INTO maimai2_game_charge (id, order_id, charge_id, price, start_date, end_date) VALUES (1, 1, 2, 1, '2019-01-01 00:00:00.000000', '2099-01-01 00:00:00.000000');
INSERT INTO maimai2_game_charge (id, order_id, charge_id, price, start_date, end_date) VALUES (2, 2, 3, 2, '2019-01-01 00:00:00.000000', '2099-01-01 00:00:00.000000');
INSERT INTO maimai2_game_charge (id, order_id, charge_id, price, start_date, end_date) VALUES (3, 3, 4, 3, '2019-01-01 00:00:00.000000', '2099-01-01 00:00:00.000000');
INSERT INTO maimai2_game_charge (id, order_id, charge_id, price, start_date, end_date) VALUES (4, 4, 5, 4, '2019-01-01 00:00:00.000000', '2099-01-01 00:00:00.000000');
INSERT INTO maimai2_game_charge (id, order_id, charge_id, price, start_date, end_date) VALUES (5, 5, 6, 5, '2019-01-01 00:00:00.000000', '2099-01-01 00:00:00.000000');

View File

@@ -1,4 +0,0 @@
INSERT INTO `maimai2_game_event` (`id`, `end_date`, `start_date`, `type`, `enable`) VALUES
(24011111, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(24011121, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(24011141, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1');

View File

@@ -1,17 +0,0 @@
-- maimai2_user_playlog
-- Set ext_bool1 as NOT NULL and give it a default value (e.g., FALSE)
UPDATE maimai2_user_playlog SET ext_bool1 = FALSE WHERE ext_bool1 IS NULL;
ALTER TABLE maimai2_user_playlog MODIFY COLUMN ext_bool1 BOOLEAN NOT NULL DEFAULT FALSE;
-- Set ext_num4 as NOT NULL (assuming it already has a default value of 0)
UPDATE maimai2_user_playlog SET ext_num4 = 0 WHERE ext_num4 IS NULL;
ALTER TABLE maimai2_user_playlog MODIFY COLUMN ext_num4 INTEGER NOT NULL DEFAULT 0;
-- maimai2_user_detail
-- Add default value for current_play_count and set it as NOT NULL
UPDATE maimai2_user_detail SET current_play_count = 0 WHERE current_play_count IS NULL;
ALTER TABLE maimai2_user_detail MODIFY COLUMN current_play_count INTEGER NOT NULL DEFAULT 0;
-- Add default value for rename_credit and set it as NOT NULL
UPDATE maimai2_user_detail SET rename_credit = 0 WHERE rename_credit IS NULL;
ALTER TABLE maimai2_user_detail MODIFY COLUMN rename_credit INTEGER NOT NULL DEFAULT 0;

View File

@@ -1,8 +0,0 @@
-- maimai2_user_playlog
ALTER TABLE maimai2_user_playlog ADD COLUMN ext_bool1 BOOLEAN;
ALTER TABLE maimai2_user_playlog ADD COLUMN ext_num4 INTEGER;
UPDATE maimai2_user_playlog SET ext_num4=0;
-- maimai2_user_detail
ALTER TABLE maimai2_user_detail ADD COLUMN current_play_count INTEGER;
ALTER TABLE maimai2_user_detail ADD COLUMN rename_credit INTEGER;

View File

@@ -1,14 +0,0 @@
INSERT INTO `maimai2_game_event` (`id`, `end_date`, `start_date`, `type`, `enable`) VALUES
(23120811, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120821, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120822, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120823, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120824, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120825, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120841, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120842, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120843, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120844, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120851, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23120852, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(23122271, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1');

View File

@@ -1,5 +0,0 @@
INSERT INTO maimai2_game_charge (id, order_id, charge_id, price, start_date, end_date) VALUES (1, 1, 2, 1, '2019-01-01 00:00:00.000000', '2099-01-01 00:00:00.000000');
INSERT INTO maimai2_game_charge (id, order_id, charge_id, price, start_date, end_date) VALUES (2, 2, 3, 2, '2019-01-01 00:00:00.000000', '2099-01-01 00:00:00.000000');
INSERT INTO maimai2_game_charge (id, order_id, charge_id, price, start_date, end_date) VALUES (3, 3, 4, 3, '2019-01-01 00:00:00.000000', '2099-01-01 00:00:00.000000');
INSERT INTO maimai2_game_charge (id, order_id, charge_id, price, start_date, end_date) VALUES (4, 4, 5, 4, '2019-01-01 00:00:00.000000', '2099-01-01 00:00:00.000000');
INSERT INTO maimai2_game_charge (id, order_id, charge_id, price, start_date, end_date) VALUES (5, 5, 6, 5, '2019-01-01 00:00:00.000000', '2099-01-01 00:00:00.000000');

View File

@@ -1,4 +0,0 @@
INSERT INTO `maimai2_game_event` (`id`, `end_date`, `start_date`, `type`, `enable`) VALUES
(24011111, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(24011121, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1'),
(24011141, '2029-01-01 00:00:00.000000', '2019-01-01 00:00:00.000000', 0, '1');

View File

@@ -1,3 +0,0 @@
UPDATE maimai2_user_playlog SET ext_bool1=false;
UPDATE maimai2_user_detail SET current_play_count=0;
UPDATE maimai2_user_detail SET rename_credit=0;

View File

@@ -1,8 +0,0 @@
-- maimai2_user_playlog
ALTER TABLE maimai2_user_playlog ADD COLUMN ext_bool1 BOOLEAN;
ALTER TABLE maimai2_user_playlog ADD COLUMN ext_num4 INTEGER;
UPDATE maimai2_user_playlog SET ext_num4=0;
-- maimai2_user_detail
ALTER TABLE maimai2_user_detail ADD COLUMN current_play_count INTEGER;
ALTER TABLE maimai2_user_detail ADD COLUMN rename_credit INTEGER;

View File

@@ -7,14 +7,10 @@ from pathlib import Path
import orjson import orjson
import xmltodict import xmltodict
from hypy_utils import write from hypy_utils import write
from hypy_utils.logging_utils import setup_logger
from hypy_utils.tqdm_utils import pmap from hypy_utils.tqdm_utils import pmap
from wand.image import Image
log = setup_logger()
def convert_path(file: Path): def convert_one(file: Path):
# Get path relative to source # Get path relative to source
rel = file.relative_to(src) rel = file.relative_to(src)
@@ -22,29 +18,8 @@ def convert_path(file: Path):
if len(rel.parts) <= 2: if len(rel.parts) <= 2:
return return
# Generate target file path
# Ignore the first segment of the relative path, and append to the destination
# Also collapse the single-item directory into the filename
# e.g. {src}/A000/music/music000001/Music.xml -> {dst}/music/000001.json
target = dst / '/'.join(rel.parts[1:-2])
file_id = ''.join(filter(str.isdigit, rel.parts[-2]))
file_id = file_id.zfill(6)
target = target / f'{file_id}.json'
return target
def convert_one(file: Path):
target = convert_path(file)
if target is None:
return
# Read xml # Read xml
try: xml = xmltodict.parse(file.read_text())
xml = xmltodict.parse(file.read_text())
except Exception as e:
log.info(f'Error parsing {file}: {e}')
return
# There should only be one root element, expand it # There should only be one root element, expand it
assert len(xml) == 1, f'Expected 1 root element, got {len(xml)}' assert len(xml) == 1, f'Expected 1 root element, got {len(xml)}'
@@ -56,103 +31,51 @@ def convert_one(file: Path):
if '@xmlns:xsd' in xml: if '@xmlns:xsd' in xml:
del xml['@xmlns:xsd'] del xml['@xmlns:xsd']
if target.exists(): # Generate target file path
log.info(f'Overwriting {target}') # Ignore the first segment of the relative path, and append to the destination
# Also collapse the single-item directory into the filename
# e.g. {src}/A000/music/music000001/Music.xml -> {dst}/music/000001.json
target = dst / '/'.join(rel.parts[1:-2])
file_id = ''.join(filter(str.isdigit, rel.parts[-2]))
target = target / f'{file_id}.json'
# Create directories if they don't exist
target.parent.mkdir(parents=True, exist_ok=True)
# Write json # Write json
write(target, orjson.dumps(xml)) write(target, orjson.dumps(xml))
def convert_dds(file: Path): def combine_music():
target = convert_path(file) # Read all music json files
if target is None: music_files = list(dst.rglob('music/*.json'))
return print(f'> Found {len(music_files)} music files')
jsons = [orjson.loads(f.read_text()) for f in music_files]
# Convert dds to jpg # Combine all music
try: combined = {d['name']['id']: {
with Image(filename=str(file)) as img:
img.format = 'jpeg'
img.save(filename=str(target.with_suffix('.png')))
except Exception as e:
log.info(f'Error converting {file}: {e}')
return
def get(d: dict, *keys: str):
"""
Get the first key that exists in the dictionary
:param d: Dictionary
:param keys: Recursive key in the format of keya.keyb.keyc...
"""
for k in keys:
ks = k.split('.')
cd = d
while len(ks) > 0:
cd = cd.get(ks.pop(0))
if cd is None:
break
if cd is not None:
return cd
return None
def convert_music_mai2(d: dict) -> (str, dict):
return d['name']['id'], {
'name': d['name']['str'], 'name': d['name']['str'],
'ver': d.get('version') or d.get('releaseTagName')['str'], 'ver': int(d['version']),
'composer': d['artistName']['str'], 'composer': d['artistName']['str'],
'genre': d['genreName']['str'] or d['genreNames'], 'genre': d['genreName']['str'],
'bpm': int(d['bpm']), 'bpm': int(d['bpm']),
'lock': f"{d['lockType']} {d['subLockType']}", 'lock': f"{d['lockType']} {d['subLockType']}",
'notes': [{ 'notes': [{
'lv': int(n['level']) + (int(n['levelDecimal']) / 10.0), 'lv': int(n['level']) + (int(n['levelDecimal']) / 10),
'designer': n['notesDesigner']['str'], 'designer': n['notesDesigner']['str'],
'lv_id': n['musicLevelID'], 'lv_id': n['musicLevelID'],
'notes': int(n['maxNotes']), 'notes': int(n['maxNotes']),
} for n in d['notesData']['Notes'] if n['isEnable'] != 'false'] } for n in d['notesData']['Notes'] if n['isEnable'] != 'false']
} } for d in jsons}
# Write combined music
def convert_music_chu3(d: dict) -> (str, dict): write(dst / '00/all-music.json', orjson.dumps(combined))
return d['name']['id'], {
'name': d['name']['str'],
'ver': d['releaseTagName']['str'],
'composer': d['artistName']['str'],
'genre': get(d, 'genreName.list.StringID.str'),
'lock': d['firstLock'],
'notes': [{
'lv': int(n['level']) + (int(n['levelDecimal']) / 100.0),
'designer': n.get('notesDesigner'),
'lv_id': n['type']['id'],
} for n in d['fumens']['MusicFumenData'] if n['enable'] != 'false']
}
def convert_music_ongeki(d: dict) -> (str, dict):
return d['Name']['id'], {
'name': d['Name']['str'],
'ver': d['VersionID']['id'],
'composer': d['ArtistName']['str'],
'genre': d['Genre']['str'],
'lock': f"{d['CostToUnlock']} {d['IsLockedAtTheBeginning']}",
'notes': [{
'lv': int(n['FumenConstIntegerPart']) + (int(n['FumenConstFractionalPart']) / 100.0),
'lv_id': i,
} for i, n in enumerate(d['FumenData']['FumenData']) if n['FumenFile']['path'] is not None],
'lunatic': d['IsLunatic']
}
if __name__ == '__main__': if __name__ == '__main__':
agupa = argparse.ArgumentParser() agupa = argparse.ArgumentParser()
# Source can be one of the following:
# - maimai/Package/Sinmai_Data/StreamingAssets
# - chusan/App/data
# - ongeki/package/mu3_Data/StreamingAssets/GameData
agupa.add_argument('source', type=str, help='Package/Sinmai_Data/StreamingAssets directory') agupa.add_argument('source', type=str, help='Package/Sinmai_Data/StreamingAssets directory')
agupa.add_argument('destination', type=str, help='Directory to extract to') agupa.add_argument('destination', type=str, help='Directory to extract to')
agupa.add_argument('-g', '--game', type=str, help='Game to convert', default='mai2', choices=['mai2', 'chu3', 'ongeki'])
args = agupa.parse_args() args = agupa.parse_args()
src = Path(args.source) src = Path(args.source)
@@ -169,7 +92,7 @@ if __name__ == '__main__':
if not d.is_dir(): if not d.is_dir():
continue continue
log.info(f'Relocating {d}') print(f'Relocating {d}')
for file in d.rglob('*.png'): for file in d.rglob('*.png'):
id = ''.join(filter(str.isdigit, file.stem)) id = ''.join(filter(str.isdigit, file.stem))
shutil.move(file, d / f'{id}.png') shutil.move(file, d / f'{id}.png')
@@ -182,36 +105,19 @@ if __name__ == '__main__':
# Assert that target directory does not exist # Assert that target directory does not exist
if dst.exists(): if dst.exists():
if input(f'{dst} already exists, delete? (y/n): ') == 'y': if input(f'{dst} already exists, delete? (y/n): ') == 'y':
log.info(f'Deleting {dst}') print(f'Deleting {dst}')
shutil.rmtree(dst) shutil.rmtree(dst)
# Find all xml files in the source directory # Find all xml files in the source directory
files = list(src.rglob('*.xml')) files = list(src.rglob('*.xml'))
log.info(f'Found {len(files)} xml files') print(f'Found {len(files)} xml files')
# Multithreaded map # Multithreaded map
pmap(convert_one, files, desc='Converting', unit='file', chunksize=50) pmap(convert_one, files, desc='Converting', unit='file', chunksize=50)
log.info('> Finished converting') print('> Finished converting')
# Find all .dds files in the source A000 directory
dds_files = list(src.rglob('*.dds'))
log.info(f'Found {len(dds_files)} dds files')
# Convert and copy dds files (CPU-intensive)
pmap(convert_dds, dds_files, desc='Converting DDS', unit='file', chunksize=50, max_workers=os.cpu_count() - 2)
log.info('> Finished converting DDS')
# Convert all music # Convert all music
log.info('Combining music') print('Combining music')
music_files = list(dst.rglob('music/*.json')) combine_music()
log.info(f'> Found {len(music_files)} music files')
jsons = [orjson.loads(f.read_text()) for f in music_files]
converter = {'mai2': convert_music_mai2, 'chu3': convert_music_chu3, 'ongeki': convert_music_ongeki}[args.game]
combined = {k: v for k, v in [converter(d) for d in jsons]}
# Write combined music
write(dst / '00/all-music.json', orjson.dumps(combined))