sync
This commit is contained in:
1 parent
0b3282da5c
commit
24ccd628d1
43 files changed
+2099
-1490
No files matched your search
@@ -10,14 +10,12 @@ class LoginResult {
|
||||
final int errorId;
|
||||
final int userId;
|
||||
final String token;
|
||||
final String? cookies;
|
||||
|
||||
const LoginResult({
|
||||
required this.success,
|
||||
required this.errorId,
|
||||
required this.userId,
|
||||
required this.token,
|
||||
this.cookies,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -51,7 +49,7 @@ class ApiService {
|
||||
return sha256.convert(bytes).toString();
|
||||
}
|
||||
|
||||
String extractQRCode(String qrCodeToken) {
|
||||
static String extractQRCode(String qrCodeToken) {
|
||||
if (qrCodeToken.length > 64) {
|
||||
return qrCodeToken.substring(qrCodeToken.length - 64);
|
||||
}
|
||||
@@ -94,85 +92,14 @@ class ApiService {
|
||||
final userId = obj['userID'] as int? ?? -1;
|
||||
final token = obj['token'] as String? ?? '';
|
||||
|
||||
// ── Extract ALL cookies from response headers ──
|
||||
// On web, Set-Cookie is a forbidden response header (browser hides it).
|
||||
// On native (dart:io), it appears as lowercase 'set-cookie'.
|
||||
// The http package may join multiple Set-Cookie lines with ', '.
|
||||
String? cookies;
|
||||
|
||||
// ignore: avoid_print
|
||||
print('[login] ── Response headers (${response.headers.length} entries) ──');
|
||||
final headerKeys = response.headers.keys.toList();
|
||||
for (final key in headerKeys) {
|
||||
final value = response.headers[key]!;
|
||||
// ignore: avoid_print
|
||||
print('[login] "$key": "$value"');
|
||||
|
||||
// Match any header name that smells like a cookie
|
||||
final lower = key.toLowerCase();
|
||||
if (lower.contains('cookie') || lower.contains('set-cookie')) {
|
||||
cookies = value;
|
||||
// ignore: avoid_print
|
||||
print('[login] >>> Found cookie header: $key = $cookies');
|
||||
}
|
||||
}
|
||||
|
||||
// Also try direct lookup with various casings (just in case iteration misses)
|
||||
if (cookies == null) {
|
||||
for (final name in ['set-cookie', 'Set-Cookie', 'SET-COOKIE', 'cookie', 'Cookie', 'COOKIE']) {
|
||||
final v = response.headers[name];
|
||||
if (v != null && v.isNotEmpty) {
|
||||
cookies = v;
|
||||
// ignore: avoid_print
|
||||
print('[login] >>> Direct lookup "$name" = $cookies');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cookies == null) {
|
||||
// ignore: avoid_print
|
||||
print('[login] WARNING: No cookie/set-cookie header found in response.');
|
||||
// Also check if response body has session info
|
||||
if (obj.containsKey('jsessionId')) {
|
||||
cookies = 'JSESSIONID=${obj['jsessionId']}';
|
||||
// ignore: avoid_print
|
||||
print('[login] >>> Found jsessionId in response body: $cookies');
|
||||
} else if (obj.containsKey('sessionId')) {
|
||||
cookies = 'JSESSIONID=${obj['sessionId']}';
|
||||
// ignore: avoid_print
|
||||
print('[login] >>> Found sessionId in response body: $cookies');
|
||||
}
|
||||
}
|
||||
|
||||
// Parse out individual cookie name=value pairs for clean forwarding
|
||||
// (strip Path, HttpOnly, etc. attributes)
|
||||
String? cleanCookies;
|
||||
if (cookies != null) {
|
||||
final parts = <String>[];
|
||||
// Split on ', ' first (http package combines multiple Set-Cookie headers)
|
||||
// Then split on '\n' (some server responses use newlines)
|
||||
for (final chunk in cookies.split(RegExp(r', |\n'))) {
|
||||
final trimmed = chunk.trim();
|
||||
if (trimmed.isEmpty) continue;
|
||||
// Extract "NAME=VALUE" before the first ';'
|
||||
final semi = trimmed.indexOf(';');
|
||||
final nv = semi > 0 ? trimmed.substring(0, semi) : trimmed;
|
||||
if (nv.contains('=')) {
|
||||
parts.add(nv);
|
||||
}
|
||||
}
|
||||
cleanCookies = parts.join('; ');
|
||||
// ignore: avoid_print
|
||||
print('[login] Clean cookies to forward: $cleanCookies');
|
||||
}
|
||||
// Session cookies (JSESSIONID) are captured from the title server's
|
||||
// UserLoginApi response, not from the Aime server — see empurple.
|
||||
|
||||
return LoginResult(
|
||||
success: errorId == 0,
|
||||
errorId: errorId,
|
||||
userId: userId,
|
||||
token: token,
|
||||
cookies: cleanCookies,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
const _channel = MethodChannel('com.ichino/qr_scanner');
|
||||
const _channel = MethodChannel('dev.naominet.purarine/qr_scanner');
|
||||
|
||||
Future<String?> decodeQRFromBytes(Uint8List bytes) async {
|
||||
try {
|
||||
|
||||
@@ -93,46 +93,20 @@ class TitleApiService {
|
||||
return _aesEncrypt(compressed);
|
||||
}
|
||||
|
||||
/// Port of empurple `HttpResult.cookieHeader()`: only the `JSESSIONID`
|
||||
/// from the Set-Cookie response headers is used for subsequent requests.
|
||||
void _captureCookiesFromResponse(http.Response response, String apiName) {
|
||||
// ignore: avoid_print
|
||||
print('[$apiName] ── Response headers (${response.headers.length} entries) ──');
|
||||
final headerKeys = response.headers.keys.toList();
|
||||
String? rawCookies;
|
||||
for (final key in headerKeys) {
|
||||
if (!key.toLowerCase().contains('set-cookie')) continue;
|
||||
final value = response.headers[key]!;
|
||||
// ignore: avoid_print
|
||||
print('[$apiName] "$key": "$value"');
|
||||
final lower = key.toLowerCase();
|
||||
if (lower.contains('set-cookie') || lower.contains('cookie')) {
|
||||
rawCookies = value;
|
||||
final match =
|
||||
RegExp(r'JSESSIONID=([^;,]+)', caseSensitive: false).firstMatch(value);
|
||||
if (match != null) {
|
||||
_cookies = 'JSESSIONID=${match.group(1)!.trim()}';
|
||||
// ignore: avoid_print
|
||||
print('[$apiName] >>> Found cookie header: $key = $rawCookies');
|
||||
}
|
||||
}
|
||||
|
||||
if (rawCookies != null) {
|
||||
// Parse JSESSIONID from the cookie value
|
||||
// The jsessionid cookie is the session key from WaCa
|
||||
final parts = <String>[];
|
||||
for (final chunk in rawCookies.split(RegExp(r', |\n'))) {
|
||||
final trimmed = chunk.trim();
|
||||
if (trimmed.isEmpty) continue;
|
||||
final semi = trimmed.indexOf(';');
|
||||
final nv = semi > 0 ? trimmed.substring(0, semi) : trimmed;
|
||||
if (nv.contains('=')) {
|
||||
parts.add(nv);
|
||||
final eq = nv.indexOf('=');
|
||||
final name = nv.substring(0, eq);
|
||||
final value = nv.substring(eq + 1);
|
||||
// ignore: avoid_print
|
||||
print('[$apiName] >>> Cookie pair: $name = $value');
|
||||
}
|
||||
}
|
||||
final cleanCookies = parts.join('; ');
|
||||
if (cleanCookies.isNotEmpty) {
|
||||
_cookies = cleanCookies;
|
||||
// ignore: avoid_print
|
||||
print('[$apiName] >>> Stored cookies for subsequent requests: $_cookies');
|
||||
print('[$apiName] >>> Captured session cookie: $_cookies');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,14 +158,15 @@ class TitleApiService {
|
||||
// ignore: avoid_print
|
||||
print(packetStr);
|
||||
|
||||
final uaSuffix = userId != 0 ? '$userId' : _config.clientId;
|
||||
final headers = <String, String>{
|
||||
'User-Agent': '$hash#$userId',
|
||||
'User-Agent': '$hash#$uaSuffix',
|
||||
'Content-Type': 'application/json',
|
||||
'Mai-Encoding': _config.apiVersion,
|
||||
'Accept-Encoding': '',
|
||||
'Charset': 'UTF-8',
|
||||
'Content-Encoding': 'deflate',
|
||||
'Host': 'maimai-gm.wahlap.com:42081',
|
||||
'number': '0',
|
||||
};
|
||||
final cookies = _cookies;
|
||||
if (cookies != null) {
|
||||
@@ -307,7 +282,7 @@ class TitleApiService {
|
||||
'Accept-Encoding': '',
|
||||
'Charset': 'UTF-8',
|
||||
'Content-Encoding': 'deflate',
|
||||
'Host': 'maimai.wahlap.com:42081',
|
||||
'number': '0',
|
||||
};
|
||||
final previewCookies = _cookies;
|
||||
if (previewCookies != null) {
|
||||
@@ -434,6 +409,17 @@ class TitleApiService {
|
||||
|
||||
// ---- UserLogout ----
|
||||
|
||||
/// Port of empurple/eaquira behavior: the server may omit `returnCode`
|
||||
/// for fire-and-forget APIs. Only a present-but-not-1 code is an error.
|
||||
void _checkReturnCode(Map<String, dynamic> json, String apiName) {
|
||||
final returnCode = json['returnCode'];
|
||||
if (returnCode == null) return;
|
||||
final code = (returnCode as num).toInt();
|
||||
if (code != 1) {
|
||||
throw TitleApiException('$apiName returnCode=$code');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> userLogout({
|
||||
required int userId,
|
||||
required int loginDateTime,
|
||||
@@ -451,16 +437,10 @@ class TitleApiService {
|
||||
};
|
||||
|
||||
final json = await _callApi(apiName, packet, userId);
|
||||
final returnCode = json['returnCode'] as int? ?? -1;
|
||||
if (returnCode != 1) {
|
||||
throw TitleApiException('UserLogoutApi returnCode=$returnCode');
|
||||
}
|
||||
_checkReturnCode(json, apiName);
|
||||
}
|
||||
|
||||
// ---- UpsertUserChargeLog (使用功能票) ----
|
||||
// Reference: KanadeBot SendTicketCommand / PayloadBuilder.generateTicketRequest
|
||||
|
||||
static const _ticketPriceMap = {1: 1, 2: 6, 3: 2, 4: 2, 5: 2, 6: 0};
|
||||
|
||||
Future<void> upsertUserChargeLog({
|
||||
required int userId,
|
||||
@@ -482,7 +462,7 @@ class TitleApiService {
|
||||
'userId': userId,
|
||||
'userChargelog': {
|
||||
'chargeId': ticketId,
|
||||
'price': _ticketPriceMap[ticketId] ?? 0,
|
||||
'price': 0,
|
||||
'purchaseDate': purchaseDateStr,
|
||||
'playCount': playCount,
|
||||
'playerRating': playerRating,
|
||||
@@ -500,10 +480,7 @@ class TitleApiService {
|
||||
};
|
||||
|
||||
final json = await _callApi(apiName, packet, userId);
|
||||
final returnCode = json['returnCode'] as int? ?? -1;
|
||||
if (returnCode != 1) {
|
||||
throw TitleApiException('UpsertUserChargelogApi returnCode=$returnCode');
|
||||
}
|
||||
_checkReturnCode(json, apiName);
|
||||
}
|
||||
|
||||
String _formatDateTime(DateTime dt) {
|
||||
@@ -523,10 +500,7 @@ class TitleApiService {
|
||||
'userId': userId,
|
||||
'userPlaylog': userPlaylog,
|
||||
}, userId);
|
||||
final returnCode = json['returnCode'] as int? ?? -1;
|
||||
if (returnCode != 1) {
|
||||
throw TitleApiException('UploadUserPlaylogApi returnCode=$returnCode');
|
||||
}
|
||||
_checkReturnCode(json, apiName);
|
||||
}
|
||||
|
||||
/// Fetch all user data needed to build an UpsertUserAll payload.
|
||||
@@ -560,10 +534,7 @@ class TitleApiService {
|
||||
) async {
|
||||
const apiName = 'UpsertUserAllApi';
|
||||
final json = await _callApi(apiName, packet, userId);
|
||||
final returnCode = json['returnCode'] as int? ?? -1;
|
||||
if (returnCode != 1) {
|
||||
throw TitleApiException('UpsertUserAllApi returnCode=$returnCode');
|
||||
}
|
||||
_checkReturnCode(json, apiName);
|
||||
}
|
||||
|
||||
/// Fetch user character list.
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
import '../config/title_server_config.dart';
|
||||
import 'title_api_service.dart';
|
||||
|
||||
/// Dart port of `eaquira/src/sdgb/payload.py::UserAll_payload` and
|
||||
/// `eaquira/action/UnlockMusic.py::music_user_all_patcher`.
|
||||
///
|
||||
/// Builds the UpsertUserAllApi request packet used by the
|
||||
/// UpsertMusic (上传成绩) / UnlockMusic (解锁歌曲) workflows.
|
||||
class UserAllPayloadBuilder {
|
||||
final TitleServerConfig config;
|
||||
|
||||
const UserAllPayloadBuilder(this.config);
|
||||
|
||||
static DateTime _shanghaiNow() =>
|
||||
DateTime.now().toUtc().add(const Duration(hours: 8));
|
||||
|
||||
static String _two(int v) => v.toString().padLeft(2, '0');
|
||||
|
||||
static String _formatPlayDate(DateTime dt) =>
|
||||
'${dt.year}-${_two(dt.month)}-${_two(dt.day)}';
|
||||
|
||||
static String _formatPlayDateTime(DateTime dt) =>
|
||||
'${dt.year}-${_two(dt.month)}-${_two(dt.day)} '
|
||||
'${_two(dt.hour)}:${_two(dt.minute)}:${_two(dt.second)}.0';
|
||||
|
||||
static int _toInt(dynamic v) {
|
||||
if (v is int) return v;
|
||||
if (v is num) return v.toInt();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// "1.56.00" -> 1056000 (Lionheart convertVersionNumber).
|
||||
static int _convertVersionNumber(String version) {
|
||||
final parts = version.split('.');
|
||||
if (parts.length != 3) return 1053000;
|
||||
final major = int.tryParse(parts[0]);
|
||||
final minor = int.tryParse(parts[1]);
|
||||
final patch = int.tryParse(parts[2]);
|
||||
if (major == null || minor == null || patch == null) return 1053000;
|
||||
return major * 1000000 + minor * 1000 + patch;
|
||||
}
|
||||
|
||||
/// Build the UpsertUserAllApi packet.
|
||||
///
|
||||
/// [generalUserInfo] is the result of `TitleApiService.fetchUserAllData`,
|
||||
/// keyed by the raw API names.
|
||||
Map<String, dynamic> build({
|
||||
required int userId,
|
||||
required int loginId,
|
||||
required int loginDateTime,
|
||||
required Map<String, dynamic> musicData,
|
||||
required Map<String, Map<String, dynamic>> generalUserInfo,
|
||||
}) {
|
||||
final userDataResp = generalUserInfo['GetUserDataApi'] ?? const {};
|
||||
final userExtendResp = generalUserInfo['GetUserExtendApi'] ?? const {};
|
||||
final userOptionResp = generalUserInfo['GetUserOptionApi'] ?? const {};
|
||||
final userRatingResp = generalUserInfo['GetUserRatingApi'] ?? const {};
|
||||
final userChargeResp = generalUserInfo['GetUserChargeApi'] ?? const {};
|
||||
final userActivityResp = generalUserInfo['GetUserActivityApi'] ?? const {};
|
||||
final userMissionResp = generalUserInfo['GetUserMissionDataApi'] ?? const {};
|
||||
|
||||
final userData =
|
||||
userDataResp['userData'] as Map<String, dynamic>? ?? const {};
|
||||
final playerRating = _toInt(userData['playerRating']);
|
||||
|
||||
final charaSlot = (userData['charaSlot'] as List<dynamic>? ?? const [])
|
||||
.map((e) => _toInt(e))
|
||||
.toList();
|
||||
final paddedCharaSlot = List<int>.generate(
|
||||
5,
|
||||
(i) => i < charaSlot.length ? charaSlot[i] : 0,
|
||||
);
|
||||
|
||||
final now = _shanghaiNow();
|
||||
final playDate = _formatPlayDate(now);
|
||||
final playDateTime = _formatPlayDateTime(now);
|
||||
final loginDate =
|
||||
_formatPlayDateTime(DateTime.fromMillisecondsSinceEpoch(loginDateTime * 1000).toUtc().add(const Duration(hours: 8)));
|
||||
|
||||
// Lionheart: GetUserDataApi 返回的部分字段不能出现在 UpsertUserAll 的
|
||||
// userData 中,否则服务器会静默丢弃整包处理。
|
||||
final newUserData = Map<String, dynamic>.from(userData)
|
||||
..remove('friendCode')
|
||||
..remove('nameplateId')
|
||||
..remove('trophyId')
|
||||
..remove('cmLastEmoneyBrand')
|
||||
..remove('cmLastEmoneyCredit');
|
||||
newUserData
|
||||
..['accessCode'] = ''
|
||||
..['isNetMember'] = 1
|
||||
..['playCount'] = _toInt(userData['playCount']) + 1
|
||||
..['currentPlayCount'] = _toInt(userData['currentPlayCount']) + 1
|
||||
..['lastGameId'] = 'SDGB'
|
||||
..['lastLoginDate'] = loginDate
|
||||
..['lastPlayDate'] = playDateTime
|
||||
..['lastPlayCredit'] = 1
|
||||
..['lastPlayMode'] = 0
|
||||
..['lastPlaceId'] = config.placeId
|
||||
..['lastPlaceName'] = config.placeName
|
||||
..['lastAllNetId'] = 0
|
||||
..['lastRegionId'] = config.regionId
|
||||
..['lastRegionName'] = config.regionName
|
||||
..['lastClientId'] = config.clientId
|
||||
..['lastCountryCode'] = 'CHN'
|
||||
..['banState'] = userDataResp['banState']
|
||||
..['dateTime'] = loginDateTime;
|
||||
|
||||
// Lionheart: userOption 需去掉 tempoVolume。
|
||||
final userOption =
|
||||
userOptionResp['userOption'] as Map<String, dynamic>? ?? const {};
|
||||
final newUserOption = Map<String, dynamic>.from(userOption)
|
||||
..remove('tempoVolume');
|
||||
|
||||
// Lionheart: userRating.udemae 的大写重复字段需去掉。
|
||||
final userRating =
|
||||
userRatingResp['userRating'] as Map<String, dynamic>? ?? const {};
|
||||
final udemae = userRating['udemae'] as Map<String, dynamic>?;
|
||||
if (udemae != null) {
|
||||
for (final key in [
|
||||
'MaxLoseNum',
|
||||
'NpcLoseNum',
|
||||
'NpcMaxLoseNum',
|
||||
'NpcMaxWinNum',
|
||||
'NpcTotalLoseNum',
|
||||
'NpcTotalWinNum',
|
||||
'NpcWinNum',
|
||||
]) {
|
||||
udemae.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Lionheart: userChargeList 只保留 4 个字段 (无 extNum1)。
|
||||
final userChargeList = [
|
||||
for (final c in (userChargeResp['userChargeList'] as List<dynamic>? ??
|
||||
const []))
|
||||
if (c is Map<String, dynamic>)
|
||||
{
|
||||
'chargeId': c['chargeId'],
|
||||
'stock': c['stock'],
|
||||
'purchaseDate': c['purchaseDate'],
|
||||
'validDate': c['validDate'],
|
||||
},
|
||||
];
|
||||
|
||||
final missionList =
|
||||
userMissionResp['userMissionDataList'] as List<dynamic>? ?? const [];
|
||||
final missions = <Map<String, dynamic>>[];
|
||||
for (final raw in missionList.take(6)) {
|
||||
final m = raw as Map<String, dynamic>;
|
||||
missions.add({
|
||||
'type': m['type'],
|
||||
'difficulty': m['difficulty'],
|
||||
'targetGenreId': m['targetGenreId'],
|
||||
'targetGenreTableId': m['targetGenreTableId'],
|
||||
'conditionGenreId': m['conditionGenreId'],
|
||||
'conditionGenreTableId': m['conditionGenreTableId'],
|
||||
'clearFlag': m['clearFlag'],
|
||||
});
|
||||
}
|
||||
|
||||
final weekly =
|
||||
userMissionResp['userWeeklyData'] as Map<String, dynamic>? ?? const {};
|
||||
|
||||
final achievement = _toInt(musicData['achievement']);
|
||||
final gradeRating = _toInt(userData['gradeRating']);
|
||||
final playlog = <String, dynamic>{
|
||||
'userId': 0,
|
||||
'orderId': 0,
|
||||
'playlogId': loginId,
|
||||
'version': _convertVersionNumber(
|
||||
userData['lastRomVersion'] as String? ?? '',
|
||||
),
|
||||
'placeId': config.placeId,
|
||||
'placeName': config.placeName,
|
||||
'loginDate': loginDateTime,
|
||||
'playDate': playDate,
|
||||
'userPlayDate': playDateTime,
|
||||
'type': 0,
|
||||
'musicId': musicData['musicId'],
|
||||
'level': musicData['level'],
|
||||
'trackNo': 1,
|
||||
'vsMode': 0,
|
||||
'vsUserName': '',
|
||||
'vsStatus': 0,
|
||||
'vsUserRating': 0,
|
||||
'vsUserAchievement': 0,
|
||||
'vsUserGradeRank': 0,
|
||||
'vsRank': 0,
|
||||
'playerNum': 1,
|
||||
'playedUserId1': 0,
|
||||
'playedUserName1': '',
|
||||
'playedMusicLevel1': 0,
|
||||
'playedUserId2': 0,
|
||||
'playedUserName2': '',
|
||||
'playedMusicLevel2': 0,
|
||||
'playedUserId3': 0,
|
||||
'playedUserName3': '',
|
||||
'playedMusicLevel3': 0,
|
||||
'achievement': achievement,
|
||||
'deluxscore': musicData['deluxscoreMax'],
|
||||
'scoreRank': musicData['scoreRank'],
|
||||
'maxCombo': 0,
|
||||
'totalCombo': 1,
|
||||
'maxSync': 1,
|
||||
'totalSync': 0,
|
||||
'tapCriticalPerfect': 0,
|
||||
'tapPerfect': 0,
|
||||
'tapGreat': 0,
|
||||
'tapGood': 0,
|
||||
'tapMiss': 1,
|
||||
'holdCriticalPerfect': 0,
|
||||
'holdPerfect': 0,
|
||||
'holdGreat': 0,
|
||||
'holdGood': 0,
|
||||
'holdMiss': 0,
|
||||
'slideCriticalPerfect': 0,
|
||||
'slidePerfect': 0,
|
||||
'slideGreat': 0,
|
||||
'slideGood': 0,
|
||||
'slideMiss': 0,
|
||||
'touchCriticalPerfect': 0,
|
||||
'touchPerfect': 0,
|
||||
'touchGreat': 0,
|
||||
'touchGood': 0,
|
||||
'touchMiss': 0,
|
||||
'breakCriticalPerfect': 0,
|
||||
'breakPerfect': 0,
|
||||
'breakGreat': 0,
|
||||
'breakGood': 0,
|
||||
'breakMiss': 0,
|
||||
'isTap': true,
|
||||
'isHold': false,
|
||||
'isSlide': false,
|
||||
'isTouch': false,
|
||||
'isBreak': false,
|
||||
'isCriticalDisp': false,
|
||||
'isFastLateDisp': true,
|
||||
'fastCount': 0,
|
||||
'lateCount': 0,
|
||||
'isAchieveNewRecord': false,
|
||||
'isDeluxscoreNewRecord': false,
|
||||
'comboStatus': musicData['comboStatus'],
|
||||
'syncStatus': musicData['syncStatus'],
|
||||
'isClear': achievement >= 500000,
|
||||
'beforeRating': playerRating,
|
||||
'afterRating': playerRating,
|
||||
'beforeGrade': gradeRating,
|
||||
'afterGrade': gradeRating,
|
||||
'afterGradeRank': _toInt(userData['gradeRank']),
|
||||
'beforeDeluxRating': playerRating,
|
||||
'afterDeluxRating': playerRating,
|
||||
'isPlayTutorial': false,
|
||||
'isEventMode': false,
|
||||
'isFreedomMode': false,
|
||||
'playMode': 0,
|
||||
'isNewFree': false,
|
||||
'trialPlayAchievement': -1,
|
||||
'extNum1': 0,
|
||||
'extNum2': 0,
|
||||
'extNum4': 0,
|
||||
'extBool1': false,
|
||||
'extBool2': false,
|
||||
};
|
||||
for (var i = 0; i < 5; i++) {
|
||||
playlog['characterId${i + 1}'] = paddedCharaSlot[i];
|
||||
playlog['characterLevel${i + 1}'] = 1;
|
||||
playlog['characterAwakening${i + 1}'] = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
'userId': userId,
|
||||
'playlogId': loginId,
|
||||
'isEventMode': false,
|
||||
'isFreePlay': false,
|
||||
'loginDateTime': loginDateTime,
|
||||
'userPlaylogList': [playlog],
|
||||
'upsertUserAll': {
|
||||
'userData': [newUserData],
|
||||
'userExtend': [userExtendResp['userExtend']],
|
||||
'userOption': [newUserOption],
|
||||
'userCharacterList': [],
|
||||
'userGhost': [],
|
||||
'userMapList': [],
|
||||
'userLoginBonusList': [],
|
||||
'userRatingList': [userRating],
|
||||
'userItemList': [],
|
||||
'userMusicDetailList': [musicData],
|
||||
'userCourseList': [],
|
||||
'userFriendSeasonRankingList': [],
|
||||
'userChargeList': userChargeList,
|
||||
'userFavoriteList': [],
|
||||
'userActivityList': [userActivityResp['userActivity']],
|
||||
'userMissionDataList': missions,
|
||||
'userWeeklyData': {
|
||||
'lastLoginWeek': weekly['lastLoginWeek'],
|
||||
'beforeLoginWeek': weekly['beforeLoginWeek'],
|
||||
'friendBonusFlag': weekly['friendBonusFlag'],
|
||||
},
|
||||
'userGamePlaylogList': [
|
||||
{
|
||||
'playlogId': loginId,
|
||||
'version': userData['lastRomVersion'],
|
||||
'playDate': playDateTime,
|
||||
'playMode': 0,
|
||||
'useTicketId': -1,
|
||||
'playCredit': 1,
|
||||
'playTrack': 1,
|
||||
'clientId': config.clientId,
|
||||
'isPlayTutorial': false,
|
||||
'isEventMode': false,
|
||||
'isNewFree': false,
|
||||
'playCount': 0,
|
||||
'playSpecial': TitleApiService.calcRandom(),
|
||||
'playOtherUserId': 0,
|
||||
},
|
||||
],
|
||||
'user2pPlaylog': {
|
||||
'userId1': 0,
|
||||
'userId2': 0,
|
||||
'userName1': '',
|
||||
'userName2': '',
|
||||
'regionId': 0,
|
||||
'placeId': 0,
|
||||
'user2pPlaylogDetailList': [],
|
||||
},
|
||||
'userIntimateList': [],
|
||||
'userShopItemStockList': [],
|
||||
'userGetPointList': [],
|
||||
'userTradeItemList': [],
|
||||
'userFavoritemusicList': [],
|
||||
'userKaleidxScopeList': [],
|
||||
'isNewCharacterList': '',
|
||||
'isNewMapList': '',
|
||||
'isNewLoginBonusList': '',
|
||||
'isNewItemList': '',
|
||||
'isNewMusicDetailList': '0',
|
||||
'isNewCourseList': '',
|
||||
'isNewFavoriteList': '',
|
||||
'isNewFriendSeasonRankingList': '',
|
||||
'isNewUserIntimateList': '',
|
||||
'isNewFavoritemusicList': '',
|
||||
'isNewKaleidxScopeList': '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// Generic item patch: replace `userItemList` / `isNewItemList` in the packet.
|
||||
/// Used by UnlockMusic (itemKind 5/6/7) and 收藏品获取 (itemKind 1/2/3/10/11/12).
|
||||
void applyItemListPatch(
|
||||
Map<String, dynamic> packet, {
|
||||
required List<Map<String, dynamic>> items,
|
||||
}) {
|
||||
final upsert = packet['upsertUserAll'] as Map<String, dynamic>;
|
||||
upsert['userItemList'] = items;
|
||||
upsert['isNewItemList'] = List.filled(items.length, '1').join();
|
||||
}
|
||||
|
||||
/// Port of `music_user_all_patcher`: append unlock items
|
||||
/// (itemKind 5=music, 6=master, 7=remaster) to the packet.
|
||||
void applyMusicUnlockPatch(
|
||||
Map<String, dynamic> packet, {
|
||||
required Map<String, dynamic> musicData,
|
||||
required int musicId,
|
||||
bool unlockMusic = false,
|
||||
bool unlockMaster = false,
|
||||
bool unlockRemaster = false,
|
||||
}) {
|
||||
applyItemListPatch(packet, items: [
|
||||
if (unlockMusic)
|
||||
{'itemKind': 5, 'itemId': musicId, 'stock': 1, 'isValid': true},
|
||||
if (unlockMaster)
|
||||
{'itemKind': 6, 'itemId': musicId, 'stock': 1, 'isValid': true},
|
||||
if (unlockRemaster)
|
||||
{'itemKind': 7, 'itemId': musicId, 'stock': 1, 'isValid': true},
|
||||
]);
|
||||
|
||||
final upsert = packet['upsertUserAll'] as Map<String, dynamic>;
|
||||
upsert['userMusicDetailList'] = [musicData];
|
||||
upsert['isNewMusicDetailList'] = '1';
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user