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

173 lines
7.9 KiB
C#

using Ananta.SDK.Network;
using Ananta.SDK.Rpc;
using Ananta.SDK.Serialization;
using Ananta.Server.ClientData.Client4229938;
using Ananta.Server.Protocol.Client4229938;
using Auto = Ananta.Server.RpcTypes.Client4229938.Auto;
using GameMethods = Ananta.Server.RpcTypes.Client4229938.Methods.Game;
namespace Ananta.Server.Handlers.Game;
/// <summary>
/// Phone contacts app ("NETWORK OF CONTACTS"). The list is push-only: the login snapshot
/// seeds the two summon numbers, and AddSpiritPhoneInfos refreshes each character's book.
/// This ports PrivateServer1's handbook push (GameRouter.Baseline.cs PushSpiritHandbookAsync)
/// and adds the C2S add/edit/delete actions as a sandbox (session-side list, re-pushed).
/// City contacts bind REAL agent-catalog NPC names so the app lists actual city residents.
/// </summary>
internal sealed partial class GameRouter
{
private const string PhoneContactsKey = "phoneContacts";
private static (string Remark, string Number)[] SummonContacts { get; } =
[
(string.Empty, "142857"), // "Call vehicle" quick summon
(string.Empty, "206337"), // "Express Depot"
];
/// <summary>Stable per-session contact book, seeded with summon + city NPC contacts.</summary>
private static Dictionary<uint, List<Auto.PhoneContact>> ContactsOf(TcpSession session)
{
if (session.Items.TryGetValue(PhoneContactsKey, out var raw) && raw is Dictionary<uint, List<Auto.PhoneContact>> dict)
return dict;
dict = [];
var city = CityContacts();
foreach (var character in ClientConfigRepository.Characters())
dict[character.TemplateId] = SummonContacts
.Select(c => new Auto.PhoneContact { Remark = c.Remark, PhoneNumber = c.Number })
.Concat(city)
.ToList();
session.Items[PhoneContactsKey] = dict;
return dict;
}
/// <summary>Real NPC names from the AgentConfig catalog become callable city contacts.</summary>
private static List<Auto.PhoneContact> CityContacts()
{
var result = new List<Auto.PhoneContact>();
var picked = 0;
foreach (var npc in NpcCatalog4229938.All)
{
if (npc.Category != "citizen" || npc.GeneralModelId == 0 || string.IsNullOrWhiteSpace(npc.Name))
continue;
var name = NameTranslator.Agent(npc.Name);
if (string.IsNullOrWhiteSpace(name) || name == npc.Name && ContainsCjk(name))
continue; // keep untranslated CN names out of the English book
result.Add(new Auto.PhoneContact { Remark = name, PhoneNumber = $"110{npc.Id % 100000000:D8}" });
if (++picked >= 24)
break;
}
return result;
}
private static bool ContainsCjk(string text)
=> text.Any(c => c is >= '\u4E00' and <= '\u9FFF');
/// <summary>Pushes every character's contact book (call after SyncPlayerInfo at world entry).</summary>
internal static async Task PushSpiritHandbookAsync(TcpSession session, CancellationToken token = default)
{
var contacts = ContactsOf(session);
foreach (var character in ClientConfigRepository.Characters())
{
var list = contacts.TryGetValue(character.TemplateId, out var l) ? l : [];
await session.NotifyAsync(MethodId.AddSpiritPhoneInfos, UxSerializer.Serialize(
new GameMethods.AddSpiritPhoneInfos4229938
{
spiritId = character.TemplateId,
phoneInfos = new Auto.PhoneInfos
{
ContactList = list,
ContactGroupList = [],
CallRecordList = [],
ContactOutgoingCallTimesDict = [],
},
}), token);
}
session.Log.Info($"[HANDBOOK] contacts pushed spirits={contacts.Count} city={contacts.Values.FirstOrDefault()?.Count ?? 0}");
}
// ---------------- client-driven phone contact actions (sandbox) ----------------
[Handler(MethodId.AskPhoneAddContact, HandlerPacketKind.Invoke)]
private async Task PhoneAddContact(Connection conn, UxRpcMessage msg)
{
var args = msg.GetArgs<GameMethods.AskPhoneAddContact4229938>();
await conn.ReturnEmptyOkAsync(msg);
var book = ContactsOf(conn.Session);
if (!book.TryGetValue(args.spiritid, out var list))
book[args.spiritid] = list = [];
if (list.All(c => c.PhoneNumber != args.phonenumber))
{
list.Add(new Auto.PhoneContact { Remark = args.contactname, PhoneNumber = args.phonenumber });
conn.Log.Info($"[PHONE] add contact spirit={args.spiritid} '{args.contactname}' {args.phonenumber}");
await PushSpiritBookAsync(conn.Session, args.spiritid);
}
}
[Handler(MethodId.AskPhoneEditContact, HandlerPacketKind.Invoke)]
private async Task PhoneEditContact(Connection conn, UxRpcMessage msg)
{
var args = msg.GetArgs<GameMethods.AskPhoneEditContact4229938>();
await conn.ReturnEmptyOkAsync(msg);
var book = ContactsOf(conn.Session);
if (book.TryGetValue(args.spiritid, out var list))
{
var contact = list.FirstOrDefault(c => c.PhoneNumber == args.oldphonenumber);
if (contact is not null)
{
contact.PhoneNumber = args.newphonenumber;
contact.Remark = args.contactname;
conn.Log.Info($"[PHONE] edit contact spirit={args.spiritid} '{args.contactname}' {args.oldphonenumber}->{args.newphonenumber}");
await PushSpiritBookAsync(conn.Session, args.spiritid);
}
}
}
[Handler(MethodId.AskPhoneDeleteContact, HandlerPacketKind.Invoke)]
private async Task PhoneDeleteContact(Connection conn, UxRpcMessage msg)
{
var args = msg.GetArgs<GameMethods.AskPhoneDeleteContact4229938>();
await conn.ReturnEmptyOkAsync(msg);
var book = ContactsOf(conn.Session);
if (book.TryGetValue(args.spiritid, out var list) && list.RemoveAll(c => c.PhoneNumber == args.phonenumber) > 0)
{
conn.Log.Info($"[PHONE] delete contact spirit={args.spiritid} {args.phonenumber}");
await PushSpiritBookAsync(conn.Session, args.spiritid);
}
}
// Group management and contact options stay accepted-but-static in the sandbox: the
// client keeps its own optimistic UI state and the server list stays the source of truth.
[Handler(MethodId.AskPhoneAddContactGroup, HandlerPacketKind.Invoke)]
private Task PhoneAddContactGroup(Connection conn, UxRpcMessage msg) => conn.ReturnEmptyOkAsync(msg);
[Handler(MethodId.AskPhoneEditContactGroup, HandlerPacketKind.Invoke)]
private Task PhoneEditContactGroup(Connection conn, UxRpcMessage msg) => conn.ReturnEmptyOkAsync(msg);
[Handler(MethodId.AskPhoneDeleteContactGroup, HandlerPacketKind.Invoke)]
private Task PhoneDeleteContactGroup(Connection conn, UxRpcMessage msg) => conn.ReturnEmptyOkAsync(msg);
[Handler(MethodId.AskPhoneAddContactToGroup, HandlerPacketKind.Invoke)]
private Task PhoneAddContactToGroup(Connection conn, UxRpcMessage msg) => conn.ReturnEmptyOkAsync(msg);
// Handled in GameRouter.Phone.cs with full call logging and option routing
private async Task PushSpiritBookAsync(TcpSession session, uint spiritId, CancellationToken token = default)
{
var contacts = ContactsOf(session);
var list = contacts.TryGetValue(spiritId, out var l) ? l : [];
await session.NotifyAsync(MethodId.AddSpiritPhoneInfos, UxSerializer.Serialize(
new GameMethods.AddSpiritPhoneInfos4229938
{
spiritId = spiritId,
phoneInfos = new Auto.PhoneInfos
{
ContactList = list,
ContactGroupList = [],
CallRecordList = [],
ContactOutgoingCallTimesDict = [],
},
}), token);
}
}