170 lines
7.7 KiB
C#
170 lines
7.7 KiB
C#
using Ananta.SDK.Rpc;
|
|
using Ananta.Server.ClientData.Client4229938;
|
|
using Ananta.Server.Protocol.Client4229938;
|
|
using GameMethods = Ananta.Server.RpcTypes.Client4229938.Methods.Game;
|
|
|
|
namespace Ananta.Server.Handlers.Game;
|
|
|
|
/// <summary>
|
|
/// Profession phone-app DATA handlers (build 4229938) — 清洁工 Washer / 赛车手 Racer.
|
|
/// Ported from the Lyco donor build on 2026-09-26.
|
|
///
|
|
/// Diagnosis (donor, 2026-09-20 — holds for our tree too): these apps are <b>not</b> blocked at the
|
|
/// entry or at the switch gate. `MainPhoneUtils.OnAppItemClick` has a branch for every job and the
|
|
/// fastpatch ClientGameSwitch module already sets EnableJanitor / EnableCatExpress /
|
|
/// EnableDutyTerminal / EnableEonBug, so `SwitchFunctionManager.CheckEnable` passes. What was
|
|
/// missing is the DATA: with no handler the client fell through to the typed-default (all-zero)
|
|
/// body and each panel rendered an empty shell.
|
|
///
|
|
/// ⛑⛑ This is a STATIC SNAPSHOT, not a simulation. There is no server-side mission generation,
|
|
/// no persisted racing result and no WasherConfig table on our side, so:
|
|
/// * 清洁工 shows one in-flight mission and a fixed history — progress never advances by itself;
|
|
/// * 赛车手 reports zero points / zero stars, so only the group whose authored gate is 0/0
|
|
/// (group 1, C级赛) is unlocked and every league the player has not earned renders 未解锁.
|
|
/// Accepting / finishing a mission is acked and nothing else happens.
|
|
///
|
|
/// ⛑ Do NOT "improve" 赛车手 by returning every group with a synthetic record. The donor measured
|
|
/// that on 2026-09-22: `RacerHistoryPageStore.BuildHistoryList` turns each group's track rows into a
|
|
/// HISTORY entry (so the app claimed "C级赛事 完成" for a player who never raced) and
|
|
/// `RacerCareerPageStore.BuildDisplayLists` treats the KEYS of CompetitionGroupInfos as the unlocked
|
|
/// set, which pushes the frontier to the last league and renders B级赛 进行中 although it is
|
|
/// authored locked. Presence means "unlocked"; the records stay empty until a race is finished.
|
|
/// </summary>
|
|
internal sealed partial class GameRouter
|
|
{
|
|
/// <summary>
|
|
/// Washer mission ids. ⛑ Hard-coded: this server ships no WasherConfig.json, so these are the
|
|
/// donor's literal rows from ConfigDump_v3. Replace with a repository lookup if the table is
|
|
/// ever copied into ClientData/4229938/Configs.
|
|
/// </summary>
|
|
private static readonly uint[] WasherMissionIds = [2u, 3u, 6u];
|
|
|
|
[Handler(MethodId.AskGetWasherMissionInfo, HandlerPacketKind.Invoke)]
|
|
private Task AskGetWasherMissionInfo(Connection conn, UxRpcMessage msg)
|
|
=> conn.ReturnAsync(msg, BuildWasherMissionInfo());
|
|
|
|
/// <summary>
|
|
/// Accept / finish (63297472 / 63889979). Both return void per DefaultReturnCatalog, so an empty
|
|
/// OK is the whole contract — they are INVOKEs, and leaving them unrouted makes the button await
|
|
/// a reply that never comes.
|
|
/// </summary>
|
|
[Handler(MethodId.AskAcceptWasherMission, HandlerPacketKind.Invoke)]
|
|
[Handler(MethodId.AskFinishWasherMission, HandlerPacketKind.Invoke)]
|
|
private async Task WasherMissionAction(Connection conn, UxRpcMessage msg)
|
|
{
|
|
conn.Log.Info(
|
|
$"[WASHER] {(msg.MethodId == MethodId.AskAcceptWasherMission ? "accept" : "finish")} " +
|
|
$"bytes={msg.Body.Length}");
|
|
await conn.ReturnEmptyOkAsync(msg);
|
|
}
|
|
|
|
[Handler(MethodId.AskRacingCompetitionGroupInfo, HandlerPacketKind.Invoke)]
|
|
private async Task AskRacingCompetitionGroupInfo(Connection conn, UxRpcMessage msg)
|
|
{
|
|
var info = BuildRacingCompetitionGroupInfo();
|
|
conn.Log.Info(
|
|
$"[RACER] group info leagues=[{string.Join(',', info.UnlockCompetitionTypes)}] " +
|
|
$"groups=[{string.Join(',', info.CompetitionGroupInfos.Keys)}] " +
|
|
$"point={info.CompetitionPoint} star={info.CompetitionStar}");
|
|
await conn.ReturnAsync(msg, info);
|
|
}
|
|
|
|
// ------------------------------------------------------------------ builders
|
|
|
|
private static GameMethods.WasherMissionInfoResult BuildWasherMissionInfo()
|
|
{
|
|
var now = (uint)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
|
var result = new GameMethods.WasherMissionInfoResult
|
|
{
|
|
LastRefreshMissionTime = now,
|
|
CurMissionIndex = 1,
|
|
CurMissionId = WasherMissionIds[0],
|
|
CurMissionEventId = 1,
|
|
// A non-zero progress is what makes the panel's progress bar render at all.
|
|
CurMissionProgress = 0.35f,
|
|
CurMissionStartTime = now,
|
|
CurMissionResult = new GameMethods.WasherMissionResult
|
|
{
|
|
MissionId = WasherMissionIds[0],
|
|
EventId = 1,
|
|
Progress = 0.35f,
|
|
RewardRate = 1f,
|
|
ProficiencyRate = 1f,
|
|
UsingTime = 0,
|
|
AddMoney = 0,
|
|
TalentPointRate = 1f,
|
|
RandomCfgId = 1,
|
|
},
|
|
HistoryMissionCnt = (uint)WasherMissionIds.Length,
|
|
HistoryMissionMoney = 0,
|
|
TodayMissionMoney = 0,
|
|
CurRandomCfgId = 1,
|
|
};
|
|
|
|
// MissionDic / RandomMissionHistory carry the same rows: the panel iterates MissionDic to
|
|
// list today's jobs, so leaving it empty reproduces the empty shell exactly.
|
|
foreach (var id in WasherMissionIds)
|
|
{
|
|
result.MissionDic[id] = new GameMethods.WasherRandomMissionId { Id = id, RandomCfgId = 1 };
|
|
result.RandomMissionHistory.Add(new GameMethods.WasherRandomMissionId { Id = id, RandomCfgId = 1 });
|
|
}
|
|
|
|
result.Spirit2HistoryMissionInfo[0] = new GameMethods.WasherSpiritHistoryMissionInfo
|
|
{
|
|
HistoryMissionCnt = (uint)WasherMissionIds.Length,
|
|
HistoryMissionMoney = 0,
|
|
TodayMissionMoney = 0,
|
|
};
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 赛车手 league snapshot, gated by the authored thresholds in
|
|
/// RacingDriverCompetitionGroupConfig: a group opens when it is not hard-locked and the player
|
|
/// has reached both point/star gates. Player progress is zero (nothing is persisted yet), so
|
|
/// only group 1 (C级赛, 0/0) opens and B级赛 renders 未解锁 — which is the truthful state for a
|
|
/// player who has never raced.
|
|
/// </summary>
|
|
private static GameMethods.RacingCompetitionGroupInfoResult BuildRacingCompetitionGroupInfo()
|
|
{
|
|
var result = new GameMethods.RacingCompetitionGroupInfoResult
|
|
{
|
|
CompetitionPoint = 0,
|
|
CompetitionStar = 0,
|
|
};
|
|
|
|
if (RacingCatalog4229938.LoadError is not null)
|
|
return result;
|
|
|
|
const uint playerPoint = 0;
|
|
const uint playerStar = 0;
|
|
|
|
foreach (var competition in RacingCatalog4229938.Competitions)
|
|
{
|
|
var anyUnlockedGroup = false;
|
|
foreach (var groupId in competition.GroupIds)
|
|
{
|
|
if (!RacingCatalog4229938.Groups.TryGetValue(groupId, out var group))
|
|
continue;
|
|
|
|
if (group.IsLocked || playerPoint < group.UnlockPoint || playerStar < group.UnlockStar)
|
|
continue;
|
|
|
|
// Present = "this group is unlocked". The track records stay EMPTY: a record here is
|
|
// read as a finished race, and no race has been finished.
|
|
if (!result.CompetitionGroupInfos.ContainsKey(groupId))
|
|
result.CompetitionGroupInfos[groupId] = new GameMethods.RacingCompetitionGroupInfo();
|
|
|
|
anyUnlockedGroup = true;
|
|
}
|
|
|
|
// A league type is unlocked only while one of its groups is reachable.
|
|
if (anyUnlockedGroup)
|
|
result.UnlockCompetitionTypes.Add(competition.Id);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|