34 Commits

Author SHA1 Message Date
Azalea
41d4de6150 [F] Fix github actions 2023-12-22 19:01:28 -05:00
Azalea
bcf3333dd2 [F] Fix github actions 2023-12-22 15:37:55 -08:00
Azalea
47fda64c90 [U] Update chusan notes 2023-12-22 16:47:04 -05:00
Azalea
4fce05b7d1 [F] Fix github actions 2023-12-21 02:13:36 -05:00
Azalea
363bcc6060 [F] Fix github actions 2023-12-21 02:10:55 -05:00
Azalea
bc734a5d25 [+] Nightly build action 2023-12-21 02:10:08 -05:00
Azalea
3f95678098 [+] Example systemd service 2023-12-21 01:52:11 -05:00
Azalea
e52c971aed [+] Build action 2023-12-21 01:47:48 -05:00
Azalea
1f82067752 [F] Resolve name conflict 2023-12-21 01:39:50 -05:00
Azalea
0ac1a4c088 [-] Remove deploy script 2023-12-21 01:28:46 -05:00
Azalea Gui
0fda25b482 [+] Docker 2023-12-21 01:27:18 -05:00
Azalea
dd70265cb6 [U] Update readme 2023-12-21 01:24:18 -05:00
Azalea
62e7d48f3c Merge rinsama/aqua : Ensure Chusan compatibility and add support for SunPlus
7a7076b1 - [chusan]Attempting to ensure compatibility with all known versions of Chusan, both before and after SunPlus
c8e1c5fb - fix BooleanStringIntDeserializer always returns false
50ceaf60 - Add Support for sunplus

Co-authored-by: Sanheiii <35133371+Sanheiii@users.noreply.github.com>
Co-authored-by: HoshimiRIN <admin@sakuramoe.dev>
2023-12-21 01:05:06 -05:00
Azalea
4905106953 [O] Optimize merge script 2023-12-21 01:04:55 -05:00
Azalea
48edab452d [+] Merge script 2023-12-21 00:44:36 -05:00
HoshimiRIN
7a7076b174 [chusan]Attempting to ensure compatibility with all known versions of Chusan, both before and after SunPlus 2023-12-17 21:50:09 +08:00
Sanheiii
c8e1c5fbb7 fix BooleanStringIntDeserializer always returns false 2023-12-16 15:39:49 +08:00
HoshimiRIN
50ceaf6097 Add Support for sunplus 2023-12-16 15:39:49 +08:00
Azalea
722d415e75 Merge pull request #4 from FeiZhaixiage/master
[API] Add a feature to get user photos.
2023-12-08 05:12:09 +09:00
肥宅虾哥
0d4221203b [API] Add a feature to retrieve user photos. 2023-12-08 02:49:19 +08:00
肥宅虾哥
4a64895e81 [api] Add maimai2 userPhoto API 2023-12-08 02:45:24 +08:00
肥宅虾哥
e271cb4555 [API] Add a feature to retrieve user photos.
Add:
Add a feature to retrieve user photos.
Fix bug:
Missing dependency: ObjectMapper
2023-12-08 02:21:49 +08:00
肥宅虾哥
0bf54e666b [API] Let web music list read from database
Add a feature to retrieve user photos.
2023-12-08 02:18:35 +08:00
肥宅虾哥
0913ef2060 [api] Add maimai2 userPhoto API 2023-12-08 02:14:20 +08:00
肥宅虾哥
7cc9fb11b6 Revert "New API to return user photos."
This reverts commit ba1f458907.
2023-12-08 02:10:49 +08:00
肥宅虾哥
9c51b1e0ee Revert "Add API - Get user photos (mai)"
This reverts commit e7848cb965.
2023-12-08 02:07:18 +08:00
肥宅虾哥
ba1f458907 New API to return user photos.
New API to return user photos.
2023-12-08 02:02:19 +08:00
肥宅虾哥
e7848cb965 Add API - Get user photos (mai)
Add:
New API to return  user photos.
Fix bug:
Missing dependency: ObjectMapper
2023-12-07 19:13:13 +08:00
Azalea
564ada10f5 Merge pull request #2 from FeiZhaixiage/patch-1 2023-12-06 03:07:40 +09:00
肥宅虾哥
48721ef7a9 Update Api - Add item list output by ItemKind.
Add item list output by ItemKind.
2023-12-05 23:56:32 +08:00
Azalea
bae06e2187 Merge pull request #1 from FeiZhaixiage/master 2023-12-05 09:53:08 +09:00
肥宅虾哥
8b8e6cb422 Update ApiMaimai2PlayerDataController.java
Add API to save options settings.
2023-12-05 06:24:46 +08:00
Azalea Gui
2ecc990aae [F] Fix Found more than one migration with version 241 2023-11-15 00:35:20 -05:00
Azalea
b47a841207 [F] chmod +x 2023-11-15 00:25:33 -05:00
24 changed files with 595 additions and 25 deletions

