173 lines
7.9 KiB
C#
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);
|
|
}
|
|
}
|