using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Lidgren.Network; using Robust.Shared.Log; using Robust.Shared.Serialization; using Robust.Shared.Utility; namespace Robust.Shared.Network { /// /// Callback for when the string table gets initialized on the client. This is NOT called on the server. /// public delegate void InitCallback(); /// /// Callback for when one or more entries in the string table get updated on the client. /// This is NOT called on the server. /// /// The entries that were updated. public delegate void StringTableUpdateCallback(MsgStringTableEntries.Entry[] entries); /// /// Contains a networked mapping of IDs -> Strings. /// internal sealed class StringTable { /// /// The ID of the packet. /// This packet must have a fixed ID so the system can bootstrap itself. /// private const int StringTablePacketId = 0; private bool _initialized = false; private readonly INetManager _network; private readonly Dictionary _strings; private int _lastStringIndex; private InitCallback? _callback; private StringTableUpdateCallback? _updateCallback; internal Dictionary Strings => _strings; public ISawmill Sawmill = default!; /// /// Default constructor. /// public StringTable(INetManager network) { _network = network; _strings = new Dictionary(); } /// /// The ID of an invalid string. /// public static int InvalidStringId => -1; /// /// Initializes the string table. /// public void Initialize(InitCallback? callback = null, StringTableUpdateCallback? updateCallback = null) { DebugTools.Assert(!_initialized); _callback = callback; _updateCallback = updateCallback; _network.RegisterNetMessage(ReceiveEntries, NetMessageAccept.Client | NetMessageAccept.Handshake); Reset(); _initialized = true; } private void ReceiveEntries(MsgStringTableEntries message) { DebugTools.Assert(_network.IsClient); Sawmill.Info($"Received message name string table."); foreach (var entry in message.Entries) { var id = entry.Id; var str = string.IsNullOrEmpty(entry.String) ? null : entry.String; if (str == null) { _strings.Remove(id); } else { if (TryFindStringId(str, out int oldId)) { if (oldId == id) continue; _strings.Remove(oldId); _strings.Add(id, str); } else { _strings.Add(id, str); } } } if (_callback == null) return; if (_network.IsClient && !_initialized) _callback?.Invoke(); _updateCallback?.Invoke(message.Entries); } /// /// Resets the string table to the state right after calling Initialize(). /// public void Reset() { _strings.Clear(); _initialized = false; // manually register the id on the client so it can bootstrap itself with incoming table entries if (!TryFindStringId(nameof(MsgStringTableEntries), out _)) { _strings.Add(StringTablePacketId, nameof(MsgStringTableEntries)); if (_network.IsClient) { _updateCallback?.Invoke(new [] { new MsgStringTableEntries.Entry { Id = StringTablePacketId, String = nameof(MsgStringTableEntries) } }); } } } /// /// Adds a string to the table. The ID is generated automatically. /// /// The string to add. /// The ID of the added string. public int AddString(string str) { // The client should receive the table from the server, not add their own. if (_network.IsClient) return -1; if (TryFindStringId(str, out int oldId)) return oldId; // no point in storing dupe strings do // find next available key { // the indexer always moves forward, so if a key is deleted the ID is never re-filled. _lastStringIndex++; if (_strings.ContainsKey(_lastStringIndex)) continue; _strings.Add(_lastStringIndex, str); BroadcastTableUpdate(_lastStringIndex, str); return _lastStringIndex; } while (true); } /// /// Adds a string with the given ID. If the string already exists with another ID, /// the existing string will be deleted. /// NOTE: You should be using AddString(), unless you know what you are doing, and /// know how this method can break things. /// /// The ID the string has to use. /// The string to add. /// The ID of the added string. public void AddStringFixed(int id, string str) { DebugTools.Assert(_network != null, "You need to call Initialize."); // The client should receive the table from the server, not add their own. if (_network!.IsClient) return; // remove existing string, if any if (TryFindStringId(str, out int oldId)) if (oldId != id) _strings.Remove(oldId); else return; // same string, no need to do anything. _strings.Add(id, str); BroadcastTableUpdate(id, str); } /// /// Gets the string with the given ID. /// /// THe ID of the string to get. /// The string with the given ID, or null. public string? GetString(int id) { return _strings.TryGetValue(id, out var str) ? str : null; } /// /// Tries to get the string with the given ID. /// /// The ID of the string. /// The string with the ID. /// True if the table contains the ID, false if it does not. public bool TryGetString(int id, [NotNullWhen(true)] out string? str) { return _strings.TryGetValue(id, out str); } /// /// Tries to find the ID of the given string. /// /// The string to find. /// The found ID of the string. /// True if the table contains the string, false if it does not. public bool TryFindStringId(string str, out int id) { // AddString needs to guarantee there are no duplicate strings. foreach (var kvs in _strings) { if (kvs.Value != str) continue; id = kvs.Key; return true; } id = 0; return false; } private void BroadcastTableUpdate(int id, string str) { if (_network.IsClient) return; if (!_network.IsRunning) return; var message = new MsgStringTableEntries(); message.Entries = new MsgStringTableEntries.Entry[1]; message.Entries[0].Id = id; message.Entries[0].String = str; _network.ServerSendToAll(message); } /// /// Sends the full table to a channel. /// /// The channel that will receive the table. public void SendFullTable(INetChannel channel) { if (_network.IsClient) return; var message = new MsgStringTableEntries(); var count = _strings.Count; message.Entries = new MsgStringTableEntries.Entry[count]; var i = 0; foreach (var kvEntries in _strings) { message.Entries[i].Id = kvEntries.Key; message.Entries[i].String = kvEntries.Value; i++; } Sawmill.Info($"Sending message name string table to {channel.RemoteEndPoint.Address}."); _network.ServerSendMessage(message, channel); } } /// /// A net message for transmitting a string table entry to clients. /// public sealed class MsgStringTableEntries : NetMessage { public override MsgGroups MsgGroup => MsgGroups.String; public Entry[] Entries { get; set; } = default!; /// /// A string table entry. /// public struct Entry { /// /// The string contained inside of the message. /// public string String { get; set; } /// /// The ID of the string inside of the message. /// public int Id { get; set; } } /// public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { var count = buffer.ReadUInt32(); Entries = new Entry[count]; for (var i = 0; i < count; i++) { Entries[i].Id = buffer.ReadVariableInt32(); Entries[i].String = buffer.ReadString(); } } /// public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) { if (Entries == null) throw new InvalidOperationException("Entries is null!"); buffer.Write(Entries.Length); foreach (var entry in Entries) { buffer.WriteVariableInt32(entry.Id); buffer.Write(entry.String); } } } }