using System;
using System.Collections.Generic;
using Lidgren.Network;
using Robust.Shared.Interfaces.Network;
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();
///
/// Contains a networked mapping of IDs -> Strings.
///
public 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 INetManager _network;
private readonly Dictionary _strings;
private int _lastStringIndex;
private InitCallback _callback;
///
/// Default constructor.
///
public StringTable()
{
_strings = new Dictionary();
}
///
/// The ID of an invalid string.
///
public static int InvalidStringId => -1;
///
/// Initializes the string table.
///
public void Initialize(INetManager network, InitCallback callback = null)
{
DebugTools.Assert(!_initialized);
_callback = callback;
_network = network;
_network.RegisterNetMessage(MsgStringTableEntries.NAME, message =>
{
if (_network.IsServer) // Server does not receive entries from clients.
return;
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();
});
Reset();
}
///
/// 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(MsgStringTableEntries.NAME, out _))
{
_strings.Add(StringTablePacketId, MsgStringTableEntries.NAME);
}
}
///
/// 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 string 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, 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 = _network.CreateNetMessage();
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 = _network.CreateNetMessage();
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++;
}
_network.ServerSendMessage(message, channel);
}
}
///
/// A net message for transmitting a string table entry to clients.
///
public class MsgStringTableEntries : NetMessage
{
#region REQUIRED
public static readonly MsgGroups GROUP = MsgGroups.String;
public static readonly string NAME = nameof(MsgStringTableEntries);
public MsgStringTableEntries(INetChannel channel) : base(NAME, GROUP) { }
#endregion
public Entry[] Entries { get; set; }
///
/// 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)
{
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)
{
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);
}
}
}
}