feat: 风险页新增数值修改、一键跑图与万花筒专区
- 修改总 Rating:只写 playerRating 与 userRating.rating,钳 0~99999, 每首歌各自的 ratingList 原样带回 - 添加舞里程(UserDetail.point):完整 int32 区间,支持累加与直接设定, 允许负值即扣减 - 一键跑图:isClear/isComplete 在客户端是从 distance 派生的,单发 flag 会被 冲掉,故把 distance 推过 End 里程针;unlockFlag 语义是反的 - 万花筒专区:发现门与获取钥匙落在 userKaleidxScopeList 同一行的两个布尔位, 钥匙不走 userItemList;上行是整行替换,所以先读回原行再叠加 - 旅行伙伴:修正误导性的示例 ID 1001(合法是三位数,表里没有的 ID 服务器照收 但客户端静默跳过),编组槽位可设定等级,上传后回读复查以区分失败原因 - 补 MIT 许可证
This commit is contained in:
1 parent
329845f228
commit
0808e367f7
15 files changed
+3597
-42
No files matched your search
@@ -8,6 +8,20 @@ import 'title_api_service.dart';
|
||||
/// Builds the UpsertUserAllApi request packet used by the
|
||||
/// UpsertMusic (上传成绩) / UnlockMusic (解锁歌曲) workflows.
|
||||
class UserAllPayloadBuilder {
|
||||
/// `UserDetail` 里这些数值字段在 C# 侧都是 `int`,超出 int32 会让服务器
|
||||
/// 反序列化失败并回 500(同 `playSpecial` 那次)。舞里程直接用整个 int32 区间,
|
||||
/// 允许负值意味着「累加模式填负数就是扣里程」。
|
||||
static const int minValue = -2147483648;
|
||||
static const int maxValue = 2147483647;
|
||||
|
||||
/// 总 Rating 的显示区间,游戏内没有超过 5 位的段位。
|
||||
static const int maxRating = 99999;
|
||||
|
||||
/// `UserChara` 的真实等级上限(`RealLevelMax`)。界面上显示的等级是
|
||||
/// `level % 10000`、转生次数是 `level ~/ 10000`,所以 999999 = 99 转生 + 9999 级。
|
||||
static const int minCharacterLevel = 1;
|
||||
static const int maxCharacterLevel = 999999;
|
||||
|
||||
final TitleServerConfig config;
|
||||
|
||||
const UserAllPayloadBuilder(this.config);
|
||||
@@ -30,6 +44,20 @@ class UserAllPayloadBuilder {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int clampValue(int v) =>
|
||||
v < minValue ? minValue : (v > maxValue ? maxValue : v);
|
||||
|
||||
/// 旅行伙伴等级不允许 0,`UserChara.Clear()` 的初值就是 1。
|
||||
static int clampCharacterLevel(int v) =>
|
||||
v < minCharacterLevel
|
||||
? minCharacterLevel
|
||||
: (v > maxCharacterLevel ? maxCharacterLevel : v);
|
||||
|
||||
static Map<String, dynamic> _userDataOf(Map<String, dynamic> packet) =>
|
||||
((packet['upsertUserAll'] as Map<String, dynamic>)['userData'] as List)
|
||||
.first
|
||||
as Map<String, dynamic>;
|
||||
|
||||
/// "1.56.00" -> 1056000 (Lionheart convertVersionNumber).
|
||||
static int _convertVersionNumber(String version) {
|
||||
final parts = version.split('.');
|
||||
@@ -369,15 +397,76 @@ class UserAllPayloadBuilder {
|
||||
upsert['isNewMusicDetailList'] = '1';
|
||||
}
|
||||
|
||||
/// 发放/更新旅行伙伴:写 `userCharacterList` 并按行数生成 `isNewCharacterList`。
|
||||
/// 客户端 BuildListData 只发增量、每位一个字符,所以这里只放要新增的行。
|
||||
/// 发放/更新旅行伙伴:写 `userCharacterList` 并给出逐行 `isNewCharacterList`。
|
||||
///
|
||||
/// [newFlags] 省略时全部按「新增行」发(每位一个 '1')。给已有角色改等级时
|
||||
/// 必须传 '0',否则服务器按插入处理。客户端 `BuildListData` 就是按
|
||||
/// 「服务端是否已有同 characterId 的行」决定这一位是 0 还是 1 的。
|
||||
void applyCharacterListPatch(
|
||||
Map<String, dynamic> packet, {
|
||||
required List<Map<String, dynamic>> characters,
|
||||
String? newFlags,
|
||||
}) {
|
||||
final upsert = packet['upsertUserAll'] as Map<String, dynamic>;
|
||||
upsert['userCharacterList'] = characters;
|
||||
upsert['isNewCharacterList'] = List.filled(characters.length, '1').join();
|
||||
upsert['isNewCharacterList'] =
|
||||
newFlags ?? List.filled(characters.length, '1').join();
|
||||
}
|
||||
|
||||
/// 一键跑图:把区域标记为已完成,并让客户端自己把状态推回「已完成」。
|
||||
///
|
||||
/// `UserMap.isClear` / `isComplete` 在客户端是从 `distance` 派生的
|
||||
/// (`MapMaster.CreateUserDataMapList` 拿 `distance` 与该区域的
|
||||
/// ReleaseFlag / End 里程针比较,没有对应针时甚至强制写回 false),
|
||||
/// 单发 flag 下次进区域选择页就会被冲掉。所以这里把 `distance` 直接推到
|
||||
/// `UserMapData.MaxDistance`,超过任何一张图的 End 针,flag 才站得住。
|
||||
///
|
||||
/// `unlockFlag` 语义是反的:`IsFinishedOpening ? 0 : 1`,取 0 表示已开启。
|
||||
/// `isLock` 客户端不回读(`ConvertUserMap` 从不读它),给 false 只是保持上行一致。
|
||||
///
|
||||
/// [newFlags] 每一位对应 [maps] 里的一行:'1' 新增、'0' 更新,
|
||||
/// 长度必须与 [maps] 一致(`BuildListData` 是一一对齐生成的)。
|
||||
void applyMapPatch(
|
||||
Map<String, dynamic> packet, {
|
||||
required List<Map<String, dynamic>> maps,
|
||||
required String newFlags,
|
||||
}) {
|
||||
assert(maps.length == newFlags.length);
|
||||
final upsert = packet['upsertUserAll'] as Map<String, dynamic>;
|
||||
upsert['userMapList'] = maps;
|
||||
upsert['isNewMapList'] = newFlags;
|
||||
}
|
||||
|
||||
/// 一行「已完成」的区域记录,字段与 `Net.VO.Mai2.UserMap` 一一对应。
|
||||
static Map<String, dynamic> completedMap(int mapId, {int? distance}) => {
|
||||
'mapId': mapId,
|
||||
'distance': distance ?? maxMapDistance,
|
||||
'isLock': false,
|
||||
'isClear': true,
|
||||
'isComplete': true,
|
||||
'unlockFlag': 0,
|
||||
};
|
||||
|
||||
/// `UserMapData.MaxDistance = 999999999`,客户端认的最大步数。
|
||||
static const int maxMapDistance = 999999999;
|
||||
|
||||
/// 万花筒:写 `userKaleidxScopeList` + `isNewKaleidxScopeList`。
|
||||
///
|
||||
/// 每行是**整行替换**(15 个字段的 `UserKaleidxScope`),所以调用方必须先把
|
||||
/// 服务器上原行的成绩/日期字段读回来合并,不然 best 记录与 playCount 会被清零。
|
||||
///
|
||||
/// 客户端 `ExportUserAll`(VOExtensions.cs:323)只在数组非空时才给这两个字段赋值,
|
||||
/// 空数组等于什么都没发;[newFlags] 长度必须等于 [scopes] 行数。
|
||||
void applyKaleidxScopePatch(
|
||||
Map<String, dynamic> packet, {
|
||||
required List<Map<String, dynamic>> scopes,
|
||||
required String newFlags,
|
||||
}) {
|
||||
assert(scopes.isNotEmpty);
|
||||
assert(scopes.length == newFlags.length);
|
||||
final upsert = packet['upsertUserAll'] as Map<String, dynamic>;
|
||||
upsert['userKaleidxScopeList'] = scopes;
|
||||
upsert['isNewKaleidxScopeList'] = newFlags;
|
||||
}
|
||||
|
||||
/// 编组旅行伙伴:改 `userData[0].charaSlot`(int[5],槽 0 为队长)。
|
||||
@@ -399,4 +488,55 @@ class UserAllPayloadBuilder {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 改总 Rating:只动 `userData.playerRating` 与 `userRating.rating`。
|
||||
///
|
||||
/// `userRating.ratingList` / `newRatingList` 里每首歌各自的 Rating 原样带回,
|
||||
/// 不重排也不改值——服务器就是按这两处的和来出「乐曲 Rating」的。
|
||||
/// playlog 的 before/after 一起对齐,否则存档里那条记录会自相矛盾。
|
||||
void applyRatingPatch(
|
||||
Map<String, dynamic> packet, {
|
||||
required int rating,
|
||||
}) {
|
||||
final userData = _userDataOf(packet);
|
||||
final before = _toInt(userData['playerRating']);
|
||||
final value = rating.clamp(0, maxRating);
|
||||
userData['playerRating'] = value;
|
||||
|
||||
final userRatingList =
|
||||
(packet['upsertUserAll'] as Map<String, dynamic>)['userRatingList']
|
||||
as List;
|
||||
// 整行换掉而不是原地写:userRating 在缺数据时是 `const {}`,改不了。
|
||||
if (userRatingList.first is Map<String, dynamic>) {
|
||||
userRatingList[0] = {
|
||||
...(userRatingList.first as Map<String, dynamic>),
|
||||
'rating': value,
|
||||
};
|
||||
}
|
||||
|
||||
for (final raw in (packet['userPlaylogList'] as List)) {
|
||||
final playlog = raw as Map<String, dynamic>;
|
||||
playlog['beforeRating'] = before;
|
||||
playlog['afterRating'] = value;
|
||||
playlog['beforeDeluxRating'] = before;
|
||||
playlog['afterDeluxRating'] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// 改舞里程:`UserDetail.point` 是余额,`totalPoint` 是累计获得量。
|
||||
///
|
||||
/// 客户端只在「获得」时把 point 钳到 99999(`UserDetail.AddMile`),
|
||||
/// 商店扣款走的是不带钳位的 `Point -= cost`,所以直接写更大的值不会被覆回;
|
||||
/// 展示就是一句 `num.ToString()`,没有位数上限。
|
||||
void applyMaiMilePatch(
|
||||
Map<String, dynamic> packet, {
|
||||
required int point,
|
||||
int? totalPoint,
|
||||
}) {
|
||||
final userData = _userDataOf(packet);
|
||||
userData['point'] = clampValue(point);
|
||||
if (totalPoint != null) {
|
||||
userData['totalPoint'] = clampValue(totalPoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user