19 Commits

Author SHA1 Message Date
Azalea Gui
02dc142eea [+] Data convert script: Add ongeki 2024-02-27 18:09:06 -05:00
Azalea Gui
5973b3bfe5 [F] Fix level calculation 2024-02-26 20:06:32 -05:00
Azalea Gui
f4e3be8d15 [+] Add chusan support for data convert 2024-02-26 19:36:54 -05:00
Azalea
c7e493d7f5 [F] Fix null 2024-02-26 17:05:32 -05:00
Azalea
759519d374 [PR] #13 from Becods: Extra fields from bud
[+] Extra fields from bud
2024-02-26 10:40:14 -05:00
Becod
3d713b13da [+] Extra fields from bud 2024-02-26 21:42:00 +08:00
Azalea
20468e612d Merge branch 'master' of https://github.com/hykilpikonna/AquaDX 2024-02-23 00:11:05 -05:00
Azalea
c175173821 Merge pull request #12 from Zaphkito/master
Add maimai 140 h041 event data
2024-02-21 05:46:55 -05:00
zaphkito
52e9285551 Add maimai 140 h041 event data 2024-02-21 18:39:39 +08:00
Azalea
f4280c0768 Merge pull request #11 from Zaphkito/master
Maimai 140 h031 event data and charge data
2024-02-18 15:57:25 -05:00
zaphkito
295ae14658 Add maimai2 charge 2024-02-19 04:40:48 +08:00
zaphkito
ccc2bcffce Maimai 140 h031 event data 2024-02-19 04:12:53 +08:00
Azalea
a47ed71799 [F] Fix typos in readme 2024-02-16 20:49:44 -08:00
Azalea
006a49cfdb [F] Fix dependency CVE 2024-02-16 15:54:09 -05:00
Azalea
9794ee259a [U] Upgrade gradle wrapper 2024-02-16 15:52:05 -05:00
Azalea
643e0e0c1f [O] Lint 2024-02-16 01:46:11 -05:00
Azalea
6afcb364d1 [+] Add eslint 2024-02-16 01:43:32 -05:00
Azalea
6d4a38404c [O] Sort recent by date, display level 2024-02-16 01:04:29 -05:00
Azalea
b925c2ef20 [U] Update readme 2024-02-12 11:26:37 -05:00
35 changed files with 1580 additions and 529 deletions

41
AquaNet/.eslintrc.cjs Normal file
View File

@@ -0,0 +1,41 @@
// ..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,11 +12,11 @@ Please check out [SVELTE.md](SVELTE.md) for more details on the technical aspect
### Running locally
First, you would need to install Node.js and yarn.
Then, you would need to start your testing AquaDX server and configure the `src/libs/config.ts` to use your URL.
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:
```shell
yarn install
yarn dev
```

View File

