fix: 解歌的 isNewItemList 按主键回读再算,不再一律发 1

排查「发 11820 解歌不显示」时发现两处问题:

- applyItemListPatch 的 isNewItemList 一直硬写全 '1'(新增),从没读服务器
  已有行。真机 BuildListData 是按 (itemKind, itemId) 比对快照给 0/1 的,
  重复解锁一首已存在的歌时服务器按插入冲突丢行,表现就是上传成功但没生效。
  这和角色/地图/万花筒三处的处理不一致,现补齐。
- 新增 TitleApiService.getUserItems/getUserItemKeys。GetUserItemApi 是按
  itemKind 分页的,首次 nextIndex 要按 itemKind * 1e10 编码
  (PacketGetUserItem.cs:19),发 0 只会拿到空页。
- 解锁上传后回读复查,把「服务器没存这行道具」与「存了但曲目池不给进」分开报。
- 待解锁列表含 10000~19999 的 DX 曲时提示:Master/Re:Master 不像老曲那样
  白送(IsUnlockMaster 里 id<10000 直接放过),必须显式带 itemKind 6/7。

需要说明:一首歌进不进选曲池由 NotesListManager.cs:70 决定
(IsOpenEvent(eventName) && !IsGameNgMusicId),事件窗口来自 GetGameEventApi、
曲目本体来自机台 Music.xml。11820 若卡在这一层,任何用户包都改不动,
复查那一步就是为了把这种情况和包的问题区分开。

README 补 isNew*List 语义、曲目可见性链路与 DX 段解锁规则三条说明。
删除因此不再被引用的 unlockMusicSuccess。
This commit is contained in:
Cookiues committed 2026-10-01 22:14:50 +08:00
1 parent 6438fb2171
commit 80df51c239
6 files changed
+213 -4

No files matched your search

+67 -2
View File
@@ -126,6 +126,12 @@ class _MusicRiskFeatureViewState extends State<MusicRiskFeatureView>
bool get _isUnlock => widget.mode == MusicRiskFeatureMode.unlockMusic;
/// ConstParameter.DxScoreBaseID=10000 / StrongScoreBaseID=20000
/// (`MAI2System/ConstParameter.cs:168,171`)。这段里的歌 Master 需要
/// 显式 itemKind 6,不像 id < 10000 那样在 IsUnlockMaster 里直接放过。
bool _isDeluxeRange(_PendingMusic music) =>
music.musicId >= 10000 && music.musicId < 20000;
@override
int? get cooldownLoginDateTime => widget.loginDateTime;
@@ -310,7 +316,26 @@ class _MusicRiskFeatureViewState extends State<MusicRiskFeatureView>
generalUserInfo: data,
);
builder.applyItemListPatch(packet, items: _buildUserItemList());
final items = _buildUserItemList();
// isNewItemList 得按 (itemKind, itemId) 主键比对服务器快照来给 0/1,
// 不能一律发 '1':重复解锁一首已经存在的歌时,服务器会把插入冲突的
// 行整行丢掉,表现就是「上传成功但没生效」。
final existingKeys = await service.getUserItemKeys(
widget.userId,
items.map((e) => e['itemKind'] as int),
);
builder.applyItemListPatch(
packet,
items: items,
newFlags: [
for (final item in items)
existingKeys.contains(
'${item['itemKind']}:${item['itemId']}',
)
? '0'
: '1',
].join(),
);
if (_isUnlock) {
builder.applyMusicDetailPatch(packet, musicData: musicData);
}
@@ -320,7 +345,7 @@ class _MusicRiskFeatureViewState extends State<MusicRiskFeatureView>
_updateStep(
RiskStep.complete,
_isUnlock
? AppStrings.unlockMusicSuccess
? await _verifyUnlock(service, items)
: AppStrings.collectiblesSuccess,
);
@@ -342,6 +367,34 @@ class _MusicRiskFeatureViewState extends State<MusicRiskFeatureView>
}
}
/// 上传后回读 GetUserItemApi,把「服务器根本没存这行道具」和「存了但游戏里
/// 还是没有这首歌」分开——后者是曲目池门禁(机台 Music.xml 没这个 ID、
/// `eventName` 指向的活动不在开放窗口内、或落在 NG 名单里),
/// 见 NotesListManager.cs:70,**任何用户数据包都改不动它**。
Future<String> _verifyUnlock(
TitleApiService service,
List<Map<String, dynamic>> items,
) async {
final requested = [
for (final item in items) '${item['itemKind']}:${item['itemId']}',
];
Set<String> saved;
try {
saved = await service.getUserItemKeys(
widget.userId,
items.map((e) => e['itemKind'] as int),
);
} catch (e) {
return AppStrings.unlockVerifyFailed('$e');
}
final missing = requested.where((k) => !saved.contains(k)).toList();
if (missing.isNotEmpty) {
return AppStrings.unlockNotSaved(missing.join(' '));
}
return AppStrings.unlockVerified(requested.length);
}
String get _notLoggedInMessage => _isUnlock
? AppStrings.unlockNotLoggedIn
: AppStrings.collectiblesNotLoggedIn;
@@ -539,6 +592,18 @@ class _MusicRiskFeatureViewState extends State<MusicRiskFeatureView>
),
],
),
// DX 段的 Master / Re:Master 不白送,勾选项里漏一个就开不出来,
// 这是这段 ID 最容易踩的坑,只在列表真含 DX 曲时才说明。
if (_pendingMusics.any(_isDeluxeRange)) ...[
const SizedBox(height: 10),
Text(
AppStrings.unlockDxRangeNotice,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.error,
height: 1.5,
),
),
],
],
),
),