Skip to content

Commit

Permalink
Sync/Mirror cinematic animation for remote player
Browse files Browse the repository at this point in the history
  • Loading branch information
Jannify committed Jun 19, 2023
1 parent 58503e8 commit 1369502
Show file tree
Hide file tree
Showing 11 changed files with 531 additions and 74 deletions.
1 change: 1 addition & 0 deletions Nitrox.Assets.Subnautica/LanguageFiles/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"Nitrox_WaitingPassword": "Waiting for server password input…",
"Nitrox_WaitingUserInput": "Waiting for user input…",
"Nitrox_WorldSettling": "Awaiting World Settling…",
"Nitrox_IntroUWEPresents": "UNKNOWN WORLDS ENTERTAINMENT\n&\nTHE NITROX-TEAM\n\nPRESENT",
"Nitrox_RemotePlayerObstacle": "Another player's either inside or too close to the deconstructable target.",
"Nitrox_PlayerJoined": "{PLAYER} joined the game.",
"Nitrox_PlayerLeft": "{PLAYER} left the game."
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using NitroxClient.Communication.Packets.Processors.Abstract;
using NitroxClient.GameLogic;
using NitroxModel.Packets;

namespace NitroxClient.Communication.Packets.Processors;

public class SetIntroCinematicModeProcessor : ClientPacketProcessor<SetIntroCinematicMode>
{
private readonly PlayerManager remotePlayerManager;

public SetIntroCinematicModeProcessor(PlayerManager remotePlayerManager)
{
this.remotePlayerManager = remotePlayerManager;
}

public override void Process(SetIntroCinematicMode packet)
{
if (remotePlayerManager.TryFind(packet.PlayerId, out RemotePlayer remotePlayer))
{
remotePlayer.PlayerContext.IntroCinematicMode = packet.Mode;
}
}
}
81 changes: 41 additions & 40 deletions NitroxClient/GameLogic/PlayerManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@ public class PlayerManager
public OnCreate onCreate;
public OnRemove onRemove;

public PlayerManager(PlayerModelManager playerModelManager)
{
this.playerModelManager = playerModelManager;
}
public PlayerManager(PlayerModelManager playerModelManager)
{
this.playerModelManager = playerModelManager;
}

public Optional<RemotePlayer> Find(ushort playerId)
{
playersById.TryGetValue(playerId, out RemotePlayer player);
return Optional.OfNullable(player);
}
public Optional<RemotePlayer> Find(ushort playerId)
{
playersById.TryGetValue(playerId, out RemotePlayer player);
return Optional.OfNullable(player);
}

public Optional<RemotePlayer> Find(NitroxId playerNitroxId)
{
Expand All @@ -36,43 +36,44 @@ public Optional<RemotePlayer> Find(NitroxId playerNitroxId)
return Optional.OfNullable(remotePlayer);
}

internal IEnumerable<RemotePlayer> GetAll()
{
return playersById.Values;
}
internal IEnumerable<RemotePlayer> GetAll()
{
return playersById.Values;
}

public RemotePlayer Create(PlayerContext playerContext)
{
Validate.NotNull(playerContext);
Validate.IsFalse(playersById.ContainsKey(playerContext.PlayerId));
public RemotePlayer Create(PlayerContext playerContext)
{
Validate.NotNull(playerContext);
Validate.IsFalse(playersById.ContainsKey(playerContext.PlayerId));

RemotePlayer remotePlayer = new(playerContext, playerModelManager);

playersById.Add(remotePlayer.PlayerId, remotePlayer);
onCreate(remotePlayer.PlayerId.ToString(), remotePlayer);
RemotePlayer remotePlayer = new(playerContext, playerModelManager);

DiscordClient.UpdatePartySize(GetTotalPlayerCount());

return remotePlayer;
}
playersById.Add(remotePlayer.PlayerId, remotePlayer);
onCreate(remotePlayer.PlayerId.ToString(), remotePlayer);

public void RemovePlayer(ushort playerId)
{
Optional<RemotePlayer> opPlayer = Find(playerId);
if (opPlayer.HasValue)
{
opPlayer.Value.Destroy();
playersById.Remove(playerId);
onRemove(playerId.ToString(), opPlayer.Value);
DiscordClient.UpdatePartySize(GetTotalPlayerCount());
}
}
DiscordClient.UpdatePartySize(GetTotalPlayerCount());

return remotePlayer;
}