24
.github/workflows/gradle.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Gradle Build
on:
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
server-id: github
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
- name: Build with Gradle
run: ./gradlew build

75
.github/workflows/nightly.yml vendored Normal file
View File

@@ -0,0 +1,75 @@
# Build script credit to https://github.com/OpenIntelWireless/itlwm/blob/master/.github/workflows/main.yml
name: Nightly Build
on:
push:
branches: [master]
jobs:
build:
permissions: write-all
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: '10'
- name: Set up JDK
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
server-id: github
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
- name: Manage Version
run: |
git fetch --prune --unshallow --tags
GIT_SHA="$(git rev-parse --short HEAD)"
CUR_TAG="$(git tag -l | grep 'nightly' | tail -1)"
VER="$(sed -n 's/version = "\(.*\)"/\1/p' build.gradle.kts)"
echo "SHORT_SHA=$GIT_SHA" >> $GITHUB_ENV
echo "VER=$VER" >> $GITHUB_ENV
if [[ -z $CUR_TAG ]]; then
echo "OLD_PRE_TAG=NULL" >> $GITHUB_ENV
else
echo "OLD_PRE_TAG=$CUR_TAG" >> $GITHUB_ENV
fi
- name: Build Artifact
run: |
./gradlew build
rm -rf build/libs/*-plain.jar
cp build/libs/*.jar "build/libs/aqua-nightly.jar"
- name: Generate Prerelease Release Notes
run: |
echo '### Nightly Release' >> ReleaseNotes.md
echo 'This nightly release is automatically built by github actions.' >> ReleaseNotes.md
echo '### The latest five updates are:' >> ReleaseNotes.md
git log -"5" --format="- %H %s" | sed '/^$/d' >> ReleaseNotes.md
- name: Delete Old Prerelease
if: env.OLD_PRE_TAG != 'NULL'
uses: dev-drprasad/delete-tag-and-release@v1.0
with:
tag_name: ${{ env.OLD_PRE_TAG }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish GitHub Release
uses: ncipollo/release-action@v1
with:
bodyFile: ReleaseNotes.md
artifacts: "build/libs/aqua-nightly.jar"
tag: "${{ env.VER }}-nightly"
token: ${{ secrets.GITHUB_TOKEN }}
draft: false
- name: Mark release undraft
run: |
gh release edit "${{ env.VER }}-nightly" --draft=false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

32
Dockerfile Normal file
View File

@@ -0,0 +1,32 @@
# Use a multi-stage build to keep the image size small
# Start with a Gradle image for building the project
FROM gradle:jdk11 as builder
# Copy the Gradle wrapper and configuration files separately to leverage Docker cache
COPY --chown=gradle:gradle gradlew /home/gradle/
COPY --chown=gradle:gradle gradle /home/gradle/gradle
COPY --chown=gradle:gradle build.gradle.kts settings.gradle.kts /home/gradle/
# Set working directory
WORKDIR /home/gradle
# Download dependencies - cached if build.gradle.kts and settings.gradle.kts are unchanged
RUN ./gradlew dependencies
# Copy the project source, this layer is rebuilt whenever a file has changed
COPY --chown=gradle:gradle src /home/gradle/src
# Build the application
RUN ./gradlew build -x test
# Start with a fresh image for the runtime
FROM openjdk:11-jre-slim
# Set the deployment directory
WORKDIR /app
# Copy only the built JAR from the builder image
COPY --from=builder /home/gradle/build/libs/aqua-?.?.??.jar /app/
# The command to run the application
CMD java -jar aqua-*.jar

View File

@@ -1,15 +1,20 @@
# Aqua Server
# AquaDX
Multipurpose game server powered by Spring Boot, for ALL.Net based games
This is a forked maintaining attempt of the [original Aqua server](https://dev.s-ul.net/NeumPhis/aqua)
### Supported Games
* CHUNITHM SUN (and below)
* CHUNITHM Paradise Lost (and below)
* Maimai DX Festival Plus (and below)
* Card Maker (1.34)
* Project DIVA Arcade Future Tone
* O.N.G.E.K.I. bright memory (and below)
Below is a list of games supported by this server.
| Game | Ver | Codename | Thanks to |
|---------------------------|------|---------------|---------------------------------------|
| SDHD: CHUNITHM (Chusan) | 2.16 | SUN Plus | [@rinsama](https://github.com/mxihan) |
| SDEZ: MaiMai DX | 1.35 | FESTiVAL Plus | |
| SDED: Card Maker | 1.34 | | |
| SBZV: Project DIVA Arcade | 7.10 | Future Tone | |
| SDDT: O.N.G.E.K.I. | 1.39 | bright MEMORY | |
Check out these docs for more information.
* [Game specific notes](docs/game_specific_notes.md)

26
docker-compose.yml Normal file
View File

@@ -0,0 +1,26 @@
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080" # Replace with your application's port
environment:
- DB_HOST=db
- DB_PORT=3306
- DB_USER=root
- DB_PASSWORD=mysecret
depends_on:
- db
db:
image: mariadb:latest
environment:
MYSQL_ROOT_PASSWORD: mysecret
MYSQL_DATABASE: myappdb
MYSQL_USER: myappuser
MYSQL_PASSWORD: myapppassword
ports:
- "3306:3306"
volumes:
- ./db:/var/lib/mysql

13
docs/aqua.service Normal file
View File

@@ -0,0 +1,13 @@
[Unit]
Description=Aqua Server
After=network.target
[Service]
WorkingDirectory=/apps/aqua
ExecStart=/usr/bin/java -jar aqua.jar
User=nobody
Type=simple
Restart=on-failure
[Install]
WantedBy=multi-user.target

View File

@@ -5,15 +5,15 @@ This document is for detailed game specific notes, if any.
## Overview
| Name | Game ID | Latest supported version | Latest supported option | Actively supported | Requires patch |
| --- | --- | --- | --- | --- | --- |
|Chunithm (Chusan)|SDHD |Sun |A152 |Yes |Yes |
|Chunithm |SDBT |Paradise Lost |A032 |Yes |Yes (Paradise) |
|Maimai DX |SDEZ |Festival |F061 |Yes |Yes |
|O.N.G.E.K.I |SDDT |Bright memory |A082 |Yes |Yes |
|Card Maker |SDED |1.34 |A030 |Yes |Yes |
|Maimai |SDEY |Finale |? |No |? |
|Project DIVA AFT |SBZV |? |? |No |? |
| Name | Game ID | Latest supported version | Latest supported option | Actively supported | Requires patch |
|-------------------|---------|--------------------------|-------------------------|--------------------|----------------|
| Chunithm (Chusan) | SDHD | Sun | A152 | Yes | Yes |
| Chunithm | SDBT | Paradise Lost | A032 | Yes | Yes (Paradise) |
| Maimai DX | SDEZ | Festival | F061 | Yes | Yes |
| O.N.G.E.K.I | SDDT | Bright memory | A082 | Yes | Yes |
| Card Maker | SDED | 1.34 | A030 | Yes | Yes |
| Maimai | SDEY | Finale | ? | No | ? |
| Project DIVA AFT | SBZV | ? | ? | No | ? |
* Actively supported: if yes, it will likely receive future bug fixes and new version support.
* Requires patch: if yes, game needs to be patched in order to work with Aqua server.
@@ -24,6 +24,7 @@ Only JP variant is supported.
### Required patches
* No encryption
* For SUN Plus: Please edit `A001/event/event00000015/Event.xml` and change `<alwaysOpen>false</alwaysOpen>` to `true`.
### Non-working features
* Global matching
@@ -97,4 +98,4 @@ Only JP variant is supported.
* Only stated version above are supported.
* Server does not consider gacha rarity and probability weight during card draw.
* Server returns same hard-coded serial for each cards. This is intentional behavior.
* Due to its high correlation with every game endpoints, this may cease to work after major game version up.
* Due to its high correlation with every game endpoints, this may cease to work after major game version up.

0
gradlew vendored Normal file → Executable file
View File

View File

@@ -1,9 +1,12 @@
package icu.samnyan.aqua.api.controller.sega.game.maimai2;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import icu.samnyan.aqua.api.model.MessageResponse;
import icu.samnyan.aqua.api.model.ReducedPageResponse;
import icu.samnyan.aqua.api.model.resp.sega.maimai2.ProfileResp;
import icu.samnyan.aqua.api.model.resp.sega.maimai2.PhotoResp;
import icu.samnyan.aqua.api.model.resp.sega.maimai2.external.ExternalUserData;
import icu.samnyan.aqua.api.model.resp.sega.maimai2.external.Maimai2DataExport;
import icu.samnyan.aqua.api.model.resp.sega.maimai2.external.Maimai2DataImport;
@@ -21,9 +24,12 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.nio.file.*;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* @author samnyan (privateamusement@protonmail.com)
@@ -89,6 +95,38 @@ public class ApiMaimai2PlayerDataController {
return divMaxLength;
}
@GetMapping("userPhoto")
public PhotoResp getUserPhoto(@RequestParam long aimeId,
@RequestParam(required = false, defaultValue = "0") int imageIndex) {
List<String> matchedFiles = new ArrayList<>();
PhotoResp Photo = new PhotoResp();
try (Stream<Path> paths = Files.walk(Paths.get("data"))) {
matchedFiles = paths
.filter(Files::isRegularFile)
.filter(path -> path.getFileName().toString().endsWith(".jpg"))
.filter(path -> {
String fileName = path.getFileName().toString();
String[] parts = fileName.split("-");
return parts.length > 0 && parts[0].equals(String.valueOf(aimeId));
})
.map(Path::getFileName)
.map(Path::toString)
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList());
Photo.setTotalImage(matchedFiles.size());
Photo.setImageIndex(imageIndex);
if(matchedFiles.size() > imageIndex){
byte[] targetImageContent = Files.readAllBytes(Paths.get("data/" + matchedFiles.get(imageIndex)));
String divData = Base64.getEncoder().encodeToString(targetImageContent);
Photo.setDivData(divData);
Photo.setFileName(matchedFiles.get(imageIndex));
}
}
catch (Exception e) {
}
return Photo;
}
@GetMapping("profile")
public ProfileResp getProfile(@RequestParam long aimeId) {
return mapper.convert(userDataRepository.findByCard_ExtId(aimeId).orElseThrow(), new TypeReference<>() {
@@ -153,8 +191,15 @@ public class ApiMaimai2PlayerDataController {
@GetMapping("item")
public ReducedPageResponse<UserItem> getItem(@RequestParam long aimeId,
@RequestParam(required = false, defaultValue = "0") int page,
@RequestParam(required = false, defaultValue = "10") int size) {
Page<UserItem> items = userItemRepository.findByUser_Card_ExtId(aimeId, PageRequest.of(page, size));
@RequestParam(required = false, defaultValue = "10") int size,
@RequestParam(required = false, defaultValue = "0") int ItemKind) {
Page<UserItem> items;
if(ItemKind == 0){
items = userItemRepository.findByUser_Card_ExtId(aimeId, PageRequest.of(page, size));
}
else{
items = userItemRepository.findByUser_Card_ExtIdAndItemKind(aimeId, ItemKind, PageRequest.of(page, size));
}
return new ReducedPageResponse<>(items.getContent(), items.getPageable().getPageNumber(), items.getTotalPages(), items.getTotalElements());
}
@@ -207,6 +252,17 @@ public class ApiMaimai2PlayerDataController {
return userOptionRepository.findByUser_Card_ExtId(aimeId).orElseThrow();
}
@PostMapping("options")
public ResponseEntity<Object> updateOptions(@RequestBody Map<String, Object> request) {
UserDetail profile = userDataRepository.findByCard_ExtId(((Number) request.get("aimeId")).longValue()).orElseThrow();
ObjectMapper objectMapper = new ObjectMapper();
UserOption userOption = objectMapper.convertValue(request.get("options"), UserOption.class);
userOption.setUser(profile);
userOptionRepository.deleteByUser(profile);
userOptionRepository.flush();
return ResponseEntity.ok(userOptionRepository.save(userOption));
}
@GetMapping("general")
public ResponseEntity<Object> getGeneralData(@RequestParam long aimeId, @RequestParam String key) {
Optional<UserGeneralData> userGeneralDataOptional = userGeneralDataRepository.findByUser_Card_ExtIdAndPropertyKey(aimeId, key);

View File

@@ -0,0 +1,15 @@
package icu.samnyan.aqua.api.model.resp.sega.maimai2;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PhotoResp {
private int imageIndex;
private int totalImage;
private String fileName;
private String divData;
}

View File

@@ -188,7 +188,7 @@ public class AllNetController {
return "http://" + addr + ":" + port + "/Maimai2Servlet/";
}
case "SDHD":
return "http://" + addr + ":" + port + "/ChusanServlet/";
return "http://" + addr + ":" + port + "/ChusanServlet/" + ver + "/";
case "SDED":
return "http://" + addr + ":" + port + "/CardMakerServlet/";
default:

View File

@@ -12,7 +12,7 @@ import java.util.Map;
* @author samnyan (privateamusement@protonmail.com)
*/
@RestController
@RequestMapping({"/ChusanServlet/ChuniServlet", "/ChusanServlet"})
@RequestMapping({"/ChusanServlet/{version}/ChuniServlet", "/ChusanServlet/{version}"})
public class ChusanServletController {
private final GameLoginHandler gameLoginHandler;
@@ -235,7 +235,8 @@ public class ChusanServletController {
}
@PostMapping("GetUserMusicApi")
String getUserMusic(@ModelAttribute Map<String, Object> request) throws JsonProcessingException {
String getUserMusic(@ModelAttribute Map<String, Object> request, @PathVariable String version) throws JsonProcessingException {
request.put("version", version);
return getUserMusicHandler.handle(request);
}

View File

@@ -0,0 +1,84 @@
package icu.samnyan.aqua.sega.chusan.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonPropertyOrder({
"musicId",
"level",
"playCount",
"scoreMax",
"missCount",
"maxComboCount",
"isFullCombo",
"isAllJustice",
"isSuccess",
"fullChain",
"maxChain",
"isLock",
"theoryCount",
"ext1"
})
public class UserMusicDetailForAncientChusan implements Serializable {
private static final long serialVersionUID = 1L;
private int musicId;
private int level;
private int playCount;
private int scoreMax;
private int missCount;
private int maxComboCount;
@JsonProperty("isFullCombo")
private boolean isFullCombo;
@JsonProperty("isAllJustice")
private boolean isAllJustice;
@JsonProperty("isSuccess")
private boolean isSuccess;
private int fullChain;
private int maxChain;
private int scoreRank;
@JsonProperty("isLock")
private boolean isLock;
private int theoryCount;
private int ext1;
public UserMusicDetailForAncientChusan(int musicId, int level, int playCount, int scoreMax, int missCount, int maxComboCount, boolean isFullCombo, boolean isAllJustice, int isSuccess, int fullChain, int maxChain, int scoreRank, boolean isLock, int theoryCount, int ext1) {
this.musicId = musicId;
this.level = level;
this.playCount = playCount;
this.scoreMax = scoreMax;
this.missCount = missCount;
this.maxComboCount = maxComboCount;
this.isFullCombo = isFullCombo;
this.isAllJustice = isAllJustice;
this.isSuccess = isSuccess > 0;
this.fullChain = fullChain;
this.maxChain = maxChain;
this.scoreRank = scoreRank;
this.isLock = isLock;
this.theoryCount = theoryCount;
this.ext1 = ext1;
}
}

View File

@@ -0,0 +1,19 @@
package icu.samnyan.aqua.sega.chusan.dto;
import icu.samnyan.aqua.sega.chusan.model.userdata.UserMusicDetail;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserMusicListItemForAncientChusan {
private int length;
private List<UserMusicDetailForAncientChusan> userMusicDetailList;
}

View File

@@ -1,6 +1,8 @@
package icu.samnyan.aqua.sega.chusan.handler.impl;
import com.fasterxml.jackson.core.JsonProcessingException;
import icu.samnyan.aqua.sega.chusan.dto.UserMusicDetailForAncientChusan;
import icu.samnyan.aqua.sega.chusan.dto.UserMusicListItemForAncientChusan;
import icu.samnyan.aqua.sega.chusan.handler.BaseHandler;
import icu.samnyan.aqua.sega.chusan.model.response.data.UserMusicListItem;
import icu.samnyan.aqua.sega.chusan.model.userdata.UserMusicDetail;
@@ -78,13 +80,51 @@ public class GetUserMusicHandler implements BaseHandler {
userMusicMap.remove(lastMusicId);
}
String version = (String) request.get("version");
Map<Integer, UserMusicListItemForAncientChusan> userMusicMapForAncientChusan = new LinkedHashMap<>();
boolean isAncient = false;
try {
if (Double.parseDouble(version) < 2.15){
userMusicMap.forEach((k, v) -> {
UserMusicListItemForAncientChusan list = new UserMusicListItemForAncientChusan();
list.setLength(v.getLength());
List<UserMusicDetailForAncientChusan> userMusicDetailForAncientChusanList = new ArrayList<>();
v.getUserMusicDetailList().forEach(userMusicDetail -> {
UserMusicDetailForAncientChusan userMusicDetailForAncientChusan = new UserMusicDetailForAncientChusan(
userMusicDetail.getMusicId(),
userMusicDetail.getLevel(),
userMusicDetail.getPlayCount(),
userMusicDetail.getScoreMax(),
userMusicDetail.getMissCount(),
userMusicDetail.getMaxComboCount(),
userMusicDetail.isFullCombo(),
userMusicDetail.isAllJustice(),
userMusicDetail.getIsSuccess(),
userMusicDetail.getFullChain(),
userMusicDetail.getMaxChain(),
userMusicDetail.getScoreRank(),
userMusicDetail.isLock(),
userMusicDetail.getTheoryCount(),
userMusicDetail.getExt1()
);
userMusicDetailForAncientChusanList.add(userMusicDetailForAncientChusan);
});
list.setUserMusicDetailList(userMusicDetailForAncientChusanList);
userMusicMapForAncientChusan.put(k, list);
});
isAncient = true;
}
} catch (Exception e) {
logger.error("Error when handling ancient version of chusan", e);
}
long nextIndex = currentIndex + dbPage.getNumberOfElements() - lastListSize;
Map<String, Object> resultMap = new LinkedHashMap<>();
resultMap.put("userId", userId);
resultMap.put("length", userMusicMap.size());
resultMap.put("nextIndex", dbPage.getNumberOfElements() < maxCount ? -1 : nextIndex);
resultMap.put("userMusicList", userMusicMap.values());
resultMap.put("userMusicList", isAncient ? userMusicMapForAncientChusan.values() : userMusicMap.values());
String json = mapper.write(resultMap);
logger.info("Response: " + json);

View File

@@ -3,6 +3,8 @@ package icu.samnyan.aqua.sega.chusan.model.userdata;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import icu.samnyan.aqua.sega.chusan.util.BooleanToIntegerDeserializer;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@@ -67,8 +69,9 @@ public class UserMusicDetail implements Serializable {
@JsonProperty("isAllJustice")
private boolean isAllJustice;
@JsonDeserialize(using = BooleanToIntegerDeserializer.class)
@JsonProperty("isSuccess")
private boolean isSuccess;
private int isSuccess;
private int fullChain;
@@ -87,7 +90,7 @@ public class UserMusicDetail implements Serializable {
user = userData;
}
public UserMusicDetail(int musicId, int level, int playCount, int scoreMax, int missCount, int maxComboCount, boolean isFullCombo, boolean isAllJustice, boolean isSuccess, int fullChain, int maxChain, int scoreRank, boolean isLock, int theoryCount, int ext1) {
public UserMusicDetail(int musicId, int level, int playCount, int scoreMax, int missCount, int maxComboCount, boolean isFullCombo, boolean isAllJustice, int isSuccess, int fullChain, int maxChain, int scoreRank, boolean isLock, int theoryCount, int ext1) {
this.musicId = musicId;
this.level = level;
this.playCount = playCount;

View File

@@ -0,0 +1,35 @@
package icu.samnyan.aqua.sega.chusan.util;
import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import java.io.IOException;
import java.util.Locale;
public class BooleanToIntegerDeserializer extends JsonDeserializer<Integer> {
@Override
public Integer deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
return switch (p.getCurrentToken()) {
case VALUE_STRING -> {
String value = p.getValueAsString();
if (value.toLowerCase(Locale.ROOT).equals("true")) {
yield 1;
} else if (value.toLowerCase(Locale.ROOT).equals("false")) {
yield 0;
} else {
try {
yield Integer.parseInt(value);
} catch (NumberFormatException e) {
throw new UnsupportedOperationException("Cannot deserialize to integer field");
}
}
}
case VALUE_NUMBER_INT -> p.getIntValue();
case VALUE_TRUE -> 1;
case VALUE_FALSE -> 0;
default -> throw new UnsupportedOperationException("Cannot deserialize to integer field");
};
}
}

View File

@@ -0,0 +1,2 @@
ALTER TABLE chusan_user_music_detail
MODIFY is_success INTEGER NOT NULL;

View File

@@ -0,0 +1,2 @@
ALTER TABLE chusan_user_music_detail
MODIFY is_success INTEGER NOT NULL;

View File

@@ -0,0 +1,32 @@
-- Step 1: Create a new table with the desired changes
CREATE TABLE temp_chusan_user_music_detail (
id INTEGER,
full_chain INTEGER NOT NULL,
is_all_justice BOOLEAN NOT NULL,
is_full_combo BOOLEAN NOT NULL,
is_lock BOOLEAN NOT NULL,
is_success INTEGER NOT NULL, -- Changed to INTEGER
level INTEGER NOT NULL,
max_chain INTEGER NOT NULL,
max_combo_count INTEGER NOT NULL,
miss_count INTEGER NOT NULL,
music_id INTEGER NOT NULL,
play_count INTEGER NOT NULL,
theory_count INTEGER,
ext1 INTEGER,
score_max INTEGER NOT NULL,
score_rank INTEGER NOT NULL,
user_id BIGINT REFERENCES chusan_user_data (id) ON DELETE CASCADE,
PRIMARY KEY (id),
CONSTRAINT chusan_user_music_detail_uq UNIQUE (level, music_id, user_id)
);
-- Step 2: Copy the data from the original table to the new table
INSERT INTO temp_chusan_user_music_detail
SELECT * FROM chusan_user_music_detail;
-- Step 3: Delete the original table
DROP TABLE chusan_user_music_detail;
-- Step 4: Rename the new table to the original table's name
ALTER TABLE temp_chusan_user_music_detail RENAME TO chusan_user_music_detail;

105
tools/merge.py Normal file
View File

@@ -0,0 +1,105 @@
#!/usr/bin/env python3
import argparse
import shlex
from subprocess import check_call, check_output
import openai
# Readline is used to fix left/right arrow keys in input(), do not remove as unused
import readline
my_base = 'master'
def gen_merge_msg(commits: str):
openai_example_commits = """
0d422120 - [API] Add a feature to retrieve user photos.
4a64895e - [api] Add maimai2 userPhoto API
e271cb45 - [API] Add a feature to retrieve user photos.
0bf54e66 - [API] Let web music list read from database
0913ef20 - [api] Add maimai2 userPhoto API
7cc9fb11 - Revert "New API to return user photos."
9c51b1e0 - Revert "Add API - Get user photos (mai)"
ba1f4589 - New API to return user photos.
e7848cb9 - Add API - Get user photos (mai)
"""
openai_example_response = "Add user photos feature for maimai2"
openai_prompt = ("I just merged the following branch, what commit message can best summarize the changes below? "
"Please make sure it is less than 100 characters long.\n\n")
complete = openai.ChatCompletion.create(
model="gpt-4",
messages=[
{"role": "system",
"content": "You are a senior software engineer helping with maintaining a game server repository."},
{"role": "user",
"content": f"{openai_prompt}{openai_example_commits}"},
{"role": "assistant",
"content": f'"{openai_example_response}"'},
{"role": "user",
"content": f"{openai_prompt}{commits}"},
]
)
m = complete.choices[0].message.content
return m.strip().strip('"')
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Helper for merging remote sibling repos')
parser.add_argument('url', type=str, help='Remote repo url')
parser.add_argument('-b', '--branch', type=str, default='master', help='Branch to merge')
parser.add_argument('-m', '--msg', type=str, default=None, help='Merge commit message')
args = parser.parse_args()
url = args.url
print(f'Merging {url}...')
# Fetch head branch
print()
print('Fetching head branch...')
check_call(f'git -c http.sslVerify=false fetch {url} {args.branch}', shell=True)
# Get the list of commits from the branch
commits = check_output(f'git log --pretty=format:"%H - %s" {my_base}..FETCH_HEAD', shell=True).decode()
commits = [c.split(' - ', 1) for c in commits.split('\n')]
commits = [c for c in commits if len(c) == 2]
commits = [f"{sha[:8]} - {msg}" for sha, msg in commits]
commits = '\n'.join(commits)
print("Commits:")
print(commits)
# Get the list of authors from the branch
authors = check_output(f'git log --pretty=format:"%an <%ae>" {my_base}..FETCH_HEAD', shell=True).decode()
authors = set(authors.split('\n'))
authors = "\n".join(f"Co-authored-by: {a}" for a in authors)
print("\nAuthors:")
print(authors)
# Create a merge commit message
owner = url.split('/')[-2]
repo = url.split('/')[-1].split('.')[0]
# If there isn't a merge message, create one by asking ChatGPT to summarize the commits
msg = args.msg
if not msg:
while True:
msg = input('\nPlease enter a merge message (or leave blank to generate one): ').strip()
if msg:
break
msg = gen_merge_msg(commits)
print(f'Generated message: {msg}')
if input('Is this okay? [y/N] ').lower() == 'y':
break
# Merge head branch
print('\nMerging fetch_head...')
check_call([*shlex.split('git merge FETCH_HEAD --no-ff --no-edit'),
'-m', f"Merge {owner}/{repo} : {msg}",
'-m', f"{commits}\n\n{authors}"])
# Push
assert input('\nPush? [Enter/N]') == ""
check_call('git push', shell=True)