127 lines
4.6 KiB
C#
127 lines
4.6 KiB
C#
namespace Ananta.Server.Protocol.Client4229938;
|
|
|
|
/// <summary>
|
|
/// 一首歌的历史成绩 —— Livehouse 音游(节奏打击)per-player 存档的最小形状。
|
|
///
|
|
/// <para>
|
|
/// 字段直接对应 wire 上的 <c>MusicClientInfoPush.RecordInfo</c>
|
|
/// (见 <c>Ananta.RpcTypes\Client4229938\MusicPlayWireContracts.cs</c> 的顶部注释):
|
|
/// <b>Perfect / Great / Miss / Special</b>,顺序不可换。
|
|
/// </para>
|
|
/// <para>
|
|
/// 这 4 + 1 个字段全是 primitive ⇒ 同一份 record 既做内存形状也做
|
|
/// <c>config/player-progress.json</c> 里的落盘形状,不需要额外的 <c>Save*</c> DTO
|
|
/// (对比 <c>FashionColoringRecord4229938</c>:它因为嵌套字典才必须配 <c>SaveFashionColoring</c>)。
|
|
/// </para>
|
|
/// </summary>
|
|
internal sealed record LivehouseRecord4229938(
|
|
uint Perfect,
|
|
uint Great,
|
|
uint Miss,
|
|
uint Special,
|
|
bool AlreadyReward);
|
|
|
|
/// <summary>
|
|
/// Livehouse 音游进度:a) 每首歌的最好成绩 b) 「首次引导播过了没有」。
|
|
///
|
|
/// <para>
|
|
/// 挂在 <see cref="WorldEntryState"/> 上并通过 <c>PlayerProgressStore</c> 落盘
|
|
/// (硬规则 2:跨 session 状态必须走 WorldEntryState + PlayerProgressStore),
|
|
/// 这样重登之后曲目列表里的等级打点还在。
|
|
/// </para>
|
|
/// <para>
|
|
/// 为什么不记「当前正在打的那一首」:<c>LiveHouseMusicStart</c> /
|
|
/// <c>LiveHouseMusicInterrupt</c> 那两条 notify 只是开局/中断通知,
|
|
/// 真正的成绩由 <c>SetLiveHouseMusicResult</c> 送来;把「进行中」放进持久化存档
|
|
/// 会在中途退出时留下脏状态 ⇒ 放在 session 里(handler 自己的字典),不进这里。
|
|
/// </para>
|
|
/// </summary>
|
|
internal static class LivehouseProgressStore
|
|
{
|
|
/// <summary>
|
|
/// 取全部曲目成绩的<b>快照</b>。
|
|
///
|
|
/// ⛑ 必须在调用方**没有**持有 <c>state.SyncRoot</c> 时调用(本方法自己会上锁)。
|
|
/// </summary>
|
|
internal static Dictionary<uint, LivehouseRecord4229938> Snapshot(WorldEntryState? state)
|
|
{
|
|
var result = new Dictionary<uint, LivehouseRecord4229938>();
|
|
if (state is null)
|
|
return result;
|
|
|
|
lock (state.SyncRoot)
|
|
{
|
|
foreach (var (musicId, record) in state.LivehouseRecords)
|
|
{
|
|
if (musicId == 0 || record is null)
|
|
continue;
|
|
|
|
result[musicId] = record;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 记一次通关成绩。<c>counts</c> 的前 4 项依次为 Perfect / Great / Miss / Special。
|
|
///
|
|
/// <para>
|
|
/// ⛑ 「更好」的判定按 <b>Perfect 数</b>递增,而不是「最后一次」:
|
|
/// 客户端 <c>AskLiveHouseMusicList</c> 回来的东西会被直接灌进
|
|
/// <c>gMusicGameManager.RecordMusicInfo</c>,回写最后一次成绩会让
|
|
/// 「打崩一局把 S 级洗成 C 级」成为常态。
|
|
/// </para>
|
|
/// </summary>
|
|
internal static void Record(WorldEntryState state, uint musicId, IReadOnlyList<uint> counts)
|
|
{
|
|
if (musicId == 0 || counts.Count == 0)
|
|
return;
|
|
|
|
var perfect = counts.Count > 0 ? counts[0] : 0u;
|
|
var great = counts.Count > 1 ? counts[1] : 0u;
|
|
var miss = counts.Count > 2 ? counts[2] : 0u;
|
|
var special = counts.Count > 3 ? counts[3] : 0u;
|
|
|
|
lock (state.SyncRoot)
|
|
{
|
|
var old = state.LivehouseRecords.GetValueOrDefault(musicId);
|
|
if (old is not null && old.Perfect >= perfect && old.Special >= special)
|
|
return;
|
|
|
|
state.LivehouseRecords[musicId] = new LivehouseRecord4229938(
|
|
perfect, great, miss, special, old?.AlreadyReward ?? false);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 消费「首次游玩」标记:第一次调用返回 <c>true</c>,之后恒 <c>false</c>。
|
|
///
|
|
/// <para>
|
|
/// 这是 <c>AskQueryLiveHouseInfo</c> 唯一会写入 <c>FirstPlay</c> 的地方。
|
|
/// 客户端拿它决定要不要播 <c>LivehouseIntroTL</c>(见 Field notes 2026-10-04)。
|
|
/// </para>
|
|
/// </summary>
|
|
internal static bool TakeFirstPlay(WorldEntryState state)
|
|
{
|
|
lock (state.SyncRoot)
|
|
{
|
|
if (state.LivehouseFirstPlayDone)
|
|
return false;
|
|
|
|
state.LivehouseFirstPlayDone = true;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/// <summary>全部抹掉 —— 供 economy reset 走同一套「抹干净」语义。</summary>
|
|
internal static void Clear(WorldEntryState state)
|
|
{
|
|
lock (state.SyncRoot)
|
|
{
|
|
state.LivehouseRecords.Clear();
|
|
state.LivehouseFirstPlayDone = false;
|
|
}
|
|
}
|
|
}
|