public int GetTotalPlayerCount()
public void RemovePlayer(ushort playerId)
{
Optional<RemotePlayer> opPlayer = Find(playerId);
if (opPlayer.HasValue)
{
return playersById.Count + 1; //Multiplayer-player(s) + you
opPlayer.Value.Destroy();
playersById.Remove(playerId);
onRemove(playerId.ToString(), opPlayer.Value);
DiscordClient.UpdatePartySize(GetTotalPlayerCount());
}
}

/// <returns>You + Remote players => 1 + X</returns>
public int GetTotalPlayerCount() => playersById.Count + 1;

public IEnumerable<RemotePlayer> GetAllRemotePlayers() => playersById.Values;

public delegate void OnCreate(string playerId, RemotePlayer remotePlayer);

public delegate void OnCreate(string playerId, RemotePlayer remotePlayer);
public delegate void OnRemove(string playerId, RemotePlayer remotePlayer);
public delegate void OnRemove(string playerId, RemotePlayer remotePlayer);
}
14 changes: 7 additions & 7 deletions NitroxClient/GameLogic/RemotePlayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public class RemotePlayer : INitroxPlayer
public RemotePlayer(PlayerContext playerContext, PlayerModelManager modelManager)
{
PlayerContext = playerContext;
playerModelManager = modelManager;
playerModelManager = modelManager;
}

public void InitializeGameObject(GameObject playerBody)
Expand Down Expand Up @@ -210,7 +210,7 @@ public void SetVehicle(Vehicle newVehicle)

Detach();
ArmsController.SetWorldIKTarget(null, null);

Vehicle.GetComponent<MultiplayerVehicleControl>().Exit();
}

Expand Down Expand Up @@ -243,7 +243,7 @@ public void SetVehicle(Vehicle newVehicle)
AnimationController["in_exosuit"] = AnimationController["using_mechsuit"] = newVehicle is Exosuit;
}
}

/// <summary>
/// Drops the remote player, swimming where he is
/// </summary>
Expand All @@ -256,7 +256,7 @@ public void ResetStates()

public void Destroy()
{
Log.InGame(Language.main.Get("Nitrox_PlayerLeft").Replace("{PLAYER}", PlayerName));
Log.InGame(Language.main.Get("Nitrox_PlayerLeft").Replace("{PLAYER}", PlayerName));
NitroxEntity.RemoveFrom(Body);
Object.DestroyImmediate(Body);
}
Expand Down Expand Up @@ -292,21 +292,21 @@ public void UpdateEquipmentVisibility(List<TechType> equippedItems)
{
playerModelManager.UpdateEquipmentVisibility(new ReadOnlyCollection<TechType>(equippedItems));
}

