[chu3] X-VERSE & LINKED VERSE (#204)

This commit is contained in:
Raymond
2026-02-22 15:23:21 -05:00
committed by GitHub
parent e755bc9380
commit ebb2e2e4a4
17 changed files with 285 additions and 69 deletions

View File

@@ -3,9 +3,10 @@
import { CHU3_MATCHINGS } from "../../libs/config.js";
import type { ChusanMatchingOption, GameOption } from "../../libs/generalTypes.js";
import { t, ts } from "../../libs/i18n.js";
import { DATA, SETTING } from "../../libs/sdk.js";
import { DATA, GAME, SETTING, USER } from "../../libs/sdk.js";
import StatusOverlays from "../StatusOverlays.svelte";
import GameSettingFields from "./GameSettingFields.svelte";
import { EN_REF } from "../../libs/i18n/en_ref.js";
let custom = false
let overlay = false
@@ -16,6 +17,7 @@
let symbols: Record<number, number> = {};
let allItems: Record<string, Record<string, { name: string }>> = {}
let submitting: string | undefined | null;
let settings: Record<string, GameOption> = {};
let existingUrl = "";
SETTING.get().then(s => {
@@ -30,6 +32,7 @@
if (opt.key.substring(0, symbolKey.length) == symbolKey && opt.value)
symbols[parseInt(opt.key.substring(symbolKey.length))] = opt.value;
})
settings = Object.fromEntries(s.map(v => [v.key, v]))
})
async function fetchSymbolData() {
@@ -49,6 +52,14 @@
return true
}
async function updateLvDifficulty() {
if (submitting) return false
submitting = `chusanLvDifficulty`
await SETTING.set("chusanLvDifficulty", settings["chusanLvDifficulty"].value).catch(e => error = e.message).finally(() => submitting = null);
changed = changed.filter(v => v != `chusanLvDifficulty`)
return true
}
// Click on "Custom" option"
function clickCustom() {
custom = true
@@ -66,13 +77,47 @@
existingUrl = opt.matching
}).catch(e => error = e.message)
}
let linkedVerseAvailable = false;
USER.me().then(async me => {
let summary = await GAME.userSummary(me.username, "chu3")
let version = summary.lastVersion.split(".");
if (version[0] == "2" && parseInt(version[1]) >= 40)
linkedVerseAvailable = true;
})
</script>
<StatusOverlays {error} {loading}/>
<div class="matching">
<h2>{t("userbox.header.matching")}</h2>
<p class="notice">{t("settings.cabNotice")}</p>
<h2>{t("userbox.header.matching")}{linkedVerseAvailable ? ` & ${t("userbox.header.linkedVerse")}` : ""}</h2>
<blockquote class="info">
{t("settings.cabNotice")}
{#if linkedVerseAvailable}
{t("userbox.lv.diffnotice")}
{/if}
</blockquote>
{#if linkedVerseAvailable}
<GameSettingFields game="chu3-linked-verse" />
{#if settings["chusanLvDifficulty"]}
<div class="field">
<label for={`chusanLvDifficulty`}>{ts(`userbox.lv.difficulty`)}</label>
<div>
<select bind:value={settings["chusanLvDifficulty"].value} id={`chusanLvDifficulty`} on:change={() => {changed = [...changed, `chusanLvDifficulty`];}}>
{#each {length: 5}, i}
<option value={i + 1}>{t(`userbox.lv.difficulty.${i + 1}` as keyof typeof EN_REF)}</option>
{/each}
</select>
{#if changed.includes(`chusanLvDifficulty`)}
<button transition:slide={{axis: "x"}} disabled={!!submitting} on:click={updateLvDifficulty}>
{t("settings.profile.save")}
</button>
{/if}
</div>
</div>
{/if}
{/if}
<div class="matching-selector">
<button on:click={_ => overlay = true}>{t('userbox.matching.select')}</button>

View File

@@ -31,7 +31,7 @@
// Available (unlocked) options for each kind of item
// In allItems: 'namePlate', 'frame', 'trophy', 'mapIcon', 'systemVoice', 'avatarAccessory'
let allItems: Record<string, Record<string, { name: string }>> = {}
let iKinds = { namePlate: 1, frame: 2, trophy: 3, trophySub1: 4, trophySub2: 5, mapIcon: 8, systemVoice: 9, avatarAccessory: 11 }
let iKinds = { namePlate: 1, frame: 2, trophy: 3, trophySub1: 4, trophySub2: 5, mapIcon: 8, systemVoice: 9, avatarAccessory: 11, stage: 13 }
// In userbox: 'nameplateId', 'frameId', 'trophyId', 'mapIconId', 'voiceId', 'avatar{Wear/Head/Face/Skin/Item/Front/Back}'
let userbox: UserBox
let avatarKinds = ['Wear', 'Head', 'Face', 'Skin', 'Item', 'Front', 'Back'] as const
@@ -256,7 +256,7 @@
type OnlyNumberPropsOf<T extends Record<string, any>> = {[Prop in keyof T as (T[Prop] extends number ? Prop : never)]: T[Prop]}
let userboxSelected: keyof OnlyNumberPropsOf<UserBox> = "avatarWear";
const userboxNewOptions = ["systemVoice", "frame", "trophy", "mapIcon"]
const userboxNewOptions = ["systemVoice", "frame", "trophy", "mapIcon", "stage"]
async function userboxSafeDrop(event: Event & { currentTarget: EventTarget & HTMLInputElement; }) {
if (!event.target) return null;
@@ -317,21 +317,23 @@
{#if !USERBOX_ENABLED.value || !USERBOX_INSTALLED}
<div class="fields">
{#each userItems as { iKey, ubKey, items }, i}
<div class="field">
<label for={ubKey}>{ts(`userbox.${ubKey}`)}</label>
<div>
<select bind:value={userbox[ubKey]} id={ubKey} on:change={() => changed = [...changed, ubKey]}>
{#each items as option}
<option value={option.itemId}>{allItems[iKey][option.itemId]?.name || `(unknown ${option.itemId})`}</option>
{/each}
</select>
{#if changed.includes(ubKey)}
<button transition:slide={{axis: "x"}} on:click={() => submit(ubKey)} disabled={!!submitting}>
{t("settings.profile.save")}
</button>
{/if}
{#if items.length > 0}
<div class="field">
<label for={ubKey}>{ts(`userbox.${ubKey}`)}</label>
<div>
<select bind:value={userbox[ubKey]} id={ubKey} on:change={() => changed = [...changed, ubKey]}>
{#each items as option}
<option value={option.itemId}>{allItems[iKey][option.itemId]?.name || `(unknown ${option.itemId})`}</option>
{/each}
</select>
{#if changed.includes(ubKey)}
<button transition:slide={{axis: "x"}} on:click={() => submit(ubKey)} disabled={!!submitting}>
{t("settings.profile.save")}
</button>
{/if}
</div>
</div>
</div>
{/if}
{/each}
</div>
{:else}
@@ -372,21 +374,23 @@
</div>
<div class="fields">
{#each userItems.filter(i => userboxNewOptions.includes(i.iKey)) as { iKey, ubKey, items }, i}
<div class="field">
<label for={ubKey}>{ts(`userbox.${ubKey}`)}</label>
<div>
<select bind:value={userbox[ubKey]} id={ubKey} on:change={() => changed = [...changed, ubKey]}>
{#each items as option}
<option value={option.itemId}>{allItems[iKey][option.itemId]?.name || `(unknown ${option.itemId})`}</option>
{/each}
</select>
{#if changed.includes(ubKey)}
<button transition:slide={{axis: "x"}} on:click={() => submit(ubKey)} disabled={!!submitting}>
{t("settings.profile.save")}
</button>
{/if}
{#if items.length > 0}
<div class="field">
<label for={ubKey}>{ts(`userbox.${ubKey}`)}</label>
<div>
<select bind:value={userbox[ubKey]} id={ubKey} on:change={() => changed = [...changed, ubKey]}>
{#each items as option}
<option value={option.itemId}>{allItems[iKey][option.itemId]?.name || `(unknown ${option.itemId})`}</option>
{/each}
</select>
{#if changed.includes(ubKey)}
<button transition:slide={{axis: "x"}} on:click={() => submit(ubKey)} disabled={!!submitting}>
{t("settings.profile.save")}
</button>
{/if}
</div>
</div>
</div>
{/if}
{/each}
</div>
{/if}

View File

@@ -29,6 +29,14 @@ export const HAS_USERBOX_ASSETS = true
// Matching servers
export const CHU3_MATCHINGS: ChusanMatchingOption[] = [
{
name: "Yukiotoko",
ui: "https://yukiotoko.metatable.sh/",
guide: "https://github.com/MewoLab/AquaDX/blob/v1-dev/docs/chu3-national-matching.md",
matching: "http://yukiotoko.chara.lol:9004/",
reflector: "http://yukiotoko.chara.lol:50201/",
coop: ["Missless", "CozyNet", "GMG"]
},
{
name: "林国对战",
ui: "https://chu3-match.sega.ink/rooms",
@@ -37,12 +45,4 @@ export const CHU3_MATCHINGS: ChusanMatchingOption[] = [
reflector: "http://reflector.naominet.live:18080/",
coop: ["RinNET", "MysteriaNET"],
},
{
name: "Yukiotoko",
ui: "https://yukiotoko.metatable.sh/",
guide: "https://github.com/MewoLab/AquaDX/blob/v1-dev/docs/chu3-national-matching.md",
matching: "http://yukiotoko.chara.lol:9004/",
reflector: "http://yukiotoko.chara.lol:50201/",
coop: ["Missless", "CozyNet", "GMG"]
}
]

View File

@@ -194,6 +194,8 @@ export const EN_REF_SETTINGS = {
'settings.fields.chusanTeamName.desc': 'Customize the text displayed on the top of your profile.',
'settings.fields.chusanInfinitePenguins.name': 'Infinite Penguins',
'settings.fields.chusanInfinitePenguins.desc': 'Set penguin statues for character level prompting to 999.',
'settings.fields.chusanLvUnlockAll.name': 'Unlock Linked Gates',
'settings.fields.chusanLvUnlockAll.desc': 'Incurs a long animated sequence, cannot be undone',
'settings.fields.chusanMatchingReflector.name': 'Matching Server Reflector',
'settings.fields.chusanMatchingReflector.desc': 'URL of the national matching server\'s UDP reflector.',
'settings.fields.chusanMatchingServer.name': 'Matching Server',
@@ -224,7 +226,7 @@ export const EN_REF_SETTINGS = {
'settings.profile.unchanged': 'Unchanged',
'settings.export': 'Export Player Data',
'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': "These settings only apply for your keychip. If you're playing on someone else's setup, please ask them to change these settings.",
'settings.siteNotice': "These settings only apply to the website.",
'settings.regionNotice': "These settings are shared amongst Mai, Ongeki and Chuni.",
'settings.regionSelector.title': "Prefecture Selector",
@@ -236,6 +238,7 @@ export const EN_REF_USERBOX = {
'userbox.header.general': 'General Settings',
'userbox.header.matching': 'National Matching',
'userbox.header.matching.symbolChat': 'Chat Symbols (Matching)',
'userbox.header.linkedVerse': 'Linked Verse',
'userbox.header.userbox': 'UserBox Settings',
'userbox.header.preview': 'UserBox Preview',
'userbox.nameplateId': 'Nameplate',
@@ -245,6 +248,7 @@ export const EN_REF_USERBOX = {
'userbox.trophyIdSub2': 'Trophy Sub #2 (Title)',
'userbox.mapIconId': 'Map Icon',
'userbox.voiceId': 'System Voice',
'userbox.stageId': 'Stage',
'userbox.avatarWear': 'Avatar Wear',
'userbox.avatarHead': 'Avatar Head',
'userbox.avatarFace': 'Avatar Face',
@@ -265,6 +269,14 @@ export const EN_REF_USERBOX = {
'userbox.matching.symbolChat': 'Message Choice',
'userbox.matching.symbolChat.default': 'Default',
'userbox.lv.diffnotice': 'In Linked Verse, you will be matched separately based on your difficulty.',
'userbox.lv.difficulty': 'Linked Verse Difficulty',
'userbox.lv.difficulty.1': 'LEVEL V (1000 life, MASTER only)',
'userbox.lv.difficulty.2': 'LEVEL IV (3000 life, MASTER only)',
'userbox.lv.difficulty.3': 'LEVEL III (5000 life, MASTER only)',
'userbox.lv.difficulty.4': 'LEVEL II (5000 life, MASTER & EXPERT only)',
'userbox.lv.difficulty.5': 'LEVEL I (5000 life, ALL difficulties)',
'userbox.new.name': 'AquaBox',
'userbox.new.setup': 'Drag and drop your Chuni game folder (Lumi or newer) into the box below to display UserBoxes with their nameplate & avatar. All files are handled in-browser.',
'userbox.new.setup.notice': 'Select the highest folder containing your game data.',
@@ -286,7 +298,7 @@ export const EN_REF_USERBOX = {
export const EN_REF_MAI_PHOTO = {
'maiphoto.title': 'Mai Memorial Photo Gallery',
'maiphoto.url_warning': 'Note: If you want to share a photo with your friend, please save the photo. Do not copy image URL because the URL contains sensitive information.',
'maiphoto.url_warning': 'If you want to share a photo with your friend, please save the photo. Do not copy image URL because the URL contains sensitive information.',
'maiphoto.none': 'No photo found. You can upload photo by clicking upload at the end of each game session.',
}

View File

@@ -19,7 +19,7 @@ allnet.server.hide-port=true
allnet.server.place-name=AquaDX
## This enables client serial validation during power on request using keychip table in database.
## Only enable this if you know what you are doing.
allnet.server.check-keychip=false
allnet.server.check-keychip=true
## Interval between keychip session clean up checks in ms. Default is 1 day.
allnet.server.keychip-ses-clean-interval=86400000
## Token that haven't been used for this amount of time will be removed from the database. Default is 2 days.
@@ -49,10 +49,10 @@ game.chunithm.team-name=
## This enables team function if you set team name here. Leave this blank to use default
game.chusan.team-name=
## This sets the reflector url for global matching
game.chusan.reflector-url=http://reflector.naominet.live:18080/
game.chusan.reflector-url=http://yukiotoko.chara.lol:50201/
## This sets the matching server url.
## When this is set, we will sync with the external matching url so that we can match with more players.
game.chusan.external-matching=https://chu3-match.sega.ink/
game.chusan.external-matching=http://yukiotoko.chara.lol:9004/
## This enables user use login bonus function if set to true.
## NOTE: THIS IS NOT TESTED, it's implemented by someone very inexperienced and might not work.
game.chusan.loginbonus-enable=false

View File

@@ -48,6 +48,11 @@ class AquaGameOptions(
@SettingField("chu3-matching")
var chusanMatchingReflector: String = "",
@SettingField("chu3-linked-verse")
var chusanLvUnlockAll: Boolean = false,
@SettingField("chu3-linked-verse")
var chusanLvDifficulty: Int = 1,
@SettingField("chu3-matching-chat")
var chusanSymbolChat1: Int? = null,

View File

@@ -41,6 +41,7 @@ class Chusan(
"trophyIdSub2" to { u, v -> u.trophyIdSub2 = v.int },
"mapIconId" to { u, v -> u.mapIconId = v.int },
"voiceId" to { u, v -> u.voiceId = v.int },
"stageId" to { u, v -> u.stageId = v.int },
"characterId" to { u, v -> u.characterId = v.int },
"avatarWear" to { u, v -> u.avatarWear = v.int },
"avatarHead" to { u, v -> u.avatarHead = v.int },

View File

@@ -13,16 +13,16 @@ class ChusanVersionHelper(val db: Chu3Repos) {
val cache: MutableMap<String, String> = mutableMapOf()
// Obtain the cabinet's most recent version
operator fun get(clientId: String): String {
fun get(clientId: String): String? {
// Try to find the version in the cache
cache[clientId]?.let { return it }
// Not found, check the most recent user
return db.userData.findTopByLastClientIdOrderByLastPlayDateDesc(clientId)?.lastDataVersion
?.also { cache[clientId] = it } ?: "2.25.13".also { log.warn("No version found for $clientId") }
?.also { cache[clientId] = it }
}
operator fun set(clientId: String, version: String) {
fun set(clientId: String, version: String) {
cache[clientId] = version
}
}

View File

@@ -9,6 +9,7 @@ import icu.samnyan.aqua.sega.chusan.model.userdata.Chu3UserItem
import icu.samnyan.aqua.sega.chusan.model.userdata.UserMusicDetail
import icu.samnyan.aqua.sega.general.model.CardStatus
import icu.samnyan.aqua.sega.general.model.UserRecentRating
import org.springframework.data.repository.findByIdOrNull
import java.time.format.DateTimeFormatter
@Suppress("UNCHECKED_CAST")
@@ -28,13 +29,19 @@ fun ChusanController.chusanInit() {
mapOf("type" to type, "length" to 0, "gameRankingList" to lst)
}
// VERSE VERSE VERSE VERSE VERSE VERSE VERSE VERSE VERSE VERSE VERSE VERSE VERSE VERSE VERSE VERSE VERSE VERSE
"GetGameCourseLevel" {
// gameCourseLevelList: [{courseId: int, startDate: date, endDate: date}]
mapOf("length" to 0, "gameCourseLevelList" to listOf(
val opts = TokenChecker.getCurrentSession()?.user?.gameOptions
val lst = listOf(
// Unlock Challenge
mapOf("courseId" to 300004, "startDate" to "2019-01-01 00:00:00", "endDate" to "2077-01-01 11:45:14"),
mapOf("courseId" to 300009, "startDate" to "2019-01-01 00:00:00", "endDate" to "2077-01-01 11:45:14")
))
mapOf("courseId" to 300009, "startDate" to "2019-01-01 00:00:00", "endDate" to "2077-01-01 11:45:14"),
) + (0..9).toList().map {
// Linked Verse
mapOf("courseId" to 500000 + (opts?.chusanLvDifficulty ?: 5) + (it * 100), "startDate" to "2019-01-01 00:00:00", "endDate" to "2077-01-01 11:45:14")
}
mapOf("length" to lst.size, "gameCourseLevelList" to lst)
}
"GetGameUCCondition" {
@@ -55,6 +62,40 @@ fun ChusanController.chusanInit() {
db.userChallenge.findByUser_Card_ExtId(uid)
}
// The implementation here isn't preferable, but it's functional
// Condition checks if the user beat a song from a previous stage and unlocks it if so
fun getLinkedVerseCampaign(): List<Any> {
val opts = TokenChecker.getCurrentSession()?.user?.gameOptions
// No other conditions appear to work properly, so the request for the gates is pretty large. Sorry
fun getChartConditions(musicId: Int) =
if (opts != null && opts.chusanLvUnlockAll) {
listOf( mapOf("type" to 3, "conditionId" to 0, "logicalOpe" to 1, "startDate" to "2024-03-08 01:00:00", "endDate" to "2099-12-31 00:00:00") )
} else {
(0..5).toList().map {
mapOf("type" to 26, "conditionId" to (musicId * 100) + it, "logicalOpe" to 2, "startDate" to "2024-03-08 01:00:00", "endDate" to "2099-12-31 00:00:00") }}
return db.gameLinkedVerse.findAll().map {
val lst = getChartConditions(it.musicId)
mapOf("linkedVerseId" to it.id + 1, "length" to lst.size, "conditionList" to lst)
} + // ORIGIN is always left unlocked by default
listOf(mapOf("linkedVerseId" to 10001, "length" to 1, "conditionList" to listOf(
mapOf("type" to 3, "conditionId" to 0, "logicalOpe" to 1, "startDate" to "2024-03-08 01:00:00", "endDate" to "2099-12-31 00:00:00")
)))
}
"GetGameLVConditionOpen" {
val lst = getLinkedVerseCampaign()
mapOf("length" to lst.size, "gameLinkedVerseConditionOpenList" to lst)
}
"GetGameLVConditionUnlock" {
val lst = getLinkedVerseCampaign()
mapOf("length" to lst.size, "gameLinkedVerseConditionUnlockList" to lst)
}
"GetUserLV" {
val lst = db.userLinkedVerse.findByUser_Card_ExtId(uid)
mapOf("length" to lst.size, "userLinkedVerseList" to lst, "userId" to uid)
}
"GetUserRecMusic".paged("userRecMusicList") {
// musicId: int, recMusicList: string
// musicId cannot be the same with the id in recMusicList
@@ -66,14 +107,9 @@ fun ChusanController.chusanInit() {
}
"GetUserRecRating".paged("userRecRatingList") {
// ratingMin: int, ratingMax: int, recMusicList: string
// This doesn't work
// listOf(
// mapOf("ratingMin" to 0, "ratingMax" to 30, "recMusicList" to "2387,1;2658,1")
// )
// Unimplemented for now
empty
}
// VERSE VERSE VERSE VERSE VERSE VERSE VERSE VERSE VERSE VERSE VERSE VERSE VERSE VERSE VERSE VERSE VERSE VERSE
// Stub handlers
"GetGameIdlist" { """{"type":"${data["type"]}","length":"0","gameIdlistList":[]}""" }
@@ -277,12 +313,16 @@ fun ChusanController.chusanInit() {
"GetUserTeam" {
val playDate = parsing { data["playDate"] as String }
val team = db.userData.findByCard_ExtId(uid)?.card?.aquaUser?.gameOptions?.chusanTeamName?.some
?: props.teamName?.some ?: "一緒に歌おう!"
?: props.teamName?.some
mapOf(
"userId" to uid, "teamId" to 1, "teamRank" to 1, "teamName" to team,
"userTeamPoint" to mapOf("userId" to uid, "teamId" to 1, "orderId" to 1, "teamPoint" to 1, "aggrDate" to playDate)
)
if (team.isNullOrEmpty())
mapOf("userId" to uid, "teamId" to 0)
else
// TODO: true team implementation
mapOf(
"userId" to uid, "teamId" to 1, "teamRank" to 1, "teamName" to team,
"userTeamPoint" to mapOf("userId" to uid, "teamId" to 1, "orderId" to 1, "teamPoint" to 1, "aggrDate" to playDate)
)
}
"GetUserRegion" {
@@ -313,7 +353,7 @@ fun ChusanController.chusanInit() {
mapOf(
"gameSetting" to mapOf(
"romVersion" to "$version.00",
"dataVersion" to versionHelper[data["clientId"].toString()],
"dataVersion" to (versionHelper.get(data["clientId"].toString()) ?: "$version.01"),
"isMaintenance" to false,
"requestInterval" to 0,
"rebootStartTime" to now.minusHours(4).format(fmt),
@@ -322,8 +362,6 @@ fun ChusanController.chusanInit() {
"maxCountCharacter" to 300,
"maxCountItem" to 300,
"maxCountMusic" to 300,
// "matchStartTime" to now.minusHours(1).format(fmt),
// "matchEndTime" to now.plusHours(7).format(fmt),
"matchStartTime" to now.withHour(0).withMinute(1).withSecond(0).format(fmt),
"matchEndTime" to now.withHour(23).withMinute(59).withSecond(0).format(fmt),
"matchTimeLimit" to 10,

View File

@@ -6,6 +6,7 @@ import icu.samnyan.aqua.sega.chusan.model.request.Chu3UserAll
import icu.samnyan.aqua.sega.chusan.model.userdata.*
import icu.samnyan.aqua.sega.general.model.CardStatus
import icu.samnyan.aqua.sega.general.model.UserRecentRating
import kotlinx.serialization.encodeToString
@Suppress("UNCHECKED_CAST")
fun ChusanController.upsertApiInit() {
@@ -55,7 +56,7 @@ fun ChusanController.upsertApiInit() {
db.userRegions.save(region)
}
versionHelper[u.lastClientId] = u.lastDataVersion
versionHelper.set(u.lastClientId, u.lastDataVersion)
// Set users
listOfNotNull(
@@ -159,6 +160,13 @@ fun ChusanController.upsertApiInit() {
db.userChallenge.saveAll(list.distinctBy { it.unlockChallengeId }.mapApply {
id = db.userChallenge.findByUserAndUnlockChallengeId(u, unlockChallengeId)?.id ?: 0 }) }
userLinkedVerseList?.let { list ->
db.userLinkedVerse.saveAll(list.map{
it.apply{ it.user = u }
}.distinctBy { it.linkedVerseId }.mapApply {
id = db.userLinkedVerse.findByUserAndLinkedVerseId(u, linkedVerseId)?.id ?: 0 })
}
// Need testing
// userLoginBonusList?.let { list ->
// db.userLoginBonus.saveAll(list.distinctBy { it["presetId"] as String }.map {

View File

@@ -91,4 +91,13 @@ class GameLoginBonusPreset : IdExposedEntity() {
var version = 0
var presetName: String? = null
var isEnabled = false
}
@Entity(name = "ChusanGameLinkedVerse")
@Table(name = "chusan_game_linked_verse")
class GameLinkedVerse: IdExposedEntity() {
var musicId = 0
var name: String? = null
var startDate: LocalDateTime? = null
var endDate: LocalDateTime? = null
}

View File

@@ -68,6 +68,13 @@ interface Chu3UserCourseRepo : Chu3UserLinked<UserCourse> {
fun findByUserAndCourseId(user: Chu3UserData, courseId: Int): UserCourse?
}
interface Chu3UserLinkedVerseRepo : Chu3UserLinked<Chu3UserLinkedVerse> {
fun findAllByUser(user: Chu3UserData): List<Chu3UserLinkedVerse?>
fun findByUserAndLinkedVerseId(user: Chu3UserData, linkedVerseId: Int): Chu3UserLinkedVerse?
}
interface Chu3GameLinkedVerseRepo : JpaRepository<GameLinkedVerse, Int>
interface Chu3UserDataRepo : GenericUserDataRepo<Chu3UserData> {
fun findTopByLastClientIdOrderByLastPlayDateDesc(lastClientId: String): Chu3UserData?
}
@@ -201,10 +208,12 @@ class Chu3Repos(
val netBattleLog: Chu3NetBattleLogRepo,
val userMisc: Chu3UserMiscRepo,
val userChallenge: Chu3UserChallengeRepo,
val userLinkedVerse: Chu3UserLinkedVerseRepo,
val gameCharge: Chu3GameChargeRepo,
val gameEvent: Chu3GameEventRepo,
val gameGachaCard: Chu3GameGachaCardRepo,
val gameGacha: Chu3GameGachaRepo,
val gameLoginBonusPresets: Chu3GameLoginBonusPresetsRepo,
val gameLoginBonus: Chu3GameLoginBonusRepo
val gameLoginBonus: Chu3GameLoginBonusRepo,
val gameLinkedVerse: Chu3GameLinkedVerseRepo
)

View File

@@ -64,4 +64,5 @@ class Chu3UserAll(
var userCMissionList: List<UserCMissionResp>? = null,
var userFavoriteMusicList: List<FavNewMusic>? = null,
var userUnlockChallengeList: List<Chu3UserChallenge>? = null,
var userLinkedVerseList: List<Chu3UserLinkedVerse>? = null
)

View File

@@ -116,6 +116,7 @@ class Chu3UserData : BaseEntity(), IUserData {
var netBattleConsecutiveWinCount = 0
var charaIllustId = 0
var skillId = 0
var stageId = 0
var overPowerPoint = 0
var overPowerRate = 0
var overPowerLowerRank = 0

View File

@@ -0,0 +1,28 @@
package icu.samnyan.aqua.sega.chusan.model.userdata
import jakarta.persistence.Entity
import jakarta.persistence.Table
import jakarta.persistence.UniqueConstraint
import java.time.LocalDateTime
@Entity(name = "ChusanUserLinkedVerse")
@Table(name = "chusan_user_linked_verse", uniqueConstraints = [UniqueConstraint(columnNames = ["user_id", "linked_verse_id"])])
class Chu3UserLinkedVerse : Chu3UserEntity() {
var linkedVerseId = 0
var clearCourseId = 0
var clearCourseLevel = 0
var clearDate: LocalDateTime = LocalDateTime.now()
var clearUserId1: Long = 0
var clearUserId2: Long = 0
var clearUserId3: Long = 0
var clearUserName0 = ""
var clearUserName1 = ""
var clearUserName2 = ""
var clearUserName3 = ""
var isFirstClear = false
var numClear = 0
var statusOpen = 0
var statusUnlock = 0
var progress: String = ""
}

View File

@@ -0,0 +1,2 @@
ALTER TABLE chusan_user_data
ADD stage_id INT NOT NULL DEFAULT 0;

View File

@@ -0,0 +1,53 @@
ALTER TABLE aqua_game_options
ADD chusan_lv_difficulty INT NOT NULL DEFAULT 5;
ALTER TABLE aqua_game_options
ADD chusan_lv_unlock_all BOOLEAN DEFAULT FALSE;
CREATE TABLE chusan_game_linked_verse (
id BIGINT auto_increment PRIMARY KEY,
name VARCHAR(255) NOT NULL,
start_date DATETIME NOT NULL,
end_date DATETIME NOT NULL,
music_id INTEGER NOT NULL
);
CREATE TABLE chusan_user_linked_verse
(
id BIGINT AUTO_INCREMENT NOT NULL PRIMARY KEY,
progress VARCHAR(64),
user_id BIGINT NOT NULL,
linked_verse_id INTEGER NOT NULL,
clear_course_id INTEGER NOT NULL,
clear_course_level INTEGER NOT NULL,
clear_date DATETIME NOT NULL,
clear_user_id1 BIGINT NOT NULL,
clear_user_id2 BIGINT NOT NULL,
clear_user_id3 BIGINT NOT NULL,
clear_user_name0 VARCHAR(8) NOT NULL,
clear_user_name1 VARCHAR(8) NOT NULL,
clear_user_name2 VARCHAR(8) NOT NULL,
clear_user_name3 VARCHAR(8) NOT NULL,
is_first_clear BOOLEAN NOT NULL DEFAULT FALSE,
num_clear INTEGER NOT NULL,
status_open INTEGER NOT NULL,
status_unlock INTEGER NOT NULL,
CONSTRAINT fku_chusan_user_linked_verse FOREIGN KEY (user_id) REFERENCES chusan_user_data (id) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT unique_user_linked_verse UNIQUE (user_id, linked_verse_id)
);
ALTER TABLE chusan_user_playlog
MODIFY COLUMN played_user_id1 BIGINT;
ALTER TABLE chusan_user_playlog
MODIFY COLUMN played_user_id2 BIGINT;
ALTER TABLE chusan_user_playlog
MODIFY COLUMN played_user_id3 BIGINT;
INSERT INTO chusan_game_linked_verse (id, name, start_date, end_date, music_id)
VALUES
(10001, 'Linked GATE ORIGIN', '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000',2838),
(10002, 'Linked GATE AIR', '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000',2846),
(10003, 'Linked GATE STAR', '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000',2858),
(10004, 'Linked GATE AMAZON', '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000',2869),
(10005, 'Linked GATE CRYSTAL', '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000',2880),
(10006, 'Linked GATE PARADISE', '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000',2891),
(10007, 'Linked GATE NEW', '2019-01-01 00:00:00.000000', '2029-01-01 00:00:00.000000',2919);