This commit is contained in:
2025-06-16 15:14:23 +02:00
committed by devbeni
parent 60fe4620ff
commit 4ff561284f
3174 changed files with 428263 additions and 0 deletions

View File

@ -0,0 +1,13 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Mirror.Tests.Common")]
[assembly: InternalsVisibleTo("Mirror.Tests")]
// need to use Unity.*.CodeGen assembly name to import Unity.CompilationPipeline
// for ILPostProcessor tests.
[assembly: InternalsVisibleTo("Unity.Mirror.Tests.CodeGen")]
[assembly: InternalsVisibleTo("Mirror.Tests.Generated")]
[assembly: InternalsVisibleTo("Mirror.Tests.Runtime")]
[assembly: InternalsVisibleTo("Mirror.Tests.Performance.Editor")]
[assembly: InternalsVisibleTo("Mirror.Tests.Performance.Runtime")]
[assembly: InternalsVisibleTo("Mirror.Editor")]
[assembly: InternalsVisibleTo("Mirror.Components")]

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: e28d5f410e25b42e6a76a2ffc10e4675
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/AssemblyInfo.cs
uploadId: 736421

View File

@ -0,0 +1,101 @@
using System;
using UnityEngine;
namespace Mirror
{
/// <summary>
/// SyncVars are used to automatically synchronize a variable between the server and all clients. The direction of synchronization depends on the Sync Direction property, ServerToClient by default.
/// <para>
/// When Sync Direction is equal to ServerToClient, the value should be changed on the server side and synchronized to all clients.
/// Otherwise, the value should be changed on the client side and synchronized to server and other clients.
/// </para>
/// <para>Hook parameter allows you to define a method to be invoked when gets an value update. Notice that the hook method will not be called on the change side.</para>
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public class SyncVarAttribute : PropertyAttribute
{
public string hook;
}
/// <summary>
/// Call this from a client to run this function on the server.
/// <para>Make sure to validate input etc. It's not possible to call this from a server.</para>
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class CommandAttribute : Attribute
{
public int channel = Channels.Reliable;
public bool requiresAuthority = true;
}
/// <summary>
/// The server uses a Remote Procedure Call (RPC) to run this function on clients.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class ClientRpcAttribute : Attribute
{
public int channel = Channels.Reliable;
public bool includeOwner = true;
}
/// <summary>
/// The server uses a Remote Procedure Call (RPC) to run this function on a specific client.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class TargetRpcAttribute : Attribute
{
public int channel = Channels.Reliable;
}
/// <summary>
/// Only an active server will run this method.
/// <para>Prints a warning if a client or in-active server tries to execute this method.</para>
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class ServerAttribute : Attribute {}
/// <summary>
/// Only an active server will run this method.
/// <para>No warning is thrown.</para>
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class ServerCallbackAttribute : Attribute {}
/// <summary>
/// Only an active client will run this method.
/// <para>Prints a warning if the server or in-active client tries to execute this method.</para>
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class ClientAttribute : Attribute {}
/// <summary>
/// Only an active client will run this method.
/// <para>No warning is printed.</para>
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class ClientCallbackAttribute : Attribute {}
/// <summary>
/// Converts a string property into a Scene property in the inspector
/// </summary>
public class SceneAttribute : PropertyAttribute {}
/// <summary>
/// Used to show private SyncList in the inspector,
/// <para> Use instead of SerializeField for non Serializable types </para>
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public class ShowInInspectorAttribute : Attribute {}
/// <summary>
/// Used to make a field readonly in the inspector
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class ReadOnlyAttribute : PropertyAttribute {}
/// <summary>
/// When defining multiple Readers/Writers for the same type, indicate which one Weaver must use.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class WeaverPriorityAttribute : Attribute {}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: c04c722ee2ffd49c8a56ab33667b10b0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/Attributes.cs
uploadId: 736421

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1c38e1bebe9947f8b842a8a57aa2b71c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,206 @@
// batching functionality encapsulated into one class.
// -> less complexity
// -> easy to test
//
// IMPORTANT: we use THRESHOLD batching, not MAXED SIZE batching.
// see threshold comments below.
//
// includes timestamp for tick batching.
// -> allows NetworkTransform etc. to use timestamp without including it in
// every single message
using System;
using System.Collections.Generic;
namespace Mirror
{
public class Batcher
{
// batching threshold instead of max size.
// -> small messages are fit into threshold sized batches
// -> messages larger than threshold are single batches
//
// in other words, we fit up to 'threshold' but still allow larger ones
// for two reasons:
// 1.) data races: skipping batching for larger messages would send a
// large spawn message immediately, while others are batched and
// only flushed at the end of the frame
// 2) timestamp batching: if each batch is expected to contain a
// timestamp, then large messages have to be a batch too. otherwise
// they would not contain a timestamp
readonly int threshold;
// TimeStamp header size. each batch has one.
public const int TimestampSize = sizeof(double);
// Message header size. each message has one.
public static int MessageHeaderSize(int messageSize) =>
Compression.VarUIntSize((ulong)messageSize);
// maximum overhead for a single message.
// useful for the outside to calculate max message sizes.
public static int MaxMessageOverhead(int messageSize) =>
TimestampSize + MessageHeaderSize(messageSize);
// full batches ready to be sent.
// DO NOT queue NetworkMessage, it would box.
// DO NOT queue each serialization separately.
// it would allocate too many writers.
// https://github.com/vis2k/Mirror/pull/3127
// => best to build batches on the fly.
readonly Queue<NetworkWriterPooled> batches = new Queue<NetworkWriterPooled>();
// current batch in progress.
// we also store the timestamp to ensure we don't add a message from another frame,
// as this would introduce subtle jitter!
//
// for example:
// - a batch is started at t=1, another message is added at t=2 and then it's flushed
// - NetworkTransform uses remoteTimestamp which is t=1
// - snapshot interpolation would off by one (or multiple) frames!
NetworkWriterPooled batch;
double batchTimestamp;
public Batcher(int threshold)
{
this.threshold = threshold;
}
// add a message for batching
// we allow any sized messages.
// caller needs to make sure they are within max packet size.
public void AddMessage(ArraySegment<byte> message, double timeStamp)
{
// safety: message timestamp is only written once.
// make sure all messages in this batch are from the same timestamp.
// otherwise it could silently introduce jitter.
//
// this happened before:
// - NetworkEarlyUpdate @ t=1 processes transport messages
// - a handler replies by sending a message
// - a new batch is started @ t=1, timestamp is encoded
// - NetworkLateUpdate @ t=2 decides it's time to broadcast
// - NetworkTransform sends @ t=2
// - we add to the above batch which already encoded t=1
// - Client receives the batch which timestamp t=1
// - NetworkTransform uses remoteTime for interpolation
// remoteTime is the batch timestamp which is t=1
// - the NetworkTransform message is actually t=2
// => smooth interpolation would be impossible!
// NT thinks the position was @ t=1 but actually it was @ t=2 !
//
// the solution: if timestamp changed, enqueue the existing batch
if (batch != null && batchTimestamp != timeStamp)
{
batches.Enqueue(batch);
batch = null;
batchTimestamp = 0;
}
// predict the needed size, which is varint(size) + content
int headerSize = Compression.VarUIntSize((ulong)message.Count);
int neededSize = headerSize + message.Count;
// when appending to a batch in progress, check final size.
// if it expands beyond threshold, then we should finalize it first.
// => less than or exactly threshold is fine.
// GetBatch() will finalize it.
// => see unit tests.
if (batch != null &&
batch.Position + neededSize > threshold)
{
batches.Enqueue(batch);
batch = null;
batchTimestamp = 0;
}
// initialize a new batch if necessary
if (batch == null)
{
// borrow from pool. we return it in GetBatch.
batch = NetworkWriterPool.Get();
// write timestamp first.
// -> double precision for accuracy over long periods of time
// -> batches are per-frame, it doesn't matter which message's
// timestamp we use.
batch.WriteDouble(timeStamp);
// remember the encoded timestamp, see safety check below.
batchTimestamp = timeStamp;
}
// add serialization to current batch. even if > threshold.
// -> we do allow > threshold sized messages as single batch
// -> WriteBytes instead of WriteSegment because the latter
// would add a size header. we want to write directly.
//
// include size prefix as varint!
// -> fixes NetworkMessage serialization mismatch corrupting the
// next message in a batch.
// -> a _lot_ of time was wasted debugging corrupt batches.
// no easy way to figure out which NetworkMessage has a mismatch.
// -> this is worth everyone's sanity.
// -> varint means we prefix with 1 byte most of the time.
// -> the same issue in NetworkIdentity was why Mirror started!
Compression.CompressVarUInt(batch, (ulong)message.Count);
batch.WriteBytes(message.Array, message.Offset, message.Count);
}
// helper function to copy a batch to writer and return it to pool
static void CopyAndReturn(NetworkWriterPooled batch, NetworkWriter writer)
{
// make sure the writer is fresh to avoid uncertain situations
if (writer.Position != 0)
throw new ArgumentException($"GetBatch needs a fresh writer!");
// copy to the target writer
ArraySegment<byte> segment = batch.ToArraySegment();
writer.WriteBytes(segment.Array, segment.Offset, segment.Count);
// return batch to pool for reuse
NetworkWriterPool.Return(batch);
}
// get the next batch which is available for sending (if any).
// TODO safely get & return a batch instead of copying to writer?
// TODO could return pooled writer & use GetBatch in a 'using' statement!
public bool GetBatch(NetworkWriter writer)
{
// get first batch from queue (if any)
if (batches.TryDequeue(out NetworkWriterPooled first))
{
CopyAndReturn(first, writer);
return true;
}
// if queue was empty, we can send the batch in progress.
if (batch != null)
{
CopyAndReturn(batch, writer);
batch = null;
return true;
}
// nothing was written
return false;
}
// return all batches to the pool for cleanup
public void Clear()
{
// return batch in progress
if (batch != null)
{
NetworkWriterPool.Return(batch);
batch = null;
batchTimestamp = 0;
}
// return all queued batches
foreach (NetworkWriterPooled queued in batches)
NetworkWriterPool.Return(queued);
batches.Clear();
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 0afaaa611a2142d48a07bdd03b68b2b3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/Batching/Batcher.cs
uploadId: 736421

View File

@ -0,0 +1,129 @@
// un-batching functionality encapsulated into one class.
// -> less complexity
// -> easy to test
//
// includes timestamp for tick batching.
// -> allows NetworkTransform etc. to use timestamp without including it in
// every single message
using System;
using System.Collections.Generic;
namespace Mirror
{
public class Unbatcher
{
// supporting adding multiple batches before GetNextMessage is called.
// just in case.
readonly Queue<NetworkWriterPooled> batches = new Queue<NetworkWriterPooled>();
public int BatchesCount => batches.Count;
// NetworkReader is only created once,
// then pointed to the first batch.
readonly NetworkReader reader = new NetworkReader(new byte[0]);
// timestamp that was written into the batch remotely.
// for the batch that our reader is currently pointed at.
double readerRemoteTimeStamp;
// helper function to start reading a batch.
void StartReadingBatch(NetworkWriterPooled batch)
{
// point reader to it
reader.SetBuffer(batch.ToArraySegment());
// read remote timestamp (double)
// -> AddBatch quarantees that we have at least 8 bytes to read
readerRemoteTimeStamp = reader.ReadDouble();
}
// add a new batch.
// returns true if valid.
// returns false if not, in which case the connection should be disconnected.
public bool AddBatch(ArraySegment<byte> batch)
{
// IMPORTANT: ArraySegment is only valid until returning. we copy it!
//
// NOTE: it's not possible to create empty ArraySegments, so we
// don't need to check against that.
// make sure we have at least 8 bytes to read for tick timestamp
if (batch.Count < Batcher.TimestampSize)
return false;
// put into a (pooled) writer
// -> WriteBytes instead of WriteSegment because the latter
// would add a size header. we want to write directly.
// -> will be returned to pool when sending!
NetworkWriterPooled writer = NetworkWriterPool.Get();
writer.WriteBytes(batch.Array, batch.Offset, batch.Count);
// first batch? then point reader there
if (batches.Count == 0)
StartReadingBatch(writer);
// add batch
batches.Enqueue(writer);
//Debug.Log($"Adding Batch {BitConverter.ToString(batch.Array, batch.Offset, batch.Count)} => batches={batches.Count} reader={reader}");
return true;
}
// get next message, unpacked from batch (if any)
// message ArraySegment is only valid until the next call.
// timestamp is the REMOTE time when the batch was created remotely.
public bool GetNextMessage(out ArraySegment<byte> message, out double remoteTimeStamp)
{
message = default;
remoteTimeStamp = 0;
// do nothing if we don't have any batches.
// otherwise the below queue.Dequeue() would throw an
// InvalidOperationException if operating on empty queue.
if (batches.Count == 0)
return false;
// was our reader pointed to anything yet?
if (reader.Capacity == 0)
return false;
// no more data to read?
if (reader.Remaining == 0)
{
// retire the batch
NetworkWriterPooled writer = batches.Dequeue();
NetworkWriterPool.Return(writer);
// do we have another batch?
if (batches.Count > 0)
{
// point reader to the next batch.
// we'll return the reader below.
NetworkWriterPooled next = batches.Peek();
StartReadingBatch(next);
}
// otherwise there's nothing more to read
else return false;
}
// use the current batch's remote timestamp
// AFTER potentially moving to the next batch ABOVE!
remoteTimeStamp = readerRemoteTimeStamp;
// enough data to read the size prefix?
if (reader.Remaining == 0)
return false;
// read the size prefix as varint
// see Batcher.AddMessage comments for explanation.
int size = (int)Compression.DecompressVarUInt(reader);
// validate size prefix, in case attackers send malicious data
if (reader.Remaining < size)
return false;
// return the message of size
message = reader.ReadBytesSegment(size);
return true;
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 328562d71e1c45c58581b958845aa7a4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/Batching/Unbatcher.cs
uploadId: 736421

View File

@ -0,0 +1,74 @@
// standalone, Unity-independent connection-quality algorithm & enum.
// don't need to use this directly, it's built into Mirror's NetworkClient.
using UnityEngine;
namespace Mirror
{
public enum ConnectionQuality : byte
{
ESTIMATING, // still estimating
POOR, // unplayable
FAIR, // very noticeable latency, not very enjoyable anymore
GOOD, // very playable for everyone but high level competitors
EXCELLENT // ideal experience for high level competitors
}
public enum ConnectionQualityMethod : byte
{
Simple, // simple estimation based on rtt and jitter
Pragmatic // based on snapshot interpolation adjustment
}
// provide different heuristics for users to choose from.
// simple heuristics to get started.
// this will be iterated on over time based on user feedback.
public static class ConnectionQualityHeuristics
{
// convenience extension to color code Connection Quality
public static Color ColorCode(this ConnectionQuality quality)
{
switch (quality)
{
case ConnectionQuality.POOR: return Color.red;
case ConnectionQuality.FAIR: return new Color(1.0f, 0.647f, 0.0f);
case ConnectionQuality.GOOD: return Color.yellow;
case ConnectionQuality.EXCELLENT: return Color.green;
default: return Color.gray; // ESTIMATING
}
}
// straight forward estimation
// rtt: average round trip time in seconds.
// jitter: average latency variance.
public static ConnectionQuality Simple(double rtt, double jitter)
{
if (rtt <= 0.100 && jitter <= 0.10) return ConnectionQuality.EXCELLENT;
if (rtt <= 0.200 && jitter <= 0.20) return ConnectionQuality.GOOD;
if (rtt <= 0.400 && jitter <= 0.50) return ConnectionQuality.FAIR;
return ConnectionQuality.POOR;
}
// snapshot interpolation based estimation.
// snap. interp. adjusts buffer time based on connection quality.
// based on this, we can measure how far away we are from the ideal.
// the returned quality will always directly correlate with gameplay.
// => requires SnapshotInterpolation dynamicAdjustment to be enabled!
public static ConnectionQuality Pragmatic(double targetBufferTime, double currentBufferTime)
{
// buffer time is set by the game developer.
// estimating in multiples is a great way to be game independent.
// for example, a fast paced shooter and a slow paced RTS will both
// have poor connection if the multiplier is >10.
double multiplier = currentBufferTime / targetBufferTime;
// empirically measured with Tanks demo + LatencySimulation.
// it's not obvious to estimate on paper.
if (multiplier <= 1.15) return ConnectionQuality.EXCELLENT;
if (multiplier <= 1.25) return ConnectionQuality.GOOD;
if (multiplier <= 1.50) return ConnectionQuality.FAIR;
// anything else is poor
return ConnectionQuality.POOR;
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: ff663b880e33e4606b545c8b497041c2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/ConnectionQuality.cs
uploadId: 736421

View File

@ -0,0 +1,44 @@
// host mode related helper functions.
// usually they set up both server & client.
// it's cleaner to keep them in one place, instead of only in server / client.
using System;
namespace Mirror
{
public static class HostMode
{
// keep the local connections setup in one function.
// makes host setup easier to follow.
internal static void SetupConnections()
{
// create local connections pair, both are connected
Utils.CreateLocalConnections(
out LocalConnectionToClient connectionToClient,
out LocalConnectionToServer connectionToServer);
// set client connection
NetworkClient.connection = connectionToServer;
// set server connection
NetworkServer.SetLocalConnection(connectionToClient);
}
// call OnConnected on server & client.
// public because NetworkClient.ConnectLocalServer was public before too.
public static void InvokeOnConnected()
{
// call server OnConnected with server's connection to client
NetworkServer.OnConnected(NetworkServer.localConnection);
// call client OnConnected with client's connection to server
// => previously we used to send a ConnectMessage to
// NetworkServer.localConnection. this would queue the message
// until NetworkClient.Update processes it.
// => invoking the client's OnConnected event directly here makes
// tests fail. so let's do it exactly the same order as before by
// queueing the event for next Update!
//OnConnectedEvent?.Invoke(connection);
((LocalConnectionToServer)NetworkClient.connection).QueueConnectedEvent();
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: d27175a08d5341fc97645b49ee533d5a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/HostMode.cs
uploadId: 736421

View File

@ -0,0 +1,146 @@
// interest management component for custom solutions like
// distance based, spatial hashing, raycast based, etc.
using System.Collections.Generic;
using UnityEngine;
namespace Mirror
{
[DisallowMultipleComponent]
[HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")]
public abstract class InterestManagement : InterestManagementBase
{
// allocate newObservers helper HashSet
readonly HashSet<NetworkConnectionToClient> newObservers =
new HashSet<NetworkConnectionToClient>();
// rebuild observers for the given NetworkIdentity.
// Server will automatically spawn/despawn added/removed ones.
// newObservers: cached hashset to put the result into
// initialize: true if being rebuilt for the first time
//
// IMPORTANT:
// => global rebuild would be more simple, BUT
// => local rebuild is way faster for spawn/despawn because we can
// simply rebuild a select NetworkIdentity only
// => having both .observers and .observing is necessary for local
// rebuilds
//
// in other words, this is the perfect solution even though it's not
// completely simple (due to .observers & .observing).
//
// Mirror maintains .observing automatically in the background. best of
// both worlds without any worrying now!
public abstract void OnRebuildObservers(NetworkIdentity identity, HashSet<NetworkConnectionToClient> newObservers);
// helper function to trigger a full rebuild.
// most implementations should call this in a certain interval.
// some might call this all the time, or only on team changes or
// scene changes and so on.
//
// IMPORTANT: check if NetworkServer.active when using Update()!
[ServerCallback]
protected void RebuildAll()
{
foreach (NetworkIdentity identity in NetworkServer.spawned.Values)
{
NetworkServer.RebuildObservers(identity, false);
}
}
public override void Rebuild(NetworkIdentity identity, bool initialize)
{
// clear newObservers hashset before using it
newObservers.Clear();
// not force hidden?
if (identity.visibility != Visibility.ForceHidden)
{
OnRebuildObservers(identity, newObservers);
}
// IMPORTANT: AFTER rebuilding add own player connection in any case
// to ensure player always sees himself no matter what.
// -> OnRebuildObservers might clear observers, so we need to add
// the player's own connection AFTER. 100% fail safe.
// -> fixes https://github.com/vis2k/Mirror/issues/692 where a
// player might teleport out of the ProximityChecker's cast,
// losing the own connection as observer.
if (identity.connectionToClient != null)
{
newObservers.Add(identity.connectionToClient);
}
bool changed = false;
// add all newObservers that aren't in .observers yet
foreach (NetworkConnectionToClient conn in newObservers)
{
// only add ready connections.
// otherwise the player might not be in the world yet or anymore
if (conn != null && conn.isReady)
{
if (initialize || !identity.observers.ContainsKey(conn.connectionId))
{
// new observer
conn.AddToObserving(identity);
// Debug.Log($"New Observer for {gameObject} {conn}");
changed = true;
}
}
}
// remove all old .observers that aren't in newObservers anymore
foreach (NetworkConnectionToClient conn in identity.observers.Values)
{
if (!newObservers.Contains(conn))
{
// removed observer
conn.RemoveFromObserving(identity, false);
// Debug.Log($"Removed Observer for {gameObject} {conn}");
changed = true;
}
}
// copy new observers to observers
if (changed)
{
identity.observers.Clear();
foreach (NetworkConnectionToClient conn in newObservers)
{
if (conn != null && conn.isReady)
identity.observers.Add(conn.connectionId, conn);
}
}
// special case for host mode: we use SetHostVisibility to hide
// NetworkIdentities that aren't in observer range from host.
// this is what games like Dota/Counter-Strike do too, where a host
// does NOT see all players by default. they are in memory, but
// hidden to the host player.
//
// this code is from UNET, it's a bit strange but it works:
// * it hides newly connected identities in host mode
// => that part was the intended behaviour
// * it hides ALL NetworkIdentities in host mode when the host
// connects but hasn't selected a character yet
// => this only works because we have no .localConnection != null
// check. at this stage, localConnection is null because
// StartHost starts the server first, then calls this code,
// then starts the client and sets .localConnection. so we can
// NOT add a null check without breaking host visibility here.
// * it hides ALL NetworkIdentities in server-only mode because
// observers never contain the 'null' .localConnection
// => that was not intended, but let's keep it as it is so we
// don't break anything in host mode. it's way easier than
// iterating all identities in a special function in StartHost.
if (initialize)
{
if (!newObservers.Contains(NetworkServer.localConnection))
{
SetHostVisibility(identity, false);
}
}
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 41d809934003479f97e992eebb7ed6af
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/InterestManagement.cs
uploadId: 736421

View File

@ -0,0 +1,103 @@
// interest management component for custom solutions like
// distance based, spatial hashing, raycast based, etc.
// low level base class allows for low level spatial hashing etc., which is 3-5x faster.
using UnityEngine;
namespace Mirror
{
[DisallowMultipleComponent]
[HelpURL("https://mirror-networking.gitbook.io/docs/guides/interest-management")]
public abstract class InterestManagementBase : MonoBehaviour
{
// initialize NetworkServer/Client .aoi.
// previously we did this in Awake(), but that's called for disabled
// components too. if we do it OnEnable(), then it's not set for
// disabled components.
protected virtual void OnEnable()
{
// do not check if == null or error if already set.
// users may enabled/disable components randomly,
// causing this to be called multiple times.
NetworkServer.aoi = this;
NetworkClient.aoi = this;
}
[ServerCallback]
public virtual void ResetState() {}
// Callback used by the visibility system to determine if an observer
// (player) can see the NetworkIdentity. If this function returns true,
// the network connection will be added as an observer.
// conn: Network connection of a player.
// returns True if the player can see this object.
public abstract bool OnCheckObserver(NetworkIdentity identity, NetworkConnectionToClient newObserver);
// Callback used by the visibility system for objects on a host.
// Objects on a host (with a local client) cannot be disabled or
// destroyed when they are not visible to the local client. So this
// function is called to allow custom code to hide these objects. A
// typical implementation will disable renderer components on the
// object. This is only called on local clients on a host.
// => need the function in here and virtual so people can overwrite!
// => not everyone wants to hide renderers!
[ServerCallback]
public virtual void SetHostVisibility(NetworkIdentity identity, bool visible)
{
foreach (Renderer rend in identity.GetComponentsInChildren<Renderer>())
rend.enabled = visible;
// reason to also set lights/audio/terrain/etc.:
// Let's say players were holding a flashlight or magic wand with a particle effect. Without this,
// host client would see the light / particles for all players in all subscenes because we don't
// hide lights and particles. Host client would hear ALL audio sources in all subscenes too. We
// hide the renderers, which covers basic objects and UI, but we don't hide anything else that may
// be a child of a networked object. Same idea for cars with lights and sounds in other subscenes
// that host client shouldn't see or hear...host client wouldn't see the car itself, but sees the
// lights moving around and hears all of their engines / horns / etc.
foreach (Light light in identity.GetComponentsInChildren<Light>())
light.enabled = visible;
foreach (AudioSource audio in identity.GetComponentsInChildren<AudioSource>())
audio.enabled = visible;
foreach (Terrain terrain in identity.GetComponentsInChildren<Terrain>())
{
terrain.drawHeightmap = visible;
terrain.drawTreesAndFoliage = visible;
}
foreach (ParticleSystem particle in identity.GetComponentsInChildren<ParticleSystem>())
{
ParticleSystem.EmissionModule emission = particle.emission;
emission.enabled = visible;
}
}
/// <summary>Called on the server when a new networked object is spawned.</summary>
// (useful for 'only rebuild if changed' interest management algorithms)
[ServerCallback]
public virtual void OnSpawned(NetworkIdentity identity) {}
/// <summary>Called on the server when a networked object is destroyed.</summary>
// (useful for 'only rebuild if changed' interest management algorithms)
[ServerCallback]
public virtual void OnDestroyed(NetworkIdentity identity) {}
public abstract void Rebuild(NetworkIdentity identity, bool initialize);
/// <summary>Adds the specified connection to the observers of identity</summary>
protected void AddObserver(NetworkConnectionToClient connection, NetworkIdentity identity)
{
connection.AddToObserving(identity);
identity.observers.Add(connection.connectionId, connection);
}
/// <summary>Removes the specified connection from the observers of identity</summary>
protected void RemoveObserver(NetworkConnectionToClient connection, NetworkIdentity identity)
{
connection.RemoveFromObserving(identity, false);
identity.observers.Remove(connection.connectionId);
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 18bd2ffe65a444f3b13d59bdac7f2228
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/InterestManagementBase.cs
uploadId: 736421

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d2656015ded44e83a24f4c4776bafd40
timeCreated: 1687920405

View File

@ -0,0 +1,13 @@
namespace Mirror
{
public interface Capture
{
// server timestamp at time of capture.
double timestamp { get; set; }
// optional gizmo drawing for visual debugging.
// history is only known on the server, which usually doesn't render.
// showing Gizmos in the Editor is enough.
void DrawGizmo();
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 347e831952e943a49095cadd39a5aeb2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/LagCompensation/Capture.cs
uploadId: 736421

View File

@ -0,0 +1,139 @@
// HistoryBounds keeps a bounding box of all the object's bounds in the past N seconds.
// useful to decide which objects to rollback, instead of rolling back all of them.
// https://www.youtube.com/watch?v=zrIY0eIyqmI (37:00)
// standalone C# implementation to be engine (and language) agnostic.
using System.Collections.Generic;
using UnityEngine;
namespace Mirror
{
// FakeByte: gather bounds in smaller buckets.
// for example, bucket(t0,t1,t2), bucket(t3,t4,t5), ...
// instead of removing old bounds t0, t1, ...
// we remove a whole bucket every 3 times: bucket(t0,t1,t2)
// and when building total bounds, we encapsulate a few larger buckets
// instead of many smaller bounds.
//
// => a bucket is encapsulate(bounds0, bounds1, bounds2) so we don't
// need a custom struct, simply reuse bounds but remember that each
// entry includes N timestamps.
//
// => note that simply reducing capture interval is _not_ the same.
// we want to capture in detail in case players run in zig-zag.
// but still grow larger buckets internally.
public class HistoryBounds
{
// mischa: use MinMaxBounds to avoid Unity Bounds.Encapsulate conversions.
readonly int boundsPerBucket;
readonly Queue<MinMaxBounds> fullBuckets;
// full bucket limit. older ones will be removed.
readonly int bucketLimit;
// bucket in progress, contains 0..boundsPerBucket bounds encapsulated.
MinMaxBounds? currentBucket;
int currentBucketSize;
// amount of total bounds, including bounds in full buckets + current
public int boundsCount { get; private set; }
// total bounds encapsulating all of the bounds history.
// totalMinMax is used for internal calculations.
// public total is used for Unity representation.
MinMaxBounds totalMinMax;
public Bounds total
{
get
{
Bounds bounds = new Bounds();
bounds.SetMinMax(totalMinMax.min, totalMinMax.max);
return bounds;
}
}
public HistoryBounds(int boundsLimit, int boundsPerBucket)
{
// bucketLimit via '/' cuts off remainder.
// that's what we want, since we always have a 'currentBucket'.
this.boundsPerBucket = boundsPerBucket;
this.bucketLimit = (boundsLimit / boundsPerBucket);
// initialize queue with maximum capacity to avoid runtime resizing
// capacity +1 because it makes the code easier if we insert first, and then remove.
fullBuckets = new Queue<MinMaxBounds>(bucketLimit + 1);
}
// insert new bounds into history. calculates new total bounds.
// Queue.Dequeue() always has the oldest bounds.
public void Insert(Bounds bounds)
{
// convert to MinMax representation for faster .Encapsulate()
MinMaxBounds minmax = new MinMaxBounds
{
min = bounds.min,
max = bounds.max
};
// initialize 'total' if not initialized yet.
// we don't want to call (0,0).Encapsulate(bounds).
if (boundsCount == 0)
{
totalMinMax = minmax;
}
// add to current bucket:
// either initialize new one, or encapsulate into existing one
if (currentBucket == null)
{
currentBucket = minmax;
}
else
{
currentBucket.Value.Encapsulate(minmax);
}
// current bucket has one more bounds.
// total bounds increased as well.
currentBucketSize += 1;
boundsCount += 1;
// always encapsulate into total immediately.
// this is free.
totalMinMax.Encapsulate(minmax);
// current bucket full?
if (currentBucketSize == boundsPerBucket)
{
// move it to full buckets
fullBuckets.Enqueue(currentBucket.Value);
currentBucket = null;
currentBucketSize = 0;
// full bucket capacity reached?
if (fullBuckets.Count > bucketLimit)
{
// remove oldest bucket
fullBuckets.Dequeue();
boundsCount -= boundsPerBucket;
// recompute total bounds
// instead of iterating N buckets, we iterate N / boundsPerBucket buckets.
// TODO technically we could reuse 'currentBucket' before clearing instead of encapsulating again
totalMinMax = minmax;
foreach (MinMaxBounds bucket in fullBuckets)
totalMinMax.Encapsulate(bucket);
}
}
}
public void Reset()
{
fullBuckets.Clear();
currentBucket = null;
currentBucketSize = 0;
boundsCount = 0;
totalMinMax = new MinMaxBounds();
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: ca9ea58b98a34f73801b162cd5de724e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/LagCompensation/HistoryBounds.cs
uploadId: 736421

View File

@ -0,0 +1,144 @@
// standalone lag compensation algorithm
// based on the Valve Networking Model:
// https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking
using System.Collections.Generic;
namespace Mirror
{
public static class LagCompensation
{
// history is of <timestamp, capture>.
// Queue allows for fast 'remove first' and 'append last'.
//
// make sure to always insert in order.
// inserting out of order like [1,2,4,3] would cause issues.
// can't safeguard this because Queue doesn't have .Last access.
public static void Insert<T>(
Queue<KeyValuePair<double, T>> history,
int historyLimit,
double timestamp,
T capture)
where T : Capture
{
// make space according to history limit.
// do this before inserting, to avoid resizing past capacity.
if (history.Count >= historyLimit)
history.Dequeue();
// insert
history.Enqueue(new KeyValuePair<double, T>(timestamp, capture));
}
// get the two snapshots closest to a given timestamp.
// those can be used to interpolate the exact snapshot at that time.
// if timestamp is newer than the newest history entry, then we extrapolate.
// 't' will be between 1 and 2, before is second last, after is last.
// callers should Lerp(before, after, t=1.5) to extrapolate the hit.
// see comments below for extrapolation.
public static bool Sample<T>(
Queue<KeyValuePair<double, T>> history,
double timestamp, // current server time
double interval, // capture interval
out T before,
out T after,
out double t) // interpolation factor
where T : Capture
{
before = default;
after = default;
t = 0;
// can't sample an empty history
// interpolation needs at least one entry.
// extrapolation needs at least two entries.
// can't Lerp(A, A, 1.5). dist(A, A) * 1.5 is always 0.
if(history.Count < 2) {
return false;
}
// older than oldest
if (timestamp < history.Peek().Key) {
return false;
}
// iterate through the history
// TODO faster version: guess start index by how many 'intervals' we are behind.
// search around that area.
// should be O(1) most of the time, unless sampling was off.
KeyValuePair<double, T> prev = new KeyValuePair<double, T>();
KeyValuePair<double, T> prevPrev = new KeyValuePair<double, T>();
foreach(KeyValuePair<double, T> entry in history) {
// exact match?
if (timestamp == entry.Key) {
before = entry.Value;
after = entry.Value;
t = Mathd.InverseLerp(before.timestamp, after.timestamp, timestamp);
return true;
}
// did we check beyond timestamp? then return the previous two.
if (entry.Key > timestamp) {
before = prev.Value;
after = entry.Value;
t = Mathd.InverseLerp(before.timestamp, after.timestamp, timestamp);
return true;
}
// remember the last two for extrapolation.
// Queue doesn't have access to .Last.
prevPrev = prev;
prev = entry;
}
// newer than newest: extrapolate up to one interval.
// let's say we capture every 100 ms:
// 100, 200, 300, 400
// and the server is at 499
// if a client sends CmdFire at time 480, then there's no history entry.
// => adding the current entry every time would be too expensive.
// worst case we would capture at 401, 402, 403, 404, ... 100 times
// => not extrapolating isn't great. low latency clients would be
// punished by missing their targets since no entry at 'time' was found.
// => extrapolation is the best solution. make sure this works as
// expected and within limits.
if (prev.Key < timestamp && timestamp <= prev.Key + interval) {
// return the last two valid snapshots.
// can't just return (after, after) because we can't extrapolate
// if their distance is 0.
before = prevPrev.Value;
after = prev.Value;
// InverseLerp will give [after, after+interval].
// but we return [before, after, t].
// so add +1 for the distance from before->after
t = 1 + Mathd.InverseLerp(after.timestamp, after.timestamp + interval, timestamp);
return true;
}
return false;
}
// never trust the client.
// we estimate when a message was sent.
// don't trust the client to tell us the time.
// https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking
// Command Execution Time = Current Server Time - Packet Latency - Client View Interpolation
// => lag compensation demo estimation is off by only ~6ms
public static double EstimateTime(double serverTime, double rtt, double bufferTime)
{
// packet latency is one trip from client to server, so rtt / 2
// client view interpolation is the snapshot interpolation buffer time
double latency = rtt / 2;
return serverTime - latency - bufferTime;
}
// convenience function to draw all history gizmos.
// this should be called from OnDrawGizmos.
public static void DrawGizmos<T>(Queue<KeyValuePair<double, T>> history)
where T : Capture
{
foreach (KeyValuePair<double, T> entry in history)
entry.Value.DrawGizmo();
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: ad53cc7d12144d0ba3a8b0a4515e5d17
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/LagCompensation/LagCompensation.cs
uploadId: 736421

View File

@ -0,0 +1,19 @@
// snapshot interpolation settings struct.
// can easily be exposed in Unity inspectors.
using System;
using UnityEngine;
namespace Mirror
{
// class so we can define defaults easily
[Serializable]
public class LagCompensationSettings
{
[Header("Buffering")]
[Tooltip("Keep this many past snapshots in the buffer. The larger this is, the further we can rewind into the past.\nMaximum rewind time := historyAmount * captureInterval")]
public int historyLimit = 6;
[Tooltip("Capture state every 'captureInterval' seconds. Larger values will space out the captures more, which gives a longer history but with possible gaps inbetween.\nSmaller values will have fewer gaps, with shorter history.")]
public float captureInterval = 0.100f; // 100 ms
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: fa80bec245f94bf8a28ec78777992a1c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/LagCompensation/LagCompensationSettings.cs
uploadId: 736421

View File

@ -0,0 +1,73 @@
// Unity's Bounds struct is represented as (center, extents).
// HistoryBounds make heavy use of .Encapsulate(), which has to convert
// Unity's (center, extents) to (min, max) every time, and then convert back.
//
// It's faster to use a (min, max) representation directly instead.
using System;
using System.Runtime.CompilerServices;
using UnityEngine;
namespace Mirror
{
public struct MinMaxBounds: IEquatable<Bounds>
{
public Vector3 min;
public Vector3 max;
// encapsulate a single point
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Encapsulate(Vector3 point)
{
min = Vector3.Min(this.min, point);
max = Vector3.Max(this.max, point);
}
// encapsulate another bounds
public void Encapsulate(MinMaxBounds bounds)
{
Encapsulate(bounds.min);
Encapsulate(bounds.max);
}
// convenience comparison with Unity's bounds, for unit tests etc.
public static bool operator ==(MinMaxBounds lhs, Bounds rhs) =>
lhs.min == rhs.min &&
lhs.max == rhs.max;
public static bool operator !=(MinMaxBounds lhs, Bounds rhs) =>
!(lhs == rhs);
public override bool Equals(object obj) =>
obj is MinMaxBounds other &&
min == other.min &&
max == other.max;
public bool Equals(MinMaxBounds other) =>
min.Equals(other.min) && max.Equals(other.max);
public bool Equals(Bounds other) =>
min.Equals(other.min) && max.Equals(other.max);
#if UNITY_2021_3_OR_NEWER
// Unity 2019/2020 don't have HashCode.Combine yet.
// this is only to avoid reflection. without defining, it works too.
// default generated by rider
public override int GetHashCode() => HashCode.Combine(min, max);
#else
public override int GetHashCode()
{
// return HashCode.Combine(min, max); without using .Combine for older Unity versions
unchecked
{
int hash = 17;
hash = hash * 23 + min.GetHashCode();
hash = hash * 23 + max.GetHashCode();
return hash;
}
}
#endif
// tostring
public override string ToString() => $"({min}, {max})";
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 4372b1e1a1cc4c669cc7bf0925f59d29
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/LagCompensation/MinMaxBounds.cs
uploadId: 736421

View File

@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
namespace Mirror
{
// a server's connection TO a LocalClient.
// sending messages on this connection causes the client's handler function to be invoked directly
public class LocalConnectionToClient : NetworkConnectionToClient
{
internal LocalConnectionToServer connectionToServer;
// packet queue
internal readonly Queue<NetworkWriterPooled> queue = new Queue<NetworkWriterPooled>();
public LocalConnectionToClient() : base(LocalConnectionId) {}
internal override void Send(ArraySegment<byte> segment, int channelId = Channels.Reliable)
{
// instead of invoking it directly, we enqueue and process next update.
// this way we can simulate a similar call flow as with remote clients.
// the closer we get to simulating host as remote, the better!
// both directions do this, so [Command] and [Rpc] behave the same way.
//Debug.Log($"Enqueue {BitConverter.ToString(segment.Array, segment.Offset, segment.Count)}");
NetworkWriterPooled writer = NetworkWriterPool.Get();
writer.WriteBytes(segment.Array, segment.Offset, segment.Count);
connectionToServer.queue.Enqueue(writer);
}
// true because local connections never timeout
internal override bool IsAlive(float timeout) => true;
// don't ping host client in host mode
protected override void UpdatePing() {}
internal override void Update()
{
base.Update();
// process internal messages so they are applied at the correct time
while (queue.Count > 0)
{
// call receive on queued writer's content, return to pool
NetworkWriterPooled writer = queue.Dequeue();
ArraySegment<byte> message = writer.ToArraySegment();
// OnTransportData assumes a proper batch with timestamp etc.
// let's make a proper batch and pass it to OnTransportData.
Batcher batcher = GetBatchForChannelId(Channels.Reliable);
batcher.AddMessage(message, NetworkTime.localTime);
using (NetworkWriterPooled batchWriter = NetworkWriterPool.Get())
{
// make a batch with our local time (double precision)
if (batcher.GetBatch(batchWriter))
{
NetworkServer.OnTransportData(connectionId, batchWriter.ToArraySegment(), Channels.Reliable);
}
}
NetworkWriterPool.Return(writer);
}
}
internal void DisconnectInternal()
{
// set not ready and handle clientscene disconnect in any case
// (might be client or host mode here)
isReady = false;
RemoveFromObservingsObservers();
}
/// <summary>Disconnects this connection.</summary>
public override void Disconnect()
{
DisconnectInternal();
connectionToServer.DisconnectInternal();
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: a88758df7db2043d6a9d926e0b6d4191
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/LocalConnectionToClient.cs
uploadId: 736421

View File

@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Mirror
{
// a localClient's connection TO a server.
// send messages on this connection causes the server's handler function to be invoked directly.
public class LocalConnectionToServer : NetworkConnectionToServer
{
internal LocalConnectionToClient connectionToClient;
// packet queue
internal readonly Queue<NetworkWriterPooled> queue = new Queue<NetworkWriterPooled>();
// see caller for comments on why we need this
bool connectedEventPending;
bool disconnectedEventPending;
internal void QueueConnectedEvent() => connectedEventPending = true;
internal void QueueDisconnectedEvent() => disconnectedEventPending = true;
// Send stage two: serialized NetworkMessage as ArraySegment<byte>
internal override void Send(ArraySegment<byte> segment, int channelId = Channels.Reliable)
{
if (segment.Count == 0)
{
Debug.LogError("LocalConnection.SendBytes cannot send zero bytes");
return;
}
// instead of invoking it directly, we enqueue and process next update.
// this way we can simulate a similar call flow as with remote clients.
// the closer we get to simulating host as remote, the better!
// both directions do this, so [Command] and [Rpc] behave the same way.
//Debug.Log($"Enqueue {BitConverter.ToString(segment.Array, segment.Offset, segment.Count)}");
NetworkWriterPooled writer = NetworkWriterPool.Get();
writer.WriteBytes(segment.Array, segment.Offset, segment.Count);
connectionToClient.queue.Enqueue(writer);
}
internal override void Update()
{
base.Update();
// should we still process a connected event?
if (connectedEventPending)
{
connectedEventPending = false;
NetworkClient.OnConnectedEvent?.Invoke();
}
// process internal messages so they are applied at the correct time
while (queue.Count > 0)
{
// call receive on queued writer's content, return to pool
NetworkWriterPooled writer = queue.Dequeue();
ArraySegment<byte> message = writer.ToArraySegment();
// OnTransportData assumes a proper batch with timestamp etc.
// let's make a proper batch and pass it to OnTransportData.
Batcher batcher = GetBatchForChannelId(Channels.Reliable);
batcher.AddMessage(message, NetworkTime.localTime);
using (NetworkWriterPooled batchWriter = NetworkWriterPool.Get())
{
// make a batch with our local time (double precision)
if (batcher.GetBatch(batchWriter))
{
NetworkClient.OnTransportData(batchWriter.ToArraySegment(), Channels.Reliable);
}
}
NetworkWriterPool.Return(writer);
}
// should we still process a disconnected event?
if (disconnectedEventPending)
{
disconnectedEventPending = false;
NetworkClient.OnDisconnectedEvent?.Invoke();
}
}
/// <summary>Disconnects this connection.</summary>
internal void DisconnectInternal()
{
// set not ready and handle clientscene disconnect in any case
// (might be client or host mode here)
// TODO remove redundant state. have one source of truth for .ready!
isReady = false;
NetworkClient.ready = false;
}
/// <summary>Disconnects this connection.</summary>
public override void Disconnect()
{
connectionToClient.DisconnectInternal();
DisconnectInternal();
// simulate what a true remote connection would do:
// first, the server should remove it:
// TODO should probably be in connectionToClient.DisconnectInternal
// because that's the NetworkServer's connection!
NetworkServer.RemoveLocalConnection();
// then call OnTransportDisconnected for proper disconnect handling,
// callbacks & cleanups.
// => otherwise OnClientDisconnected() is never called!
// => see NetworkClientTests.DisconnectCallsOnClientDisconnect_HostMode()
NetworkClient.OnTransportDisconnected();
}
// true because local connections never timeout
internal override bool IsAlive(float timeout) => true;
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: cdfff390c3504158a269e8b8662e2a40
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/LocalConnectionToServer.cs
uploadId: 736421

View File

@ -0,0 +1,186 @@
using System;
using UnityEngine;
namespace Mirror
{
// need to send time every sendInterval.
// batching automatically includes remoteTimestamp.
// all we need to do is ensure that an empty message is sent.
// and react to it.
// => we don't want to insert a snapshot on every batch.
// => do it exactly every sendInterval on every TimeSnapshotMessage.
public struct TimeSnapshotMessage : NetworkMessage {}
public struct ReadyMessage : NetworkMessage {}
public struct NotReadyMessage : NetworkMessage {}
public struct AddPlayerMessage : NetworkMessage {}
public struct SceneMessage : NetworkMessage
{
public string sceneName;
// Normal = 0, LoadAdditive = 1, UnloadAdditive = 2
public SceneOperation sceneOperation;
public bool customHandling;
}
public enum SceneOperation : byte
{
Normal,
LoadAdditive,
UnloadAdditive
}
public struct CommandMessage : NetworkMessage
{
public uint netId;
public byte componentIndex;
public ushort functionHash;
// the parameters for the Cmd function
// -> ArraySegment to avoid unnecessary allocations
public ArraySegment<byte> payload;
}
public struct RpcMessage : NetworkMessage
{
public uint netId;
public byte componentIndex;
public ushort functionHash;
// the parameters for the Cmd function
// -> ArraySegment to avoid unnecessary allocations
public ArraySegment<byte> payload;
}
[Flags] public enum SpawnFlags : byte
{
None = 0,
isOwner = 1 << 0,
isLocalPlayer = 1 << 1
}
public struct SpawnMessage : NetworkMessage
{
// netId of new or existing object
public uint netId;
// isOwner and isLocalPlayer are merged into one byte via bitwise op
public SpawnFlags spawnFlags;
public ulong sceneId;
// If sceneId != 0 then it is used instead of assetId
public uint assetId;
// Local position
public Vector3 position;
// Local rotation
public Quaternion rotation;
// Local scale
public Vector3 scale;
// serialized component data
// ArraySegment to avoid unnecessary allocations
public ArraySegment<byte> payload;
// Backwards compatibility after implementing spawnFlags
public bool isOwner
{
get => spawnFlags.HasFlag(SpawnFlags.isOwner);
set => spawnFlags =
value
? spawnFlags | SpawnFlags.isOwner
: spawnFlags & ~SpawnFlags.isOwner;
}
// Backwards compatibility after implementing spawnFlags
public bool isLocalPlayer
{
get => spawnFlags.HasFlag(SpawnFlags.isLocalPlayer);
set => spawnFlags =
value
? spawnFlags | SpawnFlags.isLocalPlayer
: spawnFlags & ~SpawnFlags.isLocalPlayer;
}
}
public struct ChangeOwnerMessage : NetworkMessage
{
public uint netId;
// isOwner and isLocalPlayer are merged into one byte via bitwise op
public SpawnFlags spawnFlags;
// Backwards compatibility after implementing spawnFlags
public bool isOwner
{
get => spawnFlags.HasFlag(SpawnFlags.isOwner);
set => spawnFlags =
value
? spawnFlags | SpawnFlags.isOwner
: spawnFlags & ~SpawnFlags.isOwner;
}
// Backwards compatibility after implementing spawnFlags
public bool isLocalPlayer
{
get => spawnFlags.HasFlag(SpawnFlags.isLocalPlayer);
set => spawnFlags =
value
? spawnFlags | SpawnFlags.isLocalPlayer
: spawnFlags & ~SpawnFlags.isLocalPlayer;
}
}
public struct ObjectSpawnStartedMessage : NetworkMessage {}
public struct ObjectSpawnFinishedMessage : NetworkMessage {}
public struct ObjectDestroyMessage : NetworkMessage
{
public uint netId;
}
public struct ObjectHideMessage : NetworkMessage
{
public uint netId;
}
public struct EntityStateMessage : NetworkMessage
{
public uint netId;
// the serialized component data
// -> ArraySegment to avoid unnecessary allocations
public ArraySegment<byte> payload;
}
// whoever wants to measure rtt, sends this to the other end.
public struct NetworkPingMessage : NetworkMessage
{
// local time is used to calculate round trip time,
// and to calculate the predicted time offset.
public double localTime;
// predicted time is sent to compare the final error, for debugging only
public double predictedTimeAdjusted;
public NetworkPingMessage(double localTime, double predictedTimeAdjusted)
{
this.localTime = localTime;
this.predictedTimeAdjusted = predictedTimeAdjusted;
}
}
// the other end responds with this message.
// we can use this to calculate rtt.
public struct NetworkPongMessage : NetworkMessage
{
// local time is used to calculate round trip time.
public double localTime;
// predicted error is used to adjust the predicted timeline.
public double predictionErrorUnadjusted;
public double predictionErrorAdjusted; // for debug purposes
public NetworkPongMessage(double localTime, double predictionErrorUnadjusted, double predictionErrorAdjusted)
{
this.localTime = localTime;
this.predictionErrorUnadjusted = predictionErrorUnadjusted;
this.predictionErrorAdjusted = predictionErrorAdjusted;
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 938f6f28a6c5b48a0bbd7782342d763b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/Messages.cs
uploadId: 736421

View File

@ -0,0 +1,16 @@
{
"name": "Mirror",
"rootNamespace": "",
"references": [
"GUID:325984b52e4128546bc7558552f8b1d2"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": true,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 30817c1a0e6d646d99c048fc403f5979
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/Mirror.asmdef
uploadId: 736421

View File

@ -0,0 +1,84 @@
using System;
using UnityEngine;
using UnityEngine.Events;
namespace Mirror
{
[Serializable] public class UnityEventNetworkConnection : UnityEvent<NetworkConnectionToClient> {}
/// <summary>Base class for implementing component-based authentication during the Connect phase</summary>
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-authenticators")]
public abstract class NetworkAuthenticator : MonoBehaviour
{
/// <summary>Notify subscribers on the server when a client is authenticated</summary>
[Header("Event Listeners (optional)")]
[Tooltip("Mirror has an internal subscriber to this event. You can add your own here.")]
public UnityEventNetworkConnection OnServerAuthenticated = new UnityEventNetworkConnection();
/// <summary>Notify subscribers on the client when the client is authenticated</summary>
[Tooltip("Mirror has an internal subscriber to this event. You can add your own here.")]
public UnityEvent OnClientAuthenticated = new UnityEvent();
/// <summary>Called when server starts, used to register message handlers if needed.</summary>
public virtual void OnStartServer() {}
/// <summary>Called when server stops, used to unregister message handlers if needed.</summary>
public virtual void OnStopServer() {}
/// <summary>Called on server from OnServerConnectInternal when a client needs to authenticate</summary>
public virtual void OnServerAuthenticate(NetworkConnectionToClient conn) {}
protected void ServerAccept(NetworkConnectionToClient conn)
{
OnServerAuthenticated.Invoke(conn);
}
protected void ServerReject(NetworkConnectionToClient conn)
{
conn.Disconnect();
}
/// <summary>Called when client starts, used to register message handlers if needed.</summary>
public virtual void OnStartClient() {}
/// <summary>Called when client stops, used to unregister message handlers if needed.</summary>
public virtual void OnStopClient() {}
/// <summary>Called on client from OnClientConnectInternal when a client needs to authenticate</summary>
public virtual void OnClientAuthenticate() {}
protected void ClientAccept()
{
OnClientAuthenticated.Invoke();
}
protected void ClientReject()
{
// Set this on the client for local reference
NetworkClient.connection.isAuthenticated = false;
// disconnect the client
NetworkClient.connection.Disconnect();
}
// Reset() instead of OnValidate():
// Any NetworkAuthenticator assigns itself to the NetworkManager, this is fine on first adding it,
// but if someone intentionally sets Authenticator to null on the NetworkManager again then the
// Authenticator will reassign itself if a value in the inspector is changed.
// My change switches OnValidate to Reset since Reset is only called when the component is first
// added (or reset is pressed).
void Reset()
{
#if UNITY_EDITOR
// automatically assign authenticator field if we add this to NetworkManager
NetworkManager manager = GetComponent<NetworkManager>();
if (manager != null && manager.authenticator == null)
{
// undo has to be called before the change happens
UnityEditor.Undo.RecordObject(manager, "Assigned NetworkManager authenticator");
manager.authenticator = this;
}
#endif
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 407fc95d4a8257f448799f26cdde0c2a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/NetworkAuthenticator.cs
uploadId: 736421

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 655ee8cba98594f70880da5cc4dc442d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/NetworkBehaviour.cs
uploadId: 736421

View File

@ -0,0 +1,483 @@
// base class for "Hybrid" sync components.
// inspired by the Quake networking model, but made to scale.
// https://www.jfedor.org/quake3/
using System;
using UnityEngine;
namespace Mirror
{
public abstract class NetworkBehaviourHybrid : NetworkBehaviour
{
// Is this a client with authority over this transform?
// This component could be on the player object or any object that has been assigned authority to this client.
protected bool IsClientWithAuthority => isClient && authority;
[Tooltip("Occasionally send a full reliable state to delta compress against. This only applies to Components with SyncMethod=Unreliable.")]
public int baselineRate = 1;
public float baselineInterval => baselineRate < int.MaxValue ? 1f / baselineRate : 0; // for 1 Hz, that's 1000ms
protected double lastBaselineTime;
protected double lastDeltaTime;
// delta compression needs to remember 'last' to compress against.
byte lastSerializedBaselineTick = 0;
byte lastDeserializedBaselineTick = 0;
[Tooltip("Enable to send all unreliable messages twice. Only useful for extremely fast-paced games since it doubles bandwidth costs.")]
public bool unreliableRedundancy = false;
[Tooltip("When sending a reliable baseline, should we also send an unreliable delta or rely on the reliable baseline to arrive in a similar time?")]
public bool baselineIsDelta = true;
// change detection: we need to do this carefully in order to get it right.
//
// DONT just check changes in UpdateBaseline(). this would introduce MrG's grid issue:
// server start in A1, reliable baseline sent to client
// server moves to A2, unreliabe delta sent to client
// server moves to A1, nothing is sent to client becuase last baseline position == position
// => client wouldn't know we moved back to A1
//
// INSTEAD: every update() check for changes since baseline:
// UpdateDelta() keeps sending only if changed since _baseline_
// UpdateBaseline() resends if there was any change in the period since last baseline.
// => this avoids the A1->A2->A1 grid issue above
bool changedSinceBaseline = false;
[Header("Debug")]
public bool debugLog = false;
public virtual void ResetState()
{
lastSerializedBaselineTick = 0;
lastDeserializedBaselineTick = 0;
changedSinceBaseline = false;
}
// user callbacks //////////////////////////////////////////////////////
protected abstract void OnSerializeBaseline(NetworkWriter writer);
protected abstract void OnDeserializeBaseline(NetworkReader reader, byte baselineTick);
protected abstract void OnSerializeDelta(NetworkWriter writer);
protected abstract void OnDeserializeDelta(NetworkReader reader, byte baselineTick);
// implementations must store the current baseline state when requested:
// - implementations can use this to compress deltas against
// - implementations can use this to detect changes since baseline
// this is called whenever a baseline was sent.
protected abstract void StoreState();
// implementations may compare current state to the last stored state.
// this way we only need to send another reliable baseline if changed since last.
// this is called every syncInterval, not every baseline sync interval.
// (see comments where this is called).
protected abstract bool StateChanged();
// user callback in case drops due to baseline mismatch need to be logged/visualized/debugged.
protected virtual void OnDrop(byte lastBaselineTick, byte baselineTick, NetworkReader reader) {}
// rpcs / cmds /////////////////////////////////////////////////////////
// reliable baseline.
// include owner in case of server authority.
[ClientRpc(channel = Channels.Reliable)]
void RpcServerToClientBaseline(ArraySegment<byte> data)
{
// baseline is broadcast to all clients.
// ignore if this object is owned by this client.
if (IsClientWithAuthority) return;
// host mode: baseline Rpc is also sent through host's local connection and applied.
// applying host's baseline as last deserialized would overwrite the owner client's data and cause jitter.
// in other words: never apply the rpcs in host mode.
if (isServer) return;
using (NetworkReaderPooled reader = NetworkReaderPool.Get(data))
{
// deserialize
// save last deserialized baseline tick number to compare deltas against
lastDeserializedBaselineTick = reader.ReadByte();
OnDeserializeBaseline(reader, lastDeserializedBaselineTick);
}
}
// unreliable delta.
// include owner in case of server authority.
[ClientRpc(channel = Channels.Unreliable)]
void RpcServerToClientDelta(ArraySegment<byte> data)
{
// delta is broadcast to all clients.
// ignore if this object is owned by this client.
if (IsClientWithAuthority) return;
// host mode: baseline Rpc is also sent through host's local connection and applied.
// applying host's baseline as last deserialized would overwrite the owner client's data and cause jitter.
// in other words: never apply the rpcs in host mode.
if (isServer) return;
// deserialize
using (NetworkReaderPooled reader = NetworkReaderPool.Get(data))
{
// deserialize
byte baselineTick = reader.ReadByte();
// ensure this delta is for our last known baseline.
// we should never apply a delta on top of a wrong baseline.
if (baselineTick != lastDeserializedBaselineTick)
{
OnDrop(lastDeserializedBaselineTick, baselineTick, reader);
// this can happen if unreliable arrives before reliable etc.
// no need to log this except when debugging.
if (debugLog) Debug.Log($"[{name}] Client: received delta for wrong baseline #{baselineTick}. Last was {lastDeserializedBaselineTick}. Ignoring.");
return;
}
OnDeserializeDelta(reader, baselineTick);
}
}
[Command(channel = Channels.Reliable)] // reliable baseline
void CmdClientToServerBaseline(ArraySegment<byte> data)
{
// deserialize
using (NetworkReaderPooled reader = NetworkReaderPool.Get(data))
{
// deserialize
lastDeserializedBaselineTick = reader.ReadByte();
OnDeserializeBaseline(reader, lastDeserializedBaselineTick);
}
}
[Command(channel = Channels.Unreliable)] // unreliable delta
void CmdClientToServerDelta(ArraySegment<byte> data)
{
using (NetworkReaderPooled reader = NetworkReaderPool.Get(data))
{
// deserialize
byte baselineTick = reader.ReadByte();
// ensure this delta is for our last known baseline.
// we should never apply a delta on top of a wrong baseline.
if (baselineTick != lastDeserializedBaselineTick)
{
OnDrop(lastDeserializedBaselineTick, baselineTick, reader);
// this can happen if unreliable arrives before reliable etc.
// no need to log this except when debugging.
if (debugLog) Debug.Log($"[{name}] Server: received delta for wrong baseline #{baselineTick} from: {connectionToClient}. Last was {lastDeserializedBaselineTick}. Ignoring.");
return;
}
OnDeserializeDelta(reader, baselineTick);
}
}
// update server ///////////////////////////////////////////////////////
protected virtual void UpdateServerBaseline(double localTime)
{
// send a reliable baseline every 1 Hz
if (localTime < lastBaselineTime + baselineInterval) return;
// only sync if changed since last reliable baseline
if (!changedSinceBaseline) return;
// save bandwidth by only transmitting what is needed.
// -> ArraySegment with random data is slower since byte[] copying
// -> Vector3? and Quaternion? nullables takes more bandwidth
byte frameCount = (byte)Time.frameCount; // perf: only access Time.frameCount once!
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
{
// serialize
writer.WriteByte(frameCount);
OnSerializeBaseline(writer);
// send (no need for redundancy since baseline is reliable)
RpcServerToClientBaseline(writer);
}
// save the last baseline's tick number.
// included in baseline to identify which one it was on client
// included in deltas to ensure they are on top of the correct baseline
lastSerializedBaselineTick = frameCount;
lastBaselineTime = NetworkTime.localTime;
// perf. & bandwidth optimization:
// send a delta right after baseline to avoid potential head of
// line blocking, or skip the delta whenever we sent reliable?
// for example:
// 1 Hz baseline
// 10 Hz delta
// => 11 Hz total if we still send delta after reliable
// => 10 Hz total if we skip delta after reliable
// in that case, skip next delta by simply resetting last delta sync's time.
if (baselineIsDelta) lastDeltaTime = localTime;
// request to store last baseline state (i.e. position) for change detection.
StoreState();
// baseline was just sent after a change. reset change detection.
changedSinceBaseline = false;
if (debugLog) Debug.Log($"[{name}] Server: sent baseline #{lastSerializedBaselineTick} to: {connectionToClient} at time: {localTime}");
}
protected virtual void UpdateServerDelta(double localTime)
{
// broadcast to all clients each 'sendInterval'
// (client with authority will drop the rpc)
// NetworkTime.localTime for double precision until Unity has it too
//
// IMPORTANT:
// snapshot interpolation requires constant sending.
// DO NOT only send if position changed. for example:
// ---
// * client sends first position at t=0
// * ... 10s later ...
// * client moves again, sends second position at t=10
// ---
// * server gets first position at t=0
// * server gets second position at t=10
// * server moves from first to second within a time of 10s
// => would be a super slow move, instead of a wait & move.
//
// IMPORTANT:
// DO NOT send nulls if not changed 'since last send' either. we
// send unreliable and don't know which 'last send' the other end
// received successfully.
//
// Checks to ensure server only sends snapshots if object is
// on server authority(!clientAuthority) mode because on client
// authority mode snapshots are broadcasted right after the authoritative
// client updates server in the command function(see above), OR,
// since host does not send anything to update the server, any client
// authoritative movement done by the host will have to be broadcasted
// here by checking IsClientWithAuthority.
// TODO send same time that NetworkServer sends time snapshot?
if (localTime < lastDeltaTime + syncInterval) return;
// look for changes every unreliable sendInterval!
// every reliable interval isn't enough, this would cause MrG's grid issue:
// server start in A1, reliable baseline sent to clients
// server moves to A2, unreliabe delta sent to clients
// server moves back to A1, nothing is sent to clients because last baseline position == position
// => clients wouldn't know we moved back to A1
// every update works, but it's unnecessary overhead since sends only happen every sendInterval
// every unreliable sendInterval is the perfect place to look for changes.
if (StateChanged()) changedSinceBaseline = true;
// only sync on change:
// unreliable isn't guaranteed to be delivered so this depends on reliable baseline.
if (!changedSinceBaseline) return;
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
{
// serialize
writer.WriteByte(lastSerializedBaselineTick);
OnSerializeDelta(writer);
// send (with optional redundancy to make up for message drops)
RpcServerToClientDelta(writer);
if (unreliableRedundancy)
RpcServerToClientDelta(writer);
}
lastDeltaTime = localTime;
if (debugLog) Debug.Log($"[{name}] Server: sent delta for #{lastSerializedBaselineTick} to: {connectionToClient} at time: {localTime}");
}
protected virtual void UpdateServerSync()
{
// server broadcasts all objects all the time.
// -> not just ServerToClient: ClientToServer need to be broadcast to others too
// perf: only grab NetworkTime.localTime property once.
double localTime = NetworkTime.localTime;
// broadcast
UpdateServerBaseline(localTime);
UpdateServerDelta(localTime);
}
// update client ///////////////////////////////////////////////////////
protected virtual void UpdateClientBaseline(double localTime)
{
// send a reliable baseline every 1 Hz
if (localTime < lastBaselineTime + baselineInterval) return;
// only sync if changed since last reliable baseline
if (!changedSinceBaseline) return;
// save bandwidth by only transmitting what is needed.
// -> ArraySegment with random data is slower since byte[] copying
// -> Vector3? and Quaternion? nullables takes more bandwidth
byte frameCount = (byte)Time.frameCount; // perf: only access Time.frameCount once!
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
{
// serialize
writer.WriteByte(frameCount);
OnSerializeBaseline(writer);
// send (no need for redundancy since baseline is reliable)
CmdClientToServerBaseline(writer);
}
// save the last baseline's tick number.
// included in baseline to identify which one it was on client
// included in deltas to ensure they are on top of the correct baseline
lastSerializedBaselineTick = frameCount;
lastBaselineTime = NetworkTime.localTime;
// perf. & bandwidth optimization:
// send a delta right after baseline to avoid potential head of
// line blocking, or skip the delta whenever we sent reliable?
// for example:
// 1 Hz baseline
// 10 Hz delta
// => 11 Hz total if we still send delta after reliable
// => 10 Hz total if we skip delta after reliable
// in that case, skip next delta by simply resetting last delta sync's time.
if (baselineIsDelta) lastDeltaTime = localTime;
// request to store last baseline state (i.e. position) for change detection.
// IMPORTANT
// OnSerialize(initial) is called for the spawn payload whenever
// someone starts observing this object. we always must make
// this the new baseline, otherwise this happens:
// - server broadcasts baseline @ t=1
// - server broadcasts delta for baseline @ t=1
// - ... time passes ...
// - new observer -> OnSerialize sends current position @ t=2
// - server broadcasts delta for baseline @ t=1
// => client's baseline is t=2 but receives delta for t=1 _!_
StoreState();
// baseline was just sent after a change. reset change detection.
changedSinceBaseline = false;
if (debugLog) Debug.Log($"[{name}] Client: sent baseline #{lastSerializedBaselineTick} at time: {localTime}");
}
protected virtual void UpdateClientDelta(double localTime)
{
// send to server each 'sendInterval'
// NetworkTime.localTime for double precision until Unity has it too
//
// IMPORTANT:
// snapshot interpolation requires constant sending.
// DO NOT only send if position changed. for example:
// ---
// * client sends first position at t=0
// * ... 10s later ...
// * client moves again, sends second position at t=10
// ---
// * server gets first position at t=0
// * server gets second position at t=10
// * server moves from first to second within a time of 10s
// => would be a super slow move, instead of a wait & move.
//
// IMPORTANT:
// DO NOT send nulls if not changed 'since last send' either. we
// send unreliable and don't know which 'last send' the other end
// received successfully.
if (localTime < lastDeltaTime + syncInterval) return;
// look for changes every unreliable sendInterval!
// every reliable interval isn't enough, this would cause MrG's grid issue:
// client start in A1, reliable baseline sent to server and other clients
// client moves to A2, unreliabe delta sent to server and other clients
// client moves back to A1, nothing is sent to server because last baseline position == position
// => server / other clients wouldn't know we moved back to A1
// every update works, but it's unnecessary overhead since sends only happen every sendInterval
// every unreliable sendInterval is the perfect place to look for changes.
if (StateChanged()) changedSinceBaseline = true;
// only sync on change:
// unreliable isn't guaranteed to be delivered so this depends on reliable baseline.
if (!changedSinceBaseline) return;
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
{
// serialize
writer.WriteByte(lastSerializedBaselineTick);
OnSerializeDelta(writer);
// send (with optional redundancy to make up for message drops)
CmdClientToServerDelta(writer);
if (unreliableRedundancy)
CmdClientToServerDelta(writer);
}
lastDeltaTime = localTime;
if (debugLog) Debug.Log($"[{name}] Client: sent delta for #{lastSerializedBaselineTick} at time: {localTime}");
}
protected virtual void UpdateClientSync()
{
// client authority, and local player (= allowed to move myself)?
if (IsClientWithAuthority)
{
// https://github.com/vis2k/Mirror/pull/2992/
if (!NetworkClient.ready) return;
// perf: only grab NetworkTime.localTime property once.
double localTime = NetworkTime.localTime;
UpdateClientBaseline(localTime);
UpdateClientDelta(localTime);
}
}
// Update() without LateUpdate() split: otherwise perf. is cut in half!
protected virtual void Update()
{
// if server then always sync to others.
if (isServer) UpdateServerSync();
// 'else if' because host mode shouldn't send anything to server.
// it is the server. don't overwrite anything there.
else if (isClient) UpdateClientSync();
}
// OnSerialize(initial) is called every time when a player starts observing us.
// note this is _not_ called just once on spawn.
// call this from inheriting classes immediately in OnSerialize().
public override void OnSerialize(NetworkWriter writer, bool initialState)
{
if (initialState)
{
// always include the tick for deltas to compare against.
byte frameCount = (byte)Time.frameCount; // perf: only access Time.frameCount once!
writer.WriteByte(frameCount);
// IMPORTANT
// OnSerialize(initial) is called for the spawn payload whenever
// someone starts observing this object. we always must make
// this the new baseline, otherwise this happens:
// - server broadcasts baseline @ t=1
// - server broadcasts delta for baseline @ t=1
// - ... time passes ...
// - new observer -> OnSerialize sends current position @ t=2
// - server broadcasts delta for baseline @ t=1
// => client's baseline is t=2 but receives delta for t=1 _!_
lastSerializedBaselineTick = (byte)Time.frameCount;
lastBaselineTime = NetworkTime.localTime;
// request to store last baseline state (i.e. position) for change detection.
StoreState();
}
}
// call this from inheriting classes immediately in OnDeserialize().
public override void OnDeserialize(NetworkReader reader, bool initialState)
{
if (initialState)
{
// save last deserialized baseline tick number to compare deltas against
lastDeserializedBaselineTick = reader.ReadByte();
}
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 657535a722c74173bdaa18a4394ce016
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/NetworkBehaviourHybrid.cs
uploadId: 736421

View File

@ -0,0 +1,33 @@
using System;
namespace Mirror
{
// backing field for sync NetworkBehaviour
public struct NetworkBehaviourSyncVar : IEquatable<NetworkBehaviourSyncVar>
{
public uint netId;
// limited to 255 behaviours per identity
public byte componentIndex;
public NetworkBehaviourSyncVar(uint netId, int componentIndex) : this()
{
this.netId = netId;
this.componentIndex = (byte)componentIndex;
}
public bool Equals(NetworkBehaviourSyncVar other)
{
return other.netId == netId && other.componentIndex == componentIndex;
}
public bool Equals(uint netId, int componentIndex)
{
return this.netId == netId && this.componentIndex == componentIndex;
}
public override string ToString()
{
return $"[netId:{netId} compIndex:{componentIndex}]";
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: b04fe7518657486089dfaf811db0b3ea
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/NetworkBehaviourSyncVar.cs
uploadId: 736421

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: abe6be14204d94224a3e7cd99dd2ea73
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/NetworkClient.cs
uploadId: 736421

View File

@ -0,0 +1,151 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Mirror
{
public static partial class NetworkClient
{
// snapshot interpolation settings /////////////////////////////////////
// TODO expose the settings to the user later.
// via NetMan or NetworkClientConfig or NetworkClient as component etc.
public static SnapshotInterpolationSettings snapshotSettings = new SnapshotInterpolationSettings();
// snapshot interpolation runtime data /////////////////////////////////
// buffer time is dynamically adjusted.
// store the current multiplier here, without touching the original in settings.
// this way we can easily reset to or compare with original where needed.
public static double bufferTimeMultiplier;
// original buffer time based on the settings
// dynamically adjusted buffer time based on dynamically adjusted multiplier
public static double initialBufferTime => NetworkServer.sendInterval * snapshotSettings.bufferTimeMultiplier;
public static double bufferTime => NetworkServer.sendInterval * bufferTimeMultiplier;
// <servertime, snaps>
public static SortedList<double, TimeSnapshot> snapshots = new SortedList<double, TimeSnapshot>();
// for smooth interpolation, we need to interpolate along server time.
// any other time (arrival on client, client local time, etc.) is not
// going to give smooth results.
// in other words, this is the remote server's time, but adjusted.
//
// internal for use from NetworkTime.
// double for long running servers, see NetworkTime comments.
internal static double localTimeline;
// catchup / slowdown adjustments are applied to timescale,
// to be adjusted in every update instead of when receiving messages.
internal static double localTimescale = 1;
// catchup /////////////////////////////////////////////////////////////
// we use EMA to average the last second worth of snapshot time diffs.
// manually averaging the last second worth of values with a for loop
// would be the same, but a moving average is faster because we only
// ever add one value.
static ExponentialMovingAverage driftEma;
// dynamic buffer time adjustment //////////////////////////////////////
// DEPRECATED 2024-10-08
[Obsolete("NeworkClient.dynamicAdjustment was moved to NetworkClient.snapshotSettings.dynamicAdjustment")]
public static bool dynamicAdjustment => snapshotSettings.dynamicAdjustment;
// DEPRECATED 2024-10-08
[Obsolete("NeworkClient.dynamicAdjustmentTolerance was moved to NetworkClient.snapshotSettings.dynamicAdjustmentTolerance")]
public static float dynamicAdjustmentTolerance => snapshotSettings.dynamicAdjustmentTolerance;
// DEPRECATED 2024-10-08
[Obsolete("NeworkClient.dynamicAdjustment was moved to NetworkClient.snapshotSettings.dynamicAdjustment")]
public static int deliveryTimeEmaDuration => snapshotSettings.deliveryTimeEmaDuration;
static ExponentialMovingAverage deliveryTimeEma; // average delivery time (standard deviation gives average jitter)
// OnValidate: see NetworkClient.cs
// add snapshot & initialize client interpolation time if needed
// initialization called from Awake
static void InitTimeInterpolation()
{
// reset timeline, localTimescale & snapshots from last session (if any)
bufferTimeMultiplier = snapshotSettings.bufferTimeMultiplier;
localTimeline = 0;
localTimescale = 1;
snapshots.Clear();
// initialize EMA with 'emaDuration' seconds worth of history.
// 1 second holds 'sendRate' worth of values.
// multiplied by emaDuration gives n-seconds.
driftEma = new ExponentialMovingAverage(NetworkServer.sendRate * snapshotSettings.driftEmaDuration);
deliveryTimeEma = new ExponentialMovingAverage(NetworkServer.sendRate * snapshotSettings.deliveryTimeEmaDuration);
}
// server sends TimeSnapshotMessage every sendInterval.
// batching already includes the remoteTimestamp.
// we simply insert it on-message here.
// => only for reliable channel. unreliable would always arrive earlier.
static void OnTimeSnapshotMessage(TimeSnapshotMessage _)
{
// insert another snapshot for snapshot interpolation.
// before calling OnDeserialize so components can use
// NetworkTime.time and NetworkTime.timeStamp.
// Unity 2019 doesn't have Time.timeAsDouble yet
OnTimeSnapshot(new TimeSnapshot(connection.remoteTimeStamp, NetworkTime.localTime));
}
// see comments at the top of this file
public static void OnTimeSnapshot(TimeSnapshot snap)
{
// Debug.Log($"NetworkClient: OnTimeSnapshot @ {snap.remoteTime:F3}");
// (optional) dynamic adjustment
if (snapshotSettings.dynamicAdjustment)
{
// set bufferTime on the fly.
// shows in inspector for easier debugging :)
bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment(
NetworkServer.sendInterval,
deliveryTimeEma.StandardDeviation,
snapshotSettings.dynamicAdjustmentTolerance
);
}
// insert into the buffer & initialize / adjust / catchup
SnapshotInterpolation.InsertAndAdjust(
snapshots,
snapshotSettings.bufferLimit,
snap,
ref localTimeline,
ref localTimescale,
NetworkServer.sendInterval,
bufferTime,
snapshotSettings.catchupSpeed,
snapshotSettings.slowdownSpeed,
ref driftEma,
snapshotSettings.catchupNegativeThreshold,
snapshotSettings.catchupPositiveThreshold,
ref deliveryTimeEma);
// Debug.Log($"inserted TimeSnapshot remote={snap.remoteTime:F2} local={snap.localTime:F2} total={snapshots.Count}");
}
// call this from early update, so the timeline is safe to use in update
static void UpdateTimeInterpolation()
{
// only while we have snapshots.
// timeline starts when the first snapshot arrives.
if (snapshots.Count > 0)
{
// progress local timeline.
// NetworkTime uses unscaled time and ignores Time.timeScale.
// fixes Time.timeScale getting server & client time out of sync:
// https://github.com/MirrorNetworking/Mirror/issues/3409
SnapshotInterpolation.StepTime(Time.unscaledDeltaTime, ref localTimeline, localTimescale);
// progress local interpolation.
// TimeSnapshot doesn't interpolate anything.
// this is merely to keep removing older snapshots.
SnapshotInterpolation.StepInterpolation(snapshots, localTimeline, out _, out _, out double t);
// Debug.Log($"NetworkClient SnapshotInterpolation @ {localTimeline:F2} t={t:F2}");
}
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: ad039071a9cc487b9f7831d28bbe8e83
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs
uploadId: 736421

View File

@ -0,0 +1,207 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine;
namespace Mirror
{
/// <summary>Base NetworkConnection class for server-to-client and client-to-server connection.</summary>
public abstract class NetworkConnection
{
public const int LocalConnectionId = 0;
/// <summary>Flag that indicates the client has been authenticated.</summary>
public bool isAuthenticated;
/// <summary>General purpose object to hold authentication data, character selection, tokens, etc.</summary>
public object authenticationData;
/// <summary>A server connection is ready after joining the game world.</summary>
// TODO move this to ConnectionToClient so the flag only lives on server
// connections? clients could use NetworkClient.ready to avoid redundant
// state.
public bool isReady;
/// <summary>Last time a message was received for this connection. Includes system and user messages.</summary>
public float lastMessageTime;
/// <summary>This connection's main object (usually the player object).</summary>
public NetworkIdentity identity { get; internal set; }
/// <summary>All NetworkIdentities owned by this connection. Can be main player, pets, etc.</summary>
// .owned is now valid both on server and on client.
// IMPORTANT: this needs to be <NetworkIdentity>, not <uint netId>.
// fixes a bug where DestroyOwnedObjects wouldn't find the
// netId anymore: https://github.com/vis2k/Mirror/issues/1380
// Works fine with NetworkIdentity pointers though.
public readonly HashSet<NetworkIdentity> owned = new HashSet<NetworkIdentity>();
// batching from server to client & client to server.
// fewer transport calls give us significantly better performance/scale.
//
// for a 64KB max message transport and 64 bytes/message on average, we
// reduce transport calls by a factor of 1000.
//
// depending on the transport, this can give 10x performance.
//
// Dictionary<channelId, batch> because we have multiple channels.
protected Dictionary<int, Batcher> batches = new Dictionary<int, Batcher>();
/// <summary>last batch's remote timestamp. not interpolated. useful for NetworkTransform etc.</summary>
// for any given NetworkMessage/Rpc/Cmd/OnSerialize, this was the time
// on the REMOTE END when it was sent.
//
// NOTE: this is NOT in NetworkTime, it needs to be per-connection
// because the server receives different batch timestamps from
// different connections.
public double remoteTimeStamp { get; internal set; }
internal NetworkConnection()
{
// set lastTime to current time when creating connection to make
// sure it isn't instantly kicked for inactivity
lastMessageTime = Time.time;
}
// TODO if we only have Reliable/Unreliable, then we could initialize
// two batches and avoid this code
protected Batcher GetBatchForChannelId(int channelId)
{
// get existing or create new writer for the channelId
Batcher batch;
if (!batches.TryGetValue(channelId, out batch))
{
// get max batch size for this channel
int threshold = Transport.active.GetBatchThreshold(channelId);
// create batcher
batch = new Batcher(threshold);
batches[channelId] = batch;
}
return batch;
}
// Send stage one: NetworkMessage<T>
/// <summary>Send a NetworkMessage to this connection over the given channel.</summary>
public void Send<T>(T message, int channelId = Channels.Reliable)
where T : struct, NetworkMessage
{
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
{
// pack message
NetworkMessages.Pack(message, writer);
// validate packet size immediately.
// we know how much can fit into one batch at max.
// if it's larger, log an error immediately with the type <T>.
// previously we only logged in Update() when processing batches,
// but there we don't have type information anymore.
int max = NetworkMessages.MaxMessageSize(channelId);
if (writer.Position > max)
{
Debug.LogError($"NetworkConnection.Send: message of type {typeof(T)} with a size of {writer.Position} bytes is larger than the max allowed message size in one batch: {max}.\nThe message was dropped, please make it smaller.");
return;
}
// send allocation free
NetworkDiagnostics.OnSend(message, channelId, writer.Position, 1);
Send(writer.ToArraySegment(), channelId);
}
}
// Send stage two: serialized NetworkMessage as ArraySegment<byte>
// internal because no one except Mirror should send bytes directly to
// the client. they would be detected as a message. send messages instead.
// => make sure to validate message<T> size before calling Send<byte>!
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal virtual void Send(ArraySegment<byte> segment, int channelId = Channels.Reliable)
{
//Debug.Log($"ConnectionSend {this} bytes:{BitConverter.ToString(segment.Array, segment.Offset, segment.Count)}");
// add to batch no matter what.
// batching will try to fit as many as possible into MTU.
// but we still allow > MTU, e.g. kcp max packet size 144kb.
// those are simply sent as single batches.
//
// IMPORTANT: do NOT send > batch sized messages directly:
// - data race: large messages would be sent directly. small
// messages would be sent in the batch at the end of frame
// - timestamps: if batching assumes a timestamp, then large
// messages need that too.
//
// NOTE: we ALWAYS batch. it's not optional, because the
// receiver needs timestamps for NT etc.
//
// NOTE: we do NOT ValidatePacketSize here yet. the final packet
// will be the full batch, including timestamp.
GetBatchForChannelId(channelId).AddMessage(segment, NetworkTime.localTime);
}
// Send stage three: hand off to transport
protected abstract void SendToTransport(ArraySegment<byte> segment, int channelId = Channels.Reliable);
// flush batched messages at the end of every Update.
internal virtual void Update()
{
// go through batches for all channels
// foreach ((int key, Batcher batcher) in batches) // Unity 2020 doesn't support deconstruct yet
foreach (KeyValuePair<int, Batcher> kvp in batches)
{
// make and send as many batches as necessary from the stored
// messages.
using (NetworkWriterPooled writer = NetworkWriterPool.Get())
{
// make a batch with our local time (double precision)
while (kvp.Value.GetBatch(writer))
{
// message size is validated in Send<T>, with test coverage.
// we can send directly without checking again.
ArraySegment<byte> segment = writer.ToArraySegment();
// send to transport
SendToTransport(segment, kvp.Key);
//UnityEngine.Debug.Log($"sending batch of {writer.Position} bytes for channel={kvp.Key} connId={connectionId}");
// reset writer for each new batch
writer.Position = 0;
}
}
}
}
/// <summary>Check if we received a message within the last 'timeout' seconds.</summary>
internal virtual bool IsAlive(float timeout) => Time.time - lastMessageTime < timeout;
/// <summary>Disconnects this connection.</summary>
// for future reference, here is how Disconnects work in Mirror.
//
// first, there are two types of disconnects:
// * voluntary: the other end simply disconnected
// * involuntary: server disconnects a client by itself
//
// UNET had special (complex) code to handle both cases differently.
//
// Mirror handles both cases the same way:
// * Disconnect is called from TOP to BOTTOM
// NetworkServer/Client -> NetworkConnection -> Transport.Disconnect()
// * Disconnect is handled from BOTTOM to TOP
// Transport.OnDisconnected -> ...
//
// in other words, calling Disconnect() does no cleanup whatsoever.
// it simply asks the transport to disconnect.
// then later the transport events will do the clean up.
public abstract void Disconnect();
// cleanup is called before the connection is removed.
// return any batches' pooled writers before the connection disappears.
// otherwise if a connection disappears before flushing, writers would
// never be returned to the pool.
public virtual void Cleanup()
{
foreach (Batcher batcher in batches.Values)
{
batcher.Clear();
}
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 11ea41db366624109af1f0834bcdde2f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/NetworkConnection.cs
uploadId: 736421

View File

@ -0,0 +1,230 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine;
namespace Mirror
{
public class NetworkConnectionToClient : NetworkConnection
{
// rpcs are collected in a buffer, and then flushed out together.
// this way we don't need one NetworkMessage per rpc.
// => prepares for LocalWorldState as well.
// ensure max size when adding!
readonly NetworkWriter reliableRpcs = new NetworkWriter();
readonly NetworkWriter unreliableRpcs = new NetworkWriter();
public virtual string address { get; private set; }
/// <summary>Unique identifier for this connection that is assigned by the transport layer.</summary>
// assigned by transport, this id is unique for every connection on server.
// clients don't know their own id and they don't know other client's ids.
public readonly int connectionId;
/// <summary>NetworkIdentities that this connection can see</summary>
// TODO move to server's NetworkConnectionToClient?
public readonly HashSet<NetworkIdentity> observing = new HashSet<NetworkIdentity>();
// unbatcher
public Unbatcher unbatcher = new Unbatcher();
// server runs a time snapshot interpolation for each client's local time.
// this is necessary for client auth movement to still be smooth on the
// server for host mode.
// TODO move them along server's timeline in the future.
// perhaps with an offset.
// for now, keep compatibility by manually constructing a timeline.
ExponentialMovingAverage driftEma;
ExponentialMovingAverage deliveryTimeEma; // average delivery time (standard deviation gives average jitter)
public double remoteTimeline;
public double remoteTimescale;
double bufferTimeMultiplier = 2;
double bufferTime => NetworkServer.sendInterval * bufferTimeMultiplier;
// <clienttime, snaps>
readonly SortedList<double, TimeSnapshot> snapshots = new SortedList<double, TimeSnapshot>();
// Snapshot Buffer size limit to avoid ever growing list memory consumption attacks from clients.
public int snapshotBufferSizeLimit = 64;
// ping for rtt (round trip time)
// useful for statistics, lag compensation, etc.
double lastPingTime = 0;
internal ExponentialMovingAverage _rtt = new ExponentialMovingAverage(NetworkTime.PingWindowSize);
/// <summary>Round trip time (in seconds) that it takes a message to go server->client->server.</summary>
public double rtt => _rtt.Value;
internal NetworkConnectionToClient() : base() { }
public NetworkConnectionToClient(int networkConnectionId, string clientAddress = "localhost") : base()
{
connectionId = networkConnectionId;
address = clientAddress;
// initialize EMA with 'emaDuration' seconds worth of history.
// 1 second holds 'sendRate' worth of values.
// multiplied by emaDuration gives n-seconds.
driftEma = new ExponentialMovingAverage(NetworkServer.sendRate * NetworkClient.snapshotSettings.driftEmaDuration);
deliveryTimeEma = new ExponentialMovingAverage(NetworkServer.sendRate * NetworkClient.snapshotSettings.deliveryTimeEmaDuration);
// buffer limit should be at least multiplier to have enough in there
snapshotBufferSizeLimit = Mathf.Max((int)NetworkClient.snapshotSettings.bufferTimeMultiplier, snapshotBufferSizeLimit);
}
public override string ToString() => $"connection({connectionId})";
public void OnTimeSnapshot(TimeSnapshot snapshot)
{
// protect against ever growing buffer size attacks
if (snapshots.Count >= snapshotBufferSizeLimit) return;
// (optional) dynamic adjustment
if (NetworkClient.snapshotSettings.dynamicAdjustment)
{
// set bufferTime on the fly.
// shows in inspector for easier debugging :)
bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment(
NetworkServer.sendInterval,
deliveryTimeEma.StandardDeviation,
NetworkClient.snapshotSettings.dynamicAdjustmentTolerance
);
// Debug.Log($"[Server]: {name} delivery std={serverDeliveryTimeEma.StandardDeviation} bufferTimeMult := {bufferTimeMultiplier} ");
}
// insert into the server buffer & initialize / adjust / catchup
SnapshotInterpolation.InsertAndAdjust(
snapshots,
NetworkClient.snapshotSettings.bufferLimit,
snapshot,
ref remoteTimeline,
ref remoteTimescale,
NetworkServer.sendInterval,
bufferTime,
NetworkClient.snapshotSettings.catchupSpeed,
NetworkClient.snapshotSettings.slowdownSpeed,
ref driftEma,
NetworkClient.snapshotSettings.catchupNegativeThreshold,
NetworkClient.snapshotSettings.catchupPositiveThreshold,
ref deliveryTimeEma
);
}
public void UpdateTimeInterpolation()
{
// timeline starts when the first snapshot arrives.
if (snapshots.Count > 0)
{
// progress local timeline.
SnapshotInterpolation.StepTime(Time.unscaledDeltaTime, ref remoteTimeline, remoteTimescale);
// progress local interpolation.
// TimeSnapshot doesn't interpolate anything.
// this is merely to keep removing older snapshots.
SnapshotInterpolation.StepInterpolation(snapshots, remoteTimeline, out _, out _, out _);
// Debug.Log($"NetworkClient SnapshotInterpolation @ {localTimeline:F2} t={t:F2}");
}
}
// Send stage three: hand off to transport
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void SendToTransport(ArraySegment<byte> segment, int channelId = Channels.Reliable) =>
Transport.active.ServerSend(connectionId, segment, channelId);
protected virtual void UpdatePing()
{
// localTime (double) instead of Time.time for accuracy over days
if (NetworkTime.localTime >= lastPingTime + NetworkTime.PingInterval)
{
// TODO it would be safer for the server to store the last N
// messages' timestamp and only send a message number.
// This way client's can't just modify the timestamp.
// predictedTime parameter is 0 because the server doesn't predict.
NetworkPingMessage pingMessage = new NetworkPingMessage(NetworkTime.localTime, 0);
Send(pingMessage, Channels.Unreliable);
lastPingTime = NetworkTime.localTime;
}
}
internal override void Update()
{
UpdatePing();
base.Update();
}
/// <summary>Disconnects this connection.</summary>
public override void Disconnect()
{
// set not ready and handle clientscene disconnect in any case
// (might be client or host mode here)
isReady = false;
reliableRpcs.Position = 0;
unreliableRpcs.Position = 0;
Transport.active.ServerDisconnect(connectionId);
// IMPORTANT: NetworkConnection.Disconnect() is NOT called for
// voluntary disconnects from the other end.
// -> so all 'on disconnect' cleanup code needs to be in
// OnTransportDisconnect, where it's called for both voluntary
// and involuntary disconnects!
}
internal void AddToObserving(NetworkIdentity netIdentity)
{
observing.Add(netIdentity);
// spawn identity for this conn
NetworkServer.ShowForConnection(netIdentity, this);
}
internal void RemoveFromObserving(NetworkIdentity netIdentity, bool isDestroyed)
{
observing.Remove(netIdentity);
if (!isDestroyed)
{
// hide identity for this conn
NetworkServer.HideForConnection(netIdentity, this);
}
}
internal void RemoveFromObservingsObservers()
{
foreach (NetworkIdentity netIdentity in observing)
{
netIdentity.RemoveObserver(this);
}
observing.Clear();
}
internal void AddOwnedObject(NetworkIdentity obj)
{
owned.Add(obj);
}
internal void RemoveOwnedObject(NetworkIdentity obj)
{
owned.Remove(obj);
}
internal void DestroyOwnedObjects()
{
// create a copy because the list might be modified when destroying
HashSet<NetworkIdentity> tmp = new HashSet<NetworkIdentity>(owned);
foreach (NetworkIdentity netIdentity in tmp)
{
if (netIdentity != null)
{
// disown scene objects, destroy instantiated objects.
if (netIdentity.sceneId != 0)
NetworkServer.RemovePlayerForConnection(this, RemovePlayerOptions.KeepActive);
else
NetworkServer.Destroy(netIdentity.gameObject);
}
}
// clear the hashset because we destroyed them all
owned.Clear();
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: bb2195f8b29d24f0680a57fde2e9fd09
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/NetworkConnectionToClient.cs
uploadId: 736421

View File

@ -0,0 +1,24 @@
using System;
using System.Runtime.CompilerServices;
namespace Mirror
{
public class NetworkConnectionToServer : NetworkConnection
{
// Send stage three: hand off to transport
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void SendToTransport(ArraySegment<byte> segment, int channelId = Channels.Reliable) =>
Transport.active.ClientSend(segment, channelId);
/// <summary>Disconnects this connection.</summary>
public override void Disconnect()
{
// set not ready and handle clientscene disconnect in any case
// (might be client or host mode here)
// TODO remove redundant state. have one source of truth for .ready!
isReady = false;
NetworkClient.ready = false;
Transport.active.ClientDisconnect();
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 761977cbf38a34ded9dd89de45445675
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/NetworkConnectionToServer.cs
uploadId: 736421

View File

@ -0,0 +1,63 @@
using System;
namespace Mirror
{
/// <summary>Profiling statistics for tool to subscribe to (profiler etc.)</summary>
public static class NetworkDiagnostics
{
/// <summary>Describes an outgoing message</summary>
public readonly struct MessageInfo
{
/// <summary>The message being sent</summary>
public readonly NetworkMessage message;
/// <summary>channel through which the message was sent</summary>
public readonly int channel;
/// <summary>how big was the message (does not include transport headers)</summary>
public readonly int bytes;
/// <summary>How many connections was the message sent to.</summary>
public readonly int count;
internal MessageInfo(NetworkMessage message, int channel, int bytes, int count)
{
this.message = message;
this.channel = channel;
this.bytes = bytes;
this.count = count;
}
}
/// <summary>Event for when Mirror sends a message. Can be subscribed to.</summary>
public static event Action<MessageInfo> OutMessageEvent;
/// <summary>Event for when Mirror receives a message. Can be subscribed to.</summary>
public static event Action<MessageInfo> InMessageEvent;
// RuntimeInitializeOnLoadMethod -> fast playmode without domain reload
[UnityEngine.RuntimeInitializeOnLoadMethod]
static void ResetStatics()
{
InMessageEvent = null;
OutMessageEvent = null;
}
internal static void OnSend<T>(T message, int channel, int bytes, int count)
where T : struct, NetworkMessage
{
if (count > 0 && OutMessageEvent != null)
{
MessageInfo outMessage = new MessageInfo(message, channel, bytes, count);
OutMessageEvent?.Invoke(outMessage);
}
}
internal static void OnReceive<T>(T message, int channel, int bytes)
where T : struct, NetworkMessage
{
if (InMessageEvent != null)
{
MessageInfo inMessage = new MessageInfo(message, channel, bytes, 1);
InMessageEvent?.Invoke(inMessage);
}
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: c3754b39e5f8740fd93f3337b2c4274e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/NetworkDiagnostics.cs
uploadId: 736421

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 9b91ecbcc199f4492b9a91e820070131
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/NetworkIdentity.cs
uploadId: 736421

View File

@ -0,0 +1,211 @@
// our ideal update looks like this:
// transport.process_incoming()
// update_world()
// transport.process_outgoing()
//
// this way we avoid unnecessary latency for low-ish server tick rates.
// for example, if we were to use this tick:
// transport.process_incoming/outgoing()
// update_world()
//
// then anything sent in update_world wouldn't be actually sent out by the
// transport until the next frame. if server runs at 60Hz, then this can add
// 16ms latency for every single packet.
//
// => instead we process incoming, update world, process_outgoing in the same
// frame. it's more clear (no race conditions) and lower latency.
// => we need to add custom Update functions to the Unity engine:
// NetworkEarlyUpdate before Update()/FixedUpdate()
// NetworkLateUpdate after LateUpdate()
// this way the user can update the world in Update/FixedUpdate/LateUpdate
// and networking still runs before/after those functions no matter what!
// => see also: https://docs.unity3d.com/Manual/ExecutionOrder.html
// => update order:
// * we add to the end of EarlyUpdate so it runs after any Unity initializations
// * we add to the end of PreLateUpdate so it runs after LateUpdate(). adding
// to the beginning of PostLateUpdate doesn't actually work.
using System;
using UnityEngine;
using UnityEngine.LowLevel;
using UnityEngine.PlayerLoop;
namespace Mirror
{
public static class NetworkLoop
{
// helper enum to add loop to begin/end of subSystemList
internal enum AddMode { Beginning, End }
// callbacks for others to hook into if they need Early/LateUpdate.
public static Action OnEarlyUpdate;
public static Action OnLateUpdate;
// RuntimeInitializeOnLoadMethod -> fast playmode without domain reload
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
static void ResetStatics()
{
OnEarlyUpdate = null;
OnLateUpdate = null;
}
// helper function to find an update function's index in a player loop
// type. this is used for testing to guarantee our functions are added
// at the beginning/end properly.
internal static int FindPlayerLoopEntryIndex(PlayerLoopSystem.UpdateFunction function, PlayerLoopSystem playerLoop, Type playerLoopSystemType)
{
// did we find the type? e.g. EarlyUpdate/PreLateUpdate/etc.
if (playerLoop.type == playerLoopSystemType)
return Array.FindIndex(playerLoop.subSystemList, (elem => elem.updateDelegate == function));
// recursively keep looking
if (playerLoop.subSystemList != null)
{
for (int i = 0; i < playerLoop.subSystemList.Length; ++i)
{
int index = FindPlayerLoopEntryIndex(function, playerLoop.subSystemList[i], playerLoopSystemType);
if (index != -1) return index;
}
}
return -1;
}
// MODIFIED AddSystemToPlayerLoopList from Unity.Entities.ScriptBehaviourUpdateOrder (ECS)
//
// => adds an update function to the Unity internal update type.
// => Unity has different update loops:
// https://medium.com/@thebeardphantom/unity-2018-and-playerloop-5c46a12a677
// EarlyUpdate
// FixedUpdate
// PreUpdate
// Update
// PreLateUpdate
// PostLateUpdate
//
// function: the custom update function to add
// IMPORTANT: according to a comment in Unity.Entities.ScriptBehaviourUpdateOrder,
// the UpdateFunction can not be virtual because
// Mono 4.6 has problems invoking virtual methods
// as delegates from native!
// ownerType: the .type to fill in so it's obvious who the new function
// belongs to. seems to be mostly for debugging. pass any.
// addMode: prepend or append to update list
internal static bool AddToPlayerLoop(PlayerLoopSystem.UpdateFunction function, Type ownerType, ref PlayerLoopSystem playerLoop, Type playerLoopSystemType, AddMode addMode)
{
// did we find the type? e.g. EarlyUpdate/PreLateUpdate/etc.
if (playerLoop.type == playerLoopSystemType)
{
// debugging
//Debug.Log($"Found playerLoop of type {playerLoop.type} with {playerLoop.subSystemList.Length} Functions:");
//foreach (PlayerLoopSystem sys in playerLoop.subSystemList)
// Debug.Log($" ->{sys.type}");
// make sure the function wasn't added yet.
// with domain reload disabled, it would otherwise be added twice:
// fixes: https://github.com/MirrorNetworking/Mirror/issues/3392
if (Array.FindIndex(playerLoop.subSystemList, (s => s.updateDelegate == function)) != -1)
{
// loop contains the function, so return true.
return true;
}
// resize & expand subSystemList to fit one more entry
int oldListLength = (playerLoop.subSystemList != null) ? playerLoop.subSystemList.Length : 0;
Array.Resize(ref playerLoop.subSystemList, oldListLength + 1);
// IMPORTANT: always insert a FRESH PlayerLoopSystem!
// We CAN NOT resize and then OVERWRITE an entry's type/loop.
// => PlayerLoopSystem has native IntPtr loop members
// => forgetting to clear those would cause undefined behaviour!
// see also: https://github.com/vis2k/Mirror/pull/2652
PlayerLoopSystem system = new PlayerLoopSystem {
type = ownerType,
updateDelegate = function
};
// prepend our custom loop to the beginning
if (addMode == AddMode.Beginning)
{
// shift to the right, write into first array element
Array.Copy(playerLoop.subSystemList, 0, playerLoop.subSystemList, 1, playerLoop.subSystemList.Length - 1);
playerLoop.subSystemList[0] = system;
}
// append our custom loop to the end
else if (addMode == AddMode.End)
{
// simply write into last array element
playerLoop.subSystemList[oldListLength] = system;
}
// debugging
//Debug.Log($"New playerLoop of type {playerLoop.type} with {playerLoop.subSystemList.Length} Functions:");
//foreach (PlayerLoopSystem sys in playerLoop.subSystemList)
// Debug.Log($" ->{sys.type}");
return true;
}
// recursively keep looking
if (playerLoop.subSystemList != null)
{
for (int i = 0; i < playerLoop.subSystemList.Length; ++i)
{
if (AddToPlayerLoop(function, ownerType, ref playerLoop.subSystemList[i], playerLoopSystemType, addMode))
return true;
}
}
return false;
}
// hook into Unity runtime to actually add our custom functions
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
static void RuntimeInitializeOnLoad()
{
//Debug.Log("Mirror: adding Network[Early/Late]Update to Unity...");
// get loop
// 2019 has GetCURRENTPlayerLoop which is safe to use without
// breaking other custom system's custom loops.
// see also: https://github.com/vis2k/Mirror/pull/2627/files
PlayerLoopSystem playerLoop = PlayerLoop.GetCurrentPlayerLoop();
// add NetworkEarlyUpdate to the end of EarlyUpdate so it runs after
// any Unity initializations but before the first Update/FixedUpdate
AddToPlayerLoop(NetworkEarlyUpdate, typeof(NetworkLoop), ref playerLoop, typeof(EarlyUpdate), AddMode.End);
// add NetworkLateUpdate to the end of PreLateUpdate so it runs after
// LateUpdate(). adding to the beginning of PostLateUpdate doesn't
// actually work.
AddToPlayerLoop(NetworkLateUpdate, typeof(NetworkLoop), ref playerLoop, typeof(PreLateUpdate), AddMode.End);
// set the new loop
PlayerLoop.SetPlayerLoop(playerLoop);
}
static void NetworkEarlyUpdate()
{
// loop functions run in edit mode and in play mode.
// however, we only want to call NetworkServer/Client in play mode.
if (!Application.isPlaying) return;
NetworkTime.EarlyUpdate();
//Debug.Log($"NetworkEarlyUpdate {Time.time}");
NetworkServer.NetworkEarlyUpdate();
NetworkClient.NetworkEarlyUpdate();
// invoke event after mirror has done it's early updating.
OnEarlyUpdate?.Invoke();
}
static void NetworkLateUpdate()
{
// loop functions run in edit mode and in play mode.
// however, we only want to call NetworkServer/Client in play mode.
if (!Application.isPlaying) return;
//Debug.Log($"NetworkLateUpdate {Time.time}");
// invoke event before mirror does its final late updating.
OnLateUpdate?.Invoke();
NetworkServer.NetworkLateUpdate();
NetworkClient.NetworkLateUpdate();
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 2c6cec4e279774b919386e05545317b8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/NetworkLoop.cs
uploadId: 736421

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 8aab4c8111b7c411b9b92cf3dbc5bd4e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/NetworkManager.cs
uploadId: 736421

View File

@ -0,0 +1,162 @@
using UnityEngine;
namespace Mirror
{
/// <summary>Shows NetworkManager controls in a GUI at runtime.</summary>
[DisallowMultipleComponent]
[AddComponentMenu("Network/Network Manager HUD")]
[RequireComponent(typeof(NetworkManager))]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-manager-hud")]
public class NetworkManagerHUD : MonoBehaviour
{
NetworkManager manager;
public int offsetX;
public int offsetY;
void Awake()
{
manager = GetComponent<NetworkManager>();
}
void OnGUI()
{
// If this width is changed, also change offsetX in GUIConsole::OnGUI
int width = 300;
GUILayout.BeginArea(new Rect(10 + offsetX, 40 + offsetY, width, 9999));
if (!NetworkClient.isConnected && !NetworkServer.active)
StartButtons();
else
StatusLabels();
if (NetworkClient.isConnected && !NetworkClient.ready)
{
if (GUILayout.Button("Client Ready"))
{
// client ready
NetworkClient.Ready();
if (NetworkClient.localPlayer == null)
NetworkClient.AddPlayer();
}
}
StopButtons();
GUILayout.EndArea();
}
void StartButtons()
{
if (!NetworkClient.active)
{
#if UNITY_WEBGL
// cant be a server in webgl build
if (GUILayout.Button("Single Player"))
{
NetworkServer.listen = false;
manager.StartHost();
}
#else
// Server + Client
if (GUILayout.Button("Host (Server + Client)"))
manager.StartHost();
#endif
// Client + IP (+ PORT)
GUILayout.BeginHorizontal();
if (GUILayout.Button("Client"))
manager.StartClient();
manager.networkAddress = GUILayout.TextField(manager.networkAddress);
// only show a port field if we have a port transport
// we can't have "IP:PORT" in the address field since this only
// works for IPV4:PORT.
// for IPV6:PORT it would be misleading since IPV6 contains ":":
// 2001:0db8:0000:0000:0000:ff00:0042:8329
if (Transport.active is PortTransport portTransport)
{
// use TryParse in case someone tries to enter non-numeric characters
if (ushort.TryParse(GUILayout.TextField(portTransport.Port.ToString()), out ushort port))
portTransport.Port = port;
}
GUILayout.EndHorizontal();
// Server Only
#if UNITY_WEBGL
// cant be a server in webgl build
GUILayout.Box("( WebGL cannot be server )");
#else
if (GUILayout.Button("Server Only"))
manager.StartServer();
#endif
}
else
{
// Connecting
GUILayout.Label($"Connecting to {manager.networkAddress}..");
if (GUILayout.Button("Cancel Connection Attempt"))
manager.StopClient();
}
}
void StatusLabels()
{
// host mode
// display separately because this always confused people:
// Server: ...
// Client: ...
if (NetworkServer.active && NetworkClient.active)
{
// host mode
GUILayout.Label($"<b>Host</b>: running via {Transport.active}");
}
else if (NetworkServer.active)
{
// server only
GUILayout.Label($"<b>Server</b>: running via {Transport.active}");
}
else if (NetworkClient.isConnected)
{
// client only
GUILayout.Label($"<b>Client</b>: connected to {manager.networkAddress} via {Transport.active}");
}
}
void StopButtons()
{
if (NetworkServer.active && NetworkClient.isConnected)
{
GUILayout.BeginHorizontal();
#if UNITY_WEBGL
if (GUILayout.Button("Stop Single Player"))
manager.StopHost();
#else
// stop host if host mode
if (GUILayout.Button("Stop Host"))
manager.StopHost();
// stop client if host mode, leaving server up
if (GUILayout.Button("Stop Client"))
manager.StopClient();
#endif
GUILayout.EndHorizontal();
}
else if (NetworkClient.isConnected)
{
// stop client if client-only
if (GUILayout.Button("Stop Client"))
manager.StopClient();
}
else if (NetworkServer.active)
{
// stop server if server-only
if (GUILayout.Button("Stop Server"))
manager.StopServer();
}
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 6442dc8070ceb41f094e44de0bf87274
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/NetworkManagerHUD.cs
uploadId: 736421

View File

@ -0,0 +1,4 @@
namespace Mirror
{
public interface NetworkMessage {}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: eb04e4848a2e4452aa2dbd7adb801c51
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/NetworkMessage.cs
uploadId: 736421

View File

@ -0,0 +1,210 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;
using UnityEngine;
namespace Mirror
{
// for performance, we (ab)use c# generics to cache the message id in a static field
// this is significantly faster than doing the computation at runtime or looking up cached results via Dictionary
// generic classes have separate static fields per type specification
public static class NetworkMessageId<T> where T : struct, NetworkMessage
{
// automated message id from type hash.
// platform independent via stable hashcode.
// => convenient so we don't need to track messageIds across projects
// => addons can work with each other without knowing their ids before
// => 2 bytes is enough to avoid collisions.
// registering a messageId twice will log a warning anyway.
public static readonly ushort Id = CalculateId();
// Gets the 32bit fnv1a hash
// To get it down to 16bit but still reduce hash collisions we cant just cast it to ushort
// Instead we take the highest 16bits of the 32bit hash and fold them with xor into the lower 16bits
// This will create a more uniform 16bit hash, the method is described in:
// http://www.isthe.com/chongo/tech/comp/fnv/ in section "Changing the FNV hash size - xor-folding"
static ushort CalculateId() => typeof(T).FullName.GetStableHashCode16();
}
// message packing all in one place, instead of constructing headers in all
// kinds of different places
//
// MsgType (2 bytes)
// Content (ContentSize bytes)
public static class NetworkMessages
{
// size of message id header in bytes
public const int IdSize = sizeof(ushort);
// Id <> Type lookup for debugging, profiler, etc.
// important when debugging messageId errors!
public static readonly Dictionary<ushort, Type> Lookup =
new Dictionary<ushort, Type>();
// dump all types for debugging
public static void LogTypes()
{
StringBuilder builder = new StringBuilder();
builder.AppendLine("NetworkMessageIds:");
foreach (KeyValuePair<ushort, Type> kvp in Lookup)
{
builder.AppendLine($" Id={kvp.Key} = {kvp.Value}");
}
Debug.Log(builder.ToString());
}
// max message content size (without header) calculation for convenience
// -> Transport.GetMaxPacketSize is the raw maximum
// -> Every message gets serialized into <<id, content>>
// -> Every serialized message get put into a batch with one timestamp per batch
// -> Every message in a batch has a varuint size header.
// use the worst case VarUInt size for the largest possible
// message size = int.max.
public static int MaxContentSize(int channelId)
{
// calculate the max possible size that can fit in a batch
int transportMax = Transport.active.GetMaxPacketSize(channelId);
return transportMax - IdSize - Batcher.MaxMessageOverhead(transportMax);
}
// max message size which includes header + content.
public static int MaxMessageSize(int channelId) =>
MaxContentSize(channelId) + IdSize;
// automated message id from type hash.
// platform independent via stable hashcode.
// => convenient so we don't need to track messageIds across projects
// => addons can work with each other without knowing their ids before
// => 2 bytes is enough to avoid collisions.
// registering a messageId twice will log a warning anyway.
// keep this for convenience. easier to use than NetworkMessageId<T>.Id.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ushort GetId<T>() where T : struct, NetworkMessage =>
NetworkMessageId<T>.Id;
// pack message before sending
// -> NetworkWriter passed as arg so that we can use .ToArraySegment
// and do an allocation free send before recycling it.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Pack<T>(T message, NetworkWriter writer)
where T : struct, NetworkMessage
{
writer.WriteUShort(NetworkMessageId<T>.Id);
writer.Write(message);
}
// read only the message id.
// common function in case we ever change the header size.
public static bool UnpackId(NetworkReader reader, out ushort messageId)
{
// read message type
try
{
messageId = reader.ReadUShort();
return true;
}
catch (System.IO.EndOfStreamException)
{
messageId = 0;
return false;
}
}
// version for handlers with channelId
// inline! only exists for 20-30 messages and they call it all the time.
internal static NetworkMessageDelegate WrapHandler<T, C>(Action<C, T, int> handler, bool requireAuthentication, bool exceptionsDisconnect)
where T : struct, NetworkMessage
where C : NetworkConnection
=> (conn, reader, channelId) =>
{
// protect against DOS attacks if attackers try to send invalid
// data packets to crash the server/client. there are a thousand
// ways to cause an exception in data handling:
// - invalid headers
// - invalid message ids
// - invalid data causing exceptions
// - negative ReadBytesAndSize prefixes
// - invalid utf8 strings
// - etc.
//
// let's catch them all and then disconnect that connection to avoid
// further attacks.
T message = default;
// record start position for NetworkDiagnostics because reader might contain multiple messages if using batching
int startPos = reader.Position;
try
{
if (requireAuthentication && !conn.isAuthenticated)
{
// message requires authentication, but the connection was not authenticated
Debug.LogWarning($"Disconnecting connection: {conn}. Received message {typeof(T)} that required authentication, but the user has not authenticated yet");
conn.Disconnect();
return;
}
//Debug.Log($"ConnectionRecv {conn} msgType:{typeof(T)} content:{BitConverter.ToString(reader.buffer.Array, reader.buffer.Offset, reader.buffer.Count)}");
// if it is a value type, just use default(T)
// otherwise allocate a new instance
message = reader.Read<T>();
}
catch (Exception exception)
{
// should we disconnect on exceptions?
if (exceptionsDisconnect)
{
Debug.LogError($"Disconnecting connection: {conn} because reading a message of type {typeof(T)} caused an Exception. This can happen if the other side accidentally (or an attacker intentionally) sent invalid data. Reason: {exception}");
conn.Disconnect();
return;
}
// otherwise log it but allow the connection to keep playing
else
{
Debug.LogError($"Caught an Exception when reading a message from: {conn} of type {typeof(T)}. Reason: {exception}");
return;
}
}
finally
{
int endPos = reader.Position;
// TODO: Figure out the correct channel
NetworkDiagnostics.OnReceive(message, channelId, endPos - startPos);
}
// user handler exception should not stop the whole server
try
{
// user implemented handler
handler((C)conn, message, channelId);
}
catch (Exception exception)
{
// should we disconnect on exceptions?
if (exceptionsDisconnect)
{
Debug.LogError($"Disconnecting connection: {conn} because handling a message of type {typeof(T)} caused an Exception. This can happen if the other side accidentally (or an attacker intentionally) sent invalid data. Reason: {exception}");
conn.Disconnect();
}
// otherwise log it but allow the connection to keep playing
else
{
Debug.LogError($"Caught an Exception when handling a message from: {conn} of type {typeof(T)}. Reason: {exception}");
}
}
};
// version for handlers without channelId
// TODO obsolete this some day to always use the channelId version.
// all handlers in this version are wrapped with 1 extra action.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static NetworkMessageDelegate WrapHandler<T, C>(Action<C, T> handler, bool requireAuthentication, bool exceptionsDisconnect)
where T : struct, NetworkMessage
where C : NetworkConnection
{
// wrap action as channelId version, call original
void Wrapped(C conn, T msg, int _) => handler(conn, msg);
return WrapHandler((Action<C, T, int>)Wrapped, requireAuthentication, exceptionsDisconnect);
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 2db134099f0df4d96a84ae7a0cd9b4bc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/NetworkMessages.cs
uploadId: 736421

View File

@ -0,0 +1,249 @@
using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine;
namespace Mirror
{
/// <summary>Network Reader for most simple types like floats, ints, buffers, structs, etc. Use NetworkReaderPool.GetReader() to avoid allocations.</summary>
// Note: This class is intended to be extremely pedantic,
// and throw exceptions whenever stuff is going slightly wrong.
// The exceptions will be handled in NetworkServer/NetworkClient.
//
// Note that NetworkWriter can be passed in constructor thanks to implicit
// ArraySegment conversion:
// NetworkReader reader = new NetworkReader(writer);
public class NetworkReader
{
// internal buffer
// byte[] pointer would work, but we use ArraySegment to also support
// the ArraySegment constructor
internal ArraySegment<byte> buffer;
/// <summary>Next position to read from the buffer</summary>
// 'int' is the best type for .Position. 'short' is too small if we send >32kb which would result in negative .Position
// -> converting long to int is fine until 2GB of data (MAX_INT), so we don't have to worry about overflows here
public int Position;
/// <summary>Remaining bytes that can be read, for convenience.</summary>
public int Remaining => buffer.Count - Position;
/// <summary>Total buffer capacity, independent of reader position.</summary>
public int Capacity => buffer.Count;
// cache encoding for ReadString instead of creating it with each time
// 1000 readers before: 1MB GC, 30ms
// 1000 readers after: 0.8MB GC, 18ms
// member(!) to avoid static state.
//
// throwOnInvalidBytes is true.
// if false, it would silently ignore the invalid bytes but continue
// with the valid ones, creating strings like "a<><61><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>".
// instead, we want to catch it manually and return String.Empty.
// this is safer. see test: ReadString_InvalidUTF8().
internal readonly UTF8Encoding encoding = new UTF8Encoding(false, true);
// while allocation free ReadArraySegment is encouraged,
// some functions can allocate a new byte[], List<T>, Texture, etc.
// we should keep a reasonable allocation size limit:
// -> server won't accidentally allocate 2GB on a mobile device
// -> client won't allocate 2GB on server for ClientToServer [SyncVar]s
// -> unlike max string length of 64 KB, we need a larger limit here.
// large enough to not break existing projects,
// small enough to reasonably limit allocation attacks.
// -> we don't know the exact size of ReadList<T> etc. because <T> is
// managed. instead, this is considered a 'collection length' limit.
public const int AllocationLimit = 1024 * 1024 * 16; // 16 MB * sizeof(T)
public NetworkReader(ArraySegment<byte> segment)
{
buffer = segment;
}
#if !UNITY_2021_3_OR_NEWER
// Unity 2019 doesn't have the implicit byte[] to segment conversion yet
public NetworkReader(byte[] bytes)
{
buffer = new ArraySegment<byte>(bytes, 0, bytes.Length);
}
#endif
// sometimes it's useful to point a reader on another buffer instead of
// allocating a new reader (e.g. NetworkReaderPool)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetBuffer(ArraySegment<byte> segment)
{
buffer = segment;
Position = 0;
}
#if !UNITY_2021_3_OR_NEWER
// Unity 2019 doesn't have the implicit byte[] to segment conversion yet
public void SetBuffer(byte[] bytes)
{
buffer = new ArraySegment<byte>(bytes, 0, bytes.Length);
Position = 0;
}
#endif
// ReadBlittable<T> from DOTSNET
// this is extremely fast, but only works for blittable types.
// => private to make sure nobody accidentally uses it for non-blittable
//
// Benchmark: see NetworkWriter.WriteBlittable!
//
// Note:
// ReadBlittable assumes same endianness for server & client.
// All Unity 2018+ platforms are little endian.
//
// This is not safe to expose to random structs.
// * StructLayout.Sequential is the default, which is safe.
// if the struct contains a reference type, it is converted to Auto.
// but since all structs here are unmanaged blittable, it's safe.
// see also: https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.layoutkind?view=netframework-4.8#system-runtime-interopservices-layoutkind-sequential
// * StructLayout.Pack depends on CPU word size.
// this may be different 4 or 8 on some ARM systems, etc.
// this is not safe, and would cause bytes/shorts etc. to be padded.
// see also: https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.structlayoutattribute.pack?view=net-6.0
// * If we force pack all to '1', they would have no padding which is
// great for bandwidth. but on some android systems, CPU can't read
// unaligned memory.
// see also: https://github.com/vis2k/Mirror/issues/3044
// * The only option would be to force explicit layout with multiples
// of word size. but this requires lots of weaver checking and is
// still questionable (IL2CPP etc.).
//
// Note: inlining ReadBlittable is enough. don't inline ReadInt etc.
// we don't want ReadBlittable to be copied in place everywhere.
internal unsafe T ReadBlittable<T>()
where T : unmanaged
{
// check if blittable for safety
#if UNITY_EDITOR
if (!UnsafeUtility.IsBlittable(typeof(T)))
{
throw new ArgumentException($"{typeof(T)} is not blittable!");
}
#endif
// calculate size
// sizeof(T) gets the managed size at compile time.
// Marshal.SizeOf<T> gets the unmanaged size at runtime (slow).
// => our 1mio writes benchmark is 6x slower with Marshal.SizeOf<T>
// => for blittable types, sizeof(T) is even recommended:
// https://docs.microsoft.com/en-us/dotnet/standard/native-interop/best-practices
int size = sizeof(T);
// ensure remaining
if (Remaining < size)
{
throw new EndOfStreamException($"ReadBlittable<{typeof(T)}> not enough data in buffer to read {size} bytes: {ToString()}");
}
// read blittable
T value;
fixed (byte* ptr = &buffer.Array[buffer.Offset + Position])
{
#if UNITY_ANDROID
// on some android systems, reading *(T*)ptr throws a NRE if
// the ptr isn't aligned (i.e. if Position is 1,2,3,5, etc.).
// here we have to use memcpy.
//
// => we can't get a pointer of a struct in C# without
// marshalling allocations
// => instead, we stack allocate an array of type T and use that
// => stackalloc avoids GC and is very fast. it only works for
// value types, but all blittable types are anyway.
//
// this way, we can still support blittable reads on android.
// see also: https://github.com/vis2k/Mirror/issues/3044
// (solution discovered by AIIO, FakeByte, mischa)
T* valueBuffer = stackalloc T[1];
UnsafeUtility.MemCpy(valueBuffer, ptr, size);
value = valueBuffer[0];
#else
// cast buffer to a T* pointer and then read from it.
value = *(T*)ptr;
#endif
}
Position += size;
return value;
}
// blittable'?' template for code reuse
// note: bool isn't blittable. need to read as byte.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal T? ReadBlittableNullable<T>()
where T : unmanaged =>
ReadByte() != 0 ? ReadBlittable<T>() : default(T?);
public byte ReadByte() => ReadBlittable<byte>();
/// <summary>Read 'count' bytes into the bytes array</summary>
// NOTE: returns byte[] because all reader functions return something.
public byte[] ReadBytes(byte[] bytes, int count)
{
// user may call ReadBytes(ReadInt()). ensure positive count.
if (count < 0) throw new ArgumentOutOfRangeException("ReadBytes requires count >= 0");
// check if passed byte array is big enough
if (count > bytes.Length)
{
throw new EndOfStreamException($"ReadBytes can't read {count} + bytes because the passed byte[] only has length {bytes.Length}");
}
// ensure remaining
if (Remaining < count)
{
throw new EndOfStreamException($"ReadBytesSegment can't read {count} bytes because it would read past the end of the stream. {ToString()}");
}
Array.Copy(buffer.Array, buffer.Offset + Position, bytes, 0, count);
Position += count;
return bytes;
}
/// <summary>Read 'count' bytes allocation-free as ArraySegment that points to the internal array.</summary>
public ArraySegment<byte> ReadBytesSegment(int count)
{
// user may call ReadBytes(ReadInt()). ensure positive count.
if (count < 0) throw new ArgumentOutOfRangeException("ReadBytesSegment requires count >= 0");
// ensure remaining
if (Remaining < count)
{
throw new EndOfStreamException($"ReadBytesSegment can't read {count} bytes because it would read past the end of the stream. {ToString()}");
}
// return the segment
ArraySegment<byte> result = new ArraySegment<byte>(buffer.Array, buffer.Offset + Position, count);
Position += count;
return result;
}
/// <summary>Reads any data type that mirror supports. Uses weaver populated Reader(T).read</summary>
public T Read<T>()
{
Func<NetworkReader, T> readerDelegate = Reader<T>.read;
if (readerDelegate == null)
{
Debug.LogError($"No reader found for {typeof(T)}. Use a type supported by Mirror or define a custom reader extension for {typeof(T)}.");
return default;
}
return readerDelegate(this);
}
// print the full buffer with position / capacity.
public override string ToString() =>
$"[{buffer.ToHexString()} @ {Position}/{Capacity}]";
}
/// <summary>Helper class that weaver populates with all reader types.</summary>
// Note that c# creates a different static variable for each type
// -> Weaver.ReaderWriterProcessor.InitializeReaderAndWriters() populates it
public static class Reader<T>
{
public static Func<NetworkReader, T> read;
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 1610f05ec5bd14d6882e689f7372596a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/NetworkReader.cs
uploadId: 736421

View File

@ -0,0 +1,420 @@
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
namespace Mirror
{
// Mirror's Weaver automatically detects all NetworkReader function types,
// but they do all need to be extensions.
public static class NetworkReaderExtensions
{
public static byte ReadByte(this NetworkReader reader) => reader.ReadBlittable<byte>();
public static byte? ReadByteNullable(this NetworkReader reader) => reader.ReadBlittableNullable<byte>();
public static sbyte ReadSByte(this NetworkReader reader) => reader.ReadBlittable<sbyte>();
public static sbyte? ReadSByteNullable(this NetworkReader reader) => reader.ReadBlittableNullable<sbyte>();
// bool is not blittable. read as ushort.
public static char ReadChar(this NetworkReader reader) => (char)reader.ReadBlittable<ushort>();
public static char? ReadCharNullable(this NetworkReader reader) => (char?)reader.ReadBlittableNullable<ushort>();
// bool is not blittable. read as byte.
public static bool ReadBool(this NetworkReader reader) => reader.ReadBlittable<byte>() != 0;
public static bool? ReadBoolNullable(this NetworkReader reader)
{
byte? value = reader.ReadBlittableNullable<byte>();
return value.HasValue ? (value.Value != 0) : default(bool?);
}
public static short ReadShort(this NetworkReader reader) => (short)reader.ReadUShort();
public static short? ReadShortNullable(this NetworkReader reader) => reader.ReadBlittableNullable<short>();
public static ushort ReadUShort(this NetworkReader reader) => reader.ReadBlittable<ushort>();
public static ushort? ReadUShortNullable(this NetworkReader reader) => reader.ReadBlittableNullable<ushort>();
public static int ReadInt(this NetworkReader reader) => reader.ReadBlittable<int>();
public static int? ReadIntNullable(this NetworkReader reader) => reader.ReadBlittableNullable<int>();
public static uint ReadUInt(this NetworkReader reader) => reader.ReadBlittable<uint>();
public static uint? ReadUIntNullable(this NetworkReader reader) => reader.ReadBlittableNullable<uint>();
public static long ReadLong(this NetworkReader reader) => reader.ReadBlittable<long>();
public static long? ReadLongNullable(this NetworkReader reader) => reader.ReadBlittableNullable<long>();
public static ulong ReadULong(this NetworkReader reader) => reader.ReadBlittable<ulong>();
public static ulong? ReadULongNullable(this NetworkReader reader) => reader.ReadBlittableNullable<ulong>();
// ReadInt/UInt/Long/ULong writes full bytes by default.
// define additional "VarInt" versions that Weaver will automatically prefer.
// 99% of the time [SyncVar] ints are small values, which makes this very much worth it.
[WeaverPriority] public static int ReadVarInt(this NetworkReader reader) => (int)Compression.DecompressVarInt(reader);
[WeaverPriority] public static uint ReadVarUInt(this NetworkReader reader) => (uint)Compression.DecompressVarUInt(reader);
[WeaverPriority] public static long ReadVarLong(this NetworkReader reader) => Compression.DecompressVarInt(reader);
[WeaverPriority] public static ulong ReadVarULong(this NetworkReader reader) => Compression.DecompressVarUInt(reader);
public static float ReadFloat(this NetworkReader reader) => reader.ReadBlittable<float>();
public static float? ReadFloatNullable(this NetworkReader reader) => reader.ReadBlittableNullable<float>();
public static double ReadDouble(this NetworkReader reader) => reader.ReadBlittable<double>();
public static double? ReadDoubleNullable(this NetworkReader reader) => reader.ReadBlittableNullable<double>();
public static decimal ReadDecimal(this NetworkReader reader) => reader.ReadBlittable<decimal>();
public static decimal? ReadDecimalNullable(this NetworkReader reader) => reader.ReadBlittableNullable<decimal>();
public static Half ReadHalf(this NetworkReader reader) => new Half(reader.ReadUShort());
/// <exception cref="T:System.ArgumentException">if an invalid utf8 string is sent</exception>
public static string ReadString(this NetworkReader reader)
{
// read number of bytes
ushort size = reader.ReadUShort();
// null support, see NetworkWriter
if (size == 0)
return null;
ushort realSize = (ushort)(size - 1);
// make sure it's within limits to avoid allocation attacks etc.
if (realSize > NetworkWriter.MaxStringLength)
throw new EndOfStreamException($"NetworkReader.ReadString - Value too long: {realSize} bytes. Limit is: {NetworkWriter.MaxStringLength} bytes");
ArraySegment<byte> data = reader.ReadBytesSegment(realSize);
// convert directly from buffer to string via encoding
// throws in case of invalid utf8.
// see test: ReadString_InvalidUTF8()
return reader.encoding.GetString(data.Array, data.Offset, data.Count);
}
public static byte[] ReadBytes(this NetworkReader reader, int count)
{
// prevent allocation attacks with a reasonable limit.
// server shouldn't allocate too much on client devices.
// client shouldn't allocate too much on server in ClientToServer [SyncVar]s.
if (count > NetworkReader.AllocationLimit)
{
// throw EndOfStream for consistency with ReadBlittable when out of data
throw new EndOfStreamException($"NetworkReader attempted to allocate {count} bytes, which is larger than the allowed limit of {NetworkReader.AllocationLimit} bytes.");
}
byte[] bytes = new byte[count];
reader.ReadBytes(bytes, count);
return bytes;
}
/// <exception cref="T:OverflowException">if count is invalid</exception>
public static byte[] ReadBytesAndSize(this NetworkReader reader)
{
// we offset count by '1' to easily support null without writing another byte.
// encoding null as '0' instead of '-1' also allows for better compression
// (ushort vs. short / varuint vs. varint) etc.
// most sizes are small, read size as VarUInt!
uint count = (uint)Compression.DecompressVarUInt(reader);
// uint count = reader.ReadUInt();
// Use checked() to force it to throw OverflowException if data is invalid
return count == 0 ? null : reader.ReadBytes(checked((int)(count - 1u)));
}
// Reads ArraySegment and size header
/// <exception cref="T:OverflowException">if count is invalid</exception>
public static ArraySegment<byte> ReadArraySegmentAndSize(this NetworkReader reader)
{
// we offset count by '1' to easily support null without writing another byte.
// encoding null as '0' instead of '-1' also allows for better compression
// (ushort vs. short / varuint vs. varint) etc.
// most sizes are small, read size as VarUInt!
uint count = (uint)Compression.DecompressVarUInt(reader);
// uint count = reader.ReadUInt();
// Use checked() to force it to throw OverflowException if data is invalid
return count == 0 ? default : reader.ReadBytesSegment(checked((int)(count - 1u)));
}
public static Vector2 ReadVector2(this NetworkReader reader) => reader.ReadBlittable<Vector2>();
public static Vector2? ReadVector2Nullable(this NetworkReader reader) => reader.ReadBlittableNullable<Vector2>();
public static Vector3 ReadVector3(this NetworkReader reader) => reader.ReadBlittable<Vector3>();
public static Vector3? ReadVector3Nullable(this NetworkReader reader) => reader.ReadBlittableNullable<Vector3>();
public static Vector4 ReadVector4(this NetworkReader reader) => reader.ReadBlittable<Vector4>();
public static Vector4? ReadVector4Nullable(this NetworkReader reader) => reader.ReadBlittableNullable<Vector4>();
public static Vector2Int ReadVector2Int(this NetworkReader reader) => reader.ReadBlittable<Vector2Int>();
public static Vector2Int? ReadVector2IntNullable(this NetworkReader reader) => reader.ReadBlittableNullable<Vector2Int>();
public static Vector3Int ReadVector3Int(this NetworkReader reader) => reader.ReadBlittable<Vector3Int>();
public static Vector3Int? ReadVector3IntNullable(this NetworkReader reader) => reader.ReadBlittableNullable<Vector3Int>();
public static Color ReadColor(this NetworkReader reader) => reader.ReadBlittable<Color>();
public static Color? ReadColorNullable(this NetworkReader reader) => reader.ReadBlittableNullable<Color>();
public static Color32 ReadColor32(this NetworkReader reader) => reader.ReadBlittable<Color32>();
public static Color32? ReadColor32Nullable(this NetworkReader reader) => reader.ReadBlittableNullable<Color32>();
public static Quaternion ReadQuaternion(this NetworkReader reader) => reader.ReadBlittable<Quaternion>();
public static Quaternion? ReadQuaternionNullable(this NetworkReader reader) => reader.ReadBlittableNullable<Quaternion>();
// Rect is a struct with properties instead of fields
public static Rect ReadRect(this NetworkReader reader) => new Rect(reader.ReadVector2(), reader.ReadVector2());
public static Rect? ReadRectNullable(this NetworkReader reader) => reader.ReadBool() ? ReadRect(reader) : default(Rect?);
// Plane is a struct with properties instead of fields
public static Plane ReadPlane(this NetworkReader reader) => new Plane(reader.ReadVector3(), reader.ReadFloat());
public static Plane? ReadPlaneNullable(this NetworkReader reader) => reader.ReadBool() ? ReadPlane(reader) : default(Plane?);
// Ray is a struct with properties instead of fields
public static Ray ReadRay(this NetworkReader reader) => new Ray(reader.ReadVector3(), reader.ReadVector3());
public static Ray? ReadRayNullable(this NetworkReader reader) => reader.ReadBool() ? ReadRay(reader) : default(Ray?);
// LayerMask is a struct with properties instead of fields
public static LayerMask ReadLayerMask(this NetworkReader reader)
{
// LayerMask doesn't have a constructor that takes an initial value.
// 32 layers as a flags enum, max value of 496, we only need a UShort.
LayerMask layerMask = default;
layerMask.value = reader.ReadUShort();
return layerMask;
}
public static LayerMask? ReadLayerMaskNullable(this NetworkReader reader) => reader.ReadBool() ? ReadLayerMask(reader) : default(LayerMask?);
public static Matrix4x4 ReadMatrix4x4(this NetworkReader reader) => reader.ReadBlittable<Matrix4x4>();
public static Matrix4x4? ReadMatrix4x4Nullable(this NetworkReader reader) => reader.ReadBlittableNullable<Matrix4x4>();
public static Guid ReadGuid(this NetworkReader reader)
{
#if !UNITY_2021_3_OR_NEWER
// Unity 2019 doesn't have Span yet
return new Guid(reader.ReadBytes(16));
#else
// ReadBlittable(Guid) isn't safe. see ReadBlittable comments.
// Guid is Sequential, but we can't guarantee packing.
if (reader.Remaining >= 16)
{
ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(reader.buffer.Array, reader.buffer.Offset + reader.Position, 16);
reader.Position += 16;
return new Guid(span);
}
throw new EndOfStreamException($"ReadGuid out of range: {reader}");
#endif
}
public static Guid? ReadGuidNullable(this NetworkReader reader) => reader.ReadBool() ? ReadGuid(reader) : default(Guid?);
public static NetworkIdentity ReadNetworkIdentity(this NetworkReader reader)
{
uint netId = reader.ReadUInt();
if (netId == 0)
return null;
// NOTE: a netId not being in spawned is common.
// for example, "[SyncVar] NetworkIdentity target" netId would not
// be known on client if the monster walks out of proximity for a
// moment. no need to log any error or warning here.
return Utils.GetSpawnedInServerOrClient(netId);
}
public static NetworkBehaviour ReadNetworkBehaviour(this NetworkReader reader)
{
// read netId first.
//
// IMPORTANT: if netId != 0, writer always writes componentIndex.
// reusing ReadNetworkIdentity() might return a null NetworkIdentity
// even if netId was != 0 but the identity disappeared on the client,
// resulting in unequal amounts of data being written / read.
// https://github.com/vis2k/Mirror/issues/2972
uint netId = reader.ReadUInt();
if (netId == 0)
return null;
// read component index in any case, BEFORE searching the spawned
// NetworkIdentity by netId.
byte componentIndex = reader.ReadByte();
// NOTE: a netId not being in spawned is common.
// for example, "[SyncVar] NetworkIdentity target" netId would not
// be known on client if the monster walks out of proximity for a
// moment. no need to log any error or warning here.
NetworkIdentity identity = Utils.GetSpawnedInServerOrClient(netId);
return identity != null
? identity.NetworkBehaviours[componentIndex]
: null;
}
public static T ReadNetworkBehaviour<T>(this NetworkReader reader) where T : NetworkBehaviour
{
return reader.ReadNetworkBehaviour() as T;
}
public static NetworkBehaviourSyncVar ReadNetworkBehaviourSyncVar(this NetworkReader reader)
{
uint netId = reader.ReadUInt();
byte componentIndex = default;
// if netId is not 0, then index is also sent to read before returning
if (netId != 0)
{
componentIndex = reader.ReadByte();
}
return new NetworkBehaviourSyncVar(netId, componentIndex);
}
public static Transform ReadTransform(this NetworkReader reader)
{
// Don't use null propagation here as it could lead to MissingReferenceException
NetworkIdentity networkIdentity = reader.ReadNetworkIdentity();
return networkIdentity != null ? networkIdentity.transform : null;
}
public static GameObject ReadGameObject(this NetworkReader reader)
{
// Don't use null propagation here as it could lead to MissingReferenceException
NetworkIdentity networkIdentity = reader.ReadNetworkIdentity();
return networkIdentity != null ? networkIdentity.gameObject : null;
}
// while SyncList<T> is recommended for NetworkBehaviours,
// structs may have .List<T> members which weaver needs to be able to
// fully serialize for NetworkMessages etc.
// note that Weaver/Readers/GenerateReader() handles this manually.
public static List<T> ReadList<T>(this NetworkReader reader)
{
// we offset count by '1' to easily support null without writing another byte.
// encoding null as '0' instead of '-1' also allows for better compression
// (ushort vs. short / varuint vs. varint) etc.
// most sizes are small, read size as VarUInt!
uint length = (uint)Compression.DecompressVarUInt(reader);
// uint length = reader.ReadUInt();
if (length == 0) return null;
length -= 1;
// prevent allocation attacks with a reasonable limit.
// server shouldn't allocate too much on client devices.
// client shouldn't allocate too much on server in ClientToServer [SyncVar]s.
if (length > NetworkReader.AllocationLimit)
{
// throw EndOfStream for consistency with ReadBlittable when out of data
throw new EndOfStreamException($"NetworkReader attempted to allocate a List<{typeof(T)}> {length} elements, which is larger than the allowed limit of {NetworkReader.AllocationLimit}.");
}
List<T> result = new List<T>((checked((int)length)));
for (int i = 0; i < length; i++)
{
result.Add(reader.Read<T>());
}
return result;
}
// while SyncSet<T> is recommended for NetworkBehaviours,
// structs may have .Set<T> members which weaver needs to be able to
// fully serialize for NetworkMessages etc.
// note that Weaver/Readers/GenerateReader() handles this manually.
public static HashSet<T> ReadHashSet<T>(this NetworkReader reader)
{
// we offset count by '1' to easily support null without writing another byte.
// encoding null as '0' instead of '-1' also allows for better compression
// (ushort vs. short / varuint vs. varint) etc.
// most sizes are small, read size as VarUInt!
uint length = (uint)Compression.DecompressVarUInt(reader);
//uint length = reader.ReadUInt();
if (length == 0) return null;
length -= 1;
HashSet<T> result = new HashSet<T>();
for (int i = 0; i < length; i++)
{
result.Add(reader.Read<T>());
}
return result;
}
public static T[] ReadArray<T>(this NetworkReader reader)
{
// we offset count by '1' to easily support null without writing another byte.
// encoding null as '0' instead of '-1' also allows for better compression
// (ushort vs. short / varuint vs. varint) etc.
// most sizes are small, read size as VarUInt!
uint length = (uint)Compression.DecompressVarUInt(reader);
//uint length = reader.ReadUInt();
if (length == 0) return null;
length -= 1;
// prevent allocation attacks with a reasonable limit.
// server shouldn't allocate too much on client devices.
// client shouldn't allocate too much on server in ClientToServer [SyncVar]s.
if (length > NetworkReader.AllocationLimit)
{
// throw EndOfStream for consistency with ReadBlittable when out of data
throw new EndOfStreamException($"NetworkReader attempted to allocate an Array<{typeof(T)}> with {length} elements, which is larger than the allowed limit of {NetworkReader.AllocationLimit}.");
}
// we can't check if reader.Remaining < length,
// because we don't know sizeof(T) since it's a managed type.
// if (length > reader.Remaining) throw new EndOfStreamException($"Received array that is too large: {length}");
T[] result = new T[length];
for (int i = 0; i < length; i++)
{
result[i] = reader.Read<T>();
}
return result;
}
public static Uri ReadUri(this NetworkReader reader)
{
string uriString = reader.ReadString();
return (string.IsNullOrWhiteSpace(uriString) ? null : new Uri(uriString));
}
public static Texture2D ReadTexture2D(this NetworkReader reader)
{
// support 'null' textures for [SyncVar]s etc.
// https://github.com/vis2k/Mirror/issues/3144
short width = reader.ReadShort();
if (width == -1) return null;
// read height
short height = reader.ReadShort();
// prevent allocation attacks with a reasonable limit.
// server shouldn't allocate too much on client devices.
// client shouldn't allocate too much on server in ClientToServer [SyncVar]s.
// log an error and return default.
// we don't want attackers to be able to trigger exceptions.
int totalSize = width * height;
if (totalSize > NetworkReader.AllocationLimit)
{
Debug.LogWarning($"NetworkReader attempted to allocate a Texture2D with total size (width * height) of {totalSize}, which is larger than the allowed limit of {NetworkReader.AllocationLimit}.");
return null;
}
Texture2D texture2D = new Texture2D(width, height);
// read pixel content
Color32[] pixels = reader.ReadArray<Color32>();
texture2D.SetPixels32(pixels);
texture2D.Apply();
return texture2D;
}
public static Sprite ReadSprite(this NetworkReader reader)
{
// support 'null' textures for [SyncVar]s etc.
// https://github.com/vis2k/Mirror/issues/3144
Texture2D texture = reader.ReadTexture2D();
if (texture == null) return null;
// otherwise create a valid sprite
return Sprite.Create(texture, reader.ReadRect(), reader.ReadVector2());
}
public static DateTime ReadDateTime(this NetworkReader reader) => DateTime.FromOADate(reader.ReadDouble());
public static DateTime? ReadDateTimeNullable(this NetworkReader reader) => reader.ReadBool() ? ReadDateTime(reader) : default(DateTime?);
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 364a9f7ccd5541e19aa2ae0b81f0b3cf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/NetworkReaderExtensions.cs
uploadId: 736421

View File

@ -0,0 +1,48 @@
// API consistent with Microsoft's ObjectPool<T>.
using System;
using System.Runtime.CompilerServices;
namespace Mirror
{
/// <summary>Pool of NetworkReaders to avoid allocations.</summary>
public static class NetworkReaderPool
{
// reuse Pool<T>
// we still wrap it in NetworkReaderPool.Get/Recyle so we can reset the
// position and array before reusing.
static readonly Pool<NetworkReaderPooled> Pool = new Pool<NetworkReaderPooled>(
// byte[] will be assigned in GetReader
() => new NetworkReaderPooled(new byte[]{}),
// initial capacity to avoid allocations in the first few frames
1000
);
// expose count for testing
public static int Count => Pool.Count;
/// <summary>Get the next reader in the pool. If pool is empty, creates a new Reader</summary>
public static NetworkReaderPooled Get(byte[] bytes)
{
// grab from pool & set buffer
NetworkReaderPooled reader = Pool.Get();
reader.SetBuffer(bytes);
return reader;
}
/// <summary>Get the next reader in the pool. If pool is empty, creates a new Reader</summary>
public static NetworkReaderPooled Get(ArraySegment<byte> segment)
{
// grab from pool & set buffer
NetworkReaderPooled reader = Pool.Get();
reader.SetBuffer(segment);
return reader;
}
/// <summary>Returns a reader to the pool.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Return(NetworkReaderPooled reader)
{
Pool.Return(reader);
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 2bacff63613ad634a98f9e4d15d29dbf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/NetworkReaderPool.cs
uploadId: 736421

View File

@ -0,0 +1,12 @@
using System;
namespace Mirror
{
/// <summary>Pooled NetworkReader, automatically returned to pool when using 'using'</summary>
public sealed class NetworkReaderPooled : NetworkReader, IDisposable
{
internal NetworkReaderPooled(byte[] bytes) : base(bytes) {}
internal NetworkReaderPooled(ArraySegment<byte> segment) : base(segment) {}
public void Dispose() => NetworkReaderPool.Return(this);
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: faafa97c32e44adf8e8888de817a370a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/NetworkReaderPooled.cs
uploadId: 736421

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: a5f5ec068f5604c32b160bc49ee97b75
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/NetworkServer.cs
uploadId: 736421

View File

@ -0,0 +1,21 @@
using UnityEngine;
namespace Mirror
{
/// <summary>Start position for player spawning, automatically registers itself in the NetworkManager.</summary>
[DisallowMultipleComponent]
[AddComponentMenu("Network/Network Start Position")]
[HelpURL("https://mirror-networking.gitbook.io/docs/components/network-start-position")]
public class NetworkStartPosition : MonoBehaviour
{
public void Awake()
{
NetworkManager.RegisterStartPosition(transform);
}
public void OnDestroy()
{
NetworkManager.UnRegisterStartPosition(transform);
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 41f84591ce72545258ea98cb7518d8b9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/NetworkStartPosition.cs
uploadId: 736421

View File

@ -0,0 +1,243 @@
// NetworkTime now uses NetworkClient's snapshot interpolated timeline.
// this gives ideal results & ensures everything is on the same timeline.
// previously, NetworkTransforms were on separate timelines.
//
// however, some of the old NetworkTime code remains for ping time (rtt).
// some users may still be using that.
using System;
using System.Runtime.CompilerServices;
using UnityEngine;
#if !UNITY_2020_3_OR_NEWER
using Stopwatch = System.Diagnostics.Stopwatch;
#endif
namespace Mirror
{
/// <summary>Synchronizes server time to clients.</summary>
public static class NetworkTime
{
/// <summary>Ping message interval, used to calculate latency / RTT and predicted time.</summary>
// 2s was enough to get a good average RTT.
// for prediction, we want to react to latency changes more rapidly.
const float DefaultPingInterval = 0.1f; // for resets
public static float PingInterval = DefaultPingInterval;
/// <summary>Average out the last few results from Ping</summary>
// const because it's used immediately in _rtt constructor.
public const int PingWindowSize = 50; // average over 50 * 100ms = 5s
static double lastPingTime;
static ExponentialMovingAverage _rtt = new ExponentialMovingAverage(PingWindowSize);
/// <summary>Returns double precision clock time _in this system_, unaffected by the network.</summary>
#if UNITY_2020_3_OR_NEWER
public static double localTime
{
// NetworkTime uses unscaled time and ignores Time.timeScale.
// fixes Time.timeScale getting server & client time out of sync:
// https://github.com/MirrorNetworking/Mirror/issues/3409
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => Time.unscaledTimeAsDouble;
}
#else
// need stopwatch for older Unity versions, but it's quite slow.
// CAREFUL: unlike Time.time, the stopwatch time is not a FRAME time.
// it changes during the frame, so we have an extra step to "cache" it in EarlyUpdate.
static readonly Stopwatch stopwatch = new Stopwatch();
static NetworkTime() => stopwatch.Start();
static double localFrameTime;
public static double localTime => localFrameTime;
#endif
/// <summary>The time in seconds since the server started.</summary>
// via global NetworkClient snapshot interpolated timeline (if client).
// on server, this is simply Time.timeAsDouble.
//
// I measured the accuracy of float and I got this:
// for the same day, accuracy is better than 1 ms
// after 1 day, accuracy goes down to 7 ms
// after 10 days, accuracy is 61 ms
// after 30 days , accuracy is 238 ms
// after 60 days, accuracy is 454 ms
// in other words, if the server is running for 2 months,
// and you cast down to float, then the time will jump in 0.4s intervals.
public static double time
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => NetworkServer.active
? localTime
: NetworkClient.localTimeline;
}
// prediction //////////////////////////////////////////////////////////
// NetworkTime.time is server time, behind by bufferTime.
// for prediction, we want server time, ahead by latency.
// so that client inputs at predictedTime=2 arrive on server at time=2.
// the more accurate this is, the more closesly will corrections be
// be applied and the less jitter we will see.
//
// we'll use a two step process to calculate predicted time:
// 1. move snapshot interpolated time to server time, without being behind by bufferTime
// 2. constantly send this time to server (included in ping message)
// server replies with how far off it was.
// client averages that offset and applies it to predictedTime to get ever closer.
//
// this is also very easy to test & verify:
// - add LatencySimulation with 50ms latency
// - log predictionError on server in OnServerPing, see if it gets closer to 0
//
// credits: FakeByte, imer, NinjaKickja, mischa
// const because it's used immediately in _predictionError constructor.
static int PredictionErrorWindowSize = 20; // average over 20 * 100ms = 2s
static ExponentialMovingAverage _predictionErrorUnadjusted = new ExponentialMovingAverage(PredictionErrorWindowSize);
public static double predictionErrorUnadjusted => _predictionErrorUnadjusted.Value;
public static double predictionErrorAdjusted { get; private set; } // for debugging
/// <summary>Predicted timeline in order for client inputs to be timestamped with the exact time when they will most likely arrive on the server. This is the basis for all prediction like PredictedRigidbody.</summary>
// on client, this is based on localTime (aka Time.time) instead of the snapshot interpolated timeline.
// this gives much better and immediately accurate results.
// -> snapshot interpolation timeline tries to emulate a server timeline without hard offset corrections.
// -> predictedTime does have hard offset corrections, so might as well use Time.time directly for this.
//
// note that predictedTime over unreliable is enough!
// even with reliable components, it gives better results than if we were
// to implemented predictedTime over reliable channel.
public static double predictedTime
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => NetworkServer.active
? localTime // server always uses it's own timeline
: localTime + predictionErrorUnadjusted; // add the offset that the server told us we are off by
}
////////////////////////////////////////////////////////////////////////
/// <summary>Clock difference in seconds between the client and the server. Always 0 on server.</summary>
// original implementation used 'client - server' time. keep it this way.
// TODO obsolete later. people shouldn't worry about this.
public static double offset => localTime - time;
/// <summary>Round trip time (in seconds) that it takes a message to go client->server->client.</summary>
public static double rtt => _rtt.Value;
/// <Summary>Round trip time variance aka jitter, in seconds.</Summary>
// "rttVariance" instead of "rttVar" for consistency with older versions.
public static double rttVariance => _rtt.Variance;
// RuntimeInitializeOnLoadMethod -> fast playmode without domain reload
[RuntimeInitializeOnLoadMethod]
public static void ResetStatics()
{
PingInterval = DefaultPingInterval;
lastPingTime = 0;
_rtt = new ExponentialMovingAverage(PingWindowSize);
#if !UNITY_2020_3_OR_NEWER
stopwatch.Restart();
#endif
}
internal static void UpdateClient()
{
// localTime (double) instead of Time.time for accuracy over days
if (localTime >= lastPingTime + PingInterval)
SendPing();
}
// Separate method so we can call it from NetworkClient directly.
internal static void SendPing()
{
// send raw predicted time without the offset applied yet.
// we then apply the offset to it after.
NetworkPingMessage pingMessage = new NetworkPingMessage
(
localTime,
predictedTime
);
NetworkClient.Send(pingMessage, Channels.Unreliable);
lastPingTime = localTime;
}
// client rtt calculation //////////////////////////////////////////////
// executed at the server when we receive a ping message
// reply with a pong containing the time from the client
// and time from the server
internal static void OnServerPing(NetworkConnectionToClient conn, NetworkPingMessage message)
{
// calculate the prediction offset that the client needs to apply to unadjusted time to reach server time.
// this will be sent back to client for corrections.
double unadjustedError = localTime - message.localTime;
// to see how well the client's final prediction worked, compare with adjusted time.
// this is purely for debugging.
// >0 means: server is ... seconds ahead of client's prediction (good if small)
// <0 means: server is ... seconds behind client's prediction.
// in other words, client is predicting too far ahead (not good)
double adjustedError = localTime - message.predictedTimeAdjusted;
// Debug.Log($"[Server] unadjustedError:{(unadjustedError*1000):F1}ms adjustedError:{(adjustedError*1000):F1}ms");
// Debug.Log($"OnServerPing conn:{conn}");
NetworkPongMessage pongMessage = new NetworkPongMessage
(
message.localTime,
unadjustedError,
adjustedError
);
conn.Send(pongMessage, Channels.Unreliable);
}
// Executed at the client when we receive a Pong message
// find out how long it took since we sent the Ping
// and update time offset & prediction offset.
internal static void OnClientPong(NetworkPongMessage message)
{
// prevent attackers from sending timestamps which are in the future
if (message.localTime > localTime) return;
// how long did this message take to come back
double newRtt = localTime - message.localTime;
_rtt.Add(newRtt);
// feed unadjusted prediction error into our exponential moving average
// store adjusted prediction error for debug / GUI purposes
_predictionErrorUnadjusted.Add(message.predictionErrorUnadjusted);
predictionErrorAdjusted = message.predictionErrorAdjusted;
// Debug.Log($"[Client] predictionError avg={(_predictionErrorUnadjusted.Value*1000):F1} ms");
}
// server rtt calculation //////////////////////////////////////////////
// Executed at the client when we receive a ping message from the server.
// in other words, this is for server sided ping + rtt calculation.
// reply with a pong containing the time from the server
internal static void OnClientPing(NetworkPingMessage message)
{
// Debug.Log($"OnClientPing conn:{conn}");
NetworkPongMessage pongMessage = new NetworkPongMessage
(
message.localTime,
0, 0 // server doesn't predict
);
NetworkClient.Send(pongMessage, Channels.Unreliable);
}
// Executed at the server when we receive a Pong message back.
// find out how long it took since we sent the Ping
// and update time offset
internal static void OnServerPong(NetworkConnectionToClient conn, NetworkPongMessage message)
{
// prevent attackers from sending timestamps which are in the future
if (message.localTime > localTime) return;
// how long did this message take to come back
double newRtt = localTime - message.localTime;
conn._rtt.Add(newRtt);
}
internal static void EarlyUpdate()
{
#if !UNITY_2020_3_OR_NEWER
localFrameTime = stopwatch.Elapsed.TotalSeconds;
#endif
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 09a0c241fc4a5496dbf4a0ab6e9a312c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/NetworkTime.cs
uploadId: 736421

View File

@ -0,0 +1,249 @@
using System;
using System.Runtime.CompilerServices;
using System.Text;
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine;
namespace Mirror
{
/// <summary>Network Writer for most simple types like floats, ints, buffers, structs, etc. Use NetworkWriterPool.GetReader() to avoid allocations.</summary>
public class NetworkWriter
{
// the limit of ushort is so we can write string size prefix as only 2 bytes.
// -1 so we can still encode 'null' into it too.
public const ushort MaxStringLength = ushort.MaxValue - 1;
// create writer immediately with it's own buffer so no one can mess with it and so that we can resize it.
// note: BinaryWriter allocates too much, so we only use a MemoryStream
// => 1500 bytes by default because on average, most packets will be <= MTU
public const int DefaultCapacity = 1500;
internal byte[] buffer = new byte[DefaultCapacity];
/// <summary>Next position to write to the buffer</summary>
public int Position;
/// <summary>Current capacity. Automatically resized if necessary.</summary>
public int Capacity => buffer.Length;
// cache encoding for WriteString instead of creating it each time.
// 1000 readers before: 1MB GC, 30ms
// 1000 readers after: 0.8MB GC, 18ms
// not(!) static for thread safety.
//
// throwOnInvalidBytes is true.
// writer should throw and user should fix if this ever happens.
// unlike reader, which needs to expect it to happen from attackers.
internal readonly UTF8Encoding encoding = new UTF8Encoding(false, true);
/// <summary>Reset both the position and length of the stream</summary>
// Leaves the capacity the same so that we can reuse this writer without
// extra allocations
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset()
{
Position = 0;
}
// NOTE that our runtime resizing comes at no extra cost because:
// 1. 'has space' checks are necessary even for fixed sized writers.
// 2. all writers will eventually be large enough to stop resizing.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void EnsureCapacity(int value)
{
if (buffer.Length < value)
{
int capacity = Math.Max(value, buffer.Length * 2);
Array.Resize(ref buffer, capacity);
}
}
/// <summary>Copies buffer until 'Position' to a new array.</summary>
// Try to use ToArraySegment instead to avoid allocations!
public byte[] ToArray()
{
byte[] data = new byte[Position];
Array.ConstrainedCopy(buffer, 0, data, 0, Position);
return data;
}
/// <summary>Returns allocation-free ArraySegment until 'Position'.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ArraySegment<byte> ToArraySegment() =>
new ArraySegment<byte>(buffer, 0, Position);
// implicit conversion for convenience
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator ArraySegment<byte>(NetworkWriter w) =>
w.ToArraySegment();
// WriteBlittable<T> from DOTSNET.
// this is extremely fast, but only works for blittable types.
//
// Benchmark:
// WriteQuaternion x 100k, Macbook Pro 2015 @ 2.2Ghz, Unity 2018 LTS (debug mode)
//
// | Median | Min | Max | Avg | Std | (ms)
// before | 30.35 | 29.86 | 48.99 | 32.54 | 4.93 |
// blittable* | 5.69 | 5.52 | 27.51 | 7.78 | 5.65 |
//
// * without IsBlittable check
// => 4-6x faster!
//
// WriteQuaternion x 100k, Macbook Pro 2015 @ 2.2Ghz, Unity 2020.1 (release mode)
//
// | Median | Min | Max | Avg | Std | (ms)
// before | 9.41 | 8.90 | 23.02 | 10.72 | 3.07 |
// blittable* | 1.48 | 1.40 | 16.03 | 2.60 | 2.71 |
//
// * without IsBlittable check
// => 6x faster!
//
// Note:
// WriteBlittable assumes same endianness for server & client.
// All Unity 2018+ platforms are little endian.
// => run NetworkWriterTests.BlittableOnThisPlatform() to verify!
//
// This is not safe to expose to random structs.
// * StructLayout.Sequential is the default, which is safe.
// if the struct contains a reference type, it is converted to Auto.
// but since all structs here are unmanaged blittable, it's safe.
// see also: https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.layoutkind?view=netframework-4.8#system-runtime-interopservices-layoutkind-sequential
// * StructLayout.Pack depends on CPU word size.
// this may be different 4 or 8 on some ARM systems, etc.
// this is not safe, and would cause bytes/shorts etc. to be padded.
// see also: https://docs.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.structlayoutattribute.pack?view=net-6.0
// * If we force pack all to '1', they would have no padding which is
// great for bandwidth. but on some android systems, CPU can't read
// unaligned memory.
// see also: https://github.com/vis2k/Mirror/issues/3044
// * The only option would be to force explicit layout with multiples
// of word size. but this requires lots of weaver checking and is
// still questionable (IL2CPP etc.).
//
// Note: inlining WriteBlittable is enough. don't inline WriteInt etc.
// we don't want WriteBlittable to be copied in place everywhere.
internal unsafe void WriteBlittable<T>(T value)
where T : unmanaged
{
// check if blittable for safety
#if UNITY_EDITOR
if (!UnsafeUtility.IsBlittable(typeof(T)))
{
Debug.LogError($"{typeof(T)} is not blittable!");
return;
}
#endif
// calculate size
// sizeof(T) gets the managed size at compile time.
// Marshal.SizeOf<T> gets the unmanaged size at runtime (slow).
// => our 1mio writes benchmark is 6x slower with Marshal.SizeOf<T>
// => for blittable types, sizeof(T) is even recommended:
// https://docs.microsoft.com/en-us/dotnet/standard/native-interop/best-practices
int size = sizeof(T);
// ensure capacity
// NOTE that our runtime resizing comes at no extra cost because:
// 1. 'has space' checks are necessary even for fixed sized writers.
// 2. all writers will eventually be large enough to stop resizing.
EnsureCapacity(Position + size);
// write blittable
fixed (byte* ptr = &buffer[Position])
{
#if UNITY_ANDROID
// on some android systems, assigning *(T*)ptr throws a NRE if
// the ptr isn't aligned (i.e. if Position is 1,2,3,5, etc.).
// here we have to use memcpy.
//
// => we can't get a pointer of a struct in C# without
// marshalling allocations
// => instead, we stack allocate an array of type T and use that
// => stackalloc avoids GC and is very fast. it only works for
// value types, but all blittable types are anyway.
//
// this way, we can still support blittable reads on android.
// see also: https://github.com/vis2k/Mirror/issues/3044
// (solution discovered by AIIO, FakeByte, mischa)
T* valueBuffer = stackalloc T[1]{value};
UnsafeUtility.MemCpy(ptr, valueBuffer, size);
#else
// cast buffer to T* pointer, then assign value to the area
*(T*)ptr = value;
#endif
}
Position += size;
}
// blittable'?' template for code reuse
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void WriteBlittableNullable<T>(T? value)
where T : unmanaged
{
// bool isn't blittable. write as byte.
WriteByte((byte)(value.HasValue ? 0x01 : 0x00));
// only write value if exists. saves bandwidth.
if (value.HasValue)
WriteBlittable(value.Value);
}
public void WriteByte(byte value) => WriteBlittable(value);
// for byte arrays with consistent size, where the reader knows how many to read
// (like a packet opcode that's always the same)
public void WriteBytes(byte[] array, int offset, int count)
{
EnsureCapacity(Position + count);
Array.ConstrainedCopy(array, offset, this.buffer, Position, count);
Position += count;
}
// write an unsafe byte* array.
// useful for bit tree compression, etc.
public unsafe bool WriteBytes(byte* ptr, int offset, int size)
{
EnsureCapacity(Position + size);
fixed (byte* destination = &buffer[Position])
{
// write 'size' bytes at position
// 10 mio writes: 868ms
// Array.Copy(value.Array, value.Offset, buffer, Position, value.Count);
// 10 mio writes: 775ms
// Buffer.BlockCopy(value.Array, value.Offset, buffer, Position, value.Count);
// 10 mio writes: 637ms
UnsafeUtility.MemCpy(destination, ptr + offset, size);
}
Position += size;
return true;
}
/// <summary>Writes any type that mirror supports. Uses weaver populated Writer(T).write.</summary>
public void Write<T>(T value)
{
Action<NetworkWriter, T> writeDelegate = Writer<T>.write;
if (writeDelegate == null)
{
Debug.LogError($"No writer found for {typeof(T)}. This happens either if you are missing a NetworkWriter extension for your custom type, or if weaving failed. Try to reimport a script to weave again.");
}
else
{
writeDelegate(this, value);
}
}
// print with buffer content for easier debugging.
// [content, position / capacity].
// showing "position / space" would be too confusing.
public override string ToString() =>
$"[{ToArraySegment().ToHexString()} @ {Position}/{Capacity}]";
}
/// <summary>Helper class that weaver populates with all writer types.</summary>
// Note that c# creates a different static variable for each type
// -> Weaver.ReaderWriterProcessor.InitializeReaderAndWriters() populates it
public static class Writer<T>
{
public static Action<NetworkWriter, T> write;
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 48d2207bcef1f4477b624725f075f9bd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/NetworkWriter.cs
uploadId: 736421

View File

@ -0,0 +1,471 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Mirror
{
// Mirror's Weaver automatically detects all NetworkWriter function types,
// but they do all need to be extensions.
public static class NetworkWriterExtensions
{
public static void WriteByte(this NetworkWriter writer, byte value) => writer.WriteBlittable(value);
public static void WriteByteNullable(this NetworkWriter writer, byte? value) => writer.WriteBlittableNullable(value);
public static void WriteSByte(this NetworkWriter writer, sbyte value) => writer.WriteBlittable(value);
public static void WriteSByteNullable(this NetworkWriter writer, sbyte? value) => writer.WriteBlittableNullable(value);
// char is not blittable. convert to ushort.
public static void WriteChar(this NetworkWriter writer, char value) => writer.WriteBlittable((ushort)value);
public static void WriteCharNullable(this NetworkWriter writer, char? value) => writer.WriteBlittableNullable((ushort?)value);
// bool is not blittable. convert to byte.
public static void WriteBool(this NetworkWriter writer, bool value) => writer.WriteBlittable((byte)(value ? 1 : 0));
public static void WriteBoolNullable(this NetworkWriter writer, bool? value) => writer.WriteBlittableNullable(value.HasValue ? ((byte)(value.Value ? 1 : 0)) : new byte?());
public static void WriteShort(this NetworkWriter writer, short value) => writer.WriteBlittable(value);
public static void WriteShortNullable(this NetworkWriter writer, short? value) => writer.WriteBlittableNullable(value);
public static void WriteUShort(this NetworkWriter writer, ushort value) => writer.WriteBlittable(value);
public static void WriteUShortNullable(this NetworkWriter writer, ushort? value) => writer.WriteBlittableNullable(value);
public static void WriteInt(this NetworkWriter writer, int value) => writer.WriteBlittable(value);
public static void WriteIntNullable(this NetworkWriter writer, int? value) => writer.WriteBlittableNullable(value);
public static void WriteUInt(this NetworkWriter writer, uint value) => writer.WriteBlittable(value);
public static void WriteUIntNullable(this NetworkWriter writer, uint? value) => writer.WriteBlittableNullable(value);
public static void WriteLong(this NetworkWriter writer, long value) => writer.WriteBlittable(value);
public static void WriteLongNullable(this NetworkWriter writer, long? value) => writer.WriteBlittableNullable(value);
public static void WriteULong(this NetworkWriter writer, ulong value) => writer.WriteBlittable(value);
public static void WriteULongNullable(this NetworkWriter writer, ulong? value) => writer.WriteBlittableNullable(value);
// WriteInt/UInt/Long/ULong writes full bytes by default.
// define additional "VarInt" versions that Weaver will automatically prefer.
// 99% of the time [SyncVar] ints are small values, which makes this very much worth it.
[WeaverPriority] public static void WriteVarInt(this NetworkWriter writer, int value) => Compression.CompressVarInt(writer, value);
[WeaverPriority] public static void WriteVarUInt(this NetworkWriter writer, uint value) => Compression.CompressVarUInt(writer, value);
[WeaverPriority] public static void WriteVarLong(this NetworkWriter writer, long value) => Compression.CompressVarInt(writer, value);
[WeaverPriority] public static void WriteVarULong(this NetworkWriter writer, ulong value) => Compression.CompressVarUInt(writer, value);
public static void WriteFloat(this NetworkWriter writer, float value) => writer.WriteBlittable(value);
public static void WriteFloatNullable(this NetworkWriter writer, float? value) => writer.WriteBlittableNullable(value);
public static void WriteDouble(this NetworkWriter writer, double value) => writer.WriteBlittable(value);
public static void WriteDoubleNullable(this NetworkWriter writer, double? value) => writer.WriteBlittableNullable(value);
public static void WriteDecimal(this NetworkWriter writer, decimal value) => writer.WriteBlittable(value);
public static void WriteDecimalNullable(this NetworkWriter writer, decimal? value) => writer.WriteBlittableNullable(value);
public static void WriteHalf(this NetworkWriter writer, Half value) => writer.WriteUShort(value._value);
public static void WriteString(this NetworkWriter writer, string value)
{
// we offset count by '1' to easily support null without writing another byte.
// encoding null as '0' instead of '-1' also allows for better compression
// (ushort vs. short / varuint vs. varint) etc.
if (value == null)
{
writer.WriteUShort(0);
return;
}
// WriteString copies into the buffer manually.
// need to ensure capacity here first, manually.
int maxSize = writer.encoding.GetMaxByteCount(value.Length);
writer.EnsureCapacity(writer.Position + 2 + maxSize); // 2 bytes position + N bytes encoding
// encode it into the buffer first.
// reserve 2 bytes for header after we know how much was written.
int written = writer.encoding.GetBytes(value, 0, value.Length, writer.buffer, writer.Position + 2);
// check if within max size, otherwise Reader can't read it.
if (written > NetworkWriter.MaxStringLength)
throw new IndexOutOfRangeException($"NetworkWriter.WriteString - Value too long: {written} bytes. Limit: {NetworkWriter.MaxStringLength} bytes");
// .Position is unchanged, so fill in the size header now.
// we already ensured that max size fits into ushort.max-1.
writer.WriteUShort(checked((ushort)(written + 1))); // Position += 2
// now update position by what was written above
writer.Position += written;
}
// Weaver needs a write function with just one byte[] parameter
// (we don't name it .Write(byte[]) because it's really a WriteBytesAndSize since we write size / null info too)
public static void WriteBytesAndSize(this NetworkWriter writer, byte[] buffer)
{
// buffer might be null, so we can't use .Length in that case
writer.WriteBytesAndSize(buffer, 0, buffer != null ? buffer.Length : 0);
}
// for byte arrays with dynamic size, where the reader doesn't know how many will come
// (like an inventory with different items etc.)
public static void WriteBytesAndSize(this NetworkWriter writer, byte[] buffer, int offset, int count)
{
// null is supported because [SyncVar]s might be structs with null byte[] arrays.
// we offset count by '1' to easily support null without writing another byte.
// encoding null as '0' instead of '-1' also allows for better compression
// (ushort vs. short / varuint vs. varint) etc.
if (buffer == null)
{
// most sizes are small, write size as VarUInt!
Compression.CompressVarUInt(writer, 0u);
// writer.WriteUInt(0u);
return;
}
// most sizes are small, write size as VarUInt!
Compression.CompressVarUInt(writer, checked((uint)count) + 1u);
// writer.WriteUInt(checked((uint)count) + 1u);
writer.WriteBytes(buffer, offset, count);
}
// writes ArraySegment of byte (most common type) and size header
public static void WriteArraySegmentAndSize(this NetworkWriter writer, ArraySegment<byte> segment)
{
writer.WriteBytesAndSize(segment.Array, segment.Offset, segment.Count);
}
// writes ArraySegment of any type, and size header
public static void WriteArraySegment<T>(this NetworkWriter writer, ArraySegment<T> segment)
{
// we offset count by '1' to easily support null without writing another byte.
// encoding null as '0' instead of '-1' also allows for better compression
// (ushort vs. short / varuint vs. varint) etc.
//
// ArraySegment technically can't be null, but users may call:
// - WriteArraySegment
// - ReadArray
// in which case ReadArray needs null support. both need to be compatible.
int count = segment.Count;
// most sizes are small, write size as VarUInt!
Compression.CompressVarUInt(writer, checked((uint)count) + 1u);
// writer.WriteUInt(checked((uint)count) + 1u);
for (int i = 0; i < count; i++)
{
writer.Write(segment.Array[segment.Offset + i]);
}
}
public static void WriteVector2(this NetworkWriter writer, Vector2 value) => writer.WriteBlittable(value);
public static void WriteVector2Nullable(this NetworkWriter writer, Vector2? value) => writer.WriteBlittableNullable(value);
public static void WriteVector3(this NetworkWriter writer, Vector3 value) => writer.WriteBlittable(value);
public static void WriteVector3Nullable(this NetworkWriter writer, Vector3? value) => writer.WriteBlittableNullable(value);
public static void WriteVector4(this NetworkWriter writer, Vector4 value) => writer.WriteBlittable(value);
public static void WriteVector4Nullable(this NetworkWriter writer, Vector4? value) => writer.WriteBlittableNullable(value);
public static void WriteVector2Int(this NetworkWriter writer, Vector2Int value) => writer.WriteBlittable(value);
public static void WriteVector2IntNullable(this NetworkWriter writer, Vector2Int? value) => writer.WriteBlittableNullable(value);
public static void WriteVector3Int(this NetworkWriter writer, Vector3Int value) => writer.WriteBlittable(value);
public static void WriteVector3IntNullable(this NetworkWriter writer, Vector3Int? value) => writer.WriteBlittableNullable(value);
public static void WriteColor(this NetworkWriter writer, Color value) => writer.WriteBlittable(value);
public static void WriteColorNullable(this NetworkWriter writer, Color? value) => writer.WriteBlittableNullable(value);
public static void WriteColor32(this NetworkWriter writer, Color32 value) => writer.WriteBlittable(value);
public static void WriteColor32Nullable(this NetworkWriter writer, Color32? value) => writer.WriteBlittableNullable(value);
public static void WriteQuaternion(this NetworkWriter writer, Quaternion value) => writer.WriteBlittable(value);
public static void WriteQuaternionNullable(this NetworkWriter writer, Quaternion? value) => writer.WriteBlittableNullable(value);
// Rect is a struct with properties instead of fields
public static void WriteRect(this NetworkWriter writer, Rect value)
{
writer.WriteVector2(value.position);
writer.WriteVector2(value.size);
}
public static void WriteRectNullable(this NetworkWriter writer, Rect? value)
{
writer.WriteBool(value.HasValue);
if (value.HasValue)
writer.WriteRect(value.Value);
}
// Plane is a struct with properties instead of fields
public static void WritePlane(this NetworkWriter writer, Plane value)
{
writer.WriteVector3(value.normal);
writer.WriteFloat(value.distance);
}
public static void WritePlaneNullable(this NetworkWriter writer, Plane? value)
{
writer.WriteBool(value.HasValue);
if (value.HasValue)
writer.WritePlane(value.Value);
}
// Ray is a struct with properties instead of fields
public static void WriteRay(this NetworkWriter writer, Ray value)
{
writer.WriteVector3(value.origin);
writer.WriteVector3(value.direction);
}
public static void WriteRayNullable(this NetworkWriter writer, Ray? value)
{
writer.WriteBool(value.HasValue);
if (value.HasValue)
writer.WriteRay(value.Value);
}
// LayerMask is a struct with properties instead of fields
public static void WriteLayerMask(this NetworkWriter writer, LayerMask layerMask)
{
// 32 layers as a flags enum, max value of 496, we only need a UShort.
writer.WriteUShort((ushort)layerMask.value);
}
public static void WriteLayerMaskNullable(this NetworkWriter writer, LayerMask? layerMask)
{
writer.WriteBool(layerMask.HasValue);
if (layerMask.HasValue)
writer.WriteLayerMask(layerMask.Value);
}
public static void WriteMatrix4x4(this NetworkWriter writer, Matrix4x4 value) => writer.WriteBlittable(value);
public static void WriteMatrix4x4Nullable(this NetworkWriter writer, Matrix4x4? value) => writer.WriteBlittableNullable(value);
public static void WriteGuid(this NetworkWriter writer, Guid value)
{
#if !UNITY_2021_3_OR_NEWER
// Unity 2019 doesn't have Span yet
byte[] data = value.ToByteArray();
writer.WriteBytes(data, 0, data.Length);
#else
// WriteBlittable(Guid) isn't safe. see WriteBlittable comments.
// Guid is Sequential, but we can't guarantee packing.
// TryWriteBytes is safe and allocation free.
writer.EnsureCapacity(writer.Position + 16);
value.TryWriteBytes(new Span<byte>(writer.buffer, writer.Position, 16));
writer.Position += 16;
#endif
}
public static void WriteGuidNullable(this NetworkWriter writer, Guid? value)
{
writer.WriteBool(value.HasValue);
if (value.HasValue)
writer.WriteGuid(value.Value);
}
public static void WriteNetworkIdentity(this NetworkWriter writer, NetworkIdentity value)
{
if (value == null)
{
writer.WriteUInt(0);
return;
}
// users might try to use unspawned / prefab GameObjects in
// rpcs/cmds/syncvars/messages. they would be null on the other
// end, and it might not be obvious why. let's make it obvious.
// https://github.com/vis2k/Mirror/issues/2060
//
// => warning (instead of exception) because we also use a warning
// if a GameObject doesn't have a NetworkIdentity component etc.
if (value.netId == 0)
Debug.LogWarning($"Attempted to serialize unspawned GameObject: {value.name}. Prefabs and unspawned GameObjects would always be null on the other side. Please spawn it before using it in [SyncVar]s/Rpcs/Cmds/NetworkMessages etc.");
writer.WriteUInt(value.netId);
}
public static void WriteNetworkBehaviour(this NetworkWriter writer, NetworkBehaviour value)
{
if (value == null)
{
writer.WriteUInt(0);
return;
}
// users might try to use unspawned / prefab NetworkBehaviours in
// rpcs/cmds/syncvars/messages. they would be null on the other
// end, and it might not be obvious why. let's make it obvious.
// https://github.com/vis2k/Mirror/issues/2060
// and more recently https://github.com/MirrorNetworking/Mirror/issues/3399
//
// => warning (instead of exception) because we also use a warning
// when writing an unspawned NetworkIdentity
if (value.netId == 0)
{
Debug.LogWarning($"Attempted to serialize unspawned NetworkBehaviour: of type {value.GetType()} on GameObject {value.name}. Prefabs and unspawned GameObjects would always be null on the other side. Please spawn it before using it in [SyncVar]s/Rpcs/Cmds/NetworkMessages etc.");
writer.WriteUInt(0);
return;
}
writer.WriteUInt(value.netId);
writer.WriteByte(value.ComponentIndex);
}
public static void WriteTransform(this NetworkWriter writer, Transform value)
{
if (value == null)
{
writer.WriteUInt(0);
return;
}
if (value.TryGetComponent(out NetworkIdentity identity))
{
writer.WriteUInt(identity.netId);
}
else
{
// if users attempt to pass a transform without NetworkIdentity
// to a [Command] or [SyncVar], it should show an obvious warning.
Debug.LogWarning($"Attempted to sync a Transform ({value}) which isn't networked. Transforms without a NetworkIdentity component can't be synced.");
writer.WriteUInt(0);
}
}
public static void WriteGameObject(this NetworkWriter writer, GameObject value)
{
if (value == null)
{
writer.WriteUInt(0);
return;
}
// warn if the GameObject doesn't have a NetworkIdentity,
if (!value.TryGetComponent(out NetworkIdentity identity))
Debug.LogWarning($"Attempted to sync a GameObject ({value}) which isn't networked. GameObject without a NetworkIdentity component can't be synced.");
// serialize the correct amount of data in any case to make sure
// that the other end can read the expected amount of data too.
writer.WriteNetworkIdentity(identity);
}
// while SyncList<T> is recommended for NetworkBehaviours,
// structs may have .List<T> members which weaver needs to be able to
// fully serialize for NetworkMessages etc.
// note that Weaver/Writers/GenerateWriter() handles this manually.
public static void WriteList<T>(this NetworkWriter writer, List<T> list)
{
// we offset count by '1' to easily support null without writing another byte.
// encoding null as '0' instead of '-1' also allows for better compression
// (ushort vs. short / varuint vs. varint) etc.
if (list is null)
{
// most sizes are small, write size as VarUInt!
Compression.CompressVarUInt(writer, 0u);
// writer.WriteUInt(0);
return;
}
// check if within max size, otherwise Reader can't read it.
if (list.Count > NetworkReader.AllocationLimit)
throw new IndexOutOfRangeException($"NetworkWriter.WriteList - List<{typeof(T)}> too big: {list.Count} elements. Limit: {NetworkReader.AllocationLimit}");
// most sizes are small, write size as VarUInt!
Compression.CompressVarUInt(writer, checked((uint)list.Count) + 1u);
// writer.WriteUInt(checked((uint)list.Count) + 1u);
for (int i = 0; i < list.Count; i++)
writer.Write(list[i]);
}
// while SyncSet<T> is recommended for NetworkBehaviours,
// structs may have .Set<T> members which weaver needs to be able to
// fully serialize for NetworkMessages etc.
// note that Weaver/Writers/GenerateWriter() handles this manually.
public static void WriteHashSet<T>(this NetworkWriter writer, HashSet<T> hashSet)
{
// we offset count by '1' to easily support null without writing another byte.
// encoding null as '0' instead of '-1' also allows for better compression
// (ushort vs. short / varuint vs. varint) etc.
if (hashSet is null)
{
// most sizes are small, write size as VarUInt!
Compression.CompressVarUInt(writer, 0u);
//writer.WriteUInt(0);
return;
}
// most sizes are small, write size as VarUInt!
Compression.CompressVarUInt(writer, checked((uint)hashSet.Count) + 1u);
//writer.WriteUInt(checked((uint)hashSet.Count) + 1u);
foreach (T item in hashSet)
writer.Write(item);
}
public static void WriteArray<T>(this NetworkWriter writer, T[] array)
{
// we offset count by '1' to easily support null without writing another byte.
// encoding null as '0' instead of '-1' also allows for better compression
// (ushort vs. short / varuint vs. varint) etc.
if (array is null)
{
// most sizes are small, write size as VarUInt!
Compression.CompressVarUInt(writer, 0u);
// writer.WriteUInt(0);
return;
}
// check if within max size, otherwise Reader can't read it.
if (array.Length > NetworkReader.AllocationLimit)
throw new IndexOutOfRangeException($"NetworkWriter.WriteArray - Array<{typeof(T)}> too big: {array.Length} elements. Limit: {NetworkReader.AllocationLimit}");
// most sizes are small, write size as VarUInt!
Compression.CompressVarUInt(writer, checked((uint)array.Length) + 1u);
// writer.WriteUInt(checked((uint)array.Length) + 1u);
for (int i = 0; i < array.Length; i++)
writer.Write(array[i]);
}
public static void WriteUri(this NetworkWriter writer, Uri uri)
{
writer.WriteString(uri?.ToString());
}
public static void WriteTexture2D(this NetworkWriter writer, Texture2D texture2D)
{
// TODO allocation protection when sending textures to server.
// currently can allocate 32k x 32k x 4 byte = 3.8 GB
// support 'null' textures for [SyncVar]s etc.
// https://github.com/vis2k/Mirror/issues/3144
// simply send -1 for width.
if (texture2D == null)
{
writer.WriteShort(-1);
return;
}
// check if within max size, otherwise Reader can't read it.
int totalSize = texture2D.width * texture2D.height;
if (totalSize > NetworkReader.AllocationLimit)
throw new IndexOutOfRangeException($"NetworkWriter.WriteTexture2D - Texture2D total size (width*height) too big: {totalSize}. Limit: {NetworkReader.AllocationLimit}");
// write dimensions first so reader can create the texture with size
// 32k x 32k short is more than enough
writer.WriteShort((short)texture2D.width);
writer.WriteShort((short)texture2D.height);
writer.WriteArray(texture2D.GetPixels32());
}
public static void WriteSprite(this NetworkWriter writer, Sprite sprite)
{
// support 'null' textures for [SyncVar]s etc.
// https://github.com/vis2k/Mirror/issues/3144
// simply send a 'null' for texture content.
if (sprite == null)
{
writer.WriteTexture2D(null);
return;
}
writer.WriteTexture2D(sprite.texture);
writer.WriteRect(sprite.rect);
writer.WriteVector2(sprite.pivot);
}
public static void WriteDateTime(this NetworkWriter writer, DateTime dateTime)
{
writer.WriteDouble(dateTime.ToOADate());
}
public static void WriteDateTimeNullable(this NetworkWriter writer, DateTime? dateTime)
{
writer.WriteBool(dateTime.HasValue);
if (dateTime.HasValue)
writer.WriteDouble(dateTime.Value.ToOADate());
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 94259792df2a404892c3e2377f58d0cb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/NetworkWriterExtensions.cs
uploadId: 736421

View File

@ -0,0 +1,40 @@
// API consistent with Microsoft's ObjectPool<T>.
using System.Runtime.CompilerServices;
namespace Mirror
{
/// <summary>Pool of NetworkWriters to avoid allocations.</summary>
public static class NetworkWriterPool
{
// reuse Pool<T>
// we still wrap it in NetworkWriterPool.Get/Recycle so we can reset the
// position before reusing.
// this is also more consistent with NetworkReaderPool where we need to
// assign the internal buffer before reusing.
static readonly Pool<NetworkWriterPooled> Pool = new Pool<NetworkWriterPooled>(
() => new NetworkWriterPooled(),
// initial capacity to avoid allocations in the first few frames
// 1000 * 1200 bytes = around 1 MB.
1000
);
// expose count for testing
public static int Count => Pool.Count;
/// <summary>Get a writer from the pool. Creates new one if pool is empty.</summary>
public static NetworkWriterPooled Get()
{
// grab from pool & reset position
NetworkWriterPooled writer = Pool.Get();
writer.Reset();
return writer;
}
/// <summary>Return a writer to the pool.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Return(NetworkWriterPooled writer)
{
Pool.Return(writer);
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 3f34b53bea38e4f259eb8dc211e4fdb6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/NetworkWriterPool.cs
uploadId: 736421

View File

@ -0,0 +1,10 @@
using System;
namespace Mirror
{
/// <summary>Pooled NetworkWriter, automatically returned to pool when using 'using'</summary>
public sealed class NetworkWriterPooled : NetworkWriter, IDisposable
{
public void Dispose() => NetworkWriterPool.Return(this);
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: a9fab936bf3c4716a452d94ad5ecbebe
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/NetworkWriterPooled.cs
uploadId: 736421

View File

@ -0,0 +1,13 @@
// convenience interface for transports which use a port.
// useful for cases where someone wants to 'just set the port' independent of
// which transport it is.
//
// note that not all transports have ports, but most do.
namespace Mirror
{
public interface PortTransport
{
ushort Port { get; set; }
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: f7c7c2820d7974cb28c7bfe9aae890a0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/PortTransport.cs
uploadId: 736421

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7e8e801f9c7f4b858d9a6c162e64ca84
timeCreated: 1694005962

View File

@ -0,0 +1,195 @@
// standalone, easy to test algorithms for prediction
using System.Collections.Generic;
using UnityEngine;
namespace Mirror
{
// prediction may capture Rigidbody3D/2D/etc. state
// have a common interface.
public interface PredictedState
{
double timestamp { get; }
// use Vector3 for both Rigidbody3D and Rigidbody2D, that's fine
Vector3 position { get; set; }
Vector3 positionDelta { get; set; }
Quaternion rotation { get; set; }
Quaternion rotationDelta { get; set; }
Vector3 velocity { get; set; }
Vector3 velocityDelta { get; set; }
Vector3 angularVelocity { get; set; }
Vector3 angularVelocityDelta { get; set; }
}
public static class Prediction
{
// get the two states closest to a given timestamp.
// those can be used to interpolate the exact state at that time.
// => RingBuffer: see prediction_ringbuffer_2 branch, but it's slower!
public static bool Sample<T>(
SortedList<double, T> history,
double timestamp, // current server time
out T before,
out T after,
out int afterIndex,
out double t) // interpolation factor
{
before = default;
after = default;
t = 0;
afterIndex = -1;
// can't sample an empty history
// interpolation needs at least two entries.
// can't Lerp(A, A, 1.5). dist(A, A) * 1.5 is always 0.
if (history.Count < 2) {
return false;
}
// older than oldest
if (timestamp < history.Keys[0]) {
return false;
}
// iterate through the history
// TODO this needs to be faster than O(N)
// search around that area.
// should be O(1) most of the time, unless sampling was off.
int index = 0; // manually count when iterating. easier than for-int loop.
KeyValuePair<double, T> prev = new KeyValuePair<double, T>();
// SortedList foreach iteration allocates a LOT. use for-int instead.
// foreach (KeyValuePair<double, T> entry in history) {
for (int i = 0; i < history.Count; ++i)
{
double key = history.Keys[i];
T value = history.Values[i];
// exact match?
if (timestamp == key)
{
before = value;
after = value;
afterIndex = index;
t = Mathd.InverseLerp(key, key, timestamp);
return true;
}
// did we check beyond timestamp? then return the previous two.
if (key > timestamp)
{
before = prev.Value;
after = value;
afterIndex = index;
t = Mathd.InverseLerp(prev.Key, key, timestamp);
return true;
}
// remember the last
prev = new KeyValuePair<double, T>(key, value);
index += 1;
}
return false;
}
// inserts a server state into the client's history.
// readjust the deltas of the states after the inserted one.
// returns the corrected final position.
// => RingBuffer: see prediction_ringbuffer_2 branch, but it's slower!
public static T CorrectHistory<T>(
SortedList<double, T> history,
int stateHistoryLimit,
T corrected, // corrected state with timestamp
T before, // state in history before the correction
T after, // state in history after the correction
int afterIndex) // index of the 'after' value so we don't need to find it again here
where T: PredictedState
{
// respect the limit
// TODO unit test to check if it respects max size
if (history.Count >= stateHistoryLimit)
{
history.RemoveAt(0);
afterIndex -= 1; // we removed the first value so all indices are off by one now
}
// PERFORMANCE OPTIMIZATION: avoid O(N) insertion, only readjust all values after.
// the end result is the same since after.delta and after.position are both recalculated.
// it's technically not correct if we were to reconstruct final position from 0..after..end but
// we never do, we only ever iterate from after..end!
//
// insert the corrected state into the history, or overwrite if already exists
// SortedList insertions are O(N)!
// history[corrected.timestamp] = corrected;
// afterIndex += 1; // we inserted the corrected value before the previous index
// the entry behind the inserted one still has the delta from (before, after).
// we need to correct it to (corrected, after).
//
// for example:
// before: (t=1.0, delta=10, position=10)
// after: (t=3.0, delta=20, position=30)
//
// then we insert:
// corrected: (t=2.5, delta=__, position=25)
//
// previous delta was from t=1.0 to t=3.0 => 2.0
// inserted delta is from t=2.5 to t=3.0 => 0.5
// multiplier is 0.5 / 2.0 = 0.25
// multiply 'after.delta(20)' by 0.25 to get the new 'after.delta(5)
//
// so the new history is:
// before: (t=1.0, delta=10, position=10)
// corrected: (t=2.5, delta=__, position=25)
// after: (t=3.0, delta= 5, position=__)
//
// so when we apply the correction, the new after.position would be:
// corrected.position(25) + after.delta(5) = 30
//
double previousDeltaTime = after.timestamp - before.timestamp; // 3.0 - 1.0 = 2.0
double correctedDeltaTime = after.timestamp - corrected.timestamp; // 3.0 - 2.5 = 0.5
// fix multiplier becoming NaN if previousDeltaTime is 0:
// double multiplier = correctedDeltaTime / previousDeltaTime;
double multiplier = previousDeltaTime != 0 ? correctedDeltaTime / previousDeltaTime : 0; // 0.5 / 2.0 = 0.25
// recalculate 'after.delta' with the multiplier
after.positionDelta = Vector3.Lerp(Vector3.zero, after.positionDelta, (float)multiplier);
after.velocityDelta = Vector3.Lerp(Vector3.zero, after.velocityDelta, (float)multiplier);
after.angularVelocityDelta = Vector3.Lerp(Vector3.zero, after.angularVelocityDelta, (float)multiplier);
// Quaternions always need to be normalized in order to be a valid rotation after operations
after.rotationDelta = Quaternion.Slerp(Quaternion.identity, after.rotationDelta, (float)multiplier).normalized;
// changes aren't saved until we overwrite them in the history
history[after.timestamp] = after;
// second step: readjust all absolute values by rewinding client's delta moves on top of it.
T last = corrected;
for (int i = afterIndex; i < history.Count; ++i)
{
double key = history.Keys[i];
T value = history.Values[i];
// correct absolute position based on last + delta.
value.position = last.position + value.positionDelta;
value.velocity = last.velocity + value.velocityDelta;
value.angularVelocity = last.angularVelocity + value.angularVelocityDelta;
// Quaternions always need to be normalized in order to be a valid rotation after operations
value.rotation = (value.rotationDelta * last.rotation).normalized; // quaternions add delta by multiplying in this order
// save the corrected entry into history.
history[key] = value;
// save last
last = value;
}
// third step: return the final recomputed state.
return last;
}
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 216d494d910445ea8a7acc7c889212d5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/Prediction/Prediction.cs
uploadId: 736421

View File

@ -0,0 +1,151 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Mirror.RemoteCalls
{
// invoke type for Cmd/Rpc
public enum RemoteCallType { Command, ClientRpc }
// remote call function delegate
public delegate void RemoteCallDelegate(NetworkBehaviour obj, NetworkReader reader, NetworkConnectionToClient senderConnection);
class Invoker
{
// GameObjects might have multiple components of TypeA.CommandA().
// when invoking, we check if 'TypeA' is an instance of the type.
// the hash itself isn't enough because we wouldn't know which component
// to invoke it on if there are multiple of the same type.
public Type componentType;
public RemoteCallType callType;
public RemoteCallDelegate function;
public bool cmdRequiresAuthority;
public bool AreEqual(Type componentType, RemoteCallType remoteCallType, RemoteCallDelegate invokeFunction) =>
this.componentType == componentType &&
this.callType == remoteCallType &&
this.function == invokeFunction;
}
/// <summary>Used to help manage remote calls for NetworkBehaviours</summary>
public static class RemoteProcedureCalls
{
public const string InvokeRpcPrefix = "InvokeUserCode_";
// one lookup for all remote calls.
// allows us to easily add more remote call types without duplicating code.
// note: do not clear those with [RuntimeInitializeOnLoad]
//
// IMPORTANT: cmd/rpc functions are identified via **HASHES**.
// an index would requires half the bandwidth, but introduces issues
// where static constructors are lazily called, so index order isn't
// guaranteed. keep hashes to avoid:
// https://github.com/vis2k/Mirror/pull/3135
// https://github.com/vis2k/Mirror/issues/3138
// BUT: 2 byte hash is enough if we check for collisions. that's what we
// do for NetworkMessage as well.
static readonly Dictionary<ushort, Invoker> remoteCallDelegates = new Dictionary<ushort, Invoker>();
static bool CheckIfDelegateExists(Type componentType, RemoteCallType remoteCallType, RemoteCallDelegate func, ushort functionHash)
{
if (remoteCallDelegates.ContainsKey(functionHash))
{
// something already registered this hash.
// it's okay if it was the same function.
Invoker oldInvoker = remoteCallDelegates[functionHash];
if (oldInvoker.AreEqual(componentType, remoteCallType, func))
{
return true;
}
// otherwise notify user. there is a rare chance of string
// hash collisions.
Debug.LogError($"Function {oldInvoker.componentType}.{oldInvoker.function.GetMethodName()} and {componentType}.{func.GetMethodName()} have the same hash. Please rename one of them. To save bandwidth, we only use 2 bytes for the hash, which has a small chance of collisions.");
}
return false;
}
// pass full function name to avoid ClassA.Func & ClassB.Func collisions
internal static ushort RegisterDelegate(Type componentType, string functionFullName, RemoteCallType remoteCallType, RemoteCallDelegate func, bool cmdRequiresAuthority = true)
{
// type+func so Inventory.RpcUse != Equipment.RpcUse
ushort hash = (ushort)(functionFullName.GetStableHashCode() & 0xFFFF);
if (CheckIfDelegateExists(componentType, remoteCallType, func, hash))
return hash;
remoteCallDelegates[hash] = new Invoker
{
callType = remoteCallType,
componentType = componentType,
function = func,
cmdRequiresAuthority = cmdRequiresAuthority
};
return hash;
}
// pass full function name to avoid ClassA.Func <-> ClassB.Func collisions
// need to pass componentType to support invoking on GameObjects with
// multiple components of same type with same remote call.
public static void RegisterCommand(Type componentType, string functionFullName, RemoteCallDelegate func, bool requiresAuthority) =>
RegisterDelegate(componentType, functionFullName, RemoteCallType.Command, func, requiresAuthority);
// pass full function name to avoid ClassA.Func <-> ClassB.Func collisions
// need to pass componentType to support invoking on GameObjects with
// multiple components of same type with same remote call.
public static void RegisterRpc(Type componentType, string functionFullName, RemoteCallDelegate func) =>
RegisterDelegate(componentType, functionFullName, RemoteCallType.ClientRpc, func);
// to clean up tests
internal static void RemoveDelegate(ushort hash) =>
remoteCallDelegates.Remove(hash);
internal static bool GetFunctionMethodName(ushort functionHash, out string methodName)
{
if (remoteCallDelegates.TryGetValue(functionHash, out Invoker invoker))
{
methodName = invoker.function.GetMethodName().Replace(InvokeRpcPrefix, "");
return true;
}
methodName = "";
return false;
}
// note: no need to throw an error if not found.
// an attacker might just try to call a cmd with an rpc's hash etc.
// returning false is enough.
static bool GetInvokerForHash(ushort functionHash, RemoteCallType remoteCallType, out Invoker invoker) =>
remoteCallDelegates.TryGetValue(functionHash, out invoker) &&
invoker != null &&
invoker.callType == remoteCallType;
// InvokeCmd/Rpc Delegate can all use the same function here
internal static bool Invoke(ushort functionHash, RemoteCallType remoteCallType, NetworkReader reader, NetworkBehaviour component, NetworkConnectionToClient senderConnection = null)
{
// IMPORTANT: we check if the message's componentIndex component is
// actually of the right type. prevents attackers trying
// to invoke remote calls on wrong components.
if (GetInvokerForHash(functionHash, remoteCallType, out Invoker invoker) &&
invoker.componentType.IsInstanceOfType(component))
{
// invoke function on this component
invoker.function(component, reader, senderConnection);
return true;
}
return false;
}
// check if the command 'requiresAuthority' which is set in the attribute
internal static bool CommandRequiresAuthority(ushort cmdHash) =>
GetInvokerForHash(cmdHash, RemoteCallType.Command, out Invoker invoker) &&
invoker.cmdRequiresAuthority;
/// <summary>Gets the handler function by hash. Useful for profilers and debuggers.</summary>
public static RemoteCallDelegate GetDelegate(ushort functionHash) =>
remoteCallDelegates.TryGetValue(functionHash, out Invoker invoker)
? invoker.function
: null;
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: d2cdbcbd1e377d6408a91acbec31ba16
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/RemoteCalls.cs
uploadId: 736421

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4468e736f87964eaebb9d55fc3e132f7
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,17 @@
// Snapshot interface so we can reuse it for all kinds of systems.
// for example, NetworkTransform, NetworkRigidbody, CharacterController etc.
// NOTE: we use '<T>' and 'where T : Snapshot' to avoid boxing.
// List<Snapshot> would cause allocations through boxing.
namespace Mirror
{
public interface Snapshot
{
// the remote timestamp (when it was sent by the remote)
double remoteTime { get; set; }
// the local timestamp (when it was received on our end)
// technically not needed for basic snapshot interpolation.
// only for dynamic buffer time adjustment.
double localTime { get; set; }
}
}

View File

@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 12afea28fdb94154868a0a3b7a9df55b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 129321
packageName: Mirror
packageVersion: 96.0.1
assetPath: Assets/Mirror/Core/SnapshotInterpolation/Snapshot.cs
uploadId: 736421

Some files were not shown because too many files have changed in this diff Show More