@@ -7,19 +7,23 @@
"dev": "vite",
"build": "vite build",
"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"
},
"devDependencies": {
"@iconify/svelte": "^3.1.6",
"@sveltejs/vite-plugin-svelte": "^3.0.1",
"@tsconfig/svelte": "^5.0.2",
"chartjs-adapter-moment": "^1.0.1",
"eslint": "^8.56.0",
"eslint-plugin-svelte": "^2.35.1",
"sass": "^1.70.0",
"svelte": "^4.2.10",
"svelte-check": "^3.6.4",
"svelte-routing": "^2.12.0",
"tslib": "^2.6.2",
"typescript": "^5.2.2",
"typescript-eslint": "^7.0.1",
"vite": "^5.1.1"
},
"dependencies": {

View File

@@ -1,5 +1,5 @@
export interface TrendEntry {
date: string
rating: number
plays: number
export interface TrendEntry {
date: string
rating: number
plays: number
}

View File

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

View File

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

View File

@@ -1,101 +1,100 @@
import {
Chart as ChartJS,
Title,
Tooltip,
Legend,
LineElement,
LinearScale,
PointElement,
CategoryScale, TimeScale, type ChartOptions, type LineOptions,
} from 'chart.js';
import moment from "moment/moment";
// @ts-ignore
import CalHeatmap from "cal-heatmap";
// @ts-ignore
import CalTooltip from 'cal-heatmap/plugins/Tooltip';
import type {Line} from "svelte-chartjs";
export function title(t: string) {
document.title = `AquaNet - ${t}`
}
export function registerChart() {
ChartJS.register(
Title,
Tooltip,
Legend,
LineElement,
LinearScale,
PointElement,
CategoryScale,
TimeScale
);
}
export function renderCal(el: HTMLElement, d: {date: any, value: any}[]) {
const cal = new CalHeatmap();
return cal.paint({
itemSelector: el,
domain: {
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)]
},
},
date: {start: moment().subtract(1, 'year').add(1, 'month').toDate()},
theme: "dark",
}, [
[CalTooltip, {text: (_: Date, v: number, d: any) =>
`${v ?? "No"} songs played on ${d.format('MMMM D, YYYY')}`}]
]);
}
export const CHARTJS_OPT: ChartOptions<"line"> = {
responsive: true,
maintainAspectRatio: false,
// TODO: Show point on hover
elements: {
point: {
radius: 0
}
},
scales: {
xAxis: {
type: 'time',
display: false
},
y: {
display: false,
}
},
plugins: {
legend: {
display: false
},
tooltip: {
mode: "index",
intersect: false
}
},
}
/**
* Usage: clazz({a: false, b: true}) -> "b"
*
* @param obj HashMap<string, boolean>
*/
export function clazz(obj: { [key: string]: boolean }) {
return Object.keys(obj).filter(k => obj[k]).join(" ")
}
import {
Chart as ChartJS,
Title,
Tooltip,
Legend,
LineElement,
LinearScale,
PointElement,
CategoryScale, TimeScale, type ChartOptions, type LineOptions,
} from 'chart.js'
import moment from 'moment/moment'
// @ts-expect-error Cal-heatmap does not have proper types
import CalHeatmap from 'cal-heatmap'
// @ts-expect-error Cal-heatmap does not have proper types
import CalTooltip from 'cal-heatmap/plugins/Tooltip'
export function title(t: string) {
document.title = `AquaNet - ${t}`
}
export function registerChart() {
ChartJS.register(
Title,
Tooltip,
Legend,
LineElement,
LinearScale,
PointElement,
CategoryScale,
TimeScale
)
}
export function renderCal(el: HTMLElement, d: {date: any, value: any}[]) {
const cal = new CalHeatmap()
return cal.paint({
itemSelector: el,
domain: {
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) ]
},
},
date: { start: moment().subtract(1, 'year').add(1, 'month').toDate() },
theme: 'dark',
}, [
[ CalTooltip, { text: (_: Date, v: number, d: any) =>
`${v ?? 'No'} songs played on ${d.format('MMMM D, YYYY')}` }]
])
}
export const CHARTJS_OPT: ChartOptions<'line'> = {
responsive: true,
maintainAspectRatio: false,
// TODO: Show point on hover
elements: {
point: {
radius: 0
}
},
scales: {
xAxis: {
type: 'time',
display: false
},
y: {
display: false,
}
},
plugins: {
legend: {
display: false
},
tooltip: {
mode: 'index',
intersect: false
}
},
}
/**
* Usage: clazz({a: false, b: true}) -> "b"
*
* @param obj HashMap<string, boolean>
*/
export function clazz(obj: { [key: string]: boolean }) {
return Object.keys(obj).filter(k => obj[k]).join(' ')
}

View File

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

View File

@@ -39,7 +39,7 @@
letter-spacing: 0.2em
margin-top: 0
opacity: 0.9
.btn-group
display: flex
gap: 8px

View File

