较新较热

This commit is contained in:
Cookiues committed 2026-06-09 17:21:11 +08:00
1 parent 2bf8030c37
commit cfff0b8b06
3 files changed
+303 -88

No files matched your search

+10 -9
View File
@@ -4,7 +4,7 @@ class AppStrings {
// Tab // Tab
static const tabHome = '主页'; static const tabHome = '主页';
static const tabTickets = '票据'; static const tabTickets = '票据';
static const tabTransfer = '传包'; static const tabTransfer = '发包';
static const tabSettings = '设置'; static const tabSettings = '设置';
static const tabAbout = '关于'; static const tabAbout = '关于';
@@ -118,7 +118,7 @@ class AppStrings {
static String ticketCooldownNotice(int remaining) => static String ticketCooldownNotice(int remaining) =>
'登录后需冷却 $ticketCooldownSeconds 秒,剩余 $remaining 秒后可使用功能票。'; '登录后需冷却 $ticketCooldownSeconds 秒,剩余 $remaining 秒后可使用功能票。';
static String packetCooldownNotice(int remaining) => static String packetCooldownNotice(int remaining) =>
'登录后需冷却 $ticketCooldownSeconds 秒,剩余 $remaining 秒后可使用传包。'; '登录后需冷却 $ticketCooldownSeconds 秒,剩余 $remaining 秒后可发包。';
// Settings // Settings
static const titleServerSettings = 'Title Server 设置'; static const titleServerSettings = 'Title Server 设置';
@@ -163,12 +163,12 @@ class AppStrings {
static const importFailed = '导入失败:剪贴板内容无效'; static const importFailed = '导入失败:剪贴板内容无效';
static const noConfigToExport = '没有可导出的配置'; static const noConfigToExport = '没有可导出的配置';
// Transfer Package (传包) // Transfer Package (发包)
static const transferTitle = '传包 (UpsertUserAll)'; static const transferTitle = '发包 (UpsertUserAll)';
static const transferDesc = '拉取用户数据后,可修改角色槽位、锁定槽位和角色等级,然后传包回服务器。'; static const transferDesc = '拉取用户数据后,可修改角色槽位、锁定槽位和角色等级,然后发包回服务器。';
static const transferFetchData = '拉取数据'; static const transferFetchData = '拉取数据';
static const transferFetching = '拉取中...'; static const transferFetching = '拉取中...';
static const transferNotLoggedIn = '尚未登录游戏服务器,无法传包。'; static const transferNotLoggedIn = '尚未登录游戏服务器,无法发包。';
static const transferCharaSlot = '角色槽位 (charaSlot)'; static const transferCharaSlot = '角色槽位 (charaSlot)';
static const transferCharaLockSlot = '锁定槽位 (charaLockSlot)'; static const transferCharaLockSlot = '锁定槽位 (charaLockSlot)';
static const transferCharacterLevels = '角色等级 (userCharacterList)'; static const transferCharacterLevels = '角色等级 (userCharacterList)';
@@ -176,10 +176,11 @@ class AppStrings {
static const transferCharacterId = '角色ID'; static const transferCharacterId = '角色ID';
static const transferLevel = '等级'; static const transferLevel = '等级';
static const transferAwakening = '觉醒'; static const transferAwakening = '觉醒';
static const transferSend = '发送传包'; static const transferSend = '发包';
static const transferSending = '发送中...'; static const transferSending = '发送中...';
static const transferSuccess = '传包完成'; static const transferSuccess = '发包完成';
static const transferFailed = '传包失败'; static const transferFailed = '发包失败';
static const transferUploadingPlaylog = '上传游戏记录...';
static const transferSlotLabel = '槽位'; static const transferSlotLabel = '槽位';
static const transferNoData = '⚠️ 未拉取到用户数据,请先点「拉取数据」按钮获取。'; static const transferNoData = '⚠️ 未拉取到用户数据,请先点「拉取数据」按钮获取。';
static const transferDataLoaded = '数据已就绪,可在下方编辑后发送。'; static const transferDataLoaded = '数据已就绪,可在下方编辑后发送。';
+276 -78
View File
@@ -9,7 +9,7 @@ import '../models/upsert_user_all.dart';
import '../services/title_api_service.dart'; import '../services/title_api_service.dart';
import 'transfer_advanced_page.dart'; import 'transfer_advanced_page.dart';
enum TransferStep { idle, fetching, ready, sending, done, failed } enum TransferStep { idle, fetching, ready, uploadingPlaylog, sending, done, failed }
class TransferPackagePage extends StatefulWidget { class TransferPackagePage extends StatefulWidget {
final int userId; final int userId;
@@ -216,14 +216,24 @@ class _TransferPackagePageState extends State<TransferPackagePage> {
return; return;
} }
// Step 1: Upload fake playlog
setState(() { setState(() {
_step = TransferStep.sending; _step = TransferStep.uploadingPlaylog;
_error = null; _error = null;
}); });
try { try {
final config = TitleServerConfigHolder().config!; final config = TitleServerConfigHolder().config!;
final service = TitleApiService(config, cookies: widget.cookies); final service = TitleApiService(config, cookies: widget.cookies);
final playlog = _buildFakePlaylog();
await service.uploadUserPlaylog(playlog, widget.userId);
if (!mounted) return;
// Step 2: UpsertUserAll
setState(() => _step = TransferStep.sending);
final payload = _buildPayload(); final payload = _buildPayload();
await service.upsertUserAll(payload.toJson(), widget.userId); await service.upsertUserAll(payload.toJson(), widget.userId);
@@ -244,6 +254,217 @@ class _TransferPackagePageState extends State<TransferPackagePage> {
} }
} }
// ─── Fake game log (UploadUserPlaylog) ─────────────────────────────────
/// Look up level + awakening for a character ID from the editable list.
(int level, int awakening) _charLevel(int charId) {
for (final e in _charEntries) {
if (e.characterId == charId) return (e.level, e.awakening);
}
return (1, 0);
}
/// Build a playlog entry with character levels from _charEntries.
Map<String, dynamic> _makePlaylogEntry({
required List<int> charaSlot,
required int musicId,
required int level,
required int trackNo,
required int achievement,
required int deluxscore,
required int scoreRank,
required int comboStatus,
required int maxCombo,
required int totalCombo,
required int maxSync,
required int totalSync,
required bool isClear,
required bool isAchieveNewRecord,
required bool isDeluxscoreNewRecord,
required int extNum4,
}) {
final rating = _apiRating;
final cfg = TitleServerConfigHolder().config;
final loginDt = widget.loginDateTime ??
DateTime.now().millisecondsSinceEpoch ~/ 1000;
final c1 = _charLevel(charaSlot.isNotEmpty ? charaSlot[0] : 0);
final c2 = _charLevel(charaSlot.length > 1 ? charaSlot[1] : 0);
final c3 = _charLevel(charaSlot.length > 2 ? charaSlot[2] : 0);
final c4 = _charLevel(charaSlot.length > 3 ? charaSlot[3] : 0);
final c5 = _charLevel(charaSlot.length > 4 ? charaSlot[4] : 0);
return {
'userId': 0,
'orderId': 0,
'playlogId': widget.playlogId ?? 0,
'version': 1053000,
'placeId': cfg?.placeId ?? 0,
'placeName': cfg?.placeName ?? '',
'loginDate': loginDt,
'playDate': _nowStr().split(' ')[0],
'userPlayDate': _nowStr(),
'type': 0,
'musicId': musicId,
'level': level,
'trackNo': trackNo,
'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,
'characterId1': charaSlot.isNotEmpty ? charaSlot[0] : 0,
'characterLevel1': c1.$1,
'characterAwakening1': c1.$2,
'characterId2': charaSlot.length > 1 ? charaSlot[1] : 0,
'characterLevel2': c2.$1,
'characterAwakening2': c2.$2,
'characterId3': charaSlot.length > 2 ? charaSlot[2] : 0,
'characterLevel3': c3.$1,
'characterAwakening3': c3.$2,
'characterId4': charaSlot.length > 3 ? charaSlot[3] : 0,
'characterLevel4': c4.$1,
'characterAwakening4': c4.$2,
'characterId5': charaSlot.length > 4 ? charaSlot[4] : 0,
'characterLevel5': c5.$1,
'characterAwakening5': c5.$2,
'achievement': achievement,
'deluxscore': deluxscore,
'scoreRank': scoreRank,
'maxCombo': maxCombo,
'totalCombo': totalCombo,
'maxSync': maxSync,
'totalSync': totalSync,
'tapCriticalPerfect': 101,
'tapPerfect': 0,
'tapGreat': 0,
'tapGood': 0,
'tapMiss': 0,
'holdCriticalPerfect': 9,
'holdPerfect': 0,
'holdGreat': 0,
'holdGood': 0,
'holdMiss': 0,
'slideCriticalPerfect': 4,
'slidePerfect': 0,
'slideGreat': 0,
'slideGood': 0,
'slideMiss': 0,
'touchCriticalPerfect': 0,
'touchPerfect': 0,
'touchGreat': 0,
'touchGood': 0,
'touchMiss': 0,
'breakCriticalPerfect': 1,
'breakPerfect': 0,
'breakGreat': 0,
'breakGood': 0,
'breakMiss': 0,
'isTap': true,
'isHold': true,
'isSlide': true,
'isTouch': false,
'isBreak': true,
'isCriticalDisp': true,
'isFastLateDisp': true,
'fastCount': 0,
'lateCount': 0,
'isAchieveNewRecord': isAchieveNewRecord,
'isDeluxscoreNewRecord': isDeluxscoreNewRecord,
'comboStatus': comboStatus,
'syncStatus': 0,
'isClear': isClear,
'beforeRating': rating,
'afterRating': rating,
'beforeGrade': 0,
'afterGrade': 0,
'afterGradeRank': 0,
'beforeDeluxRating': rating,
'afterDeluxRating': rating,
'isPlayTutorial': false,
'isEventMode': false,
'isFreedomMode': false,
'playMode': 0,
'isNewFree': false,
'trialPlayAchievement': -1,
'extNum1': 0,
'extNum2': 0,
'extNum4': extNum4,
'extBool1': false,
'extBool2': false,
};
}
/// Build musicDetail entry matching a playlog.
Map<String, dynamic> _makeMusicDetail({
required int musicId,
required int level,
required int playCount,
required int achievement,
required int comboStatus,
required int syncStatus,
required int deluxscoreMax,
required int scoreRank,
}) {
return {
'musicId': musicId,
'level': level,
'playCount': playCount,
'achievement': achievement,
'comboStatus': comboStatus,
'syncStatus': syncStatus,
'deluxscoreMax': deluxscoreMax,
'scoreRank': scoreRank,
'extNum1': 0,
};
}
int get _apiRating {
final data = _apiData;
if (data == null) return 0;
final userDataJson = data['GetUserDataApi']!;
final ud =
(userDataJson['userData'] as Map<String, dynamic>?) ?? const {};
return ud['playerRating'] as int? ?? 0;
}
Map<String, dynamic> _buildFakePlaylog() {
final charaSlot =
_slotCtrls.map((c) => int.tryParse(c.text) ?? 0).toList();
return _makePlaylogEntry(
charaSlot: charaSlot,
musicId: 834,
level: 4,
trackNo: 1,
achievement: 1000000,
deluxscore: 0,
scoreRank: 13,
comboStatus: 3,
maxCombo: 0,
totalCombo: 128,
maxSync: 0,
totalSync: 0,
isClear: true,
isAchieveNewRecord: true,
isDeluxscoreNewRecord: true,
extNum4: 101,
);
}
// ─── Build UpsertUserAll payload ──────────────────────────────────────
UpsertUserAllPayload _buildPayload() { UpsertUserAllPayload _buildPayload() {
final data = _apiData!; final data = _apiData!;
final loginDt = widget.loginDateTime ?? DateTime.now().millisecondsSinceEpoch ~/ 1000; final loginDt = widget.loginDateTime ?? DateTime.now().millisecondsSinceEpoch ~/ 1000;
@@ -293,27 +514,29 @@ class _TransferPackagePageState extends State<TransferPackagePage> {
final cfg = TitleServerConfigHolder().config; final cfg = TitleServerConfigHolder().config;
final clientId = ud['lastClientId'] ?? cfg?.clientId ?? ''; final clientId = ud['lastClientId'] ?? cfg?.clientId ?? '';
// Build musicData for userMusicDetailList — must NOT be empty. const musicId = 834;
// Matches the playlog entry below (same musicId/level/achievement/…). const level = 4;
final musicData = <String, dynamic>{ const achievement = 1000000;
'musicId': 834, const comboStatus = 3;
'level': 4, const deluxscore = 0;
'playCount': 1, const scoreRank = 13;
'achievement': 1000000,
'comboStatus': 3, // Build musicDetailList + gamePlaylog + outer playlog (all for 1 track).
'syncStatus': 0, final musicDetail = _makeMusicDetail(
'deluxscoreMax': 0, musicId: musicId,
'scoreRank': 13, level: level,
'extNum1': 0, playCount: 1,
}; achievement: achievement,
comboStatus: comboStatus,
syncStatus: 0,
deluxscoreMax: deluxscore,
scoreRank: scoreRank,
);
final upsertUserAll = { final upsertUserAll = {
'userData': [ 'userData': [
{ {
...ud, ...ud,
'playCount': (ud['playCount'] as int? ?? 0) + 1,
'currentPlayCount':
(ud['currentPlayCount'] as int? ?? 0) + 1,
'lastLoginDate': 'lastLoginDate':
widget.lastLoginDate ?? ud['lastLoginDate'] ?? '', widget.lastLoginDate ?? ud['lastLoginDate'] ?? '',
'banState': userDataJson['banState'] ?? ud['banState'] ?? 0, 'banState': userDataJson['banState'] ?? ud['banState'] ?? 0,
@@ -323,7 +546,6 @@ class _TransferPackagePageState extends State<TransferPackagePage> {
'lastRomVersion': ud['lastRomVersion'] ?? '1.53.00', 'lastRomVersion': ud['lastRomVersion'] ?? '1.53.00',
'lastDataVersion': ud['lastDataVersion'] ?? '1.50.14', 'lastDataVersion': ud['lastDataVersion'] ?? '1.50.14',
'lastPlayDate': _nowStr(), 'lastPlayDate': _nowStr(),
'lastPlayCredit': 1,
'lastPlayMode': 0, 'lastPlayMode': 0,
'lastPlaceId': cfg?.placeId ?? ud['lastPlaceId'] ?? 0, 'lastPlaceId': cfg?.placeId ?? ud['lastPlaceId'] ?? 0,
'lastPlaceName': 'lastPlaceName':
@@ -334,7 +556,7 @@ class _TransferPackagePageState extends State<TransferPackagePage> {
'lastAllNetId': ud['lastAllNetId'] ?? 0, 'lastAllNetId': ud['lastAllNetId'] ?? 0,
'lastCountryCode': ud['lastCountryCode'] ?? 'CHN', 'lastCountryCode': ud['lastCountryCode'] ?? 'CHN',
'lastClientId': clientId, 'lastClientId': clientId,
'dateTime': loginDt, 'dateTime': DateTime.now().millisecondsSinceEpoch ~/ 1000,
}, },
], ],
'userExtend': [ue], 'userExtend': [ue],
@@ -346,7 +568,7 @@ class _TransferPackagePageState extends State<TransferPackagePage> {
'userLoginBonusList': _advanced.parsedUserLoginBonusList('[]'), 'userLoginBonusList': _advanced.parsedUserLoginBonusList('[]'),
'userRatingList': [ur], 'userRatingList': [ur],
'userItemList': _advanced.parsedUserItemList('[]'), 'userItemList': _advanced.parsedUserItemList('[]'),
'userMusicDetailList': [musicData], 'userMusicDetailList': [musicDetail],
'userCourseList': <dynamic>[], 'userCourseList': <dynamic>[],
'userFriendSeasonRankingList': <dynamic>[], 'userFriendSeasonRankingList': <dynamic>[],
'userChargeList': uc, 'userChargeList': uc,
@@ -409,58 +631,25 @@ class _TransferPackagePageState extends State<TransferPackagePage> {
'isNewKaleidxScopeList': '', 'isNewKaleidxScopeList': '',
}; };
// Minimal playlog entry — required, server rejects empty playlogList. // Outer playlog matching the fake game log.
final rating = ud['playerRating'] as int? ?? 0; final playlog = _makePlaylogEntry(
final playlog = <String, dynamic>{ charaSlot: charaSlot,
'userId': 0, 'orderId': 0, musicId: musicId,
'playlogId': widget.playlogId ?? 0, 'version': 1053000, level: level,
'placeId': cfg?.placeId ?? 0, trackNo: 1,
'placeName': cfg?.placeName ?? '', achievement: achievement,
'loginDate': loginDt, deluxscore: deluxscore,
'playDate': _nowStr().split(' ')[0], scoreRank: scoreRank,
'userPlayDate': _nowStr(), 'type': 0, comboStatus: comboStatus,
'musicId': 834, 'level': 4, 'trackNo': 1, maxCombo: 0,
'vsMode': 0, 'vsUserName': '', 'vsStatus': 0, totalCombo: 128,
'vsUserRating': 0, 'vsUserAchievement': 0, 'vsUserGradeRank': 0, maxSync: 0,
'vsRank': 0, 'playerNum': 1, totalSync: 0,
'playedUserId1': 0, 'playedUserName1': '', 'playedMusicLevel1': 0, isClear: true,
'playedUserId2': 0, 'playedUserName2': '', 'playedMusicLevel2': 0, isAchieveNewRecord: true,
'playedUserId3': 0, 'playedUserName3': '', 'playedMusicLevel3': 0, isDeluxscoreNewRecord: true,
'characterId1': charaSlot.isNotEmpty ? charaSlot[0] : 0, extNum4: 101,
'characterLevel1': 1, 'characterAwakening1': 0, );
'characterId2': charaSlot.length > 1 ? charaSlot[1] : 0,
'characterLevel2': 1, 'characterAwakening2': 0,
'characterId3': charaSlot.length > 2 ? charaSlot[2] : 0,
'characterLevel3': 1, 'characterAwakening3': 0,
'characterId4': charaSlot.length > 3 ? charaSlot[3] : 0,
'characterLevel4': 1, 'characterAwakening4': 0,
'characterId5': charaSlot.length > 4 ? charaSlot[4] : 0,
'characterLevel5': 1, 'characterAwakening5': 0,
'achievement': 1000000, 'deluxscore': 0, 'scoreRank': 13,
'maxCombo': 0, 'totalCombo': 128, 'maxSync': 0, 'totalSync': 0,
'tapCriticalPerfect': 101, 'tapPerfect': 0, 'tapGreat': 0,
'tapGood': 0, 'tapMiss': 0,
'holdCriticalPerfect': 9, 'holdPerfect': 0, 'holdGreat': 0,
'holdGood': 0, 'holdMiss': 0,
'slideCriticalPerfect': 4, 'slidePerfect': 0, 'slideGreat': 0,
'slideGood': 0, 'slideMiss': 0,
'touchCriticalPerfect': 0, 'touchPerfect': 0, 'touchGreat': 0,
'touchGood': 0, 'touchMiss': 0,
'breakCriticalPerfect': 1, 'breakPerfect': 0, 'breakGreat': 0,
'breakGood': 0, 'breakMiss': 0,
'isTap': true, 'isHold': true, 'isSlide': true, 'isTouch': false,
'isBreak': true, 'isCriticalDisp': true, 'isFastLateDisp': true,
'fastCount': 0, 'lateCount': 0,
'isAchieveNewRecord': false, 'isDeluxscoreNewRecord': false,
'comboStatus': 3, 'syncStatus': 0, 'isClear': true,
'beforeRating': rating, 'afterRating': rating,
'beforeGrade': 0, 'afterGrade': 0, 'afterGradeRank': 0,
'beforeDeluxRating': rating, 'afterDeluxRating': rating,
'isPlayTutorial': false, 'isEventMode': false, 'isFreedomMode': false,
'playMode': 0, 'isNewFree': false, 'trialPlayAchievement': -1,
'extNum1': 0, 'extNum2': 0, 'extNum4': 101,
'extBool1': false, 'extBool2': false,
};
return UpsertUserAllPayload( return UpsertUserAllPayload(
userId: widget.userId, userId: widget.userId,
@@ -713,7 +902,9 @@ class _TransferPackagePageState extends State<TransferPackagePage> {
), ),
), ),
if (_step == TransferStep.fetching || _step == TransferStep.sending) if (_step == TransferStep.fetching ||
_step == TransferStep.sending ||
_step == TransferStep.uploadingPlaylog)
_buildProgress(theme), _buildProgress(theme),
if (_error != null && _apiData == null) ...[ if (_error != null && _apiData == null) ...[
const SizedBox(height: 12), const SizedBox(height: 12),
@@ -828,7 +1019,8 @@ class _TransferPackagePageState extends State<TransferPackagePage> {
Widget _buildFetchButton(ThemeData theme) { Widget _buildFetchButton(ThemeData theme) {
final hasLogin = widget.loginDateTime != null; final hasLogin = widget.loginDateTime != null;
final isLoading = _step == TransferStep.fetching; final isLoading = _step == TransferStep.fetching;
final isSending = _step == TransferStep.sending; final isSending = _step == TransferStep.sending ||
_step == TransferStep.uploadingPlaylog;
final canFetch = hasLogin && !isLoading && !isSending; final canFetch = hasLogin && !isLoading && !isSending;
return SizedBox( return SizedBox(
@@ -1190,6 +1382,14 @@ class _TransferPackagePageState extends State<TransferPackagePage> {
} }
Widget _buildProgress(ThemeData theme) { Widget _buildProgress(ThemeData theme) {
String text;
if (_step == TransferStep.fetching) {
text = AppStrings.transferFetching;
} else if (_step == TransferStep.uploadingPlaylog) {
text = AppStrings.transferUploadingPlaylog;
} else {
text = AppStrings.transferSending;
}
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
child: Center( child: Center(
@@ -1198,9 +1398,7 @@ class _TransferPackagePageState extends State<TransferPackagePage> {
const CircularProgressIndicator(), const CircularProgressIndicator(),
const SizedBox(height: 12), const SizedBox(height: 12),
Text( Text(
_step == TransferStep.fetching text,
? AppStrings.transferFetching
: AppStrings.transferSending,
style: theme.textTheme.bodyMedium?.copyWith( style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant, color: theme.colorScheme.onSurfaceVariant,
), ),
+17 -1
View File
@@ -511,7 +511,23 @@ class TitleApiService {
'${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}.0'; '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}.0';
} }
// ---- UpsertUserAll (传包) ---- // ---- UpsertUserAll (发包) ----
/// Upload a single playlog record (fake game log).
Future<void> uploadUserPlaylog(
Map<String, dynamic> userPlaylog,
int userId,
) async {
const apiName = 'UploadUserPlaylogApi';
final json = await _callApi(apiName, {
'userId': userId,
'userPlaylog': userPlaylog,
}, userId);
final returnCode = json['returnCode'] as int? ?? -1;
if (returnCode != 1) {
throw TitleApiException('UploadUserPlaylogApi returnCode=$returnCode');
}
}
/// Fetch all user data needed to build an UpsertUserAll payload. /// Fetch all user data needed to build an UpsertUserAll payload.
/// Returns a map keyed by the raw API names (e.g. 'GetUserDataApi'). /// Returns a map keyed by the raw API names (e.g. 'GetUserDataApi').