mirror of
https://github.com/MewoLab/AquaDX.git
synced 2025-12-14 11:56:15 +08:00
[O] Finish mai2 refactor
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
@@ -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) }
|
||||
|
||||
|
||||
@@ -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\":[]}";
|
||||
}
|
||||
}
|
||||
@@ -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":[]}"""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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":["GUEST","GUEST"],"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)
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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
|
||||
)
|
||||
@@ -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"}"""
|
||||
|
||||
Reference in New Issue
Block a user