@@ -10,7 +10,7 @@
Promise.all([
getMaimai("GetUserRatingApi", {userId}),
getMaimaiAllMusic().then(it => it.json())
getMaimaiAllMusic()
]).then(([rating, music]) => {
data = rating
musicInfo = music
@@ -37,7 +37,8 @@
music.note = music.notes[x.level]
const mult = getMult(x.achievement)
return {...x,
return {
...x,
music: music,
calc: (mult[1] as number) * music.note.lv,
rank: mult[2]
@@ -69,7 +70,9 @@
<div class="rating-cards">
{#each section.data as rating}
<div class="level-{rating.level}">
<img class="cover" src={`${data_host}/maimai/assetbundle/jacket_s/00${rating.musicId.toString().padStart(6, '0').substring(2)}.png`} alt="">
<img class="cover"
src={`${data_host}/maimai/assetbundle/jacket_s/00${rating.musicId.toString().padStart(6, '0').substring(2)}.png`}
alt="">
<div class="detail">
<span class="name">{rating.music.name}</span>
@@ -79,7 +82,9 @@
</span>
<span>{rating.calc.toFixed(1)}</span>
</div>
<img class="ver" src={`${data_host}/maimai/sprites/tab/title/UI_CMN_TabTitle_MaimaiTitle_Ver${rating.music.ver.toString().substring(0, 3)}.png`} alt="">
<img class="ver"
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>
{/each}
@@ -89,111 +94,106 @@
</main>
<style lang="sass">
.rating-cards
display: grid
gap: 2rem
width: 100%
.rating-cards
display: grid
gap: 2rem
width: 100%
// Fill as many columns as possible
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr))
// Fill as many columns as possible
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr))
// Center the cards
justify-items: center
align-items: center
// Center the cards
justify-items: center
align-items: center
// Style each card
> div
$border-radius: 20px
width: 200px
height: 200px
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)
// Style each card
> div
$border-radius: 20px
width: 200px
height: 200px
border-radius: $border-radius
display: flex
flex-direction: column
text-align: left
position: relative
> span
// Disable text wrapping, max 2 lines
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
// Difficulty border
border: 5px solid var(--lv-color)
.name
font-size: 1.2em
font-weight: bold
img
object-fit: cover
pointer-events: none
.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.cover
width: 100%
height: 100%
border-radius: calc($border-radius - 3px)
img.ver
height: 45px
left: -20px
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
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>

View File

@@ -34,6 +34,9 @@
console.log(trend)
console.log(music)
// Sort recent by date
user.recent.sort((a, b) => b.userPlayDate < a.userPlayDate ? -1 : 1)
d = {user, trend, recent: user.recent.map(it => {return {...music[it.musicId], ...it}})}
localStorage.setItem("tmp-user-details", JSON.stringify(d))
renderCal(calElement, trend.map(it => {return {date: it.date, value: it.plays}}))
@@ -161,8 +164,11 @@
<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="">
<div class="info">
<span class="name">{r.name}</span>
<div>
<span class="name">{r.name}</span>
</div>
<div>
<span class={`lv level-${r.level}`}>{r.notes[r.level].lv}</span>
<span class={"rank-" + ("" + getMult(r.achievement)[2])[0]}>
<span class="rank-text">{("" + getMult(r.achievement)[2]).replace("p", "+")}</span>
<span class="rank-num">{(r.achievement / 10000).toFixed(2)}%</span>
@@ -351,7 +357,7 @@ $gap: 20px
flex-direction: column
gap: 0
span
.rank-text
text-align: left
.rank-S
@@ -366,6 +372,13 @@ $gap: 20px
.rank-B
color: #6ba6ff
.lv
background: var(--lv-color)
padding: 0 6px
border-radius: 10px
opacity: 0.8
margin-right: 10px
span
display: inline-block
text-align: right

View File

@@ -6,4 +6,13 @@ $c-bg: #242424
$nav-height: 4rem
$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

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
# 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)
@@ -26,9 +26,9 @@ Check out these docs for more information.
* [Frequently asked questions](docs/frequently_asked_questions.md)
### Notes
* 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 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 edit settings for some games.
* 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.
* 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 server also provides a simple API for viewing play records and editing settings for some games.
### Usage
@@ -37,28 +37,28 @@ Check out these docs for more information.
3. Extract the zip file to a 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, edit configuration file then it will auto create the table and import some initial data.
If you want to use optional databases, please edit the configuration file then it will auto-create the table and import some initial data.
### Configuration
Configuration is saved in `config/application.properties`, spring loads this file automatically.
* 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 following request.
* You can switch to MariaDB (or MySQL) database by commenting the Sqlite part.
* For some game, you might need to change some game specific config entries.
* 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.
This will be sent to the game at booting and being used by the following request.
* You can switch to the MariaDB (or MySQL) database by commenting the Sqlite part.
* For some games, you might need to change some game-specific config entries.
### Building
You need to install JDK on your system. However, you don't need to care about Gradle, as wrapper script is included.
You need to install JDK on your system. However, you don't need to install Gradle separately, as the `gradlew` wrapper script is included.
```
gradlew clean build
```
The `build/libs` folder will contain an jar file.
The `build/libs` folder will contain a jar file.
### Credit
* **samnyan**: The creator and developer of the original Aqua server
* **Akasaka Ryuunosuke** : providing all the DIVA protocol information
* Dom Eori : Developer of forked Aqua server, from v0.0.17 and up
* **Akasaka Ryuunosuke**: providing all the DIVA protocol information
* 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 contributors by merge request, issues and other channels
* All contributors by merge requests, issues and other channels

