Skip to content

Commit

Permalink
Merge branch 'SubnauticaNitrox:master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
Papela committed Feb 18, 2023
2 parents a55b519 + b318963 commit 89957b8
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 106 deletions.
Original file line number Diff line number Diff line change
@@ -1,71 +1,53 @@
using NitroxClient.Communication.Abstract;
using NitroxClient.Communication.Abstract;
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxClient.MonoBehaviours;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.Util;
using NitroxModel.Packets;
using UnityEngine;

namespace NitroxClient.Communication.Packets.Processors
namespace NitroxClient.Communication.Packets.Processors;

public class SimulationOwnershipChangeProcessor : ClientPacketProcessor<SimulationOwnershipChange>
{
public class SimulationOwnershipChangeProcessor : ClientPacketProcessor<SimulationOwnershipChange>
{
private readonly IMultiplayerSession multiplayerSession;
private readonly SimulationOwnership simulationOwnershipManager;
private readonly IMultiplayerSession multiplayerSession;
private readonly SimulationOwnership simulationOwnershipManager;

public SimulationOwnershipChangeProcessor(IMultiplayerSession multiplayerSession, SimulationOwnership simulationOwnershipManager)
{
this.multiplayerSession = multiplayerSession;
this.simulationOwnershipManager = simulationOwnershipManager;
}
public SimulationOwnershipChangeProcessor(IMultiplayerSession multiplayerSession, SimulationOwnership simulationOwnershipManager)
{
this.multiplayerSession = multiplayerSession;
this.simulationOwnershipManager = simulationOwnershipManager;
}

public override void Process(SimulationOwnershipChange simulationOwnershipChange)
public override void Process(SimulationOwnershipChange simulationOwnershipChange)
{
foreach (SimulatedEntity simulatedEntity in simulationOwnershipChange.Entities)
{
foreach (SimulatedEntity simulatedEntity in simulationOwnershipChange.Entities)
if (multiplayerSession.Reservation.PlayerId == simulatedEntity.PlayerId)
{
if (multiplayerSession.Reservation.PlayerId == simulatedEntity.PlayerId)
if (simulatedEntity.ChangesPosition)
{
if (simulatedEntity.ChangesPosition)
{
StartBroadcastingEntityPosition(simulatedEntity.Id);
}

simulationOwnershipManager.SimulateEntity(simulatedEntity.Id, SimulationLockType.TRANSIENT);
EntityPositionBroadcaster.WatchEntity(simulatedEntity.Id);
}
else if (simulationOwnershipManager.HasAnyLockType(simulatedEntity.Id))
{
// The server has forcibly removed this lock from the client. This is generally fine for
// transient locks because it is only broadcasting position. However, exclusive locks may
// need additional cleanup (such as a person piloting a vehicle - they need to be kicked out)
// We can later add a forcibly removed callback but as of right now we have no use-cases for
// forcibly removing an exclusive lock. Just log it if it happens....

if (simulationOwnershipManager.HasExclusiveLock(simulatedEntity.Id))
{
Log.Warn($"The server has forcibly revoked an exlusive lock - this may cause undefined behaviour. GUID: {simulatedEntity.Id}");
}

simulationOwnershipManager.StopSimulatingEntity(simulatedEntity.Id);
EntityPositionBroadcaster.StopWatchingEntity(simulatedEntity.Id);
}
simulationOwnershipManager.SimulateEntity(simulatedEntity.Id, SimulationLockType.TRANSIENT);
}
}
else if (simulationOwnershipManager.HasAnyLockType(simulatedEntity.Id))
{
// The server has forcibly removed this lock from the client. This is generally fine for
// transient locks because it is only broadcasting position. However, exclusive locks may
// need additional cleanup (such as a person piloting a vehicle - they need to be kicked out)
// We can later add a forcibly removed callback but as of right now we have no use-cases for
// forcibly removing an exclusive lock. Just log it if it happens....

private void StartBroadcastingEntityPosition(NitroxId id)
{
Optional<GameObject> gameObject = NitroxEntity.GetObjectFrom(id);
if (simulationOwnershipManager.HasExclusiveLock(simulatedEntity.Id))
{
Log.Warn($"The server has forcibly revoked an exlusive lock - this may cause undefined behaviour. GUID: {simulatedEntity.Id}");
}

if (gameObject.HasValue)
{
EntityPositionBroadcaster.WatchEntity(id, gameObject.Value);
simulationOwnershipManager.StopSimulatingEntity(simulatedEntity.Id);
EntityPositionBroadcaster.StopWatchingEntity(simulatedEntity.Id);
}
#if DEBUG && ENTITY_LOG
else
{
Log.Error($"Expected to simulate an unknown entity: {id}");
}
#endif
}
}
}

69 changes: 39 additions & 30 deletions NitroxClient/MonoBehaviours/EntityPositionBroadcaster.cs
Original file line number Diff line number Diff line change
@@ -1,52 +1,61 @@
using System.Collections.Generic;
using System.Collections.Generic;
using NitroxClient.GameLogic;
using NitroxModel.Core;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.Util;
using UnityEngine;

namespace NitroxClient.MonoBehaviours
namespace NitroxClient.MonoBehaviours;

public class EntityPositionBroadcaster : MonoBehaviour
{
public class EntityPositionBroadcaster : MonoBehaviour
{
public static readonly float BROADCAST_INTERVAL = 0.25f;
public static readonly float BROADCAST_INTERVAL = 0.25f;

private static Dictionary<NitroxId, GameObject> watchingEntitiesById = new Dictionary<NitroxId, GameObject>();
private Entities entityBroadcaster;
private static HashSet<NitroxId> watchingEntityIds = new();
private Entities entityBroadcaster;

private float time;
private float time;

public void Awake()
{
entityBroadcaster = NitroxServiceLocator.LocateService<Entities>();
}
public void Awake()
{
entityBroadcaster = NitroxServiceLocator.LocateService<Entities>();
}

public void Update()
{
time += Time.deltaTime;

public void Update()
// Only do on a specific cadence to avoid hammering server
if (time >= BROADCAST_INTERVAL)
{
time += Time.deltaTime;
time = 0;

// Only do on a specific cadence to avoid hammering server
if (time >= BROADCAST_INTERVAL)
if (watchingEntityIds.Count > 0)
{
time = 0;

if (watchingEntitiesById.Count > 0)
{
entityBroadcaster.BroadcastTransforms(watchingEntitiesById);
}
Dictionary<NitroxId, GameObject> gameObjectsById = NitroxEntity.GetObjectsFrom(watchingEntityIds);
entityBroadcaster.BroadcastTransforms(gameObjectsById);
}
}
}

public static void WatchEntity(NitroxId id, GameObject gameObject)
{
watchingEntitiesById[id] = gameObject;
public static void WatchEntity(NitroxId id)
{
watchingEntityIds.Add(id);

RemotelyControlled remotelyControlled = gameObject.GetComponent<RemotelyControlled>();
Object.Destroy(remotelyControlled);
}
// The game object may not exist at this very moment (due to being spawned in async). This is OK as we will
// automatically start sending updates when we finally get it in the world. This behavior will also allow us
// to resync or respawn entities while still have broadcasting enabled without doing anything extra.
Optional<GameObject> gameObject = NitroxEntity.GetObjectFrom(id);

public static void StopWatchingEntity(NitroxId id)
if (gameObject.HasValue)
{
watchingEntitiesById.Remove(id);
RemotelyControlled remotelyControlled = gameObject.Value.GetComponent<RemotelyControlled>();
Object.Destroy(remotelyControlled);
}
}

public static void StopWatchingEntity(NitroxId id)
{
watchingEntityIds.Remove(id);
}
}
12 changes: 11 additions & 1 deletion NitroxClient/MonoBehaviours/NitroxEntity.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using NitroxModel.DataStructures;
using NitroxModel.DataStructures.Util;
using NitroxModel.Helper;
using ProtoBuf;
using UnityEngine;

namespace NitroxClient.MonoBehaviours
{
[Serializable]
[DataContract]
[ProtoContract] // REQUIRED as the game serializes/deserializes phasing entities in batches when moving around the map.
public class NitroxEntity : MonoBehaviour, IProtoTreeEventListener
{
private static Dictionary<NitroxId, GameObject> gameObjectsById = new Dictionary<NitroxId, GameObject>();
Expand Down Expand Up @@ -49,6 +52,13 @@ public static Optional<GameObject> GetObjectFrom(NitroxId id)
return Optional.OfNullable(gameObject);
}

public static Dictionary<NitroxId, GameObject> GetObjectsFrom(HashSet<NitroxId> ids)
{
return ids.Select(id => new KeyValuePair<NitroxId, GameObject>(id, gameObjectsById.GetOrDefault(id, null)))
.Where(keyValue => keyValue.Value != null)
.ToDictionary(kv => kv.Key, kv => kv.Value);
}

public static bool TryGetObjectFrom(NitroxId id, out GameObject gameObject)
{
gameObject = null;
Expand Down
24 changes: 0 additions & 24 deletions NitroxPatcher/Patches/Dynamic/EntityCell_QueueForSleep_Patch.cs

This file was deleted.

54 changes: 54 additions & 0 deletions NitroxPatcher/Patches/Dynamic/EntityCell_SleepAsync_Patch.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using HarmonyLib;
using NitroxClient.GameLogic;
using NitroxModel.Helper;

namespace NitroxPatcher.Patches.Dynamic;

/// <summary>
/// Entity cells will go sleep when the player gets out of range. This needs to be reported to the server so they can lose simulation locks.
/// </summary>
public class EntityCell_SleepAsync_Patch : NitroxPatch, IDynamicPatch
{
public static readonly MethodInfo TARGET_METHOD_ORIGINAL = Reflect.Method((EntityCell t) => t.SleepAsync(default(ProtobufSerializer)));
public static readonly MethodInfo TARGET_METHOD = AccessTools.EnumeratorMoveNext(TARGET_METHOD_ORIGINAL);

public static readonly OpCode INJECTION_OPCODE = OpCodes.Stfld;
public static readonly object INJECTION_OPERAND = Reflect.Field((EntityCell entityCell) => entityCell.state);

public static int INJECTION_POSITION = 2;

public static IEnumerable<CodeInstruction> Transpiler(MethodBase original, IEnumerable<CodeInstruction> instructions)
{
int validSpotsSeen = 0;

foreach (CodeInstruction instruction in instructions)
{
yield return instruction;

bool validInjectionInstruction = instruction.opcode.Equals(INJECTION_OPCODE) && instruction.operand.Equals(INJECTION_OPERAND);

if (validInjectionInstruction && ++validSpotsSeen == INJECTION_POSITION)
{
/*
* Injects: Callback(this);
*/
yield return TranspilerHelper.Ldloc<EntityCell>(original);
yield return new CodeInstruction(OpCodes.Call, Reflect.Method(() => Callback(default(EntityCell))));
}
}
}

public static void Callback(EntityCell entityCell)
{
Resolve<Terrain>().CellUnloaded(entityCell.BatchId, entityCell.CellId, entityCell.Level);
}

public override void Patch(Harmony harmony)
{
PatchTranspiler(harmony, TARGET_METHOD);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ private static void ReceivedSimulationLockResponse(NitroxId id, bool lockAquired
{
if (lockAquired)
{
EntityPositionBroadcaster.WatchEntity(id, context.GrabbedObject);
EntityPositionBroadcaster.WatchEntity(id);

skipPrefixPatch = true;
context.Cannon.GrabObject(context.GrabbedObject);
Expand Down
3 changes: 3 additions & 0 deletions NitroxServer/GameLogic/PlayerManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,9 @@ public Player PlayerConnected(NitroxConnection connection, string reservationKey
player.PlayerContext = playerContext;
player.Connection = connection;

// reconnecting players need to have their cell visibility refreshed
player.ClearVisibleCells();

assetPackage.Player = player;
assetPackage.ReservationKey = null;
reservations.Remove(reservationKey);
Expand Down
5 changes: 5 additions & 0 deletions NitroxServer/Player.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ public bool HasCellLoaded(AbsoluteEntityCell cell)
return visibleCells.Contains(cell);
}

public void ClearVisibleCells()
{
visibleCells.Clear();
}

public void AddModule(EquippedItemData module)
{
modules.Add(module);
Expand Down

0 comments on commit 89957b8

Please sign in to comment.