[maimai2] Support user portrait

pull/1/head
Mikira Sora 2022-12-11 04:47:44 +00:00 committed by Dom Eori
parent 75932c4b75
commit 709b977e73
8 changed files with 253 additions and 7 deletions

View File

@ -47,6 +47,10 @@ game.ongeki.version=1.05.00
## Set this true if you are using old version of Splash network patch and have no other choice.
## This is a dirty workaround. If enabled, you probably won't able to play other versions.
game.maimai2.splash-old-patch=false
## Allow users take photo as their avatar/portrait photo.
game.maimai2.userPhoto.enable=true
## Specify folder path that user portrait photo and its (.json) data save to.
game.maimai2.userPhoto.picSavePath=data/userPhoto
## Logging
spring.servlet.multipart.max-file-size=10MB

View File

@ -59,7 +59,6 @@ Only JP variant is supported.
### Non-working features
* KOP related
* User portrait
* Tournament mode
### Additional notes

View File

@ -47,6 +47,8 @@ public class Maimai2ServletController {
private final UploadUserPlaylogHandler uploadUserPlaylogHandler;
private final GetGameNgMusicIdHandler getGameNgMusicIdHandler;
private final GetUserFriendSeasonRankingHandler getUserFriendSeasonRankingHandler;
private final GetUserPortraitHandler getUserPortraitHandler;
private final UploadUserPortraitHandler uploadUserPortraitHandler;
private final CMGetUserPreviewHandler cmGetUserPreviewHandler;
private final CMGetSellingCardHandler cmGetSellingCardHandler;
private final GetUserCardPrintErrorHandler getUserCardPrintErrorHandler;
@ -60,7 +62,7 @@ public class Maimai2ServletController {
GetUserLoginBonusHandler getUserLoginBonusHandler, GetUserMapHandler getUserMapHandler, GetUserFavoriteHandler getUserFavoriteHandler,
GetUserCardHandler getUserCardHandler, GetUserMusicHandler getUserMusicHandler, GetUserRatingHandler getUserRatingHandler, GetUserRegionHandler getUserRegionHandler,
GetGameChargeHandler getGameChargeHandler, GetUserChargeHandler getUserChargeHandler, GetUserCourseHandler getUserCourseHandler, UploadUserPhotoHandler uploadUserPhotoHandler,
UploadUserPlaylogHandler uploadUserPlaylogHandler, GetGameNgMusicIdHandler getGameNgMusicIdHandler, GetUserFriendSeasonRankingHandler getUserFriendSeasonRankingHandler,
UploadUserPlaylogHandler uploadUserPlaylogHandler, UploadUserPortraitHandler uploadUserPortraitHandler, GetGameNgMusicIdHandler getGameNgMusicIdHandler,GetUserPortraitHandler getUserPortraitHandler, GetUserFriendSeasonRankingHandler getUserFriendSeasonRankingHandler,
CMGetUserPreviewHandler cmGetUserPreviewHandler, CMGetSellingCardHandler cmGetSellingCardHandler, GetUserCardPrintErrorHandler getUserCardPrintErrorHandler, CMGetUserCharacterHandler cmGetUserCharacterHandler,
UpsertUserPrintHandler upsertUserPrintHandler) {
this.getGameSettingHandler = getGameSettingHandler;
@ -93,6 +95,8 @@ public class Maimai2ServletController {
this.uploadUserPlaylogHandler = uploadUserPlaylogHandler;
this.getGameNgMusicIdHandler = getGameNgMusicIdHandler;
this.getUserFriendSeasonRankingHandler = getUserFriendSeasonRankingHandler;
this.getUserPortraitHandler = getUserPortraitHandler;
this.uploadUserPortraitHandler = uploadUserPortraitHandler;
this.cmGetUserPreviewHandler = cmGetUserPreviewHandler;
this.cmGetSellingCardHandler = cmGetSellingCardHandler;
this.getUserCardPrintErrorHandler = getUserCardPrintErrorHandler;
@ -188,10 +192,9 @@ public class Maimai2ServletController {
return getUserOptionHandler.handle(request);
}
// No support
@PostMapping("GetUserPortraitApi")
public String getUserPortraitHandler(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return "{\"length\":0,\"userPortraitList\":[]}";
return getUserPortraitHandler.handle(request);
}
@PostMapping("GetUserPreviewApi")
@ -226,10 +229,9 @@ public class Maimai2ServletController {
return uploadUserPlaylogHandler.handle(request);
}
// No support, return error code
@PostMapping("UploadUserPortraitApi")
public String uploadUserPortraitHandler(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return "{\"returnCode\":-1,\"apiName\":\"com.sega.maimai2servlet.api.UploadUserPortraitApi\"}";
return uploadUserPortraitHandler.handle(request);
}
@PostMapping("UserLoginApi")
@ -299,7 +301,7 @@ public class Maimai2ServletController {
public String getUserFriendSeasonRankingHandler(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
return getUserFriendSeasonRankingHandler.handle(request);
}
@PostMapping("Ping")
String ping(@ModelAttribute Map<String, Object> request) {
return "{\"returnCode\":\"1\"}";

View File

@ -0,0 +1,103 @@
package icu.samnyan.aqua.sega.maimai2.handler.impl;
import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonProcessingException;
import icu.samnyan.aqua.sega.maimai2.handler.BaseHandler;
import icu.samnyan.aqua.sega.maimai2.model.request.UploadUserPortrait;
import icu.samnyan.aqua.sega.maimai2.model.request.data.UserPortrait;
import icu.samnyan.aqua.sega.util.jackson.BasicMapper;
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.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
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 String picSavePath;
private final boolean enable;
public GetUserPortraitHandler(BasicMapper mapper, @Value("${game.maimai2.userPhoto.enable:}") boolean enable, @Value("${game.maimai2.userPhoto.picSavePath:}") String picSavePath) {
this.mapper = mapper;
this.picSavePath = picSavePath;
this.enable = enable;
if (enable) {
try {
Files.createDirectories(Paths.get(picSavePath));
} catch (Exception ignored) {
}
}
}
@Override
public String handle(Map<String, Object> request) throws JsonProcessingException {
if (enable) {
var userId = ((Number) request.get("userId")).longValue();
var list = new ArrayList<UserPortrait>();
try {
var filePath = Paths.get(picSavePath, userId + "-up.jpg");
var templateJsonStr = Files.readString(Paths.get(picSavePath, userId + "-up.json"));
var templateUserPortrait = mapper.read(templateJsonStr, UserPortrait.class);
var buffer = new byte[10240];
if (Files.exists(filePath)) {
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(templateUserPortrait.getFileName());
userPortrait.setPlaceId(templateUserPortrait.getPlaceId());
userPortrait.setUserId(templateUserPortrait.getUserId());
userPortrait.setClientId(templateUserPortrait.getClientId());
userPortrait.setUploadDate(templateUserPortrait.getUploadDate());
userPortrait.setDivData(Utf8.decode(Base64.getEncoder().encode(encodeBuffer)));
userPortrait.setDivNumber(list.size());
list.add(userPortrait);
}
stream.close();
for (var i = 0; i < list.size(); i++) {
var userPortrait = list.get(i);
userPortrait.setDivLength(list.size());
}
var map = new HashMap<String, Object>();
map.put("length", list.size());
map.put("userPortraitList", list);
var respJson = mapper.write(map);
return respJson;
}
} catch (Exception e) {
logger.error("Result: User photo save failed", e);
}
}
return "{\"length\":0,\"userPortraitList\":[]}";
}
}

View File

@ -0,0 +1,88 @@
package icu.samnyan.aqua.sega.maimai2.handler.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import icu.samnyan.aqua.sega.maimai2.handler.BaseHandler;
import icu.samnyan.aqua.sega.maimai2.model.request.UploadUserPortrait;
import icu.samnyan.aqua.sega.util.jackson.BasicMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.Base64;
import java.util.Map;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Component("Maimai2UploadUserPortraitHandler")
public class UploadUserPortraitHandler implements BaseHandler {
private static final Logger logger = LoggerFactory.getLogger(UploadUserPortraitHandler.class);
private final BasicMapper mapper;
private final String picSavePath;
private final boolean enable;
public UploadUserPortraitHandler(BasicMapper mapper, @Value("${game.maimai2.userPhoto.enable:}") boolean enable, @Value("${game.maimai2.userPhoto.picSavePath:}") String picSavePath) {
this.mapper = mapper;
this.picSavePath = picSavePath;
this.enable = enable;
if (enable) {
try {
Files.createDirectories(Paths.get(picSavePath));
} catch (Exception ignored) {
}
}
}
@Override
public String handle(Map<String, Object> request) throws JsonProcessingException {
/*
Maimai DX sends splited 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 score cards in a single day.
*/
if (enable) {
var uploadUserPhoto = mapper.convert(request, UploadUserPortrait.class);
var userPhoto = uploadUserPhoto.getUserPortrait();
long userId = userPhoto.getUserId();
int divNumber = userPhoto.getDivNumber();
int divLength = userPhoto.getDivLength();
String divData = userPhoto.getDivData();
try {
var tmp_filename = Paths.get(picSavePath, userId + "-up.tmp");
if (divNumber == 0)
Files.deleteIfExists(tmp_filename);
byte[] imageData = Base64.getDecoder().decode(divData);
Files.write(tmp_filename, imageData, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
logger.info(String.format("received user %d photo data %d/%d", userId, divNumber + 1, divLength));
if (divNumber == (divLength - 1)) {
var filename = Paths.get(picSavePath, userId + "-up.jpg");
Files.move(tmp_filename, filename, StandardCopyOption.REPLACE_EXISTING);
userPhoto.setDivData("");
var userPortaitMetaJson = mapper.write(userPhoto);
var json_filename = Paths.get(picSavePath, userId + "-up.json");
Files.write(json_filename, userPortaitMetaJson.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
logger.info(String.format("saved user %d photo data", userId));
}
} catch (IOException e) {
logger.error("Result: User photo save failed", e);
}
}
return "{\"returnCode\":1,\"apiName\":\"com.sega.maimai2servlet.api.UploadUserPortraitApi\"}";
}
}

View File

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

View File

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

View File

@ -38,6 +38,14 @@ public class BasicMapper {
}
public <T> T read(String jsonStr, Class<T> toClass) throws JsonProcessingException {
return mapper.readValue(jsonStr, toClass);
}
public <T> T read(String jsonStr, TypeReference<T> toValueTypeRef) throws JsonProcessingException {
return mapper.readValue(jsonStr, toValueTypeRef);
}
public <T> T convert(Object map, Class<T> toClass) {
return mapper.convertValue(map, toClass);
}