[O] Finish mai2 refactor

This commit is contained in:
Azalea
2025-03-11 03:45:54 -04:00
parent 6ffee3466f
commit aecb5572cd
35 changed files with 262 additions and 925 deletions

View File

@@ -1,7 +1,6 @@
package icu.samnyan.aqua
import icu.samnyan.aqua.sega.aimedb.AimeDbServer
import icu.samnyan.aqua.sega.maimai2.worldslink.MaimaiFutari
import icu.samnyan.aqua.spring.AutoChecker
import org.springframework.boot.SpringApplication
import org.springframework.boot.ansi.AnsiOutput
@@ -15,9 +14,6 @@ class Entry
fun main(args: Array<String>) {
AnsiOutput.setEnabled(AnsiOutput.Enabled.ALWAYS)
when (args.getOrNull(0)) {
"futari" -> return MaimaiFutari().start()
}
// If data/ is not found, create it
File("data").mkdirs()

View File

@@ -1,10 +1,11 @@
package icu.samnyan.aqua.net.transfer
import ext.*
import icu.samnyan.aqua.sega.chusan.model.request.UpsertUserAll
import icu.samnyan.aqua.sega.chusan.model.request.Chu3UserAll
import icu.samnyan.aqua.sega.chusan.model.userdata.UserActivity
import icu.samnyan.aqua.sega.chusan.model.userdata.UserItem
import icu.samnyan.aqua.sega.chusan.model.userdata.UserMusicDetail
import icu.samnyan.aqua.sega.maimai2.model.request.Mai2UserAll
import icu.samnyan.aqua.sega.util.jackson.BasicMapper
import icu.samnyan.aqua.sega.util.jackson.IMapper
import icu.samnyan.aqua.sega.util.jackson.StringMapper
@@ -48,7 +49,7 @@ class ChusanDataBroker(allNet: AllNetClient, log: (String) -> Unit): DataBroker(
val userId = mapOf("userId" to allNet.userId)
val paged = userId + mapOf("nextIndex" to 0, "maxCount" to 10000000)
return mapper.write(UpsertUserAll().apply {
return mapper.write(Chu3UserAll().apply {
userData = ls("GetUserDataApi".get("userData", userId))
userGameOption = ls("GetUserOptionApi".get("userGameOption", userId))
userCharacterList = "GetUserCharacterApi".get("userCharacterList", paged)
@@ -85,8 +86,10 @@ class MaimaiDataBroker(allNet: AllNetClient, log: (String) -> Unit): DataBroker(
val userId = mapOf("userId" to allNet.userId)
val paged = userId + mapOf("nextIndex" to 0, "maxCount" to 10000000)
return UpsertUserAll().apply {
return Mai2UserAll().apply {
userData = ls("GetUserDataApi".get("userData", userId))
// userGameOption = ls("GetUserOptionApi".get("userGameOption", userId))
}.toJson()
}

View File

@@ -2,7 +2,7 @@ package icu.samnyan.aqua.sega.chusan.handler
import ext.*
import icu.samnyan.aqua.sega.chusan.ChusanController
import icu.samnyan.aqua.sega.chusan.model.request.UpsertUserAll
import icu.samnyan.aqua.sega.chusan.model.request.Chu3UserAll
import icu.samnyan.aqua.sega.chusan.model.userdata.*
import icu.samnyan.aqua.sega.general.model.response.UserRecentRating
@@ -17,7 +17,7 @@ fun ChusanController.upsertApiInit() {
}
"UpsertUserAll" api@ {
val req = parsing { mapper.convert<UpsertUserAll>(data["upsertUserAll"]!!) }
val req = parsing { mapper.convert<Chu3UserAll>(data["upsertUserAll"]!!) }
req.run {
// UserData

View File

@@ -37,7 +37,7 @@ data class MusicIdWrapper(
val musicId: Int = 0,
)
class UpsertUserAll(
class Chu3UserAll(
var userData: List<Chu3UserData>? = null,
var userGameOption: List<UserGameOption>? = null,
var userCharacterList: List<UserCharacter>? = null,

View File

@@ -4,12 +4,10 @@ package icu.samnyan.aqua.sega.maimai2
import ext.*
import icu.samnyan.aqua.sega.general.PagedHandler
import icu.samnyan.aqua.sega.maimai2.model.response.data.UserRivalMusic
import icu.samnyan.aqua.sega.maimai2.model.response.data.UserRivalMusicDetail
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserIntimate
import icu.samnyan.aqua.sega.maimai2.model.UserRivalMusic
import icu.samnyan.aqua.sega.maimai2.model.UserRivalMusicDetail
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserKaleidx
import java.time.LocalDate
import java.util.*
fun Maimai2ServletController.initApis() {
// Used because maimai does not actually require paging implementation
@@ -157,7 +155,7 @@ fun Maimai2ServletController.initApis() {
val rivalId = parsing { data["rivalId"]!!.long }
val lst = db.userMusicDetail.findByUserId(rivalId)
val res = lst.associate { it.musicId to UserRivalMusic(it.musicId, LinkedList()) }
val res = lst.associate { it.musicId to UserRivalMusic(it.musicId) }
lst.forEach {
res[it.musicId]!!.userRivalMusicDetailList.add(
@@ -195,6 +193,7 @@ fun Maimai2ServletController.initApis() {
}
// Kaleidoscope, added on 1.50
// [{gateId, phaseId}]
"GetGameKaleidxScope" { mapOf("gameKaleidxScopeList" to ls(
mapOf("gateId" to 1, "phaseId" to findPhase(LocalDate.of(2025, 1, 18))),
mapOf("gateId" to 2, "phaseId" to 2),
@@ -203,6 +202,8 @@ fun Maimai2ServletController.initApis() {
mapOf("gateId" to 5, "phaseId" to 2),
mapOf("gateId" to 6, "phaseId" to 2),
)) }
// Request: {userId}
// Response: {userId, userKaleidxScopeList}
"GetUserKaleidxScope".unpaged {
val u = db.userData.findByCardExtId(uid)() ?: (404 - "User not found")
val lst = db.userKaleidx.findByUser(u)
@@ -213,6 +214,8 @@ fun Maimai2ServletController.initApis() {
lst
}
// Request: {userId, version, userData: [UserDetail], userPlaylogList: [UserPlaylog]}
// Response: {userId, userItemList: [UserItem]}
// Added on 1.50
"GetUserNewItemList" { mapOf("userId" to uid, "userItemList" to empty) }

View File

@@ -1,98 +0,0 @@
package icu.samnyan.aqua.sega.maimai2.handler;
import com.fasterxml.jackson.core.JsonProcessingException;
import icu.samnyan.aqua.net.db.AquaNetUser;
import icu.samnyan.aqua.net.utils.PathProps;
import icu.samnyan.aqua.sega.general.BaseHandler;
import icu.samnyan.aqua.sega.general.dao.CardRepository;
import icu.samnyan.aqua.sega.general.model.Card;
import icu.samnyan.aqua.sega.maimai2.model.request.data.UserPortrait;
import icu.samnyan.aqua.sega.util.jackson.BasicMapper;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.codec.Utf8;
import org.springframework.stereotype.Component;
import java.io.FileInputStream;
import java.nio.file.Paths;
import java.util.*;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Component("Maimai2GetUserPortraitHandler")
public class GetUserPortraitHandler implements BaseHandler {
private static final Logger logger = LoggerFactory.getLogger(GetUserPortraitHandler.class);
private final BasicMapper mapper;
private final boolean enable;
private final CardRepository cardRepo;
private final String portraitPath;
public GetUserPortraitHandler(BasicMapper mapper,
@Value("${game.maimai2.userPhoto.enable:true}") boolean enable,
CardRepository cardRepo,
PathProps paths) {
this.mapper = mapper;
this.enable = enable;
this.cardRepo = cardRepo;
this.portraitPath = paths.getAquaNetPortrait();
}
@Override
public String handle(Map<String, ?> request) throws JsonProcessingException {
if (enable) {
var userId = ((Number) request.get("userId")).longValue();
var list = new ArrayList<UserPortrait>();
var card = cardRepo.findByExtId(userId);
var user = card.map(Card::getAquaUser);
var profilePicture = user.map(AquaNetUser::getProfilePicture).orElse(null);
try {
if (!StringUtils.isEmpty(profilePicture)) {
var filePath = Paths.get(portraitPath, profilePicture);
var buffer = new byte[10240];
var stream = new FileInputStream(filePath.toFile());
while (stream.available() > 0) {
var read = stream.read(buffer, 0, 10240);
var encodeBuffer = read == 10240 ? buffer : Arrays.copyOfRange(buffer, 0, read);
var userPortrait = new UserPortrait();
userPortrait.setFileName("portrait.jpg");
userPortrait.setPlaceId(0);
userPortrait.setUserId(userId);
userPortrait.setClientId("");
userPortrait.setUploadDate("1970-01-01 09:00:00.0");
userPortrait.setDivData(Utf8.decode(Base64.getEncoder().encode(encodeBuffer)));
userPortrait.setDivNumber(list.size());
list.add(userPortrait);
}
stream.close();
for (var i = 0; i < list.size(); i++) {
var userPortrait = list.get(i);
userPortrait.setDivLength(list.size());
}
var map = new HashMap<String, Object>();
map.put("length", list.size());
map.put("userPortraitList", list);
var respJson = mapper.write(map);
return respJson;
}
} catch (Exception e) {
logger.error("Result: User photo save failed", e);
}
}
return "{\"length\":0,\"userPortraitList\":[]}";
}
}

View File

@@ -0,0 +1,63 @@
package icu.samnyan.aqua.sega.maimai2.handler
import ext.invoke
import ext.logger
import icu.samnyan.aqua.net.utils.PathProps
import icu.samnyan.aqua.sega.general.BaseHandler
import icu.samnyan.aqua.sega.general.dao.CardRepository
import icu.samnyan.aqua.sega.maimai2.model.request.Mai2UserPortrait
import org.springframework.beans.factory.annotation.Value
import org.springframework.security.crypto.codec.Utf8
import org.springframework.stereotype.Component
import java.io.FileInputStream
import java.nio.file.Paths
import java.util.*
@Component("Maimai2GetUserPortraitHandler")
class GetUserPortraitHandler(
val cardRepo: CardRepository,
@param:Value("\${game.maimai2.userPhoto.enable:true}") val enable: Boolean,
paths: PathProps
) : BaseHandler {
val portraitPath = paths.aquaNetPortrait
val log = logger()
override fun handle(request: Map<String, Any>): Any? {
if (!enable) return """{"length":0,"userPortraitList":[]}"""
val uid = (request["userId"] as Number).toLong()
val list = ArrayList<Mai2UserPortrait>()
val profilePicture = cardRepo.findByExtId(uid)()?.aquaUser?.profilePicture?.ifBlank { null }
?: return """{"length":0,"userPortraitList":[]}"""
try {
val filePath = Paths.get(portraitPath, profilePicture)
val buffer = ByteArray(10240)
FileInputStream(filePath.toFile()).use { stream ->
while (stream.available() > 0) {
val read = stream.read(buffer, 0, 10240)
val buf = if (read == 10240) buffer else Arrays.copyOfRange(buffer, 0, read)
list.add(Mai2UserPortrait().apply {
userId = uid
divData = Utf8.decode(Base64.getEncoder().encode(buf))
divNumber = list.size
})
}
}
list.forEach { it.divLength = list.size }
return mapOf(
"length" to list.size,
"userPortraitList" to list
)
} catch (e: Exception) {
log.error("Result: User photo get failed", e)
return """{"length":0,"userPortraitList":[]}"""
}
}
}

View File

@@ -3,7 +3,7 @@ package icu.samnyan.aqua.sega.maimai2.handler
import ext.invoke
import icu.samnyan.aqua.sega.general.BaseHandler
import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos
import icu.samnyan.aqua.sega.maimai2.model.response.data.UserRating
import icu.samnyan.aqua.sega.maimai2.model.UserRating
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserRate
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserUdemae
import icu.samnyan.aqua.sega.util.jackson.BasicMapper

View File

@@ -1,11 +1,8 @@
package icu.samnyan.aqua.sega.maimai2.handler
import ext.div
import ext.isoDateTime
import ext.logger
import ext.path
import ext.*
import icu.samnyan.aqua.sega.general.BaseHandler
import icu.samnyan.aqua.sega.maimai2.model.request.UploadUserPhoto
import icu.samnyan.aqua.sega.maimai2.model.request.Mai2UserPhoto
import icu.samnyan.aqua.sega.util.jackson.BasicMapper
import org.springframework.stereotype.Component
import java.io.IOException
@@ -23,9 +20,7 @@ class UploadUserPhotoHandler(private val mapper: BasicMapper) :
// Maimai DX sends split base64 data for one jpeg image.
// So, make a temp file and keep append bytes until last part received.
// If finished, rename it to other name so user can keep save multiple scorecards in a single day.
val uploadUserPhoto = mapper.convert(request, UploadUserPhoto::class.java)
val up = uploadUserPhoto.userPhoto
val up = parsing { mapper.convert(request["userPhoto"]!!, Mai2UserPhoto::class.java) }
try {
val tmpFile = tmpDir / "${up.userId}-${up.trackNo}.tmp"

View File

@@ -1,12 +1,13 @@
package icu.samnyan.aqua.sega.maimai2.handler
import ext.logger
import ext.long
import ext.millis
import ext.parsing
import icu.samnyan.aqua.sega.allnet.TokenChecker
import icu.samnyan.aqua.sega.general.BaseHandler
import icu.samnyan.aqua.sega.maimai2.model.Mai2UserDataRepo
import icu.samnyan.aqua.sega.maimai2.model.Mai2UserPlaylogRepo
import icu.samnyan.aqua.sega.maimai2.model.request.UploadUserPlaylog
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserPlaylog
import icu.samnyan.aqua.sega.util.jackson.BasicMapper
import icu.samnyan.aqua.spring.Metrics
@@ -33,9 +34,10 @@ class UploadUserPlaylogHandler(
}
override fun handle(request: Map<String, Any>): String {
val req = mapper.convert(request, UploadUserPlaylog::class.java)
val uid = parsing { request["userId"]!!.long }
val playlog = parsing { mapper.convert(request["userPlaylog"]!!, Mai2UserPlaylog::class.java) }
val version = tryParseGameVersion(req.userPlaylog.version)
val version = tryParseGameVersion(playlog.version)
if (version != null) {
val session = TokenChecker.getCurrentSession()
val gameId = if (session?.gameId in VALID_GAME_IDS) session!!.gameId else ""
@@ -47,9 +49,9 @@ class UploadUserPlaylogHandler(
// Check duplicate
val isDup = playlogRepo.findByUser_Card_ExtIdAndMusicIdAndUserPlayDate(
req.userId,
req.userPlaylog.musicId,
req.userPlaylog.userPlayDate
uid,
playlog.musicId,
playlog.userPlayDate
).size > 0
if (isDup) {
log.info("Duplicate playlog detected")
@@ -57,14 +59,14 @@ class UploadUserPlaylogHandler(
}
// Save if the user is registered
val u = userDataRepository.findByCardExtId(req.userId).getOrNull()
if (u != null) playlogRepo.save(req.userPlaylog.apply { user = u })
val u = userDataRepository.findByCardExtId(uid).getOrNull()
if (u != null) playlogRepo.save(playlog.apply { user = u })
// If the user hasn't registered (first play), save the playlog to a backlog
else {
playBacklog.putIfAbsent(req.userId, mutableListOf())
playBacklog[req.userId]?.apply {
add(BacklogEntry(millis(), req.userPlaylog))
playBacklog.putIfAbsent(uid, mutableListOf())
playBacklog[uid]?.apply {
add(BacklogEntry(millis(), playlog))
if (size > 6) clear() // Prevent abuse
}
}

View File

@@ -2,10 +2,11 @@ package icu.samnyan.aqua.sega.maimai2.handler
import ext.div
import ext.logger
import ext.parsing
import ext.path
import icu.samnyan.aqua.net.utils.PathProps
import icu.samnyan.aqua.sega.general.BaseHandler
import icu.samnyan.aqua.sega.maimai2.model.request.UploadUserPortrait
import icu.samnyan.aqua.sega.maimai2.model.request.Mai2UserPortrait
import icu.samnyan.aqua.sega.util.jackson.BasicMapper
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
@@ -34,8 +35,7 @@ class UploadUserPortraitHandler(
// Maimai DX sends split base64 data for one jpeg image.
// So, make a temp file and keep append bytes until last part received.
// If finished, rename it to other name so user can keep save multiple scorecards in a single day.
val up = mapper.convert(request, UploadUserPortrait::class.java).userPortrait
val up = parsing { mapper.convert(request["userPortrait"]!!, Mai2UserPortrait::class.java) }
val id = up.userId
val num = up.divNumber

View File

@@ -3,13 +3,12 @@ package icu.samnyan.aqua.sega.maimai2.handler
import com.fasterxml.jackson.core.JsonProcessingException
import ext.invoke
import ext.mapApply
import ext.minus
import ext.unique
import icu.samnyan.aqua.sega.general.BaseHandler
import icu.samnyan.aqua.sega.general.service.CardService
import icu.samnyan.aqua.sega.maimai2.handler.UploadUserPlaylogHandler.Companion.playBacklog
import icu.samnyan.aqua.sega.maimai2.model.*
import icu.samnyan.aqua.sega.maimai2.model.request.UpsertUserAll
import icu.samnyan.aqua.sega.maimai2.model.request.Mai2UpsertUserAll
import icu.samnyan.aqua.sega.maimai2.model.userdata.*
import icu.samnyan.aqua.sega.util.jackson.BasicMapper
import lombok.AllArgsConstructor
@@ -31,16 +30,13 @@ class UpsertUserAllHandler(
@Throws(JsonProcessingException::class)
override fun handle(request: Map<String, Any>): Any? {
val upsertUserAll = mapper.convert(request, UpsertUserAll::class.java)
val upsertUserAll = mapper.convert(request, Mai2UpsertUserAll::class.java)
val userId = upsertUserAll.userId
val req = upsertUserAll.upsertUserAll
// If user is guest, just return OK response.
if ((userId and 281474976710657L) == 281474976710657L) return SUCCESS
// UserData
if (req.userData == null) 400 - "Invalid Request"
val userData = repos.userData.findByCardExtId(userId)()
val u = repos.userData.saveAndFlush(req.userData[0].apply {
id = userData?.id ?: 0

View File

@@ -1,107 +0,0 @@
package icu.samnyan.aqua.sega.maimai2.handler;
import com.fasterxml.jackson.core.JsonProcessingException;
import icu.samnyan.aqua.sega.maimai2.model.Mai2UserCardRepo;
import icu.samnyan.aqua.sega.maimai2.model.Mai2UserDataRepo;
import icu.samnyan.aqua.sega.maimai2.model.Mai2UserPrintDetailRepo;
import icu.samnyan.aqua.sega.general.BaseHandler;
import icu.samnyan.aqua.sega.maimai2.model.request.UpsertUserPrint;
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserCard;
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserDetail;
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserPrintDetail;
import icu.samnyan.aqua.sega.util.jackson.BasicMapper;
import lombok.AllArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Component("Maimai2UpsertUserPrintHandler")
public class UpsertUserPrintHandler implements BaseHandler {
private static final Logger logger = LoggerFactory.getLogger(UpsertUserPrintHandler.class);
private final BasicMapper mapper;
private final Mai2UserCardRepo userCardRepository;
private final Mai2UserPrintDetailRepo userPrintDetailRepository;
private final Mai2UserDataRepo userDataRepository;
private long expirationTime;
public UpsertUserPrintHandler(BasicMapper mapper,
@Value("${game.cardmaker.card.expiration:15}") long expirationTime,
Mai2UserCardRepo userCardRepository,
Mai2UserPrintDetailRepo userPrintDetailRepository,
Mai2UserDataRepo userDataRepository
) {
this.mapper = mapper;
this.expirationTime = expirationTime;
this.userCardRepository = userCardRepository;
this.userPrintDetailRepository = userPrintDetailRepository;
this.userDataRepository = userDataRepository;
}
@Override
public String handle(Map<String, ?> request) throws JsonProcessingException {
long userId = ((Number) request.get("userId")).longValue();
Mai2UserDetail userData;
Optional<Mai2UserDetail> userOptional = userDataRepository.findByCardExtId(userId);
if (userOptional.isPresent()) {
userData = userOptional.get();
} else {
logger.error("User not found. userId: {}", userId);
return null;
}
UpsertUserPrint upsertUserPrint = mapper.convert(request, UpsertUserPrint.class);
Mai2UserPrintDetail userPrintDetail = upsertUserPrint.getUserPrintDetail();
Mai2UserCard newUserCard = userPrintDetail.getUserCard();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSS");
String currentDateTime = LocalDateTime.now().format(formatter);
String expirationDateTime = LocalDateTime.now().plusDays(expirationTime).format(formatter);
String randomSerialId =
String.format("%010d", ThreadLocalRandom.current().nextLong(0L, 9999999999L)) +
String.format("%010d", ThreadLocalRandom.current().nextLong(0L, 9999999999L));
newUserCard.setUser(userData);
userPrintDetail.setUser(userData);
newUserCard.setStartDate(currentDateTime);
newUserCard.setEndDate(expirationDateTime);
userPrintDetail.setSerialId(randomSerialId);
Optional<Mai2UserCard> userCardOptional = userCardRepository.findByUserAndCardId(newUserCard.getUser(), newUserCard.getCardId());
if (userCardOptional.isPresent()) {
Mai2UserCard userCard = userCardOptional.get();
newUserCard.setId(userCard.getId());
}
userCardRepository.save(newUserCard);
userPrintDetailRepository.save(userPrintDetail);
Map<String, Object> resultMap = new LinkedHashMap<>();
resultMap.put("returnCode", 1);
resultMap.put("orderId", 0);
resultMap.put("serialId", randomSerialId);
resultMap.put("startDate", currentDateTime);
resultMap.put("endDate", expirationDateTime);
return mapper.write(resultMap);
}
}

View File

@@ -0,0 +1,54 @@
package icu.samnyan.aqua.sega.maimai2.handler
import ext.invoke
import ext.logger
import ext.long
import ext.parsing
import icu.samnyan.aqua.sega.general.BaseHandler
import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserPrintDetail
import icu.samnyan.aqua.sega.util.jackson.BasicMapper
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.concurrent.ThreadLocalRandom
@Component("Maimai2UpsertUserPrintHandler")
class UpsertUserPrintHandler(
val mapper: BasicMapper,
val db: Mai2Repos,
@param:Value("\${game.cardmaker.card.expiration:15}") val expirationTime: Long,
) : BaseHandler {
val log = logger()
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSS")
override fun handle(request: Map<String, Any>): Any? {
val userId = parsing { request["userId"]!!.long }
val userData = db.userData.findByCardExtId(userId)() ?: return null
val userPrint = parsing { mapper.convert(request["userPrintDetail"]!!, Mai2UserPrintDetail::class.java) }
val newCard = userPrint.userCard ?: return null
newCard.user = userData
newCard.startDate = LocalDateTime.now().format(formatter)
newCard.endDate = LocalDateTime.now().plusDays(expirationTime).format(formatter)
newCard.id = db.userCard.findByUserAndCardId(newCard.user, newCard.cardId)()?.id ?: 0
db.userCard.save(newCard)
userPrint.user = userData
userPrint.serialId = buildString {
append(String.format("%010d", ThreadLocalRandom.current().nextLong(0L, 9999999999L)))
append(String.format("%010d", ThreadLocalRandom.current().nextLong(0L, 9999999999L)))
}
db.userPrintDetail.save(userPrint)
return mapOf(
"returnCode" to 1,
"orderId" to 0,
"serialId" to userPrint.serialId,
"startDate" to newCard.startDate,
"endDate" to newCard.endDate
)
}
}

View File

@@ -0,0 +1,13 @@
package icu.samnyan.aqua.sega.maimai2.model
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserRate
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserUdemae
class UserRating {
var rating = 0
var ratingList: List<Mai2UserRate> = emptyList()
var newRatingList: List<Mai2UserRate> = emptyList()
var nextRatingList: List<Mai2UserRate> = emptyList()
var nextNewRatingList: List<Mai2UserRate> = emptyList()
var udemae: Mai2UserUdemae = Mai2UserUdemae()
}

View File

@@ -0,0 +1,12 @@
package icu.samnyan.aqua.sega.maimai2.model
class UserRivalMusic(
var musicId: Int,
var userRivalMusicDetailList: MutableList<UserRivalMusicDetail> = mutableListOf()
)
class UserRivalMusicDetail(
var level: Int,
var achievement: Int,
var deluxscoreMax: Int
)

View File

@@ -0,0 +1,53 @@
package icu.samnyan.aqua.sega.maimai2.model.request
import icu.samnyan.aqua.sega.maimai2.model.UserRating
import icu.samnyan.aqua.sega.maimai2.model.userdata.*
class Mai2UpsertUserAll(
var userId: Long,
var upsertUserAll: Mai2UserAll
)
class Mai2UserAll {
var userData: List<Mai2UserDetail> = emptyList()
var userOption: List<Mai2UserOption>? = null
var userExtend: List<Mai2UserExtend>? = null
var userCharacterList: List<Mai2UserCharacter>? = null
var userGhost: List<Mai2UserGhost>? = null
var userMapList: List<Mai2UserMap>? = null
var userLoginBonusList: List<Mai2UserLoginBonus>? = null
var userRatingList: List<UserRating>? = null
var userItemList: List<Mai2UserItem>? = null
var userMusicDetailList: List<Mai2UserMusicDetail>? = null
var userCourseList: List<Mai2UserCourse>? = null
var userFriendSeasonRankingList: List<Mai2UserFriendSeasonRanking>? = null
var userChargeList: List<Mai2UserCharge>? = null
var userFavoriteList: List<Mai2UserFavorite>? = null
var userActivityList: List<Mai2UserActivity>? = null
var userGamePlaylogList: List<Map<String, Any>>? = null
var userFavoritemusicList: List<Mai2UserFavoriteItem>? = null
var userKaleidxScopeList: List<Mai2UserKaleidx>? = null
var userIntimateList: List<Mai2UserIntimate>? = null
var isNewCharacterList: String? = null
var isNewMapList: String? = null
var isNewLoginBonusList: String? = null
var isNewItemList: String? = null
var isNewMusicDetailList: String? = null
var isNewCourseList: String? = null
var isNewFavoriteList: String? = null
var isNewFriendSeasonRankingList: String? = null
var isNewFavoritemusicList: String? = null
var isNewKaleidxScopeList: String? = null
}
class Mai2UserFavoriteItem {
var orderId = 0
var id = 0
}
class Mai2UserActivity {
var playList: List<Mai2UserAct> = emptyList()
var musicList: List<Mai2UserAct> = emptyList()
}

View File

@@ -0,0 +1,25 @@
package icu.samnyan.aqua.sega.maimai2.model.request
class Mai2UserPhoto {
var orderId = 0
var userId: Long = 0
var divNumber = 0
var divLength = 0
var divData: String? = null
var placeId = 0
var clientId: String? = null
var uploadDate: String? = null
var playlogId: Long = 0
var trackNo = 0
}
class Mai2UserPortrait {
var userId: Long = 0
var divNumber = 0
var divLength = 0
var divData: String? = null
var placeId = 0
var clientId: String = ""
var uploadDate: String = "1970-01-01 09:00:00.0"
var fileName: String = "portrait.jpg"
}

View File

@@ -1,18 +0,0 @@
package icu.samnyan.aqua.sega.maimai2.model.request;
import icu.samnyan.aqua.sega.maimai2.model.request.data.UserPhoto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UploadUserPhoto implements Serializable {
private UserPhoto userPhoto;
}

View File

@@ -1,20 +0,0 @@
package icu.samnyan.aqua.sega.maimai2.model.request;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserPlaylog;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UploadUserPlaylog implements Serializable {
private long userId;
private Mai2UserPlaylog userPlaylog;
}

View File

@@ -1,18 +0,0 @@
package icu.samnyan.aqua.sega.maimai2.model.request;
import icu.samnyan.aqua.sega.maimai2.model.request.data.UserPortrait;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UploadUserPortrait implements Serializable {
private UserPortrait userPortrait;
}

View File

@@ -1,25 +0,0 @@
package icu.samnyan.aqua.sega.maimai2.model.request;
import com.fasterxml.jackson.annotation.JsonProperty;
import icu.samnyan.aqua.sega.maimai2.model.request.data.UserAll;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UpsertUserAll implements Serializable {
private long userId;
private long playlogId;
@JsonProperty("isEventMode")
private boolean isEventMode;
@JsonProperty("isFreePlay")
private boolean isFreePlay;
private UserAll upsertUserAll;
}

View File

@@ -1,22 +0,0 @@
package icu.samnyan.aqua.sega.maimai2.model.request;
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserPrintDetail;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Map;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UpsertUserPrint implements Serializable {
private long userId;
private long orderId;
private Map<String, Object> userPrintReserve;
private Mai2UserPrintDetail userPrintDetail;
}

View File

@@ -1,50 +0,0 @@
package icu.samnyan.aqua.sega.maimai2.model.request.data;
import icu.samnyan.aqua.sega.maimai2.model.response.data.Mai2UserActivity;
import icu.samnyan.aqua.sega.maimai2.model.response.data.UserRating;
import icu.samnyan.aqua.sega.maimai2.model.userdata.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserAll implements Serializable {
private List<Mai2UserDetail> userData;
private List<Mai2UserExtend> userExtend;
private List<Mai2UserOption> userOption;
private List<Mai2UserCharacter> userCharacterList;
private List<Mai2UserGhost> userGhost;
private List<Mai2UserMap> userMapList;
private List<Mai2UserLoginBonus> userLoginBonusList;
private List<UserRating> userRatingList;
private List<Mai2UserItem> userItemList;
private List<Mai2UserMusicDetail> userMusicDetailList;
private List<Mai2UserCourse> userCourseList;
private List<Mai2UserFriendSeasonRanking> userFriendSeasonRankingList;
private List<Mai2UserCharge> userChargeList;
private List<Mai2UserFavorite> userFavoriteList;
private List<Mai2UserActivity> userActivityList;
private List<Map<String, Object>> userGamePlaylogList;
private List<UserFavoriteItem> userFavoritemusicList;
private List<Mai2UserKaleidx> userKaleidxScopeList;
private List<Mai2UserIntimate> userIntimateList;
private String isNewCharacterList;
private String isNewMapList;
private String isNewLoginBonusList;
private String isNewItemList;
private String isNewMusicDetailList;
private String isNewCourseList;
private String isNewFavoriteList;
private String isNewFriendSeasonRankingList;
private String isNewFavoritemusicList;
private String isNewKaleidxScopeList;
}

View File

@@ -1,15 +0,0 @@
package icu.samnyan.aqua.sega.maimai2.model.request.data;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserFavoriteItem implements Serializable {
private int orderId;
private int id;
}

View File

@@ -1,26 +0,0 @@
package icu.samnyan.aqua.sega.maimai2.model.request.data;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserPhoto implements Serializable {
private int orderId;
private long userId;
private int divNumber;
private int divLength;
private String divData;
private int placeId;
private String clientId;
private String uploadDate;
private long playlogId;
private int trackNo;
}

View File

@@ -1,24 +0,0 @@
package icu.samnyan.aqua.sega.maimai2.model.request.data;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserPortrait implements Serializable {
private long userId;
private int divNumber;
private int divLength;
private String divData;
private int placeId;
private String clientId;
private String uploadDate;
private String fileName;
}

View File

@@ -1,19 +0,0 @@
package icu.samnyan.aqua.sega.maimai2.model.response.data;
import java.util.List;
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserAct;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Mai2UserActivity {
private List<Mai2UserAct> playList;
private List<Mai2UserAct> musicList;
}

View File

@@ -1,24 +0,0 @@
package icu.samnyan.aqua.sega.maimai2.model.response.data;
import java.util.List;
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserRate;
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserUdemae;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserRating {
private int rating;
private List<Mai2UserRate> ratingList;
private List<Mai2UserRate> newRatingList;
private List<Mai2UserRate> nextRatingList;
private List<Mai2UserRate> nextNewRatingList;
private Mai2UserUdemae udemae;
}

View File

@@ -1,15 +0,0 @@
package icu.samnyan.aqua.sega.maimai2.model.response.data;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserRivalMusic {
private int musicId;
private List<UserRivalMusicDetail> userRivalMusicDetailList;
}

View File

@@ -1,14 +0,0 @@
package icu.samnyan.aqua.sega.maimai2.model.response.data;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserRivalMusicDetail {
private int level;
private int achievement;
private int deluxscoreMax;
}

View File

@@ -1,96 +0,0 @@
package icu.samnyan.aqua.sega.maimai2.worldslink
import ext.*
import icu.samnyan.aqua.net.utils.PathProps
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import java.io.BufferedWriter
import java.io.File
import java.io.FileOutputStream
import java.time.LocalDateTime
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
import kotlin.io.path.readText
// KotlinX Serialization
@OptIn(ExperimentalSerializationApi::class)
private val KJson = Json {
ignoreUnknownKeys = true
isLenient = true
explicitNulls = false
coerceInputValues = true
}
// Maximum time to live for a recruit record
const val MAX_TTL = 30 * 1000
@RestController
@RequestMapping(path = ["/mai2-futari"])
class FutariLobby(val paths: PathProps) {
// <IP Address, RecruitInfo>
val recruits = mutableMapOf<UInt, RecruitRecord>()
// Append writer
lateinit var writer: BufferedWriter
val mutex = ReentrantLock()
val log = logger()
init {
paths.init()
writer = FileOutputStream(File(paths.futariRecruitLog), true).bufferedWriter()
}
fun log(data: String) = mutex.withLock {
log.info(data)
writer.write(data)
writer.newLine()
writer.flush()
}
fun log(data: RecruitRecord, msg: String) =
log("${LocalDateTime.now().isoDateTime()}: $msg: ${KJson.encodeToString(data)}")
val RecruitRecord.ip get() = RecruitInfo.MechaInfo.IpAddress
@API("recruit/start")
fun startRecruit(@RB data: String) {
val d = parsing { KJson.decodeFromString<RecruitRecord>(data) }.apply { Time = millis() }
val exists = d.ip in recruits
recruits[d.ip] = d
if (!exists) log(d, "StartRecruit")
d.RecruitInfo.MechaInfo.UserIDs = d.RecruitInfo.MechaInfo.UserIDs.map { it.str.hashToUInt().toLong() }
}
@API("recruit/finish")
fun finishRecruit(@RB data: String) {
val d = parsing { KJson.decodeFromString<RecruitRecord>(data) }
if (d.ip !in recruits) 400 - "Recruit not found"
// if (d.Keychip != recruits[d.ip]!!.Keychip) 400 - "Keychip mismatch"
recruits.remove(d.ip)
log(d, "EndRecruit")
}
@API("recruit/list")
fun listRecruit(): String {
val time = millis()
recruits.filterValues { time - it.Time > MAX_TTL }.keys.forEach { recruits.remove(it) }
return recruits.values.toList().joinToString("\n") { KJson.encodeToString(it) }
}
@API("server-list")
fun serverList() = paths.futariRelayInfo.path().readText().trim()
}
fun main(args: Array<String>) {
val json = """{"RecruitInfo":{"MechaInfo":{"IsJoin":true,"IpAddress":1820162433,"MusicID":11692,"Entrys":[true,false],"UserIDs":[281474976710657,281474976710657],"UserNames":["",""],"IconIDs":[1,1],"FumenDifs":[0,-1],"Rateing":[0,0],"ClassValue":[0,0],"MaxClassValue":[0,0],"UserType":[0,0]},"MusicID":11692,"GroupID":0,"EventModeID":false,"JoinNumber":1,"PartyStance":0,"_startTimeTicks":638725464510308001,"_recvTimeTicks":0}}"""
println(json.jsonMap().toJson())
val data = KJson.decodeFromString<RecruitRecord>(json)
println(json)
println(KJson.encodeToString(data))
println(data)
}

View File

@@ -1,187 +0,0 @@
package icu.samnyan.aqua.sega.maimai2.worldslink
import ext.logger
import ext.md5
import ext.millis
import ext.thread
import java.io.BufferedReader
import java.io.BufferedWriter
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.net.ServerSocket
import java.net.Socket
import java.net.SocketTimeoutException
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.locks.ReentrantLock
import kotlin.collections.set
import kotlin.concurrent.withLock
const val PROTO_VERSION = 1
const val MAX_STREAMS = 10
const val SO_TIMEOUT = 20000
//const val SO_TIMEOUT = 10000000
fun ctlMsg(cmd: UInt, data: String? = null) = Msg(cmd, data = data)
data class ActiveClient(
val clientKey: String,
val socket: Socket,
val reader: BufferedReader,
val writer: BufferedWriter,
val thread: Thread = Thread.currentThread(),
// <Stream ID, Destination client stub IP>
val tcpStreams: MutableMap<UInt, UInt> = mutableMapOf(),
val pendingStreams: MutableSet<UInt> = mutableSetOf(),
) {
val log = logger()
val stubIp = keychipToStubIp(clientKey)
val writeMutex = ReentrantLock()
var lastHeartbeat = millis()
fun send(msg: Msg) {
writeMutex.withLock {
try {
writer.write(msg.toString())
writer.newLine()
writer.flush()
}
catch (e: Exception) {
log.error("Error sending message", e)
socket.close()
thread.interrupt()
}
}
}
}
fun ActiveClient.handle(msg: Msg) {
// Find target by dst IP address or TCP stream ID
val target = (msg.sid?.let { tcpStreams[it] } ?: msg.dst)?.let { clients[it] }
when (msg.cmd) {
Command.CTL_HEARTBEAT -> {
lastHeartbeat = millis()
send(ctlMsg(Command.CTL_HEARTBEAT))
}
Command.DATA_BROADCAST -> {
// Broadcast to all clients. This is only used in UDP so SID is always 0
if (msg.proto != Proto.UDP) return log.warn("TCP Broadcast received, something is wrong.")
clients.values.forEach { it.send(msg.copy(src = stubIp)) }
}
Command.DATA_SEND -> {
target ?: return log.warn("Send: Target not found: ${msg.dst}")
if (msg.proto == Proto.TCP && msg.sid !in tcpStreams)
return log.warn("Stream ID not found: ${msg.sid}")
target.send(msg.copy(src = stubIp, dst = target.stubIp))
}
Command.CTL_TCP_CONNECT -> {
target ?: return log.warn("Connect: Target not found: ${msg.dst}")
val sid = msg.sid ?: return log.warn("Connect: Stream ID not found")
if (sid in tcpStreams || sid in pendingStreams)
return log.warn("Stream ID already in use: $sid")
// Add the stream to the pending list
pendingStreams.add(sid)
if (pendingStreams.size > MAX_STREAMS) {
log.warn("Too many pending streams, closing connection")
return socket.close()
}
target.send(msg.copy(src = stubIp, dst = target.stubIp))
}
Command.CTL_TCP_ACCEPT -> {
target ?: return log.warn("Accept: Target not found: ${msg.dst}")
val sid = msg.sid ?: return log.warn("Accept: Stream ID not found")
if (sid !in target.pendingStreams)
return log.warn("Stream ID not found in pending list: $sid")
// Add the stream to the active list
target.pendingStreams.remove(sid)
target.tcpStreams[sid] = stubIp
tcpStreams[sid] = target.stubIp
target.send(msg.copy(src = stubIp, dst = target.stubIp))
}
}
}
fun String.hashToUInt() = md5().let {
((it[0].toUInt() and 0xFFu) shl 24) or
((it[1].toUInt() and 0xFFu) shl 16) or
((it[2].toUInt() and 0xFFu) shl 8) or
(it[3].toUInt() and 0xFFu)
}
fun keychipToStubIp(keychip: String) = keychip.hashToUInt()
// Keychip ID to Socket
val clients = ConcurrentHashMap<UInt, ActiveClient>()
/**
* Service for the party linker for AquaMai
*/
class MaimaiFutari(private val port: Int = 20101) {
val log = logger()
fun start() {
val serverSocket = ServerSocket(port)
log.info("Server started on port $port")
while (true) {
val clientSocket = serverSocket.accept().apply {
soTimeout = SO_TIMEOUT
log.info("[+] Client connected: $remoteSocketAddress")
}
thread { handleClient(clientSocket) }
}
}
fun handleClient(socket: Socket) {
val reader = BufferedReader(InputStreamReader(socket.getInputStream()))
val writer = BufferedWriter(OutputStreamWriter(socket.getOutputStream()))
var handler: ActiveClient? = null
try {
while (!Thread.interrupted() && !socket.isClosed) {
val input = (reader.readLine() ?: break).trim('\uFEFF')
if (input != "1,3") log.info("${socket.remoteSocketAddress} (${handler?.clientKey}) <<< $input")
val message = Msg.fromString(input)
when (message.cmd) {
// Start: Register the client. Payload is the keychip
Command.CTL_START -> {
val id = message.data as String
val client = ActiveClient(id, socket, reader, writer)
clients[client.stubIp]?.socket?.close()
clients[client.stubIp] = client
handler = clients[client.stubIp]
log.info("[+] Client registered: ${socket.remoteSocketAddress} -> $id")
// Send back the version
handler?.send(ctlMsg(Command.CTL_START, "version=$PROTO_VERSION"))
}
// Handle any other command using the handler
else -> {
(handler ?: throw Exception("Client not registered")).handle(message)
}
}
}
} catch (e: Exception) {
if (e.message != "Connection reset" && e !is SocketTimeoutException)
log.error("Error in client handler", e)
} finally {
// Remove client
handler?.stubIp?.let { clients.remove(it) }
socket.close()
log.info("[-] Client disconnected: ${handler?.clientKey}")
}
}
}
fun main() = MaimaiFutari().start()

View File

@@ -1,100 +0,0 @@
@file:Suppress("PropertyName")
package icu.samnyan.aqua.sega.maimai2.worldslink
import ext.*
import kotlinx.serialization.Serializable
object Command {
// Control plane
const val CTL_START = 1u
const val CTL_BIND = 2u
const val CTL_HEARTBEAT = 3u
const val CTL_TCP_CONNECT = 4u // Accept a new multiplexed TCP stream
const val CTL_TCP_ACCEPT = 5u
const val CTL_TCP_ACCEPT_ACK = 6u
const val CTL_TCP_CLOSE = 7u
// Data plane
const val DATA_SEND = 21u
const val DATA_BROADCAST = 22u
}
object Proto {
const val TCP = 6u
const val UDP = 17u
}
data class Msg(
var cmd: UInt,
var proto: UInt? = null,
var sid: UInt? = null,
var src: UInt? = null,
var sPort: UInt? = null,
var dst: UInt? = null,
var dPort: UInt? = null,
var data: String? = null
) {
override fun toString() = ls(
1, cmd, proto, sid, src, sPort, dst, dPort,
null, null, null, null, null, null, null, null, // reserved for future use
data
).joinToString(",") { it?.str ?: "" }.trimEnd(',')
companion object {
val fields = arr(Msg::proto, Msg::sid, Msg::src, Msg::sPort, Msg::dst, Msg::dPort)
fun fromString(str: String): Msg {
val sp = str.split(',')
return Msg(0u).apply {
cmd = sp[1].toUInt()
fields.forEachIndexed { i, f -> f.set(this, sp.getOrNull(i + 2)?.some?.toUIntOrNull()) }
data = sp.drop(16).joinToString(",")
}
}
}
}
@Serializable
data class MechaInfo(
val IsJoin: Bool,
val IpAddress: UInt,
val MusicID: Int,
val Entrys: List<Bool>,
var UserIDs: List<Long>,
val UserNames: List<String>,
val IconIDs: List<Int>,
val FumenDifs: List<Int>,
val Rateing: List<Int>,
val ClassValue: List<Int>,
val MaxClassValue: List<Int>,
val UserType: List<Int>
)
@Serializable
data class RecruitInfo(
val MechaInfo: MechaInfo,
val MusicID: Int,
val GroupID: Int,
val EventModeID: Boolean,
val JoinNumber: Int,
val PartyStance: Int,
val _startTimeTicks: Long,
val _recvTimeTicks: Long
)
@Serializable
data class RecruitRecord(
val RecruitInfo: RecruitInfo,
val Keychip: String,
var Server: RelayServerInfo? = null,
var Time: Long = 0,
)
@Serializable
data class RelayServerInfo(
val name: String,
val addr: String,
val port: Int = 20101,
val official: Bool = true
)

View File

@@ -79,9 +79,9 @@ class StringMapper: IMapper(STRING_MAPPER)
// Testing code
private class A {
var cat = ""
}
private class A(
var cat: String = ""
)
fun main(args: Array<String>) {
val json = """{"cat":"meow"}"""