Files
Ananta-ps/Ananta.Server/Ananta.Handlers/Game/GameRouter.JobApps.cs
T

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;
}
}