// ----------------------------------------------------------------------------
//
// PhotonNetwork Framework for Unity - Copyright (C) 2018 Exit Games GmbH
//
//
// PhotonNetwork is the central class of the PUN package.
//
// developer@exitgames.com
// ----------------------------------------------------------------------------
namespace Photon.Pun
{
using System;
using System.Linq;
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using ExitGames.Client.Photon;
using Photon.Realtime;
using Hashtable = ExitGames.Client.Photon.Hashtable;
using SupportClassPun = ExitGames.Client.Photon.SupportClass;
public static partial class PhotonNetwork
{
private static HashSet allowedReceivingGroups = new HashSet();
private static HashSet blockedSendingGroups = new HashSet();
private static HashSet reusablePVHashset = new HashSet();
///
/// The photon view list.
///
private static NonAllocDictionary photonViewList = new NonAllocDictionary();
///
/// Gets the photon views.
///
///
/// This is an expensive operation as it returns a copy of the internal list.
///
/// The photon views.
[System.Obsolete("Use PhotonViewCollection instead for an iterable collection of current photonViews.")]
public static PhotonView[] PhotonViews
{
get
{
var views = new PhotonView[photonViewList.Count];
int idx = 0;
foreach (var v in photonViewList.Values)
{
views[idx] = v;
idx++;
}
return views;
}
}
///
/// Returns a new iterable collection of current photon views.
///
///
/// You can iterate over all PhotonViews in a simple foreach loop.
/// To use this in a while-loop, assign the new iterator to a variable and then call MoveNext on that.
///
public static NonAllocDictionary.ValueIterator PhotonViewCollection
{
get
{
return photonViewList.Values;
}
}
public static int ViewCount
{
get { return photonViewList.Count; }
}
/// Parameters: PhotonView for which ownership changed, previous owner of the view.
private static event Action OnOwnershipRequestEv;
/// Parameters: PhotonView for which ownership was requested, player who requests ownership.
private static event Action OnOwnershipTransferedEv;
/// Parameters: PhotonView for which ownership was requested, player who requested (but didn't get) ownership.
private static event Action OnOwnershipTransferFailedEv;
///
/// Registers an object for callbacks for the implemented callback-interfaces.
///
///
/// The covered callback interfaces are: IConnectionCallbacks, IMatchmakingCallbacks,
/// ILobbyCallbacks, IInRoomCallbacks, IOnEventCallback and IWebRpcCallback.
///
/// See: .Net Callbacks
///
/// The object that registers to get callbacks from PUN's LoadBalancingClient.
public static void AddCallbackTarget(object target)
{
if (target is PhotonView)
{
return;
}
IPunOwnershipCallbacks punOwnershipCallback = target as IPunOwnershipCallbacks;
if (punOwnershipCallback != null)
{
OnOwnershipRequestEv += punOwnershipCallback.OnOwnershipRequest;
OnOwnershipTransferedEv += punOwnershipCallback.OnOwnershipTransfered;
OnOwnershipTransferFailedEv += punOwnershipCallback.OnOwnershipTransferFailed;
}
NetworkingClient.AddCallbackTarget(target);
}
///
/// Removes the target object from callbacks for its implemented callback-interfaces.
///
///
/// The covered callback interfaces are: IConnectionCallbacks, IMatchmakingCallbacks,
/// ILobbyCallbacks, IInRoomCallbacks, IOnEventCallback and IWebRpcCallback.
///
/// See: .Net Callbacks
///
/// The object that unregisters from getting callbacks.
public static void RemoveCallbackTarget(object target)
{
if (target is PhotonView || NetworkingClient == null)
{
return;
}
IPunOwnershipCallbacks punOwnershipCallback = target as IPunOwnershipCallbacks;
if (punOwnershipCallback != null)
{
OnOwnershipRequestEv -= punOwnershipCallback.OnOwnershipRequest;
OnOwnershipTransferedEv -= punOwnershipCallback.OnOwnershipTransfered;
OnOwnershipTransferFailedEv -= punOwnershipCallback.OnOwnershipTransferFailed;
}
NetworkingClient.RemoveCallbackTarget(target);
}
internal static string CallbacksToString()
{
var x = NetworkingClient.ConnectionCallbackTargets.Select(m => m.ToString()).ToArray();
return string.Join(", ", x);
}
internal static byte currentLevelPrefix = 0;
/// Internally used to flag if the message queue was disabled by a "scene sync" situation (to re-enable it).
internal static bool loadingLevelAndPausedNetwork = false;
/// For automatic scene syncing, the loaded scene is put into a room property. This is the name of said prop.
internal const string CurrentSceneProperty = "curScn";
internal const string CurrentScenePropertyLoadAsync = "curScnLa";
///
/// An Object Pool can be used to keep and reuse instantiated object instances. Replaces Unity's default Instantiate and Destroy methods.
///
///
/// Defaults to the DefaultPool type.
/// To use a GameObject pool, implement IPunPrefabPool and assign it here.
/// Prefabs are identified by name.
///
public static IPunPrefabPool PrefabPool
{
get
{
return prefabPool;
}
set
{
if (value == null)
{
Debug.LogWarning("PhotonNetwork.PrefabPool cannot be set to null. It will default back to using the 'DefaultPool' Pool");
prefabPool = new DefaultPool();
}
else
{
prefabPool = value;
}
}
}
private static IPunPrefabPool prefabPool;
///
/// While enabled, the MonoBehaviours on which we call RPCs are cached, avoiding costly GetComponents<MonoBehaviour>() calls.
///
///
/// RPCs are called on the MonoBehaviours of a target PhotonView. Those have to be found via GetComponents.
///
/// When set this to true, the list of MonoBehaviours gets cached in each PhotonView.
/// You can use photonView.RefreshRpcMonoBehaviourCache() to manually refresh a PhotonView's
/// list of MonoBehaviours on demand (when a new MonoBehaviour gets added to a networked GameObject, e.g.).
///
public static bool UseRpcMonoBehaviourCache;
private static readonly Dictionary> monoRPCMethodsCache = new Dictionary>();
private static Dictionary rpcShortcuts; // lookup "table" for the index (shortcut) of an RPC name
///
/// If an RPC method is implemented as coroutine, it gets started, unless this value is false.
///
///
/// As starting coroutines causes a little memnory garbage, you may want to disable this option but it is
/// also good enough to not return IEnumerable from methods with the attribite PunRPC.
///
public static bool RunRpcCoroutines = true;
// for asynchronous network synched loading.
private static AsyncOperation _AsyncLevelLoadingOperation;
private static float _levelLoadingProgress = 0f;
///
/// Represents the scene loading progress when using LoadLevel().
///
///
/// The value is 0 if the app never loaded a scene with LoadLevel().
/// During async scene loading, the value is between 0 and 1.
/// Once any scene completed loading, it stays at 1 (signaling "done").
///
/// The level loading progress. Ranges from 0 to 1.
public static float LevelLoadingProgress
{
get
{
if (_AsyncLevelLoadingOperation != null)
{
_levelLoadingProgress = _AsyncLevelLoadingOperation.progress;
}
else if (_levelLoadingProgress > 0f)
{
_levelLoadingProgress = 1f;
}
return _levelLoadingProgress;
}
}
///
/// Called when "this client" left a room to clean up.
///
///
/// if (Server == ServerConnection.GameServer && (state == ClientState.Disconnecting || state == ClientState.DisconnectingFromGameServer))
///
private static void LeftRoomCleanup()
{
// Clean up if we were loading asynchronously.
if (_AsyncLevelLoadingOperation != null)
{
_AsyncLevelLoadingOperation.allowSceneActivation = false;
_AsyncLevelLoadingOperation = null;
}
bool wasInRoom = NetworkingClient.CurrentRoom != null;
// when leaving a room, we clean up depending on that room's settings.
bool autoCleanupSettingOfRoom = wasInRoom && CurrentRoom.AutoCleanUp;
allowedReceivingGroups = new HashSet();
blockedSendingGroups = new HashSet();
// Cleanup all network objects (all spawned PhotonViews, local and remote)
if (autoCleanupSettingOfRoom || offlineModeRoom != null)
{
LocalCleanupAnythingInstantiated(true);
}
}
///
/// Cleans up anything that was instantiated in-game (not loaded with the scene). Resets views that are not destroyed.
///
// TODO: This method name no longer matches is function. It also resets room object's views.
internal static void LocalCleanupAnythingInstantiated(bool destroyInstantiatedGameObjects)
{
//if (tempInstantiationData.Count > 0)
//{
// Debug.LogWarning("It seems some instantiation is not completed, as instantiation data is used. You should make sure instantiations are paused when calling this method. Cleaning now, despite ");
//}
// Destroy GO's (if we should)
if (destroyInstantiatedGameObjects)
{
// Fill list with Instantiated objects
HashSet instantiatedGos = new HashSet();
foreach (PhotonView view in photonViewList.Values)
{
if (view.isRuntimeInstantiated)
{
instantiatedGos.Add(view.gameObject); // HashSet keeps each object only once
}
// For non-instantiated objects (scene objects) - reset the view
else
{
view.ResetPhotonView(true);
}
}
foreach (GameObject go in instantiatedGos)
{
RemoveInstantiatedGO(go, true);
}
}
// photonViewList is cleared of anything instantiated (so scene items are left inside)
// any other lists can be
PhotonNetwork.lastUsedViewSubId = 0;
PhotonNetwork.lastUsedViewSubIdStatic = 0;
}
///
/// Resets the PhotonView "lastOnSerializeDataSent" so that "OnReliable" synched PhotonViews send a complete state to new clients (if the state doesnt change, no messages would be send otherwise!).
/// Note that due to this reset, ALL other players will receive the full OnSerialize.
///
private static void ResetPhotonViewsOnSerialize()
{
foreach (PhotonView photonView in photonViewList.Values)
{
photonView.lastOnSerializeDataSent = null;
}
}
// PHOTONVIEW/RPC related
#pragma warning disable 0414
private static readonly Type typePunRPC = typeof(PunRPC);
private static readonly Type typePhotonMessageInfo = typeof(PhotonMessageInfo);
private static readonly object keyByteZero = (byte)0;
private static readonly object keyByteOne = (byte)1;
private static readonly object keyByteTwo = (byte)2;
private static readonly object keyByteThree = (byte)3;
private static readonly object keyByteFour = (byte)4;
private static readonly object keyByteFive = (byte)5;
private static readonly object keyByteSix = (byte)6;
private static readonly object keyByteSeven = (byte)7;
private static readonly object keyByteEight = (byte)8;
private static readonly object[] emptyObjectArray = new object[0];
private static readonly Type[] emptyTypeArray = new Type[0];
#pragma warning restore 0414
///
/// Executes a received RPC event
///
internal static void ExecuteRpc(Hashtable rpcData, Player sender)
{
if (rpcData == null || !rpcData.ContainsKey(keyByteZero))
{
Debug.LogError("Malformed RPC; this should never occur. Content: " + SupportClassPun.DictionaryToString(rpcData));
return;
}
// ts: updated with "flat" event data
int netViewID = (int)rpcData[keyByteZero]; // LIMITS PHOTONVIEWS&PLAYERS
int otherSidePrefix = 0; // by default, the prefix is 0 (and this is not being sent)
if (rpcData.ContainsKey(keyByteOne))
{
otherSidePrefix = (short)rpcData[keyByteOne];
}
string inMethodName;
if (rpcData.ContainsKey(keyByteFive))
{
int rpcIndex = (byte)rpcData[keyByteFive]; // LIMITS RPC COUNT
if (rpcIndex > PhotonNetwork.PhotonServerSettings.RpcList.Count - 1)
{
Debug.LogError("Could not find RPC with index: " + rpcIndex + ". Going to ignore! Check PhotonServerSettings.RpcList");
return;
}
else
{
inMethodName = PhotonNetwork.PhotonServerSettings.RpcList[rpcIndex];
}
}
else
{
inMethodName = (string)rpcData[keyByteThree];
}
object[] arguments = null;
if (rpcData.ContainsKey(keyByteFour))
{
arguments = (object[])rpcData[keyByteFour];
}
PhotonView photonNetview = GetPhotonView(netViewID);
if (photonNetview == null)
{
int viewOwnerId = netViewID / PhotonNetwork.MAX_VIEW_IDS;
bool owningPv = (viewOwnerId == NetworkingClient.LocalPlayer.ActorNumber);
bool ownerSent = sender != null && viewOwnerId == sender.ActorNumber;
if (owningPv)
{
Debug.LogWarning("Received RPC \"" + inMethodName + "\" for viewID " + netViewID + " but this PhotonView does not exist! View was/is ours." + (ownerSent ? " Owner called." : " Remote called.") + " By: " + sender);
}
else
{
Debug.LogWarning("Received RPC \"" + inMethodName + "\" for viewID " + netViewID + " but this PhotonView does not exist! Was remote PV." + (ownerSent ? " Owner called." : " Remote called.") + " By: " + sender + " Maybe GO was destroyed but RPC not cleaned up.");
}
return;
}
if (photonNetview.Prefix != otherSidePrefix)
{
Debug.LogError("Received RPC \"" + inMethodName + "\" on viewID " + netViewID + " with a prefix of " + otherSidePrefix + ", our prefix is " + photonNetview.Prefix + ". The RPC has been ignored.");
return;
}
// Get method name
if (string.IsNullOrEmpty(inMethodName))
{
Debug.LogError("Malformed RPC; this should never occur. Content: " + SupportClassPun.DictionaryToString(rpcData));
return;
}
if (PhotonNetwork.LogLevel >= PunLogLevel.Full)
{
Debug.Log("Received RPC: " + inMethodName);
}
// SetReceiving filtering
if (photonNetview.Group != 0 && !allowedReceivingGroups.Contains(photonNetview.Group))
{
return; // Ignore group
}
Type[] argumentsTypes = null;
if (arguments != null && arguments.Length > 0)
{
argumentsTypes = new Type[arguments.Length];
int i = 0;
for (int index = 0; index < arguments.Length; index++)
{
object objX = arguments[index];
if (objX == null)
{
argumentsTypes[i] = null;
}
else
{
argumentsTypes[i] = objX.GetType();
}
i++;
}
}
int receivers = 0;
int foundMethods = 0;
if (!PhotonNetwork.UseRpcMonoBehaviourCache || photonNetview.RpcMonoBehaviours == null || photonNetview.RpcMonoBehaviours.Length == 0)
{
photonNetview.RefreshRpcMonoBehaviourCache();
}
for (int componentsIndex = 0; componentsIndex < photonNetview.RpcMonoBehaviours.Length; componentsIndex++)
{
MonoBehaviour monob = photonNetview.RpcMonoBehaviours[componentsIndex];
if (monob == null)
{
Debug.LogError("ERROR You have missing MonoBehaviours on your gameobjects!");
continue;
}
Type type = monob.GetType();
// Get [PunRPC] methods from cache
List cachedRPCMethods = null;
bool methodsOfTypeInCache = monoRPCMethodsCache.TryGetValue(type, out cachedRPCMethods);
if (!methodsOfTypeInCache)
{
List entries = SupportClassPun.GetMethods(type, typePunRPC);
monoRPCMethodsCache[type] = entries;
cachedRPCMethods = entries;
}
if (cachedRPCMethods == null)
{
continue;
}
// Check cache for valid methodname+arguments
for (int index = 0; index < cachedRPCMethods.Count; index++)
{
MethodInfo mInfo = cachedRPCMethods[index];
if (!mInfo.Name.Equals(inMethodName))
{
continue;
}
ParameterInfo[] parameters = mInfo.GetCachedParemeters();
foundMethods++;
// if we got no arguments:
if (arguments == null)
{
if (parameters.Length == 0)
{
receivers++;
object o = mInfo.Invoke((object)monob, null);
if (PhotonNetwork.RunRpcCoroutines)
{
IEnumerator ie = null;//o as IEnumerator;
if ((ie = o as IEnumerator) != null)
{
PhotonHandler.Instance.StartCoroutine(ie);
}
}
}
else if (parameters.Length == 1 && parameters[0].ParameterType == typeof(PhotonMessageInfo))
{
int sendTime = (int)rpcData[keyByteTwo];
receivers++;
object o = mInfo.Invoke((object)monob, new object[] { new PhotonMessageInfo(sender, sendTime, photonNetview) });
if (PhotonNetwork.RunRpcCoroutines)
{
IEnumerator ie = null;//o as IEnumerator;
if ((ie = o as IEnumerator) != null)
{
PhotonHandler.Instance.StartCoroutine(ie);
}
}
}
continue;
}
// if there are any arguments (in the incoming call check if the method is compatible
if (parameters.Length == arguments.Length)
{
// Normal, PhotonNetworkMessage left out
if (CheckTypeMatch(parameters, argumentsTypes))
{
receivers++;
object o = mInfo.Invoke((object)monob, arguments);
if (PhotonNetwork.RunRpcCoroutines)
{
IEnumerator ie = null;//o as IEnumerator;
if ((ie = o as IEnumerator) != null)
{
PhotonHandler.Instance.StartCoroutine(ie);
}
}
}
continue;
}
if (parameters.Length == arguments.Length + 1)
{
// Check for PhotonNetworkMessage being the last
if (parameters[parameters.Length - 1].ParameterType == typeof(PhotonMessageInfo) && CheckTypeMatch(parameters, argumentsTypes))
{
int sendTime = (int)rpcData[keyByteTwo];
object[] argumentsWithInfo = new object[arguments.Length + 1];
arguments.CopyTo(argumentsWithInfo, 0);
argumentsWithInfo[argumentsWithInfo.Length - 1] = new PhotonMessageInfo(sender, sendTime, photonNetview);
receivers++;
object o = mInfo.Invoke((object)monob, argumentsWithInfo);
if (PhotonNetwork.RunRpcCoroutines)
{
IEnumerator ie = null;//o as IEnumerator;
if ((ie = o as IEnumerator) != null)
{
PhotonHandler.Instance.StartCoroutine(ie);
}
}
}
continue;
}
if (parameters.Length == 1 && parameters[0].ParameterType.IsArray)
{
receivers++;
object o = mInfo.Invoke((object)monob, new object[] { arguments });
if (PhotonNetwork.RunRpcCoroutines)
{
IEnumerator ie = null;//o as IEnumerator;
if ((ie = o as IEnumerator) != null)
{
PhotonHandler.Instance.StartCoroutine(ie);
}
}
continue;
}
}
}
// Error handling
if (receivers != 1)
{
string argsString = string.Empty;
int argsLength = 0;
if (argumentsTypes != null)
{
argsLength = argumentsTypes.Length;
for (int index = 0; index < argumentsTypes.Length; index++)
{
Type ty = argumentsTypes[index];
if (argsString != string.Empty)
{
argsString += ", ";
}
if (ty == null)
{
argsString += "null";
}
else
{
argsString += ty.Name;
}
}
}
GameObject context = photonNetview != null ? photonNetview.gameObject : null;
if (receivers == 0)
{
if (foundMethods == 0)
{
// found no method that matches
Debug.LogErrorFormat(context, "RPC method '{0}({2})' not found on object with PhotonView {1}. Implement as non-static. Apply [PunRPC]. Components on children are not found. " +
"Return type must be void or IEnumerator (if you enable RunRpcCoroutines). RPCs are a one-way message.", inMethodName, netViewID, argsString);
}
else
{
// found a method but not the right arguments
Debug.LogErrorFormat(context, "RPC method '{0}' found on object with PhotonView {1} but has wrong parameters. Implement as '{0}({2})'. PhotonMessageInfo is optional as final parameter." +
"Return type must be void or IEnumerator (if you enable RunRpcCoroutines).", inMethodName, netViewID, argsString);
}
}
else
{
// multiple components have the same method
Debug.LogErrorFormat(context, "RPC method '{0}({2})' found {3}x on object with PhotonView {1}. Only one component should implement it." +
"Return type must be void or IEnumerator (if you enable RunRpcCoroutines).", inMethodName, netViewID, argsString, foundMethods);
}
}
}
///
/// Check if all types match with parameters. We can have more paramters then types (allow last RPC type to be different).
///
///
///
/// If the types-array has matching parameters (of method) in the parameters array (which may be longer).
private static bool CheckTypeMatch(ParameterInfo[] methodParameters, Type[] callParameterTypes)
{
if (methodParameters.Length < callParameterTypes.Length)
{
return false;
}
for (int index = 0; index < callParameterTypes.Length; index++)
{
#if NETFX_CORE
TypeInfo methodParamTI = methodParameters[index].ParameterType.GetTypeInfo();
TypeInfo callParamTI = callParameterTypes[index].GetTypeInfo();
if (callParameterTypes[index] != null && !methodParamTI.IsAssignableFrom(callParamTI) && !(callParamTI.IsEnum && System.Enum.GetUnderlyingType(methodParamTI.AsType()).GetTypeInfo().IsAssignableFrom(callParamTI)))
{
return false;
}
#else
Type type = methodParameters[index].ParameterType;
if (callParameterTypes[index] != null && !type.IsAssignableFrom(callParameterTypes[index]) && !(type.IsEnum && System.Enum.GetUnderlyingType(type).IsAssignableFrom(callParameterTypes[index])))
{
return false;
}
#endif
}
return true;
}
///
/// Destroys all Instantiates and RPCs locally and (if not localOnly) sends EvDestroy(player) and clears related events in the server buffer.
///
public static void DestroyPlayerObjects(int playerId, bool localOnly)
{
if (playerId <= 0)
{
Debug.LogError("Failed to Destroy objects of playerId: " + playerId);
return;
}
if (!localOnly)
{
// clean server's Instantiate and RPC buffers
OpRemoveFromServerInstantiationsOfPlayer(playerId);
OpCleanActorRpcBuffer(playerId);
// send Destroy(player) to anyone else
SendDestroyOfPlayer(playerId);
}
// locally cleaning up that player's objects
HashSet playersGameObjects = new HashSet();
// with ownership transfer, some objects might lose their owner.
// in that case, the creator becomes the owner again. every client can apply done below.
foreach (PhotonView view in photonViewList.Values)
{
if (view == null)
{
Debug.LogError("Null view");
continue;
}
// Mark player created objects for destruction
if (view.CreatorActorNr == playerId)
{
playersGameObjects.Add(view.gameObject);
continue;
}
if (view.OwnerActorNr == playerId)
{
var previousOwner = view.Owner;
view.OwnerActorNr = view.CreatorActorNr;
view.ControllerActorNr = view.CreatorActorNr;
// This callback was not originally here. Added with the IsMine caching changes.
if (PhotonNetwork.OnOwnershipTransferedEv != null)
{
PhotonNetwork.OnOwnershipTransferedEv(view, previousOwner);
}
}
}
// any non-local work is already done, so with the list of that player's objects, we can clean up (locally only)
foreach (GameObject gameObject in playersGameObjects)
{
RemoveInstantiatedGO(gameObject, true);
}
}
public static void DestroyAll(bool localOnly)
{
if (!localOnly)
{
OpRemoveCompleteCache();
SendDestroyOfAll();
}
LocalCleanupAnythingInstantiated(true);
}
internal static List foundPVs = new List();
/// Removes GameObject and the PhotonViews on it from local lists and optionally updates remotes. GameObject gets destroyed at end.
///
/// This method might fail and quit early due to several tests.
///
/// GameObject to cleanup.
/// For localOnly, tests of control are skipped and the server is not updated.
internal static void RemoveInstantiatedGO(GameObject go, bool localOnly)
{
// Avoid cleanup if we are quitting.
if (ConnectionHandler.AppQuits)
return;
if (go == null)
{
Debug.LogError("Failed to 'network-remove' GameObject because it's null.");
return;
}
// Don't remove the GO if it doesn't have any PhotonView
go.GetComponentsInChildren(true, foundPVs);
if (foundPVs.Count <= 0)
{
Debug.LogError("Failed to 'network-remove' GameObject because has no PhotonView components: " + go);
return;
}
PhotonView viewZero = foundPVs[0];
// Don't remove GOs that are owned by others (unless this is the master and the remote player left)
if (!localOnly)
{
//Debug.LogWarning("Destroy " + instantiationId + " creator " + creatorId, go);
if (!viewZero.IsMine)
{
Debug.LogError("Failed to 'network-remove' GameObject. Client is neither owner nor MasterClient taking over for owner who left: " + viewZero);
foundPVs.Clear(); // as foundPVs is re-used, clean it to avoid lingering references
return;
}
}
// cleanup instantiation (event and local list)
if (!localOnly)
{
ServerCleanInstantiateAndDestroy(viewZero); // server cleaning
}
int creatorActorNr = viewZero.CreatorActorNr;
// cleanup PhotonViews and their RPCs events (if not localOnly)
for (int j = foundPVs.Count - 1; j >= 0; j--)
{
PhotonView view = foundPVs[j];
if (view == null)
{
continue;
}
// TODO: Probably should have a enum that defines when auto-detachment should occur.
// Check nested PVs for different creator. Detach if different, to avoid destroying reparanted objects.
if (j != 0)
{
// view does not belong to the same object as the root PV - unparent this nested PV to avoid destruction.
if (view.CreatorActorNr != creatorActorNr)
{
view.transform.SetParent(null, true);
continue;
}
}
// Notify all children PVs of impending destruction. Send the root PV (the actual object getting destroyed) to the callbacks.
view.OnPreNetDestroy(viewZero);
// we only destroy/clean PhotonViews that were created by PhotonNetwork.Instantiate (and those have an instantiationId!)
if (view.InstantiationId >= 1)
{
LocalCleanPhotonView(view);
}
if (!localOnly)
{
OpCleanRpcBuffer(view);
}
}
if (PhotonNetwork.LogLevel >= PunLogLevel.Full)
{
Debug.Log("Network destroy Instantiated GO: " + go.name);
}
foundPVs.Clear(); // as foundPVs is re-used, clean it to avoid lingering references
go.SetActive(false); // PUN 2 disables objects before the return to the pool
prefabPool.Destroy(go); // PUN 2 always uses a PrefabPool (even for the default implementation)
}
private static readonly ExitGames.Client.Photon.Hashtable removeFilter = new ExitGames.Client.Photon.Hashtable();
private static readonly ExitGames.Client.Photon.Hashtable ServerCleanDestroyEvent = new ExitGames.Client.Photon.Hashtable();
private static readonly RaiseEventOptions ServerCleanOptions = new RaiseEventOptions() { CachingOption = EventCaching.RemoveFromRoomCache };
internal static RaiseEventOptions SendToAllOptions = new RaiseEventOptions() { Receivers = ReceiverGroup.All };
internal static RaiseEventOptions SendToOthersOptions = new RaiseEventOptions() { Receivers = ReceiverGroup.Others };
internal static RaiseEventOptions SendToSingleOptions = new RaiseEventOptions() { TargetActors = new int[1] };
///
/// Removes an instantiation event from the server's cache. Needs id and actorNr of player who instantiated.
///
private static void ServerCleanInstantiateAndDestroy(PhotonView photonView)
{
int filterId;
if (photonView.isRuntimeInstantiated)
{
filterId = photonView.InstantiationId; // actual, live InstantiationIds start with 1 and go up
// remove the Instantiate-event from the server cache:
removeFilter[keyByteSeven] = filterId;
ServerCleanOptions.CachingOption = EventCaching.RemoveFromRoomCache;
PhotonNetwork.RaiseEventInternal(PunEvent.Instantiation, removeFilter, ServerCleanOptions, SendOptions.SendReliable);
}
// Don't remove the Instantiation from the server, if it doesn't have a proper ID
else
{
filterId = photonView.ViewID;
}
// send a Destroy-event to everyone (removing an event from the cache, doesn't send this to anyone else):
ServerCleanDestroyEvent[keyByteZero] = filterId;
ServerCleanOptions.CachingOption = photonView.isRuntimeInstantiated ? EventCaching.DoNotCache : EventCaching.AddToRoomCacheGlobal; // if the view got loaded with the scene, cache EvDestroy for anyone (re)joining later
PhotonNetwork.RaiseEventInternal(PunEvent.Destroy, ServerCleanDestroyEvent, ServerCleanOptions, SendOptions.SendReliable);
}
private static void SendDestroyOfPlayer(int actorNr)
{
ExitGames.Client.Photon.Hashtable evData = new ExitGames.Client.Photon.Hashtable();
evData[keyByteZero] = actorNr;
PhotonNetwork.RaiseEventInternal(PunEvent.DestroyPlayer, evData, null, SendOptions.SendReliable);
}
private static void SendDestroyOfAll()
{
ExitGames.Client.Photon.Hashtable evData = new ExitGames.Client.Photon.Hashtable();
evData[keyByteZero] = -1;
PhotonNetwork.RaiseEventInternal(PunEvent.DestroyPlayer, evData, null, SendOptions.SendReliable);
}
private static void OpRemoveFromServerInstantiationsOfPlayer(int actorNr)
{
// removes all "Instantiation" events of player actorNr. this is not an event for anyone else
RaiseEventOptions options = new RaiseEventOptions() { CachingOption = EventCaching.RemoveFromRoomCache, TargetActors = new int[] { actorNr } };
PhotonNetwork.RaiseEventInternal(PunEvent.Instantiation, null, options, SendOptions.SendReliable);
}
internal static void RequestOwnership(int viewID, int fromOwner)
{
//Debug.Log("RequestOwnership(): " + viewID + " from: " + fromOwner + " Time: " + Environment.TickCount % 1000);
PhotonNetwork.RaiseEventInternal(PunEvent.OwnershipRequest, new int[] { viewID, fromOwner }, SendToAllOptions, SendOptions.SendReliable);
}
internal static void TransferOwnership(int viewID, int playerID)
{
//Debug.Log("TransferOwnership() view " + viewID + " to: " + playerID + " Time: " + Environment.TickCount % 1000);
PhotonNetwork.RaiseEventInternal(PunEvent.OwnershipTransfer, new int[] { viewID, playerID }, SendToAllOptions, SendOptions.SendReliable);
}
///
/// Call this on the Master to reassert ownership on clients. viewOwnerPairs are [viewId][viewOwnerActorNr] pairs. targetActor of -1 indicates send to all others.
///
internal static void OwnershipUpdate(int[] viewOwnerPairs, int targetActor = -1)
{
RaiseEventOptions opts;
if (targetActor == -1)
{
opts = SendToOthersOptions;
}
else
{
SendToSingleOptions.TargetActors[0] = targetActor;
opts = SendToSingleOptions;
}
PhotonNetwork.RaiseEventInternal(PunEvent.OwnershipUpdate, viewOwnerPairs, opts, SendOptions.SendReliable);
}
public static bool LocalCleanPhotonView(PhotonView view)
{
view.removedFromLocalViewList = true;
return photonViewList.Remove(view.ViewID);
}
public static PhotonView GetPhotonView(int viewID)
{
PhotonView result = null;
photonViewList.TryGetValue(viewID, out result);
/// Removed aggressive find that likely had no real use case, and was expensive.
//if (result == null)
//{
// PhotonView[] views = GameObject.FindObjectsOfType(typeof(PhotonView)) as PhotonView[];
// for (int i = 0; i < views.Length; i++)
// {
// PhotonView view = views[i];
// if (view.ViewID == viewID)
// {
// if (view.didAwake)
// {
// Debug.LogWarning("Had to lookup view that wasn't in photonViewList: " + view);
// }
// return view;
// }
// }
//}
return result;
}
public static void RegisterPhotonView(PhotonView netView)
{
if (!Application.isPlaying)
{
photonViewList = new NonAllocDictionary();
return;
}
if (netView.ViewID == 0)
{
// don't register views with ID 0 (not initialized). they register when a ID is assigned later on
Debug.Log("PhotonView register is ignored, because viewID is 0. No id assigned yet to: " + netView);
return;
}
PhotonView listedView = null;
bool isViewListed = photonViewList.TryGetValue(netView.ViewID, out listedView);
if (isViewListed)
{
// if some other view is in the list already, we got a problem. it might be indestructible. print out error
if (netView != listedView)
{
Debug.LogError(string.Format("PhotonView ID duplicate found: {0}. New: {1} old: {2}. Maybe one wasn't destroyed on scene load?! Check for 'DontDestroyOnLoad'. Destroying old entry, adding new.", netView.ViewID, netView, listedView));
}
else
{
return;
}
RemoveInstantiatedGO(listedView.gameObject, true);
}
// Debug.Log("adding view to known list: " + netView);
photonViewList.Add(netView.ViewID, netView);
netView.removedFromLocalViewList = false;
//Debug.LogError("view being added. " + netView); // Exit Games internal log
if (PhotonNetwork.LogLevel >= PunLogLevel.Full)
{
Debug.Log("Registered PhotonView: " + netView.ViewID);
}
}
///
/// Removes the RPCs of someone else (to be used as master).
/// This won't clean any local caches. It just tells the server to forget a player's RPCs and instantiates.
///
///
public static void OpCleanActorRpcBuffer(int actorNumber)
{
RaiseEventOptions options = new RaiseEventOptions() { CachingOption = EventCaching.RemoveFromRoomCache, TargetActors = new int[] { actorNumber } };
PhotonNetwork.RaiseEventInternal(PunEvent.RPC, null, options, SendOptions.SendReliable);
}
///
/// Instead removing RPCs or Instantiates, this removed everything cached by the actor.
///
///
public static void OpRemoveCompleteCacheOfPlayer(int actorNumber)
{
RaiseEventOptions options = new RaiseEventOptions() { CachingOption = EventCaching.RemoveFromRoomCache, TargetActors = new int[] { actorNumber } };
PhotonNetwork.RaiseEventInternal(0, null, options, SendOptions.SendReliable);
}
public static void OpRemoveCompleteCache()
{
RaiseEventOptions options = new RaiseEventOptions() { CachingOption = EventCaching.RemoveFromRoomCache, Receivers = ReceiverGroup.MasterClient };
PhotonNetwork.RaiseEventInternal(0, null, options, SendOptions.SendReliable);
}
/// This clears the cache of any player/actor who's no longer in the room (making it a simple clean-up option for a new master)
private static void RemoveCacheOfLeftPlayers()
{
Dictionary opParameters = new Dictionary();
opParameters[ParameterCode.Code] = (byte)0; // any event
opParameters[ParameterCode.Cache] = (byte)EventCaching.RemoveFromRoomCacheForActorsLeft; // option to clear the room cache of all events of players who left
NetworkingClient.LoadBalancingPeer.SendOperation((byte)OperationCode.RaiseEvent, opParameters, SendOptions.SendReliable); // TODO: Check if this is the best implementation possible
}
// Remove RPCs of view (if they are local player's RPCs)
public static void CleanRpcBufferIfMine(PhotonView view)
{
if (view.OwnerActorNr != NetworkingClient.LocalPlayer.ActorNumber && !NetworkingClient.LocalPlayer.IsMasterClient)
{
Debug.LogError("Cannot remove cached RPCs on a PhotonView thats not ours! " + view.Owner + " scene: " + view.IsRoomView);
return;
}
OpCleanRpcBuffer(view);
}
private static readonly Hashtable rpcFilterByViewId = new ExitGames.Client.Photon.Hashtable();
private static readonly RaiseEventOptions OpCleanRpcBufferOptions = new RaiseEventOptions() { CachingOption = EventCaching.RemoveFromRoomCache };
/// Cleans server RPCs for PhotonView (without any further checks).
public static void OpCleanRpcBuffer(PhotonView view)
{
rpcFilterByViewId[keyByteZero] = view.ViewID;
PhotonNetwork.RaiseEventInternal(PunEvent.RPC, rpcFilterByViewId, OpCleanRpcBufferOptions, SendOptions.SendReliable);
}
///
/// Remove all buffered RPCs from server that were sent in the targetGroup, if this is the Master Client or if this controls the individual PhotonView.
///
///
/// This method requires either:
/// - This client is the Master Client (can remove any RPCs per group).
/// - Any other client: each PhotonView is checked if it is under this client's control. Only those RPCs are removed.
///
/// Interest group that gets all RPCs removed.
public static void RemoveRPCsInGroup(int group)
{
foreach (PhotonView view in photonViewList.Values)
{
if (view.Group == group)
{
CleanRpcBufferIfMine(view);
}
}
}
///
/// Clear buffered RPCs based on filter parameters.
///
/// The viewID of the PhotonView where the RPC has been called on. We actually need its ViewID. If 0 (default) is provided, all PhotonViews/ViewIDs are considered.
/// The RPC method name, if possible we will use its hash shortcut for efficiency. If none (null or empty string) is provided all RPC method names are considered.
/// The actor numbers of the players who called/buffered the RPC. For example if two players buffered the same RPC you can clear the buffered RPC of one and keep the other. If none (null or empty array) is provided all senders are considered.
/// If the operation could be sent to the server.
public static bool RemoveBufferedRPCs(int viewId = 0, string methodName = null, int[] callersActorNumbers = null/*, params object[] parameters*/)
{
Hashtable filter = new Hashtable(2);
if (viewId != 0)
{
filter[keyByteZero] = viewId;
}
if (!string.IsNullOrEmpty(methodName))
{
// send name or shortcut (if available)
int shortcut;
if (rpcShortcuts.TryGetValue(methodName, out shortcut))
{
filter[keyByteFive] = (byte)shortcut; // LIMITS RPC COUNT
}
else
{
filter[keyByteThree] = methodName;
}
}
//if (parameters != null && parameters.Length > 0)
//{
// filter[keyByteFour] = parameters;
//}
RaiseEventOptions raiseEventOptions = new RaiseEventOptions();
raiseEventOptions.CachingOption = EventCaching.RemoveFromRoomCache;
if (callersActorNumbers != null)
{
raiseEventOptions.TargetActors = callersActorNumbers;
}
return RaiseEventInternal(PunEvent.RPC, filter, raiseEventOptions, SendOptions.SendReliable);
}
///
/// Sets level prefix for PhotonViews instantiated later on. Don't set it if you need only one!
///
///
/// Important: If you don't use multiple level prefixes, simply don't set this value. The
/// default value is optimized out of the traffic.
///
/// This won't affect existing PhotonViews (they can't be changed yet for existing PhotonViews).
///
/// Messages sent with a different level prefix will be received but not executed. This affects
/// RPCs, Instantiates and synchronization.
///
/// Be aware that PUN never resets this value, you'll have to do so yourself.
///
/// Max value is short.MaxValue = 255
public static void SetLevelPrefix(byte prefix)
{
// TODO: check can use network
currentLevelPrefix = prefix;
// TODO: should we really change the prefix for existing PVs?! better keep it!
//foreach (PhotonView view in photonViewList.Values)
//{
// view.prefix = prefix;
//}
}
/// RPC Hashtable Structure
/// (byte)0 -> (int) ViewId (combined from actorNr and actor-unique-id)
/// (byte)1 -> (short) prefix (level)
/// (byte)2 -> (int) server timestamp
/// (byte)3 -> (string) methodname
/// (byte)4 -> (object[]) parameters
/// (byte)5 -> (byte) method shortcut (alternative to name)
///
/// This is sent as event (code: 200) which will contain a sender (origin of this RPC).
static ExitGames.Client.Photon.Hashtable rpcEvent = new ExitGames.Client.Photon.Hashtable();
static RaiseEventOptions RpcOptionsToAll = new RaiseEventOptions();
internal static void RPC(PhotonView view, string methodName, RpcTarget target, Player player, bool encrypt, params object[] parameters)
{
if (blockedSendingGroups.Contains(view.Group))
{
return; // Block sending on this group
}
if (view.ViewID < 1)
{
Debug.LogError("Illegal view ID:" + view.ViewID + " method: " + methodName + " GO:" + view.gameObject.name);
}
if (PhotonNetwork.LogLevel >= PunLogLevel.Full)
{
Debug.Log("Sending RPC \"" + methodName + "\" to target: " + target + " or player:" + player + ".");
}
//ts: changed RPCs to a one-level hashtable as described in internal.txt
rpcEvent.Clear();
rpcEvent[keyByteZero] = (int)view.ViewID; // LIMITS NETWORKVIEWS&PLAYERS
if (view.Prefix > 0)
{
rpcEvent[keyByteOne] = (short)view.Prefix;
}
rpcEvent[keyByteTwo] = PhotonNetwork.ServerTimestamp;
// send name or shortcut (if available)
int shortcut = 0;
if (rpcShortcuts.TryGetValue(methodName, out shortcut))
{
rpcEvent[keyByteFive] = (byte)shortcut; // LIMITS RPC COUNT
}
else
{
rpcEvent[keyByteThree] = methodName;
}
if (parameters != null && parameters.Length > 0)
{
rpcEvent[keyByteFour] = (object[])parameters;
}
SendOptions sendOptions = new SendOptions() { Reliability = true, Encrypt = encrypt };
// if sent to target player, this overrides the target
if (player != null)
{
if (NetworkingClient.LocalPlayer.ActorNumber == player.ActorNumber)
{
ExecuteRpc(rpcEvent, player);
}
else
{
RaiseEventOptions options = new RaiseEventOptions() { TargetActors = new int[] { player.ActorNumber } };
PhotonNetwork.RaiseEventInternal(PunEvent.RPC, rpcEvent, options, sendOptions);
// NetworkingClient.OpRaiseEvent(PunEvent.RPC, rpcEvent, options, new SendOptions() { Reliability = true, Encrypt = encrypt });
}
return;
}
switch (target)
{
// send to a specific set of players
case RpcTarget.All:
RpcOptionsToAll.InterestGroup = (byte)view.Group; // NOTE: Test-wise, this is static and re-used to avoid memory garbage
PhotonNetwork.RaiseEventInternal(PunEvent.RPC, rpcEvent, RpcOptionsToAll, sendOptions);
// Execute local
ExecuteRpc(rpcEvent, NetworkingClient.LocalPlayer);
break;
case RpcTarget.Others:
{
RaiseEventOptions options = new RaiseEventOptions() { InterestGroup = (byte)view.Group };
PhotonNetwork.RaiseEventInternal(PunEvent.RPC, rpcEvent, options, sendOptions);
break;
}
case RpcTarget.AllBuffered:
{
RaiseEventOptions options = new RaiseEventOptions() { CachingOption = EventCaching.AddToRoomCache };
PhotonNetwork.RaiseEventInternal(PunEvent.RPC, rpcEvent, options, sendOptions);
// Execute local
ExecuteRpc(rpcEvent, NetworkingClient.LocalPlayer);
break;
}
case RpcTarget.OthersBuffered:
{
RaiseEventOptions options = new RaiseEventOptions() { CachingOption = EventCaching.AddToRoomCache };
PhotonNetwork.RaiseEventInternal(PunEvent.RPC, rpcEvent, options, sendOptions);
break;
}
case RpcTarget.MasterClient:
{
if (NetworkingClient.LocalPlayer.IsMasterClient)
{
ExecuteRpc(rpcEvent, NetworkingClient.LocalPlayer);
}
else
{
RaiseEventOptions options = new RaiseEventOptions() { Receivers = ReceiverGroup.MasterClient };
PhotonNetwork.RaiseEventInternal(PunEvent.RPC, rpcEvent, options, sendOptions);
}
break;
}
case RpcTarget.AllViaServer:
{
RaiseEventOptions options = new RaiseEventOptions() { InterestGroup = (byte)view.Group, Receivers = ReceiverGroup.All };
PhotonNetwork.RaiseEventInternal(PunEvent.RPC, rpcEvent, options, sendOptions);
if (PhotonNetwork.OfflineMode)
{
ExecuteRpc(rpcEvent, NetworkingClient.LocalPlayer);
}
break;
}
case RpcTarget.AllBufferedViaServer:
{
RaiseEventOptions options = new RaiseEventOptions() { InterestGroup = (byte)view.Group, Receivers = ReceiverGroup.All, CachingOption = EventCaching.AddToRoomCache };
PhotonNetwork.RaiseEventInternal(PunEvent.RPC, rpcEvent, options, sendOptions);
if (PhotonNetwork.OfflineMode)
{
ExecuteRpc(rpcEvent, NetworkingClient.LocalPlayer);
}
break;
}
default:
Debug.LogError("Unsupported target enum: " + target);
break;
}
}
/// Enable/disable receiving on given Interest Groups (applied to PhotonViews).
///
/// A client can tell the server which Interest Groups it's interested in.
/// The server will only forward events for those Interest Groups to that client (saving bandwidth and performance).
///
/// See: https://doc.photonengine.com/en-us/pun/v2/gameplay/interestgroups
///
/// See: https://doc.photonengine.com/en-us/pun/v2/demos-and-tutorials/package-demos/culling-demo
///
/// The interest groups to disable (or null).
/// The interest groups to enable (or null).
public static void SetInterestGroups(byte[] disableGroups, byte[] enableGroups)
{
// TODO: check can use network
if (disableGroups != null)
{
if (disableGroups.Length == 0)
{
// a byte[0] should disable ALL groups in one step and before any groups are enabled. we do this locally, too.
allowedReceivingGroups.Clear();
}
else
{
for (int index = 0; index < disableGroups.Length; index++)
{
byte g = disableGroups[index];
if (g <= 0)
{
Debug.LogError("Error: PhotonNetwork.SetInterestGroups was called with an illegal group number: " + g + ". The Group number should be at least 1.");
continue;
}
if (allowedReceivingGroups.Contains(g))
{
allowedReceivingGroups.Remove(g);
}
}
}
}
if (enableGroups != null)
{
if (enableGroups.Length == 0)
{
// a byte[0] should enable ALL groups in one step. we do this locally, too.
for (byte index = 0; index < byte.MaxValue; index++)
{
allowedReceivingGroups.Add(index);
}
allowedReceivingGroups.Add(byte.MaxValue);
}
else
{
for (int index = 0; index < enableGroups.Length; index++)
{
byte g = enableGroups[index];
if (g <= 0)
{
Debug.LogError("Error: PhotonNetwork.SetInterestGroups was called with an illegal group number: " + g + ". The Group number should be at least 1.");
continue;
}
allowedReceivingGroups.Add(g);
}
}
}
if (!PhotonNetwork.offlineMode)
{
NetworkingClient.OpChangeGroups(disableGroups, enableGroups);
}
}
/// Enable/disable sending on given group (applied to PhotonViews)
///
/// This does not interact with the Photon server-side.
/// It's just a client-side setting to suppress updates, should they be sent to one of the blocked groups.
///
/// This setting is not particularly useful, as it means that updates literally never reach the server or anyone else.
/// Use with care.
///
/// The interest group to affect.
/// Sets if sending to group is enabled (or not).
public static void SetSendingEnabled(byte group, bool enabled)
{
// TODO: check can use network
if (!enabled)
{
blockedSendingGroups.Add(group); // can be added to HashSet no matter if already in it
}
else
{
blockedSendingGroups.Remove(group);
}
}
/// Enable/disable sending on given groups (applied to PhotonViews)
///
/// This does not interact with the Photon server-side.
/// It's just a client-side setting to suppress updates, should they be sent to one of the blocked groups.
///
/// This setting is not particularly useful, as it means that updates literally never reach the server or anyone else.
/// Use with care.
/// The interest groups to enable sending on (or null).
/// The interest groups to disable sending on (or null).
public static void SetSendingEnabled(byte[] disableGroups, byte[] enableGroups)
{
// TODO: check can use network
if (disableGroups != null)
{
for (int index = 0; index < disableGroups.Length; index++)
{
byte g = disableGroups[index];
blockedSendingGroups.Add(g);
}
}
if (enableGroups != null)
{
for (int index = 0; index < enableGroups.Length; index++)
{
byte g = enableGroups[index];
blockedSendingGroups.Remove(g);
}
}
}
internal static void NewSceneLoaded()
{
if (loadingLevelAndPausedNetwork)
{
_AsyncLevelLoadingOperation = null;
loadingLevelAndPausedNetwork = false;
PhotonNetwork.IsMessageQueueRunning = true;
}
else
{
PhotonNetwork.SetLevelInPropsIfSynced(SceneManagerHelper.ActiveSceneName);
}
// Debug.Log("OnLevelWasLoaded photonViewList.Count: " + photonViewList.Count); // Exit Games internal log
List removeKeys = new List();
foreach (KeyValuePair kvp in photonViewList)
{
PhotonView view = kvp.Value;
if (view == null)
{
removeKeys.Add(kvp.Key);
}
}
for (int index = 0; index < removeKeys.Count; index++)
{
int key = removeKeys[index];
photonViewList.Remove(key);
}
if (removeKeys.Count > 0)
{
if (PhotonNetwork.LogLevel >= PunLogLevel.Informational)
Debug.Log("New level loaded. Removed " + removeKeys.Count + " scene view IDs from last level.");
}
}
///
/// Defines how many updated produced by OnPhotonSerialize() are batched into one message.
///
///
/// A low number increases overhead, a high number might lead to fragmented messages.
///
public static int ObjectsInOneUpdate = 20;
private static readonly PhotonStream serializeStreamOut = new PhotonStream(true, null);
private static readonly PhotonStream serializeStreamIn = new PhotonStream(false, null);
/// cache the RaiseEventOptions to prevent redundant Memory Allocation
private static RaiseEventOptions serializeRaiseEvOptions = new RaiseEventOptions();
private struct RaiseEventBatch : IEquatable
{
public byte Group;
public bool Reliable;
public override int GetHashCode()
{
return (this.Group << 1) + (this.Reliable ? 1 : 0);
}
public bool Equals(RaiseEventBatch other)
{
return this.Reliable == other.Reliable && this.Group == other.Group;
}
}
private class SerializeViewBatch : IEquatable, IEquatable
{
public readonly RaiseEventBatch Batch;
public List