3 Commits
Author SHA1 Message Date
asterisk727 4a0254ec19 chore: update readme for ongeki 2026-09-25 12:58:18 -05:00
crxmsxnandRaymond c3661d3c94 feat: ongeki 1.60 support (#240)
Co-authored-by: Raymond <101374892+raymonable@users.noreply.github.com>
2026-09-24 23:17:01 -04:00
topty bcbc770ca7 feat(mai2): stub MAGiCAL API responses (#246) 2026-09-22 16:54:23 -04:00
18 changed files with 3756 additions and 1463 deletions
+9 -9
View File
@@ -32,15 +32,15 @@ Multipurpose game server for ALL.Net games.
> [!WARNING]
> CHUNITHM pre-NEW!! and maimai pre-DX are no longer supported after February 6th, 2026 and all associated data will be removed.
| Game | Latest Ver. | Initial Ver. | Notes | Web UI | Import | AES |
|------------------------|--------------------|---------------------|-------------------------------------------------------------|--------|-------------|---------|
| SDHD: CHUNITHM | 2.50 (MATE) | 2.00 (NEW) | | ✅ | ✅ | ✅ |
| SDEZ: maimai DX | 1.65 (CiRCLE PLUS) | 1.00 (DX) | Thanks [@Menci](https://github.com/Menci) | ✅ | ✅ | ✅ |
| SDGA: maimai DX (Intl) | 1.60 (CiRCLE) | 1.00 (DX) | Thanks [@Clansty](https://github.com/clansty) | ✅ | ✅ | ✅ |
| SDED: Card Maker | 1.39 | N/A | Thanks [@Becods](https://github.com/Becods) | ❌ | ❌ | ❌ |
| SDDT: O.N.G.E.K.I. | 1.50 (Re:Fresh) | N/A | Thanks [@PenguinCaptain](https://github.com/PenguinCaptain) | ✅ | ✅ | ✅ |
| SBZV: Project DIVA | 7.10 | N/A | Minimal support | ❌ | ❌ | 🔓 |
| SDFE: Wacca | 3.07 (REVERSE) | N/A | Later versions are EOS patches | ✅ | ❌ | 🔓 |
| Game | Latest Ver. | Initial Ver. | Notes | Web UI | Import | AES |
|------------------------|-----------------------|---------------------|-------------------------------------------------------------|--------|-------------|-----|
| SDHD: CHUNITHM | 2.50 (Mate) | 2.00 (NEW) | | ✅ | ✅ | ✅ |
| SDEZ: maimai DX | 1.65 (CiRCLE PLUS) | 1.00 (DX) | Thanks [@Menci](https://github.com/Menci) | ✅ | ✅ | ✅ |
| SDGA: maimai DX (Intl) | 1.60 (CiRCLE) | 1.00 (DX) | Thanks [@Clansty](https://github.com/clansty) | ✅ | ✅ | ✅ |
| SDED: Card Maker | 1.39 | N/A | Thanks [@Becods](https://github.com/Becods) | ❌ | ❌ | ❌ |
| SDDT: O.N.G.E.K.I. | 1.60 (Re:Fresh Act 2) | N/A | Thanks [@PenguinCaptain](https://github.com/PenguinCaptain) | ✅ | ✅ | ❌ |
| SBZV: Project DIVA | 7.10 | N/A | Minimal support | ❌ | ❌ | 🔓 |
| SDFE: Wacca | 3.07 (REVERSE) | N/A | Later versions are EOS patches | ✅ | ❌ | 🔓 |
<!-- A majority of them have been left as N/A for initial version as they do not appear to have restrictions -->
+6 -1
View File
@@ -32,6 +32,7 @@ import java.time.ZoneOffset.UTC
import java.time.format.DateTimeFormatter
import java.util.*
import java.util.concurrent.locks.Lock
import kotlin.math.floor
import kotlin.reflect.KCallable
import kotlin.reflect.KClass
import kotlin.reflect.KMutableProperty1
@@ -199,6 +200,10 @@ val Any?.truthy get() = when (this) {
}
val Any?.str get() = toString()
fun truncateVersion(version: Number): Number {
return (floor(version.toFloat() / 5.0) * 5).toInt();
}
// Collections
fun <T> ls(vararg args: T) = args.toList()
inline fun <reified T> arr(vararg args: T) = arrayOf(*args)
@@ -277,4 +282,4 @@ fun List<List<Any?>>.numCsv(vararg head: Str) = head.joinToString(",") + "\n" +
joinToString("\n") { it.joinToString(",") }
// DI
inline fun <reified T : Any> ApplicationContext.lazy() = kotlin.lazy { getBean(T::class.java) }
inline fun <reified T : Any> ApplicationContext.lazy() = kotlin.lazy { getBean(T::class.java) }
@@ -3,6 +3,7 @@ package icu.samnyan.aqua.sega.general.filter
import ext.details
import ext.logger
import ext.toJson
import ext.truncateVersion
import icu.samnyan.aqua.net.components.GeoIP
import icu.samnyan.aqua.sega.allnet.TokenChecker
import icu.samnyan.aqua.sega.general.model.GameEncryptionKey
@@ -36,9 +37,6 @@ class CompressionFilter(
}
fun truncateVersion(version: Number): Number {
return (floor(version.toFloat() / 5.0) * 5).toInt();
}
fun getVersion(req: HttpServletRequest, game: String): Int {
// NOTE: this should help with some issues regarding mismatched data
val expectedVersion = (req.getHeader("Mai-Encoding") ?:
@@ -55,7 +55,12 @@ fun Maimai2ServletController.initApis() {
// Maimai only request for event type 1
"GetGameEvent" static { mapOf("type" to 1, "gameEventList" to db.gameEvent.findAll()) }
"GetGameCharge" static { db.gameCharge.findAll().let { mapOf("length" to it.size, "gameChargeList" to it) } }
"GetGameCharge" static {
db.gameCharge.findAll().let {
mapOf("length" to it.size, "gameChargeList" to it, "gamePassChargeList" to empty)
}
}
"GetUserOption" { mapOf(
"userId" to uid,
@@ -438,6 +443,12 @@ fun Maimai2ServletController.initApis() {
)
) }
"GetGameSellingPassPack" static { mapOf("gameSellingPassPackList" to empty) }
"GetUserPass" { mapOf("userId" to uid, "userPassList" to empty) }
"GetUserTicketLimitDate" static { mapOf("userTicketLimitDateList" to empty) }
// NOTE: no-op APIs moved to respect encryption
"UpsertUserPlaceCircleRegist" static { mapOf( "returnCode" to 0, "apiName" to "com.sega.maimai2servlet.api.UpsertUserPlaceCircleRegistApi") }
"GetUserScoreRanking" static { """{"returnCode":1, "apiName":"com.sega.maimai2servlet.api.GetUserScoreRankingApi"}""" }
@@ -4,6 +4,8 @@ import ext.empty
import ext.int
import ext.parsing
import ext.plus
import ext.str
import ext.truncateVersion
fun OngekiController.ongekiInit() {
fun <T> List<T>.staticLst(key: String) = mapOf("length" to size, key to this)
@@ -13,18 +15,21 @@ fun OngekiController.ongekiInit() {
initUpsertAll()
// Has type, but type is always 1
"GetGameEvent".static {
gdb.event.findAll().map {
mapOf("id" to it.id, "type" to 1, "startDate" to "2005-01-01 00:00:00.0", "endDate" to "2099-01-01 05:00:00.0")
}.staticLst("gameEventList") + mapOf("type" to 1)
"GetGameEvent" {
// val trunkVer = truncateVersion((data["version"] ?: "1.50.00").str.split(".").take(2).joinToString("").int)
val events = gdb.event.findAll()
// .filter{ it.id.str.startsWith(trunkVer.str) }
.map {
mapOf("id" to it.id, "type" to 1, "startDate" to "2005-01-01 00:00:00.0", "endDate" to "2099-01-01 05:00:00.0")
}
mapOf("gameEventList" to events, "length" to events.size, "type" to 1)
}
"GetGamePoint".static { gdb.point.findAll().staticLst("gamePointList") }
"GetGamePresent".static { gdb.present.findAll().staticLst("gamePresentList") }
"GetGameReward".static { gdb.reward.findAll().staticLst("gameRewardList") }
// Dummy endpoints
"GetGameTechMusic".static { empty.staticLst("gameTechMusicList") }
"GetGameTechMusic".static { gdb.gameData.ogkGameTechMusics.staticLst("gameTechMusicList") }
"GetGameMessage" { mapOf("type" to data["type"], "length" to 0, "gameMessageList" to empty) }
"GetGameMusicReleaseState".static { mapOf("techScore" to 0, "cardNum" to 0) }
@@ -82,4 +87,4 @@ fun OngekiController.ongekiInit() {
"UpsertClientTestmode" static { """{"returnCode":1, "apiName":"UpsertClientTestmodeApi"}""" }
"UpsertUserGplog" static { """{"returnCode":1, "apiName":"UpsertUserGplogApi"}""" }
"Ping" static { """{"returnCode":1, "apiName":"Ping"}""" }
}
}
@@ -54,11 +54,12 @@ interface OgkUserDeckRepo : OngekiUserLinked<UserDeck> {
}
interface OgkUserEventMusicRepo : OngekiUserLinked<UserEventMusic> {
fun findByUserAndEventIdAndTypeAndMusicId(
fun findByUserAndEventIdAndTypeAndMusicIdAndLevel(
userData: UserData,
eventId: Int,
type: Int,
musicId: Int
musicId: Int,
level: Int
): UserEventMusic?
}
@@ -162,7 +163,10 @@ interface OgkUserGachaRepo : OngekiUserLinked<UserGacha> {
}
// Re:Fresh
interface OgkUserEventMapRepo : OngekiUserLinked<UserEventMap>
interface OgkUserEventMapRepo : OngekiUserLinked<UserEventMap> {
fun findByUserAndEventIdAndMapId(user: UserData, eventId: Int, mapId: Int): UserEventMap?
fun findByUser_Card_ExtIdAndEventIdAndMapId(extId: Long, eventId: Int, mapId: Int): UserEventMap?
}
interface OgkUserSkinRepo : OngekiUserLinked<UserSkin>
@@ -74,7 +74,9 @@ fun OngekiController.initUpsertAll() {
// UserEventMap
userEventMap?.let {
db.eventMap.save(it.apply {
id = db.eventMap.findSingleByUser(u)?.id ?: 0 }) }
id = db.eventMap.findByUserAndEventIdAndMapId(u, it.eventId, it.mapId)?.id ?: 0
})
}
// UserPlaylogList
userPlaylogList?.let { db.playlog.saveAll(it) }
@@ -195,8 +197,9 @@ fun OngekiController.initUpsertAll() {
// UserEventMusicList
userEventMusicList?.let { list ->
db.eventMusic.saveAll(list.distinctBy { it.eventId to it.type to it.musicId }.mapApply {
id = db.eventMusic.findByUserAndEventIdAndTypeAndMusicId(u, eventId, type, musicId)?.id ?: 0 }) }
db.eventMusic.saveAll(list.distinctBy { it.eventId to it.type to it.musicId to it.level }.mapApply {
id = db.eventMusic.findByUserAndEventIdAndTypeAndMusicIdAndLevel(u, eventId, type, musicId, level)?.id ?: 0
}) }
// UserTechEventList
userTechEventList?.let { list ->
@@ -7,6 +7,7 @@ import icu.samnyan.aqua.sega.general.model.CardStatus
import icu.samnyan.aqua.sega.general.model.UserRecentRating
import icu.samnyan.aqua.sega.ongeki.model.OgkItemType
import icu.samnyan.aqua.sega.ongeki.model.UserItem
import icu.samnyan.aqua.sega.ongeki.model.UserEventMap
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Sort
import java.time.LocalDateTime
@@ -14,10 +15,25 @@ import java.time.format.DateTimeFormatter
fun OngekiController.initUser() {
val emptyEventMapData = """{"unlockPoints":0,"_unlockedTileIDs":[1],"_rewardScenarioIDs":[],"_rewardRewardKeyIDs":[],"endless":false,"loopCount":0}"""
"GetUserData" { mapOf("userId" to uid, "userData" to db.data.findByCard_ExtId(uid)) }
"GetUserOption" { mapOf("userId" to uid, "userOption" to db.option.findSingleByUser_Card_ExtId(uid)) }
"GetUserEventMap" { mapOf("userId" to uid, "userEventMap" to db.eventMap.findSingleByUser_Card_ExtId(uid)) }
"GetUserEventMap" {
val eventId = data["eventId"]?.int ?: 0
val mapId = data["mapId"]?.int ?: 0
val userEventMap = db.eventMap.findByUser_Card_ExtIdAndEventIdAndMapId(uid, eventId, mapId)
?: UserEventMap().apply {
this.eventId = eventId
this.mapId = mapId
this.mapData = emptyEventMapData
}
mapOf(
"userId" to uid,
"userEventMap" to userEventMap
)
}
"GetUserTechEvent".unpaged { db.techEvent.findByUser_Card_ExtId(uid) }
"GetUserBoss".unpaged { db.boss.findByUser_Card_ExtId(uid) }
@@ -29,6 +29,12 @@ class GameEvent {
var id: Long = 0
}
class GameTechMusic {
var eventId = 0
var musicId = 0
var level = 0
}
class GameMusic {
var id: Long = 0
var name: String = ""
@@ -105,4 +111,4 @@ class GameGacha {
@JsonProperty("isCeiling")
var isCeiling: Boolean = false
}
}
@@ -166,6 +166,7 @@ class UserChapter : OngekiUserEntity() {
var jewelCount = 0
var lastPlayMusicCategory = 0
var lastPlayMusicId = 0
var lastPlayMusicMinorCategory: String = ""
var lastPlayMusicLevel = 0
var isStoryWatched = false
var isClear = false
@@ -197,7 +198,10 @@ class UserDeck : OngekiUserEntity() {
}
@Entity(name = "OngekiUserEventMusic")
@Table(name = "ongeki_user_event_music")
@Table(
name = "ongeki_user_event_music",
uniqueConstraints = [UniqueConstraint(columnNames = ["user_id", "event_id", "type", "music_id", "level"])]
)
class UserEventMusic : OngekiUserEntity() {
var eventId = 0
var type = 0
@@ -318,6 +322,7 @@ class UserMusicItem : OngekiUserEntity() {
class UserOption : OngekiUserEntity() {
var optionSet = 0
var speed = 0
var fieldWall = 0
var mirror = 0
var judgeTiming = 0
var judgeAdjustment = 0
@@ -342,20 +347,19 @@ class UserOption : OngekiUserEntity() {
var colorSide = 0
var effectDamage = 0
var effectPos = 0
var effectAttack = 0
var judgeDisp = 0
var judgePos = 0
var judgeBreak = 0
var judgeHit = 0
var platinumBreakDisp = 0
var judgeCriticalBreak = 0
var platinumBreakDisp = 0
var matching = 0
var dispPlayerLv = 0
var dispRating = 0
var dispBP = 0
var headphone = 0
// Re:Fresh
var effectAttack = 0
}
@@ -457,6 +461,7 @@ class UserStory : OngekiUserEntity() {
var jewelCount = 0
var lastPlayMusicId = 0
var lastPlayMusicCategory = 0
var lastPlayMusicMinorCategory: String = ""
var lastPlayMusicLevel = 0
}
@@ -498,7 +503,10 @@ class UserTrainingRoom : OngekiUserEntity() {
// Re:Fresh
@Entity(name = "OngekiUserEventMap")
@Table(name = "ongeki_user_event_map")
@Table(
name = "ongeki_user_event_map",
uniqueConstraints = [UniqueConstraint(columnNames = ["user_id", "event_id", "map_id"])]
)
class UserEventMap : OngekiUserEntity() {
var eventId = 0
var mapId = 0
@@ -19,6 +19,7 @@ import icu.samnyan.aqua.sega.ongeki.model.GameChara as OgkGameChara
import icu.samnyan.aqua.sega.ongeki.model.GameEvent as OgkGameEvent
import icu.samnyan.aqua.sega.ongeki.model.GameGacha as OgkGameGacha
import icu.samnyan.aqua.sega.ongeki.model.GameGachaCard as OgkGameGachaCard
import icu.samnyan.aqua.sega.ongeki.model.GameTechMusic as OgkGameTechMusic
import icu.samnyan.aqua.sega.ongeki.model.GameMusic as OgkGameMusic
import icu.samnyan.aqua.sega.ongeki.model.GamePoint as OgkGamePoint
import icu.samnyan.aqua.sega.ongeki.model.GamePresent as OgkGamePresent
@@ -50,6 +51,7 @@ class GameDataService() {
log.warn("Game data file $f or resource $resPath not found, using empty list")
return emptyList()
}
}
// maimai2
@@ -72,6 +74,7 @@ class GameDataService() {
lateinit var ogkGameCards: List<OgkGameCard>
lateinit var ogkGameCharas: List<OgkGameChara>
lateinit var ogkGameEvents: List<OgkGameEvent>
lateinit var ogkGameTechMusics: List<OgkGameTechMusic>
lateinit var ogkGameMusics: List<OgkGameMusic>
lateinit var ogkGamePoints: List<OgkGamePoint>
lateinit var ogkGamePresents: List<OgkGamePresent>
@@ -103,6 +106,7 @@ class GameDataService() {
ogkGameCards = load("ongeki", "game_card.json")
ogkGameCharas = load("ongeki", "game_chara.json")
ogkGameEvents = load("ongeki", "game_event.json")
ogkGameTechMusics = load("ongeki", "game_tech_music.json")
ogkGameMusics = load("ongeki", "game_music.json")
ogkGamePoints = load("ongeki", "game_point.json")
ogkGamePresents = load("ongeki", "game_present.json")
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,20 @@
-- should have been not nullable in the first place smh
DELETE FROM ongeki_user_event_map
WHERE user_id IS NULL;
ALTER TABLE ongeki_user_event_map
MODIFY user_id BIGINT NOT NULL;
-- dedupe before adding new constraint just in case
DELETE FROM ongeki_user_event_map
WHERE id NOT IN (
SELECT id
FROM (
SELECT MAX(id) AS id
FROM ongeki_user_event_map
GROUP BY user_id, event_id, map_id
) AS rows_to_keep
);
ALTER TABLE ongeki_user_event_map
ADD UNIQUE KEY uniq_ongeki_user_event_map (user_id, event_id, map_id);
@@ -0,0 +1,23 @@
-- allow separate Technical Challenge records for each chart difficulty
-- drop the old constraint and dedupe with the new constraint just in case again
ALTER TABLE ongeki_user_event_music
DROP INDEX IF EXISTS UKo5PU3BgZeTKB8SNykq9U7xjfshp;
DELETE FROM ongeki_user_event_music
WHERE user_id IS NULL;
ALTER TABLE ongeki_user_event_music
MODIFY user_id BIGINT NOT NULL;
DELETE FROM ongeki_user_event_music
WHERE id NOT IN (
SELECT id
FROM (
SELECT MAX(id) AS id
FROM ongeki_user_event_music
GROUP BY user_id, event_id, type, music_id, level
) AS rows_to_keep
);
ALTER TABLE ongeki_user_event_music
ADD UNIQUE KEY uniq_ongeki_user_event_music (user_id, event_id, type, music_id, level);
@@ -0,0 +1,5 @@
ALTER TABLE ongeki_user_chapter
ADD COLUMN last_play_music_minor_category VARCHAR(255) NOT NULL DEFAULT '';
ALTER TABLE ongeki_user_story
ADD COLUMN last_play_music_minor_category VARCHAR(255) NOT NULL DEFAULT '';
@@ -0,0 +1,7 @@
-- new field wall option field
ALTER TABLE ongeki_user_option
ADD COLUMN field_wall INT NOT NULL DEFAULT 0;
-- varchar(255) not enough for event map storage
ALTER TABLE ongeki_user_event_map
MODIFY COLUMN map_data TEXT NULL;
+1 -1
View File
@@ -59,7 +59,7 @@ class Mai2Test : StringSpec({
liveTest("GetGameChargeApi") {
post("GetGameChargeApi", """{"isAll":false}""").let { (_, result) ->
result shouldBe """{"length":5,"gameChargeList":[{"chargeId":0,"orderId":0,"price":1,"startDate":"2019-01-01 00:00:00.000000","endDate":"2099-01-01 00:00:00.000000"},{"chargeId":0,"orderId":0,"price":2,"startDate":"2019-01-01 00:00:00.000000","endDate":"2099-01-01 00:00:00.000000"},{"chargeId":0,"orderId":0,"price":3,"startDate":"2019-01-01 00:00:00.000000","endDate":"2099-01-01 00:00:00.000000"},{"chargeId":0,"orderId":0,"price":4,"startDate":"2019-01-01 00:00:00.000000","endDate":"2099-01-01 00:00:00.000000"},{"chargeId":0,"orderId":0,"price":5,"startDate":"2019-01-01 00:00:00.000000","endDate":"2099-01-01 00:00:00.000000"}]}""".jsonMap()
result shouldBe """{"length":5,"gameChargeList":[{"chargeId":0,"orderId":0,"price":1,"startDate":"2019-01-01 00:00:00.000000","endDate":"2099-01-01 00:00:00.000000"},{"chargeId":0,"orderId":0,"price":2,"startDate":"2019-01-01 00:00:00.000000","endDate":"2099-01-01 00:00:00.000000"},{"chargeId":0,"orderId":0,"price":3,"startDate":"2019-01-01 00:00:00.000000","endDate":"2099-01-01 00:00:00.000000"},{"chargeId":0,"orderId":0,"price":4,"startDate":"2019-01-01 00:00:00.000000","endDate":"2099-01-01 00:00:00.000000"},{"chargeId":0,"orderId":0,"price":5,"startDate":"2019-01-01 00:00:00.000000","endDate":"2099-01-01 00:00:00.000000"}],"gamePassChargeList":[]}""".jsonMap()
}
}