mirror of
https://github.com/MewoLab/AquaDX.git
synced 2026-02-10 06:27:26 +08:00
[O] Finish mai2 refactor
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
package icu.samnyan.aqua
|
package icu.samnyan.aqua
|
||||||
|
|
||||||
import icu.samnyan.aqua.sega.aimedb.AimeDbServer
|
import icu.samnyan.aqua.sega.aimedb.AimeDbServer
|
||||||
import icu.samnyan.aqua.sega.maimai2.worldslink.MaimaiFutari
|
|
||||||
import icu.samnyan.aqua.spring.AutoChecker
|
import icu.samnyan.aqua.spring.AutoChecker
|
||||||
import org.springframework.boot.SpringApplication
|
import org.springframework.boot.SpringApplication
|
||||||
import org.springframework.boot.ansi.AnsiOutput
|
import org.springframework.boot.ansi.AnsiOutput
|
||||||
@@ -15,9 +14,6 @@ class Entry
|
|||||||
|
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
AnsiOutput.setEnabled(AnsiOutput.Enabled.ALWAYS)
|
AnsiOutput.setEnabled(AnsiOutput.Enabled.ALWAYS)
|
||||||
when (args.getOrNull(0)) {
|
|
||||||
"futari" -> return MaimaiFutari().start()
|
|
||||||
}
|
|
||||||
|
|
||||||
// If data/ is not found, create it
|
// If data/ is not found, create it
|
||||||
File("data").mkdirs()
|
File("data").mkdirs()
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package icu.samnyan.aqua.net.transfer
|
package icu.samnyan.aqua.net.transfer
|
||||||
|
|
||||||
import ext.*
|
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.UserActivity
|
||||||
import icu.samnyan.aqua.sega.chusan.model.userdata.UserItem
|
import icu.samnyan.aqua.sega.chusan.model.userdata.UserItem
|
||||||
import icu.samnyan.aqua.sega.chusan.model.userdata.UserMusicDetail
|
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.BasicMapper
|
||||||
import icu.samnyan.aqua.sega.util.jackson.IMapper
|
import icu.samnyan.aqua.sega.util.jackson.IMapper
|
||||||
import icu.samnyan.aqua.sega.util.jackson.StringMapper
|
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 userId = mapOf("userId" to allNet.userId)
|
||||||
val paged = userId + mapOf("nextIndex" to 0, "maxCount" to 10000000)
|
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))
|
userData = ls("GetUserDataApi".get("userData", userId))
|
||||||
userGameOption = ls("GetUserOptionApi".get("userGameOption", userId))
|
userGameOption = ls("GetUserOptionApi".get("userGameOption", userId))
|
||||||
userCharacterList = "GetUserCharacterApi".get("userCharacterList", paged)
|
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 userId = mapOf("userId" to allNet.userId)
|
||||||
val paged = userId + mapOf("nextIndex" to 0, "maxCount" to 10000000)
|
val paged = userId + mapOf("nextIndex" to 0, "maxCount" to 10000000)
|
||||||
|
|
||||||
return UpsertUserAll().apply {
|
return Mai2UserAll().apply {
|
||||||
userData = ls("GetUserDataApi".get("userData", userId))
|
userData = ls("GetUserDataApi".get("userData", userId))
|
||||||
|
// userGameOption = ls("GetUserOptionApi".get("userGameOption", userId))
|
||||||
|
|
||||||
}.toJson()
|
}.toJson()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package icu.samnyan.aqua.sega.chusan.handler
|
|||||||
|
|
||||||
import ext.*
|
import ext.*
|
||||||
import icu.samnyan.aqua.sega.chusan.ChusanController
|
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.chusan.model.userdata.*
|
||||||
import icu.samnyan.aqua.sega.general.model.response.UserRecentRating
|
import icu.samnyan.aqua.sega.general.model.response.UserRecentRating
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ fun ChusanController.upsertApiInit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
"UpsertUserAll" api@ {
|
"UpsertUserAll" api@ {
|
||||||
val req = parsing { mapper.convert<UpsertUserAll>(data["upsertUserAll"]!!) }
|
val req = parsing { mapper.convert<Chu3UserAll>(data["upsertUserAll"]!!) }
|
||||||
|
|
||||||
req.run {
|
req.run {
|
||||||
// UserData
|
// UserData
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ data class MusicIdWrapper(
|
|||||||
val musicId: Int = 0,
|
val musicId: Int = 0,
|
||||||
)
|
)
|
||||||
|
|
||||||
class UpsertUserAll(
|
class Chu3UserAll(
|
||||||
var userData: List<Chu3UserData>? = null,
|
var userData: List<Chu3UserData>? = null,
|
||||||
var userGameOption: List<UserGameOption>? = null,
|
var userGameOption: List<UserGameOption>? = null,
|
||||||
var userCharacterList: List<UserCharacter>? = null,
|
var userCharacterList: List<UserCharacter>? = null,
|
||||||
@@ -4,12 +4,10 @@ package icu.samnyan.aqua.sega.maimai2
|
|||||||
|
|
||||||
import ext.*
|
import ext.*
|
||||||
import icu.samnyan.aqua.sega.general.PagedHandler
|
import icu.samnyan.aqua.sega.general.PagedHandler
|
||||||
import icu.samnyan.aqua.sega.maimai2.model.response.data.UserRivalMusic
|
import icu.samnyan.aqua.sega.maimai2.model.UserRivalMusic
|
||||||
import icu.samnyan.aqua.sega.maimai2.model.response.data.UserRivalMusicDetail
|
import icu.samnyan.aqua.sega.maimai2.model.UserRivalMusicDetail
|
||||||
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserIntimate
|
|
||||||
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserKaleidx
|
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserKaleidx
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
fun Maimai2ServletController.initApis() {
|
fun Maimai2ServletController.initApis() {
|
||||||
// Used because maimai does not actually require paging implementation
|
// Used because maimai does not actually require paging implementation
|
||||||
@@ -157,7 +155,7 @@ fun Maimai2ServletController.initApis() {
|
|||||||
val rivalId = parsing { data["rivalId"]!!.long }
|
val rivalId = parsing { data["rivalId"]!!.long }
|
||||||
|
|
||||||
val lst = db.userMusicDetail.findByUserId(rivalId)
|
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 {
|
lst.forEach {
|
||||||
res[it.musicId]!!.userRivalMusicDetailList.add(
|
res[it.musicId]!!.userRivalMusicDetailList.add(
|
||||||
@@ -195,6 +193,7 @@ fun Maimai2ServletController.initApis() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Kaleidoscope, added on 1.50
|
// Kaleidoscope, added on 1.50
|
||||||
|
// [{gateId, phaseId}]
|
||||||
"GetGameKaleidxScope" { mapOf("gameKaleidxScopeList" to ls(
|
"GetGameKaleidxScope" { mapOf("gameKaleidxScopeList" to ls(
|
||||||
mapOf("gateId" to 1, "phaseId" to findPhase(LocalDate.of(2025, 1, 18))),
|
mapOf("gateId" to 1, "phaseId" to findPhase(LocalDate.of(2025, 1, 18))),
|
||||||
mapOf("gateId" to 2, "phaseId" to 2),
|
mapOf("gateId" to 2, "phaseId" to 2),
|
||||||
@@ -203,6 +202,8 @@ fun Maimai2ServletController.initApis() {
|
|||||||
mapOf("gateId" to 5, "phaseId" to 2),
|
mapOf("gateId" to 5, "phaseId" to 2),
|
||||||
mapOf("gateId" to 6, "phaseId" to 2),
|
mapOf("gateId" to 6, "phaseId" to 2),
|
||||||
)) }
|
)) }
|
||||||
|
// Request: {userId}
|
||||||
|
// Response: {userId, userKaleidxScopeList}
|
||||||
"GetUserKaleidxScope".unpaged {
|
"GetUserKaleidxScope".unpaged {
|
||||||
val u = db.userData.findByCardExtId(uid)() ?: (404 - "User not found")
|
val u = db.userData.findByCardExtId(uid)() ?: (404 - "User not found")
|
||||||
val lst = db.userKaleidx.findByUser(u)
|
val lst = db.userKaleidx.findByUser(u)
|
||||||
@@ -213,6 +214,8 @@ fun Maimai2ServletController.initApis() {
|
|||||||
|
|
||||||
lst
|
lst
|
||||||
}
|
}
|
||||||
|
// Request: {userId, version, userData: [UserDetail], userPlaylogList: [UserPlaylog]}
|
||||||
|
// Response: {userId, userItemList: [UserItem]}
|
||||||
// Added on 1.50
|
// Added on 1.50
|
||||||
"GetUserNewItemList" { mapOf("userId" to uid, "userItemList" to empty) }
|
"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 ext.invoke
|
||||||
import icu.samnyan.aqua.sega.general.BaseHandler
|
import icu.samnyan.aqua.sega.general.BaseHandler
|
||||||
import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos
|
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.Mai2UserRate
|
||||||
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserUdemae
|
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserUdemae
|
||||||
import icu.samnyan.aqua.sega.util.jackson.BasicMapper
|
import icu.samnyan.aqua.sega.util.jackson.BasicMapper
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
package icu.samnyan.aqua.sega.maimai2.handler
|
package icu.samnyan.aqua.sega.maimai2.handler
|
||||||
|
|
||||||
import ext.div
|
import ext.*
|
||||||
import ext.isoDateTime
|
|
||||||
import ext.logger
|
|
||||||
import ext.path
|
|
||||||
import icu.samnyan.aqua.sega.general.BaseHandler
|
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 icu.samnyan.aqua.sega.util.jackson.BasicMapper
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@@ -23,9 +20,7 @@ class UploadUserPhotoHandler(private val mapper: BasicMapper) :
|
|||||||
// Maimai DX sends split base64 data for one jpeg image.
|
// Maimai DX sends split base64 data for one jpeg image.
|
||||||
// So, make a temp file and keep append bytes until last part received.
|
// 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.
|
// If finished, rename it to other name so user can keep save multiple scorecards in a single day.
|
||||||
|
val up = parsing { mapper.convert(request["userPhoto"]!!, Mai2UserPhoto::class.java) }
|
||||||
val uploadUserPhoto = mapper.convert(request, UploadUserPhoto::class.java)
|
|
||||||
val up = uploadUserPhoto.userPhoto
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val tmpFile = tmpDir / "${up.userId}-${up.trackNo}.tmp"
|
val tmpFile = tmpDir / "${up.userId}-${up.trackNo}.tmp"
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
package icu.samnyan.aqua.sega.maimai2.handler
|
package icu.samnyan.aqua.sega.maimai2.handler
|
||||||
|
|
||||||
import ext.logger
|
import ext.logger
|
||||||
|
import ext.long
|
||||||
import ext.millis
|
import ext.millis
|
||||||
|
import ext.parsing
|
||||||
import icu.samnyan.aqua.sega.allnet.TokenChecker
|
import icu.samnyan.aqua.sega.allnet.TokenChecker
|
||||||
import icu.samnyan.aqua.sega.general.BaseHandler
|
import icu.samnyan.aqua.sega.general.BaseHandler
|
||||||
import icu.samnyan.aqua.sega.maimai2.model.Mai2UserDataRepo
|
import icu.samnyan.aqua.sega.maimai2.model.Mai2UserDataRepo
|
||||||
import icu.samnyan.aqua.sega.maimai2.model.Mai2UserPlaylogRepo
|
import icu.samnyan.aqua.sega.maimai2.model.Mai2UserPlaylogRepo
|
||||||
import icu.samnyan.aqua.sega.maimai2.model.request.UploadUserPlaylog
|
|
||||||
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserPlaylog
|
import icu.samnyan.aqua.sega.maimai2.model.userdata.Mai2UserPlaylog
|
||||||
import icu.samnyan.aqua.sega.util.jackson.BasicMapper
|
import icu.samnyan.aqua.sega.util.jackson.BasicMapper
|
||||||
import icu.samnyan.aqua.spring.Metrics
|
import icu.samnyan.aqua.spring.Metrics
|
||||||
@@ -33,9 +34,10 @@ class UploadUserPlaylogHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun handle(request: Map<String, Any>): String {
|
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) {
|
if (version != null) {
|
||||||
val session = TokenChecker.getCurrentSession()
|
val session = TokenChecker.getCurrentSession()
|
||||||
val gameId = if (session?.gameId in VALID_GAME_IDS) session!!.gameId else ""
|
val gameId = if (session?.gameId in VALID_GAME_IDS) session!!.gameId else ""
|
||||||
@@ -47,9 +49,9 @@ class UploadUserPlaylogHandler(
|
|||||||
|
|
||||||
// Check duplicate
|
// Check duplicate
|
||||||
val isDup = playlogRepo.findByUser_Card_ExtIdAndMusicIdAndUserPlayDate(
|
val isDup = playlogRepo.findByUser_Card_ExtIdAndMusicIdAndUserPlayDate(
|
||||||
req.userId,
|
uid,
|
||||||
req.userPlaylog.musicId,
|
playlog.musicId,
|
||||||
req.userPlaylog.userPlayDate
|
playlog.userPlayDate
|
||||||
).size > 0
|
).size > 0
|
||||||
if (isDup) {
|
if (isDup) {
|
||||||
log.info("Duplicate playlog detected")
|
log.info("Duplicate playlog detected")
|
||||||
@@ -57,14 +59,14 @@ class UploadUserPlaylogHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save if the user is registered
|
// Save if the user is registered
|
||||||
val u = userDataRepository.findByCardExtId(req.userId).getOrNull()
|
val u = userDataRepository.findByCardExtId(uid).getOrNull()
|
||||||
if (u != null) playlogRepo.save(req.userPlaylog.apply { user = u })
|
if (u != null) playlogRepo.save(playlog.apply { user = u })
|
||||||
|
|
||||||
// If the user hasn't registered (first play), save the playlog to a backlog
|
// If the user hasn't registered (first play), save the playlog to a backlog
|
||||||
else {
|
else {
|
||||||
playBacklog.putIfAbsent(req.userId, mutableListOf())
|
playBacklog.putIfAbsent(uid, mutableListOf())
|
||||||
playBacklog[req.userId]?.apply {
|
playBacklog[uid]?.apply {
|
||||||
add(BacklogEntry(millis(), req.userPlaylog))
|
add(BacklogEntry(millis(), playlog))
|
||||||
if (size > 6) clear() // Prevent abuse
|
if (size > 6) clear() // Prevent abuse
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ package icu.samnyan.aqua.sega.maimai2.handler
|
|||||||
|
|
||||||
import ext.div
|
import ext.div
|
||||||
import ext.logger
|
import ext.logger
|
||||||
|
import ext.parsing
|
||||||
import ext.path
|
import ext.path
|
||||||
import icu.samnyan.aqua.net.utils.PathProps
|
import icu.samnyan.aqua.net.utils.PathProps
|
||||||
import icu.samnyan.aqua.sega.general.BaseHandler
|
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 icu.samnyan.aqua.sega.util.jackson.BasicMapper
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
@@ -34,8 +35,7 @@ class UploadUserPortraitHandler(
|
|||||||
// Maimai DX sends split base64 data for one jpeg image.
|
// Maimai DX sends split base64 data for one jpeg image.
|
||||||
// So, make a temp file and keep append bytes until last part received.
|
// 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.
|
// If finished, rename it to other name so user can keep save multiple scorecards in a single day.
|
||||||
|
val up = parsing { mapper.convert(request["userPortrait"]!!, Mai2UserPortrait::class.java) }
|
||||||
val up = mapper.convert(request, UploadUserPortrait::class.java).userPortrait
|
|
||||||
|
|
||||||
val id = up.userId
|
val id = up.userId
|
||||||
val num = up.divNumber
|
val num = up.divNumber
|
||||||
|
|||||||
@@ -3,13 +3,12 @@ package icu.samnyan.aqua.sega.maimai2.handler
|
|||||||
import com.fasterxml.jackson.core.JsonProcessingException
|
import com.fasterxml.jackson.core.JsonProcessingException
|
||||||
import ext.invoke
|
import ext.invoke
|
||||||
import ext.mapApply
|
import ext.mapApply
|
||||||
import ext.minus
|
|
||||||
import ext.unique
|
import ext.unique
|
||||||
import icu.samnyan.aqua.sega.general.BaseHandler
|
import icu.samnyan.aqua.sega.general.BaseHandler
|
||||||
import icu.samnyan.aqua.sega.general.service.CardService
|
import icu.samnyan.aqua.sega.general.service.CardService
|
||||||
import icu.samnyan.aqua.sega.maimai2.handler.UploadUserPlaylogHandler.Companion.playBacklog
|
import icu.samnyan.aqua.sega.maimai2.handler.UploadUserPlaylogHandler.Companion.playBacklog
|
||||||
import icu.samnyan.aqua.sega.maimai2.model.*
|
import icu.samnyan.aqua.sega.maimai2.model.*
|
||||||
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.maimai2.model.userdata.*
|
||||||
import icu.samnyan.aqua.sega.util.jackson.BasicMapper
|
import icu.samnyan.aqua.sega.util.jackson.BasicMapper
|
||||||
import lombok.AllArgsConstructor
|
import lombok.AllArgsConstructor
|
||||||
@@ -31,16 +30,13 @@ class UpsertUserAllHandler(
|
|||||||
|
|
||||||
@Throws(JsonProcessingException::class)
|
@Throws(JsonProcessingException::class)
|
||||||
override fun handle(request: Map<String, Any>): Any? {
|
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 userId = upsertUserAll.userId
|
||||||
val req = upsertUserAll.upsertUserAll
|
val req = upsertUserAll.upsertUserAll
|
||||||
|
|
||||||
// If user is guest, just return OK response.
|
// If user is guest, just return OK response.
|
||||||
if ((userId and 281474976710657L) == 281474976710657L) return SUCCESS
|
if ((userId and 281474976710657L) == 281474976710657L) return SUCCESS
|
||||||
|
|
||||||
// UserData
|
|
||||||
if (req.userData == null) 400 - "Invalid Request"
|
|
||||||
|
|
||||||
val userData = repos.userData.findByCardExtId(userId)()
|
val userData = repos.userData.findByCardExtId(userId)()
|
||||||
val u = repos.userData.saveAndFlush(req.userData[0].apply {
|
val u = repos.userData.saveAndFlush(req.userData[0].apply {
|
||||||
id = userData?.id ?: 0
|
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
|
// Testing code
|
||||||
private class A {
|
private class A(
|
||||||
var cat = ""
|
var cat: String = ""
|
||||||
}
|
)
|
||||||
|
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
val json = """{"cat":"meow"}"""
|
val json = """{"cat":"meow"}"""
|
||||||
|
|||||||
Reference in New Issue
Block a user