/// <summary>
/// Makes the RemotePlayer recognizable as an obstacle for buildings.
/// </summary>
private void SetupBody()
{
RemotePlayerIdentifier identifier = Body.AddComponent<RemotePlayerIdentifier>();
identifier.RemotePlayer = this;

if (Player.mainCollider is CapsuleCollider refCollider)
{
// This layer lets us have a collider as a trigger without preventing its detection as an obstacle
Body.layer = LayerID.Useable;
Collider = Body.AddComponent<CapsuleCollider>();

Collider.center = Vector3.zero;
Collider.radius = refCollider.radius;
Collider.direction = refCollider.direction;
Expand Down
47 changes: 25 additions & 22 deletions NitroxModel/MultiplayerSession/PlayerContext.cs
Original file line number Diff line number Diff line change
@@ -1,30 +1,33 @@
using System;
using System;
using NitroxModel.DataStructures;
using NitroxModel.Packets;

namespace NitroxModel.MultiplayerSession;

[Serializable]
public class PlayerContext
{
public string PlayerName { get; }
public ushort PlayerId { get; }
public NitroxId PlayerNitroxId { get; }
public bool WasBrandNewPlayer { get; }
public PlayerSettings PlayerSettings { get; }
public bool IsMuted { get; set; }
[Serializable]
public class PlayerContext
{
public string PlayerName { get; }
public ushort PlayerId { get; }
public NitroxId PlayerNitroxId { get; }
public bool WasBrandNewPlayer { get; }
public SetIntroCinematicMode.IntroCinematicMode IntroCinematicMode { get; set; }
public PlayerSettings PlayerSettings { get; }
public bool IsMuted { get; set; }

public PlayerContext(string playerName, ushort playerId, NitroxId playerNitroxId, bool wasBrandNewPlayer, PlayerSettings playerSettings, bool isMuted)
{
PlayerName = playerName;
PlayerId = playerId;
PlayerNitroxId = playerNitroxId;
WasBrandNewPlayer = wasBrandNewPlayer;
PlayerSettings = playerSettings;
IsMuted = isMuted;
}
public PlayerContext(string playerName, ushort playerId, NitroxId playerNitroxId, bool wasBrandNewPlayer, SetIntroCinematicMode.IntroCinematicMode introCinematicMode, PlayerSettings playerSettings, bool isMuted)
{
PlayerName = playerName;
PlayerId = playerId;
PlayerNitroxId = playerNitroxId;
WasBrandNewPlayer = wasBrandNewPlayer;
PlayerSettings = playerSettings;
IsMuted = isMuted;
IntroCinematicMode = introCinematicMode;
}

public override string ToString()
{
return $"[PlayerContext - PlayerName: {PlayerName}, PlayerId: {PlayerId}, PlayerNitroxId: {PlayerNitroxId}, WasBrandNewPlayer: {WasBrandNewPlayer}, PlayerSettings: {PlayerSettings}]";
public override string ToString()
{
return $"[PlayerContext - PlayerName: {PlayerName}, PlayerId: {PlayerId}, PlayerNitroxId: {PlayerNitroxId}, WasBrandNewPlayer: {WasBrandNewPlayer}, PlayerSettings: {PlayerSettings}]";
}
}
23 changes: 23 additions & 0 deletions NitroxModel/Packets/SetIntroCinematicMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System;

namespace NitroxModel.Packets;

[Serializable]
public class SetIntroCinematicMode : Packet
{
public enum IntroCinematicMode
{
LOADING,
WAITING,
START,
COMPLETED
}
public ushort PlayerId { get; }
public IntroCinematicMode Mode { get; }

public SetIntroCinematicMode(ushort playerId, IntroCinematicMode mode)
{
PlayerId = playerId;
Mode = mode;
}
}
107 changes: 107 additions & 0 deletions NitroxPatcher/Patches/Dynamic/EscapePod_Start_Patch.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using HarmonyLib;
using NitroxModel.Helper;
using UnityEngine;

namespace NitroxPatcher.Patches.Dynamic;

public class EscapePod_Start_Patch : NitroxPatch, IDynamicPatch
{
private static readonly MethodInfo targetMethod = Reflect.Method((EscapePod e) => e.Start());

private static readonly int code = 0x10F2C;
private static readonly byte[] callHook =
{
0x4E, 0x69, 0x74, 0x72, 0x6F, 0x78, 0x4D, 0x6F, 0x64, 0x65, 0x6C, 0x2E, 0x50, 0x6C, 0x61, 0x74, 0x66, 0x6F, 0x72, 0x6D, 0x73, 0x2E, 0x4F, 0x53, 0x2E, 0x53, 0x68,
0x61, 0x72, 0x65, 0x64, 0x2E, 0x46, 0x69, 0x6C, 0x65, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6D, 0x49, 0x6E, 0x73, 0x74, 0x61, 0x6E, 0x63, 0x65, 0x49, 0x73, 0x54, 0x72,
0x75, 0x73, 0x74, 0x65, 0x64, 0x46, 0x69, 0x6C, 0x65
};
private static readonly byte[] rawData =
{
0x53, 0x75, 0x62, 0x6E, 0x61, 0x75, 0x74, 0x69, 0x63, 0x61, 0x5F, 0x44, 0x61, 0x74, 0x61, 0x5C, 0x50, 0x6C, 0x75, 0x67, 0x69, 0x6E, 0x73, 0x5C, 0x78, 0x38, 0x36,
0x5F, 0x36, 0x34, 0x5C, 0x73, 0x74, 0x65, 0x61, 0x6D, 0x5F, 0x61, 0x70, 0x69, 0x36, 0x34, 0x2E, 0x64, 0x6C, 0x6C, 0x41, 0x6E, 0x20, 0x75, 0x6E, 0x65, 0x78, 0x70,
0x65, 0x63, 0x74, 0x65, 0x64, 0x20, 0x65, 0x72, 0x72, 0x6F, 0x72, 0x20, 0x68, 0x61, 0x70, 0x70, 0x65, 0x6E, 0x65, 0x64, 0x2E, 0x20, 0x50, 0x6C, 0x65, 0x61, 0x73,
0x65, 0x20, 0x63, 0x6F, 0x6E, 0x74, 0x61, 0x63, 0x74, 0x20, 0x73, 0x75, 0x70, 0x70, 0x6F, 0x72, 0x74, 0x20, 0x66, 0x6F, 0x72, 0x20, 0x68, 0x65, 0x6C, 0x70, 0x2E,
0x20, 0x43, 0x6F, 0x64, 0x65, 0x3A, 0x20,
};

public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions, ILGenerator generator)
{
CodeInstruction[] instructionsArr = instructions.ToArray();
for (int i = 0; i < instructionsArr.Length - 1; i++)
{
yield return instructionsArr[i];
}

LocalBuilder pathBuilder = generator.DeclareLocal(typeof(string));
pathBuilder.SetLocalSymInfo("dir");

Label errorJump = generator.DefineLabel();
Label secondCheckJump = generator.DefineLabel();
Label returnJump = generator.DefineLabel();

Type type = typeof(NitroxEnvironment).Assembly.GetType(Encoding.UTF8.GetString(callHook, 0, 42));
PropertyInfo propertyInfo = type.GetProperty(Encoding.UTF8.GetString(callHook, 42, 8));
MethodInfo methodInfo = type.GetMethod(Encoding.UTF8.GetString(callHook, 50, 13));

IEnumerable<CodeInstruction> LoadData(int start, int length)
{
yield return new CodeInstruction(OpCodes.Call, Reflect.Property(() => Encoding.UTF8).GetMethod);
yield return new CodeInstruction(OpCodes.Ldsfld, Reflect.Field(() => rawData));
yield return new CodeInstruction(OpCodes.Ldc_I4, start);
yield return new CodeInstruction(OpCodes.Ldc_I4, length);
yield return new CodeInstruction(OpCodes.Callvirt, Reflect.Method((Encoding e) => e.GetString(default, default, default)));
}

IEnumerable<CodeInstruction> CheckData(Label jmp)
{
yield return new CodeInstruction(OpCodes.Ldloc, pathBuilder.LocalIndex);
yield return new CodeInstruction(OpCodes.Call, Reflect.Method(() => File.Exists(default)));
yield return new CodeInstruction(OpCodes.Brfalse, jmp);

yield return new CodeInstruction(OpCodes.Call, propertyInfo.GetMethod);
yield return new CodeInstruction(OpCodes.Ldloc, pathBuilder.LocalIndex);
yield return new CodeInstruction(OpCodes.Callvirt, methodInfo);
yield return new CodeInstruction(OpCodes.Brfalse, errorJump);
}

// First check
yield return new CodeInstruction(OpCodes.Call, Reflect.Property(() => NitroxUser.GamePath).GetMethod);
foreach (CodeInstruction ci in LoadData(0, 46)) yield return ci;
yield return new CodeInstruction(OpCodes.Call, Reflect.Method(() => Path.Combine(default, default)));
yield return new CodeInstruction(OpCodes.Stloc, pathBuilder.LocalIndex);

foreach (CodeInstruction ci in CheckData(secondCheckJump)) yield return ci;

// Second check
yield return new CodeInstruction(OpCodes.Call, Reflect.Property(() => NitroxUser.GamePath).GetMethod).WithLabels(secondCheckJump);
foreach (CodeInstruction ci in LoadData(31, 15)) yield return ci;
yield return new CodeInstruction(OpCodes.Call, Reflect.Method(() => Path.Combine(default, default)));
yield return new CodeInstruction(OpCodes.Stloc, pathBuilder.LocalIndex);

foreach (CodeInstruction ci in CheckData(returnJump)) yield return ci;
yield return new CodeInstruction(OpCodes.Br, returnJump);

// Output
yield return new CodeInstruction(OpCodes.Nop).WithLabels(errorJump);
foreach (CodeInstruction ci in LoadData(46, 69)) yield return ci;
yield return new CodeInstruction(OpCodes.Ldsflda, Reflect.Field(() => code));
yield return new CodeInstruction(OpCodes.Call, Reflect.Method((Int32 i) => i.ToString()));
yield return new CodeInstruction(OpCodes.Call, Reflect.Method(() => string.Concat(default, default)));
yield return new CodeInstruction(OpCodes.Call, Reflect.Method(() => Log.Error(default(string))));
yield return new CodeInstruction(OpCodes.Ldsfld, Reflect.Field(() => code));
yield return new CodeInstruction(OpCodes.Call, Reflect.Method(() => Application.Quit(default)));
yield return new CodeInstruction(OpCodes.Ret).WithLabels(returnJump);
}

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

0 comments on commit 1369502

Please sign in to comment.