Files
SVSimServer/SVSim.Database/Services/Friend/FriendDtos.cs
gamer147 7a13e99df7 fix(guild): emit is_friend / is_friend_apply in /guild/info members
GuildMemberInfo.cs:48-49 re-reads json["is_friend"] / json["is_friend_apply"]
UNGUARDED on top of the base-class UserInfoBase's guarded read. Our DTO had
them as int? with global WhenWritingNull → keys omitted → LitJson threw
KeyNotFoundException, crashing the client right after guild create.

Flip the fields to non-nullable int with JsonIgnoreCondition.Never so they
always ship, and wire a batched IFriendService.GetFriendRelationsAsync
lookup so the values are actually populated (single Contains-batched query
per relation table).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-04 16:18:22 -04:00

91 lines
2.8 KiB
C#

namespace SVSim.Database.Services.Friend;
/// <summary>
/// One friend in the requested viewer's friend list. Wire shape carries 15 fields;
/// most are cosmetic ints emitted as strings (matches prod). Numeric fields
/// (viewer_id, rank, emblem_id, degree_id) ship as native ints.
/// </summary>
public sealed record FriendEntry(
int ViewerId,
string Name,
string CountryCode,
int Rank,
long EmblemId,
int DegreeId,
string LastPlayTime, // "yyyy-MM-dd HH:mm:ss"
string DeviceType,
string MaxFriend,
string IsReceivedTwoPickMission,
string Birth,
string MissionChangeTime,
string MissionReceiveType,
string IsOfficial,
string IsOfficialMarkDisplayed);
/// <summary>
/// One friend apply (sent or received). Wire field <c>id</c> is the apply's PK.
/// </summary>
public sealed record FriendApplyEntry(
int Id,
int ViewerId, // OTHER viewer's id
string Name,
string CountryCode,
int Rank,
long EmblemId,
int DegreeId,
string LastPlayTime,
string CreateTime,
int MissionType); // 0 when omitted on the wire
/// <summary>
/// One recent-opponent row. <see cref="FriendStatus"/> is computed at read time:
/// 0 = NO_ACTION, 1 = IS_FRIEND, 2 = IS_SEND (caller has outgoing apply),
/// 3 = IS_RECEIVED (caller has incoming apply from opponent).
/// <see cref="FriendApplyId"/> is the relevant apply's PK when status is 2 or 3, else 0.
/// </summary>
public sealed record PlayedTogetherEntry(
int ViewerId,
string Name,
string CountryCode,
int Rank,
long EmblemId,
int DegreeId,
string LastPlayTime,
string PlayedTime,
int FriendStatus,
int FriendApplyId,
int PlayedMode,
int BattleType,
int DeckFormat,
int TwoPickType);
public sealed record FriendInfoResult(
IReadOnlyList<FriendEntry> Friends,
int Count,
int MaxCount);
public sealed record ReceiveApplyInfoResult(
IReadOnlyList<FriendApplyEntry> ReceiveApplies,
int ApproveApplyCount);
public sealed record SendApplyInfoResult(
IReadOnlyList<FriendApplyEntry> SendApplies,
int RemainingApplyCount,
int SendApplyMaxCount);
public sealed record PlayedTogetherResult(
IReadOnlyList<PlayedTogetherEntry> Histories);
/// <summary>Context recorded by <see cref="IPlayedTogetherWriter.RecordAsync"/>.</summary>
public sealed record BattleParticipationContext(
int PlayedMode,
int BattleType,
int DeckFormat,
int TwoPickType);
/// <summary>
/// Caller-relative friend flags for another viewer. Used to badge rows in guild-member,
/// invite-candidate, and join-request lists on the wire (<c>is_friend</c>, <c>is_friend_apply</c>).
/// </summary>
public sealed record FriendRelation(bool IsFriend, bool HasOutgoingApply);