View File

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

View File

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

184
gradlew.bat vendored
View File

@@ -1,92 +1,92 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
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

@@ -0,0 +1,5 @@
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

@@ -0,0 +1,4 @@
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

@@ -0,0 +1,17 @@
-- 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

@@ -0,0 +1,8 @@
-- 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

@@ -0,0 +1,14 @@
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

@@ -0,0 +1,5 @@
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

@@ -0,0 +1,4 @@
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

@@ -0,0 +1,17 @@
-- 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

@@ -0,0 +1,8 @@
-- 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

@@ -0,0 +1,14 @@
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

@@ -0,0 +1,5 @@
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

@@ -0,0 +1,4 @@
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

@@ -0,0 +1,3 @@
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

@@ -0,0 +1,8 @@
-- 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,10 +7,14 @@ from pathlib import Path
import orjson
import xmltodict
from hypy_utils import write
from hypy_utils.logging_utils import setup_logger
from hypy_utils.tqdm_utils import pmap
from wand.image import Image
log = setup_logger()
def convert_one(file: Path):
def convert_path(file: Path):
# Get path relative to source
rel = file.relative_to(src)
@@ -18,8 +22,29 @@ def convert_one(file: Path):
if len(rel.parts) <= 2:
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
xml = xmltodict.parse(file.read_text())
try:
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
assert len(xml) == 1, f'Expected 1 root element, got {len(xml)}'
@@ -31,51 +56,103 @@ def convert_one(file: Path):
if '@xmlns:xsd' in xml:
del xml['@xmlns:xsd']
# 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]))
target = target / f'{file_id}.json'
# Create directories if they don't exist
target.parent.mkdir(parents=True, exist_ok=True)
if target.exists():
log.info(f'Overwriting {target}')
# Write json
write(target, orjson.dumps(xml))
def combine_music():
# Read all music json files
music_files = list(dst.rglob('music/*.json'))
print(f'> Found {len(music_files)} music files')
jsons = [orjson.loads(f.read_text()) for f in music_files]
def convert_dds(file: Path):
target = convert_path(file)
if target is None:
return
# Combine all music
combined = {d['name']['id']: {
# Convert dds to jpg
try:
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'],
'ver': int(d['version']),
'ver': d.get('version') or d.get('releaseTagName')['str'],
'composer': d['artistName']['str'],
'genre': d['genreName']['str'],
'genre': d['genreName']['str'] or d['genreNames'],
'bpm': int(d['bpm']),
'lock': f"{d['lockType']} {d['subLockType']}",
'notes': [{
'lv': int(n['level']) + (int(n['levelDecimal']) / 10),
'lv': int(n['level']) + (int(n['levelDecimal']) / 10.0),
'designer': n['notesDesigner']['str'],
'lv_id': n['musicLevelID'],
'notes': int(n['maxNotes']),
} for n in d['notesData']['Notes'] if n['isEnable'] != 'false']
} for d in jsons}
}
# Write combined music
write(dst / '00/all-music.json', orjson.dumps(combined))
def convert_music_chu3(d: dict) -> (str, dict):
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__':
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('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()
src = Path(args.source)
@@ -92,7 +169,7 @@ if __name__ == '__main__':
if not d.is_dir():
continue
print(f'Relocating {d}')
log.info(f'Relocating {d}')
for file in d.rglob('*.png'):
id = ''.join(filter(str.isdigit, file.stem))
shutil.move(file, d / f'{id}.png')
@@ -105,19 +182,36 @@ if __name__ == '__main__':
# Assert that target directory does not exist
if dst.exists():
if input(f'{dst} already exists, delete? (y/n): ') == 'y':
print(f'Deleting {dst}')
log.info(f'Deleting {dst}')
shutil.rmtree(dst)
# Find all xml files in the source directory
files = list(src.rglob('*.xml'))
print(f'Found {len(files)} xml files')
log.info(f'Found {len(files)} xml files')
# Multithreaded map
pmap(convert_one, files, desc='Converting', unit='file', chunksize=50)
print('> Finished converting')
log.info('> 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
print('Combining music')
combine_music()
log.info('Combining music')
music_files = list(dst.rglob('music/*.json'))
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))