Merge branch 'master' of https://github.com/Regalis11/Barotrauma.git
This commit is contained in:
@@ -112,7 +112,7 @@ namespace Barotrauma.Networking
|
||||
set;
|
||||
}
|
||||
|
||||
protected ChatMessage(string senderName, string text, ChatMessageType type, Character sender, Client client, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None)
|
||||
protected ChatMessage(string senderName, string text, ChatMessageType type, Character sender, Client client, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None, Color? textColor = null)
|
||||
{
|
||||
Text = text;
|
||||
Type = type;
|
||||
@@ -122,11 +122,13 @@ namespace Barotrauma.Networking
|
||||
|
||||
SenderName = senderName;
|
||||
ChangeType = changeType;
|
||||
}
|
||||
|
||||
public static ChatMessage Create(string senderName, string text, ChatMessageType type, Character sender, Client client = null, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None)
|
||||
customTextColor = textColor;
|
||||
}
|
||||
|
||||
public static ChatMessage Create(string senderName, string text, ChatMessageType type, Character sender, Client client = null, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None, Color? textColor = null)
|
||||
{
|
||||
return new ChatMessage(senderName, text, type, sender, client ?? GameMain.NetworkMember?.ConnectedClients?.Find(c => c.Character != null && c.Character == sender), changeType);
|
||||
return new ChatMessage(senderName, text, type, sender, client ?? GameMain.NetworkMember?.ConnectedClients?.Find(c => c.Character != null && c.Character == sender), changeType, textColor);
|
||||
}
|
||||
|
||||
public static string GetChatMessageCommand(string message, out string messageWithoutCommand)
|
||||
@@ -225,27 +227,30 @@ namespace Barotrauma.Networking
|
||||
break;
|
||||
case ChatMessageType.Radio:
|
||||
case ChatMessageType.Order:
|
||||
if (receiver != null && !receiver.IsDead)
|
||||
if (receiver?.Inventory != null && !receiver.IsDead)
|
||||
{
|
||||
var receiverItem = receiver.Inventory?.AllItems.FirstOrDefault(i => i.GetComponent<WifiComponent>() != null);
|
||||
//character doesn't have a radio -> don't send
|
||||
if (receiverItem == null || !receiver.HasEquippedItem(receiverItem)) { return spokenMsg; }
|
||||
|
||||
var senderItem = sender.Inventory?.AllItems.FirstOrDefault(i => i.GetComponent<WifiComponent>() != null);
|
||||
if (senderItem == null || !sender.HasEquippedItem(senderItem)) { return spokenMsg; }
|
||||
|
||||
var receiverRadio = receiverItem.GetComponent<WifiComponent>();
|
||||
var senderRadio = senderItem.GetComponent<WifiComponent>();
|
||||
|
||||
if (!receiverRadio.CanReceive(senderRadio)) { return spokenMsg; }
|
||||
|
||||
string msg = ApplyDistanceEffect(receiverItem, senderItem, message, senderRadio.Range);
|
||||
if (sender.SpeechImpediment > 0.0f)
|
||||
foreach (Item receiverItem in receiver.Inventory.AllItems.Where(i => i.GetComponent<WifiComponent>()?.LinkToChat ?? false))
|
||||
{
|
||||
//speech impediment doesn't reduce the range when using a radio, but adds extra garbling
|
||||
msg = ApplyDistanceEffect(msg, sender.SpeechImpediment / 100.0f);
|
||||
if (sender.Inventory == null || !receiver.HasEquippedItem(receiverItem)) { continue; }
|
||||
|
||||
foreach (Item senderItem in sender.Inventory.AllItems.Where(i => i.GetComponent<WifiComponent>()?.LinkToChat ?? false))
|
||||
{
|
||||
if (!sender.HasEquippedItem(senderItem)) { continue; }
|
||||
|
||||
var receiverRadio = receiverItem.GetComponent<WifiComponent>();
|
||||
var senderRadio = senderItem.GetComponent<WifiComponent>();
|
||||
if (!receiverRadio.CanReceive(senderRadio)) { continue; }
|
||||
|
||||
string msg = ApplyDistanceEffect(receiverItem, senderItem, message, senderRadio.Range);
|
||||
if (sender.SpeechImpediment > 0.0f)
|
||||
{
|
||||
//speech impediment doesn't reduce the range when using a radio, but adds extra garbling
|
||||
msg = ApplyDistanceEffect(msg, sender.SpeechImpediment / 100.0f);
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
return msg;
|
||||
return spokenMsg;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -275,7 +280,7 @@ namespace Barotrauma.Networking
|
||||
foreach (Item item in sender.Inventory.AllItems)
|
||||
{
|
||||
var wifiComponent = item.GetComponent<WifiComponent>();
|
||||
if (wifiComponent == null || !wifiComponent.CanTransmit() || !sender.HasEquippedItem(item)) { continue; }
|
||||
if (wifiComponent == null || !wifiComponent.LinkToChat || !wifiComponent.CanTransmit() || !sender.HasEquippedItem(item)) { continue; }
|
||||
if (radio == null || wifiComponent.Range > radio.Range)
|
||||
{
|
||||
radio = wifiComponent;
|
||||
|
||||
@@ -22,7 +22,11 @@ namespace Barotrauma.Networking
|
||||
ManageSettings = 0x200,
|
||||
ManagePermissions = 0x400,
|
||||
KarmaImmunity = 0x800,
|
||||
All = 0xFFF
|
||||
BuyItems = 0x1000,
|
||||
SellInventoryItems = 0x2000,
|
||||
SellSubItems = 0x4000,
|
||||
CampaignStore = 0x8000,
|
||||
All = 0xFFFF
|
||||
}
|
||||
|
||||
class PermissionPreset
|
||||
|
||||
@@ -0,0 +1,547 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
/// <summary>
|
||||
/// Marks fields and properties as to be serialized and deserialized by <see cref="INetSerializableStruct"/>.
|
||||
/// Also contains settings for some types like maximum and minimum values for numbers to reduce bits used.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// struct NetPurchasedItem : INetSerializableStruct
|
||||
/// {
|
||||
/// [NetworkSerialize]
|
||||
/// public string Identifier;
|
||||
///
|
||||
/// [NetworkSerialize(ArrayMaxSize = 16)]
|
||||
/// public string[] Tags;
|
||||
///
|
||||
/// [NetworkSerialize(MinValueInt = 0, MaxValueInt = 8)]
|
||||
/// public int Amount;
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
/// <remarks>
|
||||
/// Using the attribute on the struct will make all fields and properties serialized
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Struct | AttributeTargets.Property)]
|
||||
public class NetworkSerialize : Attribute
|
||||
{
|
||||
public int MaxValueInt = int.MaxValue;
|
||||
public int MinValueInt = int.MinValue;
|
||||
public float MaxValueFloat = float.MaxValue;
|
||||
public float MinValueFloat = float.MinValue;
|
||||
public int NumberOfBits = 8;
|
||||
public bool IncludeColorAlpha = false;
|
||||
public int ArrayMaxSize = ushort.MaxValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Static class that contains serialize and deserialize functions for different types used in <see cref="INetSerializableStruct"/>
|
||||
/// </summary>
|
||||
public static class NetSerializableProperties
|
||||
{
|
||||
public readonly struct ReadWriteBehavior
|
||||
{
|
||||
public delegate dynamic? ReadDelegate(IReadMessage inc, Type type, NetworkSerialize attribute);
|
||||
public delegate void WriteDelegate(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg);
|
||||
|
||||
public readonly ReadDelegate ReadAction;
|
||||
public readonly WriteDelegate WriteAction;
|
||||
|
||||
public ReadWriteBehavior(ReadDelegate readAction, WriteDelegate writeAction)
|
||||
{
|
||||
ReadAction = readAction;
|
||||
WriteAction = writeAction;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly ImmutableDictionary<Type, ReadWriteBehavior> TypeBehaviors = new Dictionary<Type, ReadWriteBehavior>
|
||||
{
|
||||
{ typeof(Boolean), new ReadWriteBehavior(ReadBoolean, WriteDynamic) },
|
||||
{ typeof(Byte), new ReadWriteBehavior(ReadByte, WriteDynamic) },
|
||||
{ typeof(UInt16), new ReadWriteBehavior(ReadUInt16, WriteDynamic) },
|
||||
{ typeof(Int16), new ReadWriteBehavior(ReadInt16, WriteDynamic) },
|
||||
{ typeof(UInt32), new ReadWriteBehavior(ReadUInt32, WriteDynamic) },
|
||||
{ typeof(Int32), new ReadWriteBehavior(ReadInt32, WriteInt32) },
|
||||
{ typeof(UInt64), new ReadWriteBehavior(ReadUInt64, WriteDynamic) },
|
||||
{ typeof(Int64), new ReadWriteBehavior(ReadInt64, WriteDynamic) },
|
||||
{ typeof(Single), new ReadWriteBehavior(ReadSingle, WriteSingle) },
|
||||
{ typeof(Double), new ReadWriteBehavior(ReadDouble, WriteDynamic) },
|
||||
{ typeof(String), new ReadWriteBehavior(ReadString, WriteDynamic) },
|
||||
{ typeof(Color), new ReadWriteBehavior(ReadColor, WriteColor) },
|
||||
{ typeof(Vector2), new ReadWriteBehavior(ReadVector2, WriteVector2) }
|
||||
}.ToImmutableDictionary();
|
||||
|
||||
private static readonly ReadWriteBehavior InvalidReadWriteBehavior = new ReadWriteBehavior(ReadInvalid, WriteInvalid);
|
||||
|
||||
private static readonly ImmutableDictionary<Predicate<Type>, ReadWriteBehavior> TypePredicates = new Dictionary<Predicate<Type>, ReadWriteBehavior>
|
||||
{
|
||||
// Arrays
|
||||
{ type => type.BaseType?.IsAssignableFrom(typeof(Array)) ?? false, new ReadWriteBehavior(ReadArray, WriteArray) },
|
||||
|
||||
// Nested INetSerializableStructs
|
||||
{ type => typeof(INetSerializableStruct).IsAssignableFrom(type), new ReadWriteBehavior(ReadINetSerializableStruct, WriteINetSerializableStruct) },
|
||||
|
||||
// Enums
|
||||
{ type => type.IsEnum, new ReadWriteBehavior(ReadEnum, WriteEnum) },
|
||||
|
||||
// Nullable / Optional types
|
||||
{ type => Nullable.GetUnderlyingType(type) != null, new ReadWriteBehavior(ReadNullable, WriteNullable) }
|
||||
}.ToImmutableDictionary();
|
||||
|
||||
private static void WriteInvalid(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg) => throw new InvalidOperationException($"Type {obj?.GetType()} cannot be serialized. Did you forget to implement INetSerializableStruct?");
|
||||
|
||||
private static dynamic ReadInvalid(IReadMessage inc, Type type, NetworkSerialize attribute) => throw new InvalidOperationException($"Type {type} cannot be deserialized. Did you forget to implement INetSerializableStruct?");
|
||||
|
||||
private static void WriteNullable(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg)
|
||||
{
|
||||
if (obj is { } notNull)
|
||||
{
|
||||
msg.Write(true);
|
||||
|
||||
if (TryFindBehavior(notNull.GetType(), out ReadWriteBehavior behavior))
|
||||
{
|
||||
// uh oh, something terrible has happened!
|
||||
if (behavior.WriteAction == WriteNullable) { behavior = InvalidReadWriteBehavior; }
|
||||
|
||||
behavior.WriteAction(notNull, attribute, msg);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
msg.Write(false);
|
||||
}
|
||||
|
||||
private static dynamic? ReadNullable(IReadMessage inc, Type type, NetworkSerialize attribute)
|
||||
{
|
||||
if (!inc.ReadBoolean()) { return null; }
|
||||
|
||||
Type? underlyingType = Nullable.GetUnderlyingType(type);
|
||||
if (underlyingType is null) { throw new InvalidOperationException($"Could not get the underlying type of {type} in {nameof(ReadNullable)}"); }
|
||||
|
||||
if (TryFindBehavior(underlyingType, out ReadWriteBehavior behavior))
|
||||
{
|
||||
// uh oh, something terrible has happened!
|
||||
if (behavior.ReadAction == ReadNullable) { behavior = InvalidReadWriteBehavior; }
|
||||
|
||||
return behavior.ReadAction(inc, underlyingType, attribute);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Could not find suitable behavior for type {underlyingType} in {nameof(ReadNullable)}");
|
||||
}
|
||||
|
||||
private static void WriteEnum(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg)
|
||||
{
|
||||
if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); }
|
||||
|
||||
Range<int> range = GetEnumRange(obj.GetType());
|
||||
msg.WriteRangedInteger(Convert.ChangeType(obj, obj.GetTypeCode()), range.Start, range.End);
|
||||
}
|
||||
|
||||
private static dynamic ReadEnum(IReadMessage inc, Type type, NetworkSerialize attribute)
|
||||
{
|
||||
Range<int> range = GetEnumRange(type);
|
||||
int enumIndex = inc.ReadRangedInteger(range.Start, range.End);
|
||||
|
||||
foreach (dynamic e in Enum.GetValues(type))
|
||||
{
|
||||
if (Convert.ChangeType(e, e.GetTypeCode()) == enumIndex) { return e; }
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"An enum {type} with value {enumIndex} could not be found in {nameof(ReadEnum)}");
|
||||
}
|
||||
|
||||
private static void WriteINetSerializableStruct(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg)
|
||||
{
|
||||
if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); }
|
||||
|
||||
if (!(obj is INetSerializableStruct serializableStruct)) { throw new InvalidOperationException($"Object in {nameof(WriteINetSerializableStruct)} was {obj.GetType()} but expected {nameof(INetSerializableStruct)}"); }
|
||||
|
||||
serializableStruct.Write(msg);
|
||||
}
|
||||
|
||||
private static dynamic ReadINetSerializableStruct(IReadMessage inc, Type type, NetworkSerialize attribute)
|
||||
{
|
||||
return INetSerializableStruct.ReadDynamic(type, inc);
|
||||
}
|
||||
|
||||
private static void WriteDynamic(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg)
|
||||
{
|
||||
if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); }
|
||||
|
||||
msg.Write(obj);
|
||||
}
|
||||
|
||||
private static dynamic ReadArray(IReadMessage inc, Type type, NetworkSerialize attribute)
|
||||
{
|
||||
Type? elementType = type.GetElementType();
|
||||
if (elementType is null) { throw new InvalidOperationException($"Could not get the element type of {type} in {nameof(ReadArray)}"); }
|
||||
|
||||
int length = inc.ReadRangedInteger(0, attribute.ArrayMaxSize);
|
||||
|
||||
Array list = Array.CreateInstance(elementType, length);
|
||||
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
if (TryFindBehavior(elementType, out ReadWriteBehavior behavior))
|
||||
{
|
||||
list.SetValue(behavior.ReadAction(inc, elementType, attribute), i);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Could not find suitable behavior for type {elementType} in {nameof(ReadArray)}");
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private static void WriteArray(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg)
|
||||
{
|
||||
if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); }
|
||||
|
||||
if (!(obj is Array array)) { throw new InvalidOperationException($"Object in {nameof(WriteArray)} was {obj.GetType()} but expected {nameof(Array)}"); }
|
||||
|
||||
msg.WriteRangedInteger(array.Length, 0, attribute.ArrayMaxSize);
|
||||
|
||||
foreach (dynamic o in array)
|
||||
{
|
||||
if (TryFindBehavior(o.GetType(), out ReadWriteBehavior behavior))
|
||||
{
|
||||
behavior.WriteAction(o, attribute, msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static dynamic ReadBoolean(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadBoolean();
|
||||
|
||||
private static dynamic ReadByte(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadByte();
|
||||
|
||||
private static dynamic ReadUInt16(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadUInt16();
|
||||
|
||||
private static dynamic ReadInt16(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadInt16();
|
||||
|
||||
private static dynamic ReadUInt32(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadUInt32();
|
||||
|
||||
private static dynamic ReadInt32(IReadMessage inc, Type type, NetworkSerialize attribute)
|
||||
{
|
||||
if (IsRanged(attribute.MinValueInt, attribute.MaxValueInt))
|
||||
{
|
||||
return inc.ReadRangedInteger(attribute.MinValueInt, attribute.MaxValueInt);
|
||||
}
|
||||
|
||||
return inc.ReadInt32();
|
||||
}
|
||||
|
||||
private static void WriteInt32(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg)
|
||||
{
|
||||
if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); }
|
||||
|
||||
if (IsRanged(attribute.MinValueInt, attribute.MaxValueInt))
|
||||
{
|
||||
msg.WriteRangedInteger(obj, attribute.MinValueInt, attribute.MaxValueInt);
|
||||
return;
|
||||
}
|
||||
|
||||
msg.Write(obj);
|
||||
}
|
||||
|
||||
private static dynamic ReadUInt64(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadUInt64();
|
||||
|
||||
private static dynamic ReadInt64(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadInt64();
|
||||
|
||||
private static dynamic ReadSingle(IReadMessage inc, Type type, NetworkSerialize attribute)
|
||||
{
|
||||
if (IsRanged(attribute.MinValueFloat, attribute.MaxValueFloat))
|
||||
{
|
||||
return inc.ReadRangedSingle(attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits);
|
||||
}
|
||||
|
||||
return inc.ReadSingle();
|
||||
}
|
||||
|
||||
private static void WriteSingle(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg)
|
||||
{
|
||||
if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); }
|
||||
|
||||
if (IsRanged(attribute.MinValueFloat, attribute.MaxValueFloat))
|
||||
{
|
||||
msg.WriteRangedSingle(obj, attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits);
|
||||
return;
|
||||
}
|
||||
|
||||
msg.Write(obj);
|
||||
}
|
||||
|
||||
private static dynamic ReadDouble(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadDouble();
|
||||
|
||||
private static dynamic ReadString(IReadMessage inc, Type type, NetworkSerialize attribute) => inc.ReadString();
|
||||
|
||||
private static dynamic ReadColor(IReadMessage inc, Type type, NetworkSerialize attribute) => attribute.IncludeColorAlpha ? inc.ReadColorR8G8B8A8() : inc.ReadColorR8G8B8();
|
||||
|
||||
private static void WriteColor(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg)
|
||||
{
|
||||
if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); }
|
||||
|
||||
if (attribute.IncludeColorAlpha)
|
||||
{
|
||||
msg.WriteColorR8G8B8A8(obj);
|
||||
return;
|
||||
}
|
||||
|
||||
msg.WriteColorR8G8B8(obj);
|
||||
}
|
||||
|
||||
private static dynamic ReadVector2(IReadMessage inc, Type type, NetworkSerialize attribute)
|
||||
{
|
||||
float x;
|
||||
float y;
|
||||
|
||||
if (IsRanged(attribute.MinValueFloat, attribute.MaxValueFloat))
|
||||
{
|
||||
x = inc.ReadRangedSingle(attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits);
|
||||
y = inc.ReadRangedSingle(attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits);
|
||||
}
|
||||
else
|
||||
{
|
||||
x = inc.ReadSingle();
|
||||
y = inc.ReadSingle();
|
||||
}
|
||||
|
||||
return new Vector2(x, y);
|
||||
}
|
||||
|
||||
private static void WriteVector2(dynamic? obj, NetworkSerialize attribute, IWriteMessage msg)
|
||||
{
|
||||
if (obj is null) { throw new ArgumentNullException(nameof(obj), "Tried to write 'null' into a non-nullable type"); }
|
||||
|
||||
var (x, y) = (Vector2)obj;
|
||||
if (IsRanged(attribute.MinValueFloat, attribute.MaxValueFloat))
|
||||
{
|
||||
msg.WriteRangedSingle(x, attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits);
|
||||
msg.WriteRangedSingle(y, attribute.MinValueFloat, attribute.MaxValueFloat, attribute.NumberOfBits);
|
||||
return;
|
||||
}
|
||||
|
||||
msg.Write(x);
|
||||
msg.Write(y);
|
||||
}
|
||||
|
||||
private static bool IsRanged(float minValue, float maxValue) => minValue > float.MinValue || maxValue < float.MaxValue;
|
||||
private static bool IsRanged(int minValue, int maxValue) => minValue > int.MinValue || maxValue < int.MaxValue;
|
||||
|
||||
private static Range<int> GetEnumRange(Type type)
|
||||
{
|
||||
ImmutableArray<int> values = Enum.GetValues(type).Cast<int>().ToImmutableArray();
|
||||
return new Range<int>(values.Min(), values.Max());
|
||||
}
|
||||
|
||||
public static bool TryFindBehavior(Type type, out ReadWriteBehavior behavior)
|
||||
{
|
||||
if (TypeBehaviors.TryGetValue(type, out behavior)) { return true; }
|
||||
|
||||
foreach (var (predicate, behavior2) in TypePredicates)
|
||||
{
|
||||
if (predicate(type))
|
||||
{
|
||||
behavior = behavior2;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
behavior = InvalidReadWriteBehavior;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface that allows the creation of automatically serializable and deserializable structs.
|
||||
/// <br/><br/>
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// public enum PurchaseResult
|
||||
/// {
|
||||
/// Unknown,
|
||||
/// Completed,
|
||||
/// Declined
|
||||
/// }
|
||||
///
|
||||
/// [NetworkSerialize]
|
||||
/// struct NetStoreTransaction : INetSerializableStruct
|
||||
/// {
|
||||
/// public long Timestamp { get; set; }
|
||||
/// public PurchaseResult Result { get; set; }
|
||||
/// public NetPurchasedItem? PurchasedItem { get; set; }
|
||||
/// }
|
||||
///
|
||||
/// [NetworkSerialize]
|
||||
/// struct NetPurchasedItem : INetSerializableStruct
|
||||
/// {
|
||||
/// public string Identifier;
|
||||
/// public string[] Tags;
|
||||
/// public int Amount;
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
/// <remarks>
|
||||
/// Supported types are:<br/>
|
||||
/// <see cref="Boolean">bool</see><br/>
|
||||
/// <see cref="Byte">byte</see><br/>
|
||||
/// <see cref="UInt16">ushort</see><br/>
|
||||
/// <see cref="Int16">short</see><br/>
|
||||
/// <see cref="UInt32">uint</see><br/>
|
||||
/// <see cref="Int32">int</see><br/>
|
||||
/// <see cref="UInt64">ulong</see><br/>
|
||||
/// <see cref="Int64">long</see><br/>
|
||||
/// <see cref="Single">float</see><br/>
|
||||
/// <see cref="Double">double</see><br/>
|
||||
/// <see cref="String">string</see><br/>
|
||||
/// <see cref="Microsoft.Xna.Framework.Color"/><br/>
|
||||
/// <see cref="Microsoft.Xna.Framework.Vector2"/><br/>
|
||||
/// In addition arrays, enums and <see cref="Nullable{T}"/> are supported.<br/>
|
||||
/// Using <see cref="Nullable{T}"/> will make the field or property optional
|
||||
/// </remarks>
|
||||
/// <seealso cref="NetworkSerialize"/>
|
||||
public interface INetSerializableStruct
|
||||
{
|
||||
/// <summary>
|
||||
/// Deserializes a network message into a struct.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// public void ClientRead(IReadMessage inc)
|
||||
/// {
|
||||
/// NetStoreTransaction transaction = INetSerializableStruct.Read<NetStoreTransaction>(inc);
|
||||
/// if (transaction.Result == PurchaseResult.Declined)
|
||||
/// {
|
||||
/// Console.WriteLine("Purchase declined!");
|
||||
/// return;
|
||||
/// }
|
||||
///
|
||||
/// if (transaction.PurchasedItem is { } item)
|
||||
/// {
|
||||
/// // Purchased 3x Wrench with tags: smallitem, mechanical, tool
|
||||
/// Console.WriteLine($"Purchased {item.Amount}x {item.Identifier} with tags: {string.Join(", ", item.Tags)}");
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
/// <param name="inc">Incoming network message</param>
|
||||
/// <typeparam name="T">Type of the struct that implements <see cref="INetSerializableStruct"/></typeparam>
|
||||
/// <returns>A new struct of type T with fields and properties deserialized</returns>
|
||||
public static T Read<T>(IReadMessage inc) where T : INetSerializableStruct => (T)ReadDynamic(typeof(T), inc);
|
||||
|
||||
public static dynamic ReadDynamic(Type type, IReadMessage inc)
|
||||
{
|
||||
object? newObject = Activator.CreateInstance(type);
|
||||
if (newObject is null) { return default!; }
|
||||
|
||||
PropertyInfo[] properties = type.GetProperties();
|
||||
foreach (PropertyInfo info in properties)
|
||||
{
|
||||
NetworkSerialize? attribute = GetAttribute(info, newObject);
|
||||
if (attribute is null) { continue; }
|
||||
|
||||
if (NetSerializableProperties.TryFindBehavior(info.PropertyType, out var behavior))
|
||||
{
|
||||
object? value = behavior.ReadAction(inc, info.PropertyType, attribute);
|
||||
info.SetValue(newObject, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError($"Unsupported property type \"{info.PropertyType}\" in {newObject}!");
|
||||
}
|
||||
}
|
||||
|
||||
FieldInfo[] fields = type.GetFields();
|
||||
foreach (FieldInfo info in fields)
|
||||
{
|
||||
NetworkSerialize? attribute = GetAttribute(info, newObject);
|
||||
if (attribute is null) { continue; }
|
||||
|
||||
if (NetSerializableProperties.TryFindBehavior(info.FieldType, out var behavior))
|
||||
{
|
||||
object? value = behavior.ReadAction(inc, info.FieldType, attribute);
|
||||
info.SetValue(newObject, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError($"Unsupported field type \"{info.FieldType}\" in {newObject}!");
|
||||
}
|
||||
}
|
||||
|
||||
return newObject;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the struct into a network message
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// public void ServerWrite(IWriteMessage msg)
|
||||
/// {
|
||||
/// INetSerializableStruct transaction = new NetStoreTransaction
|
||||
/// {
|
||||
/// Result = PurchaseResult.Completed,
|
||||
/// Timestamp = DateTimeOffset.Now.ToUnixTimeSeconds(),
|
||||
/// PurchasedItem = new NetPurchasedItem
|
||||
/// {
|
||||
/// Identifier = "Wrench",
|
||||
/// Amount = 3,
|
||||
/// Tags = new []{ "smallitem", "mechanical", "tool" }
|
||||
/// }
|
||||
/// };
|
||||
///
|
||||
/// transaction.Write(msg);
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
/// </summary>
|
||||
/// <param name="msg">Outgoing network message</param>
|
||||
public void Write(IWriteMessage msg)
|
||||
{
|
||||
PropertyInfo[] properties = GetType().GetProperties();
|
||||
foreach (PropertyInfo info in properties)
|
||||
{
|
||||
NetworkSerialize? attribute = GetAttribute(info, this);
|
||||
if (attribute is null) { continue; }
|
||||
|
||||
if (NetSerializableProperties.TryFindBehavior(info.PropertyType, out var behavior))
|
||||
{
|
||||
behavior.WriteAction(info.GetValue(this), attribute, msg);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Unsupported property type \"{info.PropertyType}\" in {this}");
|
||||
}
|
||||
}
|
||||
|
||||
FieldInfo[] fields = GetType().GetFields();
|
||||
foreach (FieldInfo info in fields)
|
||||
{
|
||||
NetworkSerialize? attribute = GetAttribute(info, this);
|
||||
if (attribute is null) { continue; }
|
||||
|
||||
if (NetSerializableProperties.TryFindBehavior(info.FieldType, out var behavior))
|
||||
{
|
||||
behavior.WriteAction(info.GetValue(this), attribute, msg);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Unsupported field type \"{info.FieldType}\" in {this}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static NetworkSerialize? GetAttribute(MemberInfo info, object baseClass) => info.GetCustomAttribute<NetworkSerialize>() ?? baseClass.GetType().GetCustomAttribute<NetworkSerialize>();
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,8 @@ namespace Barotrauma.Networking
|
||||
REQUEST_STARTGAMEFINALIZE, //tell the server you're ready to finalize round initialization
|
||||
|
||||
ERROR, //tell the server that an error occurred
|
||||
CREW,
|
||||
CREW, //hiring UI
|
||||
MEDICAL, //medical clinic
|
||||
READY_CHECK,
|
||||
READY_TO_SPAWN,
|
||||
LUA_NET_MESSAGE
|
||||
@@ -81,6 +82,8 @@ namespace Barotrauma.Networking
|
||||
EVENTACTION,
|
||||
CREW, //anything related to managing bots in multiplayer
|
||||
READY_CHECK, //start, end and update a ready check
|
||||
MEDICAL, //medical clinic
|
||||
|
||||
LUA_NET_MESSAGE
|
||||
}
|
||||
enum ServerNetObject
|
||||
@@ -145,7 +148,9 @@ namespace Barotrauma.Networking
|
||||
NotOnWhitelist,
|
||||
ExcessiveDesyncOldEvent,
|
||||
ExcessiveDesyncRemovedEvent,
|
||||
SyncTimeout
|
||||
SyncTimeout,
|
||||
SteamP2PError,
|
||||
SteamP2PTimeOut,
|
||||
}
|
||||
|
||||
abstract partial class NetworkMember
|
||||
@@ -231,24 +236,24 @@ namespace Barotrauma.Networking
|
||||
|
||||
var radio = sender.Inventory.AllItems.FirstOrDefault(i => i.GetComponent<WifiComponent>() != null);
|
||||
if (radio == null || !sender.HasEquippedItem(radio)) { return false; }
|
||||
|
||||
|
||||
var radioComponent = radio.GetComponent<WifiComponent>();
|
||||
if (radioComponent == null) { return false; }
|
||||
return radioComponent.HasRequiredContainedItems(sender, addMessage: false);
|
||||
}
|
||||
|
||||
public void AddChatMessage(string message, ChatMessageType type, string senderName = "", Client senderClient = null, Character senderCharacter = null, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None)
|
||||
public void AddChatMessage(string message, ChatMessageType type, string senderName = "", Client senderClient = null, Character senderCharacter = null, PlayerConnectionChangeType changeType = PlayerConnectionChangeType.None, Color? textColor = null)
|
||||
{
|
||||
AddChatMessage(ChatMessage.Create(senderName, message, type, senderCharacter, senderClient, changeType: changeType));
|
||||
AddChatMessage(ChatMessage.Create(senderName, message, type, senderCharacter, senderClient, changeType: changeType, textColor: textColor));
|
||||
}
|
||||
|
||||
public virtual void AddChatMessage(ChatMessage message)
|
||||
{
|
||||
if (string.IsNullOrEmpty(message.Text)) { return; }
|
||||
|
||||
|
||||
if (message.Sender != null && !message.Sender.IsDead)
|
||||
{
|
||||
message.Sender.ShowSpeechBubble(2.0f, ChatMessage.MessageColor[(int)message.Type]);
|
||||
message.Sender.ShowSpeechBubble(2.0f, message.Color);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,18 +23,22 @@ namespace Barotrauma.Networking
|
||||
/// </summary>
|
||||
public int? WallSectionIndex { get; set; }
|
||||
|
||||
public bool IsNewOrder { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Same as calling <see cref="OrderChatMessage.OrderChatMessage(Order, string, int, string, ISpatialEntity, Character, Character)"/>, but the text parameter is set using <see cref="Order.GetChatMessage(string, string, bool, string)"/>
|
||||
/// Same as calling <see cref="OrderChatMessage.OrderChatMessage(Order, string, int, string, ISpatialEntity, Character, Character)"/>,
|
||||
/// but the text parameter is set using <see cref="Order.GetChatMessage(string, string, bool, string)"/>
|
||||
/// </summary>
|
||||
public OrderChatMessage(Order order, string orderOption, int priority, ISpatialEntity targetEntity, Character targetCharacter, Character sender)
|
||||
public OrderChatMessage(Order order, string orderOption, int priority, ISpatialEntity targetEntity, Character targetCharacter, Character sender, bool isNewOrder = true)
|
||||
: this(order, orderOption, priority,
|
||||
order?.GetChatMessage(targetCharacter?.Name, sender?.CurrentHull?.DisplayName, givingOrderToSelf: targetCharacter == sender, orderOption: orderOption, priority: priority),
|
||||
targetEntity, targetCharacter, sender)
|
||||
order?.GetChatMessage(targetCharacter?.Name, sender?.CurrentHull?.DisplayName, targetCharacter == sender, orderOption, isNewOrder),
|
||||
targetEntity, targetCharacter, sender, isNewOrder)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public OrderChatMessage(Order order, string orderOption, int priority, string text, ISpatialEntity targetEntity, Character targetCharacter, Character sender)
|
||||
public OrderChatMessage(Order order, string orderOption, int priority, string text, ISpatialEntity targetEntity,
|
||||
Character targetCharacter, Character sender, bool isNewOrder = true)
|
||||
: base(sender?.Name, text, ChatMessageType.Order, sender, GameMain.NetworkMember.ConnectedClients.Find(c => c.Character == sender))
|
||||
{
|
||||
Order = order;
|
||||
@@ -42,9 +46,11 @@ namespace Barotrauma.Networking
|
||||
OrderPriority = priority;
|
||||
TargetCharacter = targetCharacter;
|
||||
TargetEntity = targetEntity;
|
||||
IsNewOrder = isNewOrder;
|
||||
}
|
||||
|
||||
public static void WriteOrder(IWriteMessage msg, Order order, Character targetCharacter, ISpatialEntity targetEntity, string orderOption, int orderPriority, int? wallSectionIndex)
|
||||
public static void WriteOrder(IWriteMessage msg, Order order, Character targetCharacter, ISpatialEntity targetEntity,
|
||||
string orderOption, int orderPriority, int? wallSectionIndex, bool isNewOrder)
|
||||
{
|
||||
msg.Write((byte)Order.PrefabList.IndexOf(order.Prefab));
|
||||
msg.Write(targetCharacter == null ? (UInt16)0 : targetCharacter.ID);
|
||||
@@ -100,11 +106,13 @@ namespace Barotrauma.Networking
|
||||
msg.Write((byte)(wallSectionIndex ?? order.WallSectionIndex ?? 0));
|
||||
}
|
||||
}
|
||||
|
||||
msg.Write(isNewOrder);
|
||||
}
|
||||
|
||||
private void WriteOrder(IWriteMessage msg)
|
||||
{
|
||||
WriteOrder(msg, Order, TargetCharacter, TargetEntity, OrderOption, OrderPriority, WallSectionIndex);
|
||||
WriteOrder(msg, Order, TargetCharacter, TargetEntity, OrderOption, OrderPriority, WallSectionIndex, IsNewOrder);
|
||||
}
|
||||
|
||||
public struct OrderMessageInfo
|
||||
@@ -119,8 +127,10 @@ namespace Barotrauma.Networking
|
||||
public OrderTarget TargetPosition { get; }
|
||||
public int? WallSectionIndex { get; }
|
||||
public int Priority { get; }
|
||||
public bool IsNewOrder { get; }
|
||||
|
||||
public OrderMessageInfo(int orderIndex, Order orderPrefab, string orderOption, int? orderOptionIndex, Character targetCharacter, Order.OrderTargetType targetType, Entity targetEntity, OrderTarget targetPosition, int? wallSectionIndex, int orderPriority)
|
||||
public OrderMessageInfo(int orderIndex, Order orderPrefab, string orderOption, int? orderOptionIndex, Character targetCharacter,
|
||||
Order.OrderTargetType targetType, Entity targetEntity, OrderTarget targetPosition, int? wallSectionIndex, int orderPriority, bool isNewOrder)
|
||||
{
|
||||
OrderIndex = orderIndex;
|
||||
OrderPrefab = orderPrefab;
|
||||
@@ -132,6 +142,7 @@ namespace Barotrauma.Networking
|
||||
TargetPosition = targetPosition;
|
||||
WallSectionIndex = wallSectionIndex;
|
||||
Priority = orderPriority;
|
||||
IsNewOrder = isNewOrder;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,7 +216,10 @@ namespace Barotrauma.Networking
|
||||
wallSectionIndex = msg.ReadByte();
|
||||
}
|
||||
|
||||
return new OrderMessageInfo(orderIndex, orderPrefab, orderOption, optionIndex, targetCharacter, orderTargetType, targetEntity, orderTargetPosition, wallSectionIndex, orderPriority);
|
||||
bool isNewOrder = msg.ReadBoolean();
|
||||
|
||||
return new OrderMessageInfo(orderIndex, orderPrefab, orderOption, optionIndex, targetCharacter,
|
||||
orderTargetType, targetEntity, orderTargetPosition, wallSectionIndex, orderPriority, isNewOrder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,19 +38,19 @@ namespace Barotrauma.Networking
|
||||
public static class NetworkEnumExtensions
|
||||
{
|
||||
public static bool IsCompressed(this PacketHeader h)
|
||||
=> h.IsBitSet(PacketHeader.IsCompressed);
|
||||
=> h.HasFlag(PacketHeader.IsCompressed);
|
||||
|
||||
public static bool IsConnectionInitializationStep(this PacketHeader h)
|
||||
=> h.IsBitSet(PacketHeader.IsConnectionInitializationStep);
|
||||
=> h.HasFlag(PacketHeader.IsConnectionInitializationStep);
|
||||
|
||||
public static bool IsDisconnectMessage(this PacketHeader h)
|
||||
=> h.IsBitSet(PacketHeader.IsDisconnectMessage);
|
||||
=> h.HasFlag(PacketHeader.IsDisconnectMessage);
|
||||
|
||||
public static bool IsServerMessage(this PacketHeader h)
|
||||
=> h.IsBitSet(PacketHeader.IsServerMessage);
|
||||
=> h.HasFlag(PacketHeader.IsServerMessage);
|
||||
|
||||
public static bool IsHeartbeatMessage(this PacketHeader h)
|
||||
=> h.IsBitSet(PacketHeader.IsHeartbeatMessage);
|
||||
=> h.HasFlag(PacketHeader.IsHeartbeatMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -81,8 +81,7 @@ namespace Barotrauma.Networking
|
||||
public const string SavePath = "ServerLogs";
|
||||
|
||||
private readonly Queue<LogMessage> lines;
|
||||
|
||||
private int unsavedLineCount;
|
||||
private readonly Queue<LogMessage> unsavedLines;
|
||||
|
||||
private readonly bool[] msgTypeHidden = new bool[Enum.GetValues(typeof(MessageType)).Length];
|
||||
|
||||
@@ -98,6 +97,7 @@ namespace Barotrauma.Networking
|
||||
{
|
||||
ServerName = serverName;
|
||||
lines = new Queue<LogMessage>();
|
||||
unsavedLines = new Queue<LogMessage>();
|
||||
|
||||
foreach (MessageType messageType in Enum.GetValues(typeof(MessageType)))
|
||||
{
|
||||
@@ -117,22 +117,19 @@ namespace Barotrauma.Networking
|
||||
#endif
|
||||
|
||||
lines.Enqueue(newText);
|
||||
unsavedLines.Enqueue(newText);
|
||||
|
||||
#if CLIENT
|
||||
if (listBox != null)
|
||||
{
|
||||
AddLine(newText);
|
||||
|
||||
listBox.UpdateScrollBarSize();
|
||||
}
|
||||
#endif
|
||||
|
||||
unsavedLineCount++;
|
||||
|
||||
if (unsavedLineCount >= LinesPerFile)
|
||||
if (unsavedLines.Count() >= LinesPerFile)
|
||||
{
|
||||
Save();
|
||||
unsavedLineCount = 0;
|
||||
unsavedLines.Clear();
|
||||
}
|
||||
|
||||
while (lines.Count > LinesPerFile)
|
||||
@@ -176,7 +173,7 @@ namespace Barotrauma.Networking
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllLines(filePath, lines.Select(l => l.SanitizedText));
|
||||
File.WriteAllLines(filePath, unsavedLines.Select(l => l.SanitizedText));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
||||
@@ -97,7 +97,7 @@ namespace Barotrauma.Networking
|
||||
private readonly SerializableProperty property;
|
||||
private readonly string typeString;
|
||||
private readonly object parentObject;
|
||||
|
||||
|
||||
public string Name
|
||||
{
|
||||
get { return property.Name; }
|
||||
@@ -214,7 +214,7 @@ namespace Barotrauma.Networking
|
||||
|
||||
public void Write(IWriteMessage msg, object overrideValue = null)
|
||||
{
|
||||
if (overrideValue == null) overrideValue = property.GetValue(parentObject);
|
||||
if (overrideValue == null) { overrideValue = Value; }
|
||||
switch (typeString)
|
||||
{
|
||||
case "float":
|
||||
@@ -315,6 +315,7 @@ namespace Barotrauma.Networking
|
||||
{
|
||||
NetPropertyData netPropertyData = new NetPropertyData(this, property, typeName);
|
||||
UInt32 key = ToolBox.StringToUInt32Hash(property.Name, md5);
|
||||
if (key == 0) { key++; } //0 is reserved to indicate the end of the netproperties section of a message
|
||||
if (netProperties.ContainsKey(key)){ throw new Exception("Hashing collision in ServerSettings.netProperties: " + netProperties[key] + " has same key as " + property.Name + " (" + key.ToString() + ")"); }
|
||||
netProperties.Add(key, netPropertyData);
|
||||
}
|
||||
@@ -720,13 +721,6 @@ namespace Barotrauma.Networking
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize("", true)]
|
||||
public string CampaignSubmarines
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
private YesNoMaybe traitorsEnabled;
|
||||
[Serialize(YesNoMaybe.No, true)]
|
||||
public YesNoMaybe TraitorsEnabled
|
||||
@@ -1070,7 +1064,6 @@ namespace Barotrauma.Networking
|
||||
}
|
||||
|
||||
#if SERVER
|
||||
MultiPlayerCampaign.UpdateCampaignSubs();
|
||||
SelectNonHiddenSubmarine();
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -124,7 +124,6 @@ namespace Barotrauma.Steam
|
||||
return unlocked;
|
||||
}
|
||||
|
||||
|
||||
public static bool IncrementStat(string statName, int increment)
|
||||
{
|
||||
if (!isInitialized || !Steamworks.SteamClient.IsValid) { return false; }
|
||||
@@ -136,6 +135,10 @@ namespace Barotrauma.Steam
|
||||
DebugConsole.NewMessage("Failed to increment stat \"" + statName + "\".");
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
StoreStats();
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
@@ -150,9 +153,19 @@ namespace Barotrauma.Steam
|
||||
DebugConsole.NewMessage("Failed to increment stat \"" + statName + "\".");
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
StoreStats();
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
public static int GetStatInt(string statName)
|
||||
{
|
||||
if (!isInitialized || !Steamworks.SteamClient.IsValid) { return 0; }
|
||||
return Steamworks.SteamUserStats.GetStatInt(statName);
|
||||
}
|
||||
|
||||
public static bool StoreStats()
|
||||
{
|
||||
if (!isInitialized || !Steamworks.SteamClient.IsValid) { return false; }
|
||||
@@ -167,6 +180,17 @@ namespace Barotrauma.Steam
|
||||
return success;
|
||||
}
|
||||
|
||||
public static bool TryGetUnlockedAchievements(out List<Steamworks.Data.Achievement> achievements)
|
||||
{
|
||||
if (!isInitialized || !Steamworks.SteamClient.IsValid)
|
||||
{
|
||||
achievements = null;
|
||||
return false;
|
||||
}
|
||||
achievements = Steamworks.SteamUserStats.Achievements.Where(a => a.State).ToList();
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void Update(float deltaTime)
|
||||
{
|
||||
if (!isInitialized) { return; }
|
||||
|
||||
Reference in New Issue
Block a user