40 Commits

Author SHA1 Message Date
Pinapelz
6776353556 feat: batch manual export for mai2 (#199) 2025-12-30 14:20:53 -05:00
Raymond
3b19257ab1 feat: option APIs for chu3, mai2 & ongeki (#194) 2025-12-21 18:19:09 -05:00
Raymond
a0cd7456ee fix: clean up settings page to match settings layout 2025-12-21 12:23:27 -05:00
Azalea
aeafa6a396 [M] Rename to avoid migration conflict 2025-12-14 05:27:17 +09:00
Paiton Bertschy
448426a96d chore: remove a300 events (dummy events) 2025-12-14 05:27:17 +09:00
thewiilover
dfa6176689 chore: ad chusan xverse a071 to a300 event ids 2025-12-14 05:27:17 +09:00
Menci
d996fba291 [+] Fedy for AquaGameOptions 2025-12-13 10:21:42 +09:00
Menci
0cb2a95ff3 [F] Fix inheritance in KClass<T>.vars() 2025-12-13 10:21:42 +09:00
Menci
be5220fd51 [O] Use Iterable<T>.mapApply() 2025-12-13 10:21:42 +09:00
Menci
f23c0d6fe1 [RF] Re-organize game options 2025-12-13 10:21:42 +09:00
Menci
5aca650602 [F] CardTimestamp relationship definition 2025-12-10 14:03:51 +09:00
Menci
7c72348016 [+] Card Timestamp 2025-12-10 14:03:51 +09:00
radiumerst
5eee6505f9 Fix: KALEIDXSCOPE crashing on final phase
Add gates 7-10 to prevent crashing when entered

Set Gates 1-9 to LIFE999 BASIC
Set Gate 10 to LIFE999/999 BASIC
2025-12-10 14:03:27 +09:00
Azalea
43b7ea65a5 [-] Replace login blocking with an @ AquaDX prefix 2025-11-23 23:43:23 +08:00
Paiton Bertschy
85149dcd03 Update src/main/resources/db/80/V1000_61__ongeki_refresh_a017_to_a032.sql
Co-authored-by: 凌莞~(=^▽^=) <opensource@c5y.moe>
2025-11-19 13:33:23 +08:00
Paiton Bertschy
a767c8949c Update src/main/resources/db/80/V1000_61__ongeki_refresh_a017_to_a032.sql
Co-authored-by: 凌莞~(=^▽^=) <opensource@c5y.moe>
2025-11-19 13:33:23 +08:00
Paiton Bertschy
bbeb476a62 Update src/main/resources/db/80/V1000_61__ongeki_refresh_a017_to_a032.sql
Co-authored-by: 凌莞~(=^▽^=) <opensource@c5y.moe>
2025-11-19 13:33:23 +08:00
Paiton Bertschy
c444350cef Remove duplicate entries from SQL data
Removed duplicate entries for '7mai' and 'HiTECH NINJA' in the SQL file.
2025-11-19 13:33:23 +08:00
thewiilover
13a318d519 chroe: adds all the new content ids for ongeki 2025-11-19 13:33:23 +08:00
thewiilover
e744d96c96 chore: add ongeki ReFresh event ids from a017-a032 to new migration 2025-11-19 13:33:23 +08:00
Clansty
491044d37a [-] remove munet migration notice 2025-11-14 22:11:21 +08:00
Azalea
9d30cf1e7d [+] Pagination 2025-11-14 18:52:46 +08:00
凌莞~(=^▽^=)
d2608472d8 fix: total_point null (#185) 2025-10-25 03:46:05 +08:00
Menci
34aae0c87a [F] Player name validation (#186) 2025-10-21 03:46:43 +09:00
Clansty
69bd35a579 [O] Change tip 2025-10-11 04:05:56 +08:00
Menci
3e6c0b4159 feat: user management APIs (#184) 2025-10-07 13:21:01 -07:00
Menci
a33ec8b11c feat: crop pfp to at most 1024px (#183) 2025-10-07 13:20:49 -07:00
Azalea
dd03ca38a1 [F] Fix memory leak 2025-10-07 03:40:49 +08:00
Menci
1cac5e451a refactor: user registrar (#182) 2025-10-06 12:32:52 -07:00
Azalea
010d4592e4 hot fix 2025-10-07 00:30:48 +08:00
Menci
b0d0f8ef7d feat: some data management APIs (#176) 2025-10-06 09:27:39 -07:00
Raymond
967d311ee4 chusan x-v a001 (#180)
Co-authored-by: asterisk727 <59166650+asterisk727@users.noreply.github.com>
2025-10-04 10:59:02 -07:00
crxmsxn
d5b777d720 fix: mobile styling for favorites (#181) 2025-10-04 10:57:31 -07:00
Raymond
2ab2666ad0 Feat: Favorites (for all supported games) (#174) 2025-09-29 22:56:26 -07:00
alexay7
4971f2be78 [+] Add support for geki cm and fix chusan cm implementation (#175) 2025-09-29 22:47:31 -07:00
Menci
b0a49d6626 [+] Add APIs (#177) 2025-09-29 21:54:58 -07:00
凌莞~(=^▽^=)
d830854eaa fix: X-Verse Username (#179) 2025-09-29 22:35:56 -04:00
凌莞~(=^▽^=)
68820d5a86 chore: Bump versions 2025-09-13 21:49:46 +08:00
Raymond
8b079bc40b fix: nvm looks like shit 2025-09-12 11:34:25 -04:00
Raymond
b0dd9b845f chore: bump versions 2025-09-12 11:33:23 -04:00
70 changed files with 3100 additions and 520 deletions

View File

@@ -1,32 +0,0 @@
<script lang="ts">
export let username: string;
export let email: string;
let shouldShow = navigator.language.startsWith('zh');
// 会导致瞬间出现,但是不知道为什么 svelte 的 transition 动画不工作
// if (!shouldShow) {
// fetch('https://47.122.72.135/ip/isChina')
// .then(it => it.json())
// .then(it => shouldShow = it)
// .catch(() => shouldShow = false);
// }
const jump = () => {
const params = new URLSearchParams();
if (username) params.set('username', username);
if (email) params.set('email', email);
location.href = `https://portal.mumur.net/register?${params.toString()}`;
}
</script>
{#if shouldShow}
<div class="cursor-pointer" on:click={jump}>
<h2>MuNET 了解一下!</h2>
<div>
<p>MuNET 是 AquaDX 的继任者,提供更适合中国用户的服务器和更好的游戏体验。</p>
<p>如果你还没有游戏数据,建议在 MuNET 上创建账号并开始游戏。点击立即前往</p>
</div>
</div>
{/if}

View File

@@ -0,0 +1,60 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
export let page: number
export let totalPages: number
const dispatch = createEventDispatcher()
let editing = false
let inputPage: number
function updatePage(newPage: number) {
if (newPage > 0 && newPage <= totalPages) dispatch('updatePage', newPage)
}
function startEditing() {
inputPage = page
editing = true
}
function finishEditing() {
editing = false
if (inputPage !== page) updatePage(inputPage)
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Enter') finishEditing()
else if (event.key === 'Escape') editing = false
}
</script>
<div class="pagination">
<button on:click={() => updatePage(page - 1)} disabled={page <= 1}>Previous</button>
{#if editing}
<input bind:value={inputPage} on:blur={finishEditing} on:keydown={handleKeydown} min="1" max={totalPages} autofocus/>
{:else}
<span on:click={startEditing} role="button" tabindex="0" on:keydown={(e) => e.key === 'Enter' && startEditing()}>
Page {page} of {totalPages}
</span>
{/if}
<button on:click={() => updatePage(page + 1)} disabled={page >= totalPages}>Next</button>
</div>
<style lang="sass">
.pagination
display: flex
justify-content: center
align-items: center
margin: 1rem 0
gap: 1rem
input
width: 100px
text-align: center
span[role="button"]
cursor: pointer
</style>

View File

@@ -1,7 +1,6 @@
<script> <script>
import { fade } from "svelte/transition"; import { fade } from "svelte/transition";
import { FADE_IN, FADE_OUT } from "../../libs/config"; import { FADE_IN, FADE_OUT } from "../../libs/config";
import GameSettingFields from "./GameSettingFields.svelte";
import { t, ts } from "../../libs/i18n"; import { t, ts } from "../../libs/i18n";
import useLocalStorage from "../../libs/hooks/useLocalStorage.svelte"; import useLocalStorage from "../../libs/hooks/useLocalStorage.svelte";
import RegionSelector from "./RegionSelector.svelte"; import RegionSelector from "./RegionSelector.svelte";
@@ -11,9 +10,8 @@
<div out:fade={FADE_OUT} in:fade={FADE_IN} class="fields"> <div out:fade={FADE_OUT} in:fade={FADE_IN} class="fields">
<blockquote> <blockquote>
{ts("settings.gameNotice")} {ts("settings.siteNotice")}
</blockquote> </blockquote>
<GameSettingFields game="general"/>
<div class="field"> <div class="field">
<div class="bool"> <div class="bool">
<input id="rounding" type="checkbox" bind:checked={rounding.value}/> <input id="rounding" type="checkbox" bind:checked={rounding.value}/>

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { slide, fade } from "svelte/transition"; import { slide, fade } from "svelte/transition";
import { FADE_IN, FADE_OUT } from "../../libs/config"; import { FADE_IN, FADE_OUT, DATA_HOST } from "../../libs/config";
import { t } from "../../libs/i18n.js"; import { t } from "../../libs/i18n.js";
import Icon from "@iconify/svelte"; import Icon from "@iconify/svelte";
import StatusOverlays from "../StatusOverlays.svelte"; import StatusOverlays from "../StatusOverlays.svelte";
@@ -35,6 +35,169 @@
break break
} }
} }
async function exportBatchManual() {
submitting = "batchExport"
const DIFFICULTY_MAP: Record<number, string> = {
0: "Basic",
1: "Advanced",
2: "Expert",
3: "Master",
4: "Re:Master"
}
const DAN_MAP: Record<number, string> = {
1: "DAN_1",
2: "DAN_2",
3: "DAN_3",
4: "DAN_4",
5: "DAN_5",
6: "DAN_6",
7: "DAN_7",
8: "DAN_8",
9: "DAN_9",
10: "DAN_10",
11: "SHINDAN_1",
12: "SHINDAN_2",
13: "SHINDAN_3",
14: "SHINDAN_4",
15: "SHINDAN_5",
16: "SHINDAN_6",
17: "SHINDAN_7",
18: "SHINDAN_8",
19: "SHINDAN_9",
20: "SHINDAN_10",
21: "SHINKAIDEN",
22: "URAKAIDEN"
}
const CLASS_MAP: Record<number, string> = {
0: "B5",
1: "B4",
2: "B3",
3: "B2",
4: "B1",
5: "A5",
6: "A4",
7: "A3",
8: "A2",
9: "A1",
10: "S5",
11: "S4",
12: "S3",
13: "S2",
14: "S1",
15: "SS5",
16: "SS4",
17: "SS3",
18: "SS2",
19: "SS1",
20: "SSS5",
21: "SSS4",
22: "SSS3",
23: "SSS2",
24: "SSS1",
25: "LEGEND"
}
let data: any
let musicData: any
let output: any = {
"meta": {
"game": "maimaidx",
"playtype": "Single",
"service": "AquaDX-Manual"
},
"scores": [],
"classes": {}
}
try {
musicData = await fetch(`${DATA_HOST}/d/mai2/00/all-music.json`).then(res => res.json())
} catch (e) {
error = e.message;
submitting = ""
return;
}
try {
data = await GAME.export('mai2');
} catch (e) {
error = e.message;
submitting = ""
return;
}
if (data && "userPlaylogList" in data) {
for (let score of data.userPlaylogList) {
if(score.musicId > 100000){
continue; // UTAGE charts are not supported
}
const musicItem = musicData[score.musicId as string];
if (!musicItem) continue;
let difficulty = null;
if (!(score.level in DIFFICULTY_MAP))
continue;
const isDX = score.musicId >= 10000;
difficulty = isDX ? `DX ${DIFFICULTY_MAP[score.level]}` : DIFFICULTY_MAP[score.level];
const percent = score.achievement/10000;
const pcrit = score.tapCriticalPerfect + score.holdCriticalPerfect + score.slideCriticalPerfect + score.touchCriticalPerfect + score.breakCriticalPerfect;
const perfect = score.tapPerfect + score.holdPerfect + score.slidePerfect + score.touchPerfect + score.breakPerfect;
const great = score.tapGreat + score.holdGreat + score.slideGreat + score.touchGreat + score.breakGreat;
const good = score.tapGood + score.holdGood + score.slideGood + score.touchGood + score.breakGood;
const miss = score.tapMiss + score.holdMiss + score.slideMiss + score.touchMiss + score.breakMiss;
const judgements = {
"pcrit": pcrit,
"perfect": perfect,
"great": great,
"good": good,
"miss": miss
}
let lamp = null;
if (score.isAllPerfect) {
lamp = "ALL PERFECT";
if (score.percent == 101.0) {
lamp = "ALL PERFECT+";
}
} else if (score.isFullCombo) {
lamp = "FULL COMBO";
if (good == 0 && great == 0) {
lamp = "FULL COMBO+";
}
} else if (score.isClear) {
lamp = "CLEAR";
} else {
lamp = "FAILED";
}
const optional = {
"fast": score.fastCount,
"slow": score.lateCount,
"maxCombo": score.maxCombo
}
output.scores.push({
"percent": percent,
"lamp": lamp,
"matchType": "inGameID",
"identifier": score.musicId.toString(),
"difficulty": difficulty,
"timeAchieved": new Date(score.userPlayDate).getTime(),
"judgements": judgements,
"optional": optional
})
}
}
if(data.userData.courseRank in DAN_MAP){
output.classes["dan"] = DAN_MAP[data.userData.courseRank]
}
if(data.userData.classRank in CLASS_MAP){
output.classes["matchingClass"] = CLASS_MAP[data.userData.classRank]
}
download(JSON.stringify(output), `AquaDX_maimai2_BatchManualExport_${values[0]}.json`)
submitting = ""
}
function exportData() { function exportData() {
submitting = "export" submitting = "export"
@@ -70,6 +233,10 @@
<Icon icon="bxs:file-export"/> <Icon icon="bxs:file-export"/>
{t('settings.export')} {t('settings.export')}
</button> </button>
<button class="exportBatchManualButton" on:click={exportBatchManual}>
<Icon icon="bxs:file-export"/>
{t('settings.batchManualExport')}
</button>
</div> </div>
<StatusOverlays {error} loading={!values[0] || !!submitting}/> <StatusOverlays {error} loading={!values[0] || !!submitting}/>

View File

@@ -107,7 +107,8 @@ export interface GenericGameSummary {
lastVersion: string lastVersion: string
ratingComposition: { [key: string]: any } ratingComposition: { [key: string]: any }
recent: GenericGamePlaylog[] recent: GenericGamePlaylog[]
rival?: boolean rival?: boolean,
favorites?: number[]
} }
export interface MusicMeta { export interface MusicMeta {

View File

@@ -28,6 +28,7 @@ export const EN_REF_USER = {
'UserHome.RemoveRival': "Remove from Rival", 'UserHome.RemoveRival': "Remove from Rival",
'UserHome.InvalidGame': "Game ${game} is not supported on the web UI yet. We only support maimai, chunithm, wacca, and ongeki for now.", 'UserHome.InvalidGame': "Game ${game} is not supported on the web UI yet. We only support maimai, chunithm, wacca, and ongeki for now.",
'UserHome.ShowMoreRecent': 'Show more', 'UserHome.ShowMoreRecent': 'Show more',
'UserHome.FavoriteSongs': 'Favorite Songs'
} }
export const EN_REF_Welcome = { export const EN_REF_Welcome = {
@@ -143,23 +144,35 @@ export const EN_REF_HOME = {
export const EN_REF_SETTINGS = { export const EN_REF_SETTINGS = {
'settings.title': 'Settings', 'settings.title': 'Settings',
'settings.tabs.profile': 'Profile', 'settings.tabs.profile': 'Profile',
'settings.tabs.game': 'Game', 'settings.tabs.global': 'Global',
'settings.tabs.chu3': 'Chuni', 'settings.tabs.chu3': 'Chuni',
'settings.tabs.mai2': 'Mai', 'settings.tabs.mai2': 'Mai',
'settings.tabs.ongeki': 'Ongeki', 'settings.tabs.ongeki': 'Ongeki',
'settings.tabs.wacca': 'Wacca', 'settings.tabs.wacca': 'Wacca',
'settings.fields.unlockMusic.name': 'Unlock All Music', 'settings.fields.mai2UnlockMusic.name': 'Unlock All Music',
'settings.fields.unlockMusic.desc': 'Unlock all music and master difficulty in game.', 'settings.fields.mai2UnlockMusic.desc': 'Unlock all music and master difficulty.',
'settings.fields.unlockChara.name': 'Unlock All Characters', 'settings.fields.mai2UnlockChara.name': 'Unlock All Characters',
'settings.fields.unlockChara.desc': 'Unlock all characters, voices, and partners in game.', 'settings.fields.mai2UnlockChara.desc': 'Unlock all characters (new characters start at level 1).',
'settings.fields.unlockCollectables.name': 'Unlock All Collectables', 'settings.fields.mai2UnlockCharaMaxLevel.name': 'Max Character Level',
'settings.fields.unlockCollectables.desc': 'Unlock all collectables (nameplate, title, icon, frame) in game.', 'settings.fields.mai2UnlockCharaMaxLevel.desc': 'Set all characters to max level.',
'settings.fields.unlockTickets.name': 'Unlock All Tickets', 'settings.fields.mai2UnlockPartners.name': 'Unlock All Partners',
'settings.fields.unlockTickets.desc': 'Infinite map/ex tickets (Note: maimai still limits which tickets can be used).', 'settings.fields.mai2UnlockPartners.desc': 'Unlock all partners.',
'settings.fields.waccaInfiniteWp.name': 'Wacca: Infinite WP', 'settings.fields.mai2UnlockCollectables.name': 'Unlock All Collectables',
'settings.fields.waccaInfiniteWp.desc': 'Set WP to 999999', 'settings.fields.mai2UnlockCollectables.desc': 'Unlock all collectables (nameplate, title, icon, frame).',
'settings.fields.waccaAlwaysVip.name': 'Wacca: Always VIP', 'settings.fields.mai2UnlockTickets.name': 'Unlock All Tickets',
'settings.fields.waccaAlwaysVip.desc': 'Set VIP expiration date to 2077-01-01', 'settings.fields.mai2UnlockTickets.desc': 'Infinite tickets (Note: client still limits which tickets can be used).',
'settings.fields.waccaUnlockMusic.name': 'Unlock All Music',
'settings.fields.waccaUnlockMusic.desc': 'Unlock all music.',
'settings.fields.waccaUnlockPlates.name': 'Unlock All Plates',
'settings.fields.waccaUnlockPlates.desc': 'Unlock all plates.',
'settings.fields.waccaUnlockCollectables.name': 'Unlock All Collectables',
'settings.fields.waccaUnlockCollectables.desc': 'Unlock all collectables (icon, trophy).',
'settings.fields.waccaUnlockTickets.name': 'Infinite Tickets',
'settings.fields.waccaUnlockTickets.desc': 'Infinite tickets.',
'settings.fields.waccaInfiniteWp.name': 'Infinite WP',
'settings.fields.waccaInfiniteWp.desc': 'Set WP to 999999.',
'settings.fields.waccaAlwaysVip.name': 'Always VIP',
'settings.fields.waccaAlwaysVip.desc': 'Set VIP expiration date to 2077-01-01.',
'settings.fields.chusanTeamName.name': 'Team Name', 'settings.fields.chusanTeamName.name': 'Team Name',
'settings.fields.chusanTeamName.desc': 'Customize the text displayed on the top of your profile.', 'settings.fields.chusanTeamName.desc': 'Customize the text displayed on the top of your profile.',
'settings.fields.chusanInfinitePenguins.name': 'Infinite Penguins', 'settings.fields.chusanInfinitePenguins.name': 'Infinite Penguins',
@@ -195,10 +208,10 @@ export const EN_REF_SETTINGS = {
'settings.export': 'Export Player Data', 'settings.export': 'Export Player Data',
'settings.batchManualExport': "Export in Batch Manual (for Tachi)", 'settings.batchManualExport': "Export in Batch Manual (for Tachi)",
'settings.cabNotice': "Note: These settings will only affect your own cab/setup. If you're playing on someone else's setup, please contact them to change these settings.", 'settings.cabNotice': "Note: These settings will only affect your own cab/setup. If you're playing on someone else's setup, please contact them to change these settings.",
'settings.gameNotice': "These only apply to Mai and Wacca.", 'settings.siteNotice': "These settings only apply to the website.",
'settings.regionNotice': "These only apply to Mai, Ongeki and Chuni.", 'settings.regionNotice': "These settings are shared amongst Mai, Ongeki and Chuni.",
'settings.regionSelector.title': "Prefecture Selector", 'settings.regionSelector.title': "Prefecture Selector",
'settings.regionSelector.desc': "Select the region where you want the game to think you are playing", 'settings.regionSelector.desc': "Select the region where you want the game to identify you",
'settings.regionSelector.select': "Select Prefecture", 'settings.regionSelector.select': "Select Prefecture",
} }

View File

@@ -40,6 +40,7 @@ const zhUser: typeof EN_REF_USER = {
'UserHome.RemoveRival': "移除劲敌", 'UserHome.RemoveRival': "移除劲敌",
'UserHome.InvalidGame': "游戏 ${game} 还不支持网页端查看。我们目前只支持舞萌、中二、华卡和音击。", 'UserHome.InvalidGame': "游戏 ${game} 还不支持网页端查看。我们目前只支持舞萌、中二、华卡和音击。",
'UserHome.ShowMoreRecent': "显示更多", 'UserHome.ShowMoreRecent': "显示更多",
'UserHome.FavoriteSongs': "收藏歌曲"
} }
const zhWelcome: typeof EN_REF_Welcome = { const zhWelcome: typeof EN_REF_Welcome = {
@@ -155,23 +156,35 @@ const zhHome: typeof EN_REF_HOME = {
const zhSettings: typeof EN_REF_SETTINGS = { const zhSettings: typeof EN_REF_SETTINGS = {
'settings.title': '用户设置', 'settings.title': '用户设置',
'settings.tabs.profile': '个人资料', 'settings.tabs.profile': '个人资料',
'settings.tabs.game': '游戏设置', 'settings.tabs.global': '全局',
'settings.tabs.chu3': '中二', 'settings.tabs.chu3': '中二',
'settings.tabs.mai2': '舞萌', 'settings.tabs.mai2': '舞萌',
'settings.tabs.ongeki': '音击', 'settings.tabs.ongeki': '音击',
'settings.tabs.wacca': '华卡', 'settings.tabs.wacca': '华卡',
'settings.fields.unlockMusic.name': '解锁谱面', 'settings.fields.mai2UnlockMusic.name': '解锁谱面',
'settings.fields.unlockMusic.desc': '在游戏中解锁所有曲目和大师难度谱面。', 'settings.fields.mai2UnlockMusic.desc': '解锁所有曲目和大师难度谱面。',
'settings.fields.unlockChara.name': '解锁角色', 'settings.fields.mai2UnlockChara.name': '解锁角色',
'settings.fields.unlockChara.desc': '在游戏中解锁所有角色、语音和伙伴。', 'settings.fields.mai2UnlockChara.desc': '解锁所有角色(新角色从 1 级开始)。',
'settings.fields.unlockCollectables.name': '解锁收藏品', 'settings.fields.mai2UnlockCharaMaxLevel.name': '角色满级',
'settings.fields.unlockCollectables.desc': '在游戏中解锁所有收藏品(名牌、称号、图标、背景图)。', 'settings.fields.mai2UnlockCharaMaxLevel.desc': '将所有角色设置为满级。',
'settings.fields.unlockTickets.name': '解锁游戏券', 'settings.fields.mai2UnlockPartners.name': '解锁搭档',
'settings.fields.unlockTickets.desc': '无限跑图券/解锁券maimai 客户端仍限制一些券不能使用)。', 'settings.fields.mai2UnlockPartners.desc': '解锁所有搭档。',
'settings.fields.waccaInfiniteWp.name': '华卡:无限 WP', 'settings.fields.mai2UnlockCollectables.name': '解锁收藏品',
'settings.fields.waccaInfiniteWp.desc': '将 WP 设置为 999999', 'settings.fields.mai2UnlockCollectables.desc': '解锁所有收藏品(姓名框、称号、头像、背景)。',
'settings.fields.waccaAlwaysVip.name': '华卡:永久会员', 'settings.fields.mai2UnlockTickets.name': '解锁功能票',
'settings.fields.waccaAlwaysVip.desc': '将 VIP 到期时间设置为 2077-01-01', 'settings.fields.mai2UnlockTickets.desc': '无限功能票(注:客户端仍限制一些功能票不能使用)。',
'settings.fields.waccaUnlockMusic.name': '解锁谱面',
'settings.fields.waccaUnlockMusic.desc': '解锁所有曲目。',
'settings.fields.waccaUnlockPlates.name': '解锁铭牌',
'settings.fields.waccaUnlockPlates.desc': '解锁所有铭牌。',
'settings.fields.waccaUnlockCollectables.name': '解锁收藏品',
'settings.fields.waccaUnlockCollectables.desc': '解锁所有收藏品。',
'settings.fields.waccaUnlockTickets.name': '无限解锁券',
'settings.fields.waccaUnlockTickets.desc': '无限解锁券。',
'settings.fields.waccaInfiniteWp.name': '无限 WP',
'settings.fields.waccaInfiniteWp.desc': '将 WP 设置为 999999。',
'settings.fields.waccaAlwaysVip.name': '永久会员',
'settings.fields.waccaAlwaysVip.desc': '将 VIP 到期时间设置为 2077-01-01。',
'settings.fields.chusanTeamName.name': '队伍名称', 'settings.fields.chusanTeamName.name': '队伍名称',
'settings.fields.chusanTeamName.desc': '自定义显示在个人资料顶部的文本。', 'settings.fields.chusanTeamName.desc': '自定义显示在个人资料顶部的文本。',
'settings.fields.chusanInfinitePenguins.name': '我是桐谷遥', 'settings.fields.chusanInfinitePenguins.name': '我是桐谷遥',
@@ -207,9 +220,10 @@ const zhSettings: typeof EN_REF_SETTINGS = {
'settings.export': '导出玩家数据', 'settings.export': '导出玩家数据',
'settings.batchManualExport': "导出 Batch Manual 格式(用于 Tachi", 'settings.batchManualExport': "导出 Batch Manual 格式(用于 Tachi",
'settings.cabNotice': '注意:下面这些设置只会影响你自己的机器,如果你是在其他人的机器上玩的话,请联系机主来改设置', 'settings.cabNotice': '注意:下面这些设置只会影响你自己的机器,如果你是在其他人的机器上玩的话,请联系机主来改设置',
'settings.gameNotice': "这些设置仅对舞萌和华卡生效。",
// AI // AI
'settings.regionNotice': "这些设置仅适用于舞萌、音击和中二。", 'settings.siteNotice': "这些设置仅适用于网站。",
// AI
'settings.regionNotice': "这些设置在舞萌、音击和中二节奏之间共享。",
// AI // AI
'settings.regionSelector.title': "地区选择器", 'settings.regionSelector.title': "地区选择器",
// AI // AI

View File

@@ -10,7 +10,6 @@
import { t } from "../libs/i18n"; import { t } from "../libs/i18n";
import ImportDataAction from "./Home/ImportDataAction.svelte"; import ImportDataAction from "./Home/ImportDataAction.svelte";
import Communities from "./Home/Communities.svelte"; import Communities from "./Home/Communities.svelte";
import MigrateAction from "./Home/MigrateAction.svelte";
USER.ensureLoggedIn(); USER.ensureLoggedIn();
@@ -59,9 +58,6 @@
</ActionCard> </ActionCard>
<ImportDataAction/> <ImportDataAction/>
{#if me}
<MigrateAction username={me.username}/>
{/if}
</div> </div>
{:else if tab === 1} {:else if tab === 1}
<div out:fade={FADE_OUT} in:fade={FADE_IN}> <div out:fade={FADE_OUT} in:fade={FADE_IN}>

View File

@@ -1,69 +0,0 @@
<script lang="ts">
import { fade } from "svelte/transition"
import { t } from "../../libs/i18n";
import ActionCard from "../../components/ActionCard.svelte";
import { CARD, GAME, USER } from "../../libs/sdk";
export let username: string;
let shouldShow = navigator.language.startsWith('zh');
let showWarning = false;
let isCardBindIssue = false;
if (!shouldShow) {
fetch('https://47.122.72.135/ip/isChina')
.then(it => it.json())
.then(it => shouldShow = it)
.catch(() => shouldShow = false);
}
CARD.userGames(username).then(games => {
if (!Object.values(games).some(it => it)) {
isCardBindIssue = true;
}
})
const handleClick = () => {
if (isCardBindIssue) {
showWarning = true;
return
}
jump()
}
const jump = () => {
const token = localStorage.getItem('token')
location.href = `https://portal.mumur.net/migrateFromAquaDx/${token}`
}
</script>
{#if shouldShow}
<ActionCard color="190, 149, 255" icon="system-uicons:jump-up" on:click={handleClick}>
<h3>迁移到 MuNET</h3>
<span>更适合中国宝宝体质的服务器AquaDX 的继任者。点击查看详情</span>
</ActionCard>
{/if}
{#if showWarning}
<div class="overlay" transition:fade>
<div>
<h2>提示</h2>
<p>看起来你在 AquaDX 还没有游戏数据,也许是因为没有绑卡或者绑定的卡不是在游戏中点击“查看卡号”获取的…</p>
<p>现在迁移的话,大概会导致你的游戏数据无法被正确的迁移。建议你先去检查一下吧</p>
<div class="buttons">
<button on:click={() => showWarning = false}>{t('action.cancel')}</button>
<button on:click={jump}>继续</button>
</div>
</div>
</div>
{/if}
<style lang="sass">
h3
font-size: 1.3rem
margin: 0
.buttons
display: grid
grid-template-columns: 1fr 1fr
gap: 1rem
</style>

View File

@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte";
import { title } from "../libs/ui"; import { title } from "../libs/ui";
import { GAME } from "../libs/sdk"; import { GAME } from "../libs/sdk";
import type { GenericRanking } from "../libs/generalTypes"; import type { GenericRanking } from "../libs/generalTypes";
@@ -8,6 +9,7 @@
import { t } from "../libs/i18n"; import { t } from "../libs/i18n";
import UserCard from "../components/UserCard.svelte"; import UserCard from "../components/UserCard.svelte";
import Tooltip from "../components/Tooltip.svelte"; import Tooltip from "../components/Tooltip.svelte";
import Pagination from "../components/Pagination.svelte";
export let game: GameName = 'mai2'; export let game: GameName = 'mai2';
@@ -15,15 +17,45 @@
let d: { users: GenericRanking[] }; let d: { users: GenericRanking[] };
let error: string | null; let error: string | null;
let page = 1
const perPage = 50
let totalPages = 1
function handleUpdatePage(event: CustomEvent<number>) {
page = event.detail;
const url = new URL(window.location.toString())
url.searchParams.set('page', page.toString())
history.pushState({}, '', url.toString())
window.scrollTo(0, 0)
}
onMount(() => {
const url = new URL(window.location.toString())
const pageParam = url.searchParams.get('page')
if (pageParam) {
page = parseInt(pageParam, 10) || 1
}
window.addEventListener('popstate', () => {
const url = new URL(window.location.toString())
const pageParam = url.searchParams.get('page')
page = parseInt(pageParam, 10) || 1
window.scrollTo(0, 0)
})
})
Promise.all([GAME.ranking(game)]) Promise.all([GAME.ranking(game)])
.then(([users]) => { .then(([users]) => {
console.log(users) d = { users }
d = { users }; totalPages = Math.ceil(users.length / perPage)
}) })
.catch((e) => error = e.message); .catch((e) => error = e.message);
let hoveringUser = ""; let hoveringUser = "";
let hoverLoading = false; let hoverLoading = false;
$: paginatedUsers = d ? d.users.slice((page - 1) * perPage, page * perPage) : []
</script> </script>
<main class="content leaderboard"> <main class="content leaderboard">
@@ -37,8 +69,12 @@
</div> </div>
{#if d} {#if d}
{#if page > 1}
<Pagination {page} {totalPages} on:updatePage={handleUpdatePage} />
{/if}
<div class="leaderboard-container"> <div class="leaderboard-container">
<div class="lb-user" on:mouseenter={() => hoveringUser = d.users[0].username} role="heading" aria-level="2"> <div class="lb-user" on:mouseenter={() => hoveringUser = paginatedUsers[0]?.username} role="heading" aria-level="2">
<span class="rank">{t("Leaderboard.Rank")}</span> <span class="rank">{t("Leaderboard.Rank")}</span>
<span class="name"></span> <span class="name"></span>
<span class="rating">{t("Leaderboard.Rating")}</span> <span class="rating">{t("Leaderboard.Rating")}</span>
@@ -46,7 +82,7 @@
<span class="fc">{t("Leaderboard.FC")}</span> <span class="fc">{t("Leaderboard.FC")}</span>
<span class="ap">{t("Leaderboard.AP")}</span> <span class="ap">{t("Leaderboard.AP")}</span>
</div> </div>
{#each d.users as user, i (user.rank)} {#each paginatedUsers as user, i (user.rank)}
<div class="lb-user" class:alternate={i % 2 === 1} role="listitem" <div class="lb-user" class:alternate={i % 2 === 1} role="listitem"
on:mouseover={() => hoveringUser = user.username} on:focus={() => {}}> on:mouseover={() => hoveringUser = user.username} on:focus={() => {}}>
@@ -70,6 +106,8 @@
{/each} {/each}
</div> </div>
<Pagination {page} {totalPages} on:updatePage={handleUpdatePage} />
<Tooltip triggeredBy=".name" loading={hoverLoading}> <Tooltip triggeredBy=".name" loading={hoverLoading}>
<UserCard username={hoveringUser} {game} setLoading={l => hoverLoading = l} /> <UserCard username={hoveringUser} {game} setLoading={l => hoverLoading = l} />
</Tooltip> </Tooltip>
@@ -132,5 +170,4 @@
&.alternate &.alternate
background-color: vars.$ov-light background-color: vars.$ov-light
</style> </style>

View File

@@ -23,7 +23,7 @@
let error: string; let error: string;
let submitting = "" let submitting = ""
let tab = 0 let tab = 0
let tabs = [ 'profile', 'game' ] let tabs = ['profile']
const profileFields = [ const profileFields = [
[ 'displayName', t('settings.profile.name') ], [ 'displayName', t('settings.profile.name') ],
@@ -45,18 +45,11 @@
me = m me = m
CARD.userGames(m.username).then(games => { CARD.userGames(m.username).then(games => {
if (games.chu3 && !tabs.includes('chu3')) { tabs = [
tabs = [...tabs, 'chu3'] ...tabs,
} ...['chu3', 'mai2','wacca', 'ongeki'].filter(v => games[v as keyof typeof games]), // :xdx:
if (games.mai2 && !tabs.includes('mai2')) { 'global'
tabs = [...tabs, 'mai2'] ]
}
if (games.wacca && !tabs.includes('wacca')) {
tabs = [...tabs, 'wacca']
}
if (games.ongeki && !tabs.includes('ongeki')) {
tabs = [...tabs, 'ongeki']
}
}) })
}).catch(e => error = e.message) }).catch(e => error = e.message)
getMe() getMe()
@@ -80,11 +73,12 @@
// Don't know why this isn't just a part of the cropper module. Have to do this myself.. What a shame // Don't know why this isn't just a part of the cropper module. Have to do this myself.. What a shame
let canvas = document.createElement("canvas"); let canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d"); let ctx = canvas.getContext("2d");
canvas.width = 256; const size = Math.round(Math.min(pfpCrop.width, pfpCrop.height, 1024));
canvas.height = 256; canvas.width = size;
canvas.height = size;
let img = document.createElement("img"); let img = document.createElement("img");
img.onload = () => { img.onload = () => {
ctx?.drawImage(img, pfpCrop.x, pfpCrop.y, pfpCrop.width, pfpCrop.height, 0, 0, 256, 256); ctx?.drawImage(img, pfpCrop.x, pfpCrop.y, pfpCrop.width, pfpCrop.height, 0, 0, size, size);
canvas.toBlob(blob => { canvas.toBlob(blob => {
if (!blob) return; if (!blob) return;
submitting = 'profilePicture' submitting = 'profilePicture'
@@ -216,7 +210,7 @@
<WaccaSettings /> <WaccaSettings />
{:else if tabs[tab] === 'ongeki'} {:else if tabs[tab] === 'ongeki'}
<OngekiSettings /> <OngekiSettings />
{:else if tabs[tab] === 'game'} {:else if tabs[tab] === 'global'}
<GeneralGameSettings /> <GeneralGameSettings />
{/if} {/if}

View File

@@ -325,7 +325,6 @@
<RatingComposition title="Recent 10" comp={d.user.ratingComposition.recent10} {allMusics} game={game != "auto" ? game : "mai2"} top={10}/> <RatingComposition title="Recent 10" comp={d.user.ratingComposition.recent10} {allMusics} game={game != "auto" ? game : "mai2"} top={10}/>
{/if} {/if}
<div class="recent"> <div class="recent">
<h2>{t('UserHome.RecentScores')}</h2> <h2>{t('UserHome.RecentScores')}</h2>
<div class="scores"> <div class="scores">
@@ -368,6 +367,22 @@
{/if} {/if}
</div> </div>
</div> </div>
{#if d.user.favorites != null && d.user.favorites.length > 0}
<div class="favorites">
<h2>{t('UserHome.FavoriteSongs')}</h2>
<div class="scores">
{#each d.user.favorites as favoriteSongId, i}
<div>
<img src={`${DATA_HOST}/d/${game}/music/00${favoriteSongId.toString().padStart(6, '0').substring(2)}.png`} alt="" on:error={coverNotFound} />
<div class="info">
<div class="song-title">{allMusics[favoriteSongId.toString()] ? allMusics[favoriteSongId.toString()].name : t("UserHome.UnknownSong")}</div>
</div>
</div>
{/each}
</div>
</div>
{/if}
{/if} {/if}
<StatusOverlays {error} loading={!d || isLoading} /> <StatusOverlays {error} loading={!d || isLoading} />
@@ -554,6 +569,57 @@
flex-direction: row flex-direction: row
justify-content: space-between justify-content: space-between
.favorites
.scores
display: grid
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr))
gap: 20px
// Image and song info
> div
display: flex
align-items: center
gap: 20px
background-color: rgba(white, 0.03)
border-radius: vars.$border-radius
img
width: 50px
height: 50px
border-radius: vars.$border-radius
object-fit: cover
// Song info and score
> div.info
flex: 1
display: flex
justify-content: space-between
overflow: hidden
flex-direction: column
.first-line
display: flex
flex-direction: row
// Limit song name to one line
.song-title
max-width: 90%
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
// Make song score and rank not wrap
> div:last-child
white-space: nowrap
@media (max-width: vars.$w-mobile)
flex-direction: column
gap: 0
.rank-text
text-align: left
// Recent Scores section // Recent Scores section
.recent .recent
.scores .scores

View File

@@ -5,7 +5,6 @@
import Icon from "@iconify/svelte"; import Icon from "@iconify/svelte";
import { USER } from "../libs/sdk"; import { USER } from "../libs/sdk";
import { t } from "../libs/i18n" import { t } from "../libs/i18n"
import MunetRegisterBanner from "../components/MunetRegisterBanner.svelte";
let params = new URLSearchParams(window.location.search) let params = new URLSearchParams(window.location.search)
@@ -100,9 +99,6 @@
state = 'verify' state = 'verify'
verifyMsg = t("welcome.verify-state-2") verifyMsg = t("welcome.verify-state-2")
} }
else if (e.message === 'Login not allowed: Card has been migrated to Minato.') {
location.href = `https://portal.mumur.net/login?username=${encodeURIComponent(email)}`
}
else { else {
error = e.message error = e.message
submitting = false // unnecessary? see line 113, same for both reset functions submitting = false // unnecessary? see line 113, same for both reset functions
@@ -221,9 +217,6 @@
on:turnstile-expired={_ => window.location.reload()} on:turnstile-expired={_ => window.location.reload()}
on:turnstile-timeout={_ => console.log(error = t('welcome.turnstile-timeout'))} /> on:turnstile-timeout={_ => console.log(error = t('welcome.turnstile-timeout'))} />
{/if} {/if}
{#if isSignup}
<MunetRegisterBanner username={username} email={email}/>
{/if}
</div> </div>
{:else if state === "submitreset"} {:else if state === "submitreset"}
<div class="login-form" transition:slide> <div class="login-form" transition:slide>

View File

@@ -33,9 +33,9 @@ Below is a list of games supported by this server.
| Game | Ver | Codename | Thanks to | | Game | Ver | Codename | Thanks to |
|------------------------|------|-------------|------------------------------------------------------| |------------------------|------|-------------|------------------------------------------------------|
| SDHD: CHUNITHM | 2.30 | VERSE | | | SDHD: CHUNITHM | 2.40 | X-VERSE | |
| SDEZ: MaiMai DX | 1.55 | PRiSM Plus | | | SDEZ: MaiMai DX | 1.55 | PRiSM Plus | |
| SDGA: MaiMai DX (Intl) | 1.50 | PRiSM | [@Clansty](https://github.com/clansty) | | SDGA: MaiMai DX (Intl) | 1.55 | PRiSM | [@Clansty](https://github.com/clansty) |
| SDED: Card Maker | 1.39 | | [@Becods](https://github.com/Becods) | | SDED: Card Maker | 1.39 | | [@Becods](https://github.com/Becods) |
| SDDT: O.N.G.E.K.I. | 1.50 | Re:Fresh | [@PenguinCaptain](https://github.com/PenguinCaptain) | | SDDT: O.N.G.E.K.I. | 1.50 | Re:Fresh | [@PenguinCaptain](https://github.com/PenguinCaptain) |
| SBZV: Project DIVA | 7.10 | Future Tone | | | SBZV: Project DIVA | 7.10 | Future Tone | |

View File

@@ -15,6 +15,7 @@ import kotlinx.coroutines.withContext
import org.apache.tika.Tika import org.apache.tika.Tika
import org.apache.tika.mime.MimeTypes import org.apache.tika.mime.MimeTypes
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.context.ApplicationContext
import org.springframework.http.HttpHeaders import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity.BodyBuilder import org.springframework.http.ResponseEntity.BodyBuilder
@@ -34,12 +35,15 @@ import java.util.concurrent.locks.Lock
import kotlin.reflect.KCallable import kotlin.reflect.KCallable
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KMutableProperty1 import kotlin.reflect.KMutableProperty1
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.isSubclassOf import kotlin.reflect.full.isSubclassOf
import kotlin.reflect.full.memberProperties import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.javaField
import kotlin.reflect.jvm.jvmErasure import kotlin.reflect.jvm.jvmErasure
typealias RP = RequestParam typealias RP = RequestParam
typealias RB = RequestBody typealias RB = RequestBody
typealias RT = RequestPart
typealias RH = RequestHeader typealias RH = RequestHeader
typealias PV = PathVariable typealias PV = PathVariable
typealias API = RequestMapping typealias API = RequestMapping
@@ -79,7 +83,9 @@ annotation class SettingField(
// Reflection // Reflection
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun <T : Any> KClass<T>.vars() = memberProperties.mapNotNull { it as? Var<T, Any> } fun <T : Any> KClass<T>.ownVars() = declaredMemberProperties.sortedBy { it.javaField?.declaringClass?.declaredFields?.indexOf(it.javaField) ?: Int.MAX_VALUE }.mapNotNull { it as? Var<T, Any> }
@Suppress("UNCHECKED_CAST")
fun <T : Any> KClass<T>.vars(): List<Var<T, Any>> = supertypes.mapNotNull { it.classifier as? KClass<*> }.filter { !it.java.isInterface }.flatMap{ it.vars() as List<Var<T, Any>> } + ownVars()
fun <T : Any> KClass<T>.varsMap() = vars().associateBy { it.name } fun <T : Any> KClass<T>.varsMap() = vars().associateBy { it.name }
fun <T : Any> KClass<T>.getters() = java.methods.filter { it.name.startsWith("get") } fun <T : Any> KClass<T>.getters() = java.methods.filter { it.name.startsWith("get") }
fun <T : Any> KClass<T>.gettersMap() = getters().associateBy { it.name.removePrefix("get").firstCharLower() } fun <T : Any> KClass<T>.gettersMap() = getters().associateBy { it.name.removePrefix("get").firstCharLower() }
@@ -263,3 +269,6 @@ val <S> Pair<*, S>.r get() = component2()
val Query.exec get() = resultList.map { (it as Array<*>).toList() } val Query.exec get() = resultList.map { (it as Array<*>).toList() }
fun List<List<Any?>>.numCsv(vararg head: Str) = head.joinToString(",") + "\n" + fun List<List<Any?>>.numCsv(vararg head: Str) = head.joinToString(",") + "\n" +
joinToString("\n") { it.joinToString(",") } joinToString("\n") { it.joinToString(",") }
// DI
inline fun <reified T> ApplicationContext.lazy() = lazy { getBean(T::class.java) }

View File

@@ -21,15 +21,15 @@ val JSON_FUZZY_BOOLEAN = SimpleModule().addDeserializer(Boolean::class.java, obj
else -> 400 - "Invalid boolean value ${parser.text}" else -> 400 - "Invalid boolean value ${parser.text}"
} }
}) })
val JSON_DATETIME = SimpleModule().addDeserializer(java.time.LocalDateTime::class.java, object : JsonDeserializer<java.time.LocalDateTime>() { val JSON_DATETIME = SimpleModule().addDeserializer(java.time.LocalDateTime::class.java, object : JsonDeserializer<LocalDateTime>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext) = override fun deserialize(parser: JsonParser, context: DeserializationContext) =
// First try standard formats via asDateTime() method // First try standard formats via asDateTime() method
parser.text.asDateTime() ?: try { parser.text.takeIf { it.isNotEmpty() }?.run { asDateTime() ?: try {
// Try maimai2 format (yyyy-MM-dd HH:mm:ss.0) // Try maimai2 format (yyyy-MM-dd HH:mm:ss.0)
LocalDateTime.parse(parser.text, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.0")) LocalDateTime.parse(parser.text, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.0"))
} catch (e: Exception) { } catch (e: Exception) {
400 - "Invalid date time value ${parser.text}" 400 - "Invalid date time value ${parser.text}"
} } }
}) })
val JACKSON = jacksonObjectMapper().apply { val JACKSON = jacksonObjectMapper().apply {
setSerializationInclusion(JsonInclude.Include.NON_NULL) setSerializationInclusion(JsonInclude.Include.NON_NULL)

View File

@@ -30,7 +30,8 @@ class CardController(
val cardService: CardService, val cardService: CardService,
val cardGameService: CardGameService, val cardGameService: CardGameService,
val cardRepository: CardRepository, val cardRepository: CardRepository,
val props: AquaNetProps val props: AquaNetProps,
val fedy: Fedy
) { ) {
companion object { companion object {
val log = logger() val log = logger()
@@ -80,10 +81,12 @@ class CardController(
val id = cardService.sanitizeCardId(cardId) val id = cardService.sanitizeCardId(cardId)
// Create a new card // Create a new card
cardService.registerByAccessCode(id, u) val newCard = cardService.registerByAccessCode(id, u)
log.info("Net /card/link : Created new card $id for user ${u.username}") log.info("Net /card/link : Created new card $id for user ${u.username}")
fedy.onCardLinked(newCard.luid, oldExtId = null, ghostExtId = u.ghostCard.extId, emptyList())
return SUCCESS return SUCCESS
} }
@@ -98,6 +101,8 @@ class CardController(
val games = migrate.split(',') val games = migrate.split(',')
cardGameService.migrate(card, games) cardGameService.migrate(card, games)
fedy.onCardLinked(card.luid, oldExtId = card.extId, ghostExtId = u.ghostCard.extId, games)
log.info("Net /card/link : Linked card ${card.id} to user ${u.username} and migrated data to ${games.joinToString()}") log.info("Net /card/link : Linked card ${card.id} to user ${u.username} and migrated data to ${games.joinToString()}")
SUCCESS SUCCESS
@@ -115,10 +120,14 @@ class CardController(
// Ghost cards cannot be unlinked // Ghost cards cannot be unlinked
if (card.isGhost) 400 - "Account virtual cards cannot be unlinked" if (card.isGhost) 400 - "Account virtual cards cannot be unlinked"
val luid = card.luid
// Unbind the card // Unbind the card
card.aquaUser = null card.aquaUser = null
async { cardRepository.save(card) } async { cardRepository.save(card) }
fedy.onCardUnlinked(luid)
log.info("Net /card/unlink : Unlinked card ${card.id} from user ${u.username}") log.info("Net /card/unlink : Unlinked card ${card.id} from user ${u.username}")
SUCCESS SUCCESS
@@ -136,7 +145,7 @@ class CardController(
* *
* Assumption: The card is already linked to the user. * Assumption: The card is already linked to the user.
*/ */
suspend fun <T : IUserData> migrateCard(repo: GenericUserDataRepo<T>, cardRepo: CardRepository, card: Card): Bool { suspend fun <T : IUserData> migrateCard(gameName: Str, repo: GenericUserDataRepo<T>, cardRepo: CardRepository, card: Card): Bool {
val ghost = card.aquaUser!!.ghostCard val ghost = card.aquaUser!!.ghostCard
// Check if data already exists in the user's ghost card // Check if data already exists in the user's ghost card
@@ -144,7 +153,7 @@ suspend fun <T : IUserData> migrateCard(repo: GenericUserDataRepo<T>, cardRepo:
// Create a new dummy card for deleted data // Create a new dummy card for deleted data
it.card = async { it.card = async {
cardRepo.save(Card().apply { cardRepo.save(Card().apply {
luid = "Migrated data of ghost card ${ghost.id} for user ${card.aquaUser!!.auId} on ${utcNow().isoDateTime()}" luid = "Migrated data of ghost card ${ghost.id} for user ${card.aquaUser!!.auId} on ${utcNow().isoDateTime()} (${gameName})"
// Randomize an extId outside the normal range // Randomize an extId outside the normal range
extId = Random.nextLong(0x7FFFFFF7L shl 32, 0x7FFFFFFFL shl 32) extId = Random.nextLong(0x7FFFFFF7L shl 32, 0x7FFFFFFFL shl 32)
registerTime = LocalDateTime.now() registerTime = LocalDateTime.now()
@@ -162,6 +171,23 @@ suspend fun <T : IUserData> migrateCard(repo: GenericUserDataRepo<T>, cardRepo:
return true return true
} }
suspend fun <T : IUserData> orphanData(gameName: Str, repo: GenericUserDataRepo<T>, cardRepo: CardRepository, card: Card) {
// Orphan the data by assigning them to a dummy card
repo.findByCard(card)?.let {
// Create a new dummy card for orphaned data
it.card = async {
cardRepo.save(Card().apply {
luid = "Unmigrated data of card ${card.luid} for user ${card.aquaUser!!.auId} on ${utcNow().isoDateTime()} (${gameName})"
// Randomize an extId outside the normal range
extId = Random.nextLong(0x7FFFFFF7L shl 32, 0x7FFFFFFFL shl 32)
registerTime = LocalDateTime.now()
accessTime = registerTime
})
}
async { repo.save(it) }
}
}
suspend fun getSummaryFor(repo: GenericUserDataRepo<*>, card: Card): Map<Str, Any>? { suspend fun getSummaryFor(repo: GenericUserDataRepo<*>, card: Card): Map<Str, Any>? {
val data = async { repo.findByCard(card) } ?: return null val data = async { repo.findByCard(card) } ?: return null
return mapOf( return mapOf(
@@ -180,7 +206,8 @@ class CardGameService(
val diva: icu.samnyan.aqua.sega.diva.dao.userdata.PlayerProfileRepository, val diva: icu.samnyan.aqua.sega.diva.dao.userdata.PlayerProfileRepository,
val safety: AquaNetSafetyService, val safety: AquaNetSafetyService,
val cardRepo: CardRepository, val cardRepo: CardRepository,
val em: EntityManager val em: EntityManager,
val cardService: CardService
) { ) {
companion object { companion object {
val log = logger() val log = logger()
@@ -189,18 +216,22 @@ class CardGameService(
suspend fun migrate(crd: Card, games: List<String>) = async { suspend fun migrate(crd: Card, games: List<String>) = async {
// Migrate data from the card to the user's ghost card // Migrate data from the card to the user's ghost card
// An easy migration is to change the UserData card field to the user's ghost card // An easy migration is to change the UserData card field to the user's ghost card
val dataRepos = mapOf(
"mai2" to maimai2,
"chu3" to chusan,
"ongeki" to ongeki,
"wacca" to wacca,
)
val remainingGames = dataRepos.keys.toMutableSet()
games.forEach { game -> games.forEach { game ->
when (game) { val dataRepo = dataRepos[game] ?: return@forEach
"mai2" -> migrateCard(maimai2, cardRepo, crd) if (migrateCard(game, dataRepo, cardRepo, crd))
"chu3" -> migrateCard(chusan, cardRepo, crd) // Update timestamp for the ghost card (data migrated in)
"ongeki" -> migrateCard(ongeki, cardRepo, crd) cardService.updateCardTimestamp(crd.aquaUser!!.ghostCard, game, resetCreatedAt = true)
"wacca" -> migrateCard(wacca, cardRepo, crd) remainingGames.remove(game)
// TODO: diva
// "diva" -> diva.findByPdId(card.extId.toInt()).getOrNull()?.let {
// it.pdId = card.aquaUser!!.ghostCard
// }
}
} }
// For remaining games, orphan the data by assigning them to a dummy card
remainingGames.forEach { game -> orphanData(game, dataRepos[game]!!, cardRepo, crd) }
} }
suspend fun getSummary(card: Card) = async { suspend fun getSummary(card: Card) = async {

View File

@@ -1,32 +1,40 @@
package icu.samnyan.aqua.net package icu.samnyan.aqua.net
import ext.* import ext.*
import icu.samnyan.aqua.sega.general.service.CardService import icu.samnyan.aqua.net.components.EmailProperties
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import java.security.MessageDigest import java.security.MessageDigest
import icu.samnyan.aqua.net.db.AquaNetUserRepo
import icu.samnyan.aqua.net.db.AquaNetUserFedyRepo
import icu.samnyan.aqua.net.utils.SUCCESS import icu.samnyan.aqua.net.utils.SUCCESS
import icu.samnyan.aqua.net.components.JWT import icu.samnyan.aqua.net.components.JWT
import icu.samnyan.aqua.net.db.AquaNetUserFedy import icu.samnyan.aqua.net.db.AquaGameOptions
import icu.samnyan.aqua.net.db.AquaNetUser import icu.samnyan.aqua.net.db.AquaNetUser
import icu.samnyan.aqua.net.games.ImportController import icu.samnyan.aqua.net.db.AquaUserServices
import icu.samnyan.aqua.net.games.mai2.Mai2Import import icu.samnyan.aqua.net.games.mai2.Mai2Import
import icu.samnyan.aqua.net.games.ExportOptions import icu.samnyan.aqua.net.games.ExportOptions
import icu.samnyan.aqua.sega.maimai2.handler.UploadUserPlaylogHandler as Mai2UploadUserPlaylogHandler import icu.samnyan.aqua.sega.maimai2.handler.UploadUserPlaylogHandler as Mai2UploadUserPlaylogHandler
import icu.samnyan.aqua.sega.maimai2.handler.UpsertUserAllHandler as Mai2UpsertUserAllHandler import icu.samnyan.aqua.sega.maimai2.handler.UpsertUserAllHandler as Mai2UpsertUserAllHandler
import icu.samnyan.aqua.net.utils.ApiException import icu.samnyan.aqua.net.utils.ApiException
import java.util.Arrays
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.transaction.PlatformTransactionManager import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.support.TransactionTemplate import org.springframework.transaction.support.TransactionTemplate
import icu.samnyan.aqua.sega.maimai2.model.Mai2UserDataRepo import icu.samnyan.aqua.sega.maimai2.model.Mai2UserDataRepo
import icu.samnyan.aqua.net.games.GenericUserDataRepo import icu.samnyan.aqua.net.games.GenericUserDataRepo
import icu.samnyan.aqua.net.games.IUserData import icu.samnyan.aqua.net.games.IUserData
import icu.samnyan.aqua.net.utils.PathProps
import icu.samnyan.aqua.sega.chusan.model.Chu3UserDataRepo
import icu.samnyan.aqua.sega.general.dao.CardRepository
import icu.samnyan.aqua.sega.general.model.Card
import icu.samnyan.aqua.sega.general.service.CardService
import icu.samnyan.aqua.sega.ongeki.OgkUserDataRepo
import icu.samnyan.aqua.sega.wacca.model.db.WcUserRepo
import org.springframework.context.ApplicationContext
import org.springframework.web.multipart.MultipartFile
import java.time.Instant
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import kotlin.io.path.getLastModifiedTime
import kotlin.io.path.isRegularFile
import kotlin.io.path.writeBytes
@Configuration @Configuration
@ConfigurationProperties(prefix = "aqua-net.fedy") @ConfigurationProperties(prefix = "aqua-net.fedy")
@@ -36,26 +44,48 @@ class FedyProps {
var remote: String = "" var remote: String = ""
} }
enum class FedyEvent { data class UserProfilePicture(val url: Str, val updatedAtMs: Long)
Linked, data class UserBasicInfo(
Unlinked, val auId: Long, val ghostExtId: Long, val registrationTimeMs: Long,
Upserted, val username: Str, val displayName: Str, val email: Str, val passwordHash: Str, val profileBio: Str,
Imported, val profilePicture: UserProfilePicture?, val gameOptions: Map<Str, Any?>?,
} )
private data class UserUpdatedEvent(val user: UserBasicInfo, val isNewlyCreated: Bool)
private data class CardCreatedEvent(val luid: Str, val extId: Long)
private data class CardLinkedEvent(val luid: Str, val oldExtId: Long?, val ghostExtId: Long, val migratedGames: List<Str>)
private data class CardUnlinkedEvent(val luid: Str)
private data class DataUpdatedEvent(val extId: Long, val isGhostCard: Bool, val game: Str, val removeOldData: Bool)
private data class FedyEvent(
var userUpdated: UserUpdatedEvent? = null,
var cardCreated: CardCreatedEvent? = null,
var cardLinked: CardLinkedEvent? = null,
var cardUnlinked: CardUnlinkedEvent? = null,
var dataUpdated: DataUpdatedEvent? = null,
)
@RestController @RestController
@API("/api/v2/fedy") @API("/api/v2/fedy", consumes = ["multipart/form-data"])
class Fedy( class Fedy(
val jwt: JWT, val jwt: JWT,
val userRepo: AquaNetUserRepo, val emailProps: EmailProperties,
val userFedyRepo: AquaNetUserFedyRepo, val cardRepo: CardRepository,
val mai2Import: Mai2Import,
val mai2UserDataRepo: Mai2UserDataRepo, val mai2UserDataRepo: Mai2UserDataRepo,
val mai2UploadUserPlaylog: Mai2UploadUserPlaylogHandler, val chu3UserDataRepo: Chu3UserDataRepo,
val mai2UpsertUserAll: Mai2UpsertUserAllHandler, val ongekiUserDataRepo: OgkUserDataRepo,
val waccaUserDataRepo: WcUserRepo,
val props: FedyProps, val props: FedyProps,
val transactionManager: PlatformTransactionManager val paths: PathProps,
val transactionManager: PlatformTransactionManager,
ctx: ApplicationContext
) { ) {
val us by ctx.lazy<AquaUserServices>()
val cardService by ctx.lazy<CardService>()
val mai2Import by ctx.lazy<Mai2Import>()
val mai2UploadUserPlaylog by ctx.lazy<Mai2UploadUserPlaylogHandler>()
val mai2UpsertUserAll by ctx.lazy<Mai2UpsertUserAllHandler>()
val transaction by lazy { TransactionTemplate(transactionManager) } val transaction by lazy { TransactionTemplate(transactionManager) }
private fun Str.checkKey() { private fun Str.checkKey() {
@@ -63,73 +93,108 @@ class Fedy(
if (!MessageDigest.isEqual(this.toByteArray(), props.key.toByteArray())) 403 - "Invalid Key" if (!MessageDigest.isEqual(this.toByteArray(), props.key.toByteArray())) 403 - "Invalid Key"
} }
@API("/status") val suppressEvents = ThreadLocal.withInitial { false }
fun handleStatus(@RP token: Str): Any { private fun <T> handleFedy(key: Str, block: () -> T): T {
val user = jwt.auth(token) val old = suppressEvents.get()
val userFedy = userFedyRepo.findByAquaNetUserAuId(user.auId) suppressEvents.set(true)
return mapOf("linkedAt" to (userFedy?.createdAt?.toEpochMilli() ?: 0)) try {
}
@API("/link")
fun handleLink(@RP token: Str, @RP nonce: Str): Any {
val user = jwt.auth(token)
if (userFedyRepo.findByAquaNetUserAuId(user.auId) != null) 412 - "User already linked"
val userFedy = AquaNetUserFedy(aquaNetUser = user)
userFedyRepo.save(userFedy)
notify(FedyEvent.Linked, mapOf("auId" to user.auId, "nonce" to nonce))
return mapOf("linkedAt" to userFedy.createdAt.toEpochMilli())
}
@API("/unlink")
fun handleUnlink(@RP token: Str): Any {
val user = jwt.auth(token)
val userFedy = userFedyRepo.findByAquaNetUserAuId(user.auId) ?: 412 - "User not linked"
userFedyRepo.delete(userFedy)
notify(FedyEvent.Unlinked, mapOf("auId" to user.auId))
return SUCCESS
}
private fun ensureUser(auId: Long): AquaNetUser {
val userFedy = userFedyRepo.findByAquaNetUserAuId(auId) ?: 404 - "User not linked"
val user = userRepo.findByAuId(auId) ?: 404 - "User not found"
return user
}
data class UnlinkByRemoteReq(val auId: Long)
@API("/unlink-by-remote")
fun handleUnlinkByRemote(@RH(KEY_HEADER) key: Str, @RB req: UnlinkByRemoteReq): Any {
key.checkKey() key.checkKey()
val user = ensureUser(req.auId) return block()
userFedyRepo.deleteByAquaNetUserAuId(user.auId) } finally { suppressEvents.set(old) }
// No need to notify remote, because initiated by remote
return SUCCESS
} }
data class PullReq(val auId: Long, val game: Str, val exportOptions: ExportOptions) data class FedyErr(val code: Int, val message: Str)
@API("/pull")
fun handlePull(@RH(KEY_HEADER) key: Str, @RB req: PullReq): Any { data class UserPullReq(val auId: Long)
key.checkKey() data class UserPullRes(val user: UserBasicInfo?)
val user = ensureUser(req.auId) @API("/user/pull")
fun catched(block: () -> Any) = fun handleUserPull(@RH(KEY_HEADER) key: Str, @RT(REQ_PART) req: UserPullReq): UserPullRes = handleFedy(key) {
try { mapOf("result" to block()) } UserPullRes(us.userRepo.findByAuId(req.auId)?.fedyBasicInfo())
catch (e: ApiException) { mapOf("error" to mapOf("code" to e.code, "message" to e.message.toString())) } }
return when (req.game) {
"mai2" -> catched { mai2Import.export(user, req.exportOptions) } data class UserLookupReq(val username: Str?, val email: Str?)
data class UserLookupRes(val user: UserBasicInfo?)
@API("/user/lookup")
fun handleUserLogin(@RH(KEY_HEADER) key: Str, @RT(REQ_PART) req: UserLookupReq): UserLookupRes = handleFedy(key) {
UserLookupRes(user =
(req.username?.let { us.userRepo.findByUsernameIgnoreCase(it) } ?: req.email?.let {us.userRepo.findByEmailIgnoreCase(it) })
?.takeIf { it.emailConfirmed || !emailProps.enable }
?.fedyBasicInfo()
)
}
data class UserRegisterReq(val username: Str, val email: Str, val password: Str)
data class UserRegisterRes(val error: FedyErr? = null, val user: UserBasicInfo? = null)
@API("/user/register")
fun handleUserRegister(@RH(KEY_HEADER) key: Str, @RT(REQ_PART) req: UserRegisterReq): UserRegisterRes = handleFedy(key) {
{
UserRegisterRes(user = us.create(req.username, req.email, req.password, "", emailConfirmed = true).fedyBasicInfo())
} caught { UserRegisterRes(error = it) }
}
data class UserUpdateReq(val auId: Long, val fields: Map<Str, Str?>?, val gameOptions: Map<Str, Any?>?)
data class UserUpdateRes(val error: FedyErr? = null, val user: UserBasicInfo? = null)
@API("/user/update")
fun handleUserUpdate(@RH(KEY_HEADER) key: Str, @RT(REQ_PART) req: UserUpdateReq, @RT(PFP_PART) pfpFile: MultipartFile?): UserUpdateRes = handleFedy(key) {
{
val ru = us.userRepo.findByAuId(req.auId) ?: (404 - "User not found")
val fields = req.fields?.filterValues { it != null }?.mapValues { it.value as Str } ?: emptyMap()
fields.forEach { (k, v) ->
if (k == "email") { ru.email = us.validateEmail(v) }
else us.update(ru, k, v)
}
pfpFile?.apply {
val mime = TIKA.detect(pfpFile.bytes).takeIf { it.startsWith("image/") } ?: (400 - "Invalid file type")
val name = "${ru.auId}${MIMES.forName(mime)?.extension ?: ".jpg"}"
(paths.aquaNetPortrait.path() / name).writeBytes(bytes)
ru.profilePicture = name
}
req.gameOptions?.apply {
val options = ru.gameOptions ?: AquaGameOptions().also { ru.gameOptions = it }
forEach { (k, v) -> v?.let { GAME_OPTIONS_FIELDS[k]?.set(options, it) } }
}
us.userRepo.save(ru)
if (fields.containsKey("pwHash") ?: false) { us.clearAllSessions(ru) }
UserUpdateRes(user = ru.fedyBasicInfo())
} caught { UserUpdateRes(error = it) }
}
private fun AquaNetUser.fedyBasicInfo() = UserBasicInfo(
auId, ghostCard.extId, regTime,
username, displayName, email, pwHash, profileBio ?: "",
profilePicture
?.let { paths.aquaNetPortrait.path() / it }?.takeIf { it.isRegularFile() }
?.let { UserProfilePicture(
url = "/uploads/net/portrait/${profilePicture}",
updatedAtMs = it.getLastModifiedTime().toMillis()
) },
gameOptions?.let { o -> GAME_OPTIONS_FIELDS.mapValues { it.value.get(o) } }
)
data class DataPullReq(val extId: Long, val game: Str, val createdAtMs: Long, val updatedAtMs: Long, val exportOptions: ExportOptions)
data class DataPullResult(val data: Any?, val createdAtMs: Long, val updatedAtMs: Long, val isRebased: Bool)
data class DataPullRes(val error: FedyErr? = null, val result: DataPullResult? = null)
@API("/data/pull")
fun handleDataPull(@RH(KEY_HEADER) key: Str, @RT(REQ_PART) req: DataPullReq): DataPullRes = handleFedy(key) {
val card = cardRepo.findByExtId(req.extId).orElse(null)
?: (404 - "Card with extId ${req.extId} not found")
val cardTimestamp = cardService.getCardTimestamp(card, req.game)
if (cardTimestamp.updatedAt.toEpochMilli() == req.updatedAtMs) return@handleFedy DataPullRes(error = null, result = null) // No changes
val isRebased = req.createdAtMs > 0 && cardTimestamp.createdAt.toEpochMilli() > req.createdAtMs
val exportOptions = if (!isRebased) { req.exportOptions } else { req.exportOptions.copy(playlogAfter = null) }
{
DataPullRes(result = DataPullResult(data = when (req.game) {
"mai2" -> mai2Import.export(card, exportOptions)
else -> 406 - "Unsupported game" else -> 406 - "Unsupported game"
} }, createdAtMs = cardTimestamp.createdAt.toEpochMilli(), updatedAtMs = cardTimestamp.updatedAt.toEpochMilli(), isRebased = isRebased))
} caught { DataPullRes(error = it) }
} }
data class PushReq(val auId: Long, val game: Str, val data: JDict, val removeOldData: Bool) data class DataPushReq(val extId: Long, val game: Str, val data: JDict, val removeOldData: Bool, val updatedAtMs: Long)
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@API("/push") @API("/data/push")
fun handlePush(@RH(KEY_HEADER) key: Str, @RB req: PushReq): Any { fun handleDataPush(@RH(KEY_HEADER) key: Str, @RT(REQ_PART) req: DataPushReq): Any = handleFedy(key) {
key.checkKey() val extId = req.extId
val user = ensureUser(req.auId)
val extId = user.ghostCard.extId
fun<UserData : IUserData, UserRepo : GenericUserDataRepo<UserData>> removeOldData(repo: UserRepo) { fun<UserData : IUserData, UserRepo : GenericUserDataRepo<UserData>> removeOldData(repo: UserRepo) {
val oldData = repo.findByCard_ExtId(extId) val oldData = repo.findByCard_ExtId(extId)
if (oldData.isPresent) { if (oldData.isPresent) {
@@ -138,6 +203,7 @@ class Fedy(
repo.flush() repo.flush()
} }
} }
val card = cardRepo.findByExtId(extId).orElse(null) ?: (404 - "Card not found")
transaction.execute { when (req.game) { transaction.execute { when (req.game) {
"mai2" -> { "mai2" -> {
if (req.removeOldData) { removeOldData(mai2UserDataRepo) } if (req.removeOldData) { removeOldData(mai2UserDataRepo) }
@@ -148,30 +214,112 @@ class Fedy(
} }
else -> 406 - "Unsupported game" else -> 406 - "Unsupported game"
} } } }
cardService.updateCardTimestamp(card, req.game, now = Instant.ofEpochMilli(req.updatedAtMs), resetCreatedAt = req.removeOldData)
return SUCCESS SUCCESS
} }
fun onUpserted(game: Str, maybeExtId: Any?) = maybeNotifyAsync(FedyEvent.Upserted, game, maybeExtId) data class CardResolveReq(val luid: Str, val pairedLuid: Str?, val createIfNotFound: Bool)
fun onImported(game: Str, maybeExtId: Any?) = maybeNotifyAsync(FedyEvent.Imported, game, maybeExtId) data class CardResolveRes(val extId: Long, val isGhostCard: Bool, val isNewlyCreated: Bool, val isPairedLuidDiverged: Bool)
@API("/card/resolve")
fun handleCardResolve(@RH(KEY_HEADER) key: Str, @RT(REQ_PART) req: CardResolveReq): CardResolveRes = handleFedy(key) {
var card = cardService.tryLookup(req.luid)
var isNewlyCreated = false
if (card != null) {
card = card.maybeGhost()
if (!card.isGhost) isNewlyCreated = isCardFresh(card)
} else if (req.createIfNotFound) {
card = cardService.registerByAccessCode(req.luid, null)
isNewlyCreated = true
log.info("Fedy /card/resolve : Created new card ${card.id} (${card.luid})")
}
var isPairedLuidDiverged = false
if (req.pairedLuid != null) {
var pairedCard = cardService.tryLookup(req.pairedLuid)?.maybeGhost()
if (pairedCard?.extId != card?.extId) {
var isGhost = pairedCard?.isGhost == true
var isNonFresh = pairedCard != null && !isCardFresh(pairedCard)
if (isGhost || isNonFresh) isPairedLuidDiverged = true
else if (card?.isGhost == true) {
// Ensure paired card is linked, if the main card is linked
// If the main card is not linked, there's nothing Fedy can do. It's Fedy's best effort.
if (pairedCard == null) { pairedCard = cardService.registerByAccessCode(req.pairedLuid, card.aquaUser) }
else { pairedCard.aquaUser = card.aquaUser; cardRepo.save(pairedCard) }
log.info("Fedy /card/resolve : Created paired card ${pairedCard.id} (${pairedCard.luid}) for user ${card.aquaUser?.auId} (${card.aquaUser?.username})")
}
}
}
private fun maybeNotifyAsync(event: FedyEvent, game: Str, maybeExtId: Any?) = if (!props.enabled) {} else CompletableFuture.runAsync { try { CardResolveRes(
val extId = maybeExtId?.long ?: return@runAsync card?.extId ?: 0,
val user = userRepo.findByGhostCardExtId(extId) ?: return@runAsync card?.isGhost ?: false,
val userFedy = userFedyRepo.findByAquaNetUserAuId(user.auId) ?: return@runAsync isNewlyCreated,
notify(event, mapOf("auId" to user.auId, "game" to game)) isPairedLuidDiverged)
}
data class CardLinkReq(val auId: Long, val luid: Str)
@API("/card/link")
fun handleCardLink(@RH(KEY_HEADER) key: Str, @RT(REQ_PART) req: CardLinkReq): Any = handleFedy(key) {
val ru = us.userRepo.findByAuId(req.auId) ?: (404 - "User not found")
var card = cardService.tryLookup(req.luid)
if (card == null) {
card = cardService.registerByAccessCode(req.luid, ru)
log.info("Fedy /card/link : Linked new card ${card.id} (${card.luid}) to user ${ru.auId} (${ru.username})")
} else {
if (card.isGhost) 400 - "Account virtual cards cannot be unlinked"
val cu = card.aquaUser
if (cu != null) {
if (cu.auId == req.auId) log.info("Fedy /card/link : Existing card ${card.id} (${card.luid}) already linked to user ${ru.auId} (${ru.username})")
else 400 - "Card linked to another user"
} else {
card.aquaUser = ru
cardRepo.save(card)
log.info("Fedy /card/link : Linked existing card ${card.id} (${card.luid}) to user ${ru.auId} (${ru.username})")
}
}
}
data class CardUnlinkReq(val auId: Long, val luid: Str)
@API("/card/unlink")
fun handleCardUnlink(@RH(KEY_HEADER) key: Str, @RT(REQ_PART) req: CardUnlinkReq): Any = handleFedy(key) {
val card = cardService.tryLookup(req.luid)
val cu = card?.aquaUser ?: return@handleFedy SUCCESS // Nothing to do
if (cu.auId != req.auId) 400 - "Card linked to another user"
if (card.isGhost) 400 - "Account virtual cards cannot be unlinked"
card.aquaUser = null
cardRepo.save(card)
log.info("Fedy /card/unlink : Unlinked card ${card.id} (${card.luid}) from user ${cu.auId} (${cu.username})")
}
fun onUserUpdated(u: AquaNetUser, isNew: Bool = false) = maybeNotifyAsync { FedyEvent(userUpdated = UserUpdatedEvent(u.fedyBasicInfo(), isNew)) }
fun onCardCreated(luid: Str, extId: Long) = maybeNotifyAsync { FedyEvent(cardCreated = CardCreatedEvent(luid, extId)) }
fun onCardLinked(luid: Str, oldExtId: Long?, ghostExtId: Long, migratedGames: List<Str>) = maybeNotifyAsync { FedyEvent(cardLinked = CardLinkedEvent(luid, oldExtId, ghostExtId, migratedGames)) }
fun onCardUnlinked(luid: Str) = maybeNotifyAsync { FedyEvent(cardUnlinked = CardUnlinkedEvent(luid)) }
fun onDataUpdated(extId: Long, game: Str, removeOldData: Bool) = maybeNotifyAsync {
val card = cardRepo.findByExtId(extId).orElse(null) ?: return@maybeNotifyAsync null // Card not found, nothing to do
FedyEvent(dataUpdated = DataUpdatedEvent(extId, card.isGhost, game, removeOldData))
}
private fun maybeNotifyAsync(getEvent: () -> FedyEvent?) = if (!props.enabled || suppressEvents.get()) {} else CompletableFuture.runAsync {
var event: FedyEvent? = null
try {
event = getEvent()
if (event == null) return@runAsync // Nothing to do
notify(event)
} catch (e: Exception) { } catch (e: Exception) {
log.error("Error handling Fedy on maybeNotifyAsync($event, $game, $maybeExtId)", e) log.error("Error handling Fedy on maybeNotifyAsync($event)", e)
} } }
}.let {}
private fun notify(event: FedyEvent, body: Any?) { private fun notify(event: FedyEvent) {
val MAX_RETRY = 3 val MAX_RETRY = 3
val body = body?.toJson() ?: "{}" val body = event.toJson() ?: "{}"
var retry = 0 var retry = 0
var shouldRetry = true var shouldRetry = true
while (retry < MAX_RETRY) { while (true) {
try { try {
val response = "${props.remote.trimEnd('/')}/notify/${event.name}".request() val response = "${props.remote.trimEnd('/')}/notify".request()
.header("Content-Type" to "application/json") .header("Content-Type" to "application/json")
.header(KEY_HEADER to props.key) .header(KEY_HEADER to props.key)
.post(body) .post(body)
@@ -191,14 +339,33 @@ class Fedy(
} }
} }
// Apparently existing cards could possibly be fresh and never used in any game. Treat them as new cards.
private fun isCardFresh(c: Card): Bool {
fun <T : IUserData> checkForGame(repo: GenericUserDataRepo<T>, card: Card): Bool = repo.findByCard(card) != null
return when {
checkForGame(mai2UserDataRepo, c) -> false
checkForGame(chu3UserDataRepo, c) -> false
checkForGame(ongekiUserDataRepo, c) -> false
checkForGame(waccaUserDataRepo, c) -> false
else -> true
}
}
private infix fun <T> (() -> T).caught(onError: (FedyErr) -> T) =
try { this() }
catch (e: ApiException) { onError(FedyErr(code = e.code, message = e.message.toString())) }
companion object companion object
{ {
const val KEY_HEADER = "X-Fedy-Key" const val KEY_HEADER = "X-Fedy-Key"
const val REQ_PART = "request"
const val PFP_PART = "profilePicture"
@Suppress("UNCHECKED_CAST")
val GAME_OPTIONS_FIELDS = listOf(
O::mai2UnlockMusic, O::mai2UnlockChara, O::mai2UnlockCharaMaxLevel, O::mai2UnlockPartners, O::mai2UnlockCollectables, O::mai2UnlockTickets
).map { it as Var<O, Any?> }.associateBy { it.name }
val log = logger() val log = logger()
}
}
fun getGameName(gameId: Str) = when (gameId) { typealias O = AquaGameOptions
"SDEZ" -> "mai2"
else -> null // Not supported
}
}
}

View File

@@ -19,7 +19,8 @@ class FrontierProps {
@API("/api/v2/frontier") @API("/api/v2/frontier")
class Frontier( class Frontier(
val cardService: CardService, val cardService: CardService,
val props: FrontierProps val props: FrontierProps,
val fedy: Fedy
) { ) {
fun Str.checkFtk() { fun Str.checkFtk() {
if (this != props.ftk) 403 - "Invalid FTK" if (this != props.ftk) 403 - "Invalid FTK"
@@ -35,6 +36,9 @@ class Frontier(
if (async { cardService.cardRepo.findByLuid(accessCode) }.isPresent) 400 - "Card already registered" if (async { cardService.cardRepo.findByLuid(accessCode) }.isPresent) 400 - "Card already registered"
val card = async { cardService.registerByAccessCode(accessCode) } val card = async { cardService.registerByAccessCode(accessCode) }
fedy.onCardCreated(accessCode, card.extId)
return mapOf( return mapOf(
"card" to card, "card" to card,
"id" to card.extId // Expose hidden ID "id" to card.extId // Expose hidden ID

View File

@@ -14,7 +14,8 @@ import kotlin.reflect.jvm.jvmErasure
class SettingsApi( class SettingsApi(
val us: AquaUserServices, val us: AquaUserServices,
val userRepo: AquaNetUserRepo, val userRepo: AquaNetUserRepo,
val goRepo: AquaGameOptionsRepo val goRepo: AquaGameOptionsRepo,
val fedy: Fedy
) { ) {
// Get all params with SettingField annotation // Get all params with SettingField annotation
val fields = AquaGameOptions::class.vars() val fields = AquaGameOptions::class.vars()
@@ -41,6 +42,6 @@ class SettingsApi(
} }
// Check field type // Check field type
field.setCast(options, value) field.setCast(options, value)
goRepo.save(options) goRepo.save(options).also { fedy.onUserUpdated(u) }
} }
} }

View File

@@ -3,20 +3,15 @@ package icu.samnyan.aqua.net
import ext.* import ext.*
import icu.samnyan.aqua.net.components.* import icu.samnyan.aqua.net.components.*
import icu.samnyan.aqua.net.db.* import icu.samnyan.aqua.net.db.*
import icu.samnyan.aqua.net.db.AquaUserServices.Companion.SETTING_FIELDS
import icu.samnyan.aqua.net.utils.PathProps import icu.samnyan.aqua.net.utils.PathProps
import icu.samnyan.aqua.net.utils.SUCCESS import icu.samnyan.aqua.net.utils.SUCCESS
import icu.samnyan.aqua.sega.general.dao.CardRepository import icu.samnyan.aqua.sega.general.dao.CardRepository
import icu.samnyan.aqua.sega.general.model.Card
import icu.samnyan.aqua.sega.general.model.CardStatus
import icu.samnyan.aqua.sega.general.service.CardService
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile import org.springframework.web.multipart.MultipartFile
import java.time.Instant import java.time.Instant
import java.time.LocalDateTime
import kotlin.io.path.writeBytes import kotlin.io.path.writeBytes
@RestController @RestController
@@ -26,15 +21,14 @@ class UserRegistrar(
val hasher: PasswordEncoder, val hasher: PasswordEncoder,
val turnstileService: TurnstileService, val turnstileService: TurnstileService,
val emailService: EmailService, val emailService: EmailService,
val fedy: Fedy,
val geoIP: GeoIP, val geoIP: GeoIP,
val jwt: JWT, val jwt: JWT,
val confirmationRepo: EmailConfirmationRepo, val confirmationRepo: EmailConfirmationRepo,
val resetPasswordRepo: ResetPasswordRepo, val resetPasswordRepo: ResetPasswordRepo,
val cardRepo: CardRepository, val cardRepo: CardRepository,
val cardService: CardService,
val validator: AquaUserServices, val validator: AquaUserServices,
val emailProps: EmailProperties, val emailProps: EmailProperties,
val sessionRepo: SessionTokenRepo,
final val paths: PathProps final val paths: PathProps
) { ) {
val portraitPath = paths.aquaNetPortrait.path() val portraitPath = paths.aquaNetPortrait.path()
@@ -68,29 +62,7 @@ class UserRegistrar(
val country = geoIP.getCountry(ip) val country = geoIP.getCountry(ip)
// Create user // Create user
val u = async { AquaNetUser( val u = async { validator.create(username, email, password, country) }
username = validator.checkUsername(username),
email = validator.checkEmail(email),
pwHash = validator.checkPwHash(password),
regTime = millis(), lastLogin = millis(), country = country,
) }
// Create a ghost card
val card = Card().apply {
extId = cardService.randExtID(cardExtIdStart, cardExtIdEnd)
luid = extId.toString()
registerTime = LocalDateTime.now()
accessTime = registerTime
aquaUser = u
isGhost = true
}
u.ghostCard = card
// Save the user
async {
userRepo.save(u)
cardRepo.save(card)
}
// Send confirmation email // Send confirmation email
emailService.sendConfirmation(u) emailService.sendConfirmation(u)
@@ -114,8 +86,6 @@ class UserRegistrar(
?: (400 - "User not found") ?: (400 - "User not found")
if (!hasher.matches(password, user.pwHash)) 400 - "Invalid password" if (!hasher.matches(password, user.pwHash)) 400 - "Invalid password"
if (user.ghostCard.status == CardStatus.MIGRATED_TO_MINATO) 400 - "Login not allowed: Card has been migrated to Minato."
// Check if email is verified // Check if email is verified
if (!user.emailConfirmed && emailProps.enable) { if (!user.emailConfirmed && emailProps.enable) {
// Check if last confirmation email was sent within a minute // Check if last confirmation email was sent within a minute
@@ -200,16 +170,13 @@ class UserRegistrar(
if (reset.createdAt.plusSeconds(60 * 60 * 24).isBefore(Instant.now())) 400 - "Token expired" if (reset.createdAt.plusSeconds(60 * 60 * 24).isBefore(Instant.now())) 400 - "Token expired"
// Change the password // Change the password
async { userRepo.save(reset.aquaNetUser.apply { pwHash = validator.checkPwHash(password) }) } val u = reset.aquaNetUser
async { userRepo.save(u.apply { pwHash = validator.checkPwHash(password) }) }
fedy.onUserUpdated(u)
// Remove the token from the list // Remove the token from the list
resetPasswordRepo.delete(reset) resetPasswordRepo.delete(reset)
// Clear all sessions
sessionRepo.deleteAll(
sessionRepo.findByAquaNetUserAuId(reset.aquaNetUser.auId)
)
return SUCCESS return SUCCESS
} }
@@ -227,8 +194,13 @@ class UserRegistrar(
// Check if the token is expired // Check if the token is expired
if (confirmation.createdAt.plusSeconds(60 * 60 * 24).isBefore(Instant.now())) 400 - "Token expired" if (confirmation.createdAt.plusSeconds(60 * 60 * 24).isBefore(Instant.now())) 400 - "Token expired"
// Check if the email is already confirmed
val u = confirmation.aquaNetUser
if (u.emailConfirmed) 400 - "Email already confirmed"
// Confirm the email // Confirm the email
async { userRepo.save(confirmation.aquaNetUser.apply { emailConfirmed = true }) } async { userRepo.save(u.apply { emailConfirmed = true }) }
fedy.onUserUpdated(u, isNew = true)
return SUCCESS return SUCCESS
} }
@@ -245,22 +217,16 @@ class UserRegistrar(
@API("/setting") @API("/setting")
@Doc("Validate and set a user setting field.", "Success message") @Doc("Validate and set a user setting field.", "Success message")
suspend fun setting(@RP token: Str, @RP key: Str, @RP value: Str) = jwt.auth(token) { u -> suspend fun setting(@RP token: Str, @RP key: Str, @RP value: Str) = jwt.auth(token) { u ->
// Check if the key is a settable field
val field = SETTING_FIELDS.find { it.name == key } ?: (400 - "Invalid setting")
async { async {
// Set the validated field validator.update(u, key, value)
field.setter.call(u, field.checker.call(validator, value))
// Save the user // Save the user
userRepo.save(u) userRepo.save(u)
// Clear all tokens if changing password // Clear all tokens if changing password
if (key == "pwHash") if (key == "pwHash") validator.clearAllSessions(u)
sessionRepo.deleteAll(
sessionRepo.findByAquaNetUserAuId(u.auId)
)
} }
fedy.onUserUpdated(u)
SUCCESS SUCCESS
} }
@@ -299,6 +265,7 @@ class UserRegistrar(
(portraitPath / name).writeBytes(bytes) (portraitPath / name).writeBytes(bytes)
userRepo.save(u.apply { profilePicture = name }) userRepo.save(u.apply { profilePicture = name })
} }
fedy.onUserUpdated(u)
SUCCESS SUCCESS
} }

View File

@@ -2,10 +2,7 @@ package icu.samnyan.aqua.net.db
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
import ext.SettingField import ext.SettingField
import jakarta.persistence.Entity import jakarta.persistence.*
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
@Entity @Entity
@@ -14,21 +11,29 @@ class AquaGameOptions(
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long = 0, var id: Long = 0,
@SettingField("general") @SettingField("mai2") @Column(name = "mai2_unlock_music")
var unlockMusic: Boolean = false, var mai2UnlockMusic: Boolean = false,
@SettingField("mai2") @Column(name = "mai2_unlock_chara")
@SettingField("general") var mai2UnlockChara: Boolean = false,
var unlockChara: Boolean = false, @SettingField("mai2") @Column(name = "mai2_unlock_chara_max_level")
var mai2UnlockCharaMaxLevel: Boolean = false,
@SettingField("general") @SettingField("mai2") @Column(name = "mai2_unlock_partners")
var unlockCollectables: Boolean = false, var mai2UnlockPartners: Boolean = false,
@SettingField("mai2") @Column(name = "mai2_unlock_collectables")
@SettingField("general") var mai2UnlockCollectables: Boolean = false,
var unlockTickets: Boolean = false, @SettingField("mai2") @Column(name = "mai2_unlock_tickets")
var mai2UnlockTickets: Boolean = false,
@SettingField("wacca")
var waccaUnlockMusic: Boolean = false,
@SettingField("wacca")
var waccaUnlockPlates: Boolean = false,
@SettingField("wacca")
var waccaUnlockCollectables: Boolean = false,
@SettingField("wacca")
var waccaUnlockTickets: Boolean = false,
@SettingField("wacca") @SettingField("wacca")
var waccaInfiniteWp: Boolean = false, var waccaInfiniteWp: Boolean = false,
@SettingField("wacca") @SettingField("wacca")
var waccaAlwaysVip: Boolean = false, var waccaAlwaysVip: Boolean = false,

View File

@@ -2,6 +2,8 @@ package icu.samnyan.aqua.net.db
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
import ext.* import ext.*
import icu.samnyan.aqua.net.UserRegistrar.Companion.cardExtIdEnd
import icu.samnyan.aqua.net.UserRegistrar.Companion.cardExtIdStart
import icu.samnyan.aqua.net.components.JWT import icu.samnyan.aqua.net.components.JWT
import icu.samnyan.aqua.sega.allnet.AllNetProps import icu.samnyan.aqua.sega.allnet.AllNetProps
import icu.samnyan.aqua.sega.allnet.KeyChipRepo import icu.samnyan.aqua.sega.allnet.KeyChipRepo
@@ -9,11 +11,13 @@ import icu.samnyan.aqua.sega.allnet.KeychipSession
import icu.samnyan.aqua.sega.general.GameMusicPopularity import icu.samnyan.aqua.sega.general.GameMusicPopularity
import icu.samnyan.aqua.sega.general.dao.CardRepository import icu.samnyan.aqua.sega.general.dao.CardRepository
import icu.samnyan.aqua.sega.general.model.Card import icu.samnyan.aqua.sega.general.model.Card
import icu.samnyan.aqua.sega.general.service.CardService
import jakarta.persistence.* import jakarta.persistence.*
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.io.Serializable import java.io.Serializable
import java.time.LocalDateTime
import kotlin.jvm.optionals.getOrNull import kotlin.jvm.optionals.getOrNull
import kotlin.reflect.KFunction import kotlin.reflect.KFunction
import kotlin.reflect.KMutableProperty import kotlin.reflect.KMutableProperty
@@ -124,7 +128,9 @@ class AquaUserServices(
val allNetProps: AllNetProps, val allNetProps: AllNetProps,
val jwt: JWT, val jwt: JWT,
val em: EntityManager, val em: EntityManager,
val pop: GameMusicPopularity val pop: GameMusicPopularity,
val cardService: CardService,
val sessionRepo: SessionTokenRepo,
) { ) {
companion object { companion object {
val SETTING_FIELDS = AquaUserServices::class.functions val SETTING_FIELDS = AquaUserServices::class.functions
@@ -136,6 +142,43 @@ class AquaUserServices(
} }
} }
fun create(username: Str, email: Str, password: Str, country: Str, emailConfirmed: Boolean = false): AquaNetUser {
// Create user
val u = AquaNetUser(
username = checkUsername(username),
email = validateEmail(email),
pwHash = checkPwHash(password),
regTime = millis(), lastLogin = millis(), country = country,
emailConfirmed = emailConfirmed
)
// Create a ghost card
val card = Card().apply {
extId = cardService.randExtID(cardExtIdStart, cardExtIdEnd)
luid = extId.toString()
registerTime = LocalDateTime.now()
accessTime = registerTime
aquaUser = u
isGhost = true
}
u.ghostCard = card
// Save the user
userRepo.save(u)
cardRepo.save(card)
return u
}
fun update(user: AquaNetUser, key: Str, value: Str) {
// Check if the key is a settable field
val field = SETTING_FIELDS.find { it.name == key } ?: (400 - "Invalid setting")
// Set the validated field
field.setter.call(user, field.checker.call(this, value))
}
fun clearAllSessions(user: AquaNetUser) = sessionRepo.deleteAll(sessionRepo.findByAquaNetUserAuId(user.auId))
suspend fun <T> byName(username: Str, callback: suspend (AquaNetUser) -> T) = suspend fun <T> byName(username: Str, callback: suspend (AquaNetUser) -> T) =
async { userRepo.findByUsernameIgnoreCase(username) }?.let { callback(it) } ?: (404 - "User not found") async { userRepo.findByUsernameIgnoreCase(username) }?.let { callback(it) } ?: (404 - "User not found")
@@ -173,7 +216,7 @@ class AquaUserServices(
400 - "User with username `$this` already exists" 400 - "User with username `$this` already exists"
} }
fun checkEmail(email: Str) = email.apply { fun validateEmail(email: Str) = email.apply {
// Check if email is valid // Check if email is valid
if (!isValidEmail()) 400 - "Invalid email" if (!isValidEmail()) 400 - "Invalid email"

View File

@@ -1,29 +0,0 @@
package icu.samnyan.aqua.net.db
import jakarta.persistence.*
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
import java.io.Serializable
import java.time.Instant
@Entity
@Table(name = "aqua_net_user_fedy")
class AquaNetUserFedy(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long = 0,
@Column(nullable = false)
var createdAt: Instant = Instant.now(),
// Linking to the AquaNetUser
@OneToOne
@JoinColumn(name = "auId", referencedColumnName = "auId")
var aquaNetUser: AquaNetUser,
) : Serializable
@Repository
interface AquaNetUserFedyRepo : JpaRepository<AquaNetUserFedy, Long> {
fun findByAquaNetUserAuId(auId: Long): AquaNetUserFedy?
fun deleteByAquaNetUserAuId(auId: Long): Unit
}

View File

@@ -5,7 +5,7 @@ import icu.samnyan.aqua.net.BotProps
import icu.samnyan.aqua.net.db.AquaUserServices import icu.samnyan.aqua.net.db.AquaUserServices
import icu.samnyan.aqua.net.utils.SUCCESS import icu.samnyan.aqua.net.utils.SUCCESS
import icu.samnyan.aqua.sega.general.model.Card import icu.samnyan.aqua.sega.general.model.Card
import icu.samnyan.aqua.sega.general.model.CardStatus import icu.samnyan.aqua.sega.general.service.CardService
import jakarta.annotation.PostConstruct import jakarta.annotation.PostConstruct
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
@@ -27,9 +27,12 @@ abstract class GameApiController<T : IUserData>(val name: String, userDataClass:
abstract val playlogRepo: GenericPlaylogRepo<*> abstract val playlogRepo: GenericPlaylogRepo<*>
abstract val userMusicRepo: GenericUserMusicRepo<*> abstract val userMusicRepo: GenericUserMusicRepo<*>
abstract val shownRanks: List<Pair<Int, String>> abstract val shownRanks: List<Pair<Int, String>>
abstract val settableFields: Map<String, (T, String) -> Unit> abstract val settableFields: Map<String, (T, String) -> Unit>
open val gettableFields: Set<String> = setOf() open val gettableFields: Set<String> = setOf()
@Autowired lateinit var cardService: CardService
@API("trend") @API("trend")
abstract suspend fun trend(@RP username: String): List<TrendOut> abstract suspend fun trend(@RP username: String): List<TrendOut>
@API("user-summary") @API("user-summary")
@@ -56,7 +59,7 @@ abstract class GameApiController<T : IUserData>(val name: String, userDataClass:
val reqUser = token?.let { us.jwt.auth(it) }?.let { u -> val reqUser = token?.let { us.jwt.auth(it) }?.let { u ->
// Optimization: If the user is not banned, we don't need to process user information // Optimization: If the user is not banned, we don't need to process user information
if (!u.ghostCard.rankingBanned && !u.cards.any { it.rankingBanned } && u.ghostCard.status == CardStatus.NORMAL) null if (!u.ghostCard.rankingBanned && !u.cards.any { it.rankingBanned } && u.ghostCard.status.isNormal) null
else u else u
} }
@@ -124,13 +127,11 @@ abstract class GameApiController<T : IUserData>(val name: String, userDataClass:
(settableFields.keys.toSet() + gettableFields) (settableFields.keys.toSet() + gettableFields)
.associateWith { k -> (vm[k] ?: error("Field $k not found")) } .associateWith { k -> (vm[k] ?: error("Field $k not found")) }
} } } }
@API("user-detail") @API("user-detail")
suspend fun userDetail(@RP username: String) = us.cardByName(username) { card -> suspend fun userDetail(@RP username: String) = us.cardByName(username) { card ->
val u = userDataRepo.findByCard(card) ?: (404 - "User not found") val u = userDataRepo.findByCard(card) ?: (404 - "User not found")
userDetailFields.toList().associate { (k, f) -> k to f.invoke(u) } userDetailFields.toList().associate { (k, f) -> k to f.invoke(u) }
} }
@API("user-detail-set") @API("user-detail-set")
suspend fun userDetailSet(@RP token: String, @RP field: String, @RP value: String): Any { suspend fun userDetailSet(@RP token: String, @RP field: String, @RP value: String): Any {
val prop = settableFields[field] ?: (400 - "Invalid field $field") val prop = settableFields[field] ?: (400 - "Invalid field $field")
@@ -139,16 +140,22 @@ abstract class GameApiController<T : IUserData>(val name: String, userDataClass:
val user = async { userDataRepo.findByCard(u.ghostCard) } ?: (404 - "User not found") val user = async { userDataRepo.findByCard(u.ghostCard) } ?: (404 - "User not found")
prop(user, value) prop(user, value)
async { userDataRepo.save(user) } async { userDataRepo.save(user) }
cardService.updateCardTimestamp(u.ghostCard, name)
SUCCESS SUCCESS
} }
} }
@API("user-option")
open suspend fun userOption(@RP token: String): Any? = 400 - "Unsupported by this game"
@API("user-option-set")
open suspend fun userOptionSet(@RP token: String, @RP field: String, @RP value: Int): Any = 400 - "Unsupported by this game"
@API("user-music-from-list") @API("user-music-from-list")
suspend fun userMusicFromList(@RP username: Str, @RB musicList: List<Int>) = us.cardByName(username) { card -> suspend fun userMusicFromList(@RP username: Str, @RB musicList: List<Int>) = us.cardByName(username) { card ->
userMusicRepo.findByUser_Card_ExtIdAndMusicIdIn(card.extId, musicList) userMusicRepo.findByUser_Card_ExtIdAndMusicIdIn(card.extId, musicList)
} }
fun genericUserSummary(card: Card, ratingComp: Map<String, String>, rival: Boolean? = null): GenericGameSummary { fun genericUserSummary(card: Card, ratingComp: Map<String, String>, rival: Boolean? = null, favorites: List<Int>? = null): GenericGameSummary {
// Summary values: total plays, player rating, server-wide ranking // Summary values: total plays, player rating, server-wide ranking
// number of each rank, max combo, number of full combo, number of all perfect // number of each rank, max combo, number of full combo, number of all perfect
val user = userDataRepo.findByCard(card) ?: (404 - "Game data not found") val user = userDataRepo.findByCard(card) ?: (404 - "Game data not found")
@@ -199,7 +206,8 @@ abstract class GameApiController<T : IUserData>(val name: String, userDataClass:
ratingComposition = ratingComp, ratingComposition = ratingComp,
recent = plays.sortedBy { it.userPlayDate.toString() }.takeLast(100).reversed(), recent = plays.sortedBy { it.userPlayDate.toString() }.takeLast(100).reversed(),
lastPlayedHost = user.lastClientId?.let { us.userRepo.findByKeychip(it)?.username }, lastPlayedHost = user.lastClientId?.let { us.userRepo.findByKeychip(it)?.username },
rival = rival rival = rival,
favorites = favorites
) )
} }

View File

@@ -8,10 +8,10 @@ import java.time.LocalDate
const val LETTERS = "" + const val LETTERS = "" +
"" + "" +
"" ""
const val SYMBOLS = "・:;?!~/+-×÷=♂♀∀#&*@☆○◎◇□△▽♪†‡ΣαβγθφψωДё$()._" const val SYMBOLS = "・:;?!~/+-×÷=♂♀∀#&*@☆○◎◇□△▽♪†‡ΣαβγθφψωДё$()._ "
const val KANA = "あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわをん" + const val KANA = "あいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわをん" +
"アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン" "アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン"
const val SEGA_USERNAME_CAHRS = LETTERS + SYMBOLS + KANA const val SEGA_USERNAME_CHARS = LETTERS + SYMBOLS + KANA
const val WACCA_USERNAME_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + const val WACCA_USERNAME_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
"abcdefghijklmnopqrstuvwxyz" + "abcdefghijklmnopqrstuvwxyz" +
"0123456789" + "0123456789" +

View File

@@ -6,6 +6,8 @@ import icu.samnyan.aqua.net.db.AquaUserServices
import icu.samnyan.aqua.net.Fedy import icu.samnyan.aqua.net.Fedy
import icu.samnyan.aqua.net.utils.AquaNetProps import icu.samnyan.aqua.net.utils.AquaNetProps
import icu.samnyan.aqua.net.utils.SUCCESS import icu.samnyan.aqua.net.utils.SUCCESS
import icu.samnyan.aqua.sega.general.model.Card
import icu.samnyan.aqua.sega.general.service.CardService
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.repository.NoRepositoryBean import org.springframework.data.repository.NoRepositoryBean
@@ -16,7 +18,6 @@ import java.util.*
import kotlin.io.path.Path import kotlin.io.path.Path
import kotlin.io.path.writeText import kotlin.io.path.writeText
import kotlin.reflect.KClass import kotlin.reflect.KClass
import org.springframework.context.annotation.Lazy
data class ExportOptions( data class ExportOptions(
val playlogAfter: String? = null val playlogAfter: String? = null
@@ -49,6 +50,7 @@ interface IUserRepo<UserModel, ThisModel>: JpaRepository<ThisModel, Long> {
* Import controller for a game * Import controller for a game
* *
* @param game: 4-letter Game ID * @param game: 4-letter Game ID
* @param gameName: mai2/chu3/ongeki
* @param exportFields: Mapping of type names to variables in the export model * @param exportFields: Mapping of type names to variables in the export model
* (e.g. "Mai2UserCharacter" -> Mai2DataExport::userCharacterList) * (e.g. "Mai2UserCharacter" -> Mai2DataExport::userCharacterList)
* @param exportRepos: Mapping of variables to repositories that can be used to find the data * @param exportRepos: Mapping of variables to repositories that can be used to find the data
@@ -56,6 +58,7 @@ interface IUserRepo<UserModel, ThisModel>: JpaRepository<ThisModel, Long> {
*/ */
abstract class ImportController<ExportModel: IExportClass<UserModel>, UserModel: IUserData>( abstract class ImportController<ExportModel: IExportClass<UserModel>, UserModel: IUserData>(
val game: String, val game: String,
val gameName: String,
val exportClass: KClass<ExportModel>, val exportClass: KClass<ExportModel>,
val exportFields: Map<String, Var<ExportModel, Any>>, val exportFields: Map<String, Var<ExportModel, Any>>,
val exportRepos: Map<Var<ExportModel, Any>, IUserRepo<UserModel, *>>, val exportRepos: Map<Var<ExportModel, Any>, IUserRepo<UserModel, *>>,
@@ -70,7 +73,7 @@ abstract class ImportController<ExportModel: IExportClass<UserModel>, UserModel:
@Autowired lateinit var netProps: AquaNetProps @Autowired lateinit var netProps: AquaNetProps
@Autowired lateinit var transManager: PlatformTransactionManager @Autowired lateinit var transManager: PlatformTransactionManager
val trans by lazy { TransactionTemplate(transManager) } val trans by lazy { TransactionTemplate(transManager) }
@Autowired @Lazy lateinit var fedy: Fedy @Autowired lateinit var cardService: CardService
init { init {
artemisRenames.values.forEach { artemisRenames.values.forEach {
@@ -81,11 +84,11 @@ abstract class ImportController<ExportModel: IExportClass<UserModel>, UserModel:
val listRepos = exportRepos.filter { it.key returns List::class } val listRepos = exportRepos.filter { it.key returns List::class }
val singleRepos = exportRepos.filter { !(it.key returns List::class) } val singleRepos = exportRepos.filter { !(it.key returns List::class) }
fun export(u: AquaNetUser): ExportModel = export(u, ExportOptions()) fun export(u: AquaNetUser): ExportModel = export(u.ghostCard, ExportOptions())
fun export(u: AquaNetUser, options: ExportOptions) = createEmpty().apply { fun export(c: Card, options: ExportOptions) = createEmpty().apply {
gameId = game gameId = game
userData = userDataRepo.findByCard(u.ghostCard) ?: (404 - "User not found") userData = userDataRepo.findByCard(c) ?: (404 - "User not found")
exportRepos.forEach { (f, u) -> exportRepos.forEach { (f, u) ->
if (f returns List::class) f.set(this, u.findByUser(userData)) if (f returns List::class) f.set(this, u.findByUser(userData))
else u.findSingleByUser(userData)()?.let { f.set(this, it) } else u.findSingleByUser(userData)()?.let { f.set(this, it) }
@@ -147,7 +150,7 @@ abstract class ImportController<ExportModel: IExportClass<UserModel>, UserModel:
} }
} }
Fedy.getGameName(game)?.let { fedy.onImported(it, u.ghostCard.extId) } cardService.updateCardTimestamp(u.ghostCard, gameName, resetCreatedAt = true)
SUCCESS SUCCESS
} }

View File

@@ -47,7 +47,8 @@ data class GenericGameSummary(
val recent: List<IGenericGamePlaylog>, val recent: List<IGenericGamePlaylog>,
val rival: Boolean? val rival: Boolean?,
val favorites: List<Int>?
) )
data class GenericRankingPlayer( data class GenericRankingPlayer(

View File

@@ -17,7 +17,7 @@ import kotlin.reflect.full.declaredMembers
class Chu3Import( class Chu3Import(
val repos: Chu3Repos, val repos: Chu3Repos,
) : ImportController<Chu3DataExport, Chu3UserData>( ) : ImportController<Chu3DataExport, Chu3UserData>(
"SDHD", Chu3DataExport::class, "SDHD", "chu3", Chu3DataExport::class,
exportFields = Chu3DataExport::class.vars().associateBy { exportFields = Chu3DataExport::class.vars().associateBy {
it.name.replace("List", "").lowercase() it.name.replace("List", "").lowercase()
}, },

View File

@@ -6,7 +6,12 @@ import icu.samnyan.aqua.net.games.*
import icu.samnyan.aqua.net.utils.* import icu.samnyan.aqua.net.utils.*
import icu.samnyan.aqua.sega.chusan.model.* import icu.samnyan.aqua.sega.chusan.model.*
import icu.samnyan.aqua.sega.chusan.model.userdata.Chu3UserData import icu.samnyan.aqua.sega.chusan.model.userdata.Chu3UserData
import icu.samnyan.aqua.sega.chusan.model.userdata.UserGameOption
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import kotlin.jvm.optionals.getOrNull
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.memberProperties
@RestController @RestController
@API("api/v2/game/chu3") @API("api/v2/game/chu3")
@@ -25,7 +30,7 @@ class Chusan(
// Only show > AAA rank // Only show > AAA rank
override val shownRanks = chu3Scores.filter { it.first >= 95 * 10000 } override val shownRanks = chu3Scores.filter { it.first >= 95 * 10000 }
override val settableFields: Map<String, (Chu3UserData, String) -> Unit> by lazy { mapOf( override val settableFields: Map<String, (Chu3UserData, String) -> Unit> by lazy { mapOf(
"userName" to usernameCheck(SEGA_USERNAME_CAHRS), "userName" to usernameCheck(SEGA_USERNAME_CHARS),
"nameplateId" to { u, v -> u.nameplateId = v.int }, "nameplateId" to { u, v -> u.nameplateId = v.int },
"frameId" to { u, v -> u.frameId = v.int }, "frameId" to { u, v -> u.frameId = v.int },
"trophyId" to { u, v -> u.trophyId = v.int }, "trophyId" to { u, v -> u.trophyId = v.int },
@@ -33,6 +38,7 @@ class Chusan(
"trophyIdSub2" to { u, v -> u.trophyIdSub2 = v.int }, "trophyIdSub2" to { u, v -> u.trophyIdSub2 = v.int },
"mapIconId" to { u, v -> u.mapIconId = v.int }, "mapIconId" to { u, v -> u.mapIconId = v.int },
"voiceId" to { u, v -> u.voiceId = v.int }, "voiceId" to { u, v -> u.voiceId = v.int },
"characterId" to { u, v -> u.characterId = v.int },
"avatarWear" to { u, v -> u.avatarWear = v.int }, "avatarWear" to { u, v -> u.avatarWear = v.int },
"avatarHead" to { u, v -> u.avatarHead = v.int }, "avatarHead" to { u, v -> u.avatarHead = v.int },
"avatarFace" to { u, v -> u.avatarFace = v.int }, "avatarFace" to { u, v -> u.avatarFace = v.int },
@@ -44,7 +50,7 @@ class Chusan(
"lastRomVersion" to { u, v -> u.lastRomVersion = v }, "lastRomVersion" to { u, v -> u.lastRomVersion = v },
"lastDataVersion" to { u, v -> u.lastDataVersion = v }, "lastDataVersion" to { u, v -> u.lastDataVersion = v },
) } ) }
override val gettableFields: Set<String> = setOf("level", "playerRating", "characterId") override val gettableFields: Set<String> = setOf("level", "playerRating")
override suspend fun userSummary(@RP username: Str, @RP token: String?) = us.cardByName(username) { card -> override suspend fun userSummary(@RP username: Str, @RP token: String?) = us.cardByName(username) { card ->
// Summary values: total plays, player rating, server-wide ranking // Summary values: total plays, player rating, server-wide ranking
@@ -60,7 +66,9 @@ class Chusan(
"new" to (extra["rating_new_list"] ?: ""), "new" to (extra["rating_new_list"] ?: ""),
) )
genericUserSummary(card, ratingComposition) val misc = rp.userMisc.findByUser_Card_ExtId(card.extId).firstOrNull()
genericUserSummary(card, ratingComposition, null, misc?.favMusic)
} }
/** /**
@@ -92,6 +100,23 @@ class Chusan(
) )
} }
@API("user-option")
override suspend fun userOption(@RP token: String): Any? = us.jwt.auth(token) { u ->
rp.userGameOption.findByUser_Card_ExtId(u.ghostCard.extId).getOrNull(0)
}
@API("user-option-set")
override suspend fun userOptionSet(@RP token: String, @RP field: String, @RP value: Int): Any = us.jwt.auth(token) { u ->
val gameOptions = rp.userGameOption.findSingleByUser_Card_ExtId(u.ghostCard.extId).getOrNull()
val property = UserGameOption::class.memberProperties.filterIsInstance<KMutableProperty1<Any, Any?>>().find{ it.name == field }
if (property != null && gameOptions != null) {
property.setter.call(gameOptions, value)
rp.userGameOption.save(gameOptions)
200 - "Success"
} else
400 - "Invalid parameters"
}
// UserBox related APIs // UserBox related APIs
@API("user-box") @API("user-box")
fun userBox(@RP token: String) = us.jwt.auth(token) { fun userBox(@RP token: String) = us.jwt.auth(token) {

View File

@@ -19,7 +19,7 @@ import kotlin.reflect.full.declaredMembers
class Mai2Import( class Mai2Import(
val repos: Mai2Repos, val repos: Mai2Repos,
) : ImportController<Maimai2DataExport, Mai2UserDetail>( ) : ImportController<Maimai2DataExport, Mai2UserDetail>(
"SDEZ", Maimai2DataExport::class, "SDEZ", "mai2", Maimai2DataExport::class,
exportFields = Maimai2DataExport::class.vars().associateBy { exportFields = Maimai2DataExport::class.vars().associateBy {
it.name.replace("List", "").lowercase() it.name.replace("List", "").lowercase()
}, },

View File

@@ -3,6 +3,7 @@ package icu.samnyan.aqua.net.games.mai2
import ext.* import ext.*
import icu.samnyan.aqua.net.db.AquaUserServices import icu.samnyan.aqua.net.db.AquaUserServices
import icu.samnyan.aqua.net.utils.SUCCESS import icu.samnyan.aqua.net.utils.SUCCESS
import icu.samnyan.aqua.sega.general.service.CardService
import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserMusicDetail import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserMusicDetail
import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PostMapping
@@ -13,6 +14,7 @@ import org.springframework.web.bind.annotation.RestController
class Mai2MusicDetailImport( class Mai2MusicDetailImport(
val us: AquaUserServices, val us: AquaUserServices,
val repos: Mai2Repos, val repos: Mai2Repos,
val cardService: CardService,
) { ) {
@PostMapping("import-music-detail") @PostMapping("import-music-detail")
suspend fun importMusicDetail(@RP token: String, @RB data: List<Mai2UserMusicDetail>) = us.jwt.auth(token) { u -> suspend fun importMusicDetail(@RP token: String, @RB data: List<Mai2UserMusicDetail>) = us.jwt.auth(token) { u ->
@@ -39,6 +41,7 @@ class Mai2MusicDetailImport(
} }
} }
repos.userMusicDetail.saveAll(data) repos.userMusicDetail.saveAll(data)
cardService.updateCardTimestamp(card, "mai2")
SUCCESS SUCCESS
} }
} }

View File

@@ -11,6 +11,9 @@ import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import java.util.* import java.util.*
import kotlin.jvm.optionals.getOrNull
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.full.memberProperties
@RestController @RestController
@API("api/v2/game/mai2") @API("api/v2/game/mai2")
@@ -29,7 +32,7 @@ class Maimai2(
// Only show > S rank // Only show > S rank
override val shownRanks = mai2Scores.filter { it.first >= 97 * 10000 } override val shownRanks = mai2Scores.filter { it.first >= 97 * 10000 }
override val settableFields: Map<String, (Mai2UserDetail, String) -> Unit> by lazy { mapOf( override val settableFields: Map<String, (Mai2UserDetail, String) -> Unit> by lazy { mapOf(
"userName" to usernameCheck(SEGA_USERNAME_CAHRS), "userName" to usernameCheck(SEGA_USERNAME_CHARS),
"iconId" to { u, v -> u.iconId = v.int() }, "iconId" to { u, v -> u.iconId = v.int() },
"plateId" to { u, v -> u.plateId = v.int() }, "plateId" to { u, v -> u.plateId = v.int() },
"titleId" to { u, v -> u.titleId = v.int() }, "titleId" to { u, v -> u.titleId = v.int() },
@@ -66,7 +69,7 @@ class Maimai2(
} }
} }
genericUserSummary(card, ratingComposition, isMyRival) genericUserSummary(card, ratingComposition, isMyRival, extra["favorite_music"]?.split(",")?.mapNotNull{it -> it.toIntOrNull()})
} }
@API("user-rating") @API("user-rating")
@@ -110,6 +113,7 @@ class Maimai2(
val user = userDataRepo.findByCard(card) ?: (404 - "User not found") val user = userDataRepo.findByCard(card) ?: (404 - "User not found")
user.userName = newNameFull user.userName = newNameFull
userDataRepo.save(user) userDataRepo.save(user)
cardService.updateCardTimestamp(card, "mai2")
} }
mapOf("newName" to newNameFull) mapOf("newName" to newNameFull)
} }
@@ -139,10 +143,29 @@ class Maimai2(
loginBonus.add(newBonus) loginBonus.add(newBonus)
} }
repos.userLoginBonus.saveAll(loginBonus) repos.userLoginBonus.saveAll(loginBonus)
cardService.updateCardTimestamp(card, "mai2")
} }
SUCCESS SUCCESS
} }
@API("user-option")
override suspend fun userOption(@RP token: String) = us.jwt.auth(token) { u ->
repos.userOption.findByUser_Card_ExtId(u.ghostCard.extId).getOrNull(0)
}
@API("user-option-set")
override suspend fun userOptionSet(@RP token: String, @RP field: String, @RP value: Int): Any = us.jwt.auth(token) { u ->
val gameOptions = repos.userOption.findSingleByUser_Card_ExtId(u.ghostCard.extId).getOrNull()
val property = Mai2UserOption::class.memberProperties.filterIsInstance<KMutableProperty1<Any, Any?>>().find{ it.name == field }
if (property != null && gameOptions != null) {
property.setter.call(gameOptions, value)
repos.userOption.save(gameOptions)
200 - "Success"
} else
400 - "Invalid parameters"
}
@API("owned-items") @API("owned-items")
suspend fun ownedItems(@RP token: String) = us.jwt.auth(token) { u -> suspend fun ownedItems(@RP token: String) = us.jwt.auth(token) { u ->
us.cardByName(u.username) { card -> us.cardByName(u.username) { card ->
@@ -172,6 +195,7 @@ class Maimai2(
myRival.propertyValue = myRivalList.joinToString(",") myRival.propertyValue = myRivalList.joinToString(",")
repos.userGeneralData.save(myRival) repos.userGeneralData.save(myRival)
cardService.updateCardTimestamp(myCard, "mai2")
} }
SUCCESS SUCCESS
} }

View File

@@ -1,15 +1,23 @@
package icu.samnyan.aqua.net.games.ongeki package icu.samnyan.aqua.net.games.ongeki
import ext.API import ext.API
import ext.RP
import ext.minus
import icu.samnyan.aqua.net.db.AquaUserServices import icu.samnyan.aqua.net.db.AquaUserServices
import icu.samnyan.aqua.net.games.* import icu.samnyan.aqua.net.games.*
import icu.samnyan.aqua.net.utils.* import icu.samnyan.aqua.net.utils.*
import icu.samnyan.aqua.sega.ongeki.OgkUserDataRepo import icu.samnyan.aqua.sega.ongeki.OgkUserDataRepo
import icu.samnyan.aqua.sega.ongeki.OgkUserGeneralDataRepo import icu.samnyan.aqua.sega.ongeki.OgkUserGeneralDataRepo
import icu.samnyan.aqua.sega.ongeki.OgkUserMusicDetailRepo import icu.samnyan.aqua.sega.ongeki.OgkUserMusicDetailRepo
import icu.samnyan.aqua.sega.ongeki.OgkUserOptionRepo
import icu.samnyan.aqua.sega.ongeki.OgkUserPlaylogRepo import icu.samnyan.aqua.sega.ongeki.OgkUserPlaylogRepo
import icu.samnyan.aqua.sega.ongeki.model.UserData import icu.samnyan.aqua.sega.ongeki.model.UserData
import icu.samnyan.aqua.sega.ongeki.model.UserOption
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import kotlin.jvm.optionals.getOrNull
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.full.memberProperties
@RestController @RestController
@API("api/v2/game/ongeki") @API("api/v2/game/ongeki")
@@ -18,7 +26,8 @@ class Ongeki(
override val playlogRepo: OgkUserPlaylogRepo, override val playlogRepo: OgkUserPlaylogRepo,
override val userDataRepo: OgkUserDataRepo, override val userDataRepo: OgkUserDataRepo,
override val userMusicRepo: OgkUserMusicDetailRepo, override val userMusicRepo: OgkUserMusicDetailRepo,
val userGeneralDataRepository: OgkUserGeneralDataRepo val userGeneralDataRepository: OgkUserGeneralDataRepo,
val userOptionRepo: OgkUserOptionRepo
): GameApiController<UserData>("ongeki", UserData::class) { ): GameApiController<UserData>("ongeki", UserData::class) {
override suspend fun trend(username: String) = us.cardByName(username) { card -> override suspend fun trend(username: String) = us.cardByName(username) { card ->
findTrend(playlogRepo.findByUser_Card_ExtId(card.extId) findTrend(playlogRepo.findByUser_Card_ExtId(card.extId)
@@ -27,7 +36,7 @@ class Ongeki(
override val shownRanks = ongekiScores.filter { it.first >= 950000 } override val shownRanks = ongekiScores.filter { it.first >= 950000 }
override val settableFields: Map<String, (UserData, String) -> Unit> by lazy { mapOf( override val settableFields: Map<String, (UserData, String) -> Unit> by lazy { mapOf(
"userName" to usernameCheck(SEGA_USERNAME_CAHRS), "userName" to usernameCheck(SEGA_USERNAME_CHARS),
"lastRomVersion" to { u, v -> u.lastRomVersion = v }, "lastRomVersion" to { u, v -> u.lastRomVersion = v },
"lastDataVersion" to { u, v -> u.lastDataVersion = v }, "lastDataVersion" to { u, v -> u.lastDataVersion = v },
@@ -45,4 +54,21 @@ class Ongeki(
genericUserSummary(card, ratingComposition) genericUserSummary(card, ratingComposition)
} }
@API("user-option")
override suspend fun userOption(@RP token: String) = us.jwt.auth(token) { u ->
userOptionRepo.findByUser_Card_ExtId(u.ghostCard.extId).getOrNull(0)
}
@API("user-option-set")
override suspend fun userOptionSet(@RP token: String, @RP field: String, @RP value: Int): Any = us.jwt.auth(token) { u ->
val gameOptions = userOptionRepo.findSingleByUser_Card_ExtId(u.ghostCard.extId).getOrNull()
val property = UserOption::class.memberProperties.filterIsInstance<KMutableProperty1<Any, Any?>>().find{ it.name == field }
if (property != null && gameOptions != null) {
property.setter.call(gameOptions, value)
userOptionRepo.save(gameOptions)
200 - "Success"
} else
400 - "Invalid parameters"
}
} }

View File

@@ -31,7 +31,10 @@ class Wacca(
override suspend fun userSummary(@RP username: String, @RP token: String?) = us.cardByName(username) { card -> override suspend fun userSummary(@RP username: String, @RP token: String?) = us.cardByName(username) { card ->
// TODO: Rating composition // TODO: Rating composition
genericUserSummary(card, mapOf())
val data = userDataRepo.findByCard_ExtId(card.extId)
genericUserSummary(card, mapOf(), null, if (data.isPresent) data.get().favoriteSongs else null)
} }
override val shownRanks: List<Pair<Int, String>> = waccaScores.filter { it.first > 85 * 10000 } override val shownRanks: List<Pair<Int, String>> = waccaScores.filter { it.first > 85 * 10000 }

View File

@@ -1,11 +1,10 @@
package icu.samnyan.aqua.sega.aimedb package icu.samnyan.aqua.sega.aimedb
import ext.* import ext.*
import icu.samnyan.aqua.net.BotProps import icu.samnyan.aqua.net.Fedy
import icu.samnyan.aqua.net.db.AquaUserServices import icu.samnyan.aqua.net.db.AquaUserServices
import icu.samnyan.aqua.sega.allnet.AllNetProps import icu.samnyan.aqua.sega.allnet.AllNetProps
import icu.samnyan.aqua.sega.general.model.Card import icu.samnyan.aqua.sega.general.model.Card
import icu.samnyan.aqua.sega.general.model.CardStatus
import icu.samnyan.aqua.sega.general.service.CardService import icu.samnyan.aqua.sega.general.service.CardService
import io.netty.buffer.ByteBuf import io.netty.buffer.ByteBuf
import io.netty.buffer.ByteBufUtil import io.netty.buffer.ByteBufUtil
@@ -26,6 +25,7 @@ class AimeDB(
val cardService: CardService, val cardService: CardService,
val us: AquaUserServices, val us: AquaUserServices,
val allNetProps: AllNetProps, val allNetProps: AllNetProps,
val fedy: Fedy,
): ChannelInboundHandlerAdapter() { ): ChannelInboundHandlerAdapter() {
val logger = logger() val logger = logger()
@@ -68,9 +68,9 @@ class AimeDB(
*/ */
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
if (msg !is Map<*, *>) return if (msg !is Map<*, *>) return
try {
val type = msg["type"] as Int val type = msg["type"] as Int
val data = msg["data"] as ByteBuf val data = msg["data"] as ByteBuf
try {
val base = data.decodeHeader() val base = data.decodeHeader()
val handler = handlers[type] ?: return logger.error("AimeDB: Unknown request type 0x${type.toString(16)}") val handler = handlers[type] ?: return logger.error("AimeDB: Unknown request type 0x${type.toString(16)}")
@@ -89,6 +89,7 @@ class AimeDB(
handler.fn(data)?.let { ctx.write(it) } handler.fn(data)?.let { ctx.write(it) }
} finally { } finally {
data.release()
ctx.flush() ctx.flush()
ctx.close() ctx.close()
} }
@@ -200,6 +201,8 @@ class AimeDB(
status = 1 status = 1
aimeId = card.extId aimeId = card.extId
fedy.onCardCreated(luid, card.extId)
} }
else logger.warn("> Duplicated Aime Card Register detected, access code: $luid") else logger.warn("> Duplicated Aime Card Register detected, access code: $luid")

View File

@@ -10,6 +10,7 @@ import icu.samnyan.aqua.sega.chusan.model.Chu3Repos
import icu.samnyan.aqua.sega.general.GameMusicPopularity import icu.samnyan.aqua.sega.general.GameMusicPopularity
import icu.samnyan.aqua.sega.general.MeowApi import icu.samnyan.aqua.sega.general.MeowApi
import icu.samnyan.aqua.sega.general.RequestContext import icu.samnyan.aqua.sega.general.RequestContext
import icu.samnyan.aqua.sega.general.service.CardService
import icu.samnyan.aqua.sega.util.jackson.BasicMapper import icu.samnyan.aqua.sega.util.jackson.BasicMapper
import icu.samnyan.aqua.sega.util.jackson.StringMapper import icu.samnyan.aqua.sega.util.jackson.StringMapper
import icu.samnyan.aqua.spring.Metrics import icu.samnyan.aqua.spring.Metrics
@@ -29,13 +30,14 @@ class ChusanController(
val cmMapper: BasicMapper, val cmMapper: BasicMapper,
val db: Chu3Repos, val db: Chu3Repos,
val us: AquaUserServices, val us: AquaUserServices,
val cardService: CardService,
val versionHelper: ChusanVersionHelper, val versionHelper: ChusanVersionHelper,
val props: ChusanProps, val props: ChusanProps,
val pop: GameMusicPopularity, val pop: GameMusicPopularity,
val chusan: Chusan val chusan: Chusan
): MeowApi({ api, resp -> ): MeowApi({ api, resp ->
if (resp is String) resp if (resp is String) resp
else (if ("CM" in api) cmMapper else mapper).write(resp) else (if ("CM" in api || api == "GetUserItemApi" ) cmMapper else mapper).write(resp)
}) { }) {
val log = LoggerFactory.getLogger(ChusanController::class.java) val log = LoggerFactory.getLogger(ChusanController::class.java)

View File

@@ -236,9 +236,7 @@ fun ChusanController.chusanInit() {
) + userDict ) + userDict
if (user.card?.status == CardStatus.MIGRATED_TO_MINATO) { if (user.card?.status == CardStatus.MIGRATED_TO_MINATO) {
res["userName"] = "JiaQQqun / CardMigrated" res["userName"] = "${res["userName"]}"
res["rating"] = 0
res["playerLevel"] = 0
} }
res res
@@ -394,13 +392,6 @@ fun ChusanController.chusanInit() {
// } // }
// process() // process()
val user = db.userData.findByCard_ExtId(uid)()
if (user?.card?.status == CardStatus.MIGRATED_TO_MINATO) {
"""{"returnCode":"0"}"""
}
else {
"""{"returnCode":"1"}""" """{"returnCode":"1"}"""
} }
} }
}

View File

@@ -54,23 +54,48 @@ fun ChusanController.cmApiInit() {
db.userGacha.save(it) db.userGacha.save(it)
} }
//Set the hasCompleted to true
val printState = db.userCardPrintState.findByUserAndGachaIdAndHasCompleted(u, gachaId, false)
if (printState.isEmpty()) return@api null
printState.forEach { it.hasCompleted = true }
db.userCardPrintState.saveAll(printState)
// Append the order ID to the response with the key "orderId"
val fullPrintState = printState.map {
mapOf(
"orderId" to it.id,
"hasCompleted" to it.hasCompleted,
"limitDate" to it.limitDate.toString(),
"placeId" to it.placeId,
"cardId" to it.cardId,
"gachaId" to it.gachaId,
"userId" to uid
)
}
u.card?.let { cardService.updateCardTimestamp(it, "chu3") }
mapOf( mapOf(
"returnCode" to 1, "returnCode" to 1,
"apiName" to "CMUpsertUserGachaApi", "apiName" to "CMUpsertUserGachaApi",
"userCardPrintStateList" to db.userCardPrintState.findByUserAndGachaIdAndHasCompleted(u, gachaId, false) "userCardPrintStateList" to fullPrintState
) )
} }
"CMUpsertUserPrintCancel" { "CMUpsertUserPrintCancel" {
val orderIdList: List<Long> = cmMapper.convert<List<Long>>(parsing { data["orderIdList"]!! }) val orderIdList: List<Long> = cmMapper.convert<List<Long>>(parsing { data["orderIdList"]!! })
db.userCardPrintState.saveAll(orderIdList.mapNotNull { val states = db.userCardPrintState.saveAll(orderIdList.mapNotNull {
// TODO: The original code by Eori writes findById but I don't think that is correct... // TODO: The original code by Eori writes findById but I don't think that is correct...
db.userCardPrintState.findById(it)()?.apply { db.userCardPrintState.findById(it)()?.apply {
hasCompleted = true hasCompleted = true
} }
}) })
states.firstOrNull()?.user?.card?.let { cardService.updateCardTimestamp(it, "chu3") }
mapOf("returnCode" to 1, "apiName" to "CMUpsertUserPrintCancelApi") mapOf("returnCode" to 1, "apiName" to "CMUpsertUserPrintCancelApi")
} }
@@ -93,6 +118,8 @@ fun ChusanController.cmApiInit() {
db.userCardPrintState.save(this) db.userCardPrintState.save(this)
} }
u.card?.let { cardService.updateCardTimestamp(it, "chu3") }
mapOf("returnCode" to 1, "apiName" to "CMUpsertUserPrintSubtractApi") mapOf("returnCode" to 1, "apiName" to "CMUpsertUserPrintSubtractApi")
} }
} }

View File

@@ -4,6 +4,7 @@ import ext.*
import icu.samnyan.aqua.sega.chusan.ChusanController import icu.samnyan.aqua.sega.chusan.ChusanController
import icu.samnyan.aqua.sega.chusan.model.request.Chu3UserAll import icu.samnyan.aqua.sega.chusan.model.request.Chu3UserAll
import icu.samnyan.aqua.sega.chusan.model.userdata.* import icu.samnyan.aqua.sega.chusan.model.userdata.*
import icu.samnyan.aqua.sega.general.model.CardStatus
import icu.samnyan.aqua.sega.general.model.response.UserRecentRating import icu.samnyan.aqua.sega.general.model.response.UserRecentRating
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@@ -13,6 +14,7 @@ fun ChusanController.upsertApiInit() {
charge.user = db.userData.findByCard_ExtId(uid)() ?: (400 - "User not found") charge.user = db.userData.findByCard_ExtId(uid)() ?: (400 - "User not found")
charge.id = db.userCharge.findByUser_Card_ExtIdAndChargeId(uid, charge.chargeId)?.id ?: 0 charge.id = db.userCharge.findByUser_Card_ExtIdAndChargeId(uid, charge.chargeId)?.id ?: 0
db.userCharge.save(charge) db.userCharge.save(charge)
charge.user.card?.let { cardService.updateCardTimestamp(it, "chu3") }
"""{"returnCode":"1"}""" """{"returnCode":"1"}"""
} }
@@ -25,10 +27,23 @@ fun ChusanController.upsertApiInit() {
val u = (userData?.get(0) ?: return@api null).apply { val u = (userData?.get(0) ?: return@api null).apply {
id = oldUser?.id ?: 0 id = oldUser?.id ?: 0
card = oldUser?.card ?: us.cardRepo.findByExtId(uid).expect("Card not found") card = oldUser?.card ?: us.cardRepo.findByExtId(uid).expect("Card not found")
userName = userName.fromChusanUsername()
val version = data["version"] as? String ?: "0.00"
val versionNumber = version.toDoubleOrNull() ?: 0.0
userName = if (versionNumber >= 2.40) {
userName
} else {
userName.fromChusanUsername()
}
userNameEx = "" userNameEx = ""
}.also { db.userData.saveAndFlush(it) } }.also { db.userData.saveAndFlush(it) }
// If the user was previously migrated to Minato, saving would mark them "migrated and then cleared".
if (u.card?.status == CardStatus.MIGRATED_TO_MINATO) {
u.card?.status = CardStatus.NORMAL_MIGRATED_TO_MINATO_AND_THEN_CLEARED
us.cardRepo.save(u.card!!)
}
// Only save if it is a valid region and the user has played at least a song // Only save if it is a valid region and the user has played at least a song
req.userPlaylogList?.firstOrNull()?.regionId?.let { rid -> req.userPlaylogList?.firstOrNull()?.regionId?.let { rid ->
val region = db.userRegions.findByUserAndRegionId(u, rid)?.apply { val region = db.userRegions.findByUserAndRegionId(u, rid)?.apply {
@@ -92,10 +107,15 @@ fun ChusanController.upsertApiInit() {
score = it.score score = it.score
} }
val version = data["version"] as? String ?: "0.00"
val versionNumber = version.toDoubleOrNull() ?: 0.0
if (versionNumber < 2.40) {
// 2.40以下版本需要转换编码
selectUserName = selectUserName.fromChusanUsername() selectUserName = selectUserName.fromChusanUsername()
opponentUserName1 = opponentUserName1.fromChusanUsername() opponentUserName1 = opponentUserName1.fromChusanUsername()
opponentUserName2 = opponentUserName2.fromChusanUsername() opponentUserName2 = opponentUserName2.fromChusanUsername()
opponentUserName3 = opponentUserName3.fromChusanUsername() opponentUserName3 = opponentUserName3.fromChusanUsername()
}
}) } }) }
// List data // List data
@@ -173,6 +193,8 @@ fun ChusanController.upsertApiInit() {
}.also { db.userCMissionProgress.save(it) } }.also { db.userCMissionProgress.save(it) }
} }
} }
u.card?.let { cardService.updateCardTimestamp(it, "chu3") }
} }
"""{"returnCode":1}""" """{"returnCode":1}"""

View File

@@ -3,6 +3,10 @@ package icu.samnyan.aqua.sega.chusan.model.userdata
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.annotation.JsonSerialize import com.fasterxml.jackson.databind.annotation.JsonSerialize
import icu.samnyan.aqua.net.games.BaseEntity import icu.samnyan.aqua.net.games.BaseEntity
import icu.samnyan.aqua.net.games.IUserData import icu.samnyan.aqua.net.games.IUserData
@@ -10,8 +14,22 @@ import icu.samnyan.aqua.sega.chusan.model.request.UserEmoney
import icu.samnyan.aqua.sega.general.model.Card import icu.samnyan.aqua.sega.general.model.Card
import icu.samnyan.aqua.sega.util.jackson.AccessCodeSerializer import icu.samnyan.aqua.sega.util.jackson.AccessCodeSerializer
import jakarta.persistence.* import jakarta.persistence.*
import kotlinx.io.IOException
import lombok.NoArgsConstructor import lombok.NoArgsConstructor
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
class FlexibleDateTimeDeserializer : JsonDeserializer<LocalDateTime?>() {
@Throws(IOException::class)
public override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): LocalDateTime {
return LocalDateTime.parse(p.getText(), FORMATTER)
}
companion object {
// Card Maker needs the date ending with ".0" and chunithm sends the dates without it so we need a flexible parser
private val FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss[.S]")
}
}
@Entity(name = "ChusanUserData") @Entity(name = "ChusanUserData")
@Table(name = "chusan_user_data") @Table(name = "chusan_user_data")
@@ -50,12 +68,14 @@ class Chu3UserData : BaseEntity(), IUserData {
var totalExpertHighScore: Long = 0 var totalExpertHighScore: Long = 0
var totalMasterHighScore: Long = 0 var totalMasterHighScore: Long = 0
var totalUltimaHighScore: Long = 0 var totalUltimaHighScore: Long = 0
@JsonDeserialize(using = FlexibleDateTimeDeserializer::class)
var eventWatchedDate: LocalDateTime = LocalDateTime.now() var eventWatchedDate: LocalDateTime = LocalDateTime.now()
var friendCount = 0 var friendCount = 0
var firstGameId: String = "" var firstGameId: String = ""
var firstRomVersion: String = "" var firstRomVersion: String = ""
var firstDataVersion: String = "" var firstDataVersion: String = ""
@JsonDeserialize(using = FlexibleDateTimeDeserializer::class)
override var firstPlayDate: LocalDateTime = LocalDateTime.now() override var firstPlayDate: LocalDateTime = LocalDateTime.now()
var lastGameId: String = "" var lastGameId: String = ""
@@ -65,6 +85,7 @@ class Chu3UserData : BaseEntity(), IUserData {
@JsonIgnore @JsonIgnore
var lastLoginDate: LocalDateTime = LocalDateTime.now() var lastLoginDate: LocalDateTime = LocalDateTime.now()
@JsonDeserialize(using = FlexibleDateTimeDeserializer::class)
override var lastPlayDate: LocalDateTime = LocalDateTime.now() override var lastPlayDate: LocalDateTime = LocalDateTime.now()
var lastPlaceId = 0 var lastPlaceId = 0
var lastPlaceName: String = "" var lastPlaceName: String = ""

View File

@@ -1,5 +1,6 @@
package icu.samnyan.aqua.sega.chusan.model.userdata package icu.samnyan.aqua.sega.chusan.model.userdata
import com.fasterxml.jackson.annotation.JsonIgnore
import jakarta.persistence.Entity import jakarta.persistence.Entity
import jakarta.persistence.Table import jakarta.persistence.Table
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -8,6 +9,8 @@ import java.time.LocalDateTime
@Table(name = "chusan_user_print_state") @Table(name = "chusan_user_print_state")
class UserCardPrintState( class UserCardPrintState(
var hasCompleted: Boolean = false, var hasCompleted: Boolean = false,
@JsonIgnore
var limitDate: LocalDateTime = LocalDateTime.now(), var limitDate: LocalDateTime = LocalDateTime.now(),
var placeId: Int = 0, var placeId: Int = 0,
var cardId: Int = 0, var cardId: Int = 0,

View File

@@ -9,7 +9,7 @@ enum class CardStatus {
NORMAL, NORMAL,
// Reserved for future use // Reserved for future use
NORMAL_RESERVED_1, NORMAL_MIGRATED_TO_MINATO_AND_THEN_CLEARED,
NORMAL_RESERVED_2, NORMAL_RESERVED_2,
NORMAL_RESERVED_3, NORMAL_RESERVED_3,
NORMAL_RESERVED_4, NORMAL_RESERVED_4,
@@ -22,7 +22,12 @@ enum class CardStatus {
// Deleted statuses // Deleted statuses
OVERWRITTEN, OVERWRITTEN,
DELETED, DELETED,
MIGRATED_TO_MINATO, MIGRATED_TO_MINATO;
/**
* Returns true if card status is in any NORMAL state
*/
val isNormal get() = this.ordinal in NORMAL.ordinal..NORMAL_RESERVED_9.ordinal
} }
/** /**

View File

@@ -0,0 +1,32 @@
package icu.samnyan.aqua.sega.general.model
import ext.Str
import jakarta.persistence.*
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
import java.time.Instant
@Entity(name = "SegaCardTimestamp")
@Table(name = "sega_card_timestamp")
class CardTimestamp(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long = 0,
@Column(nullable = false)
var createdAt: Instant = Instant.now(),
@Column(nullable = false)
var updatedAt: Instant = Instant.now(),
var game: Str,
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "card_id")
var card: Card? = null,
)
@Repository
interface CardTimestampRepo : JpaRepository<CardTimestamp, Long> {
fun findByCardIdAndGame(cardId: Long, game: Str): CardTimestamp?
}

View File

@@ -1,10 +1,16 @@
package icu.samnyan.aqua.sega.general.service package icu.samnyan.aqua.sega.general.service
import ext.Bool
import ext.Str
import ext.minus import ext.minus
import icu.samnyan.aqua.net.Fedy
import icu.samnyan.aqua.net.db.AquaNetUser import icu.samnyan.aqua.net.db.AquaNetUser
import icu.samnyan.aqua.sega.general.dao.CardRepository import icu.samnyan.aqua.sega.general.dao.CardRepository
import icu.samnyan.aqua.sega.general.model.Card import icu.samnyan.aqua.sega.general.model.Card
import icu.samnyan.aqua.sega.general.model.CardTimestamp
import icu.samnyan.aqua.sega.general.model.CardTimestampRepo
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.time.Instant
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.* import java.util.*
import java.util.concurrent.ThreadLocalRandom import java.util.concurrent.ThreadLocalRandom
@@ -14,7 +20,7 @@ import kotlin.jvm.optionals.getOrNull
* @author samnyan (privateamusement@protonmail.com) * @author samnyan (privateamusement@protonmail.com)
*/ */
@Service @Service
class CardService(val cardRepo: CardRepository) class CardService(val cardRepo: CardRepository, val cardTimestampRepo: CardTimestampRepo, val fedy: Fedy)
{ {
/** /**
* Find a card by External ID * Find a card by External ID
@@ -106,4 +112,13 @@ class CardService(val cardRepo: CardRepository)
} }
return eid return eid
} }
fun getCardTimestamp(card: Card, game: Str, now: Instant = Instant.now()) =
cardTimestampRepo.findByCardIdAndGame(card.id, game) ?: CardTimestamp(game = game, card = card, createdAt = now, updatedAt = now);
fun updateCardTimestamp(card: Card, game: Str, now: Instant = Instant.now(), resetCreatedAt: Bool = false) {
cardTimestampRepo.save(getCardTimestamp(card, game, now).apply { updatedAt = now }
.apply { if (resetCreatedAt) createdAt = now });
fedy.onDataUpdated(card.extId, game, resetCreatedAt)
}
} }

View File

@@ -9,6 +9,7 @@ import icu.samnyan.aqua.sega.maimai2.model.UserRivalMusicDetail
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserKaleidx import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserKaleidx
import icu.samnyan.aqua.sega.maimai2.model.userdata.UserRegions import icu.samnyan.aqua.sega.maimai2.model.userdata.UserRegions
import java.time.LocalDate import java.time.LocalDate
import kotlin.random.Random
fun Maimai2ServletController.initApis() { fun Maimai2ServletController.initApis() {
val log = logger() val log = logger()
@@ -114,10 +115,7 @@ fun Maimai2ServletController.initApis() {
) )
if (d.card?.status == CardStatus.MIGRATED_TO_MINATO) { if (d.card?.status == CardStatus.MIGRATED_TO_MINATO) {
res["userName"] = "JiaQQqun / CardMigrated" res["userName"] = "${res["userName"]}"
res["dispRate"] = 1
res["playerRating"] = 66564
res["totalAwake"] = 7114
} }
res res
@@ -133,10 +131,6 @@ fun Maimai2ServletController.initApis() {
"Bearer" to "meow", "bearer" to "meow" "Bearer" to "meow", "bearer" to "meow"
) )
if (d?.card?.status == CardStatus.MIGRATED_TO_MINATO) {
res["returnCode"] = 0
}
// Get regionId from request // Get regionId from request
val region = data["regionId"] as? Int val region = data["regionId"] as? Int
@@ -149,6 +143,7 @@ fun Maimai2ServletController.initApis() {
regionId = region regionId = region
} }
db.userRegions.save(region) db.userRegions.save(region)
// d.card?.let { cardService.updateCardTimestamp(it, "mai2") } // TODO: why save regions on login?
} }
res res
@@ -230,12 +225,16 @@ fun Maimai2ServletController.initApis() {
// Kaleidoscope, added on 1.50 // Kaleidoscope, added on 1.50
// [{gateId, phaseId}] // [{gateId, phaseId}]
"GetGameKaleidxScope" { mapOf("gameKaleidxScopeList" to ls( "GetGameKaleidxScope" { mapOf("gameKaleidxScopeList" to ls(
mapOf("gateId" to 1, "phaseId" to findPhase(LocalDate.of(2025, 1, 18))), mapOf("gateId" to 1, "phaseId" to 6),
mapOf("gateId" to 2, "phaseId" to 2), mapOf("gateId" to 2, "phaseId" to 6),
mapOf("gateId" to 3, "phaseId" to 2), mapOf("gateId" to 3, "phaseId" to 6),
mapOf("gateId" to 4, "phaseId" to findPhase(LocalDate.of(2025, 2, 25))), mapOf("gateId" to 4, "phaseId" to 6),
mapOf("gateId" to 5, "phaseId" to 2), mapOf("gateId" to 5, "phaseId" to 6),
mapOf("gateId" to 6, "phaseId" to 2), mapOf("gateId" to 6, "phaseId" to 6),
mapOf("gateId" to 7, "phaseId" to 6),
mapOf("gateId" to 8, "phaseId" to 6),
mapOf("gateId" to 9, "phaseId" to 6),
mapOf("gateId" to 10, "phaseId" to 13),
)) } )) }
// Request: {userId} // Request: {userId}
// Response: {userId, userKaleidxScopeList} // Response: {userId, userKaleidxScopeList}
@@ -312,14 +311,6 @@ fun Maimai2ServletController.initApis() {
) )
} }
"GetServerAnnouncement" static { mapOf(
"title" to "",
"announcement" to "",
"showOnIdle" to false,
"showOnUserLogin" to false,
"imageUrl" to "",
) }
"GetGameWeeklyData" static { mapOf( "GetGameWeeklyData" static { mapOf(
"gameWeeklyData" to mapOf( "gameWeeklyData" to mapOf(
"missionCategory" to 0, "missionCategory" to 0,
@@ -362,4 +353,71 @@ fun Maimai2ServletController.initApis() {
"userRecommendSelectionMusicIdList" to (net.recommendedMusic[user.id] ?: empty) "userRecommendSelectionMusicIdList" to (net.recommendedMusic[user.id] ?: empty)
) )
} }
"GetGameFesta" { mapOf(
"eventId" to 0,
"isRallyPeriod" to false,
"isCircleJoinNotAllowed" to false,
"jackingFestaSideId" to Random.nextInt(0, 3),
"festaSideDataList" to empty,
) }
"GetPlaceCircleData" static { mapOf(
"returnCode" to 0,
"circleId" to 0,
"aggrDate" to ""
) }
"GetUserCircleData" static { mapOf(
"circleId" to 0,
"circleName" to "一緒に歌おう!",
"isPlace" to false,
"circleClass" to 0,
"lastLoginDate" to "",
"circlePointRankingList" to empty
) }
"GetUserCirclePointData" { mapOf(
"userId" to uid,
"aggrDate" to "",
"userCirclePointDataList" to empty
) }
"GetUserCirclePointRanking" static { mapOf(
"circleId" to 0,
"circleName" to "一緒に歌おう!",
"aggrDate" to "",
"lastMonthCircleRank" to 0,
"lastMonthPoint" to 0
) }
"GetUserFesta" static { mapOf(
"userFestaData" to mapOf(
"eventId" to 0,
"circleId" to 0,
"festaSideId" to 0,
"circleTotalFestaPoint" to 0,
"currentTotalFestaPoint" to 0,
"circleRankInFestaSide" to 0,
"circleRecordDate" to "",
"isDailyBonus" to false,
"participationRewardGet" to false,
"receivedRewardBorder" to 0
),
"userResultFestaData" to mapOf(
"eventId" to 0,
"circleId" to 0,
"circleName" to "一緒に歌おう!",
"festaSideId" to 0,
"circleRankInFestaSide" to 0,
"receivedRewardBorder" to 0,
"circleTotalFestaPoint" to 0,
"resultRewardGet" to false,
)
) }
"UpsertUserPlaceCircleRegist" static { mapOf(
"returnCode" to 0,
"apiName" to "UpsertUserPlaceCircleRegistApi"
) }
} }

View File

@@ -40,9 +40,6 @@ class Maimai2ServletController(
val db: Mai2Repos, val db: Mai2Repos,
val net: Maimai2, val net: Maimai2,
): MeowApi(serialize = { _, resp -> if (resp is String) resp else resp.toJson() }) { ): MeowApi(serialize = { _, resp -> if (resp is String) resp else resp.toJson() }) {
@Autowired @Lazy lateinit var fedy: Fedy
companion object { companion object {
private val log = logger() private val log = logger()
private val empty = listOf<Any>() private val empty = listOf<Any>()
@@ -55,7 +52,7 @@ class Maimai2ServletController(
val endpointList = setOf("GetGameRankingApi","GetUserCharacterApi","GetUserItemApi","GetUserPortraitApi", val endpointList = setOf("GetGameRankingApi","GetUserCharacterApi","GetUserItemApi","GetUserPortraitApi",
"GetUserRatingApi","UploadUserPhotoApi","UploadUserPlaylogApi","UploadUserPortraitApi","UpsertUserAllApi", "GetUserRatingApi","UploadUserPhotoApi","UploadUserPlaylogApi","UploadUserPortraitApi","UpsertUserAllApi",
"CMGetUserCardApi","CMGetUserCardPrintErrorApi","CMGetUserDataApi","CMGetUserItemApi","CMUpsertUserPrintApi", "CMGetUserCardApi","CMGetUserCardPrintErrorApi","CMGetUserDataApi","CMGetUserItemApi","CMUpsertUserPrintApi",
"GetUserFavoriteItemApi","GetServerAnnouncementApi") "GetUserFavoriteItemApi")
val noopEndpoint = setOf("GetUserScoreRankingApi", "UpsertClientBookkeepingApi", val noopEndpoint = setOf("GetUserScoreRankingApi", "UpsertClientBookkeepingApi",
"UpsertClientSettingApi", "UpsertClientTestmodeApi", "UpsertClientUploadApi", "Ping", "RemoveTokenApi", "UpsertClientSettingApi", "UpsertClientTestmodeApi", "UpsertClientUploadApi", "Ping", "RemoveTokenApi",
@@ -95,7 +92,6 @@ class Maimai2ServletController(
val ctx = RequestContext(req, data.mut) val ctx = RequestContext(req, data.mut)
serialize(api, handlers[api]!!(ctx) ?: noop).also { serialize(api, handlers[api]!!(ctx) ?: noop).also {
log.info("$token : $api > ${it.truncate(500)}") log.info("$token : $api > ${it.truncate(500)}")
if (api == "UpsertUserAllApi") { fedy.onUpserted("mai2", data["userId"]) }
} }
} }
} catch (e: Exception) { } catch (e: Exception) {

View File

@@ -1,11 +1,14 @@
package icu.samnyan.aqua.sega.maimai2.handler package icu.samnyan.aqua.sega.maimai2.handler
import ext.int
import ext.logger import ext.logger
import ext.mapApply
import icu.samnyan.aqua.net.games.mai2.Maimai2 import icu.samnyan.aqua.net.games.mai2.Maimai2
import icu.samnyan.aqua.sega.general.BaseHandler import icu.samnyan.aqua.sega.general.BaseHandler
import icu.samnyan.aqua.sega.general.dao.CardRepository import icu.samnyan.aqua.sega.general.dao.CardRepository
import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2ItemKind import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2ItemKind
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserCharacter
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import kotlin.jvm.optionals.getOrNull import kotlin.jvm.optionals.getOrNull
@@ -15,34 +18,25 @@ class GetUserCharacterHandler(
val maimai2: Maimai2, val maimai2: Maimai2,
val cardRepo: CardRepository, val cardRepo: CardRepository,
) : BaseHandler { ) : BaseHandler {
val itemUnlock = maimai2.itemMapping[Mai2ItemKind.chara.name]?.map { mapOf( val charaIds = maimai2.itemMapping[Mai2ItemKind.chara.name]?.map { it.key.int() } ?: emptyList()
"characterId" to it.key,
"level" to 9999,
"awakening" to 1,
"useCount" to 0
) }
init { init {
if (itemUnlock.isNullOrEmpty()) logger.warn("Mai2 item info is empty") if (charaIds.isEmpty()) logger.warn("Mai2 item info is empty")
} }
override fun handle(request: Map<String, Any>): Any { override fun handle(request: Map<String, Any>): Any {
val userId = (request["userId"] as Number).toLong() val userId = (request["userId"] as Number).toLong()
val gameOptions = cardRepo.findByExtId(userId).getOrNull()?.aquaUser?.gameOptions
// Aqua Net game unlock feature val userCharacterList = repos.userCharacter.findByUser_Card_ExtId(userId)
cardRepo.findByExtId(userId).getOrNull()?.aquaUser?.gameOptions?.let { opt -> .let { if (gameOptions?.mai2UnlockChara != true) it else (
if (!opt.unlockChara or itemUnlock.isNullOrEmpty()) return@let charaIds.associateWith { Mai2UserCharacter().apply { characterId = it; level = 1 } } +
it.associateBy { it.characterId }
logger.info("Response: ${itemUnlock!!.size} Characters - All unlock") ).values }
return mapOf( .let { if (gameOptions?.mai2UnlockCharaMaxLevel != true) it else it.mapApply { level = 9999 } }
"userId" to userId,
"userCharacterList" to itemUnlock
)
}
return mapOf( return mapOf(
"userId" to userId, "userId" to userId,
"userCharacterList" to repos.userCharacter.findByUser_Card_ExtId(userId) "userCharacterList" to userCharacterList
) )
} }

View File

@@ -50,10 +50,11 @@ class GetUserItemHandler(
// Aqua Net game unlock feature // Aqua Net game unlock feature
cardRepo.findByExtId(userId).getOrNull()?.aquaUser?.gameOptions?.let { opt -> cardRepo.findByExtId(userId).getOrNull()?.aquaUser?.gameOptions?.let { opt ->
val items = when { val items = when {
(kind in 5..8) && opt.unlockMusic -> musicUnlock.getValue(kind) (kind in 5..8) && opt.mai2UnlockMusic -> musicUnlock.getValue(kind)
(kind in 1..3 || kind == 11) && opt.unlockCollectables -> itemUnlock[kind] (kind in 1..3 || kind == 11) && opt.mai2UnlockCollectables -> itemUnlock[kind]
(kind == 12) && opt.unlockTickets -> itemUnlock[kind] (kind == 12) && opt.mai2UnlockTickets -> itemUnlock[kind]
(kind in 9..10) && opt.unlockChara -> itemUnlock[kind] (kind == 9) && opt.mai2UnlockChara -> itemUnlock[kind]
(kind == 10) && opt.mai2UnlockPartners -> itemUnlock[kind]
else -> emptyList() else -> emptyList()
} }

View File

@@ -6,6 +6,7 @@ import ext.millis
import ext.parsing import ext.parsing
import icu.samnyan.aqua.sega.allnet.TokenChecker import icu.samnyan.aqua.sega.allnet.TokenChecker
import icu.samnyan.aqua.sega.general.BaseHandler import icu.samnyan.aqua.sega.general.BaseHandler
import icu.samnyan.aqua.sega.general.service.CardService
import icu.samnyan.aqua.sega.maimai2.model.Mai2UserDataRepo import icu.samnyan.aqua.sega.maimai2.model.Mai2UserDataRepo
import icu.samnyan.aqua.sega.maimai2.model.Mai2UserPlaylogRepo import icu.samnyan.aqua.sega.maimai2.model.Mai2UserPlaylogRepo
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserPlaylog import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserPlaylog
@@ -22,7 +23,8 @@ import kotlin.jvm.optionals.getOrNull
class UploadUserPlaylogHandler( class UploadUserPlaylogHandler(
private val userDataRepository: Mai2UserDataRepo, private val userDataRepository: Mai2UserDataRepo,
private val playlogRepo: Mai2UserPlaylogRepo, private val playlogRepo: Mai2UserPlaylogRepo,
private val mapper: BasicMapper private val mapper: BasicMapper,
private val cardService: CardService
) : BaseHandler { ) : BaseHandler {
data class BacklogEntry(val time: Long, val playlog: Mai2UserPlaylog) data class BacklogEntry(val time: Long, val playlog: Mai2UserPlaylog)
companion object { companion object {
@@ -60,7 +62,10 @@ class UploadUserPlaylogHandler(
// Save if the user is registered // Save if the user is registered
val u = userDataRepository.findByCardExtId(uid).getOrNull() val u = userDataRepository.findByCardExtId(uid).getOrNull()
if (u != null) playlogRepo.save(playlog.apply { user = u }) if (u != null) {
playlogRepo.save(playlog.apply { user = u })
// u.card?.let { cardService.updateCardTimestamp(it, "mai2") } // No need: always followed by an UpsertUserAll
}
// If the user hasn't registered (first play), save the playlog to a backlog // If the user hasn't registered (first play), save the playlog to a backlog
else { else {

View File

@@ -5,11 +5,14 @@ import ext.invoke
import ext.mapApply import ext.mapApply
import ext.unique import ext.unique
import icu.samnyan.aqua.sega.general.BaseHandler import icu.samnyan.aqua.sega.general.BaseHandler
import icu.samnyan.aqua.sega.general.model.CardStatus
import icu.samnyan.aqua.sega.general.service.CardService import icu.samnyan.aqua.sega.general.service.CardService
import icu.samnyan.aqua.sega.maimai2.handler.UploadUserPlaylogHandler.Companion.playBacklog import icu.samnyan.aqua.sega.maimai2.handler.UploadUserPlaylogHandler.Companion.playBacklog
import icu.samnyan.aqua.sega.maimai2.model.* import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos
import icu.samnyan.aqua.sega.maimai2.model.request.Mai2UpsertUserAll import icu.samnyan.aqua.sega.maimai2.model.request.Mai2UpsertUserAll
import icu.samnyan.aqua.sega.maimai2.model.userdata.* import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserDetail
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserGeneralData
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserRate
import icu.samnyan.aqua.sega.util.jackson.BasicMapper import icu.samnyan.aqua.sega.util.jackson.BasicMapper
import lombok.AllArgsConstructor import lombok.AllArgsConstructor
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -25,7 +28,6 @@ class UpsertUserAllHandler(
val cardService: CardService, val cardService: CardService,
val repos: Mai2Repos val repos: Mai2Repos
) : BaseHandler { ) : BaseHandler {
fun String.isValidUsername() = isNotBlank() && length <= 8 fun String.isValidUsername() = isNotBlank() && length <= 8
@Throws(JsonProcessingException::class) @Throws(JsonProcessingException::class)
@@ -59,6 +61,12 @@ class UpsertUserAllHandler(
} }
}) })
// If the user was previously migrated to Minato, saving would mark them "migrated and then cleared".
if (u.card?.status == CardStatus.MIGRATED_TO_MINATO) {
u.card?.status = CardStatus.NORMAL_MIGRATED_TO_MINATO_AND_THEN_CLEARED
cardService.cardRepo.save(u.card!!)
}
// Check playlog backlog // Check playlog backlog
if (playBacklog.containsKey(userId)) playBacklog.remove(userId)?.forEach { if (playBacklog.containsKey(userId)) playBacklog.remove(userId)?.forEach {
repos.userPlaylog.save(it.playlog.apply { user = u }) repos.userPlaylog.save(it.playlog.apply { user = u })
@@ -163,6 +171,8 @@ class UpsertUserAllHandler(
}) })
} }
u.card?.let { cardService.updateCardTimestamp(it, "mai2") }
return SUCCESS return SUCCESS
} }

View File

@@ -5,6 +5,7 @@ import ext.logger
import ext.long import ext.long
import ext.parsing import ext.parsing
import icu.samnyan.aqua.sega.general.BaseHandler import icu.samnyan.aqua.sega.general.BaseHandler
import icu.samnyan.aqua.sega.general.service.CardService
import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserPrintDetail import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserPrintDetail
import icu.samnyan.aqua.sega.util.jackson.BasicMapper import icu.samnyan.aqua.sega.util.jackson.BasicMapper
@@ -18,6 +19,7 @@ import java.util.concurrent.ThreadLocalRandom
class UpsertUserPrintHandler( class UpsertUserPrintHandler(
val mapper: BasicMapper, val mapper: BasicMapper,
val db: Mai2Repos, val db: Mai2Repos,
val cardService: CardService,
@param:Value("\${game.cardmaker.card.expiration:15}") val expirationTime: Long, @param:Value("\${game.cardmaker.card.expiration:15}") val expirationTime: Long,
) : BaseHandler { ) : BaseHandler {
val log = logger() val log = logger()
@@ -43,6 +45,8 @@ class UpsertUserPrintHandler(
} }
db.userPrintDetail.save(userPrint) db.userPrintDetail.save(userPrint)
userData.card?.let { cardService.updateCardTimestamp(it, "mai2") }
return mapOf( return mapOf(
"returnCode" to 1, "returnCode" to 1,
"orderId" to 0, "orderId" to 0,

View File

@@ -9,6 +9,7 @@ fun OngekiController.ongekiInit() {
fun <T> List<T>.staticLst(key: String) = mapOf("length" to size, key to this) fun <T> List<T>.staticLst(key: String) = mapOf("length" to size, key to this)
initUser() initUser()
cmApiInit()
initUpsertAll() initUpsertAll()
// Has type, but type is always 1 // Has type, but type is always 1

View File

@@ -0,0 +1,351 @@
package icu.samnyan.aqua.sega.ongeki
import ext.asDateTime
import ext.empty
import ext.int
import ext.invoke
import ext.long
import ext.mapApply
import ext.minus
import ext.parsing
import icu.samnyan.aqua.sega.ongeki.model.*
import java.time.LocalDateTime
fun OngekiController.cmApiInit() {
"CMGetUserData" {
val user = db.data.findByCard_ExtId(uid)() ?: (400 - "User not found")
mapOf("userId" to uid, "userData" to user)
}
"PrinterLogin" { mapOf("returnCode" to 1) }
"PrinterLogout" { mapOf("returnCode" to 1) }
"CMGetUserCard".unpaged("userCardList"){
db.card.findByUser_Card_ExtId(uid).map { it.apply { printCount = 99 } }
}
"CMGetUserCharacter".unpaged("userCharacterList") { db.character.findByUser_Card_ExtId(uid)}
"CMGetUserItem" {
val kind = (parsing { data["nextIndex"]!!.long } / 10000000000L).toInt()
var dat = db.item.findByUser_Card_ExtIdAndItemKind(uid, kind)
// Check if user have infinite kaika
if (kind == OgkItemType.KaikaItem.ordinal) {
val u = db.data.findByCard_ExtId(uid)()
u?.card?.aquaUser?.gameOptions?.let {
if (it.ongekiInfiniteKaika) {
dat = listOf(UserItem().apply {
user = u
itemKind = OgkItemType.KaikaItem.ordinal
itemId = 1
stock = 999
})
}
}
}
mapOf("userId" to uid, "length" to dat.size, "nextIndex" to -1, "itemKind" to kind, "userItemList" to dat)
}
"GetGameGacha" {
gdb.gacha.findAll().let {
mapOf("length" to it.size, "gameGachaList" to it, "registIdList" to emptyList<Any>())
}
}
"GetGameGachaCardById" {
val gachaId = parsing { data["gachaId"]!!.long }
gdb.gachaCard.findAllByGachaId(gachaId).let {
mapOf("gachaId" to gachaId, "length" to it.size, "isPickup" to false, "gameGachaCardList" to it,
"emissionList" to empty, "afterCalcList" to empty
)
}
}
"GetUserGacha" {
db.gacha.findByUser_Card_ExtId(uid).let{
mapOf("userId" to uid, "length" to it.size, "nextIndex" to 0, "userGachaList" to it)
}
}
"RollGacha" {
val (gachaId, tmpTimes) = parsing { data["gachaId"]!!.long to data["times"]!!.int }
var times = tmpTimes
val user = db.data.findByCard_ExtId(uid)() ?: (400 - "User not found")
val foundGacha = gdb.gacha.findById(gachaId)() ?: (404 - "Gacha not found")
val foundUserGacha = db.gacha.findByUserAndGachaId(user, gachaId)
// Official gacha percentages: R 76% SR 21% SSR 3%
val probabilities = listOf(76, 21, 3)
var rarityResults: List<Int> = emptyList()
// If this is the first x5 pull or any x11 pull, an SR is guaranteed
if(foundUserGacha != null && ((times == 5 && foundUserGacha.fiveGachaCnt==0) || times==11)){
rarityResults = listOf(3)
times -= 1
}
rarityResults = rarityResults + List(times) {
val rand = (1..100).random()
when {
rand <= probabilities[0] -> 2
rand <= probabilities[0] + probabilities[1] -> 3
else -> 4
}
}
val pulledCards = if(foundGacha.kind==0 || foundGacha.kind==3) {
// If it is the free or the permanent gacha, include cards from the gacha itself
gdb.gachaCard.findAllByGachaId(gachaId).groupBy { it.rarity }
} else {
// If it is a normal gacha, include all cards from the permanent gacha
gdb.gachaCard.findAllByGachaIdAndPermanent(gachaId).groupBy { it.rarity }
}
val finalPulls = rarityResults.map { rarity -> pulledCards[rarity]?.random()}
mapOf("length" to finalPulls.size, "gameGachaCardList" to finalPulls)
}
"CMUpsertUserGacha" api@ {
val all: OngekiCMUpsertUserGacha = mapper.convert(data["cmUpsertUserGacha"]!!)
val gacha = parsing { data["gachaId"]!!.long }
val pullCount = parsing { data["gachaCnt"]!!.int }
val earnedSelectPoints = parsing { data["selectPoint"]!!.int }
// User data
val oldUser = db.data.findByCard_ExtId(uid)()
val u: UserData = all.userData?.get(0)?.apply {
id = oldUser?.id ?: 0
card = oldUser?.card ?: us.cardRepo.findByExtId(uid)() ?: (404 - "Card not found")
// Set eventWatchedDate with lastPlayDate, because client doesn't update it
cmEventWatchedDate = oldUser?.lastPlayDate ?: ""
db.data.save(this)
} ?: oldUser ?: return@api null
db.gacha.findByUserAndGachaId(u, gacha)?.apply {
totalGachaCnt += pullCount
// If the user pulled a select gacha increase the stats
if (earnedSelectPoints > 0) {
selectPoint += earnedSelectPoints
ceilingGachaCnt += pullCount
}
// Stats related with the group of pulls the user makes
if (pullCount == 5) {
fiveGachaCnt += 1
}else if (pullCount == 11) {
elevenGachaCnt+=1
}
// Update the daily gacha
val parsedDailyGachaDate = dailyGachaDate.asDateTime()!!
if (
parsedDailyGachaDate.dayOfMonth != LocalDateTime.now().dayOfMonth ||
parsedDailyGachaDate.monthValue != LocalDateTime.now().monthValue ||
parsedDailyGachaDate.year != LocalDateTime.now().year
) {
dailyGachaCnt += pullCount
dailyGachaDate = LocalDateTime.now().toString()
}else{
dailyGachaCnt += pullCount
}
}?: UserGacha().apply {
user = u
gachaId = gacha
totalGachaCnt = pullCount
selectPoint = earnedSelectPoints
ceilingGachaCnt = if (earnedSelectPoints > 0) 1 else 0
fiveGachaCnt = if (pullCount == 5) 1 else 0
elevenGachaCnt = if (pullCount == 11) 1 else 0
dailyGachaCnt = pullCount
db.gacha.save(this)
}
all.run {
// Set users
listOfNotNull(
userCharacterList, userCardList, userItemList
).flatten().forEach { it.user = u }
// UserCharacterList
userCharacterList?.let { list ->
db.character.saveAll(list.distinctBy { it.characterId }.mapApply {
id = db.character.findByUserAndCharacterId(u, characterId)()?.id ?: 0
})
}
// UserCardList
userCardList?.let { list ->
db.card.saveAll(list.distinctBy { it.cardId }.mapApply {
id = db.card.findByUserAndCardId(u, cardId)()?.id ?: 0
})
}
// UserItemList
userItemList?.let { list ->
db.item.saveAll(list.distinctBy { it.itemKind to it.itemId }.mapApply {
id = db.item.findByUserAndItemKindAndItemId(u, itemKind, itemId)()?.id ?: 0
})
}
}
u.card?.let { cardService.updateCardTimestamp(it, "ongeki") }
null
}
"CMUpsertUserAll" api@{
val all: OngekiCMUpsertAll = mapper.convert(data["cmUpsertUserAll"]!!)
// User data
val oldUser = db.data.findByCard_ExtId(uid)()
val u: UserData = all.userData?.get(0)?.apply {
id = oldUser?.id ?: 0
card = oldUser?.card ?: us.cardRepo.findByExtId(uid)() ?: (404 - "Card not found")
// Set eventWatchedDate with lastPlayDate, because client doesn't update it
cmEventWatchedDate = oldUser?.lastPlayDate ?: ""
db.data.save(this)
} ?: oldUser ?: return@api null
all.run {
// Set users
listOfNotNull(
userActivityList, userCharacterList, userCardList, userItemList
).flatten().forEach { it.user = u }
// UserActivityList
userActivityList?.let { list ->
db.activity.saveAll(list.distinctBy { it.activityId to it.kind }.mapApply {
id = db.activity.findByUserAndKindAndActivityId(u, kind, activityId)()?.id ?: 0
})
}
// UserCharacterList
userCharacterList?.let { list ->
db.character.saveAll(list.distinctBy { it.characterId }.mapApply {
id = db.character.findByUserAndCharacterId(u, characterId)()?.id ?: 0
})
}
// UserCardList
userCardList?.let { list ->
db.card.saveAll(list.distinctBy { it.cardId }.mapApply {
id = db.card.findByUserAndCardId(u, cardId)()?.id ?: 0
})
}
// UserItemList
userItemList?.let { list ->
db.item.saveAll(list.distinctBy { it.itemKind to it.itemId }.mapApply {
id = db.item.findByUserAndItemKindAndItemId(u, itemKind, itemId)()?.id ?: 0
})
}
}
u.card?.let { cardService.updateCardTimestamp(it, "ongeki") }
null
}
"CMUpsertUserSelectGacha" api@ {
val all: OngekiCMUpsertUserSelectGacha = mapper.convert(data["cmUpsertUserSelectGacha"]!!)
val selectLog:List<OngekiCMSelectGachaLog> = mapper.convert(data["selectGachaLogList"]!!)
// User data
val oldUser = db.data.findByCard_ExtId(uid)()
val u: UserData = all.userData?.get(0)?.apply {
id = oldUser?.id ?: 0
card = oldUser?.card ?: us.cardRepo.findByExtId(uid)() ?: (404 - "Card not found")
// Set eventWatchedDate with lastPlayDate, because client doesn't update it
cmEventWatchedDate = oldUser?.lastPlayDate ?: ""
db.data.save(this)
} ?: oldUser ?: return@api null
if(selectLog.isNotEmpty()) {
val selectionInfo = selectLog.first()
db.gacha.findByUserAndGachaId(u, selectionInfo.gachaId)?.apply {
// Total reset of selectPoints
selectPoint = 0
// Set the flag for the selection gacha so that the user cant use it anymore
useSelectPoint = 1
} ?: UserGacha().apply {
// It should be impossible to reach this
user = u
gachaId = selectionInfo.gachaId
db.gacha.save(this)
}
}
all.run {
// Set users
listOfNotNull(
userCharacterList, userCardList
).flatten().forEach { it.user = u }
if (all.isNewCharacterList?.contains("1") ?: false) {
// UserCharacterList
userCharacterList?.let { list ->
db.character.saveAll(list.distinctBy { it.characterId }.mapApply {
id = db.character.findByUserAndCharacterId(u, characterId)()?.id ?: 0
})
}
}
if (all.isNewCardList?.contains("1") ?: false) {
// UserCardList
userCardList?.let { list ->
db.card.saveAll(list.distinctBy { it.cardId }.mapApply {
id = db.card.findByUserAndCardId(u, cardId)()?.id ?: 0
})
}
}
}
u.card?.let { cardService.updateCardTimestamp(it, "ongeki") }
null
}
"CMUpsertUserPrintPlaylog" api@ {
// User payment logs, useless
null
}
"CMUpsertUserPrint" api@ {
// User print information, useless
null
}
"CMUpsertUserPrintlog" api@ {
// User print logs, useless
null
}
"CMGetUserGachaSupply" {
// Mock function, not sure of functionality
mapOf("supplyId" to 0, "length" to 0, "supplyCardList" to emptyList<Any>())
}
"GetGameTheater" {
// Mock function, not sure of functionality
mapOf("length" to 0, "gameTheaterList" to emptyList<Any>(), "registIdList" to emptyList<Any>())
}
}

View File

@@ -7,6 +7,7 @@ import icu.samnyan.aqua.sega.allnet.TokenChecker
import icu.samnyan.aqua.sega.general.GameMusicPopularity import icu.samnyan.aqua.sega.general.GameMusicPopularity
import icu.samnyan.aqua.sega.general.MeowApi import icu.samnyan.aqua.sega.general.MeowApi
import icu.samnyan.aqua.sega.general.RequestContext import icu.samnyan.aqua.sega.general.RequestContext
import icu.samnyan.aqua.sega.general.service.CardService
import icu.samnyan.aqua.sega.util.jackson.BasicMapper import icu.samnyan.aqua.sega.util.jackson.BasicMapper
import icu.samnyan.aqua.spring.Metrics import icu.samnyan.aqua.spring.Metrics
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
@@ -22,6 +23,7 @@ class OngekiController(
val gdb: OngekiGameRepos, val gdb: OngekiGameRepos,
val us: AquaUserServices, val us: AquaUserServices,
val pop: GameMusicPopularity, val pop: GameMusicPopularity,
val cardService: CardService,
): MeowApi({ _, resp -> if (resp is String) resp else mapper.write(resp) }) { ): MeowApi({ _, resp -> if (resp is String) resp else mapper.write(resp) }) {
val log = logger() val log = logger()

View File

@@ -151,6 +151,19 @@ interface OgkUserRegionsRepo: OngekiUserLinked<UserRegions> {
fun findByUserAndRegionId(user: UserData, regionId: Int): UserRegions? fun findByUserAndRegionId(user: UserData, regionId: Int): UserRegions?
} }
interface OgkGameGachaCardRepo : JpaRepository<GameGachaCard, Long> {
fun findAllByGachaId(gachaId: Long): List<GameGachaCard>
@Query("SELECT g FROM OngekiGameGachaCard g WHERE g.gachaId = :gachaId OR g.gachaId = 1112")
fun findAllByGachaIdAndPermanent(gachaId: Long): List<GameGachaCard>
}
interface OgkGameGachaRepo : JpaRepository<GameGacha, Long>
interface OgkUserGachaRepo : OngekiUserLinked<UserGacha> {
fun findByUserAndGachaId(user: UserData, gachaId: Long): UserGacha?
}
// Re:Fresh // Re:Fresh
interface OgkUserEventMapRepo : OngekiUserLinked<UserEventMap> interface OgkUserEventMapRepo : OngekiUserLinked<UserEventMap>
interface OgkUserSkinRepo : OngekiUserLinked<UserSkin> interface OgkUserSkinRepo : OngekiUserLinked<UserSkin>
@@ -195,6 +208,7 @@ class OngekiUserRepos(
val eventMap: OgkUserEventMapRepo, val eventMap: OgkUserEventMapRepo,
val skin: OgkUserSkinRepo, val skin: OgkUserSkinRepo,
val regions: OgkUserRegionsRepo, val regions: OgkUserRegionsRepo,
val gacha: OgkUserGachaRepo,
) )
@Component @Component
@@ -207,6 +221,8 @@ class OngekiGameRepos(
val present: OgkGamePresentRepo, val present: OgkGamePresentRepo,
val reward: OgkGameRewardRepo, val reward: OgkGameRewardRepo,
val skill: OgkGameSkillRepo, val skill: OgkGameSkillRepo,
val gachaCard: OgkGameGachaCardRepo,
val gacha:OgkGameGachaRepo
) )
@Component @Component

View File

@@ -4,6 +4,7 @@ import ext.int
import ext.invoke import ext.invoke
import ext.mapApply import ext.mapApply
import ext.minus import ext.minus
import icu.samnyan.aqua.sega.general.model.CardStatus
import icu.samnyan.aqua.sega.ongeki.model.OngekiUpsertUserAll import icu.samnyan.aqua.sega.ongeki.model.OngekiUpsertUserAll
import icu.samnyan.aqua.sega.ongeki.model.UserData import icu.samnyan.aqua.sega.ongeki.model.UserData
import icu.samnyan.aqua.sega.ongeki.model.UserGeneralData import icu.samnyan.aqua.sega.ongeki.model.UserGeneralData
@@ -49,6 +50,12 @@ fun OngekiController.initUpsertAll() {
db.regions.save(region) db.regions.save(region)
} }
// If the user was previously migrated to Minato, saving would mark them "migrated and then cleared".
if (u.card?.status == CardStatus.MIGRATED_TO_MINATO) {
u.card?.status = CardStatus.NORMAL_MIGRATED_TO_MINATO_AND_THEN_CLEARED
us.cardRepo.save(u.card!!)
}
all.run { all.run {
// Set users // Set users
listOfNotNull( listOfNotNull(
@@ -202,6 +209,8 @@ fun OngekiController.initUpsertAll() {
id = db.kop.findByUserAndKopIdAndAreaId(u, kopId, areaId)()?.id ?: 0 }) } id = db.kop.findByUserAndKopIdAndAreaId(u, kopId, areaId)()?.id ?: 0 }) }
} }
u.card?.let { cardService.updateCardTimestamp(it, "ongeki") }
null null
} }
} }

View File

@@ -162,26 +162,13 @@ fun OngekiController.initUser() {
) )
if (u.card?.status == CardStatus.MIGRATED_TO_MINATO) { if (u.card?.status == CardStatus.MIGRATED_TO_MINATO) {
res["userName"] = "JiaQQqun / CardMigrated" res["userName"] = "${res["userName"]}"
res["level"] = 0
res["exp"] = 0
res["playerRating"] = 0
res["newPlayerRating"] = 0
} }
res res
} }
"GameLogin" { "GameLogin" { """{"returnCode":"1"}""" }
val user = db.data.findByCard_ExtId(uid)()
if (user?.card?.status == CardStatus.MIGRATED_TO_MINATO) {
"""{"returnCode":"0"}"""
}
else {
"""{"returnCode":"1"}"""
}
}
"GetUserRecentRating".unpaged { "GetUserRecentRating".unpaged {
db.generalData.findByUser_Card_ExtIdAndPropertyKey(uid, "recent_rating_list")()?.let { recent -> db.generalData.findByUser_Card_ExtIdAndPropertyKey(uid, "recent_rating_list")()?.let { recent ->

View File

@@ -1,6 +1,7 @@
package icu.samnyan.aqua.sega.ongeki.model package icu.samnyan.aqua.sega.ongeki.model
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonProperty
import jakarta.persistence.* import jakarta.persistence.*
@Entity(name = "OngekiGameCard") @Entity(name = "OngekiGameCard")
@@ -112,3 +113,41 @@ class GameSkill {
var category: String = "" var category: String = ""
var info: String = "" var info: String = ""
} }
@Entity(name = "OngekiGameGachaCard")
@Table(name = "ongeki_game_gacha_card")
class GameGachaCard {
@Id
var cardId: Long = 0
var gachaId: Long = 0
var rarity: Int = 0
var weight: Int = 0
@JsonProperty("isPickup")
var isPickup: Boolean = false
@JsonProperty("isSelect")
var isSelect: Boolean = false
}
@Entity(name = "OngekiGameGacha")
@Table(name = "ongeki_game_gacha")
class GameGacha {
@Id
var gachaId: Long = 0
var gachaName: String = ""
var type: Int = 0
var kind: Int = 0
var maxSelectPoint: Int = 0
var ceilingCnt: Int = 0
var changeRateCnt1: Int = 0
var changeRateCnt2: Int = 0
var startDate: String = ""
var endDate: String = ""
var noticeStartDate: String = ""
var noticeEndDate: String = ""
var convertEndDate: String = ""
@JsonProperty("isCeiling")
var isCeiling: Boolean = false
}

View File

@@ -0,0 +1,44 @@
package icu.samnyan.aqua.sega.ongeki.model
class OngekiCMUpsertUserGacha {
var userData: List<UserData>? = null
var userCharacterList: List<UserCharacter>? = null
var userCardList: List<UserCard>? = null
var gameGachaCardList: List<GameGachaCard>? = null
var userItemList: List<UserItem>? = null
// These are strings with as many 1s as new elements are present
var isNewCharacterList: String? = null
var isNewCardList: String? = null
var isNewItemList: String? = null
}
class OngekiCMUpsertAll {
var userData: List<UserData>? = null
var userActivityList: List<UserActivity>? = null
var userCharacterList: List<UserCharacter>? = null
var userCardList: List<UserCard>? = null
var userItemList: List<UserItem>? = null
// These are strings with as many 1s as new elements are given
var isNewCharacterList: String? = null
var isNewCardList: String? = null
var isNewItemList: String? = null
}
class OngekiCMUpsertUserSelectGacha {
var userData: List<UserData>? = null
var userCharacterList: List<UserCharacter>? = null
var userCardList: List<UserCard>? = null
// These are strings with as many 1s as new elements are given
var isNewCharacterList: String? = null
var isNewCardList: String? = null
}
class OngekiCMSelectGachaLog {
var gachaId: Long = 0
var useSelectPoint: Int = 0
var convertType: Int = 0
var convertItem: Int = 0
}

View File

@@ -8,6 +8,7 @@ import icu.samnyan.aqua.sega.general.model.Card
import icu.samnyan.aqua.sega.util.jackson.AccessCodeSerializer import icu.samnyan.aqua.sega.util.jackson.AccessCodeSerializer
import jakarta.persistence.* import jakarta.persistence.*
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime
@MappedSuperclass @MappedSuperclass
class OngekiUserEntity : BaseEntity(), IUserEntity<UserData> { class OngekiUserEntity : BaseEntity(), IUserEntity<UserData> {
@@ -517,10 +518,27 @@ class UserSkin : OngekiUserEntity() {
@Entity(name = "OngekiUserRegions") @Entity(name = "OngekiUserRegions")
@Table( @Table(
name = "ongeki_user_regions", name = "ongeki_user_regions",
uniqueConstraints = [UniqueConstraint(columnNames = ["user_id", "regionId"])] uniqueConstraints = [UniqueConstraint(columnNames = ["user_id", "region_id"])]
) )
class UserRegions : OngekiUserEntity() { class UserRegions : OngekiUserEntity() {
var regionId = 0 var regionId = 0
var playCount = 1 var playCount = 1
var created: String = LocalDate.now().toString() var created: String = LocalDate.now().toString()
} }
@Entity(name="OngekiUserGacha")
@Table(
name = "ongeki_user_gacha",
uniqueConstraints = [UniqueConstraint(columnNames = ["user_id", "gacha_id"])]
)
class UserGacha : OngekiUserEntity() {
var gachaId:Long = 0
var totalGachaCnt = 0
var ceilingGachaCnt = 0
var selectPoint = 0
var useSelectPoint = 0
var dailyGachaCnt = 0
var fiveGachaCnt = 0
var elevenGachaCnt = 0
var dailyGachaDate: String = LocalDateTime.now().toString()
}

View File

@@ -205,28 +205,28 @@ fun WaccaServer.init() {
val go = u.card?.aquaUser?.gameOptions ?: AquaGameOptions() val go = u.card?.aquaUser?.gameOptions ?: AquaGameOptions()
// All unlock // All unlock
if (go.unlockMusic && wacca.musicMapping.isNotEmpty()) { if (go.waccaUnlockMusic && wacca.musicMapping.isNotEmpty()) {
items[MUSIC_UNLOCK()] = wacca.musicMapping.map { (id, v) -> MUSIC_UNLOCK(u, id, p1 = v.notes.size.long() - 1) } items[MUSIC_UNLOCK()] = wacca.musicMapping.map { (id, v) -> MUSIC_UNLOCK(u, id, p1 = v.notes.size.long() - 1) }
} }
if (go.unlockTickets) { if (go.waccaUnlockTickets) {
var i = 0 var i = 0
items[TICKET()] = enabledTickets.flatMap { (1..5).map { TICKET(u, it).apply { id = (i++).toLong() } } } items[TICKET()] = enabledTickets.flatMap { (1..5).map { TICKET(u, it).apply { id = (i++).toLong() } } }
} }
if (go.unlockChara) { if (go.waccaUnlockPlates) {
wacca.itemMapping["plates"]?.let { items[USER_PLATE()] = it.map { (k, _) -> USER_PLATE(u, k.int()) } } wacca.itemMapping["plates"]?.let { items[USER_PLATE()] = it.map { (k, _) -> USER_PLATE(u, k.int()) } }
} }
if (go.unlockCollectables) { if (go.waccaUnlockCollectables) {
// TODO: Add titles // TODO: Add titles
mapOf("icon" to ICON, "plates" to USER_PLATE, "trophy" to TROPHY).map { (name, type) -> mapOf("icon" to ICON, "trophy" to TROPHY).map { (name, type) ->
wacca.itemMapping[name]?.let { items[type()] = it.map { (k, _) -> type(u, k.int()) } } wacca.itemMapping[name]?.let { items[type()] = it.map { (k, _) -> type(u, k.int()) } }
} }
} }
val status = u.lStatus().toMutableList() val status = u.lStatus().toMutableList()
if (u.card?.status == CardStatus.MIGRATED_TO_MINATO) { // if (u.card?.status == CardStatus.MIGRATED_TO_MINATO) {
status[1] = "JiaQQqun / CardMigrated" // status[1] = "${status[1]}@AquaDX"
} // }
u.run { ls( u.run { ls(
"0 status" - status, "0 status" - status,

View File

@@ -0,0 +1,840 @@
CREATE TABLE ongeki_game_gacha
(
gacha_id BIGINT AUTO_INCREMENT NOT NULL,
gacha_name VARCHAR(255) NOT NULL,
type INT NOT NULL,
kind INT NOT NULL,
is_ceiling BOOLEAN NOT NULL,
max_select_point INT NOT NULL,
ceiling_cnt INT NOT NULL,
change_rate_cnt1 INT NOT NULL,
change_rate_cnt2 INT NOT NULL,
start_date DATETIME NOT NULL,
end_date DATETIME NOT NULL,
notice_start_date DATETIME NOT NULL,
notice_end_date DATETIME NOT NULL,
convert_end_date DATETIME NOT NULL,
CONSTRAINT pk_ongk_gacha PRIMARY KEY (gacha_id)
);
CREATE TABLE ongeki_game_gacha_card
(
card_id BIGINT AUTO_INCREMENT NOT NULL,
gacha_id BIGINT NOT NULL,
rarity INT NOT NULL,
weight INT NOT NULL,
is_pickup BOOLEAN NOT NULL,
is_select BOOLEAN NOT NULL,
CONSTRAINT pk_ongk_gacha_card PRIMARY KEY (card_id, gacha_id)
);
ALTER TABLE ongeki_game_gacha_card
ADD CONSTRAINT FK_ONGEKI_GAME_GACHA_CARD_ON_GACHA FOREIGN KEY (gacha_id) REFERENCES ongeki_game_gacha (gacha_id );
CREATE TABLE ongeki_user_gacha
(
id BIGINT AUTO_INCREMENT NOT NULL,
user_id BIGINT NOT NULL,
gacha_id BIGINT NOT NULL,
total_gacha_cnt INT NOT NULL,
ceiling_gacha_cnt INT NOT NULL,
select_point INT NOT NULL,
use_select_point INT NOT NULL,
daily_gacha_cnt INT NOT NULL,
five_gacha_cnt INT NOT NULL,
eleven_gacha_cnt INT NOT NULL,
daily_gacha_date DATETIME NOT NULL,
CONSTRAINT pk_ongk_user_gacha PRIMARY KEY (id)
);
ALTER TABLE ongeki_user_gacha
ADD CONSTRAINT FK_ONGEKI_USER_GACHA_ON_USER FOREIGN KEY (user_id) REFERENCES ongeki_user_data (id);
ALTER TABLE ongeki_user_gacha
ADD CONSTRAINT FK_ONGEKI_USER_GACHA_ON_GACHA FOREIGN KEY (gacha_id) REFERENCES ongeki_game_gacha (gacha_id);
ALTER TABLE ongeki_user_gacha
ADD CONSTRAINT unq_ongk_user_gacha UNIQUE (user_id, gacha_id);
INSERT INTO ongeki_game_gacha (gacha_id, gacha_name, `type`, kind, is_ceiling, max_select_point, ceiling_cnt, change_rate_cnt1, change_rate_cnt2, start_date, end_date, notice_start_date, notice_end_date, convert_end_date)
VALUES
(1011, '無料ガチャ', 0, 3, 0, 0, 0, 0, 0, '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2999-01-01 00:00:00.000'),
(1012, '無料ガチャSR確定', 0, 3, 0, 0, 0, 0, 0, '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2999-01-01 00:00:00.000'),
(1112, 'レギュラーガチャ', 0, 0, 0, 0, 0, 0, 0, '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2999-01-01 00:00:00.000'),
(1123, 'AQUA属性オンリーガチャ', 0, 1, 0, 0, 0, 0, 0, '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2999-01-01 00:00:00.000'),
(1128, 'bitter chocolate kiss ガチャ', 0, 4, 0, 0, 0, 0, 0, '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2999-01-01 00:00:00.000'),
(1135, 'オンゲキ bright 大感謝祭ガチャ', 0, 1, 0, 0, 0, 0, 0, '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2999-01-01 00:00:00.000'),
(1137, 'テアトル展開!プロジェクト奏坂ドールズリミテッドガチャ', 0, 1, 0, 0, 0, 0, 0, '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2999-01-01 00:00:00.000'),
(1138, 'WONDER SHOOTER EASTER ガチャ', 0, 4, 0, 0, 0, 0, 0, '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2999-01-01 00:00:00.000'),
(1139, 'ラスボス登場!?悪の侵略者ガチャ', 0, 4, 0, 0, 0, 0, 0, '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2999-01-01 00:00:00.000'),
(1140, 'カラフルアンブレラガチャ', 0, 4, 0, 0, 0, 0, 0, '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2999-01-01 00:00:00.000'),
(1141, 'It''s Showtimeワンダフルサーカスガチャ', 0, 4, 0, 0, 0, 0, 0, '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2999-01-01 00:00:00.000'),
(1143, 'マーチングポケッツ ピックアップガチャ', 0, 4, 0, 0, 0, 0, 0, '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2999-01-01 00:00:00.000'),
(1144, 'bitter flavor ピックアップガチャ', 0, 4, 0, 0, 0, 0, 0, '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2999-01-01 00:00:00.000'),
(1145, '7EVENDAYS⇔HOLIDAYS ピックアップガチャ', 0, 1, 0, 0, 0, 0, 0, '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2999-01-01 00:00:00.000'),
(1146, '⊿TRiEDGE ピックアップガチャ', 0, 1, 0, 0, 0, 0, 0, '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2999-01-01 00:00:00.000'),
(1147, 'R.B.P. ピックアップガチャ', 0, 1, 0, 0, 0, 0, 0, '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2999-01-01 00:00:00.000'),
(1148, '皇城 セツナ ピックアップガチャ', 0, 1, 0, 0, 0, 0, 0, '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2999-01-01 00:00:00.000'),
(1149, 'ASTERISM ピックアップガチャ', 0, 1, 0, 0, 0, 0, 0, '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2999-01-01 00:00:00.000'),
(1151, 'スラッシュスキル ピックアップガチャ', 0, 1, 0, 0, 0, 0, 0, '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2999-01-01 00:00:00.000'),
(1152, '高瀬 梨緒オンリーガチャ', 0, 1, 0, 0, 0, 0, 0, '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2999-01-01 00:00:00.000'),
(1154, 'トラストスキル ピックアップガチャ', 0, 1, 0, 0, 0, 0, 0, '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2999-01-01 00:00:00.000'),
(1156, 'bright memory振り返りガチャ', 0, 2, 0, 0, 0, 0, 0, '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2999-01-01 00:00:00.000'),
(1159, 'オンゲキ&オンゲキ PLUS ピックアップガチャ', 0, 2, 0, 0, 0, 0, 0, '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2999-01-01 00:00:00.000'),
(1160, 'SUMMER SUMMER PLUS ピックアップガチャ', 0, 2, 0, 0, 0, 0, 0, '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2999-01-01 00:00:00.000'),
(1161, 'R.E.D. & R.E.D. PLUS ピックアップガチャ', 0, 2, 0, 0, 0, 0, 0, '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2999-01-01 00:00:00.000'),
(1162, 'bright & bright MEMORY ピックアップガチャ', 0, 2, 0, 0, 0, 0, 0, '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2999-01-01 00:00:00.000'),
(1167, '6周年記念!! 6rd Anniversaryセレクトガチャ', 0, 2, 1, 11, 11, 0, 0, '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2020-01-01 00:00:00.000', '2999-01-01 00:00:00.000', '2999-01-01 00:00:00.000');
INSERT INTO ongeki_game_gacha_card (card_id, gacha_id,rarity,weight,is_pickup,is_select)
VALUES
(100003, 1112, 2, 100, 0, 0),
(100003, 1149, 2, 100, 0, 0),
(100004, 1112, 2, 100, 0, 0),
(100004, 1149, 2, 100, 0, 0),
(100006, 1112, 3, 100, 0, 0),
(100006, 1159, 3, 100, 0, 0),
(100007, 1112, 3, 100, 0, 0),
(100007, 1159, 3, 100, 0, 0),
(100008, 1112, 4, 100, 0, 0),
(100008, 1159, 4, 100, 0, 0),
(100008, 1167, 4, 100, 0, 1),
(100012, 1112, 2, 100, 0, 0),
(100012, 1149, 2, 100, 0, 0),
(100013, 1112, 2, 100, 0, 0),
(100013, 1149, 2, 100, 0, 0),
(100015, 1112, 3, 100, 0, 0),
(100015, 1159, 3, 100, 0, 0),
(100016, 1112, 3, 100, 0, 0),
(100016, 1159, 3, 100, 0, 0),
(100017, 1112, 4, 100, 0, 0),
(100017, 1159, 4, 100, 0, 0),
(100017, 1167, 4, 100, 0, 1),
(100021, 1112, 2, 100, 0, 0),
(100021, 1149, 2, 100, 0, 0),
(100022, 1112, 2, 100, 0, 0),
(100022, 1149, 2, 100, 0, 0),
(100024, 1112, 3, 100, 0, 0),
(100024, 1159, 3, 100, 0, 0),
(100025, 1112, 3, 100, 0, 0),
(100025, 1159, 3, 100, 0, 0),
(100026, 1112, 4, 100, 0, 0),
(100026, 1159, 4, 100, 0, 0),
(100026, 1167, 4, 100, 0, 1),
(100030, 1112, 2, 100, 0, 0),
(100030, 1146, 2, 100, 0, 0),
(100030, 1152, 2, 100, 1, 0),
(100033, 1152, 3, 100, 1, 0),
(100033, 1159, 3, 100, 0, 0),
(100034, 1112, 4, 100, 0, 0),
(100034, 1152, 4, 100, 1, 0),
(100034, 1159, 4, 100, 0, 0),
(100034, 1167, 4, 100, 0, 1),
(100037, 1112, 2, 100, 0, 0),
(100037, 1146, 2, 100, 0, 0),
(100040, 1112, 3, 100, 0, 0),
(100040, 1159, 3, 100, 0, 0),
(100041, 1112, 4, 100, 0, 0),
(100041, 1159, 4, 100, 0, 0),
(100041, 1167, 4, 100, 0, 1),
(100044, 1112, 2, 100, 0, 0),
(100044, 1146, 2, 100, 0, 0),
(100047, 1112, 3, 100, 0, 0),
(100047, 1159, 3, 100, 0, 0),
(100048, 1112, 4, 100, 0, 0),
(100048, 1159, 4, 100, 0, 0),
(100048, 1167, 4, 100, 0, 1),
(100051, 1112, 2, 100, 0, 0),
(100051, 1144, 2, 100, 0, 0),
(100053, 1112, 3, 100, 0, 0),
(100053, 1144, 3, 100, 0, 0),
(100053, 1159, 3, 100, 0, 0),
(100054, 1112, 4, 100, 0, 0),
(100054, 1144, 4, 100, 0, 0),
(100054, 1159, 4, 100, 0, 0),
(100054, 1167, 4, 100, 0, 1),
(100057, 1112, 2, 100, 0, 0),
(100057, 1144, 2, 100, 0, 0),
(100059, 1112, 3, 100, 0, 0),
(100059, 1144, 3, 100, 0, 0),
(100059, 1159, 3, 100, 0, 0),
(100060, 1112, 4, 100, 0, 0),
(100060, 1144, 4, 100, 0, 0),
(100060, 1159, 4, 100, 0, 0),
(100060, 1167, 4, 100, 0, 1),
(100063, 1112, 2, 100, 0, 0),
(100063, 1147, 2, 100, 0, 0),
(100065, 1112, 3, 100, 0, 0),
(100065, 1147, 3, 100, 0, 0),
(100065, 1159, 3, 100, 0, 0),
(100066, 1112, 4, 100, 0, 0),
(100066, 1147, 4, 100, 0, 0),
(100066, 1159, 4, 100, 0, 0),
(100066, 1167, 4, 100, 0, 1),
(100069, 1112, 2, 100, 0, 0),
(100069, 1145, 2, 100, 0, 0),
(100071, 1112, 3, 100, 0, 0),
(100071, 1145, 3, 100, 0, 0),
(100071, 1159, 3, 100, 0, 0),
(100072, 1112, 4, 100, 0, 0),
(100072, 1145, 4, 100, 0, 0),
(100072, 1159, 4, 100, 0, 0),
(100072, 1167, 4, 100, 0, 1),
(100075, 1112, 2, 100, 0, 0),
(100075, 1145, 2, 100, 0, 0),
(100077, 1112, 3, 100, 0, 0),
(100077, 1145, 3, 100, 0, 0),
(100077, 1159, 3, 100, 0, 0),
(100078, 1112, 4, 100, 0, 0),
(100078, 1145, 4, 100, 0, 0),
(100078, 1159, 4, 100, 0, 0),
(100078, 1167, 4, 100, 0, 1),
(100081, 1112, 2, 100, 0, 0),
(100081, 1147, 2, 100, 0, 0),
(100083, 1112, 3, 100, 0, 0),
(100083, 1147, 3, 100, 0, 0),
(100083, 1159, 3, 100, 0, 0),
(100084, 1112, 4, 100, 0, 0),
(100084, 1147, 4, 100, 0, 0),
(100084, 1159, 4, 100, 0, 0),
(100084, 1167, 4, 100, 0, 1),
(100087, 1112, 2, 100, 0, 0),
(100087, 1147, 2, 100, 0, 0),
(100089, 1112, 3, 100, 0, 0),
(100089, 1147, 3, 100, 0, 0),
(100089, 1159, 3, 100, 0, 0),
(100090, 1112, 4, 100, 0, 0),
(100090, 1147, 4, 100, 0, 0),
(100090, 1159, 4, 100, 0, 0),
(100090, 1167, 4, 100, 0, 1),
(100173, 1112, 2, 100, 0, 0),
(100173, 1149, 2, 100, 0, 0),
(100174, 1112, 2, 100, 0, 0),
(100174, 1149, 2, 100, 0, 0),
(100175, 1112, 2, 100, 0, 0),
(100175, 1149, 2, 100, 0, 0),
(100176, 1112, 2, 100, 0, 0),
(100176, 1146, 2, 100, 0, 0),
(100176, 1152, 2, 100, 1, 0),
(100177, 1112, 2, 100, 0, 0),
(100177, 1146, 2, 100, 0, 0),
(100178, 1112, 2, 100, 0, 0),
(100178, 1146, 2, 100, 0, 0),
(100179, 1112, 2, 100, 0, 0),
(100179, 1144, 2, 100, 0, 0),
(100180, 1112, 2, 100, 0, 0),
(100180, 1144, 2, 100, 0, 0),
(100181, 1112, 2, 100, 0, 0),
(100181, 1147, 2, 100, 0, 0),
(100182, 1112, 2, 100, 0, 0),
(100182, 1145, 2, 100, 0, 0),
(100183, 1112, 2, 100, 0, 0),
(100183, 1145, 2, 100, 0, 0),
(100184, 1112, 2, 100, 0, 0),
(100184, 1147, 2, 100, 0, 0),
(100185, 1112, 2, 100, 0, 0),
(100185, 1147, 2, 100, 0, 0),
(100209, 1011, 2, 100, 0, 0),
(100223, 1112, 2, 100, 0, 0),
(100223, 1149, 2, 100, 0, 0),
(100224, 1112, 2, 100, 0, 0),
(100224, 1149, 2, 100, 0, 0),
(100225, 1112, 2, 100, 0, 0),
(100225, 1149, 2, 100, 0, 0),
(100226, 1112, 2, 100, 0, 0),
(100226, 1146, 2, 100, 0, 0),
(100226, 1152, 2, 100, 1, 0),
(100227, 1112, 2, 100, 0, 0),
(100227, 1146, 2, 100, 0, 0),
(100228, 1112, 2, 100, 0, 0),
(100228, 1146, 2, 100, 0, 0),
(100229, 1112, 2, 100, 0, 0),
(100229, 1144, 2, 100, 0, 0),
(100230, 1112, 2, 100, 0, 0),
(100230, 1144, 2, 100, 0, 0),
(100231, 1112, 2, 100, 0, 0),
(100231, 1147, 2, 100, 0, 0),
(100232, 1112, 2, 100, 0, 0),
(100232, 1145, 2, 100, 0, 0),
(100233, 1112, 2, 100, 0, 0),
(100233, 1145, 2, 100, 0, 0),
(100234, 1112, 2, 100, 0, 0),
(100234, 1147, 2, 100, 0, 0),
(100235, 1112, 2, 100, 0, 0),
(100235, 1147, 2, 100, 0, 0),
(100236, 1011, 3, 100, 0, 0),
(100236, 1012, 3, 100, 0, 0),
(100237, 1011, 3, 100, 0, 0),
(100237, 1012, 3, 100, 0, 0),
(100238, 1011, 3, 100, 0, 0),
(100238, 1012, 3, 100, 0, 0),
(100239, 1011, 3, 100, 0, 0),
(100239, 1012, 3, 100, 0, 0),
(100240, 1011, 3, 100, 0, 0),
(100240, 1012, 3, 100, 0, 0),
(100241, 1011, 3, 100, 0, 0),
(100241, 1012, 3, 100, 0, 0),
(100242, 1011, 3, 100, 0, 0),
(100242, 1012, 3, 100, 0, 0),
(100243, 1011, 3, 100, 0, 0),
(100243, 1012, 3, 100, 0, 0),
(100244, 1011, 3, 100, 0, 0),
(100244, 1012, 3, 100, 0, 0),
(100245, 1011, 3, 100, 0, 0),
(100245, 1012, 3, 100, 0, 0),
(100246, 1011, 3, 100, 0, 0),
(100246, 1012, 3, 100, 0, 0),
(100247, 1011, 3, 100, 0, 0),
(100247, 1012, 3, 100, 0, 0),
(100248, 1011, 3, 100, 0, 0),
(100248, 1012, 3, 100, 0, 0),
(100249, 1112, 3, 100, 0, 0),
(100249, 1159, 3, 100, 0, 0),
(100250, 1112, 3, 100, 0, 0),
(100250, 1159, 3, 100, 0, 0),
(100251, 1112, 3, 100, 0, 0),
(100251, 1159, 3, 100, 0, 0),
(100252, 1011, 3, 100, 0, 0),
(100252, 1012, 3, 100, 0, 0),
(100253, 1011, 3, 100, 0, 0),
(100253, 1012, 3, 100, 0, 0),
(100254, 1011, 3, 100, 0, 0),
(100254, 1012, 3, 100, 0, 0),
(100255, 1011, 3, 100, 0, 0),
(100255, 1012, 3, 100, 0, 0),
(100256, 1011, 3, 100, 0, 0),
(100256, 1012, 3, 100, 0, 0),
(100257, 1011, 3, 100, 0, 0),
(100257, 1012, 3, 100, 0, 0),
(100258, 1011, 3, 100, 0, 0),
(100258, 1012, 3, 100, 0, 0),
(100259, 1011, 3, 100, 0, 0),
(100259, 1012, 3, 100, 0, 0),
(100260, 1011, 3, 100, 0, 0),
(100260, 1012, 3, 100, 0, 0),
(100261, 1011, 3, 100, 0, 0),
(100261, 1012, 3, 100, 0, 0),
(100262, 1112, 3, 100, 0, 0),
(100262, 1159, 3, 100, 0, 0),
(100263, 1112, 3, 100, 0, 0),
(100263, 1159, 3, 100, 0, 0),
(100264, 1112, 3, 100, 0, 0),
(100264, 1159, 3, 100, 0, 0),
(100265, 1112, 3, 100, 0, 0),
(100265, 1152, 3, 100, 1, 0),
(100265, 1159, 3, 100, 0, 0),
(100266, 1112, 3, 100, 0, 0),
(100266, 1159, 3, 100, 0, 0),
(100267, 1112, 3, 100, 0, 0),
(100267, 1159, 3, 100, 0, 0),
(100268, 1112, 3, 100, 0, 0),
(100268, 1144, 3, 100, 0, 0),
(100268, 1159, 3, 100, 0, 0),
(100269, 1112, 3, 100, 0, 0),
(100269, 1144, 3, 100, 0, 0),
(100269, 1159, 3, 100, 0, 0),
(100270, 1112, 3, 100, 0, 0),
(100270, 1147, 3, 100, 0, 0),
(100270, 1159, 3, 100, 0, 0),
(100271, 1112, 3, 100, 0, 0),
(100271, 1145, 3, 100, 0, 0),
(100271, 1159, 3, 100, 0, 0),
(100272, 1112, 3, 100, 0, 0),
(100272, 1145, 3, 100, 0, 0),
(100272, 1159, 3, 100, 0, 0),
(100273, 1112, 3, 100, 0, 0),
(100273, 1147, 3, 100, 0, 0),
(100273, 1159, 3, 100, 0, 0),
(100274, 1112, 3, 100, 0, 0),
(100274, 1147, 3, 100, 0, 0),
(100274, 1159, 3, 100, 0, 0),
(100275, 1112, 4, 100, 0, 0),
(100275, 1159, 4, 100, 0, 0),
(100275, 1167, 4, 100, 0, 1),
(100276, 1112, 4, 100, 0, 0),
(100276, 1159, 4, 100, 0, 0),
(100276, 1167, 4, 100, 0, 1),
(100277, 1112, 4, 100, 0, 0),
(100277, 1159, 4, 100, 0, 0),
(100277, 1167, 4, 100, 0, 1),
(100278, 1112, 4, 100, 0, 0),
(100278, 1152, 4, 100, 1, 0),
(100278, 1159, 4, 100, 0, 0),
(100278, 1167, 4, 100, 0, 1),
(100280, 1112, 4, 100, 0, 0),
(100280, 1159, 4, 100, 0, 0),
(100280, 1167, 4, 100, 0, 1),
(100282, 1112, 4, 100, 0, 0),
(100282, 1144, 4, 100, 0, 0),
(100282, 1159, 4, 100, 0, 0),
(100282, 1167, 4, 100, 0, 1),
(100284, 1112, 4, 100, 0, 0),
(100284, 1145, 4, 100, 0, 0),
(100284, 1159, 4, 100, 0, 0),
(100284, 1167, 4, 100, 0, 1),
(100285, 1112, 4, 100, 0, 0),
(100285, 1145, 4, 100, 0, 0),
(100285, 1159, 4, 100, 0, 0),
(100285, 1167, 4, 100, 0, 1),
(100286, 1112, 4, 100, 0, 0),
(100286, 1147, 4, 100, 0, 0),
(100286, 1159, 4, 100, 0, 0),
(100286, 1167, 4, 100, 0, 1),
(100336, 1011, 3, 100, 0, 0),
(100336, 1012, 3, 100, 0, 0),
(100337, 1011, 3, 100, 0, 0),
(100337, 1012, 3, 100, 0, 0),
(100338, 1011, 3, 100, 0, 0),
(100338, 1012, 3, 100, 0, 0),
(100339, 1112, 2, 100, 0, 0),
(100339, 1149, 2, 100, 0, 0),
(100340, 1112, 2, 100, 0, 0),
(100340, 1149, 2, 100, 0, 0),
(100341, 1112, 2, 100, 0, 0),
(100341, 1149, 2, 100, 0, 0),
(100342, 1112, 2, 100, 0, 0),
(100342, 1146, 2, 100, 0, 0),
(100342, 1152, 2, 100, 1, 0),
(100343, 1112, 2, 100, 0, 0),
(100343, 1146, 2, 100, 0, 0),
(100344, 1112, 2, 100, 0, 0),
(100344, 1146, 2, 100, 0, 0),
(100345, 1112, 2, 100, 0, 0),
(100345, 1144, 2, 100, 0, 0),
(100346, 1112, 2, 100, 0, 0),
(100346, 1144, 2, 100, 0, 0),
(100347, 1112, 2, 100, 0, 0),
(100347, 1147, 2, 100, 0, 0),
(100348, 1112, 2, 100, 0, 0),
(100348, 1145, 2, 100, 0, 0),
(100349, 1112, 2, 100, 0, 0),
(100349, 1145, 2, 100, 0, 0),
(100350, 1112, 2, 100, 0, 0),
(100350, 1147, 2, 100, 0, 0),
(100351, 1112, 2, 100, 0, 0),
(100351, 1147, 2, 100, 0, 0),
(100407, 1112, 4, 100, 0, 0),
(100407, 1143, 4, 100, 0, 0),
(100407, 1160, 4, 100, 0, 0),
(100407, 1167, 4, 100, 0, 1),
(100411, 1112, 2, 100, 0, 0),
(100411, 1146, 2, 100, 0, 0),
(100411, 1152, 2, 100, 1, 0),
(100412, 1112, 2, 100, 0, 0),
(100412, 1146, 2, 100, 0, 0),
(100413, 1112, 2, 100, 0, 0),
(100413, 1146, 2, 100, 0, 0),
(100414, 1112, 2, 100, 0, 0),
(100415, 1112, 2, 100, 0, 0),
(100416, 1112, 2, 100, 0, 0),
(100417, 1112, 2, 100, 0, 0),
(100418, 1112, 3, 100, 0, 0),
(100418, 1149, 3, 100, 0, 0),
(100418, 1160, 3, 100, 0, 0),
(100419, 1112, 3, 100, 0, 0),
(100419, 1160, 3, 100, 0, 0),
(100420, 1112, 3, 100, 0, 0),
(100420, 1144, 3, 100, 0, 0),
(100421, 1147, 3, 100, 0, 0),
(100421, 1160, 3, 100, 0, 0),
(100422, 1112, 3, 100, 0, 0),
(100422, 1144, 3, 100, 0, 0),
(100422, 1160, 3, 100, 0, 0),
(100423, 1112, 3, 100, 0, 0),
(100423, 1160, 3, 100, 0, 0),
(100424, 1112, 3, 100, 0, 0),
(100424, 1143, 3, 100, 0, 0),
(100424, 1160, 3, 100, 0, 0),
(100425, 1112, 3, 100, 0, 0),
(100425, 1160, 3, 100, 0, 0),
(100426, 1112, 3, 100, 0, 0),
(100426, 1147, 3, 100, 0, 0),
(100426, 1160, 3, 100, 0, 0),
(100427, 1112, 3, 100, 0, 0),
(100427, 1160, 3, 100, 0, 0),
(100428, 1112, 3, 100, 0, 0),
(100428, 1160, 3, 100, 0, 0),
(100429, 1112, 3, 100, 0, 0),
(100429, 1160, 3, 100, 0, 0),
(100430, 1112, 3, 100, 0, 0),
(100430, 1160, 3, 100, 0, 0),
(100431, 1112, 4, 100, 0, 0),
(100431, 1143, 4, 100, 0, 0),
(100431, 1160, 4, 100, 0, 0),
(100431, 1167, 4, 100, 0, 1),
(100432, 1112, 4, 100, 0, 0),
(100432, 1143, 4, 100, 0, 0),
(100432, 1160, 4, 100, 0, 0),
(100432, 1167, 4, 100, 0, 1),
(100433, 1112, 4, 100, 0, 0),
(100433, 1160, 4, 100, 0, 0),
(100433, 1167, 4, 100, 0, 1),
(100434, 1112, 4, 100, 0, 0),
(100434, 1144, 4, 100, 0, 0),
(100434, 1160, 4, 100, 0, 0),
(100434, 1167, 4, 100, 0, 1),
(100435, 1112, 4, 100, 0, 0),
(100435, 1123, 4, 100, 0, 0),
(100435, 1147, 4, 100, 0, 0),
(100435, 1154, 4, 100, 0, 0),
(100435, 1160, 4, 100, 0, 0),
(100435, 1167, 4, 100, 0, 1),
(100436, 1112, 4, 100, 0, 0),
(100436, 1160, 4, 100, 0, 0),
(100436, 1167, 4, 100, 0, 1),
(100437, 1112, 4, 100, 0, 0),
(100437, 1160, 4, 100, 0, 0),
(100437, 1167, 4, 100, 0, 1),
(100438, 1112, 4, 100, 0, 0),
(100438, 1147, 4, 100, 0, 0),
(100438, 1160, 4, 100, 0, 0),
(100438, 1167, 4, 100, 0, 1),
(100439, 1112, 4, 100, 0, 0),
(100439, 1146, 4, 100, 0, 0),
(100439, 1152, 4, 100, 1, 0),
(100439, 1160, 4, 100, 0, 0),
(100439, 1167, 4, 100, 0, 1),
(100440, 1011, 2, 100, 0, 0),
(100441, 1011, 2, 100, 0, 0),
(100445, 1112, 2, 100, 0, 0),
(100445, 1143, 2, 100, 0, 0),
(100447, 1112, 2, 100, 0, 0),
(100447, 1143, 2, 100, 0, 0),
(100449, 1112, 2, 100, 0, 0),
(100449, 1143, 2, 100, 0, 0),
(100454, 1011, 4, 100, 0, 0),
(100606, 1112, 3, 100, 0, 0),
(100607, 1112, 3, 100, 0, 0),
(100608, 1112, 3, 100, 0, 0),
(100608, 1143, 3, 100, 0, 0),
(100608, 1160, 3, 100, 0, 0),
(100609, 1112, 3, 100, 0, 0),
(100609, 1143, 3, 100, 0, 0),
(100609, 1160, 3, 100, 0, 0),
(100610, 1112, 3, 100, 0, 0),
(100610, 1143, 3, 100, 0, 0),
(100610, 1160, 3, 100, 0, 0),
(100634, 1011, 3, 100, 0, 0),
(100634, 1012, 3, 100, 0, 0),
(100636, 1011, 3, 100, 0, 0),
(100636, 1012, 3, 100, 0, 0),
(100637, 1011, 3, 100, 0, 0),
(100637, 1012, 3, 100, 0, 0),
(100638, 1011, 3, 100, 0, 0),
(100638, 1012, 3, 100, 0, 0),
(100639, 1011, 3, 100, 0, 0),
(100639, 1012, 3, 100, 0, 0),
(100640, 1011, 3, 100, 0, 0),
(100640, 1012, 3, 100, 0, 0),
(100641, 1011, 3, 100, 0, 0),
(100641, 1012, 3, 100, 0, 0),
(100642, 1011, 3, 100, 0, 0),
(100642, 1012, 3, 100, 0, 0),
(100643, 1011, 3, 100, 0, 0),
(100643, 1012, 3, 100, 0, 0),
(100644, 1011, 3, 100, 0, 0),
(100644, 1012, 3, 100, 0, 0),
(100645, 1011, 3, 100, 0, 0),
(100645, 1012, 3, 100, 0, 0),
(100646, 1011, 3, 100, 0, 0),
(100646, 1012, 3, 100, 0, 0),
(100647, 1112, 2, 100, 0, 0),
(100648, 1112, 2, 100, 0, 0),
(100649, 1112, 2, 100, 0, 0),
(100650, 1011, 2, 100, 0, 0),
(100651, 1011, 2, 100, 0, 0),
(100652, 1011, 2, 100, 0, 0),
(100653, 1112, 2, 100, 0, 0),
(100653, 1143, 2, 100, 0, 0),
(100654, 1112, 2, 100, 0, 0),
(100654, 1143, 2, 100, 0, 0),
(100655, 1112, 2, 100, 0, 0),
(100655, 1143, 2, 100, 0, 0),
(100662, 1112, 2, 100, 0, 0),
(100665, 1011, 3, 100, 0, 0),
(100665, 1012, 3, 100, 0, 0),
(100666, 1011, 3, 100, 0, 0),
(100666, 1012, 3, 100, 0, 0),
(100667, 1011, 3, 100, 0, 0),
(100667, 1012, 3, 100, 0, 0),
(100692, 1112, 2, 100, 0, 0),
(100692, 1149, 2, 100, 0, 0),
(100693, 1112, 2, 100, 0, 0),
(100693, 1149, 2, 100, 0, 0),
(100694, 1112, 2, 100, 0, 0),
(100694, 1149, 2, 100, 0, 0),
(100701, 1112, 2, 100, 0, 0),
(100701, 1146, 2, 100, 0, 0),
(100701, 1152, 2, 100, 1, 0),
(100702, 1011, 2, 100, 0, 0),
(100703, 1011, 2, 100, 0, 0),
(100711, 1112, 3, 100, 0, 0),
(100711, 1161, 3, 100, 0, 0),
(100712, 1112, 3, 100, 0, 0),
(100712, 1161, 3, 100, 0, 0),
(100725, 1112, 4, 100, 0, 0),
(100725, 1146, 4, 100, 0, 0),
(100725, 1161, 4, 100, 0, 0),
(100725, 1167, 4, 100, 0, 1),
(100726, 1112, 4, 100, 0, 0),
(100726, 1161, 4, 100, 0, 0),
(100726, 1167, 4, 100, 0, 1),
(100755, 1112, 2, 100, 0, 0),
(100756, 1112, 2, 100, 0, 0),
(100757, 1112, 3, 100, 0, 0),
(100757, 1160, 3, 100, 0, 0),
(100758, 1112, 3, 100, 0, 0),
(100758, 1160, 3, 100, 0, 0),
(100759, 1112, 3, 100, 0, 0),
(100759, 1160, 3, 100, 0, 0),
(100760, 1112, 4, 100, 0, 0),
(100760, 1149, 4, 100, 0, 0),
(100760, 1160, 4, 100, 0, 0),
(100760, 1167, 4, 100, 0, 1),
(100761, 1112, 4, 100, 0, 0),
(100761, 1160, 4, 100, 0, 0),
(100761, 1167, 4, 100, 0, 1),
(100762, 1112, 3, 100, 0, 0),
(100762, 1160, 3, 100, 0, 0),
(100763, 1112, 3, 100, 0, 0),
(100763, 1160, 3, 100, 0, 0),
(100764, 1112, 3, 100, 0, 0),
(100764, 1160, 3, 100, 0, 0),
(100765, 1112, 3, 100, 0, 0),
(100765, 1160, 3, 100, 0, 0),
(100766, 1112, 3, 100, 0, 0),
(100766, 1146, 3, 100, 0, 0),
(100766, 1160, 3, 100, 0, 0),
(100767, 1112, 4, 100, 0, 0),
(100767, 1160, 4, 100, 0, 0),
(100767, 1167, 4, 100, 0, 1),
(100768, 1112, 4, 100, 0, 0),
(100768, 1160, 4, 100, 0, 0),
(100768, 1167, 4, 100, 0, 1),
(100771, 1112, 2, 100, 0, 0),
(100772, 1011, 3, 100, 0, 0),
(100772, 1012, 3, 100, 0, 0),
(100777, 1112, 3, 100, 0, 0),
(100777, 1146, 3, 100, 0, 0),
(100777, 1152, 3, 100, 1, 0),
(100777, 1160, 3, 100, 0, 0),
(100778, 1112, 3, 100, 0, 0),
(100778, 1160, 3, 100, 0, 0),
(100779, 1112, 4, 100, 0, 0),
(100779, 1154, 4, 100, 0, 0),
(100779, 1160, 4, 100, 0, 0),
(100779, 1167, 4, 100, 0, 1),
(100780, 1112, 4, 100, 0, 0),
(100780, 1149, 4, 100, 0, 0),
(100780, 1160, 4, 100, 0, 0),
(100780, 1167, 4, 100, 0, 1),
(100783, 1112, 3, 100, 0, 0),
(100783, 1160, 3, 100, 0, 0),
(100784, 1112, 4, 100, 0, 0),
(100784, 1160, 4, 100, 0, 0),
(100784, 1167, 4, 100, 0, 1),
(100785, 1112, 3, 100, 0, 0),
(100785, 1149, 3, 100, 0, 0),
(100785, 1151, 3, 100, 0, 0),
(100785, 1161, 3, 100, 0, 0),
(100786, 1112, 3, 100, 0, 0),
(100786, 1149, 3, 100, 0, 0),
(100786, 1151, 3, 100, 0, 0),
(100786, 1161, 3, 100, 0, 0),
(100787, 1112, 4, 100, 0, 0),
(100787, 1151, 4, 100, 0, 0),
(100787, 1161, 4, 100, 0, 0),
(100787, 1167, 4, 100, 0, 1),
(100805, 1112, 3, 100, 0, 0),
(100805, 1160, 3, 100, 0, 0),
(100842, 1112, 3, 100, 0, 0),
(100980, 1011, 4, 100, 0, 0),
(100982, 1112, 4, 100, 0, 0),
(100982, 1161, 4, 100, 0, 0),
(100982, 1167, 4, 100, 0, 1),
(100983, 1112, 4, 100, 0, 0),
(100983, 1161, 4, 100, 0, 0),
(100983, 1167, 4, 100, 0, 1),
(100984, 1137, 4, 100, 1, 0),
(100984, 1146, 4, 100, 0, 0),
(100984, 1167, 4, 100, 0, 1),
(100985, 1112, 4, 100, 0, 0),
(100985, 1161, 4, 100, 0, 0),
(100985, 1167, 4, 100, 0, 1),
(100986, 1137, 4, 100, 1, 0),
(100986, 1167, 4, 100, 0, 1),
(100987, 1112, 4, 100, 0, 0),
(100987, 1149, 4, 100, 0, 0),
(100987, 1154, 4, 100, 0, 0),
(100987, 1161, 4, 100, 0, 0),
(100987, 1167, 4, 100, 0, 1),
(100988, 1112, 4, 100, 0, 0),
(100988, 1154, 4, 100, 0, 0),
(100988, 1161, 4, 100, 0, 0),
(100988, 1167, 4, 100, 0, 1),
(100989, 1112, 4, 100, 0, 0),
(100989, 1123, 4, 100, 0, 0),
(100989, 1146, 4, 100, 0, 0),
(100989, 1152, 4, 100, 1, 0),
(100989, 1161, 4, 100, 0, 0),
(100989, 1167, 4, 100, 0, 1),
(100996, 1112, 3, 100, 0, 0),
(100996, 1161, 3, 100, 0, 0),
(100997, 1137, 3, 100, 1, 0),
(100998, 1137, 3, 100, 1, 0),
(100999, 1112, 3, 100, 0, 0),
(100999, 1161, 3, 100, 0, 0),
(101000, 1112, 3, 100, 0, 0),
(101000, 1161, 3, 100, 0, 0),
(101001, 1137, 3, 100, 1, 0),
(101002, 1137, 3, 100, 1, 0),
(101002, 1146, 3, 100, 0, 0),
(101003, 1112, 3, 100, 0, 0),
(101003, 1161, 3, 100, 0, 0),
(101004, 1112, 3, 100, 0, 0),
(101004, 1161, 3, 100, 0, 0),
(101005, 1112, 3, 100, 0, 0),
(101005, 1149, 3, 100, 0, 0),
(101005, 1161, 3, 100, 0, 0),
(101020, 1112, 2, 100, 0, 0),
(101020, 1149, 2, 100, 0, 0),
(101021, 1112, 2, 100, 0, 0),
(101021, 1146, 2, 100, 0, 0),
(101021, 1152, 2, 100, 1, 0),
(101022, 1112, 2, 100, 0, 0),
(101023, 1112, 2, 100, 0, 0),
(101023, 1146, 2, 100, 0, 0),
(101024, 1112, 2, 100, 0, 0),
(101024, 1146, 2, 100, 0, 0),
(101025, 1112, 2, 100, 0, 0),
(101025, 1149, 2, 100, 0, 0),
(101026, 1112, 2, 100, 0, 0),
(101293, 1112, 4, 100, 0, 0),
(101293, 1123, 4, 100, 0, 0),
(101293, 1137, 4, 100, 1, 0),
(101293, 1154, 4, 100, 0, 0),
(101293, 1161, 4, 100, 0, 0),
(101293, 1167, 4, 100, 0, 1),
(101294, 1112, 4, 100, 0, 0),
(101294, 1161, 4, 100, 0, 0),
(101294, 1167, 4, 100, 0, 1),
(101295, 1112, 4, 100, 0, 0),
(101295, 1123, 4, 100, 0, 0),
(101295, 1149, 4, 100, 0, 0),
(101295, 1161, 4, 100, 0, 0),
(101295, 1167, 4, 100, 0, 1),
(101296, 1112, 4, 100, 0, 0),
(101296, 1149, 4, 100, 0, 0),
(101296, 1151, 4, 100, 0, 0),
(101296, 1154, 4, 100, 0, 0),
(101296, 1161, 4, 100, 0, 0),
(101296, 1167, 4, 100, 0, 1),
(101297, 1137, 4, 100, 1, 0),
(101297, 1167, 4, 100, 0, 1),
(101298, 1112, 3, 100, 0, 0),
(101298, 1146, 3, 100, 0, 0),
(101298, 1161, 3, 100, 0, 0),
(101299, 1112, 3, 100, 0, 0),
(101299, 1161, 3, 100, 0, 0),
(101300, 1112, 3, 100, 0, 0),
(101300, 1151, 3, 100, 0, 0),
(101300, 1161, 3, 100, 0, 0),
(101301, 1137, 3, 100, 1, 0),
(101301, 1146, 3, 100, 0, 0),
(101301, 1152, 3, 100, 1, 0),
(101315, 1146, 2, 100, 0, 0),
(101315, 1151, 2, 100, 0, 0),
(101316, 1151, 2, 100, 0, 0),
(101320, 1112, 4, 100, 0, 0),
(101320, 1146, 4, 100, 0, 0),
(101320, 1161, 4, 100, 0, 0),
(101320, 1167, 4, 100, 0, 1),
(101321, 1112, 3, 100, 0, 0),
(101321, 1161, 3, 100, 0, 0),
(101553, 1011, 4, 100, 0, 0),
(101566, 1112, 3, 100, 0, 0),
(101566, 1135, 3, 100, 1, 0),
(101566, 1148, 3, 100, 0, 0),
(101566, 1162, 3, 100, 0, 0),
(101567, 1112, 4, 100, 0, 0),
(101567, 1135, 4, 100, 1, 0),
(101567, 1148, 4, 100, 0, 0),
(101567, 1151, 4, 100, 0, 0),
(101567, 1162, 4, 100, 0, 0),
(101567, 1167, 4, 100, 0, 1),
(101589, 1011, 2, 100, 0, 0),
(101592, 1112, 4, 100, 0, 0),
(101592, 1135, 4, 100, 1, 0),
(101592, 1149, 4, 100, 0, 0),
(101592, 1154, 4, 100, 0, 0),
(101592, 1162, 4, 100, 0, 0),
(101592, 1167, 4, 100, 0, 1),
(101593, 1137, 4, 100, 1, 0),
(101593, 1167, 4, 100, 0, 1),
(101594, 1112, 4, 100, 0, 0),
(101594, 1123, 4, 100, 0, 0),
(101594, 1135, 4, 100, 1, 0),
(101594, 1154, 4, 100, 0, 0),
(101594, 1162, 4, 100, 0, 0),
(101594, 1167, 4, 100, 0, 1),
(101595, 1112, 4, 100, 0, 0),
(101595, 1123, 4, 100, 0, 0),
(101595, 1128, 4, 100, 1, 0),
(101595, 1135, 4, 100, 1, 0),
(101595, 1167, 4, 100, 0, 1),
(101596, 1112, 4, 100, 0, 0),
(101596, 1138, 4, 100, 1, 0),
(101596, 1146, 4, 100, 0, 0),
(101596, 1154, 4, 100, 0, 0),
(101596, 1156, 4, 100, 1, 0),
(101596, 1162, 4, 100, 0, 0),
(101596, 1167, 4, 100, 0, 1),
(101597, 1112, 4, 100, 0, 0),
(101597, 1139, 4, 100, 1, 0),
(101597, 1156, 4, 100, 1, 0),
(101597, 1162, 4, 100, 0, 0),
(101597, 1167, 4, 100, 0, 1),
(101598, 1137, 4, 100, 1, 0),
(101598, 1148, 4, 100, 0, 0),
(101599, 1112, 4, 100, 0, 0),
(101599, 1140, 4, 100, 1, 0),
(101599, 1156, 4, 100, 1, 0),
(101599, 1162, 4, 100, 0, 0),
(101599, 1167, 4, 100, 0, 1),
(101600, 1112, 4, 100, 0, 0),
(101600, 1141, 4, 100, 1, 0),
(101600, 1156, 4, 100, 1, 0),
(101600, 1162, 4, 100, 0, 0),
(101600, 1167, 4, 100, 0, 1),
(101601, 1137, 3, 100, 1, 0),
(101602, 1112, 3, 100, 0, 0),
(101602, 1135, 3, 100, 1, 0),
(101602, 1149, 3, 100, 0, 0),
(101602, 1162, 3, 100, 0, 0),
(101603, 1112, 3, 100, 0, 0),
(101603, 1128, 3, 100, 1, 0),
(101603, 1135, 3, 100, 1, 0),
(101603, 1146, 3, 100, 0, 0),
(101603, 1162, 3, 100, 0, 0),
(101604, 1112, 3, 100, 0, 0),
(101604, 1138, 3, 100, 1, 0),
(101604, 1149, 3, 100, 0, 0),
(101604, 1156, 3, 100, 1, 0),
(101604, 1162, 3, 100, 0, 0),
(101605, 1112, 3, 100, 0, 0),
(101605, 1139, 3, 100, 1, 0),
(101605, 1156, 3, 100, 1, 0),
(101605, 1162, 3, 100, 0, 0),
(101606, 1137, 3, 100, 1, 0),
(101606, 1167, 3, 100, 0, 1),
(101607, 1112, 3, 100, 0, 0),
(101607, 1140, 3, 100, 1, 0),
(101607, 1156, 3, 100, 1, 0),
(101607, 1162, 3, 100, 0, 0),
(101608, 1112, 3, 100, 0, 0),
(101608, 1141, 3, 100, 1, 0),
(101608, 1156, 3, 100, 1, 0),
(101608, 1162, 3, 100, 0, 0),
(101619, 1112, 2, 100, 0, 0),
(101619, 1135, 2, 100, 1, 0),
(101619, 1148, 2, 100, 0, 0),
(101620, 1011, 2, 100, 0, 0),
(101627, 1011, 4, 100, 0, 0);

View File

@@ -0,0 +1,101 @@
INSERT INTO chusan_game_event (id, type, end_date, start_date, enable)
VALUES
(2507, 13, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(2508, 13, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(2509, 13, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(2510, 13, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(2511, 13, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(2512, 13, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17000, 3, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17001, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17002, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17003, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17004, 3, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17005, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17006, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17007, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17008, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17009, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17010, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17011, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17012, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17013, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17014, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17015, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17016, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17017, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17018, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17019, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17020, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17021, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17022, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17023, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17024, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17025, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17026, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17027, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17028, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17029, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17030, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17031, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17032, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17033, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17034, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17035, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17036, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17040, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17041, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17042, 17, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17043, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17044, 14, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17045, 10, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17046, 12, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17047, 7, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17048, 7, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17049, 3, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17050, 3, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17051, 5, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17052, 3, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17053, 11, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17054, 4, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17055, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17056, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17057, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17058, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17059, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17060, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17061, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17062, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17063, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17064, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17065, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17066, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17067, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17068, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17069, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17070, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17071, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17072, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17073, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17074, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17075, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17076, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17077, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17078, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17079, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17080, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17081, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17082, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17083, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17084, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17085, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17086, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17087, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17088, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17089, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true)
ON DUPLICATE KEY UPDATE
type = VALUES(type),
end_date = VALUES(end_date),
start_date = VALUES(start_date),
enable = VALUES(enable);

View File

@@ -0,0 +1 @@
DROP TABLE aqua_net_user_fedy;

View File

@@ -0,0 +1,11 @@
# Fix NULL values in maimai2_user_detail point and total_point fields
# These fields were added without NOT NULL constraint in V1000_36
# First, set NULL values to 0
UPDATE maimai2_user_detail SET point = 0 WHERE point IS NULL;
UPDATE maimai2_user_detail SET total_point = 0 WHERE total_point IS NULL;
# Then add NOT NULL constraint
ALTER TABLE maimai2_user_detail MODIFY point INT NOT NULL DEFAULT 0;
ALTER TABLE maimai2_user_detail MODIFY total_point INT NOT NULL DEFAULT 0;

View File

@@ -0,0 +1,289 @@
INSERT IGNORE INTO `ongeki_game_music` (id, artist_name, boss_card_id, boss_level, genre, level0, level1, level2, level3, level4, name, sort_name) VALUES
(337, 'LOVE☆MAXガールズ「ゴシックは魔法乙女」', '100816', 15, 'POPSANIME', '4', '6', '9', 12, 0, 'わたしたち魔法乙女です☆', 'ワタシタチマホウオトメテス'),
(591, 'TJ.hangneil', '101557', 80, 'オンゲキ', '6', '10', '13', 15, 0, 'Apollo', 'APOLLO'),
(945, '柊かえCV:立花芽恵夢「Re:ステージ!プリズムステップ」', '100915', 40, 'POPSANIME', '4', '8', '12', 14, 0, 'ガジェットはプリンセス', 'カシエツトハフリンセス'),
(946, 'テトラルキア「Re:ステージ!プリズムステップ」', '100915', 15, 'POPSANIME', '2', '7', '11', 14, 0, 'ユニゾンモノローグ', 'ユニソンモノロク'),
(1000, 'あべにゅうぷろじぇくと feat.エンジェルオールスターズ(めぐる・すみれ・遥・葵・クルミ・テスラ・ナイン)', '102185', 7, 'POPSANIME', '3', '7', '10', 12, 0, 'えんじぇる♡ピタゴリ☆ほーみたい!', 'エンシエルヒタコリホミタイ'),
(1001, '周防パトラ', '102217', 10, 'POPSANIME', '4', '8', '11', 13, 0, '偉大なる悪魔は実は大天使パトラちゃん様なのだ!', 'イタイナルアクマハシツハタイテンシハトラチヤンサマナノタ'),
(1002, '周防パトラ', '102217', 8, 'POPSANIME', '3', '6', '9', 12, 0, 'ハートサーモグラフィー', 'ハトサモクラフイ'),
(1061, 'ARForest', '100765', 35, 'オンゲキ', '5', '9', '11', 14, 0, 'Flow Into You', 'FLOWINTOYOU'),
(1074, 'Feryquitous高瀬 梨緒(CV久保 ユリカ)', '101872', 24, 'オンゲキ', '3', '8', '12', 13, 0, 'Ai Me', 'AIME'),
(1075, 'KENSUKE結城 莉玖(CV朝日奈 丸佳)', '101873', 25, 'オンゲキ', '4', '8', '12', 13, 0, 'がおっと!!最強☆ぱーりーないと', 'カオツトサイキヨウハリナイト'),
(1076, '曲:テラ(少女理論観測所)/歌:藍原 椿(CV橋本 ちなみ)', '101874', 26, 'オンゲキ', '3', '7', '11', 13, 0, '耐冬花麗', 'ツハキウルワシ'),
(1077, '提供', '100070', 15, 'オンゲキ', '4', '8', '12', 14, 0, 'Never Ending Adventure', 'NEVERENDINGADVENTURE'),
(1078, 'brz1128', '100762', 40, 'オンゲキ', '4', '9', '12', 14, 0, 'ALLNIGHT_DANCER', 'ALLNIGHTDANCER'),
(1079, '7mai', '101571', 63, 'オンゲキ', '6', '9', '12', 14, 0, 'Nýx', 'NYX'),
(1080, 'Aoi', '100785', 80, 'オンゲキ', '7', '10', '13', 15, 0, 'Sargasso', 'SARGASSO'),
(1083, 'Artifact vs. Dualcast', '100001', 43, 'チュウマイ', '4', '9', '12', 14, 0, 'Energizing Flame', 'ENERGIZINGFLAME'),
(1084, 'owltree feat.nietree', '100064', 44, 'チュウマイ', '4', '9', '12', 14, 0, 'QuiQ', 'QUIQ'),
(1085, '歌:長瀬有花/曲:いよわ', '102586', 5, 'POPSANIME', '2', '6', '10', 13, 0, 'オレンジスケール', 'オレンシスケル'),
(1086, '歌:長瀬有花/曲:いよわ', '102586', 15, 'POPSANIME', '4', '8', '11', 13, 0, 'ほんの感想', 'ホンノカンソウ'),
(1087, 'ジョー・力一', '101600', 11, 'POPSANIME', '2', '7', '10', 13, 0, 'レイテストショーマン', 'レイテストシヨマン'),
(1088, '名取さな', '101842', 37, 'POPSANIME', '2', '7', '10', 13, 0, 'いっかい書いてさようなら', 'イツカイカイテサヨウナラ'),
(1089, 'K-forest「Phigros」', '100209', 15, 'VARIETY', '3', '9', '12', 14, 0, 'Break Over', 'BREAKOVER'),
(1090, 'Halv「Phigros」', '100029', 25, 'VARIETY', '3', '9', '12', 14, 0, 'Concvssion', 'CONCVSSION'),
(1091, 'Sakuzyo「Phigros」', '100074', 65, 'VARIETY', '6', '10', '13', 15, 0, 'Distorted Fate', 'DISTORTEDFATE'),
(1092, 'Blacklolita', '102300', 60, 'オンゲキ', '3', '9', '13', 14, 0, '[HALO]', 'HALO'),
(1093, '矢鴇つかさ feat. kalon.', '100443', 26, 'チュウマイ', '2', '8', '11', 13, 0, 'SPILL OVER COLORS', 'SPILLOVERCOLORS'),
(1094, '原口沙輔 feat.重音テト', '102503', 10, 'niconico', '2', '8', '10', 13, 0, '人マニア', 'ヒトマニア'),
(1095, 'マサラダ', '102503', 15, 'niconico', '3', '7', '10', 13, 0, 'ライアーダンサー', 'ライアタンサ'),
(1096, '柊マグネタイト', '100420', 28, 'niconico', '2', '7', '10', 13, 0, 'リアライズ', 'リアライス'),
(1097, 'かめりあ', '100048', 71, 'チュウマイ', '6', '9', '13', 15, 0, 'Λzure Vixen', 'AZUREVIXEN'),
(1098, '曲:脇眞富/歌:オンゲキシューターズ', '102711', 7, 'オンゲキ', '3', '7', '10', 13, 0, 'パピプペ Popping Talk', 'ハヒフヘPOPPINGTALK'),
(1099, 'パソコン音楽クラブ', '100248', 33, 'チュウマイ', '3', '9', '13', 14, 0, 'Ignition', 'IGNITION'),
(1100, 'kamome sano', '100022', 39, 'チュウマイ', '5', '8', '12', 14, 0, 'crazy (about you)', 'CRAZYABOUTYOU'),
(1101, '天川はの', '100211', 5, 'POPSANIME', '2', '6', '10', 12, 0, 'Honeycomb', 'HONEYCOMB'),
(1102, 'DÉ DÉ MOUSE & WaMi', '100061', 9, 'VARIETY', '2', '7', '11', 13, 0, 'As You Feel', 'ASYOUFEEL'),
(1103, '歌:櫻川めぐ/曲:堀江晶太', '102571', 1, 'POPSANIME', '2', '5', '10', 13, 0, '一冊のアロー', 'イツサツノアロ'),
(1104, '歌:松下/曲:堀江晶太', '100427', 10, 'POPSANIME', '2', '6', '10', 13, 0, 'Hey Darling!', 'HEYDARLING'),
(1105, 'Mitsukiyo「ブルーアーカイブ -Blue Archive-」', '102598', 5, 'VARIETY', '3', '9', '12', 14, 0, 'Unwelcome School', 'UNWELCOMESCHOOL'),
(1106, 'SUGAR RUSH「ブルーアーカイブ -Blue Archive-」', '102598', 10, 'VARIETY', '3', '6', '11', 13, 0, '彩りキャンバス', 'イロトリキヤンハス'),
(1107, 'KARUT「ブルーアーカイブ -Blue Archive-」', '102603', 20, 'VARIETY', '3', '8', '11', 13, 0, 'Glitch Street', 'GLITCHSTREET'),
(1108, 'Nor「ブルーアーカイブ -Blue Archive-」', '102598', 15, 'VARIETY', '3', '7', '10', 13, 0, 'Usagi Flap', 'USAGIFLAP'),
(1109, 'Nor「ブルーアーカイブ -Blue Archive-」', '102603', 25, 'VARIETY', '3', '7', '10', 13, 0, 'RE Aoharu', 'REAOHARU'),
(1113, 'B小町 ルビーCV伊駒ゆりえ、有馬かなCV潘めぐみ、MEMちょCV大久保瑠美', '100250', 2, 'POPSANIME', '3', '7', '10', 13, 0, 'POP IN 2', 'POPIN2'),
(7077, '曲:穴山大輔, 水野健治/歌:藤沢 柚子(CV久保田 梨沙)', '100769', 40, 'オンゲキ', '1', '5', '8', 12, 0, '夏色花火 -藤沢 柚子ソロver.-', 'ナツイロハナヒフシサワユスソロVER'),
(7078, '曲:穴山大輔, 水野健治/歌:早乙女 彩華(CV中島 唯)', '100770', 40, 'オンゲキ', '1', '5', '8', 12, 0, '夏色花火 -早乙女 彩華ソロver.-', 'ナツイロハナヒサオトメアヤカソロVER'),
(7079, 'Powerless柏木 咲姫(CV石見 舞菜香)', '100755', 40, 'オンゲキ', '3', '6', '11', 13, 0, 'Iudicium “Apocalypsis Mix” -柏木 咲姫ソロver.-', 'IUDICIUMAPOCALYPSISMIXカシワキサキソロVER'),
(7080, 'Powerless柏木 美亜(CV和氣 あず未)', '100756', 40, 'オンゲキ', '3', '6', '11', 13, 0, 'Iudicium “Apocalypsis Mix” -柏木 美亜ソロver.-', 'IUDICIUMAPOCALYPSISMIXカシワキミアソロVER'),
(7152, '曲:アオワイファイ/歌:星咲 あかり(CV赤尾 ひかる)', '100001', 40, 'オンゲキ', '2', '5', '10', 12, 0, 'WakeUP MakeUP FEVER! -星咲 あかりソロver.-', 'WAKEUPMAKEUPFEVERホシサキアカリソロVER'),
(7153, '曲:アオワイファイ/歌:高瀬 梨緒(CV久保 ユリカ)', '100028', 40, 'オンゲキ', '2', '5', '10', 12, 0, 'WakeUP MakeUP FEVER! -高瀬 梨緒ソロver.-', 'WAKEUPMAKEUPFEVERタカセリオソロVER'),
(7154, '曲:アオワイファイ/歌:藍原 椿(CV橋本 ちなみ)', '100042', 40, 'オンゲキ', '2', '5', '10', 12, 0, 'WakeUP MakeUP FEVER! -藍原 椿ソロver.-', 'WAKEUPMAKEUPFEVERアイハラツハキソロVER'),
(7155, '曲:アオワイファイ/歌:桜井 春菜(CV近藤 玲奈)', '100049', 40, 'オンゲキ', '2', '5', '10', 12, 0, 'WakeUP MakeUP FEVER! -桜井 春菜ソロver.-', 'WAKEUPMAKEUPFEVERサクライハルナソロVER'),
(7156, '曲:アオワイファイ/歌:東雲 つむぎ(CV和泉 風花)', '100452', 40, 'オンゲキ', '2', '5', '10', 12, 0, 'WakeUP MakeUP FEVER! -東雲 つむぎソロver.-', 'WAKEUPMAKEUPFEVERシメツムキソロVER'),
(8060, 'LeaF', '101320', 56, 'チュウマイ', '0', '0', '0', 0, 14, 'macrocosmos', 'MACROCOSMOS'),
(8177, 'オルタンシア「Re:ステージ!プリズムステップ」', '100915', 50, 'POPSANIME', '0', '0', '0', 0, 14, '君とインフィニティ -2021-', 'キミトインフイニテイ2021'),
(8178, 'aran', '3', 40, 'オンゲキ', '0', '0', '0', 0, 0, 'Random Access Emotions', 'RANDOMACCESSEMOTIONS'),
(8185, 'Tanchiky', '100476', 41, 'VARIETY', '0', '0', '0', 0, 0, 'ENERGY SYNERGY MATRIX', 'ENERGYSYNERGYMATRIX'),
(8186, 'HiTECH NINJA', '100031', 10, 'オンゲキ', '0', '0', '0', 0, 14, 'Dolphika', 'DOLPHIKA'),
(8187, '曲:アオワイファイ/歌:オンゲキシューターズ', '100236', 50, 'オンゲキ', '0', '0', '0', 0, 14, 'WakeUP MakeUP FEVER!', 'WAKEUPMAKEUPFEVER'),
(8188, 'LOVE☆MAXガールズ「ゴシックは魔法乙女」', '100816', 55, 'POPSANIME', '0', '0', '0', 0, 14, 'わたしたち魔法乙女です☆', 'ワタシタチマホウオトメテス');
INSERT IGNORE INTO `ongeki_game_event` (`id`) VALUES
('1500430101'),
('1500431601'),
('1500432001'),
('1500450101'),
('1500450301'),
('1500450302'),
('1500451601'),
('1500451701'),
('1500451902'),
('1500520101'),
('1500520102'),
('1500520301'),
('1500520302'),
('1500521301'),
('1500540101'),
('1500540102'),
('1500540103'),
('1500540301'),
('1500540302'),
('1500540303'),
('1500540304'),
('1500540305'),
('1500540501'),
('1500540502'),
('1500540601'),
('1500541501'),
('1500541502'),
('1500541503'),
('1500541601'),
('1500541602'),
('1500541701'),
('1500610101'),
('1500610102'),
('1500610301'),
('1500610302'),
('1500610303'),
('1500610701'),
('1500611301'),
('1500630101'),
('1500630102'),
('1500630301'),
('1500630501'),
('1500630601'),
('1500630801'),
('1500631501'),
('1500631601'),
('1500631701'),
('1500631801'),
('1500710101'),
('1500710102'),
('1500710103'),
('1500710301'),
('1500710302'),
('1500710303'),
('1500710304'),
('1500710305'),
('1500710501'),
('1500710701'),
('1500711301'),
('1500720101'),
('1500720102'),
('1500720103'),
('1500720301'),
('1500720302'),
('1500720303'),
('1500720304'),
('1500720305'),
('1500720501'),
('1500720502'),
('1500721501'),
('1500721502'),
('1500721601'),
('1500721602'),
('1500721701'),
('1500721802'),
('1500730101'),
('1500730301'),
('1500730801'),
('1500760101'),
('1500760501'),
('1500760601'),
('1500761401'),
('1500761501'),
('1500761601'),
('1500761701'),
('1500761801'),
('1500810101'),
('1500810102'),
('1500810103'),
('1500810301'),
('1500810302'),
('1500810303'),
('1500810701'),
('1500811301'),
('1500811401'),
('1500811501'),
('1500811502'),
('1500811503'),
('1500811601'),
('1500811701'),
('1500811902'),
('1500821801'),
('1500830101'),
('1500830102'),
('1500830301'),
('1500830302'),
('1500830303'),
('1500830304'),
('1500830305'),
('1500830306'),
('1500830501'),
('1500830601'),
('1500831501'),
('1500831502'),
('1500831601'),
('1500831701'),
('1500910701'),
('1500931801'),
('1501051801'),
('1501051802');
INSERT IGNORE INTO `ongeki_game_card` (id, name, nick_name, attribute, chara_id, school, gakunen, rarity, level_param, skill_id, cho_kaika_skill_id, card_number, version) VALUES
(100048, '【SSR】藍原 椿[サディスティック・スマイル]', 'サディスティック・スマイル', 'Leaf', 1005, '奏坂学園', '高校1年生', 'SSR', '60,257,280,295,307,317,0,0,0,322', 100106, 100107, '[O.N.G.E.K.I.]1.00-0057', '1.00'),
(102036, '【SSR】三角 葵[夏宵スターマイン]', '夏宵スターマイン', 'Aqua', 1002, 'プロモーション', '高校2年生', 'SSR', '60,257,280,295,307,317,0,0,0,322', 105020, 105021, '[O.N.G.E.K.I.]Special Card', '1.45'),
(102037, '【SSR】藍原 椿[夏宵スターマイン]', '夏宵スターマイン', 'Leaf', 1005, 'プロモーション', '高校1年生', 'SSR', '60,257,280,295,307,317,0,0,0,322', 105020, 105021, '[O.N.G.E.K.I.]Special Card', '1.45'),
(102049, '【SSR】結城 莉玖[I got a gig tonight!]', 'I got a gig tonight!', 'Fire', 1004, '奏坂学園', '高校1年生', 'SSR', '66,277,297,309,316,327,0,0,0,332', 150013, 150014, '[O.N.G.E.K.I.]1.50-0027', '1.50'),
(102050, '【SSR】逢坂 茜[Rule the World!!]', 'Rule the World!!', 'Fire', 1011, '奏坂学園', '高校3年生', 'SSR', '66,277,297,309,316,327,0,0,0,332', 150015, 150016, '[O.N.G.E.K.I.]1.50-0028', '1.50'),
(102297, '【SSR】柏木 美亜[Primera Fes. Bridal Stage]', 'Primera Fes. Bridal Stage', 'Fire', 1013, '奏坂学園', '中学2年生', 'SSR', '70,300,313,323,330,335,0,0,0,340', 115134, 115135, '[O.N.G.E.K.I.]1.50-0046', '1.50'),
(102298, '【SSR】珠洲島 有栖[Primera Fes. Bridal Stage]', 'Primera Fes. Bridal Stage', 'Aqua', 1012, '奏坂学園', '高校1年生', 'SSR', '60,257,280,295,307,317,0,0,0,322', 120006, 120007, '[O.N.G.E.K.I.]1.50-0047', '1.50'),
(102299, '【SSR】三角 葵[Primera Fes. Bridal Stage]', 'Primera Fes. Bridal Stage', 'Aqua', 1002, '奏坂学園', '高校2年生', 'SSR', '60,257,280,295,307,317,0,0,0,322', 115122, 115123, '[O.N.G.E.K.I.]1.50-0048', '1.50'),
(102300, '【SSR】柏木 美亜[Primera Fes. Bridal Stage(集合Ver.)対戦相手専用]', 'Primera Fes. Bridal Stage(集合Ver.)対戦相手専用', 'Fire', 1013, '奏坂学園', '中学2年生', 'SSR', '60,257,280,295,307,317,0,0,0,322', 100000, 100041, '[O.N.G.E.K.I.]1.50-', '1.50'),
(102510, '【SR】重音テト[お ま え]', 'お ま え', 'Fire', 46194, '重音テト', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 100033, 100074, '[O.N.G.E.K.I.]1.50-E-0024', '1.50'),
(102511, '【SR】重音テト[踊れ 踊れ 嘘に踊れ]', '踊れ 踊れ 嘘に踊れ', 'Fire', 46194, '重音テト', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 105042, 105043, '[O.N.G.E.K.I.]1.50-E-0025', '1.50'),
(102512, '【SR】重音テト[踊ったもん勝ち]', '踊ったもん勝ち', 'Fire', 46194, '重音テト', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 120088, 120089, '[O.N.G.E.K.I.]1.50-E-0026', '1.50'),
(102515, '【SSR】重音テト[人マニア]', '人マニア', 'Fire', 46194, '重音テト', '-', 'SSR', '60,257,280,295,307,317,0,0,0,322', 100138, 100139, '[O.N.G.E.K.I.]1.50-E-0029', '1.50'),
(102516, '【SSR】重音テト[ライアーダンサー]', 'ライアーダンサー', 'Fire', 46194, '重音テト', '-', 'SSR', '60,257,280,295,307,317,0,0,0,322', 100031, 100072, '[O.N.G.E.K.I.]1.50-E-0030', '1.50'),
(102571, '【R】和泉 妃愛[生徒会副会長]', '生徒会副会長', 'Fire', 46203, 'ハミダシクリエイティブ', '-', 'R', '50,197,212,227,242,257,0,0,0,257', 100005, 100046, '[O.N.G.E.K.I.]1.50-E-0039', '1.50'),
(102572, '【R】常磐 華乃[生徒会広報]', '生徒会広報', 'Fire', 46204, 'ハミダシクリエイティブ', '-', 'R', '50,197,212,227,242,257,0,0,0,257', 110092, 110093, '[O.N.G.E.K.I.]1.50-E-0040', '1.50'),
(102573, '【R】錦 あすみ[生徒会書記]', '生徒会書記', 'Fire', 46205, 'ハミダシクリエイティブ', '-', 'R', '50,197,212,227,242,257,0,0,0,257', 110148, 110149, '[O.N.G.E.K.I.]1.50-E-0041', '1.50'),
(102574, '【R】鎌倉 詩桜[前生徒会長]', '前生徒会長', 'Fire', 46206, 'ハミダシクリエイティブ', '-', 'R', '50,197,212,227,242,257,0,0,0,257', 130006, 130007, '[O.N.G.E.K.I.]1.50-E-0042', '1.50'),
(102575, '【R】竜閑 天梨[生徒会お手伝い]', '生徒会お手伝い', 'Fire', 46207, 'ハミダシクリエイティブ', '-', 'R', '50,197,212,227,242,257,0,0,0,257', 110142, 110143, '[O.N.G.E.K.I.]1.50-E-0043', '1.50'),
(102576, '【SR】和泉 妃愛[ダメ兄を甘やかすことに喜びを感じる人気声優]', 'ダメ兄を甘やかすことに喜びを感じる人気声優', 'Fire', 46203, 'ハミダシクリエイティブ', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 125012, 125013, '[O.N.G.E.K.I.]1.50-E-0044', '1.50'),
(102577, '【SR】常磐 華乃[黒歴史を抱える売れっ子イラストレーター]', '黒歴史を抱える売れっ子イラストレーター', 'Fire', 46204, 'ハミダシクリエイティブ', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 100025, 100066, '[O.N.G.E.K.I.]1.50-E-0045', '1.50'),
(102578, '【SR】錦 あすみ[電脳世界「から」受肉した天使]', '電脳世界「から」受肉した天使', 'Fire', 46205, 'ハミダシクリエイティブ', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 100016, 100057, '[O.N.G.E.K.I.]1.50-E-0046', '1.50'),
(102579, '【SR】鎌倉 詩桜[前会長にして元凶の気まぐれ小説家]', '前会長にして元凶の気まぐれ小説家', 'Fire', 46206, 'ハミダシクリエイティブ', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 100033, 100074, '[O.N.G.E.K.I.]1.50-E-0047', '1.50'),
(102580, '【SR】竜閑 天梨[今をときめく人気読モ]', '今をときめく人気読モ', 'Fire', 46207, 'ハミダシクリエイティブ', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 115036, 115037, '[O.N.G.E.K.I.]1.50-E-0048', '1.50'),
(102581, '【SSR】和泉 妃愛[妃愛&あすみ]', '妃愛&あすみ', 'Fire', 46203, 'ハミダシクリエイティブ', '-', 'SSR', '60,257,280,295,307,317,0,0,0,322', 130062, 130063, '[O.N.G.E.K.I.]1.50-E-0049', '1.50'),
(102582, '【SSR】和泉 妃愛[ハミダシクリエイティブ]', 'ハミダシクリエイティブ', 'Fire', 46203, 'ハミダシクリエイティブ', '-', 'SSR', '60,257,280,295,307,317,0,0,0,322', 125046, 125047, '[O.N.G.E.K.I.]1.50-E-0050', '1.50'),
(102586, '【R】長瀬有花[RIOT MUSIC]', 'RIOT MUSIC', 'Aqua', 46208, '長瀬有花', '-', 'R', '50,197,212,227,242,257,0,0,0,257', 100005, 100046, '[O.N.G.E.K.I.]1.50-E-0051', '1.50'),
(102588, '【SR】長瀬有花[長瀬有花の日常①]', '長瀬有花の日常①', 'Aqua', 46208, '長瀬有花', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 100025, 100066, '[O.N.G.E.K.I.]1.50-E-0052', '1.50'),
(102589, '【SR】長瀬有花[長瀬有花の日常②]', '長瀬有花の日常②', 'Aqua', 46208, '長瀬有花', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 100033, 100074, '[O.N.G.E.K.I.]1.50-E-0053', '1.50'),
(102590, '【SR】長瀬有花[長瀬有花の日常③]', '長瀬有花の日常③', 'Aqua', 46208, '長瀬有花', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 100018, 100059, '[O.N.G.E.K.I.]1.50-E-0054', '1.50'),
(102591, '【SR】長瀬有花[長瀬有花の日常④]', '長瀬有花の日常④', 'Aqua', 46208, '長瀬有花', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 110086, 110087, '[O.N.G.E.K.I.]1.50-E-0055', '1.50'),
(102592, '【SR】長瀬有花[長瀬有花の日常⑤]', '長瀬有花の日常⑤', 'Aqua', 46208, '長瀬有花', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 110094, 110095, '[O.N.G.E.K.I.]1.50-E-0056', '1.50'),
(102593, '【SR】長瀬有花[長瀬有花の日常⑥]', '長瀬有花の日常⑥', 'Aqua', 46208, '長瀬有花', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 125022, 125023, '[O.N.G.E.K.I.]1.50-E-0057', '1.50'),
(102595, '【SSR】長瀬有花[Eureka]', 'Eureka', 'Aqua', 46208, '長瀬有花', '-', 'SSR', '60,257,280,295,307,317,0,0,0,322', 110070, 110071, '[O.N.G.E.K.I.]1.50-E-0058', '1.50'),
(102596, '【SSR】長瀬有花[だつりょく系アーティスト]', 'だつりょく系アーティスト', 'Aqua', 46208, '長瀬有花', '-', 'SSR', '60,257,280,295,307,317,0,0,0,322', 105020, 105021, '[O.N.G.E.K.I.]1.50-E-0059', '1.50'),
(102598, '【R】陸八魔 アル[便利屋68]', '便利屋68', 'Fire', 46209, 'ブルーアーカイブ', '-', 'R', '50,197,212,227,242,257,0,0,0,257', 100005, 100046, '[O.N.G.E.K.I.]1.50-E-0067', '1.50'),
(102599, '【R】鬼方 カヨコ[便利屋68]', '便利屋68', 'Fire', 46210, 'ブルーアーカイブ', '-', 'R', '50,197,212,227,242,257,0,0,0,257', 110092, 110093, '[O.N.G.E.K.I.]1.50-E-0068', '1.50'),
(102600, '【R】浅黄 ムツキ[便利屋68]', '便利屋68', 'Fire', 46211, 'ブルーアーカイブ', '-', 'R', '50,197,212,227,242,257,0,0,0,257', 110142, 110143, '[O.N.G.E.K.I.]1.50-E-0069', '1.50'),
(102601, '【R】伊草 ハルカ[便利屋68]', '便利屋68', 'Fire', 46212, 'ブルーアーカイブ', '-', 'R', '50,197,212,227,242,257,0,0,0,257', 110108, 110109, '[O.N.G.E.K.I.]1.50-E-0070', '1.50'),
(102602, '【R】桐藤 ナギサ[ティーパーティー]', 'ティーパーティー', 'Aqua', 46213, 'ブルーアーカイブ', '-', 'R', '50,197,212,227,242,257,0,0,0,257', 125050, 125051, '[O.N.G.E.K.I.]1.50-E-0096', '1.50'),
(102603, '【R】聖園 ミカ[ティーパーティー]', 'ティーパーティー', 'Aqua', 46214, 'ブルーアーカイブ', '-', 'R', '50,197,212,227,242,257,0,0,0,257', 100005, 100046, '[O.N.G.E.K.I.]1.50-E-0097', '1.50'),
(102604, '【R】百合園 セイア[ティーパーティー]', 'ティーパーティー', 'Aqua', 46215, 'ブルーアーカイブ', '-', 'R', '50,197,212,227,242,257,0,0,0,257', 100004, 100045, '[O.N.G.E.K.I.]1.50-E-0098', '1.50'),
(102605, '【R】阿慈谷 ヒフミ[補習授業部]', '補習授業部', 'Aqua', 46216, 'ブルーアーカイブ', '-', 'R', '50,197,212,227,242,257,0,0,0,257', 100004, 100045, '[O.N.G.E.K.I.]1.50-E-0099', '1.50'),
(102606, '【R】白洲 アズサ[補習授業部]', '補習授業部', 'Aqua', 46217, 'ブルーアーカイブ', '-', 'R', '50,197,212,227,242,257,0,0,0,257', 110092, 110093, '[O.N.G.E.K.I.]1.50-E-0100', '1.50'),
(102607, '【R】下江 コハル[補習授業部]', '補習授業部', 'Aqua', 46218, 'ブルーアーカイブ', '-', 'R', '50,197,212,227,242,257,0,0,0,257', 100003, 100044, '[O.N.G.E.K.I.]1.50-E-0101', '1.50'),
(102608, '【R】浦和 ハナコ[補習授業部]', '補習授業部', 'Aqua', 46219, 'ブルーアーカイブ', '-', 'R', '50,197,212,227,242,257,0,0,0,257', 125006, 125007, '[O.N.G.E.K.I.]1.50-E-0102', '1.50'),
(102609, '【R】柚鳥 ナツ[放課後スイーツ部]', '放課後スイーツ部', 'Leaf', 46220, 'ブルーアーカイブ', '-', 'R', '50,197,212,227,242,257,0,0,0,257', 110084, 110085, '[O.N.G.E.K.I.]1.50-E-0071', '1.50'),
(102610, '【R】杏山 カズサ[放課後スイーツ部]', '放課後スイーツ部', 'Leaf', 46221, 'ブルーアーカイブ', '-', 'R', '50,197,212,227,242,257,0,0,0,257', 100005, 100046, '[O.N.G.E.K.I.]1.50-E-0072', '1.50'),
(102611, '【R】栗村 アイリ[放課後スイーツ部]', '放課後スイーツ部', 'Leaf', 46222, 'ブルーアーカイブ', '-', 'R', '50,197,212,227,242,257,0,0,0,257', 105004, 105005, '[O.N.G.E.K.I.]1.50-E-0073', '1.50'),
(102612, '【R】伊原木 ヨシミ[放課後スイーツ部]', '放課後スイーツ部', 'Leaf', 46223, 'ブルーアーカイブ', '-', 'R', '50,197,212,227,242,257,0,0,0,257', 110092, 110093, '[O.N.G.E.K.I.]1.50-E-0074', '1.50'),
(102613, '【R】美甘 ネル[Cleaning&Clearing]', 'Cleaning&Clearing', 'Fire', 46224, 'ブルーアーカイブ', '-', 'R', '50,197,212,227,242,257,0,0,0,257', 110092, 110093, '[O.N.G.E.K.I.]1.50-E-0075', '1.50'),
(102614, '【R】一之瀬 アスナ[Cleaning&Clearing]', 'Cleaning&Clearing', 'Fire', 46225, 'ブルーアーカイブ', '-', 'R', '50,197,212,227,242,257,0,0,0,257', 100004, 100045, '[O.N.G.E.K.I.]1.50-E-0076', '1.50'),
(102615, '【R】角楯 カリン[Cleaning&Clearing]', 'Cleaning&Clearing', 'Fire', 46226, 'ブルーアーカイブ', '-', 'R', '50,197,212,227,242,257,0,0,0,257', 100005, 100046, '[O.N.G.E.K.I.]1.50-E-0077', '1.50'),
(102616, '【R】室笠 アカネ[Cleaning&Clearing]', 'Cleaning&Clearing', 'Fire', 46227, 'ブルーアーカイブ', '-', 'R', '50,197,212,227,242,257,0,0,0,257', 105112, 105113, '[O.N.G.E.K.I.]1.50-E-0078', '1.50'),
(102617, '【R】飛鳥馬 トキ[Cleaning&Clearing]', 'Cleaning&Clearing', 'Fire', 46228, 'ブルーアーカイブ', '-', 'R', '50,197,212,227,242,257,0,0,0,257', 110092, 110093, '[O.N.G.E.K.I.]1.50-E-0079', '1.50'),
(102628, '【SR】陸八魔 アル[外部経営顧問としての入社提案]', '外部経営顧問としての入社提案', 'Fire', 46209, 'ブルーアーカイブ', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 120018, 120019, '[O.N.G.E.K.I.]1.50-E-0080', '1.50'),
(102629, '【SR】鬼方 カヨコ[雨の降る日の路地裏]', '雨の降る日の路地裏', 'Fire', 46210, 'ブルーアーカイブ', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 100025, 100066, '[O.N.G.E.K.I.]1.50-E-0081', '1.50'),
(102630, '【SR】浅黄 ムツキ[誰かからの特別な依頼]', '誰かからの特別な依頼', 'Fire', 46211, 'ブルーアーカイブ', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 100016, 100057, '[O.N.G.E.K.I.]1.50-E-0082', '1.50'),
(102631, '【SR】伊草 ハルカ[間違えて]', '間違えて', 'Fire', 46212, 'ブルーアーカイブ', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 115008, 115009, '[O.N.G.E.K.I.]1.50-E-0083', '1.50'),
(102632, '【SR】桐藤 ナギサ[紅茶とコク]', '紅茶とコク', 'Aqua', 46213, 'ブルーアーカイブ', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 110066, 110067, '[O.N.G.E.K.I.]1.50-E-0103', '1.50'),
(102633, '【SR】聖園 ミカ[屋根裏部屋のお姫様]', '屋根裏部屋のお姫様', 'Aqua', 46214, 'ブルーアーカイブ', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 105042, 105043, '[O.N.G.E.K.I.]1.50-E-0104', '1.50'),
(102634, '【SR】百合園 セイア[眠り姫からの誘い]', '眠り姫からの誘い', 'Aqua', 46215, 'ブルーアーカイブ', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 110086, 110087, '[O.N.G.E.K.I.]1.50-E-0105', '1.50'),
(102635, '【SR】阿慈谷 ヒフミ[偶然のお出かけ]', '偶然のお出かけ', 'Aqua', 46216, 'ブルーアーカイブ', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 105052, 105053, '[O.N.G.E.K.I.]1.50-E-0106', '1.50'),
(102636, '【SR】白洲 アズサ[計画]', '計画', 'Aqua', 46217, 'ブルーアーカイブ', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 100025, 100066, '[O.N.G.E.K.I.]1.50-E-0107', '1.50'),
(102637, '【SR】下江 コハル[何するつもり?]', '何するつもり?', 'Aqua', 46218, 'ブルーアーカイブ', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 115052, 115053, '[O.N.G.E.K.I.]1.50-E-0108', '1.50'),
(102638, '【SR】浦和 ハナコ[少し変わった女の子]', '少し変わった女の子', 'Aqua', 46219, 'ブルーアーカイブ', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 100018, 100059, '[O.N.G.E.K.I.]1.50-E-0109', '1.50'),
(102639, '【SR】柚鳥 ナツ[3時間目「実習発掘」]', '3時間目「実習発掘」', 'Leaf', 46220, 'ブルーアーカイブ', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 100017, 100058, '[O.N.G.E.K.I.]1.50-E-0084', '1.50'),
(102640, '【SR】杏山 カズサ[バッドタイミング]', 'バッドタイミング', 'Leaf', 46221, 'ブルーアーカイブ', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 110094, 110095, '[O.N.G.E.K.I.]1.50-E-0085', '1.50'),
(102641, '【SR】栗村 アイリ[ちょっとしたデート]', 'ちょっとしたデート', 'Leaf', 46222, 'ブルーアーカイブ', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 105000, 105001, '[O.N.G.E.K.I.]1.50-E-0086', '1.50'),
(102642, '【SR】伊原木 ヨシミ[その輝きの、すぐそばに]', 'その輝きの、すぐそばに', 'Leaf', 46223, 'ブルーアーカイブ', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 100033, 100074, '[O.N.G.E.K.I.]1.50-E-0087', '1.50'),
(102643, '【SR】美甘 ネル[輝く夜に]', '輝く夜に', 'Fire', 46224, 'ブルーアーカイブ', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 120018, 120019, '[O.N.G.E.K.I.]1.50-E-0088', '1.50'),
(102644, '【SR】一之瀬 アスナ[壊れたアスナ]', '壊れたアスナ', 'Fire', 46225, 'ブルーアーカイブ', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 110136, 110137, '[O.N.G.E.K.I.]1.50-E-0089', '1.50'),
(102645, '【SR】角楯 カリン[メイドカフェ!]', 'メイドカフェ!', 'Fire', 46226, 'ブルーアーカイブ', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 120074, 120075, '[O.N.G.E.K.I.]1.50-E-0090', '1.50'),
(102646, '【SR】室笠 アカネ[輝く午後を迎えて]', '輝く午後を迎えて', 'Fire', 46227, 'ブルーアーカイブ', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 130000, 130001, '[O.N.G.E.K.I.]1.50-E-0091', '1.50'),
(102647, '【SR】飛鳥馬 トキ[メイドの中のメイド]', 'メイドの中のメイド', 'Fire', 46228, 'ブルーアーカイブ', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 110094, 110095, '[O.N.G.E.K.I.]1.50-E-0092', '1.50'),
(102658, '【SSR】聖園 ミカ[聖園ミカ、ついに登場~☆ って感じかな?]', '聖園ミカ、ついに登場~☆ って感じかな?', 'Aqua', 46214, 'ブルーアーカイブ', '-', 'SSR', '67,287,307,319,326,337,0,0,0,342', 115134, 115135, '[O.N.G.E.K.I.]1.50-E-0110', '1.50'),
(102659, '【SSR】美甘 ネル[コールサイン・ダブルオー]', 'コールサイン・ダブルオー', 'Fire', 46224, 'ブルーアーカイブ', '-', 'SSR', '60,257,280,295,307,317,0,0,0,322', 120006, 120007, '[O.N.G.E.K.I.]1.50-E-0093', '1.50'),
(102660, '【SSR】阿慈谷 ヒフミ[私たちの、青春の物語を!!]', '私たちの、青春の物語を!!', 'Aqua', 46216, 'ブルーアーカイブ', '-', 'SSR', '60,257,280,295,307,317,0,0,0,322', 110098, 110099, '[O.N.G.E.K.I.]1.50-E-0111', '1.50'),
(102661, '【SSR】栗村 アイリ[-ive aLIVE!]', '-ive aLIVE!', 'Leaf', 46222, 'ブルーアーカイブ', '-', 'SSR', '60,257,280,295,307,317,0,0,0,322', 110068, 110069, '[O.N.G.E.K.I.]1.50-E-0094', '1.50'),
(102662, '【SSR】白洲 アズサ[Et Omnia Vanitas]', 'Et Omnia Vanitas', 'Aqua', 46217, 'ブルーアーカイブ', '-', 'SSR', '60,257,280,295,307,317,0,0,0,322', 100144, 100145, '[O.N.G.E.K.I.]1.50-E-0112', '1.50'),
(102663, '【SSR】陸八魔 アル[あっはは!これくらい簡単よ!]', 'あっはは!これくらい簡単よ!', 'Fire', 46209, 'ブルーアーカイブ', '-', 'SSR', '60,257,280,295,307,317,0,0,0,322', 110070, 110071, '[O.N.G.E.K.I.]1.50-E-0095', '1.50'),
(102664, '【SSR】桐藤 ナギサ[いつでも余裕をもって、優雅に]', 'いつでも余裕をもって、優雅に', 'Aqua', 46213, 'ブルーアーカイブ', '-', 'SSR', '60,257,280,295,307,317,0,0,0,322', 105020, 105021, '[O.N.G.E.K.I.]1.50-301', '1.50'),
(102665, '【SSR】飛鳥馬 トキ[命令を遂行します。]', '命令を遂行します。', 'Fire', 46228, 'ブルーアーカイブ', '-', 'SSR', '60,257,280,295,307,317,0,0,0,322', 125046, 125047, '[O.N.G.E.K.I.]1.50-302', '1.50'),
(102688, '【SSR】星咲 あかり[ハーバリウム・フェアリーズ]', 'ハーバリウム・フェアリーズ', 'Fire', 1000, '奏坂学園', '高校2年生', 'SSR', '67,290,310,322,332,340,0,0,0,345', 150001, 150002, '[O.N.G.E.K.I.]1.50-0029', '1.50'),
(102689, '【SSR】藤沢 柚子[ハーバリウム・フェアリーズ]', 'ハーバリウム・フェアリーズ', 'Leaf', 1001, '奏坂学園', '高校2年生', 'SSR', '67,290,310,322,332,340,0,0,0,345', 150005, 150006, '[O.N.G.E.K.I.]1.50-0030', '1.50'),
(102690, '【SSR】三角 葵[ハーバリウム・フェアリーズ]', 'ハーバリウム・フェアリーズ', 'Aqua', 1002, '奏坂学園', '高校2年生', 'SSR', '67,290,310,322,332,340,0,0,0,345', 150003, 150004, '[O.N.G.E.K.I.]1.50-0031', '1.50'),
(102691, '【SSR】高瀬 梨緒[ハーバリウム・フェアリーズ]', 'ハーバリウム・フェアリーズ', 'Aqua', 1003, '奏坂学園', '高校2年生', 'SSR', '67,290,310,322,332,340,0,0,0,345', 150003, 150004, '[O.N.G.E.K.I.]1.50-0032', '1.50'),
(102692, '【SSR】結城 莉玖[ハーバリウム・フェアリーズ]', 'ハーバリウム・フェアリーズ', 'Fire', 1004, '奏坂学園', '高校1年生', 'SSR', '67,290,310,322,332,340,0,0,0,345', 150001, 150002, '[O.N.G.E.K.I.]1.50-0033', '1.50'),
(102693, '【SSR】藍原 椿[ハーバリウム・フェアリーズ]', 'ハーバリウム・フェアリーズ', 'Leaf', 1005, '奏坂学園', '高校1年生', 'SSR', '67,290,310,322,332,340,0,0,0,345', 150005, 150006, '[O.N.G.E.K.I.]1.50-0034', '1.50'),
(102696, '【SSR】九條 楓[ハーバリウム・フェアリーズ]', 'ハーバリウム・フェアリーズ', 'Leaf', 1008, '奏坂学園', '高校3年生', 'SSR', '67,290,310,322,332,340,0,0,0,345', 150005, 150006, '[O.N.G.E.K.I.]1.50-0037', '1.50'),
(102699, '【SSR】逢坂 茜[ハーバリウム・フェアリーズ]', 'ハーバリウム・フェアリーズ', 'Fire', 1011, '奏坂学園', '高校3年生', 'SSR', '67,290,310,322,332,340,0,0,0,345', 150001, 150002, '[O.N.G.E.K.I.]1.50-0040', '1.50'),
(102700, '【SSR】珠洲島 有栖[ハーバリウム・フェアリーズ]', 'ハーバリウム・フェアリーズ', 'Aqua', 1012, '奏坂学園', '高校1年生', 'SSR', '67,290,310,322,332,340,0,0,0,345', 150003, 150004, '[O.N.G.E.K.I.]1.50-0041', '1.50'),
(102706, '【SSR】西館 ハク[アイシング・ドリーム]', 'アイシング・ドリーム', 'Fire', 32001, 'Re:ステージ!プリズムステップ', '-', 'SSR', '60,257,280,295,307,317,0,0,0,322', 130002, 130003, '[O.N.G.E.K.I.]1.50-0019', '1.50'),
(102707, '【SSR】結城 莉玖[O.N.G.E.K.I. 7th Anniversary]', 'O.N.G.E.K.I. 7th Anniversary', 'Fire', 1004, '奏坂学園', '高校1年生', 'SSR', '65,275,295,307,314,325,0,0,0,330', 100034, 100075, '[O.N.G.E.K.I.]1.50-0049', '1.50'),
(102708, '【SSR】桜井 春菜[O.N.G.E.K.I. 7th Anniversary]', 'O.N.G.E.K.I. 7th Anniversary', 'Fire', 1007, '奏坂学園', '高校2年生', 'SSR', '66,280,300,312,319,330,0,0,0,335', 135038, 135039, '[O.N.G.E.K.I.]1.50-0050', '1.50'),
(102709, '【SSR】井之原 小星[O.N.G.E.K.I. 7th Anniversary]', 'O.N.G.E.K.I. 7th Anniversary', 'Leaf', 1010, '奏坂学園', '高校1年生', 'SSR', '65,275,295,307,314,325,0,0,0,330', 135074, 135075, '[O.N.G.E.K.I.]1.50-0051', '1.50'),
(102710, '【SSR】逢坂 茜[O.N.G.E.K.I. 7th Anniversary]', 'O.N.G.E.K.I. 7th Anniversary', 'Fire', 1011, '奏坂学園', '高校3年生', 'SSR', '65,275,295,307,314,325,0,0,0,330', 105020, 105021, '[O.N.G.E.K.I.]1.50-0052', '1.50'),
(102711, '【SSR】柏木 美亜[O.N.G.E.K.I. 7th Anniversary]', 'O.N.G.E.K.I. 7th Anniversary', 'Fire', 1013, '奏坂学園', '中学2年生', 'SSR', '66,280,300,312,319,330,0,0,0,335', 115120, 115121, '[O.N.G.E.K.I.]1.50-0053', '1.50'),
(102721, '【SR】西郷・R・いろり[もちもちダウナー有能]', 'もちもちダウナー有能', 'Fire', 46019, '名取さな', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 120068, 120069, '[O.N.G.E.K.I.]1.50-E-0060', '1.50'),
(102722, '【SR】名取さな[名取探偵事務所所長]', '名取探偵事務所所長', 'Fire', 46019, '名取さな', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 120018, 120019, '[O.N.G.E.K.I.]1.50-E-0061', '1.50'),
(102723, '【SR】名取さな[みんなで踊ろうおしりぷり音頭]', 'みんなで踊ろうおしりぷり音頭', 'Fire', 46019, '名取さな', '-', 'SR', '50,222,237,252,267,282,0,0,0,282', 115068, 115069, '[O.N.G.E.K.I.]1.50-E-0062', '1.50'),
(102724, '【SSR】名取さな[名取探偵事務所開店!!]', '名取探偵事務所開店!!', 'Fire', 46019, '名取さな', '-', 'SSR', '65,270,291,304,314,322,0,0,0,327', 115132, 115133, '[O.N.G.E.K.I.]1.50-E-0063', '1.50'),
(102725, '【SSR】名取さな[さなちゃんねる夏祭り]', 'さなちゃんねる夏祭り', 'Fire', 46019, '名取さな', '-', 'SSR', '65,270,291,304,314,322,0,0,0,327', 110004, 110005, '[O.N.G.E.K.I.]1.50-E-0064', '1.50'),
(102726, '【SSR】名取さな[ハロー・マイ・バースデイ]', 'ハロー・マイ・バースデイ', 'Fire', 46019, '名取さな', '-', 'SSR', '65,270,291,304,314,322,0,0,0,327', 130086, 130087, '[O.N.G.E.K.I.]1.50-E-0065', '1.50'),
(102727, '【SSR】名取さな[王国からの招待状]', '王国からの招待状', 'Fire', 46019, '名取さな', '-', 'SSR', '65,270,291,304,314,322,0,0,0,327', 120006, 120007, '[O.N.G.E.K.I.]1.50-E-0066', '1.50'),
(102734, '【SSR】旭 日向[Earthly Light(集合Ver.)]', 'Earthly Light(集合Ver.)', 'Leaf', 32001, 'Re:ステージ!プリズムステップ', '-', 'SSR', '60,257,280,295,307,317,0,0,0,322', 100000, 100041, '[O.N.G.E.K.I.]1.50-371', '1.50'),
(102738, '【SR+】珠洲島 有栖[Nexture 05「SIRIUS」]', 'Nexture 05「SIRIUS」', 'Aqua', 1012, '奏坂学園', '高校1年生', 'SRPlus', '53,240,263,278,290,300,0,0,0,300', 120072, 120073, '[O.N.G.E.K.I.]Special Card', '1.50');

View File

@@ -0,0 +1,11 @@
CREATE TABLE sega_card_timestamp
(
id BIGINT AUTO_INCREMENT NOT NULL,
created_at datetime(3) NOT NULL,
updated_at datetime(3) NOT NULL,
game VARCHAR(255) NOT NULL,
card_id BIGINT NOT NULL,
PRIMARY KEY (id),
CONSTRAINT fk_sega_card_timestamp_on_sega_card FOREIGN KEY (card_id) REFERENCES sega_card (id) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT unq_sega_card_timestamp_on_game_card UNIQUE (game, card_id)
);

View File

@@ -0,0 +1,30 @@
-- Add new unlock columns
ALTER TABLE aqua_game_options ADD COLUMN mai2_unlock_music BIT NOT NULL DEFAULT 0;
ALTER TABLE aqua_game_options ADD COLUMN mai2_unlock_chara BIT NOT NULL DEFAULT 0;
ALTER TABLE aqua_game_options ADD COLUMN mai2_unlock_chara_max_level BIT NOT NULL DEFAULT 0;
ALTER TABLE aqua_game_options ADD COLUMN mai2_unlock_partners BIT NOT NULL DEFAULT 0;
ALTER TABLE aqua_game_options ADD COLUMN mai2_unlock_collectables BIT NOT NULL DEFAULT 0;
ALTER TABLE aqua_game_options ADD COLUMN mai2_unlock_tickets BIT NOT NULL DEFAULT 0;
ALTER TABLE aqua_game_options ADD COLUMN wacca_unlock_music BIT NOT NULL DEFAULT 0;
ALTER TABLE aqua_game_options ADD COLUMN wacca_unlock_plates BIT NOT NULL DEFAULT 0;
ALTER TABLE aqua_game_options ADD COLUMN wacca_unlock_collectables BIT NOT NULL DEFAULT 0;
ALTER TABLE aqua_game_options ADD COLUMN wacca_unlock_tickets BIT NOT NULL DEFAULT 0;
-- Migrate data
UPDATE aqua_game_options SET
mai2_unlock_music = unlock_music,
mai2_unlock_chara = unlock_chara,
mai2_unlock_chara_max_level = unlock_chara,
mai2_unlock_partners = unlock_chara,
mai2_unlock_collectables = unlock_collectables,
mai2_unlock_tickets = unlock_tickets,
wacca_unlock_music = unlock_music,
wacca_unlock_plates = unlock_chara | unlock_collectables,
wacca_unlock_collectables = unlock_collectables,
wacca_unlock_tickets = unlock_tickets;
-- Drop old columns
ALTER TABLE aqua_game_options DROP COLUMN unlock_music;
ALTER TABLE aqua_game_options DROP COLUMN unlock_chara;
ALTER TABLE aqua_game_options DROP COLUMN unlock_collectables;
ALTER TABLE aqua_game_options DROP COLUMN unlock_tickets;

View File

@@ -0,0 +1,112 @@
INSERT INTO chusan_game_event (id, type, end_date, start_date, enable)
VALUES
(17090, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17091, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17092, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17093, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17094, 3, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17095, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17096, 11, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17100, 3, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17101, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17102, 3, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17103, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17104, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17105, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17106, 17, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17107, 4, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17108, 5, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17109, 7, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17150, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17151, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17152, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17153, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17154, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17155, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17156, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17157, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17158, 14, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17159, 3, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17160, 11, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17200, 3, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17201, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17202, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17203, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17204, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17205, 3, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17206, 10, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17207, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17208, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17209, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17210, 17, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17211, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17213, 4, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17214, 5, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17250, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17251, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17252, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17256, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17257, 3, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17258, 3, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17259, 11, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17260, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17261, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17262, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17263, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17300, 3, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17301, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17302, 3, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17303, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17304, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17305, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17306, 17, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17307, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17308, 14, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17309, 4, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17310, 5, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17350, 3, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17351, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17352, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17353, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17354, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17355, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17356, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17357, 10, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17358, 11, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17380, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17382, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17383, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17384, 7, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17385, 7, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17386, 3, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17400, 3, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17401, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17402, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17403, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17404, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17405, 3, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17406, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17407, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17408, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17409, 17, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17410, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17411, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17412, 14, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17413, 4, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17414, 5, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17450, 3, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17451, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17452, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17453, 8, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17454, 3, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17480, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17481, 3, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17482, 1, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17483, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17484, 2, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true),
(17485, 17, '2029-01-01 00:00:00.000000','2019-01-01 00:00:00.000000',true)
ON DUPLICATE KEY UPDATE
type = VALUES(type),
end_date = VALUES(end_date),
start_date = VALUES(start_date),
enable = VALUES(enable);