first commit
This commit is contained in:
160
NitroxClient/GameLogic/AI.cs
Normal file
160
NitroxClient/GameLogic/AI.cs
Normal file
@@ -0,0 +1,160 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxClient.MonoBehaviours;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.Packets;
|
||||
using UnityEngine;
|
||||
using static NitroxModel.Packets.RangedAttackLastTargetUpdate;
|
||||
|
||||
namespace NitroxClient.GameLogic;
|
||||
|
||||
public class AI
|
||||
{
|
||||
private readonly IPacketSender packetSender;
|
||||
private readonly Dictionary<Creature, CreatureAction> actions = [];
|
||||
private readonly Dictionary<string, Type> cachedCreatureActionTypeByFullName;
|
||||
|
||||
/// <summary>
|
||||
/// Contains the types derived from CreatureAction which should be synced.
|
||||
/// Actions concerning the creature movement should be ignored as it's already done through SplineFollowing
|
||||
/// </summary>
|
||||
private readonly HashSet<Type> creatureActionWhitelist =
|
||||
[
|
||||
typeof(AttackLastTarget), typeof(RangedAttackLastTarget), typeof(AttackCyclops), typeof(Poop)
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// In the future, ensure all creatures are synced. We want each of them to be individually
|
||||
/// checked (that all their actions are synced) before marking them as synced.
|
||||
/// </summary>
|
||||
private readonly HashSet<Type> syncedCreatureWhitelist =
|
||||
[
|
||||
typeof(ReaperLeviathan), typeof(SeaDragon), typeof(SeaTreader), typeof(GhostLeviathan)
|
||||
];
|
||||
|
||||
public AI(IPacketSender packetSender)
|
||||
{
|
||||
this.packetSender = packetSender;
|
||||
Assembly assembly = Assembly.GetAssembly(typeof(CreatureAction));
|
||||
cachedCreatureActionTypeByFullName = assembly.GetTypes()
|
||||
.Where(type => typeof(CreatureAction).IsAssignableFrom(type))
|
||||
.ToDictionary(t => t.FullName, t => t);
|
||||
}
|
||||
|
||||
public void BroadcastNewAction(NitroxId creatureId, Creature creature, CreatureAction newAction)
|
||||
{
|
||||
if (!syncedCreatureWhitelist.Contains(creature.GetType()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
packetSender.Send(new CreatureActionChanged(creatureId, newAction.GetType().FullName));
|
||||
}
|
||||
|
||||
public void CreatureActionChanged(NitroxId id, string creatureActionTypeName)
|
||||
{
|
||||
if (!NitroxEntity.TryGetComponentFrom(id, out Creature creature))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (cachedCreatureActionTypeByFullName.TryGetValue(creatureActionTypeName, out Type creatureActionType) &&
|
||||
creature.TryGetComponent(creatureActionType, out Component component) && component is CreatureAction creatureAction)
|
||||
{
|
||||
actions[creature] = creatureAction;
|
||||
}
|
||||
}
|
||||
|
||||
public static void AggressiveWhenSeeTargetChanged(NitroxId creatureId, NitroxId targetId, bool locked, float aggressionAmount)
|
||||
{
|
||||
if (!NitroxEntity.TryGetComponentFrom(creatureId, out AggressiveWhenSeeTarget aggressiveWhenSeeTarget) ||
|
||||
!NitroxEntity.TryGetObjectFrom(targetId, out GameObject targetObject))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Creature creature = aggressiveWhenSeeTarget.creature;
|
||||
|
||||
// Code from AggressiveWhenSeeTarget.ScanForAggressionTarget
|
||||
creature.Aggression.Value = aggressionAmount;
|
||||
LastTarget lastTarget = aggressiveWhenSeeTarget.lastTarget;
|
||||
lastTarget.SetTargetInternal(targetObject);
|
||||
lastTarget.targetLocked = locked;
|
||||
|
||||
if (aggressiveWhenSeeTarget.sightedSound && !aggressiveWhenSeeTarget.sightedSound.GetIsPlaying())
|
||||
{
|
||||
// This call doesn't broadcast a sound packet
|
||||
aggressiveWhenSeeTarget.sightedSound.StartEvent();
|
||||
}
|
||||
|
||||
if (creature.TryGetComponent(out AttackLastTarget attackLastTarget))
|
||||
{
|
||||
attackLastTarget.currentTarget = targetObject;
|
||||
}
|
||||
}
|
||||
|
||||
public static void AttackCyclopsTargetChanged(NitroxId creatureId, NitroxId targetId, float aggressiveToNoiseAmount)
|
||||
{
|
||||
if (!NitroxEntity.TryGetComponentFrom(creatureId, out AttackCyclops attackCyclops) ||
|
||||
!NitroxEntity.TryGetObjectFrom(targetId, out GameObject targetObject))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Kinda stuff from AttackCyclops.UpdateAggression
|
||||
attackCyclops.aggressiveToNoise.Value = aggressiveToNoiseAmount;
|
||||
// Force currentTarget to null to ensure SetCurrentTarget detects a change
|
||||
attackCyclops.currentTarget = null;
|
||||
attackCyclops.SetCurrentTarget(targetObject, targetObject.GetComponent<CyclopsDecoy>());
|
||||
}
|
||||
|
||||
public static void RangedAttackLastTargetUpdate(NitroxId creatureId, NitroxId targetId, int attackTypeIndex, ActionState state)
|
||||
{
|
||||
if (!NitroxEntity.TryGetComponentFrom(creatureId, out RangedAttackLastTarget rangedAttackLastTarget) ||
|
||||
!NitroxEntity.TryGetObjectFrom(targetId, out GameObject targetObject))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RangedAttackLastTarget.RangedAttackType attackType = rangedAttackLastTarget.attackTypes[attackTypeIndex];
|
||||
rangedAttackLastTarget.currentAttack = attackType;
|
||||
rangedAttackLastTarget.currentTarget = targetObject;
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case ActionState.CHARGING:
|
||||
rangedAttackLastTarget.StartCharging(attackType);
|
||||
break;
|
||||
case ActionState.CASTING:
|
||||
rangedAttackLastTarget.StartCasting(attackType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static void CreaturePoopPerformed(NitroxId creatureId)
|
||||
{
|
||||
if (NitroxEntity.TryGetComponentFrom(creatureId, out Poop poop))
|
||||
{
|
||||
// Code from Poop.Perform
|
||||
SafeAnimator.SetBool(poop.creature.GetAnimator(), poop.animationParameterName, false);
|
||||
poop.recourceSpawned = true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetActionForCreature(Creature creature, out CreatureAction action)
|
||||
{
|
||||
return actions.TryGetValue(creature, out action);
|
||||
}
|
||||
|
||||
public bool IsCreatureActionWhitelisted(CreatureAction creatureAction)
|
||||
{
|
||||
return creatureActionWhitelist.Contains(creatureAction.GetType());
|
||||
}
|
||||
|
||||
public bool IsCreatureWhitelisted(Creature creature)
|
||||
{
|
||||
return syncedCreatureWhitelist.Contains(creature.GetType());
|
||||
}
|
||||
}
|
316
NitroxClient/GameLogic/Bases/BuildUtils.cs
Normal file
316
NitroxClient/GameLogic/Bases/BuildUtils.cs
Normal file
@@ -0,0 +1,316 @@
|
||||
using System.Collections.Generic;
|
||||
using NitroxClient.GameLogic.Spawning.Bases;
|
||||
using NitroxClient.GameLogic.Spawning.Metadata;
|
||||
using NitroxClient.MonoBehaviours;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.DataStructures.GameLogic;
|
||||
using NitroxModel.DataStructures.GameLogic.Bases;
|
||||
using NitroxModel.DataStructures.GameLogic.Entities;
|
||||
using NitroxModel.DataStructures.GameLogic.Entities.Bases;
|
||||
using NitroxModel.DataStructures.Util;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.Bases;
|
||||
|
||||
public static class BuildUtils
|
||||
{
|
||||
public static bool TryGetIdentifier(BaseDeconstructable baseDeconstructable, out BuildPieceIdentifier identifier, BaseCell baseCell = null, Base.Face? baseFace = null)
|
||||
{
|
||||
// It is unimaginable to have a BaseDeconstructable that is not child of a BaseCell
|
||||
if (!baseCell && !baseDeconstructable.TryGetComponentInParent(out baseCell, true))
|
||||
{
|
||||
identifier = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
identifier = GetIdentifier(baseDeconstructable, baseCell, baseFace);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static BuildPieceIdentifier GetIdentifier(BaseDeconstructable baseDeconstructable, BaseCell baseCell, Base.Face? baseFace = null)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Recipe = baseDeconstructable.recipe.ToDto(),
|
||||
BaseFace = baseFace?.ToDto() ?? baseDeconstructable.face?.ToDto(),
|
||||
BaseCell = baseCell.cell.ToDto(),
|
||||
PiecePoint = baseDeconstructable.deconstructedBase.WorldToGrid(baseDeconstructable.transform.position).ToDto()
|
||||
};
|
||||
}
|
||||
|
||||
public static bool TryGetGhostFace(BaseGhost baseGhost, out Base.Face face)
|
||||
{
|
||||
// Copied code from BaseAddModuleGhost.Finish() and BaseAddFaceGhost.Finish() to obtain the face at which the module was spawned
|
||||
switch (baseGhost)
|
||||
{
|
||||
case BaseAddModuleGhost moduleGhost:
|
||||
face = moduleGhost.anchoredFace.Value;
|
||||
face.cell += baseGhost.targetBase.GetAnchor();
|
||||
return true;
|
||||
case BaseAddFaceGhost faceGhost:
|
||||
if (faceGhost.anchoredFace.HasValue)
|
||||
{
|
||||
face = faceGhost.anchoredFace.Value;
|
||||
face.cell += faceGhost.targetBase.GetAnchor();
|
||||
return true;
|
||||
}
|
||||
if (BaseAddFaceGhost.FindFirstMaskedFace(faceGhost.ghostBase, out face))
|
||||
{
|
||||
Vector3 point = faceGhost.ghostBase.GridToWorld(Int3.zero);
|
||||
faceGhost.targetOffset = faceGhost.targetBase.WorldToGrid(point);
|
||||
face.cell += faceGhost.targetOffset;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case BaseAddWaterPark waterPark:
|
||||
if (waterPark.anchoredFace.HasValue)
|
||||
{
|
||||
face = waterPark.anchoredFace.Value;
|
||||
face.cell += waterPark.targetBase.GetAnchor();
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case BaseAddMapRoomGhost:
|
||||
face = new(GetMapRoomFunctionalityCell(baseGhost), 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
face = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Even if the corresponding module was found, in some cases (with WaterParks notably) we don't want to transfer the id.
|
||||
/// We then return false because the GameObject may have already been marked.
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// Whether or not the id was successfully transferred
|
||||
/// </returns>
|
||||
public static bool TryTransferIdFromGhostToModule(BaseGhost baseGhost, NitroxId id, ConstructableBase constructableBase, out GameObject moduleObject)
|
||||
{
|
||||
// 1. Find the face of the target piece
|
||||
Base.Face? face = null;
|
||||
bool isWaterPark = baseGhost is BaseAddWaterPark;
|
||||
bool isMapRoomGhost = baseGhost is BaseAddMapRoomGhost;
|
||||
// Only four types of ghost which spawn a module
|
||||
if (baseGhost is BaseAddFaceGhost faceGhost && faceGhost.modulePrefab ||
|
||||
baseGhost is BaseAddModuleGhost moduleGhost && moduleGhost.modulePrefab ||
|
||||
isMapRoomGhost ||
|
||||
isWaterPark)
|
||||
{
|
||||
if (TryGetGhostFace(baseGhost, out Base.Face ghostFace))
|
||||
{
|
||||
face = ghostFace;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"Couldn't find the module spawned by {baseGhost}");
|
||||
moduleObject = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// If the ghost is under a BaseDeconstructable(Clone), it may have an associated module
|
||||
else if (IsBaseDeconstructable(constructableBase))
|
||||
{
|
||||
face = new(constructableBase.moduleFace.Value.cell + baseGhost.targetBase.GetAnchor(), constructableBase.moduleFace.Value.direction);
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (constructableBase.techType)
|
||||
{
|
||||
case TechType.BaseWaterPark:
|
||||
// Edge case that happens when a Deconstructed WaterPark is built onto another deconstructed WaterPark that has its module
|
||||
// A new module will be created by the current Deconstructed WaterPark which is the one we'll be aiming at
|
||||
IBaseModuleGeometry baseModuleGeometry = constructableBase.GetComponentInChildren<IBaseModuleGeometry>(true);
|
||||
if (baseModuleGeometry != null)
|
||||
{
|
||||
face = baseModuleGeometry.geometryFace;
|
||||
}
|
||||
break;
|
||||
|
||||
case TechType.BaseMoonpool:
|
||||
// Moonpools are a very specific case, we tweak them to work as interior pieces (while they're not)
|
||||
Optional<GameObject> objectOptional = baseGhost.targetBase.gameObject.EnsureComponent<MoonpoolManager>().RegisterMoonpool(constructableBase.transform, id);
|
||||
moduleObject = objectOptional.Value;
|
||||
return moduleObject;
|
||||
|
||||
case TechType.BaseMapRoom:
|
||||
// In the case the ghost is under a BaseDeconstructable, this is a good way to identify a MapRoom
|
||||
face = new(GetMapRoomFunctionalityCell(baseGhost), 0);
|
||||
isMapRoomGhost = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
moduleObject = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!face.HasValue)
|
||||
{
|
||||
if (constructableBase.techType != TechType.BaseWaterPark)
|
||||
{
|
||||
Log.Error($"No face could be found for ghost {baseGhost}");
|
||||
}
|
||||
moduleObject = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. Use that face to find the newly created piece and set its id to the desired one
|
||||
if (isMapRoomGhost)
|
||||
{
|
||||
MapRoomFunctionality mapRoomFunctionality = baseGhost.targetBase.GetMapRoomFunctionalityForCell(face.Value.cell);
|
||||
if (mapRoomFunctionality)
|
||||
{
|
||||
// As MapRooms can be built as the first piece of a base, we need to make sure that they receive a new id if they're not in a base
|
||||
if (constructableBase.GetComponentInParent<Base>(true))
|
||||
{
|
||||
NitroxEntity.SetNewId(mapRoomFunctionality.gameObject, id);
|
||||
}
|
||||
else
|
||||
{
|
||||
NitroxEntity.SetNewId(mapRoomFunctionality.gameObject, id.Increment());
|
||||
}
|
||||
moduleObject = mapRoomFunctionality.gameObject;
|
||||
return true;
|
||||
}
|
||||
Log.Error($"Couldn't find MapRoomFunctionality of built MapRoom (cell: {face.Value.cell})");
|
||||
moduleObject = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
IBaseModule module = baseGhost.targetBase.GetModule(face.Value);
|
||||
if (module != null)
|
||||
{
|
||||
// If the WaterPark is higher than one, it means that the newly built WaterPark will be merged with one that already has a NitroxEntity
|
||||
if (module is WaterPark waterPark && waterPark.height > 1)
|
||||
{
|
||||
// as the WaterPark is necessarily merged, we won't need to do anything about it
|
||||
moduleObject = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
moduleObject = (module as Component).gameObject;
|
||||
NitroxEntity.SetNewId(moduleObject, id);
|
||||
return true;
|
||||
}
|
||||
// When a WaterPark is merged with another one, we won't find its module but we don't care about that
|
||||
if (!isWaterPark)
|
||||
{
|
||||
Log.Error("Couldn't find the module's GameObject of built interior piece when transferring its NitroxEntity to the module.");
|
||||
}
|
||||
|
||||
moduleObject = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// The criteria to make sure that a ConstructableBase is one of a BaseDeconstructable is if it has a moduleFace
|
||||
/// because this field is only filled for the base deconstruction (<see cref="BaseDeconstructable.Deconstruct"/>, <seealso cref="ConstructableBase.LinkModule(Base.Face?)"/>).
|
||||
/// </remarks>
|
||||
public static bool IsBaseDeconstructable(ConstructableBase constructableBase)
|
||||
{
|
||||
return constructableBase.moduleFace.HasValue;
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// A BaseDeconstructable's ghost component is a simple BaseGhost so we need to identify it by the parent ConstructableBase instead.
|
||||
/// </remarks>
|
||||
/// <param name="faceAlreadyLinked">Whether <see cref="ConstructableBase.moduleFace"/> was already set or not</param>
|
||||
public static bool IsUnderBaseDeconstructable(BaseGhost baseGhost, bool faceAlreadyLinked)
|
||||
{
|
||||
return baseGhost.TryGetComponentInParent(out ConstructableBase constructableBase, true) &&
|
||||
(IsBaseDeconstructable(constructableBase) || !faceAlreadyLinked);
|
||||
}
|
||||
|
||||
public static Int3 GetMapRoomFunctionalityCell(BaseGhost baseGhost)
|
||||
{
|
||||
// Code found from Base.GetMapRoomFunctionalityForCell
|
||||
return baseGhost.targetBase.NormalizeCell(baseGhost.targetBase.WorldToGrid(baseGhost.ghostBase.occupiedBounds.center));
|
||||
}
|
||||
|
||||
public static MapRoomEntity CreateMapRoomEntityFrom(MapRoomFunctionality mapRoomFunctionality, Base @base, NitroxId id, NitroxId parentId)
|
||||
{
|
||||
Int3 mapRoomCell = @base.NormalizeCell(@base.WorldToGrid(mapRoomFunctionality.transform.position));
|
||||
return new(id, parentId, mapRoomCell.ToDto());
|
||||
}
|
||||
|
||||
// TODO: Use this for a latter singleplayer save converter
|
||||
public static List<GlobalRootEntity> GetGlobalRootChildren(Transform globalRoot, EntityMetadataManager entityMetadataManager)
|
||||
{
|
||||
List<GlobalRootEntity> entities = new();
|
||||
foreach (Transform child in globalRoot)
|
||||
{
|
||||
if (child.TryGetComponent(out Base @base))
|
||||
{
|
||||
entities.Add(BuildEntitySpawner.From(@base, entityMetadataManager));
|
||||
}
|
||||
else if (child.TryGetComponent(out Constructable constructable))
|
||||
{
|
||||
if (constructable is ConstructableBase constructableBase)
|
||||
{
|
||||
entities.Add(GhostEntitySpawner.From(constructableBase));
|
||||
continue;
|
||||
}
|
||||
entities.Add(ModuleEntitySpawner.From(constructable));
|
||||
}
|
||||
}
|
||||
return entities;
|
||||
}
|
||||
|
||||
public static List<Entity> GetChildEntities(Base targetBase, NitroxId baseId, EntityMetadataManager entityMetadataManager)
|
||||
{
|
||||
List<Entity> childEntities = new();
|
||||
void AddChild(Entity childEntity)
|
||||
{
|
||||
// Making sure that childEntities are correctly parented
|
||||
childEntity.ParentId = baseId;
|
||||
childEntities.Add(childEntity);
|
||||
}
|
||||
|
||||
foreach (Transform transform in targetBase.transform)
|
||||
{
|
||||
if (transform.TryGetComponent(out MapRoomFunctionality mapRoomFunctionality))
|
||||
{
|
||||
if (!mapRoomFunctionality.TryGetNitroxId(out NitroxId mapRoomId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
AddChild(CreateMapRoomEntityFrom(mapRoomFunctionality, targetBase, mapRoomId, baseId));
|
||||
}
|
||||
else if (transform.TryGetComponent(out IBaseModule baseModule))
|
||||
{
|
||||
// IBaseModules without a NitroxEntity are related to BaseDeconstructable and are saved with their ghost
|
||||
if (!(baseModule as MonoBehaviour).GetComponent<NitroxEntity>())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
MonoBehaviour moduleMB = baseModule as MonoBehaviour;
|
||||
AddChild(InteriorPieceEntitySpawner.From(baseModule, entityMetadataManager));
|
||||
}
|
||||
else if (transform.TryGetComponent(out Constructable constructable))
|
||||
{
|
||||
if (constructable is ConstructableBase constructableBase)
|
||||
{
|
||||
AddChild(GhostEntitySpawner.From(constructableBase));
|
||||
continue;
|
||||
}
|
||||
AddChild(ModuleEntitySpawner.From(constructable));
|
||||
}
|
||||
}
|
||||
|
||||
if (targetBase.TryGetComponent(out MoonpoolManager nitroxMoonpool))
|
||||
{
|
||||
nitroxMoonpool.GetSavedMoonpools().ForEach(AddChild);
|
||||
}
|
||||
|
||||
return childEntities;
|
||||
}
|
||||
|
||||
public static Component AliveOrNull(this IBaseModule baseModule)
|
||||
{
|
||||
return (baseModule as Component).AliveOrNull();
|
||||
}
|
||||
}
|
410
NitroxClient/GameLogic/Bases/BuildingHandler.cs
Normal file
410
NitroxClient/GameLogic/Bases/BuildingHandler.cs
Normal file
@@ -0,0 +1,410 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NitroxClient.Communication;
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxClient.GameLogic.Spawning.Bases;
|
||||
using NitroxClient.GameLogic.Spawning.Metadata;
|
||||
using NitroxClient.MonoBehaviours;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.DataStructures.GameLogic;
|
||||
using NitroxModel.DataStructures.GameLogic.Bases;
|
||||
using NitroxModel.DataStructures.GameLogic.Entities.Bases;
|
||||
using NitroxModel.DataStructures.Util;
|
||||
using NitroxModel.Packets;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.Bases;
|
||||
|
||||
public partial class BuildingHandler : MonoBehaviour
|
||||
{
|
||||
public static BuildingHandler Main;
|
||||
|
||||
public Queue<Packet> BuildQueue;
|
||||
private bool working;
|
||||
|
||||
public Dictionary<NitroxId, DateTimeOffset> BasesCooldown;
|
||||
|
||||
/// <summary>
|
||||
/// When processing deconstruction-related packets, it's required to pass part of their data to the patches
|
||||
/// so that they can work accordingly (mainly to differentiate local actions from remotely issued ones).
|
||||
/// </summary>
|
||||
public TemporaryBuildData Temp;
|
||||
|
||||
/// <summary>
|
||||
/// TimeSpan before which local player can build on a base that was modified by another player
|
||||
/// </summary>
|
||||
private static readonly TimeSpan MultiplayerBuildCooldown = TimeSpan.FromSeconds(2);
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (Main)
|
||||
{
|
||||
Log.Error($"Another instance of {nameof(BuildingHandler)} is already running. Deleting the current one.");
|
||||
Destroy(this);
|
||||
return;
|
||||
}
|
||||
Main = this;
|
||||
BuildQueue = new();
|
||||
LatestResyncRequestTimeOffset = DateTimeOffset.UtcNow;
|
||||
BasesCooldown = new();
|
||||
Temp = new();
|
||||
Operations = new();
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
CleanCooldowns();
|
||||
if (BuildQueue.Count > 0 && !working && !Resyncing)
|
||||
{
|
||||
working = true;
|
||||
StartCoroutine(SafelyTreatNextBuildCommand());
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator SafelyTreatNextBuildCommand()
|
||||
{
|
||||
Packet packet = BuildQueue.Dequeue();
|
||||
yield return TreatBuildCommand(packet).OnYieldError(exception => Log.Error(exception, $"An error happened when processing build command {packet}"));
|
||||
working = false;
|
||||
}
|
||||
|
||||
private IEnumerator TreatBuildCommand(Packet buildCommand)
|
||||
{
|
||||
switch (buildCommand)
|
||||
{
|
||||
case PlaceGhost placeGhost:
|
||||
yield return BuildGhost(placeGhost);
|
||||
break;
|
||||
case PlaceModule placeModule:
|
||||
yield return BuildModule(placeModule);
|
||||
break;
|
||||
case ModifyConstructedAmount modifyConstructedAmount:
|
||||
PeekNextModifyCommands(ref modifyConstructedAmount);
|
||||
yield return ProgressConstruction(modifyConstructedAmount);
|
||||
break;
|
||||
case UpdateBase updateBase:
|
||||
yield return UpdatePlacedBase(updateBase);
|
||||
break;
|
||||
case PlaceBase placeBase:
|
||||
yield return BuildBase(placeBase);
|
||||
break;
|
||||
case BaseDeconstructed baseDeconstructed:
|
||||
yield return DeconstructBase(baseDeconstructed);
|
||||
break;
|
||||
case PieceDeconstructed pieceDeconstructed:
|
||||
yield return DeconstructPiece(pieceDeconstructed);
|
||||
break;
|
||||
default:
|
||||
Log.Error($"Found an unhandled build command packet: {buildCommand}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the next build command is also a ModifyConstructedAmount applied on the same object, we'll just skip the current one to apply the new one.
|
||||
/// </summary>
|
||||
private void PeekNextModifyCommands(ref ModifyConstructedAmount currentCommand)
|
||||
{
|
||||
while (BuildQueue.Count > 0 && BuildQueue.Peek() is ModifyConstructedAmount nextCommand && nextCommand.GhostId.Equals(currentCommand.GhostId))
|
||||
{
|
||||
BuildQueue.Dequeue();
|
||||
currentCommand = nextCommand;
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator BuildGhost(PlaceGhost placeGhost)
|
||||
{
|
||||
GhostEntity ghostEntity = placeGhost.GhostEntity;
|
||||
Transform parent = GetParentOrGlobalRoot(ghostEntity.ParentId);
|
||||
yield return GhostEntitySpawner.RestoreGhost(parent, ghostEntity);
|
||||
BasesCooldown[ghostEntity.ParentId ?? ghostEntity.Id] = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public IEnumerator BuildModule(PlaceModule placeModule)
|
||||
{
|
||||
ModuleEntity moduleEntity = placeModule.ModuleEntity;
|
||||
Transform parent = GetParentOrGlobalRoot(moduleEntity.ParentId);
|
||||
TaskResult<Optional<GameObject>> result = new();
|
||||
yield return ModuleEntitySpawner.RestoreModule(parent, moduleEntity, result);
|
||||
if (result.value.HasValue)
|
||||
{
|
||||
this.Resolve<EntityMetadataManager>().ApplyMetadata(result.value.Value.gameObject, moduleEntity.Metadata);
|
||||
}
|
||||
BasesCooldown[moduleEntity.ParentId ?? moduleEntity.Id] = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public IEnumerator ProgressConstruction(ModifyConstructedAmount modifyConstructedAmount)
|
||||
{
|
||||
if (NitroxEntity.TryGetComponentFrom(modifyConstructedAmount.GhostId, out Constructable constructable))
|
||||
{
|
||||
BasesCooldown[modifyConstructedAmount.GhostId] = DateTimeOffset.UtcNow;
|
||||
if (modifyConstructedAmount.ConstructedAmount == 0f)
|
||||
{
|
||||
constructable.constructedAmount = 0f;
|
||||
yield return constructable.ProgressDeconstruction();
|
||||
Destroy(constructable.gameObject);
|
||||
BasesCooldown.Remove(modifyConstructedAmount.GhostId);
|
||||
yield break;
|
||||
}
|
||||
if (modifyConstructedAmount.ConstructedAmount >= 1f)
|
||||
{
|
||||
constructable.SetState(true, true);
|
||||
yield return BuildingPostSpawner.ApplyPostSpawner(gameObject, modifyConstructedAmount.GhostId);
|
||||
yield break;
|
||||
}
|
||||
constructable.SetState(false, false);
|
||||
constructable.constructedAmount = modifyConstructedAmount.ConstructedAmount;
|
||||
yield return constructable.ProgressDeconstruction();
|
||||
constructable.UpdateMaterial();
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator BuildBase(PlaceBase placeBase)
|
||||
{
|
||||
if (!NitroxEntity.TryGetComponentFrom(placeBase.FormerGhostId, out ConstructableBase constructableBase))
|
||||
{
|
||||
FailedOperations++;
|
||||
yield break;
|
||||
}
|
||||
BaseGhost baseGhost = constructableBase.model.GetComponent<BaseGhost>();
|
||||
constructableBase.SetState(true, true);
|
||||
NitroxEntity.SetNewId(baseGhost.targetBase.gameObject, placeBase.FormerGhostId);
|
||||
|
||||
// Specific case : just a moonpool built as a base
|
||||
if (constructableBase.techType == TechType.BaseMoonpool)
|
||||
{
|
||||
// For a new base, the moonpool will be the only cell which is 0, 0, 0
|
||||
Int3 absoluteCell = new(0, 0, 0);
|
||||
// Deterministic id, see MoonpoolManager.LateAssignNitroxEntity
|
||||
NitroxId moonpoolId = placeBase.FormerGhostId.Increment();
|
||||
baseGhost.targetBase.gameObject.EnsureComponent<MoonpoolManager>().RegisterMoonpool(absoluteCell, moonpoolId);
|
||||
}
|
||||
|
||||
BasesCooldown[placeBase.FormerGhostId] = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public IEnumerator UpdatePlacedBase(UpdateBase updateBase)
|
||||
{
|
||||
if (!NitroxEntity.TryGetComponentFrom<Base>(updateBase.BaseId, out _))
|
||||
{
|
||||
Log.Error($"Couldn't find base with id: {updateBase.BaseId} when processing packet: {updateBase}");
|
||||
FailedOperations++;
|
||||
yield break;
|
||||
}
|
||||
|
||||
OperationTracker tracker = EnsureTracker(updateBase.BaseId);
|
||||
tracker.RegisterOperation(updateBase.OperationId);
|
||||
|
||||
if (!NitroxEntity.TryGetComponentFrom(updateBase.FormerGhostId, out ConstructableBase constructableBase))
|
||||
{
|
||||
tracker.FailedOperations++;
|
||||
Log.Error($"Couldn't find ghost with id: {updateBase.FormerGhostId} when processing packet: {updateBase}");
|
||||
yield break;
|
||||
}
|
||||
Temp.ChildrenTransfer = updateBase.ChildrenTransfer;
|
||||
|
||||
BaseGhost baseGhost = constructableBase.model.GetComponent<BaseGhost>();
|
||||
constructableBase.SetState(true, true);
|
||||
BasesCooldown[updateBase.BaseId] = DateTimeOffset.UtcNow;
|
||||
// In the case the built piece was an interior piece, we'll want to transfer the id to it.
|
||||
if (BuildUtils.TryTransferIdFromGhostToModule(baseGhost, updateBase.FormerGhostId, constructableBase, out GameObject moduleObject))
|
||||
{
|
||||
yield return BuildingPostSpawner.ApplyPostSpawner(moduleObject, updateBase.FormerGhostId);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator DeconstructBase(BaseDeconstructed baseDeconstructed)
|
||||
{
|
||||
if (!NitroxEntity.TryGetObjectFrom(baseDeconstructed.FormerBaseId, out GameObject baseObject))
|
||||
{
|
||||
FailedOperations++;
|
||||
Log.Error($"Couldn't find base with id: {baseDeconstructed.FormerBaseId} when processing packet: {baseDeconstructed}");
|
||||
yield break;
|
||||
}
|
||||
BaseDeconstructable[] deconstructableChildren = baseObject.GetComponentsInChildren<BaseDeconstructable>(true);
|
||||
|
||||
if (deconstructableChildren.Length == 1 && deconstructableChildren[0])
|
||||
{
|
||||
using (PacketSuppressor<BaseDeconstructed>.Suppress())
|
||||
using (PacketSuppressor<PieceDeconstructed>.Suppress())
|
||||
{
|
||||
deconstructableChildren[0].Deconstruct();
|
||||
}
|
||||
BasesCooldown[baseDeconstructed.FormerBaseId] = DateTimeOffset.UtcNow;
|
||||
yield break;
|
||||
}
|
||||
Log.Error($"Found multiple {nameof(BaseDeconstructable)} under base {baseObject} while there should be only one");
|
||||
EnsureTracker(baseDeconstructed.FormerBaseId).FailedOperations++;
|
||||
}
|
||||
|
||||
public IEnumerator DeconstructPiece(PieceDeconstructed pieceDeconstructed)
|
||||
{
|
||||
if (!NitroxEntity.TryGetComponentFrom(pieceDeconstructed.BaseId, out Base @base))
|
||||
{
|
||||
FailedOperations++;
|
||||
Log.Error($"Couldn't find base with id: {pieceDeconstructed.BaseId} when processing packet: {pieceDeconstructed}");
|
||||
yield break;
|
||||
}
|
||||
|
||||
OperationTracker tracker = EnsureTracker(pieceDeconstructed.BaseId);
|
||||
|
||||
BuildPieceIdentifier pieceIdentifier = pieceDeconstructed.BuildPieceIdentifier;
|
||||
Transform cellObject = @base.GetCellObject(pieceIdentifier.BaseCell.ToUnity());
|
||||
if (!cellObject)
|
||||
{
|
||||
Log.Error($"Couldn't find cell object {pieceIdentifier.BaseCell} when destructing piece {pieceDeconstructed}");
|
||||
yield break;
|
||||
}
|
||||
BaseDeconstructable[] deconstructableChildren = cellObject.GetComponentsInChildren<BaseDeconstructable>(true);
|
||||
foreach (BaseDeconstructable baseDeconstructable in deconstructableChildren)
|
||||
{
|
||||
if (!BuildUtils.TryGetIdentifier(baseDeconstructable, out BuildPieceIdentifier identifier) || !identifier.Equals(pieceIdentifier))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
using (PacketSuppressor<BaseDeconstructed>.Suppress())
|
||||
using (PacketSuppressor<PieceDeconstructed>.Suppress())
|
||||
using (PacketSuppressor<WaterParkDeconstructed>.Suppress())
|
||||
using (PacketSuppressor<LargeWaterParkDeconstructed>.Suppress())
|
||||
using (Temp.Fill(pieceDeconstructed))
|
||||
{
|
||||
baseDeconstructable.Deconstruct();
|
||||
}
|
||||
tracker.RegisterOperation(pieceDeconstructed.OperationId);
|
||||
BasesCooldown[pieceDeconstructed.BaseId] = DateTimeOffset.UtcNow;
|
||||
yield break;
|
||||
}
|
||||
Log.Error($"Couldn't find the right BaseDeconstructable to be destructed under {pieceDeconstructed.BaseId}");
|
||||
tracker.FailedOperations++;
|
||||
}
|
||||
|
||||
public static Transform GetParentOrGlobalRoot(NitroxId id)
|
||||
{
|
||||
if (id != null && NitroxEntity.TryGetObjectFrom(id, out GameObject parentObject))
|
||||
{
|
||||
return parentObject.transform;
|
||||
}
|
||||
return LargeWorldStreamer.main.globalRoot.transform;
|
||||
}
|
||||
|
||||
private void CleanCooldowns()
|
||||
{
|
||||
BasesCooldown.RemoveWhere(DateTimeOffset.UtcNow, (time, curr) => (curr - time) >= MultiplayerBuildCooldown);
|
||||
}
|
||||
|
||||
public class TemporaryBuildData : IDisposable
|
||||
{
|
||||
public NitroxId Id;
|
||||
public InteriorPieceEntity NewWaterPark;
|
||||
public List<NitroxId> MovedChildrenIds;
|
||||
public (NitroxId, NitroxId) ChildrenTransfer;
|
||||
public bool Transfer;
|
||||
public Dictionary<NitroxId, List<NitroxId>> MovedChildrenIdsByNewHostId;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Id = null;
|
||||
NewWaterPark = null;
|
||||
MovedChildrenIds = null;
|
||||
ChildrenTransfer = (null, null);
|
||||
Transfer = false;
|
||||
MovedChildrenIdsByNewHostId = null;
|
||||
}
|
||||
|
||||
public TemporaryBuildData Fill(PieceDeconstructed pieceDeconstructed)
|
||||
{
|
||||
Id = pieceDeconstructed.PieceId;
|
||||
if (pieceDeconstructed is WaterParkDeconstructed waterParkDeconstructed)
|
||||
{
|
||||
NewWaterPark = waterParkDeconstructed.NewWaterPark;
|
||||
MovedChildrenIds = waterParkDeconstructed.MovedChildrenIds;
|
||||
Transfer = waterParkDeconstructed.Transfer;
|
||||
return this;
|
||||
}
|
||||
if (pieceDeconstructed is LargeWaterParkDeconstructed largeWaterParkDeconstructed)
|
||||
{
|
||||
MovedChildrenIdsByNewHostId = largeWaterParkDeconstructed.MovedChildrenIdsByNewHostId;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Building resync-related part of <see cref="BuildingHandler"/>.
|
||||
/// </summary>
|
||||
public partial class BuildingHandler
|
||||
{
|
||||
private static readonly TimeSpan ResyncRequestCooldown = TimeSpan.FromSeconds(10);
|
||||
private DateTimeOffset LatestResyncRequestTimeOffset;
|
||||
|
||||
private Dictionary<NitroxId, OperationTracker> Operations;
|
||||
// TODO: Should be used to track total fails when more stuff is built towards resyncing
|
||||
public int FailedOperations;
|
||||
|
||||
public bool Resyncing;
|
||||
|
||||
public OperationTracker EnsureTracker(NitroxId baseId)
|
||||
{
|
||||
if (!Operations.TryGetValue(baseId, out OperationTracker tracker))
|
||||
{
|
||||
Operations[baseId] = tracker = new();
|
||||
}
|
||||
return tracker;
|
||||
}
|
||||
|
||||
public int GetCurrentOperationIdOrDefault(NitroxId baseId)
|
||||
{
|
||||
if (baseId != null && Operations.TryGetValue(baseId, out OperationTracker tracker))
|
||||
{
|
||||
return tracker.LastOperationId + tracker.LocalOperations;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public void StartResync<T>(Dictionary<T, int> entities) where T : Entity
|
||||
{
|
||||
Resyncing = true;
|
||||
FailedOperations = 0;
|
||||
BuildQueue.Clear();
|
||||
working = true;
|
||||
InitializeOperations(entities.ToDictionary(pair => pair.Key.Id, pair => pair.Value));
|
||||
}
|
||||
|
||||
public void StopResync()
|
||||
{
|
||||
working = false;
|
||||
Resyncing = false;
|
||||
}
|
||||
|
||||
public void InitializeOperations(Dictionary<NitroxId, int> operations)
|
||||
{
|
||||
foreach (KeyValuePair<NitroxId, int> pair in operations)
|
||||
{
|
||||
EnsureTracker(pair.Key).ResetToId(pair.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public void AskForResync()
|
||||
{
|
||||
if (!Multiplayer.Main || !Multiplayer.Main.InitialSyncCompleted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
TimeSpan deltaTime = DateTimeOffset.UtcNow - LatestResyncRequestTimeOffset;
|
||||
if (deltaTime < ResyncRequestCooldown)
|
||||
{
|
||||
double timeLeft = ResyncRequestCooldown.TotalSeconds - deltaTime.TotalSeconds;
|
||||
Log.InGame(Language.main.Get("Nitrox_ResyncOnCooldown").Replace("{TIME_LEFT}", string.Format("{0:N2}", timeLeft)));
|
||||
return;
|
||||
}
|
||||
LatestResyncRequestTimeOffset = DateTimeOffset.UtcNow;
|
||||
|
||||
this.Resolve<IPacketSender>().Send(new BuildingResyncRequest());
|
||||
Log.InGame(Language.main.Get("Nitrox_ResyncRequested"));
|
||||
}
|
||||
}
|
142
NitroxClient/GameLogic/Bases/GhostMetadataApplier.cs
Normal file
142
NitroxClient/GameLogic/Bases/GhostMetadataApplier.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using System.Collections;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
|
||||
using NitroxModel.DataStructures.GameLogic.Entities.Metadata.Bases;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using UnityEngine;
|
||||
using UWE;
|
||||
|
||||
namespace NitroxClient.GameLogic.Bases;
|
||||
|
||||
public static class GhostMetadataApplier
|
||||
{
|
||||
public static void ApplyBasicMetadataTo(this GhostMetadata ghostMetadata, BaseGhost baseGhost)
|
||||
{
|
||||
baseGhost.targetOffset = ghostMetadata.TargetOffset.ToUnity();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the given metadata to a ghost depending on their types.
|
||||
/// </summary>
|
||||
/// <returns>An extra instruction set to yield for BaseDeconstructable ghosts or null if unrequired.</returns>
|
||||
public static IEnumerator ApplyMetadataToGhost(BaseGhost baseGhost, EntityMetadata entityMetadata, Base @base)
|
||||
{
|
||||
if (entityMetadata is not GhostMetadata ghostMetadata)
|
||||
{
|
||||
Log.Error($"Trying to apply metadata to a ghost that is not of type {nameof(GhostMetadata)} : [{entityMetadata.GetType()}]");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (BuildUtils.IsUnderBaseDeconstructable(baseGhost, false) &&
|
||||
entityMetadata is BaseDeconstructableGhostMetadata deconstructableMetadata)
|
||||
{
|
||||
return deconstructableMetadata.ApplyBaseDeconstructableMetadataTo(baseGhost, @base);
|
||||
}
|
||||
|
||||
switch (baseGhost)
|
||||
{
|
||||
case BaseAddWaterPark:
|
||||
case BaseAddPartitionDoorGhost:
|
||||
case BaseAddModuleGhost:
|
||||
case BaseAddFaceGhost:
|
||||
if (ghostMetadata is BaseAnchoredFaceGhostMetadata faceMetadata)
|
||||
{
|
||||
faceMetadata.ApplyBaseAnchoredFaceMetadataTo(baseGhost);
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
|
||||
case BaseAddPartitionGhost:
|
||||
if (ghostMetadata is BaseAnchoredCellGhostMetadata cellMetadata)
|
||||
{
|
||||
cellMetadata.ApplyBaseAnchoredCellMetadataTo(baseGhost);
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
ghostMetadata.ApplyBasicMetadataTo(baseGhost);
|
||||
return null;
|
||||
}
|
||||
Log.Error($"[{nameof(GhostMetadataApplier)}] Metadata of type {entityMetadata.GetType()} can't be applied to ghost of type {baseGhost.GetType()}");
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void ApplyBaseAnchoredCellMetadataTo(this BaseAnchoredCellGhostMetadata ghostMetadata, BaseGhost baseGhost)
|
||||
{
|
||||
ApplyBasicMetadataTo(ghostMetadata, baseGhost);
|
||||
if (ghostMetadata.AnchoredCell.HasValue && baseGhost is BaseAddPartitionGhost ghost)
|
||||
{
|
||||
ghost.anchoredCell = ghostMetadata.AnchoredCell.Value.ToUnity();
|
||||
}
|
||||
}
|
||||
|
||||
public static void ApplyBaseAnchoredFaceMetadataTo(this BaseAnchoredFaceGhostMetadata ghostMetadata, BaseGhost baseGhost)
|
||||
{
|
||||
ApplyBasicMetadataTo(ghostMetadata, baseGhost);
|
||||
if (ghostMetadata.AnchoredFace.HasValue)
|
||||
{
|
||||
switch (baseGhost)
|
||||
{
|
||||
case BaseAddWaterPark ghost:
|
||||
ghost.anchoredFace = ghostMetadata.AnchoredFace.Value.ToUnity();
|
||||
break;
|
||||
case BaseAddPartitionDoorGhost ghost:
|
||||
ghost.anchoredFace = ghostMetadata.AnchoredFace.Value.ToUnity();
|
||||
break;
|
||||
case BaseAddModuleGhost ghost:
|
||||
ghost.anchoredFace = ghostMetadata.AnchoredFace.Value.ToUnity();
|
||||
break;
|
||||
case BaseAddFaceGhost ghost:
|
||||
ghost.anchoredFace = ghostMetadata.AnchoredFace.Value.ToUnity();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerator ApplyBaseDeconstructableMetadataTo(this BaseDeconstructableGhostMetadata ghostMetadata, BaseGhost baseGhost, Base @base)
|
||||
{
|
||||
ghostMetadata.ApplyBasicMetadataTo(baseGhost);
|
||||
baseGhost.DisableGhostModelScripts();
|
||||
if (ghostMetadata.ModuleFace.HasValue)
|
||||
{
|
||||
if (!baseGhost.TryGetComponentInParent(out ConstructableBase constructableBase, true))
|
||||
{
|
||||
Log.Error($"Couldn't find an interior piece's parent ConstructableBase to apply a {nameof(BaseDeconstructableGhostMetadata)} to.");
|
||||
yield break;
|
||||
}
|
||||
|
||||
constructableBase.moduleFace = ghostMetadata.ModuleFace.Value.ToUnity();
|
||||
|
||||
IPrefabRequest request = PrefabDatabase.GetPrefabAsync(ghostMetadata.ClassId);
|
||||
yield return request;
|
||||
if (!request.TryGetPrefab(out GameObject prefab))
|
||||
{
|
||||
// Without its module, the ghost will be useless, so we delete it (like in base game)
|
||||
Object.Destroy(constructableBase.gameObject);
|
||||
Log.Error($"Couldn't find a prefab for module of interior piece of ClassId: {ghostMetadata.ClassId}");
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (!baseGhost.targetBase)
|
||||
{
|
||||
baseGhost.targetBase = @base;
|
||||
}
|
||||
|
||||
Base.Face face = ghostMetadata.ModuleFace.Value.ToUnity();
|
||||
face.cell += baseGhost.targetBase.GetAnchor();
|
||||
GameObject moduleObject = baseGhost.targetBase.SpawnModule(prefab, face);
|
||||
if (!moduleObject)
|
||||
{
|
||||
Object.Destroy(constructableBase.gameObject);
|
||||
Log.Error("Module couldn't be spawned for interior piece");
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (moduleObject.TryGetComponent(out IBaseModule baseModule))
|
||||
{
|
||||
baseModule.constructed = constructableBase.constructedAmount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
82
NitroxClient/GameLogic/Bases/GhostMetadataRetriever.cs
Normal file
82
NitroxClient/GameLogic/Bases/GhostMetadataRetriever.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel.DataStructures.GameLogic.Entities.Metadata.Bases;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.Bases;
|
||||
|
||||
/// <summary>
|
||||
/// Because of the multiple possible types for Ghost components, the retrieving of their metadata is inappropriate for the MetadataExtractor system
|
||||
/// </summary>
|
||||
public static class GhostMetadataRetriever
|
||||
{
|
||||
public static GhostMetadata GetMetadataForGhost(BaseGhost baseGhost)
|
||||
{
|
||||
// Specific case in which a piece was deconstructed and resulted in a BaseDeconstructable with a normal BaseGhost
|
||||
if (BuildUtils.IsUnderBaseDeconstructable(baseGhost, true))
|
||||
{
|
||||
return GetBaseDeconstructableMetadata(baseGhost);
|
||||
}
|
||||
|
||||
GhostMetadata metadata = baseGhost switch
|
||||
{
|
||||
BaseAddWaterPark or BaseAddPartitionDoorGhost or BaseAddModuleGhost or BaseAddFaceGhost => baseGhost.GetBaseAnchoredFaceMetadata(),
|
||||
BaseAddPartitionGhost => baseGhost.GetBaseAnchoredCellMetadata(),
|
||||
_ => baseGhost.GetMetadata<GhostMetadata>(),
|
||||
};
|
||||
return metadata;
|
||||
}
|
||||
|
||||
public static T GetMetadata<T>(this BaseGhost baseGhost) where T : GhostMetadata, new()
|
||||
{
|
||||
T metadata = new()
|
||||
{
|
||||
TargetOffset = baseGhost.targetOffset.ToDto()
|
||||
};
|
||||
return metadata;
|
||||
}
|
||||
|
||||
public static BaseDeconstructableGhostMetadata GetBaseDeconstructableMetadata(this BaseGhost baseGhost)
|
||||
{
|
||||
BaseDeconstructableGhostMetadata metadata = baseGhost.GetMetadata<BaseDeconstructableGhostMetadata>();
|
||||
if (baseGhost.TryGetComponentInParent(out ConstructableBase constructableBase, true) && constructableBase.moduleFace.HasValue)
|
||||
{
|
||||
Base.Face moduleFace = constructableBase.moduleFace.Value;
|
||||
metadata.ModuleFace = moduleFace.ToDto();
|
||||
moduleFace.cell += baseGhost.targetBase.GetAnchor();
|
||||
Component baseModule = baseGhost.targetBase.GetModule(moduleFace).AliveOrNull();
|
||||
if (baseModule && baseModule.TryGetComponent(out PrefabIdentifier identifier))
|
||||
{
|
||||
metadata.ClassId = identifier.ClassId;
|
||||
}
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
public static BaseAnchoredFaceGhostMetadata GetBaseAnchoredFaceMetadata(this BaseGhost baseGhost)
|
||||
{
|
||||
BaseAnchoredFaceGhostMetadata metadata = baseGhost.GetMetadata<BaseAnchoredFaceGhostMetadata>();
|
||||
metadata.AnchoredFace = baseGhost switch
|
||||
{
|
||||
BaseAddWaterPark ghost => ghost.anchoredFace?.ToDto(),
|
||||
BaseAddPartitionDoorGhost ghost => ghost.anchoredFace?.ToDto(),
|
||||
BaseAddModuleGhost ghost => ghost.anchoredFace?.ToDto(),
|
||||
BaseAddFaceGhost ghost => ghost.anchoredFace?.ToDto(),
|
||||
_ => null
|
||||
};
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
public static BaseAnchoredCellGhostMetadata GetBaseAnchoredCellMetadata(this BaseGhost baseGhost)
|
||||
{
|
||||
BaseAnchoredCellGhostMetadata metadata = baseGhost.GetMetadata<BaseAnchoredCellGhostMetadata>();
|
||||
if (baseGhost is BaseAddPartitionGhost ghost && ghost.anchoredCell.HasValue)
|
||||
{
|
||||
metadata.AnchoredCell = ghost.anchoredCell.Value.ToDto();
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
}
|
45
NitroxClient/GameLogic/Bases/OperationTracker.cs
Normal file
45
NitroxClient/GameLogic/Bases/OperationTracker.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System;
|
||||
|
||||
namespace NitroxClient.GameLogic.Bases;
|
||||
|
||||
public class OperationTracker
|
||||
{
|
||||
// TODO: Add the target's Id in here if future works requires it
|
||||
public int LastOperationId = -1;
|
||||
/// <summary>
|
||||
/// Accounts for locally-issued build actions
|
||||
/// </summary>
|
||||
public int LocalOperations;
|
||||
/// <summary>
|
||||
/// Calculated number of missed build actions
|
||||
/// </summary>
|
||||
public int MissedOperations;
|
||||
/// <summary>
|
||||
/// Number of detected issues when trying to apply actions remotely
|
||||
/// </summary>
|
||||
public int FailedOperations;
|
||||
|
||||
public void RegisterOperation(int newOperationId)
|
||||
{
|
||||
// If the progress was never registered, we don't need to account for missed operations
|
||||
if (LastOperationId != -1)
|
||||
{
|
||||
MissedOperations += Math.Max(newOperationId - (LastOperationId + LocalOperations) - 1, 0);
|
||||
}
|
||||
LastOperationId = newOperationId;
|
||||
LocalOperations = 0;
|
||||
}
|
||||
|
||||
public void ResetToId(int operationId = 0)
|
||||
{
|
||||
LastOperationId = operationId;
|
||||
LocalOperations = 0;
|
||||
MissedOperations = 0;
|
||||
FailedOperations = 0;
|
||||
}
|
||||
|
||||
public bool IsDesynced()
|
||||
{
|
||||
return MissedOperations + FailedOperations > 0;
|
||||
}
|
||||
}
|
170
NitroxClient/GameLogic/BulletManager.cs
Normal file
170
NitroxClient/GameLogic/BulletManager.cs
Normal file
@@ -0,0 +1,170 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using NitroxClient.GameLogic.Spawning.WorldEntities;
|
||||
using NitroxClient.MonoBehaviours;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel.DataStructures;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// Registers one stasis sphere per connected remote player, and syncs their behaviour.<br/>
|
||||
/// Also syncs remote torpedo (of all types) shots and hits.
|
||||
/// </summary>
|
||||
public class BulletManager
|
||||
{
|
||||
private readonly PlayerManager playerManager;
|
||||
|
||||
// This only allows for one stasis sphere per player
|
||||
// (which is the normal capacity, but could be adapted for a mod letting multiple stasis spheres)
|
||||
private readonly Dictionary<ushort, StasisSphere> stasisSpherePerPlayerId = [];
|
||||
|
||||
/// <summary>
|
||||
/// TechTypes of objects which should have a Vehicle MB
|
||||
/// </summary>
|
||||
private static readonly HashSet<TechType> preloadedVehicleTypes = [
|
||||
TechType.Seamoth, TechType.Exosuit
|
||||
];
|
||||
|
||||
private readonly Dictionary<TechType, GameObject> torpedoPrefabByTechType = [];
|
||||
|
||||
private GameObject stasisSpherePrefab;
|
||||
|
||||
public BulletManager(PlayerManager playerManager)
|
||||
{
|
||||
this.playerManager = playerManager;
|
||||
}
|
||||
|
||||
public void ShootSeamothTorpedo(NitroxId bulletId, TechType techType, Vector3 position, Quaternion rotation, float speed, float lifeTime)
|
||||
{
|
||||
if (!torpedoPrefabByTechType.TryGetValue(techType, out GameObject prefab))
|
||||
{
|
||||
Log.ErrorOnce($"[{nameof(BulletManager)}] Received ShootSeamothTorpedo request with TechType: {techType} but no prefab was loaded for it");
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject torpedoClone = GameObjectHelper.SpawnFromPrefab(prefab, bulletId);
|
||||
// We mark it to be able to ignore events from remote bullets
|
||||
torpedoClone.AddComponent<RemotePlayerBullet>();
|
||||
|
||||
// We cast it to Bullet to ensure we're calling the same method as in Vehicle.TorpedoShot
|
||||
Bullet seamothTorpedo = torpedoClone.GetComponent<SeamothTorpedo>();
|
||||
seamothTorpedo.Shoot(position, rotation, speed, lifeTime);
|
||||
}
|
||||
|
||||
public void TorpedoHit(NitroxId bulletId, Vector3 position, Quaternion rotation)
|
||||
{
|
||||
// On the local player, the torpedo might have already exploded while the packet is received with latency.
|
||||
// Therefore we don't need to log a failed query of bulletId
|
||||
if (NitroxEntity.TryGetComponentFrom(bulletId, out SeamothTorpedo torpedo))
|
||||
{
|
||||
torpedo.tr.position = position;
|
||||
torpedo.tr.rotation = rotation;
|
||||
torpedo.OnHit(default);
|
||||
torpedo.Deactivate();
|
||||
}
|
||||
}
|
||||
|
||||
public void TorpedoTargetAcquired(NitroxId bulletId, NitroxId targetId, Vector3 position, Quaternion rotation)
|
||||
{
|
||||
// The target object might not be findable in which case we'll just ignore it
|
||||
// because the explosion will still be moved to the right spot
|
||||
if (NitroxEntity.TryGetComponentFrom(bulletId, out SeamothTorpedo torpedo) &&
|
||||
NitroxEntity.TryGetObjectFrom(targetId, out GameObject targetObject))
|
||||
{
|
||||
torpedo.tr.position = position;
|
||||
torpedo.tr.rotation = rotation;
|
||||
// Stuff from SeamothTorpedo.RepeatingTargeting
|
||||
torpedo.homingTarget = targetObject;
|
||||
torpedo.CancelInvoke();
|
||||
}
|
||||
}
|
||||
|
||||
public void ShootStasisSphere(ushort playerId, Vector3 position, Quaternion rotation, float speed, float lifeTime, float chargeNormalized)
|
||||
{
|
||||
StasisSphere cloneSphere = EnsurePlayerHasSphere(playerId);
|
||||
|
||||
cloneSphere.Shoot(position, rotation, speed, lifeTime, chargeNormalized);
|
||||
}
|
||||
|
||||
public void StasisSphereHit(ushort playerId, Vector3 position, Quaternion rotation, float chargeNormalized, float consumption)
|
||||
{
|
||||
StasisSphere cloneSphere = EnsurePlayerHasSphere(playerId);
|
||||
|
||||
// Setup the sphere in case the shot was sent earlier
|
||||
cloneSphere.Shoot(position, rotation, 0, 0, chargeNormalized);
|
||||
// We override this field (set by .Shoot) with the right data
|
||||
cloneSphere._consumption = consumption;
|
||||
|
||||
// Code from Bullet.Update when finding an object to hit
|
||||
cloneSphere._visible = true;
|
||||
cloneSphere.OnMadeVisible();
|
||||
cloneSphere.EnableField();
|
||||
cloneSphere.Deactivate();
|
||||
}
|
||||
|
||||
private StasisSphere EnsurePlayerHasSphere(ushort playerId)
|
||||
{
|
||||
if (stasisSpherePerPlayerId.TryGetValue(playerId, out StasisSphere remoteSphere) && remoteSphere)
|
||||
{
|
||||
return remoteSphere;
|
||||
}
|
||||
|
||||
// It should be set to inactive automatically in Bullet.Awake
|
||||
GameObject playerSphereClone = GameObject.Instantiate(stasisSpherePrefab);
|
||||
playerSphereClone.name = $"remote-{playerId}-{playerSphereClone.name}";
|
||||
// We mark it to be able to ignore events from remote bullets
|
||||
playerSphereClone.AddComponent<RemotePlayerBullet>();
|
||||
StasisSphere stasisSphere = playerSphereClone.GetComponent<StasisSphere>();
|
||||
|
||||
stasisSpherePerPlayerId[playerId] = stasisSphere;
|
||||
return stasisSphere;
|
||||
}
|
||||
|
||||
private void DestroyPlayerSphere(ushort playerId)
|
||||
{
|
||||
if (stasisSpherePerPlayerId.TryGetValue(playerId, out StasisSphere stasisSphere) && stasisSphere)
|
||||
{
|
||||
GameObject.Destroy(stasisSphere.gameObject);
|
||||
}
|
||||
stasisSpherePerPlayerId.Remove(playerId);
|
||||
}
|
||||
|
||||
public IEnumerator Initialize()
|
||||
{
|
||||
TaskResult<GameObject> result = new();
|
||||
|
||||
// Load torpedo types prefab and store them by tech type
|
||||
foreach (TechType techType in preloadedVehicleTypes)
|
||||
{
|
||||
yield return DefaultWorldEntitySpawner.RequestPrefab(techType, result);
|
||||
if (result.value && result.value.TryGetComponent(out Vehicle vehicle) && vehicle.torpedoTypes != null)
|
||||
{
|
||||
foreach (TorpedoType torpedoType in vehicle.torpedoTypes)
|
||||
{
|
||||
torpedoPrefabByTechType[torpedoType.techType] = torpedoType.prefab;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load the stasis sphere prefab
|
||||
yield return DefaultWorldEntitySpawner.RequestPrefab(TechType.StasisRifle, result);
|
||||
StasisRifle rifle = result.value.GetComponent<StasisRifle>();
|
||||
if (rifle)
|
||||
{
|
||||
stasisSpherePrefab = rifle.effectSpherePrefab;
|
||||
}
|
||||
|
||||
// Setup remote players' stasis spheres
|
||||
foreach (RemotePlayer remotePlayer in playerManager.GetAll())
|
||||
{
|
||||
EnsurePlayerHasSphere(remotePlayer.PlayerId);
|
||||
}
|
||||
|
||||
playerManager.OnCreate += (playerId, _) => { EnsurePlayerHasSphere(playerId); };
|
||||
playerManager.OnRemove += (playerId, _) => { DestroyPlayerSphere(playerId); };
|
||||
}
|
||||
|
||||
public class RemotePlayerBullet : MonoBehaviour;
|
||||
}
|
24
NitroxClient/GameLogic/ChatUI/ChatLogEntry.cs
Normal file
24
NitroxClient/GameLogic/ChatUI/ChatLogEntry.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.ChatUI
|
||||
{
|
||||
public class ChatLogEntry
|
||||
{
|
||||
public string PlayerName { get; }
|
||||
public string MessageText { get; set; }
|
||||
public Color32 PlayerColor { get; }
|
||||
public string Time { get; set; }
|
||||
public GameObject EntryObject { get; set; }
|
||||
|
||||
public ChatLogEntry(string playerName, string messageText, Color32 playerColor)
|
||||
{
|
||||
PlayerName = playerName;
|
||||
MessageText = messageText;
|
||||
PlayerColor = playerColor;
|
||||
UpdateTime();
|
||||
}
|
||||
|
||||
public void UpdateTime() => Time = DateTime.Now.ToString("HH:mm");
|
||||
}
|
||||
}
|
131
NitroxClient/GameLogic/ChatUI/PlayerChatManager.cs
Normal file
131
NitroxClient/GameLogic/ChatUI/PlayerChatManager.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
using System.Collections;
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxClient.GameLogic.Settings;
|
||||
using NitroxClient.MonoBehaviours.Gui.Chat;
|
||||
using NitroxModel.Helper;
|
||||
using NitroxModel.Packets;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UWE;
|
||||
using static NitroxClient.Unity.Helper.AssetBundleLoader;
|
||||
|
||||
namespace NitroxClient.GameLogic.ChatUI
|
||||
{
|
||||
public class PlayerChatManager
|
||||
{
|
||||
private readonly IMultiplayerSession multiplayerSession;
|
||||
|
||||
private const char SERVER_COMMAND_PREFIX = '/';
|
||||
|
||||
public bool IsChatSelected
|
||||
{
|
||||
get => PlayerChat.IsReady && playerChat.selected;
|
||||
}
|
||||
|
||||
public PlayerChatManager(IMultiplayerSession multiplayerSession)
|
||||
{
|
||||
this.multiplayerSession = multiplayerSession;
|
||||
|
||||
if (NitroxEnvironment.IsNormal) //Testing would fail because it's trying to access runtime MonoBehaviours.
|
||||
{
|
||||
CoroutineHost.StartCoroutine(LoadChatLogAsset());
|
||||
}
|
||||
}
|
||||
|
||||
private PlayerChat playerChat;
|
||||
private GameObject chatKeyHint;
|
||||
public Transform PlayerChaTransform => playerChat.transform;
|
||||
|
||||
public void ShowChat() => Player.main.StartCoroutine(ShowChatAsync());
|
||||
private IEnumerator ShowChatAsync()
|
||||
{
|
||||
yield return new WaitUntil(() => PlayerChat.IsReady);
|
||||
playerChat.Show();
|
||||
}
|
||||
|
||||
public void HideChat() => Player.main.StartCoroutine(HideChatAsync());
|
||||
private IEnumerator HideChatAsync()
|
||||
{
|
||||
yield return new WaitUntil(() => PlayerChat.IsReady);
|
||||
playerChat.Deselect();
|
||||
playerChat.Hide();
|
||||
}
|
||||
|
||||
public void SelectChat() => Player.main.StartCoroutine(SelectChatAsync());
|
||||
private IEnumerator SelectChatAsync()
|
||||
{
|
||||
yield return new WaitUntil(() => PlayerChat.IsReady);
|
||||
playerChat.Show();
|
||||
playerChat.Select();
|
||||
|
||||
if (!NitroxPrefs.ChatUsed.Value)
|
||||
{
|
||||
DisableChatKeyHint();
|
||||
}
|
||||
}
|
||||
|
||||
public void DeselectChat() => Player.main.StartCoroutine(DeselectChatAsync());
|
||||
private IEnumerator DeselectChatAsync()
|
||||
{
|
||||
yield return new WaitUntil(() => PlayerChat.IsReady);
|
||||
playerChat.Deselect();
|
||||
}
|
||||
|
||||
public void AddMessage(string playerName, string message, Color color) => Player.main.StartCoroutine(AddMessageAsync(playerName, message, color));
|
||||
private IEnumerator AddMessageAsync(string playerName, string message, Color color)
|
||||
{
|
||||
yield return new WaitUntil(() => PlayerChat.IsReady);
|
||||
playerChat.WriteLogEntry(playerName, message, color);
|
||||
}
|
||||
|
||||
public void SendMessage()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(playerChat.InputText))
|
||||
{
|
||||
playerChat.Select();
|
||||
return;
|
||||
}
|
||||
|
||||
string trimmedInput = playerChat.InputText.Trim();
|
||||
if (trimmedInput[0] == SERVER_COMMAND_PREFIX)
|
||||
{
|
||||
// Server command
|
||||
multiplayerSession.Send(new ServerCommand(trimmedInput.Substring(1)));
|
||||
playerChat.InputText = "";
|
||||
playerChat.Select();
|
||||
return;
|
||||
}
|
||||
|
||||
// We shouldn't add the message to the local chat instantly but instead let the server tell us if this message is added or not
|
||||
multiplayerSession.Send(new ChatMessage(multiplayerSession.Reservation.PlayerId, trimmedInput));
|
||||
playerChat.InputText = "";
|
||||
playerChat.Select();
|
||||
}
|
||||
|
||||
private IEnumerator LoadChatLogAsset()
|
||||
{
|
||||
yield return LoadUIAsset(NitroxAssetBundle.CHAT_LOG, true);
|
||||
|
||||
GameObject playerChatGameObject = (GameObject)NitroxAssetBundle.CHAT_LOG.LoadedAssets[0];
|
||||
playerChat = playerChatGameObject.AddComponent<PlayerChat>();
|
||||
|
||||
yield return playerChat.SetupChatComponents();
|
||||
}
|
||||
|
||||
public IEnumerator LoadChatKeyHint()
|
||||
{
|
||||
if (!NitroxPrefs.ChatUsed.Value)
|
||||
{
|
||||
yield return LoadUIAsset(NitroxAssetBundle.CHAT_KEY_HINT, false);
|
||||
chatKeyHint = NitroxAssetBundle.CHAT_KEY_HINT.LoadedAssets[0] as GameObject;
|
||||
}
|
||||
}
|
||||
|
||||
private void DisableChatKeyHint()
|
||||
{
|
||||
chatKeyHint.GetComponentInChildren<Text>().CrossFadeAlpha(0, 1, false);
|
||||
chatKeyHint.GetComponentInChildren<Image>().CrossFadeAlpha(0, 1, false);
|
||||
NitroxPrefs.ChatUsed.Value = true;
|
||||
}
|
||||
}
|
||||
}
|
80
NitroxClient/GameLogic/CorrectedTimeManager.cs
Normal file
80
NitroxClient/GameLogic/CorrectedTimeManager.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using NitroxClient.Communication.NetworkingLayer.LiteNetLib;
|
||||
using NitroxModel.Networking;
|
||||
|
||||
namespace NitroxClient.GameLogic;
|
||||
|
||||
public partial class TimeManager
|
||||
{
|
||||
private readonly NtpSyncer ntpSyncer;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the local player could obtain a correction from the global NTP server
|
||||
/// </summary>
|
||||
private bool clientOnlineMode;
|
||||
/// <summary>
|
||||
/// Local's UTC correction with a global NTP server ("real"). Client Correction = Real UTC Time - Client UTC Time
|
||||
/// </summary>
|
||||
private TimeSpan clientCorrection;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not server could obtain a correction from the global NTP server
|
||||
/// </summary>
|
||||
private bool serverOnlineMode;
|
||||
/// <summary>
|
||||
/// Server's UTC correction with a global NTP server ("real"). Server Correction = Real UTC Time - Server UTC Time
|
||||
/// </summary>
|
||||
private TimeSpan serverCorrection;
|
||||
|
||||
/// <summary>
|
||||
/// Correction Delta = Server UTC Time - Client UTC Time. Calculated thanks to <see cref="ClockSyncProcedure"/>
|
||||
/// </summary>
|
||||
private TimeSpan correctionDelta;
|
||||
|
||||
public DateTimeOffset ServerUtcNow()
|
||||
{
|
||||
if (clientOnlineMode && serverOnlineMode)
|
||||
{
|
||||
// From clientCorrection and serverCorrection we deduce the following equation:
|
||||
// Server UTC Time + Server correction = Client UTC Time + Client correction
|
||||
// from this equation we deduce Server UTC Time which is the below value
|
||||
return DateTimeOffset.UtcNow + clientCorrection - serverCorrection;
|
||||
}
|
||||
|
||||
// In any other case than the above one, we can only rely on the clock sync procedure for which the equation gives the below value
|
||||
return DateTimeOffset.UtcNow + correctionDelta;
|
||||
}
|
||||
|
||||
public void SetCorrectionDelta(long remoteTimeDelta)
|
||||
{
|
||||
correctionDelta = new TimeSpan(remoteTimeDelta);
|
||||
Log.Info($"OFFLINE mode: delta = {correctionDelta}");
|
||||
}
|
||||
|
||||
public void SetClientCorrectionData(bool clientOnlineMode, TimeSpan correctionOffset)
|
||||
{
|
||||
this.clientOnlineMode = clientOnlineMode;
|
||||
clientCorrection = correctionOffset;
|
||||
Log.Info($"Client ONLINE: correction = {correctionOffset}");
|
||||
}
|
||||
|
||||
public void SetServerCorrectionData(bool serverOnlineMode, long serverUtcCorrectionTicks)
|
||||
{
|
||||
this.serverOnlineMode = serverOnlineMode;
|
||||
serverCorrection = new(serverUtcCorrectionTicks);
|
||||
Log.Info($"Server ONLINE: correction = {serverCorrection}");
|
||||
}
|
||||
|
||||
public void AttemptNtpSync()
|
||||
{
|
||||
ntpSyncer.Setup(false, (onlineMode, correction) =>
|
||||
{
|
||||
if (onlineMode)
|
||||
{
|
||||
SetClientCorrectionData(onlineMode, correction);
|
||||
}
|
||||
});
|
||||
ntpSyncer.RequestNtpService();
|
||||
}
|
||||
}
|
201
NitroxClient/GameLogic/Cyclops.cs
Normal file
201
NitroxClient/GameLogic/Cyclops.cs
Normal file
@@ -0,0 +1,201 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NitroxClient.Communication;
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxClient.MonoBehaviours;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.DataStructures.Util;
|
||||
using NitroxModel.Packets;
|
||||
using NitroxModel_Subnautica.DataStructures.GameLogic;
|
||||
using NitroxModel_Subnautica.Packets;
|
||||
using UnityEngine;
|
||||
using static NitroxClient.GameLogic.Spawning.Metadata.Extractor.CyclopsMetadataExtractor;
|
||||
|
||||
namespace NitroxClient.GameLogic
|
||||
{
|
||||
public class Cyclops
|
||||
{
|
||||
private readonly IPacketSender packetSender;
|
||||
private readonly Vehicles vehicles;
|
||||
private readonly Entities entities;
|
||||
|
||||
public Cyclops(IPacketSender packetSender, Vehicles vehicles, Entities entities)
|
||||
{
|
||||
this.packetSender = packetSender;
|
||||
this.vehicles = vehicles;
|
||||
this.entities = entities;
|
||||
}
|
||||
|
||||
public void BroadcastMetadataChange(NitroxId id)
|
||||
{
|
||||
GameObject gameObject = NitroxEntity.RequireObjectFrom(id);
|
||||
CyclopsGameObject cyclops = new CyclopsGameObject() { GameObject = gameObject };
|
||||
entities.EntityMetadataChanged(cyclops, id);
|
||||
}
|
||||
|
||||
public void BroadcastLaunchDecoy(NitroxId id)
|
||||
{
|
||||
CyclopsDecoyLaunch packet = new CyclopsDecoyLaunch(id);
|
||||
packetSender.Send(packet);
|
||||
}
|
||||
|
||||
public void BroadcastActivateFireSuppression(NitroxId id)
|
||||
{
|
||||
CyclopsFireSuppression packet = new CyclopsFireSuppression(id);
|
||||
packetSender.Send(packet);
|
||||
}
|
||||
|
||||
public void LaunchDecoy(NitroxId id)
|
||||
{
|
||||
GameObject cyclops = NitroxEntity.RequireObjectFrom(id);
|
||||
CyclopsDecoyManager decoyManager = cyclops.RequireComponent<CyclopsDecoyManager>();
|
||||
using (PacketSuppressor<EntityMetadataUpdate>.Suppress())
|
||||
{
|
||||
decoyManager.Invoke(nameof(CyclopsDecoyManager.LaunchWithDelay), 3f);
|
||||
decoyManager.decoyLaunchButton.UpdateText();
|
||||
decoyManager.subRoot.voiceNotificationManager.PlayVoiceNotification(decoyManager.subRoot.decoyNotification, false, true);
|
||||
decoyManager.subRoot.BroadcastMessage("UpdateTotalDecoys", decoyManager.decoyCount, SendMessageOptions.DontRequireReceiver);
|
||||
CyclopsDecoyLaunchButton decoyLaunchButton = cyclops.RequireComponentInChildren<CyclopsDecoyLaunchButton>();
|
||||
decoyLaunchButton.StartCooldown();
|
||||
}
|
||||
}
|
||||
|
||||
public void StartFireSuppression(NitroxId id)
|
||||
{
|
||||
GameObject cyclops = NitroxEntity.RequireObjectFrom(id);
|
||||
CyclopsFireSuppressionSystemButton fireSuppButton = cyclops.RequireComponentInChildren<CyclopsFireSuppressionSystemButton>();
|
||||
using (PacketSuppressor<CyclopsFireSuppression>.Suppress())
|
||||
{
|
||||
// Infos from SubFire.StartSystem
|
||||
fireSuppButton.subFire.StartCoroutine(StartFireSuppressionSystem(fireSuppButton.subFire));
|
||||
fireSuppButton.StartCooldown();
|
||||
}
|
||||
}
|
||||
|
||||
// Remake of the StartSystem Coroutine from original player. Some Methods are not used from the original coroutine
|
||||
// For example no temporaryClose as this will be initiated anyway from the originating Player
|
||||
// Also the fire extiguishing will not start cause the initial player is already extiguishing the fires. Else this could double/triple/... the extinguishing
|
||||
private IEnumerator StartFireSuppressionSystem(SubFire fire)
|
||||
{
|
||||
fire.subRoot.voiceNotificationManager.PlayVoiceNotification(fire.subRoot.fireSupressionNotification, false, true);
|
||||
yield return Yielders.WaitFor3Seconds;
|
||||
fire.fireSuppressionActive = true;
|
||||
fire.subRoot.fireSuppressionState = true;
|
||||
fire.subRoot.BroadcastMessage("NewAlarmState", null, SendMessageOptions.DontRequireReceiver);
|
||||
fire.Invoke(nameof(SubFire.CancelFireSuppression), fire.fireSuppressionSystemDuration);
|
||||
float doorCloseDuration = 30f;
|
||||
fire.gameObject.BroadcastMessage("TemporaryLock", doorCloseDuration, SendMessageOptions.DontRequireReceiver);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers a <see cref="CyclopsDamage"/> packet
|
||||
/// </summary>
|
||||
public void OnCreateDamagePoint(SubRoot subRoot)
|
||||
{
|
||||
BroadcastDamageState(subRoot, Optional.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the player repairs a <see cref="CyclopsDamagePoint"/>. Right now it's not possible to partially repair because it would be difficult to implement.
|
||||
/// <see cref="CyclopsDamagePoint"/>s are coupled with <see cref="LiveMixin"/>, which is used with just about anything that has health.
|
||||
/// I would need to hook onto <see cref="LiveMixin.AddHealth(float)"/>, or maybe the repair gun event to catch when something repairs a damage point, which I don't
|
||||
/// believe is worth the effort. A <see cref="CyclopsDamagePoint"/> is already fully repaired in a little over a second. This can trigger sending
|
||||
/// <see cref="CyclopsDamagePointRepaired"/> and <see cref="CyclopsDamage"/> packets
|
||||
/// </summary>
|
||||
public void OnDamagePointRepaired(SubRoot subRoot, CyclopsDamagePoint damagePoint, float repairAmount)
|
||||
{
|
||||
if (!subRoot.TryGetIdOrWarn(out NitroxId subId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < subRoot.damageManager.damagePoints.Length; i++)
|
||||
{
|
||||
if (subRoot.damageManager.damagePoints[i] == damagePoint)
|
||||
{
|
||||
CyclopsDamagePointRepaired packet = new(subId, i, repairAmount);
|
||||
packetSender.Send(packet);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send out a <see cref="CyclopsDamage"/> packet
|
||||
/// </summary>
|
||||
private void BroadcastDamageState(SubRoot subRoot, Optional<DamageInfo> info)
|
||||
{
|
||||
if (!subRoot.TryGetIdOrWarn(out NitroxId subId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LiveMixin subHealth = subRoot.gameObject.RequireComponent<LiveMixin>();
|
||||
if (subHealth.health <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
CyclopsDamageInfoData damageInfo = null;
|
||||
if (info.HasValue)
|
||||
{
|
||||
DamageInfo damage = info.Value;
|
||||
Optional<NitroxId> dealerId = damage.dealer.GetId();
|
||||
// Source of the damage. Used if the damage done to the Cyclops was not calculated on other clients. Currently it's just used to figure out what sounds and
|
||||
// visual effects should be used.
|
||||
damageInfo = new CyclopsDamageInfoData(subId, dealerId, damage.originalDamage, damage.damage, damage.position.ToDto(), damage.type);
|
||||
}
|
||||
|
||||
int[] damagePointIndexes = GetActiveDamagePoints(subRoot).ToArray();
|
||||
CyclopsFireData[] firePoints = GetActiveRoomFires(subRoot.GetComponent<SubFire>()).ToArray();
|
||||
|
||||
CyclopsDamage packet = new(subId, subRoot.GetComponent<LiveMixin>().health, subRoot.damageManager.subLiveMixin.health, subRoot.GetComponent<SubFire>().liveMixin.health, damagePointIndexes, firePoints, damageInfo);
|
||||
packetSender.Send(packet);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all of the index locations of <see cref="CyclopsDamagePoint"/>s in <see cref="CyclopsExternalDamageManager.damagePoints"/>.
|
||||
/// </summary>
|
||||
private IEnumerable<int> GetActiveDamagePoints(SubRoot subRoot)
|
||||
{
|
||||
for (int i = 0; i < subRoot.damageManager.damagePoints.Length; i++)
|
||||
{
|
||||
if (subRoot.damageManager.damagePoints[i].gameObject.activeSelf)
|
||||
{
|
||||
yield return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all of the index locations of all the fires on the <see cref="SubRoot"/>. <see cref="SubFire.RoomFire.spawnNodes"/> contains
|
||||
/// a static list of all possible fire nodes.
|
||||
/// </summary>
|
||||
private IEnumerable<CyclopsFireData> GetActiveRoomFires(SubFire subFire)
|
||||
{
|
||||
if (!subFire.subRoot.TryGetIdOrWarn(out NitroxId subRootId))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<CyclopsRooms, SubFire.RoomFire> roomFire in subFire.roomFires)
|
||||
{
|
||||
for (int i = 0; i < roomFire.Value.spawnNodes.Length; i++)
|
||||
{
|
||||
if (roomFire.Value.spawnNodes[i].childCount > 0)
|
||||
{
|
||||
if (!roomFire.Value.spawnNodes[i].GetComponentInChildren<Fire>().TryGetIdOrWarn(out NitroxId fireId))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return new CyclopsFireData(fireId, subRootId, roomFire.Key, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
152
NitroxClient/GameLogic/CyclopsPawn.cs
Normal file
152
NitroxClient/GameLogic/CyclopsPawn.cs
Normal file
@@ -0,0 +1,152 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract;
|
||||
using NitroxClient.MonoBehaviours;
|
||||
using NitroxClient.MonoBehaviours.Cyclops;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// A virtual entity responsible for one player's movement in the cyclops.
|
||||
/// It simulates the local player's movements by creating a fake player in a <see cref="VirtualCyclops"/>'s instance and then giving data about the real movement.
|
||||
/// </summary>
|
||||
public class CyclopsPawn
|
||||
{
|
||||
private static readonly List<CharacterController> controllers = [];
|
||||
public static readonly int PLAYER_LAYER = 1 << LayerMask.NameToLayer("Player");
|
||||
|
||||
private readonly INitroxPlayer player;
|
||||
private readonly NitroxCyclops cyclops;
|
||||
private readonly Transform virtualTransform;
|
||||
private readonly Transform realCyclopsTransform;
|
||||
private readonly bool isLocalPlayer;
|
||||
public readonly GameObject RealObject;
|
||||
public GameObject Handle;
|
||||
public CharacterController Controller;
|
||||
public Func<bool> MaintainPredicate;
|
||||
|
||||
public Vector3 Position
|
||||
{
|
||||
get => Handle.transform.position;
|
||||
set { Handle.transform.position = value; }
|
||||
}
|
||||
|
||||
public CyclopsPawn(INitroxPlayer player, NitroxCyclops cyclops)
|
||||
{
|
||||
this.player = player;
|
||||
this.cyclops = cyclops;
|
||||
virtualTransform = VirtualCyclops.Instance.transform;
|
||||
realCyclopsTransform = cyclops.transform;
|
||||
|
||||
if (player is ILocalNitroxPlayer)
|
||||
{
|
||||
isLocalPlayer = true;
|
||||
RealObject = Player.mainObject;
|
||||
CyclopsMotor cyclopsMotor = Player.mainObject.GetComponent<CyclopsMotor>();
|
||||
MaintainPredicate = () => cyclopsMotor.canControl && !Player.main.isPiloting;
|
||||
}
|
||||
else if (player is RemotePlayer remotePlayer)
|
||||
{
|
||||
RealObject = remotePlayer.Body;
|
||||
MaintainPredicate = () => !remotePlayer.PilotingChair;
|
||||
}
|
||||
|
||||
Initialize($"{player.PlayerName}-Pawn", RealObject.transform.localPosition);
|
||||
}
|
||||
|
||||
public void Initialize(string name, Vector3 localPosition)
|
||||
{
|
||||
PlayerController playerController = Player.main.GetComponent<PlayerController>();
|
||||
GroundMotor groundMotor = Player.main.GetComponent<GroundMotor>();
|
||||
CharacterController reference = Player.main.GetComponent<CharacterController>();
|
||||
|
||||
Handle = GameObject.CreatePrimitive(PrimitiveType.Capsule);
|
||||
Handle.layer = 1 << PLAYER_LAYER;
|
||||
Handle.name = name;
|
||||
Handle.transform.parent = virtualTransform;
|
||||
Handle.transform.localPosition = localPosition;
|
||||
GameObject.DestroyImmediate(Handle.GetComponent<Collider>());
|
||||
|
||||
Controller = Handle.AddComponent<CharacterController>();
|
||||
Controller.height = playerController.standheight - playerController.cameraOffset;
|
||||
// Calculation from Groundmotor.SetControllerHeight
|
||||
Vector3 center = groundMotor.colliderCenter;
|
||||
center.y = -Controller.height * 0.5f - playerController.cameraOffset;
|
||||
Controller.center = center;
|
||||
Controller.radius = playerController.controllerRadius;
|
||||
Controller.skinWidth = reference.skinWidth;
|
||||
Controller.stepOffset = groundMotor.controller.stepOffset;
|
||||
Controller.slopeLimit = groundMotor.controller.slopeLimit;
|
||||
|
||||
RegisterController();
|
||||
|
||||
Handle.AddComponent<CyclopsPawnIdentifier>().Pawn = this;
|
||||
}
|
||||
|
||||
public void RegisterController()
|
||||
{
|
||||
foreach (CharacterController controller in controllers)
|
||||
{
|
||||
Physics.IgnoreCollision(controller, Controller);
|
||||
}
|
||||
controllers.Add(Controller);
|
||||
}
|
||||
|
||||
public void SetReference()
|
||||
{
|
||||
Handle.transform.localPosition = RealObject.transform.localPosition;
|
||||
if (!isLocalPlayer)
|
||||
{
|
||||
Handle.transform.localRotation = RealObject.transform.localRotation;
|
||||
}
|
||||
}
|
||||
|
||||
public void MaintainPosition()
|
||||
{
|
||||
RealObject.transform.localPosition = Handle.transform.localPosition;
|
||||
RealObject.transform.rotation = realCyclopsTransform.rotation;
|
||||
if (!isLocalPlayer)
|
||||
{
|
||||
RealObject.transform.localRotation = Handle.transform.localRotation;
|
||||
}
|
||||
}
|
||||
|
||||
public void Unregister()
|
||||
{
|
||||
if (cyclops)
|
||||
{
|
||||
if (isLocalPlayer)
|
||||
{
|
||||
cyclops.OnLocalPlayerExit();
|
||||
}
|
||||
else
|
||||
{
|
||||
cyclops.OnPlayerExit((RemotePlayer)player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replicates openable being blocked only if the pawn causing the block is in the cyclops associated to the virtual one.
|
||||
/// </summary>
|
||||
public void BlockOpenable(Openable openable, bool blockState)
|
||||
{
|
||||
if (cyclops.Virtual)
|
||||
{
|
||||
openable.blocked = blockState;
|
||||
cyclops.Virtual.ReplicateBlock(openable, blockState);
|
||||
}
|
||||
}
|
||||
|
||||
public void Terminate()
|
||||
{
|
||||
controllers.Remove(Controller);
|
||||
GameObject.Destroy(Handle);
|
||||
}
|
||||
}
|
||||
|
||||
public class CyclopsPawnIdentifier : MonoBehaviour
|
||||
{
|
||||
public CyclopsPawn Pawn;
|
||||
}
|
370
NitroxClient/GameLogic/Entities.cs
Normal file
370
NitroxClient/GameLogic/Entities.cs
Normal file
@@ -0,0 +1,370 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NitroxClient.Communication;
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract;
|
||||
using NitroxClient.GameLogic.Spawning;
|
||||
using NitroxClient.GameLogic.Spawning.Abstract;
|
||||
using NitroxClient.GameLogic.Spawning.Bases;
|
||||
using NitroxClient.GameLogic.Spawning.Metadata;
|
||||
using NitroxClient.GameLogic.Spawning.WorldEntities;
|
||||
using NitroxClient.MonoBehaviours;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.DataStructures.GameLogic;
|
||||
using NitroxModel.DataStructures.GameLogic.Entities;
|
||||
using NitroxModel.DataStructures.GameLogic.Entities.Bases;
|
||||
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
|
||||
using NitroxModel.DataStructures.Util;
|
||||
using NitroxModel.Packets;
|
||||
using UnityEngine;
|
||||
using UWE;
|
||||
|
||||
namespace NitroxClient.GameLogic
|
||||
{
|
||||
public class Entities
|
||||
{
|
||||
private readonly IPacketSender packetSender;
|
||||
private readonly ThrottledPacketSender throttledPacketSender;
|
||||
private readonly EntityMetadataManager entityMetadataManager;
|
||||
private readonly SimulationOwnership simulationOwnership;
|
||||
|
||||
private readonly Dictionary<NitroxId, Type> spawnedAsType = new();
|
||||
private readonly Dictionary<NitroxId, List<Entity>> pendingParentEntitiesByParentId = new Dictionary<NitroxId, List<Entity>>();
|
||||
|
||||
private readonly Dictionary<Type, IEntitySpawner> entitySpawnersByType = new Dictionary<Type, IEntitySpawner>();
|
||||
|
||||
public List<Entity> EntitiesToSpawn { get; private init; }
|
||||
private bool spawningEntities;
|
||||
|
||||
private readonly HashSet<NitroxId> deletedEntitiesIds = new();
|
||||
private readonly List<SimulatedEntity> pendingSimulatedEntities = new();
|
||||
|
||||
public Entities(IPacketSender packetSender, ThrottledPacketSender throttledPacketSender, EntityMetadataManager entityMetadataManager, PlayerManager playerManager, LocalPlayer localPlayer, LiveMixinManager liveMixinManager, TimeManager timeManager, SimulationOwnership simulationOwnership)
|
||||
{
|
||||
this.packetSender = packetSender;
|
||||
this.throttledPacketSender = throttledPacketSender;
|
||||
this.entityMetadataManager = entityMetadataManager;
|
||||
this.simulationOwnership = simulationOwnership;
|
||||
EntitiesToSpawn = new();
|
||||
|
||||
entitySpawnersByType[typeof(PrefabChildEntity)] = new PrefabChildEntitySpawner();
|
||||
entitySpawnersByType[typeof(PathBasedChildEntity)] = new PathBasedChildEntitySpawner();
|
||||
entitySpawnersByType[typeof(InstalledModuleEntity)] = new InstalledModuleEntitySpawner();
|
||||
entitySpawnersByType[typeof(InstalledBatteryEntity)] = new InstalledBatteryEntitySpawner();
|
||||
entitySpawnersByType[typeof(InventoryEntity)] = new InventoryEntitySpawner();
|
||||
entitySpawnersByType[typeof(InventoryItemEntity)] = new InventoryItemEntitySpawner(entityMetadataManager);
|
||||
entitySpawnersByType[typeof(WorldEntity)] = new WorldEntitySpawner(entityMetadataManager, playerManager, localPlayer, this, simulationOwnership);
|
||||
entitySpawnersByType[typeof(PlaceholderGroupWorldEntity)] = entitySpawnersByType[typeof(WorldEntity)];
|
||||
entitySpawnersByType[typeof(PrefabPlaceholderEntity)] = entitySpawnersByType[typeof(WorldEntity)];
|
||||
entitySpawnersByType[typeof(EscapePodWorldEntity)] = entitySpawnersByType[typeof(WorldEntity)];
|
||||
entitySpawnersByType[typeof(PlayerWorldEntity)] = entitySpawnersByType[typeof(WorldEntity)];
|
||||
entitySpawnersByType[typeof(VehicleWorldEntity)] = entitySpawnersByType[typeof(WorldEntity)];
|
||||
entitySpawnersByType[typeof(SerializedWorldEntity)] = entitySpawnersByType[typeof(WorldEntity)];
|
||||
entitySpawnersByType[typeof(GlobalRootEntity)] = new GlobalRootEntitySpawner();
|
||||
entitySpawnersByType[typeof(BaseLeakEntity)] = new BaseLeakEntitySpawner(liveMixinManager);
|
||||
entitySpawnersByType[typeof(BuildEntity)] = new BuildEntitySpawner(this, (BaseLeakEntitySpawner)entitySpawnersByType[typeof(BaseLeakEntity)]);
|
||||
entitySpawnersByType[typeof(RadiationLeakEntity)] = new RadiationLeakEntitySpawner(timeManager);
|
||||
entitySpawnersByType[typeof(ModuleEntity)] = new ModuleEntitySpawner(this);
|
||||
entitySpawnersByType[typeof(GhostEntity)] = new GhostEntitySpawner();
|
||||
entitySpawnersByType[typeof(OxygenPipeEntity)] = new OxygenPipeEntitySpawner(this, (WorldEntitySpawner)entitySpawnersByType[typeof(WorldEntity)]);
|
||||
entitySpawnersByType[typeof(PlacedWorldEntity)] = new PlacedWorldEntitySpawner((WorldEntitySpawner)entitySpawnersByType[typeof(WorldEntity)]);
|
||||
entitySpawnersByType[typeof(InteriorPieceEntity)] = new InteriorPieceEntitySpawner(this, entityMetadataManager);
|
||||
entitySpawnersByType[typeof(GeyserWorldEntity)] = entitySpawnersByType[typeof(WorldEntity)];
|
||||
entitySpawnersByType[typeof(ReefbackEntity)] = entitySpawnersByType[typeof(WorldEntity)];
|
||||
entitySpawnersByType[typeof(ReefbackChildEntity)] = entitySpawnersByType[typeof(WorldEntity)];
|
||||
entitySpawnersByType[typeof(CreatureRespawnEntity)] = entitySpawnersByType[typeof(WorldEntity)];
|
||||
}
|
||||
|
||||
public void EntityMetadataChanged(object o, NitroxId id)
|
||||
{
|
||||
Optional<EntityMetadata> metadata = entityMetadataManager.Extract(o);
|
||||
|
||||
if (metadata.HasValue)
|
||||
{
|
||||
BroadcastMetadataUpdate(id, metadata.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public void EntityMetadataChangedThrottled(object o, NitroxId id, float throttleTime = 0.2f)
|
||||
{
|
||||
// As throttled broadcasting is done after some time by a different function, this is where the packet sending should be interrupted
|
||||
if (PacketSuppressor<EntityMetadataUpdate>.IsSuppressed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Optional<EntityMetadata> metadata = entityMetadataManager.Extract(o);
|
||||
|
||||
if (metadata.HasValue)
|
||||
{
|
||||
BroadcastMetadataUpdateThrottled(id, metadata.Value, throttleTime);
|
||||
}
|
||||
}
|
||||
|
||||
public void BroadcastMetadataUpdate(NitroxId id, EntityMetadata metadata)
|
||||
{
|
||||
packetSender.Send(new EntityMetadataUpdate(id, metadata));
|
||||
}
|
||||
|
||||
public void BroadcastMetadataUpdateThrottled(NitroxId id, EntityMetadata metadata, float throttleTime = 0.2f)
|
||||
{
|
||||
throttledPacketSender.SendThrottled(new EntityMetadataUpdate(id, metadata), packet => packet.Id, throttleTime);
|
||||
}
|
||||
|
||||
public void BroadcastEntitySpawnedByClient(Entity entity, bool requireRespawn = false)
|
||||
{
|
||||
packetSender.Send(new EntitySpawnedByClient(entity, requireRespawn));
|
||||
}
|
||||
|
||||
private IEnumerator SpawnNewEntities()
|
||||
{
|
||||
bool restarted = false;
|
||||
yield return SpawnBatchAsync(EntitiesToSpawn).OnYieldError(exception =>
|
||||
{
|
||||
Log.Error(exception);
|
||||
if (EntitiesToSpawn.Count > 0)
|
||||
{
|
||||
restarted = true;
|
||||
// It's safe to run a new time because the processed entity is removed first so it won't infinitely throw errors
|
||||
CoroutineHost.StartCoroutine(SpawnNewEntities());
|
||||
}
|
||||
});
|
||||
spawningEntities = restarted;
|
||||
if (!spawningEntities)
|
||||
{
|
||||
entityMetadataManager.ClearNewerMetadata();
|
||||
deletedEntitiesIds.Clear();
|
||||
simulationOwnership.ClearNewerSimulations();
|
||||
}
|
||||
}
|
||||
|
||||
public void EnqueueEntitiesToSpawn(List<Entity> entitiesToEnqueue)
|
||||
{
|
||||
EntitiesToSpawn.InsertRange(0, entitiesToEnqueue);
|
||||
if (!spawningEntities)
|
||||
{
|
||||
spawningEntities = true;
|
||||
CoroutineHost.StartCoroutine(SpawnNewEntities());
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Yield returning takes too much time (at least once per IEnumerator branch) and it quickly gets out of hand with long function call hierarchies so
|
||||
/// we want to reduce the amount of yield operations and only skip to the next frame when required (to maintain the FPS).
|
||||
/// Also saves resources by using the IOut instances
|
||||
/// </remarks>
|
||||
/// <param name="forceRespawn">Should children be spawned even if already marked as spawned</param>
|
||||
public IEnumerator SpawnBatchAsync(List<Entity> batch, bool forceRespawn = false, bool skipFrames = true)
|
||||
{
|
||||
// we divide the FPS by 2.5 because we consider (time for 1 frame + spawning time without a frame + extra computing time)
|
||||
float allottedTimePerFrame = 0.4f / Application.targetFrameRate;
|
||||
float timeLimit = Time.realtimeSinceStartup + allottedTimePerFrame;
|
||||
|
||||
TaskResult<Optional<GameObject>> entityResult = new();
|
||||
TaskResult<Exception> exception = new();
|
||||
|
||||
while (batch.Count > 0)
|
||||
{
|
||||
entityResult.Set(Optional.Empty);
|
||||
exception.Set(null);
|
||||
|
||||
Entity entity = batch[^1];
|
||||
batch.RemoveAt(batch.Count - 1);
|
||||
|
||||
// Preconditions which may get the spawn process cancelled or postponed
|
||||
if (deletedEntitiesIds.Remove(entity.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (WasAlreadySpawned(entity) && !forceRespawn)
|
||||
{
|
||||
UpdateEntity(entity);
|
||||
continue;
|
||||
}
|
||||
else if (entity.ParentId != null && !IsParentReady(entity.ParentId))
|
||||
{
|
||||
AddPendingParentEntity(entity);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Executing the spawn instructions whether they're sync or async
|
||||
IEntitySpawner entitySpawner = entitySpawnersByType[entity.GetType()];
|
||||
if (entitySpawner is not ISyncEntitySpawner syncEntitySpawner ||
|
||||
(!syncEntitySpawner.SpawnSyncSafe(entity, entityResult, exception) && exception.Get() == null))
|
||||
{
|
||||
IEnumerator coroutine = entitySpawner.SpawnAsync(entity, entityResult);
|
||||
if (coroutine != null)
|
||||
{
|
||||
yield return coroutine.OnYieldError(Log.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// Any error in there would make spawning children useless
|
||||
if (exception.Get() != null)
|
||||
{
|
||||
Log.Error(exception.Get());
|
||||
continue;
|
||||
}
|
||||
else if (!entityResult.Get().Value)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
entityMetadataManager.ApplyMetadata(entityResult.Get().Value, entity.Metadata);
|
||||
simulationOwnership.ApplyNewerSimulation(entity.Id);
|
||||
|
||||
MarkAsSpawned(entity);
|
||||
|
||||
// Finding out about all children (can be hidden in the object's hierarchy or in a pending list)
|
||||
|
||||
if (!entitySpawner.SpawnsOwnChildren(entity))
|
||||
{
|
||||
batch.AddRange(entity.ChildEntities);
|
||||
|
||||
List<NitroxId> childrenIds = entity.ChildEntities.Select(entity => entity.Id).ToList();
|
||||
if (pendingParentEntitiesByParentId.TryGetValue(entity.Id, out List<Entity> pendingEntities))
|
||||
{
|
||||
IEnumerable<Entity> childrenToAdd = pendingEntities.Where(e => !childrenIds.Contains(e.Id));
|
||||
batch.AddRange(childrenToAdd);
|
||||
pendingParentEntitiesByParentId.Remove(entity.Id);
|
||||
}
|
||||
}
|
||||
|
||||
// Skip a frame to maintain FPS
|
||||
if (Time.realtimeSinceStartup >= timeLimit && skipFrames)
|
||||
{
|
||||
yield return new WaitForEndOfFrame();
|
||||
timeLimit = Time.realtimeSinceStartup + allottedTimePerFrame;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator SpawnEntityAsync(Entity entity, bool forceRespawn = false, bool skipFrames = false)
|
||||
{
|
||||
return SpawnBatchAsync(new() { entity }, forceRespawn, skipFrames);
|
||||
}
|
||||
|
||||
public void CleanupExistingEntities(List<Entity> dirtyEntities)
|
||||
{
|
||||
foreach (Entity entity in dirtyEntities)
|
||||
{
|
||||
RemoveEntityHierarchy(entity);
|
||||
|
||||
Optional<GameObject> gameObject = NitroxEntity.GetObjectFrom(entity.Id);
|
||||
|
||||
if (gameObject.HasValue)
|
||||
{
|
||||
DestroyObject(gameObject.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Either perform a special operation (e.g. for plants) or a simple <see cref="UnityEngine.Object.Destroy"/>
|
||||
/// </summary>
|
||||
public static void DestroyObject(GameObject gameObject)
|
||||
{
|
||||
if (gameObject.TryGetComponent(out Plantable plantable))
|
||||
{
|
||||
plantable.FreeSpot();
|
||||
return;
|
||||
}
|
||||
if (gameObject.TryGetComponent(out GrownPlant grownPlant))
|
||||
{
|
||||
grownPlant.seed.AliveOrNull()?.FreeSpot();
|
||||
return;
|
||||
}
|
||||
|
||||
UnityEngine.Object.Destroy(gameObject);
|
||||
}
|
||||
|
||||
private void UpdateEntity(Entity entity)
|
||||
{
|
||||
if (!NitroxEntity.TryGetObjectFrom(entity.Id, out GameObject gameObject))
|
||||
{
|
||||
#if DEBUG && ENTITY_LOG
|
||||
Log.Error($"Entity was already spawned but not found(is it in another chunk?) NitroxId: {entity.Id} TechType: {entity.TechType} ClassId: {entity.ClassId} Transform: {entity.Transform}");
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
entityMetadataManager.ApplyMetadata(gameObject, entity.Metadata);
|
||||
}
|
||||
|
||||
private void AddPendingParentEntity(Entity entity)
|
||||
{
|
||||
if (!pendingParentEntitiesByParentId.TryGetValue(entity.ParentId, out List<Entity> pendingEntities))
|
||||
{
|
||||
pendingEntities = new List<Entity>();
|
||||
pendingParentEntitiesByParentId[entity.ParentId] = pendingEntities;
|
||||
}
|
||||
|
||||
pendingEntities.Add(entity);
|
||||
}
|
||||
|
||||
// Entites can sometimes be spawned as one thing but need to be respawned later as another. For example, a flare
|
||||
// spawned inside an Inventory as an InventoryItemEntity can later be dropped in the world as a WorldEntity. Another
|
||||
// example would be a base ghost that needs to be respawned a completed piece.
|
||||
public bool WasAlreadySpawned(Entity entity)
|
||||
{
|
||||
if (spawnedAsType.TryGetValue(entity.Id, out Type type))
|
||||
{
|
||||
return type == entity.GetType() && NitroxEntity.TryGetObjectFrom(entity.Id, out _);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool IsKnownEntity(NitroxId id)
|
||||
{
|
||||
return spawnedAsType.ContainsKey(id);
|
||||
}
|
||||
|
||||
public Type RequireEntityType(NitroxId id)
|
||||
{
|
||||
if (spawnedAsType.TryGetValue(id, out Type type))
|
||||
{
|
||||
return type;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Did not have a type for {id}");
|
||||
}
|
||||
|
||||
public bool IsParentReady(NitroxId id)
|
||||
{
|
||||
return WasParentSpawned(id) || NitroxEntity.TryGetObjectFrom(id, out GameObject _);
|
||||
}
|
||||
|
||||
public bool WasParentSpawned(NitroxId id)
|
||||
{
|
||||
return spawnedAsType.ContainsKey(id);
|
||||
}
|
||||
|
||||
public void MarkAsSpawned(Entity entity)
|
||||
{
|
||||
spawnedAsType[entity.Id] = entity.GetType();
|
||||
}
|
||||
|
||||
public void RemoveEntity(NitroxId id) => spawnedAsType.Remove(id);
|
||||
|
||||
public void MarkForDeletion(NitroxId id)
|
||||
{
|
||||
deletedEntitiesIds.Add(id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allows the ability to respawn an entity and its entire hierarchy. Callers are responsible for ensuring the
|
||||
/// entity is no longer in the world.
|
||||
/// </summary>
|
||||
public void RemoveEntityHierarchy(Entity entity)
|
||||
{
|
||||
RemoveEntity(entity.Id);
|
||||
|
||||
foreach (Entity child in entity.ChildEntities)
|
||||
{
|
||||
RemoveEntityHierarchy(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
71
NitroxClient/GameLogic/EquipmentSlots.cs
Normal file
71
NitroxClient/GameLogic/EquipmentSlots.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.Packets;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic;
|
||||
|
||||
public class EquipmentSlots
|
||||
{
|
||||
private readonly IPacketSender packetSender;
|
||||
private readonly Entities entities;
|
||||
|
||||
public EquipmentSlots(IPacketSender packetSender, Entities entities)
|
||||
{
|
||||
this.packetSender = packetSender;
|
||||
this.entities = entities;
|
||||
}
|
||||
|
||||
public void BroadcastEquip(Pickupable pickupable, GameObject owner, string slot)
|
||||
{
|
||||
if (!owner.TryGetIdOrWarn(out NitroxId ownerId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!pickupable.TryGetIdOrWarn(out NitroxId itemId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (owner.TryGetComponent(out Player player))
|
||||
{
|
||||
entities.EntityMetadataChanged(player, ownerId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// UWE also sends module events here as they are technically equipment of the vehicles.
|
||||
ModuleAdded moduleAdded = new(itemId, ownerId, slot);
|
||||
packetSender.Send(moduleAdded);
|
||||
}
|
||||
}
|
||||
|
||||
public void BroadcastUnequip(Pickupable pickupable, GameObject owner, string slot)
|
||||
{
|
||||
if (!owner.TryGetIdOrWarn(out NitroxId ownerId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pickupable.TryGetIdOrWarn(out NitroxId itemId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (owner.TryGetComponent(out Player player))
|
||||
{
|
||||
entities.EntityMetadataChanged(player, ownerId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Reactor rod can't be unequipped so this will only happen when a Nuclear Reactor is destroyed (in which case we don't need this code)
|
||||
if (pickupable.GetTechType() == TechType.ReactorRod)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// UWE also sends module events here as they are technically equipment of the vehicles.
|
||||
ModuleRemoved moduleRemoved = new(itemId, ownerId);
|
||||
packetSender.Send(moduleRemoved);
|
||||
}
|
||||
}
|
||||
}
|
132
NitroxClient/GameLogic/ExosuitModuleEvent.cs
Normal file
132
NitroxClient/GameLogic/ExosuitModuleEvent.cs
Normal file
@@ -0,0 +1,132 @@
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel_Subnautica.Packets;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic;
|
||||
|
||||
public class ExosuitModuleEvent
|
||||
{
|
||||
private static readonly int useToolAnimation = Animator.StringToHash("use_tool");
|
||||
private static readonly int bashAnimation = Animator.StringToHash("bash");
|
||||
|
||||
private readonly IPacketSender packetSender;
|
||||
|
||||
public ExosuitModuleEvent(IPacketSender packetSender)
|
||||
{
|
||||
this.packetSender = packetSender;
|
||||
}
|
||||
|
||||
public void BroadcastClawUse(ExosuitClawArm clawArm, float cooldown)
|
||||
{
|
||||
ExosuitArmAction action;
|
||||
|
||||
// If cooldown of claw arm matches pickup cooldown, the exosuit arm performed a pickup action
|
||||
if (cooldown == clawArm.cooldownPickup)
|
||||
{
|
||||
action = ExosuitArmAction.START_USE_TOOL;
|
||||
} // Else if it matches the punch cooldown, it has punched something (or nothing but water, who knows)
|
||||
else if (cooldown == clawArm.cooldownPunch)
|
||||
{
|
||||
action = ExosuitArmAction.ALT_HIT;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error("Cooldown time does not match pickup or punch time");
|
||||
return;
|
||||
}
|
||||
|
||||
BroadcastArmAction(TechType.ExosuitClawArmModule, clawArm, action, null, null);
|
||||
}
|
||||
|
||||
public static void UseClaw(ExosuitClawArm clawArm, ExosuitArmAction armAction)
|
||||
{
|
||||
switch (armAction)
|
||||
{
|
||||
case ExosuitArmAction.START_USE_TOOL:
|
||||
clawArm.animator.SetTrigger(useToolAnimation);
|
||||
break;
|
||||
case ExosuitArmAction.ALT_HIT:
|
||||
clawArm.animator.SetTrigger(bashAnimation);
|
||||
clawArm.fxControl.Play(0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static void UseDrill(ExosuitDrillArm drillArm, ExosuitArmAction armAction)
|
||||
{
|
||||
switch (armAction)
|
||||
{
|
||||
case ExosuitArmAction.START_USE_TOOL:
|
||||
drillArm.animator.SetBool(useToolAnimation, true);
|
||||
drillArm.loop.Play();
|
||||
break;
|
||||
case ExosuitArmAction.END_USE_TOOL:
|
||||
drillArm.animator.SetBool(useToolAnimation, false);
|
||||
drillArm.StopEffects();
|
||||
break;
|
||||
default:
|
||||
Log.Error($"Drill arm got an arm action he should not get: {armAction}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void BroadcastArmAction(TechType techType, IExosuitArm exosuitArm, ExosuitArmAction armAction, Vector3? opVector, Quaternion? opRotation)
|
||||
{
|
||||
if (exosuitArm.GetGameObject().TryGetIdOrWarn(out NitroxId id))
|
||||
{
|
||||
ExosuitArmActionPacket packet = new(techType, id, armAction, opVector?.ToDto(), opRotation?.ToDto());
|
||||
packetSender.Send(packet);
|
||||
}
|
||||
}
|
||||
|
||||
public void BroadcastArmAction(TechType techType, IExosuitArm exosuitArm, ExosuitArmAction armAction)
|
||||
{
|
||||
if (exosuitArm.GetGameObject().TryGetIdOrWarn(out NitroxId id))
|
||||
{
|
||||
ExosuitArmActionPacket packet = new(techType, id, armAction, null, null);
|
||||
packetSender.Send(packet);
|
||||
}
|
||||
}
|
||||
|
||||
public static void UseGrappling(ExosuitGrapplingArm grapplingArm, ExosuitArmAction armAction, Vector3? opHitVector)
|
||||
{
|
||||
switch (armAction)
|
||||
{
|
||||
case ExosuitArmAction.END_USE_TOOL:
|
||||
grapplingArm.animator.SetBool(useToolAnimation, false);
|
||||
grapplingArm.ResetHook();
|
||||
break;
|
||||
case ExosuitArmAction.START_USE_TOOL:
|
||||
{
|
||||
grapplingArm.animator.SetBool(useToolAnimation, true);
|
||||
if (!grapplingArm.rope.isLaunching)
|
||||
{
|
||||
grapplingArm.rope.LaunchHook(35f);
|
||||
}
|
||||
|
||||
GrapplingHook hook = grapplingArm.hook;
|
||||
|
||||
hook.transform.parent = null;
|
||||
hook.transform.position = grapplingArm.front.transform.position;
|
||||
hook.SetFlying(true);
|
||||
Exosuit componentInParent = grapplingArm.GetComponentInParent<Exosuit>();
|
||||
|
||||
if (!opHitVector.HasValue)
|
||||
{
|
||||
Log.Error("No vector given that contains the hook direction");
|
||||
return;
|
||||
}
|
||||
|
||||
hook.rb.velocity = opHitVector.Value;
|
||||
Utils.PlayFMODAsset(grapplingArm.shootSound, grapplingArm.front, 15f);
|
||||
grapplingArm.grapplingStartPos = componentInParent.transform.position;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
Log.Error($"Grappling arm got an arm action he should not get: {armAction}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
21
NitroxClient/GameLogic/FMOD/FMODSoundSuppressor.cs
Normal file
21
NitroxClient/GameLogic/FMOD/FMODSoundSuppressor.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
|
||||
namespace NitroxClient.GameLogic.FMOD;
|
||||
|
||||
/// <summary>
|
||||
/// Suppresses sounds played by base Subnautica, but not by Nitrox
|
||||
/// </summary>
|
||||
public readonly struct FMODSoundSuppressor : IDisposable
|
||||
{
|
||||
public static bool SuppressFMODEvents { get; private set; }
|
||||
|
||||
public FMODSoundSuppressor()
|
||||
{
|
||||
SuppressFMODEvents = true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
SuppressFMODEvents = false;
|
||||
}
|
||||
}
|
46
NitroxClient/GameLogic/FMOD/FMODSystem.cs
Normal file
46
NitroxClient/GameLogic/FMOD/FMODSystem.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using NitroxClient.Communication;
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.DataStructures.Unity;
|
||||
using NitroxModel.GameLogic.FMOD;
|
||||
using NitroxModel.Packets;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.FMOD;
|
||||
|
||||
public class FMODSystem
|
||||
{
|
||||
private readonly IPacketSender packetSender;
|
||||
|
||||
public FMODSystem(IPacketSender packetSender)
|
||||
{
|
||||
this.packetSender = packetSender;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="FMODSoundSuppressor"/>
|
||||
public static FMODSoundSuppressor SuppressSubnauticaSounds() => new();
|
||||
|
||||
/// <summary>
|
||||
/// Suppresses sending any sound packet
|
||||
/// </summary>
|
||||
public static PacketSuppressor<FMODAssetPacket, FMODEventInstancePacket, FMODCustomEmitterPacket, FMODCustomLoopingEmitterPacket, FMODStudioEmitterPacket> SuppressSendingSounds()
|
||||
{
|
||||
return PacketSuppressor<FMODAssetPacket, FMODEventInstancePacket, FMODCustomEmitterPacket, FMODCustomLoopingEmitterPacket, FMODStudioEmitterPacket>.Suppress();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SoundHelper.CalculateVolume"/>
|
||||
public static float CalculateVolume(Vector3 p1, Vector3 p2, float radius, float volume) => SoundHelper.CalculateVolume(Vector3.Distance(p1, p2), radius, volume);
|
||||
|
||||
public void SendAssetPlay(string path, NitroxVector3 position, float volume) => packetSender.Send(new FMODAssetPacket(path, position, volume));
|
||||
|
||||
public void SendCustomEmitterPlay(NitroxId id, string assetPath) => packetSender.Send(new FMODCustomEmitterPacket(id, assetPath, true));
|
||||
public void SendCustomEmitterStop(NitroxId id, string assetPath) => packetSender.Send(new FMODCustomEmitterPacket(id, assetPath, false));
|
||||
|
||||
public void SendCustomLoopingEmitterPlay(NitroxId id, string assetPath) => packetSender.Send(new FMODCustomLoopingEmitterPacket(id, assetPath));
|
||||
|
||||
public void SendStudioEmitterPlay(NitroxId id, string assetPath, bool allowFadeout) => packetSender.Send(new FMODStudioEmitterPacket(id, assetPath, true, allowFadeout));
|
||||
public void SendStudioEmitterStop(NitroxId id, string assetPath, bool allowFadeout) => packetSender.Send(new FMODStudioEmitterPacket(id, assetPath, false, allowFadeout));
|
||||
|
||||
public void SendEventInstancePlay(NitroxId id, string assetPath, NitroxVector3 position, float volume) => packetSender.Send(new FMODEventInstancePacket(id, true, assetPath, position, volume));
|
||||
public void SendEventInstanceStop(NitroxId id, string assetPath, NitroxVector3 position, float volume) => packetSender.Send(new FMODEventInstancePacket(id, false, assetPath, position, volume));
|
||||
}
|
149
NitroxClient/GameLogic/Fires.cs
Normal file
149
NitroxClient/GameLogic/Fires.cs
Normal file
@@ -0,0 +1,149 @@
|
||||
using System.Collections.Generic;
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxClient.Communication.Packets.Processors;
|
||||
using NitroxClient.MonoBehaviours;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.Packets;
|
||||
using NitroxModel_Subnautica.DataStructures.GameLogic;
|
||||
using NitroxModel_Subnautica.Packets;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles all of the <see cref="Fire"/>s in the game. Currently, the only known Fire spawning is in <see cref="SubFire.CreateFire(SubFire.RoomFire)"/>. The
|
||||
/// fires in the Aurora come loaded with the map and do not grow in size. If we want to create a Fire spawning mechanic outside of Cyclops fires, it should be
|
||||
/// added to <see cref="Fires.Create(CyclopsFireData)"/>. Fire dousing goes by Id and does not need to be
|
||||
/// modified
|
||||
/// </summary>
|
||||
public class Fires
|
||||
{
|
||||
private readonly IPacketSender packetSender;
|
||||
|
||||
/// <summary>
|
||||
/// Used to reduce the <see cref="FireDoused"/> packet spam as fires are being doused. A packet is only sent after
|
||||
/// the douse amount surpasses <see cref="FIRE_DOUSE_AMOUNT_TRIGGER"/>
|
||||
/// </summary>
|
||||
private readonly Dictionary<NitroxId, float> fireDouseAmount = new Dictionary<NitroxId, float>();
|
||||
|
||||
/// <summary>
|
||||
/// Each extinguisher hit is from 0.15 to 0.25. 5 is a bit less than half a second of full extinguishing
|
||||
/// </summary>
|
||||
private const float FIRE_DOUSE_AMOUNT_TRIGGER = 5f;
|
||||
|
||||
public Fires(IPacketSender packetSender)
|
||||
{
|
||||
this.packetSender = packetSender;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when <see cref="SubFire.CreateFire(SubFire.RoomFire)"/> is executed. To create a new fire manually,
|
||||
/// call <see cref="Create(CyclopsFireData)"/>
|
||||
/// </summary>
|
||||
public void OnCreate(Fire fire, SubFire.RoomFire room, int nodeIndex)
|
||||
{
|
||||
if (!fire.TryGetIdOrWarn(out NitroxId fireId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!fire.fireSubRoot.TryGetIdOrWarn(out NitroxId subRootId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CyclopsFireCreated packet = new CyclopsFireCreated(fireId, subRootId, room.roomLinks.room, nodeIndex);
|
||||
packetSender.Send(packet);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when <see cref="Fire.Douse(float)"/> is executed. To Douse a fire manually, retrieve the <see cref="Fire"/> call the Douse method
|
||||
/// </summary>
|
||||
public void OnDouse(Fire fire, float douseAmount)
|
||||
{
|
||||
if (!fire.TryGetIdOrWarn(out NitroxId fireId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Temporary packet limiter
|
||||
if (!fireDouseAmount.ContainsKey(fireId))
|
||||
{
|
||||
fireDouseAmount.Add(fireId, douseAmount);
|
||||
}
|
||||
else
|
||||
{
|
||||
float summedDouseAmount = fireDouseAmount[fireId] + douseAmount;
|
||||
|
||||
if (summedDouseAmount > FIRE_DOUSE_AMOUNT_TRIGGER)
|
||||
{
|
||||
// It is significantly faster to keep the key as a 0 value than to remove it and re-add it later.
|
||||
fireDouseAmount[fireId] = 0;
|
||||
|
||||
FireDoused packet = new FireDoused(fireId, douseAmount);
|
||||
packetSender.Send(packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new <see cref="Fire"/>. Majority of code copied from <see cref="SubFire.CreateFire(SubFire.RoomFire)"/>. Currently does not support Fires created outside of a Cyclops
|
||||
/// </summary>
|
||||
public void Create(CyclopsFireData fireData)
|
||||
{
|
||||
SubFire subFire = NitroxEntity.RequireObjectFrom(fireData.CyclopsId).GetComponent<SubRoot>().damageManager.subFire;
|
||||
Dictionary<CyclopsRooms, SubFire.RoomFire> roomFiresDict = subFire.roomFires;
|
||||
// Copied from SubFire_CreateFire_Patch, which copies from SubFire.CreateFire()
|
||||
Transform transform2 = roomFiresDict[fireData.Room].spawnNodes[fireData.NodeIndex];
|
||||
|
||||
// If a fire already exists at the node, replace the old Id with the new one
|
||||
if (transform2.childCount > 0)
|
||||
{
|
||||
Fire existingFire = transform2.GetComponentInChildren<Fire>();
|
||||
|
||||
if (existingFire.TryGetNitroxId(out NitroxId existingFireId) && existingFireId != fireData.CyclopsId)
|
||||
{
|
||||
Log.Error($"[Fires.Create Fire already exists at node index {fireData.NodeIndex}! Replacing existing Fire Id {existingFireId} with Id {fireData.CyclopsId}]");
|
||||
|
||||
NitroxEntity.SetNewId(existingFire.gameObject, fireData.CyclopsId);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
List<Transform> availableNodes = subFire.availableNodes;
|
||||
availableNodes.Clear();
|
||||
foreach (Transform transform in roomFiresDict[fireData.Room].spawnNodes)
|
||||
{
|
||||
if (transform.childCount == 0)
|
||||
{
|
||||
availableNodes.Add(transform);
|
||||
}
|
||||
}
|
||||
|
||||
roomFiresDict[fireData.Room].fireValue++;
|
||||
PrefabSpawn component = transform2.GetComponent<PrefabSpawn>();
|
||||
if (!component)
|
||||
{
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error(
|
||||
$"[{nameof(CyclopsFireCreatedProcessor)} Cannot create new Cyclops fire! PrefabSpawn component could not be found in fire node! Fire Id: {fireData.FireId} SubRoot Id: {fireData.CyclopsId} Room: {fireData.Room} NodeIndex: {fireData.NodeIndex}]");
|
||||
}
|
||||
|
||||
component.SpawnManual(delegate(GameObject fireGO)
|
||||
{
|
||||
Fire componentInChildren = fireGO.GetComponentInChildren<Fire>();
|
||||
if (componentInChildren)
|
||||
{
|
||||
componentInChildren.fireSubRoot = subFire.subRoot;
|
||||
NitroxEntity.SetNewId(componentInChildren.gameObject, fireData.FireId);
|
||||
}
|
||||
});
|
||||
|
||||
subFire.roomFires = roomFiresDict;
|
||||
subFire.availableNodes = availableNodes;
|
||||
}
|
||||
}
|
||||
}
|
17
NitroxClient/GameLogic/HUD/Components/ButtonTooltip.cs
Normal file
17
NitroxClient/GameLogic/HUD/Components/ButtonTooltip.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.HUD.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Shows the ToolipText when hovering the GameObject containg this component
|
||||
/// </summary>
|
||||
public class ButtonTooltip : MonoBehaviour, ITooltip
|
||||
{
|
||||
public string TooltipText { get; set; }
|
||||
public bool showTooltipOnDrag => false;
|
||||
|
||||
public void GetTooltip(TooltipData tooltip)
|
||||
{
|
||||
tooltip.prefix.Append(TooltipText);
|
||||
}
|
||||
}
|
39
NitroxClient/GameLogic/HUD/NitroxPDATab.cs
Normal file
39
NitroxClient/GameLogic/HUD/NitroxPDATab.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
namespace NitroxClient.GameLogic.HUD;
|
||||
|
||||
public abstract class NitroxPDATab
|
||||
{
|
||||
/// <summary>
|
||||
/// Text showing up when hovering the tab icon
|
||||
/// </summary>
|
||||
public abstract string ToolbarTip { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Asset name for the tab's icon sprite
|
||||
/// </summary>
|
||||
public abstract string TabIconAssetName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Base game tab from which we will take the icon as a placeholder
|
||||
/// </summary>
|
||||
public abstract PDATab FallbackTabIcon { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The uGUI_PDATab component that will be used in-game
|
||||
/// </summary>
|
||||
public abstract uGUI_PDATab uGUI_PDATab { get;}
|
||||
|
||||
/// <summary>
|
||||
/// Should be a new int value that isn't currently used by default game (>7) nor by another custom tab
|
||||
/// </summary>
|
||||
public abstract PDATab PDATabId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Create uGUI_PDATab component thanks to the now existing uGUI_PDA component
|
||||
/// </summary>
|
||||
public abstract void OnInitializePDA(uGUI_PDA uGUI_PDA);
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to render the pings images over the PDA when this PDA tab is open
|
||||
/// </summary>
|
||||
public abstract bool KeepPingsVisible { get; }
|
||||
}
|
43
NitroxClient/GameLogic/HUD/NitroxPDATabManager.cs
Normal file
43
NitroxClient/GameLogic/HUD/NitroxPDATabManager.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NitroxClient.GameLogic.HUD;
|
||||
|
||||
public class NitroxPDATabManager
|
||||
{
|
||||
public readonly Dictionary<PDATab, NitroxPDATab> CustomTabs = new();
|
||||
|
||||
private readonly Dictionary<string, Atlas.Sprite> tabSpritesByName = new();
|
||||
private readonly Dictionary<string, TabSpriteLoadedEvent> spriteLoadedCallbackByName = new();
|
||||
|
||||
public NitroxPDATabManager()
|
||||
{
|
||||
void RegisterTab(NitroxPDATab nitroxTab)
|
||||
{
|
||||
CustomTabs.Add(nitroxTab.PDATabId, nitroxTab);
|
||||
}
|
||||
|
||||
RegisterTab(new PlayerListTab());
|
||||
}
|
||||
|
||||
public void AddTabSprite(string spriteName, Atlas.Sprite sprite)
|
||||
{
|
||||
tabSpritesByName.Add(spriteName, sprite);
|
||||
if (spriteLoadedCallbackByName.TryGetValue(spriteName, out TabSpriteLoadedEvent spriteLoadedEvent))
|
||||
{
|
||||
spriteLoadedEvent.Invoke(sprite);
|
||||
spriteLoadedCallbackByName.Remove(spriteName);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetTabSprite(string spriteName, out Atlas.Sprite sprite) => tabSpritesByName.TryGetValue(spriteName, out sprite);
|
||||
|
||||
public delegate void TabSpriteLoadedEvent(Atlas.Sprite sprite);
|
||||
|
||||
public void SetSpriteLoadedCallback(string tabName, TabSpriteLoadedEvent callback)
|
||||
{
|
||||
if (!spriteLoadedCallbackByName.ContainsKey(tabName))
|
||||
{
|
||||
spriteLoadedCallbackByName.Add(tabName, callback);
|
||||
}
|
||||
}
|
||||
}
|
244
NitroxClient/GameLogic/HUD/PdaTabs/uGUI_PlayerListTab.cs
Normal file
244
NitroxClient/GameLogic/HUD/PdaTabs/uGUI_PlayerListTab.cs
Normal file
@@ -0,0 +1,244 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxClient.GameLogic.HUD.Components;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract;
|
||||
using NitroxModel.Core;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using static NitroxClient.Unity.Helper.AssetBundleLoader;
|
||||
|
||||
namespace NitroxClient.GameLogic.HUD.PdaTabs;
|
||||
|
||||
/// <summary>
|
||||
/// The component containing a new PDA tab based on ping manager tab
|
||||
/// </summary>
|
||||
public class uGUI_PlayerListTab : uGUI_PingTab
|
||||
{
|
||||
private NitroxPDATabManager nitroxPDATabManager;
|
||||
private PlayerManager playerManager;
|
||||
private LocalPlayer localPlayer;
|
||||
private IPacketSender packetSender;
|
||||
|
||||
private readonly Dictionary<string, Sprite> assets = new();
|
||||
public bool FinishedLoadingAssets { get; private set; }
|
||||
|
||||
private new readonly Dictionary<string, uGUI_PlayerPingEntry> entries = new();
|
||||
private PrefabPool<uGUI_PlayerPingEntry> pool;
|
||||
private new readonly Dictionary<string, uGUI_PlayerPingEntry> tempSort = new();
|
||||
|
||||
public override void Awake()
|
||||
{
|
||||
// Copied from uGUI_PingTab.Awake but we don't want it to be executed because it creates a PrefabPool
|
||||
selectableVisibilityToggle = new SelectableWrapper(visibilityToggle, delegate (GameInput.Button button)
|
||||
{
|
||||
if (button == GameInput.Button.UISubmit)
|
||||
{
|
||||
visibilityToggle.isOn = !visibilityToggle.isOn;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
nitroxPDATabManager = NitroxServiceLocator.LocateService<NitroxPDATabManager>();
|
||||
playerManager = NitroxServiceLocator.LocateService<PlayerManager>();
|
||||
localPlayer = NitroxServiceLocator.LocateService<LocalPlayer>();
|
||||
packetSender = NitroxServiceLocator.LocateService<IPacketSender>();
|
||||
// Need to reassign manually these variables and get rid of the objects we don't need
|
||||
content = gameObject.FindChild("Content").GetComponent<CanvasGroup>();
|
||||
pingManagerLabel = gameObject.GetComponentInChildren<TextMeshProUGUI>();
|
||||
scrollRect = gameObject.GetComponentInChildren<ScrollRect>();
|
||||
pingCanvas = (RectTransform)content.transform.Find("ScrollView/Viewport/ScrollCanvas");
|
||||
|
||||
pool = new PrefabPool<uGUI_PlayerPingEntry>(prefabEntry, pingCanvas, 8, 4, delegate (uGUI_PlayerPingEntry entry)
|
||||
{
|
||||
entry.Uninitialize();
|
||||
}, delegate (uGUI_PlayerPingEntry entry)
|
||||
{
|
||||
entry.Uninitialize();
|
||||
});
|
||||
}
|
||||
|
||||
public IEnumerator Start()
|
||||
{
|
||||
Transform buttonAll = content.transform.Find("ButtonAll");
|
||||
DestroyImmediate(buttonAll.gameObject);
|
||||
|
||||
yield return LoadAllAssets(NitroxAssetBundle.PLAYER_LIST_TAB);
|
||||
|
||||
foreach (Object asset in NitroxAssetBundle.PLAYER_LIST_TAB.LoadedAssets)
|
||||
{
|
||||
if (asset is Sprite sprite)
|
||||
{
|
||||
if (asset.name.Equals("player_list_tab@3x"))
|
||||
{
|
||||
nitroxPDATabManager.AddTabSprite(asset.name, new Atlas.Sprite(sprite));
|
||||
}
|
||||
assets.Add(asset.name, sprite);
|
||||
}
|
||||
}
|
||||
|
||||
FinishedLoadingAssets = true;
|
||||
_isDirty = true;
|
||||
}
|
||||
|
||||
public Sprite GetSprite(string assetName)
|
||||
{
|
||||
if (assets.TryGetValue(assetName, out Sprite sprite))
|
||||
{
|
||||
return sprite;
|
||||
}
|
||||
return Sprite.Create(new Texture2D(100, 100), new Rect(0, 0, 100, 100), new Vector2(50, 50), 100);
|
||||
}
|
||||
|
||||
public new void OnEnable()
|
||||
{
|
||||
// Enter events for player join and disconnect
|
||||
playerManager.OnCreate += OnAdd;
|
||||
playerManager.OnRemove += OnRemove;
|
||||
}
|
||||
|
||||
public new void OnDestroy()
|
||||
{
|
||||
playerManager.OnCreate -= OnAdd;
|
||||
playerManager.OnRemove -= OnRemove;
|
||||
}
|
||||
|
||||
public override void OnLanguageChanged()
|
||||
{
|
||||
pingManagerLabel.text = Language.main.Get("Nitrox_PlayerListTabName");
|
||||
entries.Values.ForEach(entry => entry.OnLanguageChanged());
|
||||
}
|
||||
|
||||
public override void OnLateUpdate(bool _)
|
||||
{
|
||||
UpdateEntries();
|
||||
}
|
||||
|
||||
public new void UpdateEntries()
|
||||
{
|
||||
if (!_isDirty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_isDirty = false;
|
||||
|
||||
Dictionary<string, INitroxPlayer> players = playerManager.GetAll().ToDictionary<RemotePlayer, string, INitroxPlayer>(player => player.PlayerId.ToString(), player => player);
|
||||
players.Add(localPlayer.PlayerId.ToString(), localPlayer);
|
||||
|
||||
foreach (KeyValuePair<string, INitroxPlayer> entry in players)
|
||||
{
|
||||
if (!entries.ContainsKey(entry.Key))
|
||||
{
|
||||
// Sets up a new entry for the player
|
||||
AddNewEntry(entry.Key, entry.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort the items by alphabetical order (based on SN's code)
|
||||
tempSort.Clear();
|
||||
foreach (KeyValuePair<string, uGUI_PlayerPingEntry> entry in entries)
|
||||
{
|
||||
if (!entry.Value.IsLocalPlayer)
|
||||
{
|
||||
tempSort.Add(entry.Value.PlayerName, entry.Value);
|
||||
}
|
||||
}
|
||||
|
||||
List<string> sorted = new(tempSort.Keys);
|
||||
sorted.Sort();
|
||||
|
||||
entries[localPlayer.PlayerId.ToString()].rectTransform.SetSiblingIndex(0);
|
||||
for (int j = 0; j < sorted.Count; j++)
|
||||
{
|
||||
string id = tempSort[sorted[j]].id;
|
||||
entries[id].rectTransform.SetSiblingIndex(j + 1);
|
||||
}
|
||||
}
|
||||
|
||||
public uGUI_PlayerPingEntry GetEntry()
|
||||
{
|
||||
uGUI_PlayerPingEntry uGUI_PlayerEntry;
|
||||
if (pool.pool.Count == 0)
|
||||
{
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
uGUI_PlayerEntry = Instantiate(prefabEntry).GetComponent<uGUI_PlayerPingEntry>();
|
||||
uGUI_PlayerEntry.rectTransform.SetParent(pingCanvas, false);
|
||||
uGUI_PlayerEntry.Uninitialize();
|
||||
pool.pool.Add(uGUI_PlayerEntry);
|
||||
}
|
||||
}
|
||||
int index = pool.pool.Count - 1;
|
||||
uGUI_PlayerEntry = pool.pool[index];
|
||||
pool.pool.RemoveAt(index);
|
||||
return uGUI_PlayerEntry;
|
||||
}
|
||||
|
||||
public void MakePrefab(GameObject basePrefab)
|
||||
{
|
||||
// We need to instantiate the prefab as we cannot directly make modifications in it
|
||||
GameObject newPrefab = Instantiate(basePrefab);
|
||||
newPrefab.name = "PlayerEntry";
|
||||
// We never want this to appear
|
||||
DestroyImmediate(newPrefab.FindChild("ColorToggle"));
|
||||
|
||||
// Need to modify the pingTab's script from uGUI_PingEntry to uGUI_PlayerEntry
|
||||
uGUI_PingEntry pingEntry = newPrefab.GetComponent<uGUI_PingEntry>();
|
||||
uGUI_PlayerPingEntry playerEntry = newPrefab.AddComponent<uGUI_PlayerPingEntry>();
|
||||
playerEntry.visibility = pingEntry.visibility;
|
||||
playerEntry.visibilityIcon = pingEntry.visibilityIcon;
|
||||
playerEntry.icon = pingEntry.icon;
|
||||
playerEntry.label = pingEntry.label;
|
||||
playerEntry._rectTransform = pingEntry._rectTransform;
|
||||
playerEntry.id = pingEntry.id;
|
||||
playerEntry.spriteVisible = pingEntry.spriteVisible;
|
||||
playerEntry.spriteHidden = pingEntry.spriteHidden;
|
||||
DestroyImmediate(pingEntry);
|
||||
|
||||
// Make buttons for mute, kick, tp
|
||||
Transform container = newPrefab.transform;
|
||||
playerEntry.ShowObject = newPrefab.FindChild("ButtonVisibility");
|
||||
playerEntry.ShowObject.AddComponent<ButtonTooltip>();
|
||||
|
||||
playerEntry.MuteObject = Instantiate(playerEntry.ShowObject, container);
|
||||
playerEntry.KickObject = Instantiate(playerEntry.ShowObject, container);
|
||||
playerEntry.TeleportToObject = Instantiate(playerEntry.ShowObject, container);
|
||||
playerEntry.TeleportToMeObject = Instantiate(playerEntry.ShowObject, container);
|
||||
playerEntry.MuteObject.name = "MuteObject";
|
||||
playerEntry.KickObject.name = "KickObject";
|
||||
playerEntry.TeleportToObject.name = "TeleportToObject";
|
||||
playerEntry.TeleportToMeObject.name = "TeleportToMeObject";
|
||||
|
||||
prefabEntry = newPrefab;
|
||||
}
|
||||
|
||||
private void AddNewEntry(string playerId, INitroxPlayer player)
|
||||
{
|
||||
uGUI_PlayerPingEntry entry = GetEntry();
|
||||
entry.Initialize(playerId, player.PlayerName, this);
|
||||
entry.UpdateEntryForNewPlayer(player, localPlayer, packetSender);
|
||||
entries.Add(playerId, entry);
|
||||
}
|
||||
|
||||
private void OnAdd(ushort playerId, RemotePlayer remotePlayer)
|
||||
{
|
||||
_isDirty = true;
|
||||
}
|
||||
|
||||
private void OnRemove(ushort playerId, RemotePlayer remotePlayers)
|
||||
{
|
||||
string playerIdString = playerId.ToString();
|
||||
if (!entries.ContainsKey(playerIdString))
|
||||
{
|
||||
return;
|
||||
}
|
||||
uGUI_PlayerPingEntry entry = entries[playerIdString];
|
||||
entries.Remove(playerIdString);
|
||||
pool.Release(entry);
|
||||
_isDirty = true;
|
||||
}
|
||||
}
|
244
NitroxClient/GameLogic/HUD/PdaTabs/uGUI_PlayerPingEntry.cs
Normal file
244
NitroxClient/GameLogic/HUD/PdaTabs/uGUI_PlayerPingEntry.cs
Normal file
@@ -0,0 +1,244 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxClient.Communication.Packets.Processors;
|
||||
using NitroxClient.GameLogic.HUD.Components;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract;
|
||||
using NitroxClient.MonoBehaviours.Gui.Modals;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using NitroxModel.Core;
|
||||
using NitroxModel.DataStructures.GameLogic;
|
||||
using NitroxModel.Packets;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UWE;
|
||||
|
||||
namespace NitroxClient.GameLogic.HUD.PdaTabs;
|
||||
|
||||
public class uGUI_PlayerPingEntry : uGUI_PingEntry
|
||||
{
|
||||
private uGUI_PlayerListTab parent;
|
||||
private INitroxPlayer player;
|
||||
|
||||
public string PlayerName => player?.PlayerName ?? string.Empty;
|
||||
public bool IsLocalPlayer => player is LocalPlayer;
|
||||
private bool showPing;
|
||||
|
||||
private bool muted
|
||||
{
|
||||
get
|
||||
{
|
||||
if (player is RemotePlayer remotePlayer && remotePlayer.PlayerContext != null)
|
||||
{
|
||||
return remotePlayer.PlayerContext.IsMuted;
|
||||
}
|
||||
// By default we don't care about the local state
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public GameObject ShowObject;
|
||||
public GameObject MuteObject;
|
||||
public GameObject KickObject;
|
||||
public GameObject TeleportToObject;
|
||||
public GameObject TeleportToMeObject;
|
||||
|
||||
public Sprite MutedSprite;
|
||||
public Sprite UnmutedSprite;
|
||||
public Sprite KickSprite;
|
||||
public Sprite TeleportToSprite;
|
||||
public Sprite TeleportToMeSprite;
|
||||
|
||||
public new void Awake()
|
||||
{
|
||||
NitroxServiceLocator.LocateService<MutePlayerProcessor>().OnPlayerMuted += (playerId, _) =>
|
||||
{
|
||||
if (player is RemotePlayer remotePlayer && remotePlayer.PlayerId == playerId)
|
||||
{
|
||||
RefreshMuteButton();
|
||||
}
|
||||
};
|
||||
NitroxServiceLocator.LocateService<PermsChangedProcessor>().OnPermissionsChanged += (perms) => RefreshButtonsVisibility();
|
||||
}
|
||||
|
||||
public IEnumerator Start()
|
||||
{
|
||||
// We must one frame so that the UI elements are initialized properly
|
||||
yield return null;
|
||||
// This action must happen after the yield so that they're correctly placed
|
||||
UpdateButtonsPosition();
|
||||
// We trigger it at least once so that the localizations are updated with the PlayerName
|
||||
OnLanguageChanged();
|
||||
}
|
||||
|
||||
public void Initialize(string id, string name, uGUI_PlayerListTab parent)
|
||||
{
|
||||
this.id = id;
|
||||
this.parent = parent;
|
||||
|
||||
gameObject.SetActive(true);
|
||||
visibilityIcon.sprite = spriteVisible;
|
||||
icon.SetForegroundSprite(SpriteManager.Get(SpriteManager.Group.Tab, "TabInventory"));
|
||||
showPing = true;
|
||||
|
||||
UpdateLabel(name);
|
||||
OnLanguageChanged();
|
||||
|
||||
CoroutineHost.StartCoroutine(AssignSprites());
|
||||
}
|
||||
|
||||
public void OnLanguageChanged()
|
||||
{
|
||||
GetTooltip(ShowObject).TooltipText = GetLocalizedText(showPing ? "Nitrox_HidePing" : "Nitrox_ShowPing");
|
||||
GetTooltip(MuteObject).TooltipText = GetLocalizedText(muted ? "Nitrox_Unmute" : "Nitrox_Mute");
|
||||
GetTooltip(KickObject).TooltipText = GetLocalizedText("Nitrox_Kick");
|
||||
GetTooltip(TeleportToObject).TooltipText = GetLocalizedText("Nitrox_TeleportTo");
|
||||
GetTooltip(TeleportToMeObject).TooltipText = GetLocalizedText("Nitrox_TeleportToMe");
|
||||
}
|
||||
|
||||
public new void Uninitialize()
|
||||
{
|
||||
base.Uninitialize();
|
||||
player = null;
|
||||
}
|
||||
|
||||
public void UpdateLabel(string text)
|
||||
{
|
||||
label.text = text;
|
||||
}
|
||||
|
||||
public void UpdateEntryForNewPlayer(INitroxPlayer newPlayer, LocalPlayer localPlayer, IPacketSender packetSender)
|
||||
{
|
||||
player = newPlayer;
|
||||
|
||||
UpdateLabel(player.PlayerName);
|
||||
Color playerColor = player.PlayerSettings.PlayerColor.ToUnity();
|
||||
icon.SetColors(playerColor, playerColor, playerColor);
|
||||
RefreshMuteButton();
|
||||
|
||||
// We need to update each button's listener whether or not they have enough perms because they may become OP during playtime
|
||||
ClearButtonListeners();
|
||||
|
||||
GetToggle(ShowObject).onValueChanged.AddListener(delegate (bool toggled)
|
||||
{
|
||||
if (player is RemotePlayer remotePlayer)
|
||||
{
|
||||
PingInstance pingInstance = remotePlayer.PlayerModel.GetComponentInChildren<PingInstance>();
|
||||
pingInstance.SetVisible(toggled);
|
||||
GetTooltip(ShowObject).TooltipText = GetLocalizedText(toggled ? "Nitrox_HidePing" : "Nitrox_ShowPing");
|
||||
visibilityIcon.sprite = toggled ? spriteVisible : spriteHidden;
|
||||
}
|
||||
});
|
||||
// Each of those clicks involves a confirmation modal
|
||||
GetToggle(MuteObject).onValueChanged.AddListener(delegate (bool toggled)
|
||||
{
|
||||
Modal.Get<ConfirmModal>()?.Show(GetLocalizedText(muted ? "Nitrox_Unmute" : "Nitrox_Mute", true), () =>
|
||||
{
|
||||
GetToggle(MuteObject).SetIsOnWithoutNotify(!toggled);
|
||||
if (player is RemotePlayer remotePlayer)
|
||||
{
|
||||
packetSender.Send(new ServerCommand($"{(toggled ? "" : "un")}mute {player.PlayerName}"));
|
||||
}
|
||||
});
|
||||
});
|
||||
GetToggle(KickObject).onValueChanged.AddListener(delegate (bool toggled)
|
||||
{
|
||||
Modal.Get<ConfirmModal>()?.Show(GetLocalizedText("Nitrox_Kick", true), () =>
|
||||
{
|
||||
packetSender.Send(new ServerCommand($"kick {player.PlayerName}"));
|
||||
});
|
||||
});
|
||||
GetToggle(TeleportToObject).onValueChanged.AddListener(delegate (bool toggled)
|
||||
{
|
||||
Modal.Get<ConfirmModal>()?.Show(GetLocalizedText("Nitrox_TeleportTo", true), () =>
|
||||
{
|
||||
packetSender.Send(new ServerCommand($"warp {player.PlayerName}"));
|
||||
});
|
||||
});
|
||||
GetToggle(TeleportToMeObject).onValueChanged.AddListener(delegate (bool toggled)
|
||||
{
|
||||
Modal.Get<ConfirmModal>()?.Show(GetLocalizedText("Nitrox_TeleportToMe", true), () =>
|
||||
{
|
||||
packetSender.Send(new ServerCommand($"warp {player.PlayerName} {localPlayer.PlayerName}"));
|
||||
});
|
||||
});
|
||||
|
||||
RefreshButtonsVisibility();
|
||||
}
|
||||
|
||||
private string GetLocalizedText(string key, bool isQuestion = false)
|
||||
{
|
||||
return Language.main.Get(isQuestion ? $"{key}Question" : key).Replace("{PLAYER}", PlayerName);
|
||||
}
|
||||
|
||||
public void UpdateButtonsPosition()
|
||||
{
|
||||
float OFFSET = 0f;
|
||||
List<GameObject> buttonsToAlign = new() { MuteObject, KickObject, TeleportToObject, TeleportToMeObject };
|
||||
foreach (GameObject buttonObject in buttonsToAlign)
|
||||
{
|
||||
RectTransform buttonRect = buttonObject.GetComponent<RectTransform>();
|
||||
buttonRect.anchoredPosition = new Vector2(rectTransform.anchoredPosition.x, 0f);
|
||||
buttonRect.localPosition = new Vector2(OFFSET, 0);
|
||||
OFFSET += 80f;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearButtonListeners()
|
||||
{
|
||||
GetToggle(MuteObject).onValueChanged = new Toggle.ToggleEvent();
|
||||
GetToggle(KickObject).onValueChanged = new Toggle.ToggleEvent();
|
||||
GetToggle(TeleportToObject).onValueChanged = new Toggle.ToggleEvent();
|
||||
GetToggle(TeleportToMeObject).onValueChanged = new Toggle.ToggleEvent();
|
||||
}
|
||||
|
||||
private IEnumerator AssignSprites()
|
||||
{
|
||||
yield return new WaitUntil(() => parent.FinishedLoadingAssets);
|
||||
|
||||
// NB: Those textures MUST be exported with a Texture Type of "Sprite (2D and UI)", else they will look blurry not matter what
|
||||
// NB 2: Those textures for the buttons are scaled 68x61 but the image inside but not hit the borders to have a better render
|
||||
MutedSprite = parent.GetSprite("muted@3x");
|
||||
UnmutedSprite = parent.GetSprite("unmuted@3x");
|
||||
KickSprite = parent.GetSprite("kick@3x");
|
||||
TeleportToSprite = parent.GetSprite("teleport_to@3x");
|
||||
TeleportToMeSprite = parent.GetSprite("teleport_to_me@3x");
|
||||
|
||||
MuteObject.FindChild("Eye").GetComponent<Image>().sprite = muted ? MutedSprite : UnmutedSprite;
|
||||
KickObject.FindChild("Eye").GetComponent<Image>().sprite = KickSprite;
|
||||
TeleportToObject.FindChild("Eye").GetComponent<Image>().sprite = TeleportToSprite;
|
||||
TeleportToMeObject.FindChild("Eye").GetComponent<Image>().sprite = TeleportToMeSprite;
|
||||
}
|
||||
|
||||
private void RefreshMuteButton()
|
||||
{
|
||||
GetToggle(MuteObject).SetIsOnWithoutNotify(muted);
|
||||
GetTooltip(MuteObject).TooltipText = GetLocalizedText(muted ? "Nitrox_Unmute" : "Nitrox_Mute");
|
||||
MuteObject.FindChild("Eye").GetComponent<Image>().sprite = muted ? MutedSprite : UnmutedSprite;
|
||||
}
|
||||
|
||||
private void RefreshButtonsVisibility()
|
||||
{
|
||||
LocalPlayer localPlayer = NitroxServiceLocator.LocateService<LocalPlayer>();
|
||||
|
||||
bool isNotLocalPlayer = !IsLocalPlayer;
|
||||
// We don't want any control buttons to appear for the local player
|
||||
ShowObject.SetActive(isNotLocalPlayer);
|
||||
|
||||
// The perms here should be the same as the perm each command asks for
|
||||
MuteObject.SetActive(isNotLocalPlayer && localPlayer.Permissions >= Perms.MODERATOR);
|
||||
KickObject.SetActive(isNotLocalPlayer && localPlayer.Permissions >= Perms.MODERATOR);
|
||||
TeleportToObject.SetActive(isNotLocalPlayer && localPlayer.Permissions >= Perms.MODERATOR);
|
||||
TeleportToMeObject.SetActive(isNotLocalPlayer && localPlayer.Permissions >= Perms.MODERATOR);
|
||||
}
|
||||
|
||||
private Toggle GetToggle(GameObject gameObject)
|
||||
{
|
||||
return gameObject.GetComponent<Toggle>();
|
||||
}
|
||||
|
||||
private ButtonTooltip GetTooltip(GameObject gameObject)
|
||||
{
|
||||
return gameObject.GetComponent<ButtonTooltip>();
|
||||
}
|
||||
}
|
42
NitroxClient/GameLogic/HUD/PlayerListTab.cs
Normal file
42
NitroxClient/GameLogic/HUD/PlayerListTab.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using NitroxClient.GameLogic.HUD.PdaTabs;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.HUD;
|
||||
|
||||
public class PlayerListTab : NitroxPDATab
|
||||
{
|
||||
private uGUI_PDATab tab;
|
||||
|
||||
public override string ToolbarTip => "Nitrox_PlayerListTabName";
|
||||
|
||||
public override string TabIconAssetName => "player_list_tab@3x";
|
||||
|
||||
public override PDATab FallbackTabIcon => PDATab.Inventory;
|
||||
|
||||
public override uGUI_PDATab uGUI_PDATab => tab;
|
||||
|
||||
public override PDATab PDATabId => (PDATab)8;
|
||||
|
||||
public override void OnInitializePDA(uGUI_PDA uGUI_PDA)
|
||||
{
|
||||
// We need to copy the ping manager tab which is the closest to what we want
|
||||
GameObject pdaScreen = uGUI_PDA.gameObject;
|
||||
uGUI_PingTab pingTab = pdaScreen.GetComponentInChildren<uGUI_PingTab>();
|
||||
GameObject pingTabObject = pingTab.gameObject;
|
||||
|
||||
GameObject tabCopy = GameObject.Instantiate(pingTabObject, pdaScreen.transform.Find("Content"));
|
||||
tabCopy.name = "PlayerListTab";
|
||||
|
||||
// Set the tab inactive to suppress the uGUI_PlayerListTab awake(). We first need to set the
|
||||
// prefab, which is used inside the awake method and the base class awake method.
|
||||
tabCopy.SetActive(false);
|
||||
uGUI_PlayerListTab newTab = tabCopy.AddComponent<uGUI_PlayerListTab>();
|
||||
newTab.MakePrefab(pingTab.prefabEntry);
|
||||
GameObject.DestroyImmediate(tabCopy.GetComponent<uGUI_PingTab>());
|
||||
tabCopy.SetActive(true);
|
||||
|
||||
tab = newTab;
|
||||
}
|
||||
|
||||
public override bool KeepPingsVisible => true;
|
||||
}
|
33
NitroxClient/GameLogic/HUD/PlayerVitalsManager.cs
Normal file
33
NitroxClient/GameLogic/HUD/PlayerVitalsManager.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System.Collections.Generic;
|
||||
using NitroxClient.MonoBehaviours.Gui.HUD;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.HUD;
|
||||
|
||||
public class PlayerVitalsManager
|
||||
{
|
||||
private readonly Dictionary<ushort, RemotePlayerVitals> vitalsByPlayerId = new();
|
||||
|
||||
public RemotePlayerVitals CreateOrFindForPlayer(RemotePlayer remotePlayer)
|
||||
{
|
||||
if (!vitalsByPlayerId.TryGetValue(remotePlayer.PlayerId, out RemotePlayerVitals vitals))
|
||||
{
|
||||
vitalsByPlayerId[remotePlayer.PlayerId] = vitals = RemotePlayerVitals.CreateForPlayer(remotePlayer);
|
||||
}
|
||||
return vitals;
|
||||
}
|
||||
|
||||
public void RemoveForPlayer(ushort playerId)
|
||||
{
|
||||
if (vitalsByPlayerId.TryGetValue(playerId, out RemotePlayerVitals vitals))
|
||||
{
|
||||
vitalsByPlayerId.Remove(playerId);
|
||||
Object.Destroy(vitals.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryFindForPlayer(ushort playerId, out RemotePlayerVitals vitals)
|
||||
{
|
||||
return vitalsByPlayerId.TryGetValue(playerId, out vitals);
|
||||
}
|
||||
}
|
104
NitroxClient/GameLogic/Helper/BaseSerializationHelper.cs
Normal file
104
NitroxClient/GameLogic/Helper/BaseSerializationHelper.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
|
||||
namespace NitroxClient.GameLogic.Helper;
|
||||
|
||||
public static class BaseSerializationHelper
|
||||
{
|
||||
public static byte[] CompressBytes(byte[] array)
|
||||
{
|
||||
if (array == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
using MemoryStream output = new();
|
||||
using DeflateStream stream = new(output, CompressionLevel.Optimal);
|
||||
CompressStream(stream, array);
|
||||
stream.Close();
|
||||
return output.ToArray();
|
||||
}
|
||||
|
||||
public static byte[] DecompressBytes(byte[] array, int size)
|
||||
{
|
||||
if (array == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
using MemoryStream input = new(array);
|
||||
using DeflateStream stream = new(input, CompressionMode.Decompress);
|
||||
return DecompressStream(stream, size);
|
||||
}
|
||||
|
||||
public static void CompressStream(Stream stream, byte[] array)
|
||||
{
|
||||
using BinaryWriter writer = new(stream);
|
||||
|
||||
ushort zeroCounter = 0;
|
||||
foreach (byte value in array)
|
||||
{
|
||||
if (value == 0 && zeroCounter != ushort.MaxValue)
|
||||
{
|
||||
zeroCounter++;
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.Write(zeroCounter);
|
||||
writer.Write(value);
|
||||
zeroCounter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (zeroCounter != 0)
|
||||
{
|
||||
writer.Write(zeroCounter);
|
||||
}
|
||||
|
||||
writer.Close();
|
||||
}
|
||||
|
||||
public static byte[] DecompressStream(Stream stream, int size)
|
||||
{
|
||||
using BinaryReader reader = new(stream);
|
||||
byte[] result = new byte[size];
|
||||
|
||||
int i = 0;
|
||||
bool zeroPart = true;
|
||||
while (i < size)
|
||||
{
|
||||
if (zeroPart)
|
||||
{
|
||||
ushort zeroLength = reader.ReadUInt16();
|
||||
|
||||
for (int c = 0; c < zeroLength; c++)
|
||||
{
|
||||
result[i] = 0;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result[i] = reader.ReadByte();
|
||||
i++;
|
||||
}
|
||||
|
||||
zeroPart = !zeroPart;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static byte[] CompressData<TInput>(TInput[] array, Converter<TInput, byte> converter)
|
||||
{
|
||||
return CompressBytes(Array.ConvertAll(array, converter));
|
||||
}
|
||||
|
||||
public static TInput[] DecompressData<TInput>(byte[] array, int size, Converter<byte, TInput> converter)
|
||||
{
|
||||
if (array == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return Array.ConvertAll(DecompressBytes(array, size), converter);
|
||||
}
|
||||
}
|
37
NitroxClient/GameLogic/Helper/BatteryChildEntityHelper.cs
Normal file
37
NitroxClient/GameLogic/Helper/BatteryChildEntityHelper.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using NitroxModel.DataStructures.GameLogic.Entities;
|
||||
using NitroxModel.DataStructures.GameLogic;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using System.Collections.Generic;
|
||||
using UWE;
|
||||
using UnityEngine;
|
||||
using System;
|
||||
using NitroxModel.Core;
|
||||
|
||||
namespace NitroxClient.GameLogic.Helper;
|
||||
|
||||
/// <summary>
|
||||
/// Vehicles and items are created without a battery loaded into them. Subnautica usually spawns these in async; however, this
|
||||
/// is disabled in nitrox so we can properly tag the id. Here we create the installed battery (with a new NitroxId) and have the
|
||||
/// entity spawner take care of loading it in.
|
||||
/// </summary>
|
||||
public static class BatteryChildEntityHelper
|
||||
{
|
||||
private static readonly Lazy<Entities> entities = new (() => NitroxServiceLocator.LocateService<Entities>());
|
||||
|
||||
public static void TryPopulateInstalledBattery(GameObject gameObject, List<Entity> toPopulate, NitroxId parentId)
|
||||
{
|
||||
if (gameObject.TryGetComponent(out EnergyMixin energyMixin))
|
||||
{
|
||||
PopulateInstalledBattery(energyMixin, toPopulate, parentId);
|
||||
}
|
||||
}
|
||||
|
||||
public static void PopulateInstalledBattery(EnergyMixin energyMixin, List<Entity> toPopulate, NitroxId parentId)
|
||||
{
|
||||
InstalledBatteryEntity installedBattery = new(new NitroxId(), energyMixin.defaultBattery.ToDto(), null, parentId, new List<Entity>());
|
||||
toPopulate.Add(installedBattery);
|
||||
|
||||
CoroutineHost.StartCoroutine(entities.Value.SpawnEntityAsync(installedBattery));
|
||||
}
|
||||
}
|
36
NitroxClient/GameLogic/Helper/EquipmentHelper.cs
Normal file
36
NitroxClient/GameLogic/Helper/EquipmentHelper.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NitroxModel.DataStructures.Util;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.Helper
|
||||
{
|
||||
public class EquipmentHelper
|
||||
{
|
||||
private static readonly List<Func<GameObject, Equipment>> equipmentFinders = new()
|
||||
{
|
||||
o => o.GetComponent<Charger>().AliveOrNull()?.equipment,
|
||||
o => o.GetComponent<BaseNuclearReactor>().AliveOrNull()?.equipment,
|
||||
o => o.GetComponent<CyclopsDecoyLoadingTube>().AliveOrNull()?.decoySlots,
|
||||
o => o.GetComponent<Exosuit>().AliveOrNull()?.modules,
|
||||
o => o.GetComponent<SeaMoth>().AliveOrNull()?.modules,
|
||||
o => o.GetComponent<UpgradeConsole>().AliveOrNull()?.modules,
|
||||
o => o.GetComponent<Vehicle>().AliveOrNull()?.modules,
|
||||
o => o.GetComponent<VehicleUpgradeConsoleInput>().AliveOrNull()?.equipment,
|
||||
o => string.Equals("Player", o.GetComponent<Player>().AliveOrNull()?.name, StringComparison.InvariantCulture) ? Inventory.main.equipment : null
|
||||
};
|
||||
|
||||
public static Optional<Equipment> FindEquipmentComponent(GameObject owner)
|
||||
{
|
||||
foreach (Func<GameObject, Equipment> equipmentFinder in equipmentFinders)
|
||||
{
|
||||
Equipment equipment = equipmentFinder(owner);
|
||||
if (equipment != null)
|
||||
{
|
||||
return Optional.Of(equipment);
|
||||
}
|
||||
}
|
||||
return Optional.Empty;
|
||||
}
|
||||
}
|
||||
}
|
106
NitroxClient/GameLogic/Helper/InventoryContainerHelper.cs
Normal file
106
NitroxClient/GameLogic/Helper/InventoryContainerHelper.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using NitroxClient.GameLogic.Bases;
|
||||
using NitroxClient.GameLogic.PlayerLogic;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.DataStructures.Util;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.Helper
|
||||
{
|
||||
public class InventoryContainerHelper
|
||||
{
|
||||
private static readonly Regex LockerRegex = new(@"Locker0([0-9])StorageRoot$", RegexOptions.IgnoreCase);
|
||||
private const string LOCKER_BASE_NAME = "submarine_locker_01_0";
|
||||
private const string PLAYER_OBJECT_NAME = "Player";
|
||||
private const string ESCAPEPOD_OBJECT_NAME = "EscapePod";
|
||||
|
||||
public static Optional<ItemsContainer> TryGetContainerByOwner(GameObject owner)
|
||||
{
|
||||
SeamothStorageContainer seamothStorageContainer = owner.GetComponent<SeamothStorageContainer>();
|
||||
if (seamothStorageContainer)
|
||||
{
|
||||
return Optional.Of(seamothStorageContainer.container);
|
||||
}
|
||||
StorageContainer storageContainer = owner.GetComponentInChildren<StorageContainer>(true);
|
||||
if (storageContainer)
|
||||
{
|
||||
return Optional.Of(storageContainer.container);
|
||||
}
|
||||
BaseBioReactor baseBioReactor = owner.GetComponentInChildren<BaseBioReactor>(true);
|
||||
if (baseBioReactor)
|
||||
{
|
||||
return Optional.Of(baseBioReactor.container);
|
||||
}
|
||||
if (owner.name == PLAYER_OBJECT_NAME)
|
||||
{
|
||||
return Optional.Of(Inventory.Get().container);
|
||||
}
|
||||
RemotePlayerIdentifier remotePlayerId = owner.GetComponent<RemotePlayerIdentifier>();
|
||||
if (remotePlayerId)
|
||||
{
|
||||
return Optional.Of(remotePlayerId.RemotePlayer.Inventory);
|
||||
}
|
||||
|
||||
return Optional.Empty;
|
||||
}
|
||||
|
||||
|
||||
public static bool TryGetOwnerId(Transform ownerTransform, out NitroxId ownerId)
|
||||
{
|
||||
Transform parent = ownerTransform.parent;
|
||||
if (!parent)
|
||||
{
|
||||
Log.Error("Trying to get the ownerId of a storage that doesn't have a parent");
|
||||
ownerId = null;
|
||||
return false;
|
||||
}
|
||||
// TODO: in the future maybe use a switch on the PrefabId (it's always the same structure in a prefab)
|
||||
// and then statically look for the right object because we'll know exactly which one it is
|
||||
|
||||
// To treat the WaterPark in parent case, we need its case to happen before the IBaseModule one because
|
||||
// IBaseModule will get the WaterPark but not get the id on the right object like in the first case
|
||||
if (parent.TryGetComponent(out WaterPark waterPark))
|
||||
{
|
||||
return waterPark.planter.TryGetIdOrWarn(out ownerId);
|
||||
}
|
||||
else if (parent.GetComponent<Constructable>() || parent.GetComponent<IBaseModule>().AliveOrNull())
|
||||
{
|
||||
return parent.TryGetIdOrWarn(out ownerId);
|
||||
}
|
||||
else if (parent.TryGetComponentInParent(out LargeRoomWaterPark largeRoomWaterPark, true) &&
|
||||
parent.TryGetNitroxId(out ownerId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// For regular water parks, the main object contains the StorageRoot and the planter at the same level
|
||||
else if (LockerRegex.IsMatch(ownerTransform.gameObject.name))
|
||||
{
|
||||
string lockerId = ownerTransform.gameObject.name.Substring(7, 1);
|
||||
string lockerName = $"{LOCKER_BASE_NAME}{lockerId}";
|
||||
GameObject locker = parent.gameObject.FindChild(lockerName);
|
||||
if (!locker)
|
||||
{
|
||||
Log.Error($"Could not find Locker Object: {lockerName}");
|
||||
ownerId = null;
|
||||
return false;
|
||||
}
|
||||
if (!locker.TryGetComponentInChildren(out StorageContainer storageContainer, true))
|
||||
{
|
||||
Log.Error($"Could not find {nameof(StorageContainer)} From Object: {lockerName}");
|
||||
ownerId = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return storageContainer.TryGetIdOrWarn(out ownerId);
|
||||
}
|
||||
else if (parent.name.StartsWith(ESCAPEPOD_OBJECT_NAME))
|
||||
{
|
||||
StorageContainer storageContainer = parent.RequireComponentInChildren<StorageContainer>(true);
|
||||
return storageContainer.TryGetIdOrWarn(out ownerId);
|
||||
}
|
||||
|
||||
return parent.TryGetIdOrWarn(out ownerId);
|
||||
}
|
||||
}
|
||||
}
|
56
NitroxClient/GameLogic/Helper/TransientLocalObjectManager.cs
Normal file
56
NitroxClient/GameLogic/Helper/TransientLocalObjectManager.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NitroxModel.DataStructures.Util;
|
||||
|
||||
namespace NitroxClient.GameLogic.Helper
|
||||
{
|
||||
/**
|
||||
* Class used for temporarily storing variables local to patched methods. Certain circumstances require that these
|
||||
* be referenced at a later point and most of the time it is too prohibitive to expose global statics.
|
||||
*
|
||||
* An example use-case is the created gameobject from the vehicle constructor class. This gameobject is only accessible
|
||||
* locally when crafted. We need to access it at future times to retrieve and set its GUID.
|
||||
*/
|
||||
public static class TransientLocalObjectManager
|
||||
{
|
||||
public enum TransientObjectType
|
||||
{
|
||||
BASE_GHOST_NEWLY_CONSTRUCTED_BASE_GAMEOBJECT,
|
||||
|
||||
LATEST_DECONSTRUCTED_BASE_PIECE_GHOST,
|
||||
LATEST_DECONSTRUCTED_BASE_PIECE_GUID,
|
||||
|
||||
LATER_CONSTRUCTED_BASE,
|
||||
LATER_OBJECT_LATEST_BASE,
|
||||
LATER_OBJECT_LATEST_CELL,
|
||||
}
|
||||
|
||||
private static readonly Dictionary<TransientObjectType, object> localObjectsById = new();
|
||||
|
||||
public static void Add(TransientObjectType key, object o)
|
||||
{
|
||||
localObjectsById[key] = o;
|
||||
}
|
||||
|
||||
public static void Remove(TransientObjectType key)
|
||||
{
|
||||
localObjectsById.Remove(key);
|
||||
}
|
||||
|
||||
public static Optional<object> Get(TransientObjectType key)
|
||||
{
|
||||
localObjectsById.TryGetValue(key, out object obj);
|
||||
return Optional.OfNullable(obj);
|
||||
}
|
||||
|
||||
public static T Require<T>(TransientObjectType key)
|
||||
{
|
||||
if (!localObjectsById.TryGetValue(key, out object obj))
|
||||
{
|
||||
throw new Exception($"Did not have an entry for key: {key}");
|
||||
}
|
||||
|
||||
return (T)obj;
|
||||
}
|
||||
}
|
||||
}
|
69
NitroxClient/GameLogic/Helper/VehicleChildEntityHelper.cs
Normal file
69
NitroxClient/GameLogic/Helper/VehicleChildEntityHelper.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NitroxClient.MonoBehaviours;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.DataStructures.GameLogic;
|
||||
using NitroxModel.DataStructures.GameLogic.Entities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.Helper;
|
||||
|
||||
public static class VehicleChildEntityHelper
|
||||
{
|
||||
private static readonly HashSet<Type> interactiveChildTypes = new HashSet<Type> // we must sync ids of these types when creating vehicles (mainly cyclops)
|
||||
{
|
||||
typeof(Openable),
|
||||
typeof(CyclopsLocker),
|
||||
typeof(Fabricator),
|
||||
typeof(FireExtinguisherHolder),
|
||||
typeof(StorageContainer),
|
||||
typeof(SeamothStorageContainer),
|
||||
typeof(VehicleDockingBay),
|
||||
typeof(DockedVehicleHandTarget),
|
||||
typeof(UpgradeConsole),
|
||||
typeof(DockingBayDoor),
|
||||
typeof(CyclopsDecoyLoadingTube),
|
||||
typeof(BatterySource),
|
||||
typeof(SubNameInput),
|
||||
typeof(WeldablePoint),
|
||||
typeof(CyclopsVehicleStorageTerminalManager),
|
||||
typeof(CyclopsLightingPanel)
|
||||
};
|
||||
|
||||
public static void PopulateChildren(NitroxId vehicleId, string vehiclePath, List<Entity> toPopulate, GameObject current)
|
||||
{
|
||||
string currentPath = current.GetFullHierarchyPath();
|
||||
string relativePathName = currentPath.Replace(vehiclePath, string.Empty).TrimStart('/');
|
||||
|
||||
if (relativePathName.Length > 0)
|
||||
{
|
||||
// generate PathBasedChildEntities for gameObjects under the main vehicle.
|
||||
foreach (MonoBehaviour mono in current.GetComponents<MonoBehaviour>())
|
||||
{
|
||||
// We don't to accidentally tag this game object unless we know it has an applicable mono
|
||||
if (interactiveChildTypes.Contains(mono.GetType()))
|
||||
{
|
||||
NitroxId id = NitroxEntity.GetIdOrGenerateNew(mono.gameObject);
|
||||
|
||||
PathBasedChildEntity pathBasedChildEntity = new(relativePathName, id, null, null, vehicleId, new());
|
||||
toPopulate.Add(pathBasedChildEntity);
|
||||
|
||||
if (mono is BatterySource batterySource) // cyclops has a battery source as a deeply-nested child
|
||||
{
|
||||
BatteryChildEntityHelper.PopulateInstalledBattery(batterySource, pathBasedChildEntity.ChildEntities, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// both seamoth and exosuit have energymixin as a direct component. populate the battery if it exists
|
||||
BatteryChildEntityHelper.TryPopulateInstalledBattery(current, toPopulate, vehicleId);
|
||||
}
|
||||
|
||||
foreach (Transform child in current.transform)
|
||||
{
|
||||
PopulateChildren(vehicleId, vehiclePath, toPopulate, child.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using NitroxModel.Packets;
|
||||
|
||||
namespace NitroxClient.GameLogic.InitialSync.Abstract;
|
||||
|
||||
public interface IInitialSyncProcessor<in TPacket> where TPacket : Packet
|
||||
{
|
||||
HashSet<Type> DependentProcessors { get; }
|
||||
|
||||
IEnumerator Process(TPacket packet, WaitScreen.ManualWaitItem waitScreenItem);
|
||||
}
|
||||
|
||||
public interface IInitialSyncProcessor : IInitialSyncProcessor<InitialPlayerSync>
|
||||
{
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using NitroxModel.Packets;
|
||||
|
||||
namespace NitroxClient.GameLogic.InitialSync.Abstract;
|
||||
|
||||
public abstract class InitialSyncProcessor : IInitialSyncProcessor
|
||||
{
|
||||
public virtual List<Func<InitialPlayerSync, IEnumerator>> Steps { get; } = new();
|
||||
public virtual HashSet<Type> DependentProcessors { get; } = new();
|
||||
|
||||
public virtual IEnumerator Process(InitialPlayerSync packet, WaitScreen.ManualWaitItem waitScreenItem)
|
||||
{
|
||||
for (int i = 0; i < Steps.Count; i++)
|
||||
{
|
||||
yield return Steps[i](packet);
|
||||
waitScreenItem.SetProgress((float)i / Steps.Count);
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddDependency<TDependency>() where TDependency : IInitialSyncProcessor
|
||||
{
|
||||
DependentProcessors.Add(typeof(TDependency));
|
||||
}
|
||||
|
||||
public void AddStep(Func<InitialPlayerSync, IEnumerator> step)
|
||||
{
|
||||
Steps.Add(step);
|
||||
}
|
||||
|
||||
public void AddStep(Action<InitialPlayerSync> step)
|
||||
{
|
||||
Steps.Add(sync =>
|
||||
{
|
||||
step(sync);
|
||||
return Array.Empty<object>().GetEnumerator();
|
||||
});
|
||||
}
|
||||
|
||||
public void AddStep(Func<IEnumerator> step)
|
||||
{
|
||||
Steps.Add(_ => step());
|
||||
}
|
||||
}
|
@@ -0,0 +1,70 @@
|
||||
using System.Collections;
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxClient.Communication.NetworkingLayer.LiteNetLib;
|
||||
using NitroxClient.GameLogic.InitialSync.Abstract;
|
||||
using NitroxClient.GameLogic.Settings;
|
||||
using NitroxClient.MonoBehaviours.Gui.Modals;
|
||||
using NitroxModel.Networking;
|
||||
using NitroxModel.Packets;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.InitialSync;
|
||||
|
||||
public class ClockSyncInitialSyncProcessor : InitialSyncProcessor
|
||||
{
|
||||
private readonly TimeManager timeManager;
|
||||
private readonly NtpSyncer ntpSyncer;
|
||||
private readonly LiteNetLibClient liteNetLibClient;
|
||||
|
||||
public ClockSyncInitialSyncProcessor(TimeManager timeManager, NtpSyncer ntpSyncer, IClient client)
|
||||
{
|
||||
this.timeManager = timeManager;
|
||||
this.ntpSyncer = ntpSyncer;
|
||||
liteNetLibClient = (LiteNetLibClient)client;
|
||||
|
||||
AddStep(initialSync => NTPSync(initialSync.TimeData.TimePacket));
|
||||
}
|
||||
|
||||
public IEnumerator NTPSync(TimeChange timeData)
|
||||
{
|
||||
timeManager.SetServerCorrectionData(timeData.OnlineMode, timeData.UtcCorrectionTicks);
|
||||
|
||||
ntpSyncer.Setup(true);
|
||||
ntpSyncer.RequestNtpService();
|
||||
|
||||
yield return new WaitUntil(() => ntpSyncer.Finished);
|
||||
|
||||
if (ntpSyncer.OnlineMode)
|
||||
{
|
||||
timeManager.SetClientCorrectionData(true, ntpSyncer.CorrectionOffset);
|
||||
// If server AND client are in online mode, we have everything we need
|
||||
if (timeData.OnlineMode)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
|
||||
Log.Warn($"Both client ({(ntpSyncer.OnlineMode ? "ONLINE" : "OFFLINE")}) and server ({(timeData.OnlineMode ? "ONLINE" : "OFFLINE")}) aren't in ONLINE mode. Falling back to {nameof(ClockSyncProcedure)}");
|
||||
|
||||
yield return GetAveragePing();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Procedure to calculate an average time delta with the server
|
||||
/// </summary>
|
||||
private IEnumerator GetAveragePing()
|
||||
{
|
||||
int procedureDuration = (int)NitroxPrefs.OfflineClockSyncDuration.Value; // seconds
|
||||
using ClockSyncProcedure clockSyncProcedure = ClockSyncProcedure.Start(liteNetLibClient, procedureDuration);
|
||||
yield return new WaitForSecondsRealtime(procedureDuration);
|
||||
bool success = clockSyncProcedure.TryGetSafeAverageRTD(out long remoteTimeDelta);
|
||||
|
||||
Log.Info($"[success: {success}] calculated RTD: {remoteTimeDelta}");
|
||||
timeManager.SetCorrectionDelta(remoteTimeDelta);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
yield return Modal.Get<InfoModal>().ShowAsync("Clock desync fixer failed. Ensure both you and the server are connected to the internet. Or try increasing the \"Offline Clock Sync Duration\" value in the settings, and restart your game.");
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,70 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using NitroxClient.Communication;
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxClient.GameLogic.Helper;
|
||||
using NitroxClient.GameLogic.InitialSync.Abstract;
|
||||
using NitroxClient.MonoBehaviours;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.DataStructures.Util;
|
||||
using NitroxModel.Packets;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.InitialSync;
|
||||
|
||||
public sealed class EquippedItemInitialSyncProcessor : InitialSyncProcessor
|
||||
{
|
||||
public EquippedItemInitialSyncProcessor()
|
||||
{
|
||||
AddDependency<PlayerInitialSyncProcessor>();
|
||||
AddDependency<RemotePlayerInitialSyncProcessor>();
|
||||
AddDependency<GlobalRootInitialSyncProcessor>();
|
||||
}
|
||||
|
||||
public override IEnumerator Process(InitialPlayerSync packet, WaitScreen.ManualWaitItem waitScreenItem)
|
||||
{
|
||||
int totalEquippedItemsDone = 0;
|
||||
|
||||
using (PacketSuppressor<EntitySpawnedByClient>.Suppress())
|
||||
{
|
||||
foreach (KeyValuePair<string, NitroxId> equippedItem in packet.EquippedItems)
|
||||
{
|
||||
string slot = equippedItem.Key;
|
||||
NitroxId id = equippedItem.Value;
|
||||
|
||||
waitScreenItem.SetProgress(totalEquippedItemsDone, packet.EquippedItems.Count);
|
||||
|
||||
GameObject gameObject = NitroxEntity.RequireObjectFrom(id);
|
||||
Pickupable pickupable = gameObject.RequireComponent<Pickupable>();
|
||||
|
||||
GameObject player = Player.mainObject;
|
||||
Optional<Equipment> opEquipment = EquipmentHelper.FindEquipmentComponent(player);
|
||||
|
||||
if (opEquipment.HasValue)
|
||||
{
|
||||
Equipment equipment = opEquipment.Value;
|
||||
InventoryItem inventoryItem = new(pickupable);
|
||||
inventoryItem.container = equipment;
|
||||
inventoryItem.item.Reparent(equipment.tr);
|
||||
|
||||
Dictionary<string, InventoryItem> itemsBySlot = equipment.equipment;
|
||||
itemsBySlot[slot] = inventoryItem;
|
||||
|
||||
equipment.UpdateCount(pickupable.GetTechType(), true);
|
||||
Equipment.SendEquipmentEvent(pickupable, 0, player, slot);
|
||||
equipment.NotifyEquip(slot, inventoryItem);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Info($"Could not find equipment type for {gameObject.name}");
|
||||
}
|
||||
|
||||
totalEquippedItemsDone++;
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
Log.Info($"Recieved initial sync with {totalEquippedItemsDone} pieces of equipped items");
|
||||
}
|
||||
}
|
@@ -0,0 +1,83 @@
|
||||
using System.Collections;
|
||||
using NitroxClient.GameLogic.Bases;
|
||||
using NitroxClient.GameLogic.InitialSync.Abstract;
|
||||
using NitroxClient.MonoBehaviours;
|
||||
using NitroxClient.MonoBehaviours.Cyclops;
|
||||
using NitroxModel.MultiplayerSession;
|
||||
using NitroxModel.Packets;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.InitialSync;
|
||||
|
||||
/// <summary>
|
||||
/// Makes sure players can be spawned in entities in the global root (such as vehicles/escape pod).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This allows for:<br/>
|
||||
/// - vehicles to use equipment
|
||||
/// - other players to be set as drivers of some vehicle
|
||||
/// </remarks>
|
||||
public sealed class GlobalRootInitialSyncProcessor : InitialSyncProcessor
|
||||
{
|
||||
private readonly Entities entities;
|
||||
private readonly Vehicles vehicles;
|
||||
private readonly PlayerManager playerManager;
|
||||
private readonly BulletManager bulletManager;
|
||||
|
||||
public GlobalRootInitialSyncProcessor(Entities entities, Vehicles vehicles, PlayerManager playerManager, BulletManager bulletManager)
|
||||
{
|
||||
this.entities = entities;
|
||||
this.vehicles = vehicles;
|
||||
this.playerManager = playerManager;
|
||||
this.bulletManager = bulletManager;
|
||||
|
||||
// As we migrate systems over to entities, we want to ensure the required components are in place to spawn these entities.
|
||||
// For example, migrating inventories to the entity system requires players are spawned in the world before we try to add
|
||||
// inventory items to them. Eventually, all of the below processors will become entities on their own
|
||||
AddDependency<PlayerInitialSyncProcessor>();
|
||||
AddDependency<RemotePlayerInitialSyncProcessor>();
|
||||
AddDependency<StoryGoalInitialSyncProcessor>();
|
||||
|
||||
AddStep(WorldSettledForBuildings);
|
||||
AddStep(SpawnEntities);
|
||||
AddStep(RestoreDrivers);
|
||||
}
|
||||
|
||||
public IEnumerator WorldSettledForBuildings(InitialPlayerSync packet)
|
||||
{
|
||||
yield return new WaitUntil(LargeWorldStreamer.main.IsWorldSettled);
|
||||
// Make sure all building-related prefabs are fully loaded (happen to bug when launching multiple clients locally)
|
||||
yield return Base.InitializeAsync();
|
||||
yield return BaseGhost.InitializeAsync();
|
||||
yield return BaseDeconstructable.InitializeAsync();
|
||||
yield return VirtualCyclops.InitializeConstructablesCache();
|
||||
yield return bulletManager.Initialize();
|
||||
|
||||
BuildingHandler.Main.InitializeOperations(packet.BuildOperationIds);
|
||||
}
|
||||
|
||||
public IEnumerator SpawnEntities(InitialPlayerSync packet)
|
||||
{
|
||||
Log.Info($"Received initial sync packet with {packet.GlobalRootEntities.Count} global root entities");
|
||||
yield return entities.SpawnBatchAsync(packet.GlobalRootEntities);
|
||||
}
|
||||
|
||||
public void RestoreDrivers(InitialPlayerSync packet)
|
||||
{
|
||||
// At this step, vehicles have been spawned already (by SpawnEntities)
|
||||
foreach (PlayerContext playerContext in packet.OtherPlayers)
|
||||
{
|
||||
if (playerContext.DrivingVehicle != null)
|
||||
{
|
||||
Log.Info($"Restoring driver state of {playerContext.PlayerName} in {playerContext.DrivingVehicle}");
|
||||
vehicles.SetOnPilotMode(playerContext.DrivingVehicle, playerContext.PlayerId, true);
|
||||
if (playerManager.TryFind(playerContext.PlayerId, out RemotePlayer remotePlayer))
|
||||
{
|
||||
// As remote players are still driving, they aren't updating their IsUnderwater state so AnimationSender.Update
|
||||
// isn't going to send a packet. Therefore we need to set this by hand
|
||||
remotePlayer.UpdateAnimationAndCollider(AnimChangeType.UNDERWATER, AnimChangeState.OFF);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NitroxClient.Communication;
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxClient.GameLogic.InitialSync.Abstract;
|
||||
using NitroxModel.DataStructures.GameLogic;
|
||||
using NitroxModel.Packets;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
|
||||
namespace NitroxClient.GameLogic.InitialSync;
|
||||
|
||||
public sealed class PdaInitialSyncProcessor : InitialSyncProcessor
|
||||
{
|
||||
public PdaInitialSyncProcessor()
|
||||
{
|
||||
AddDependency<ClockSyncInitialSyncProcessor>();
|
||||
}
|
||||
|
||||
// The steps are ordered like their call order in Player.OnProtoDeserialize
|
||||
public override List<Func<InitialPlayerSync, IEnumerator>> Steps { get; } =
|
||||
[
|
||||
RestoreKnownTech,
|
||||
RestorePDALog,
|
||||
RestoreEncyclopediaEntries,
|
||||
RestorePDAScanner
|
||||
];
|
||||
|
||||
private static IEnumerator RestoreKnownTech(InitialPlayerSync packet)
|
||||
{
|
||||
List<TechType> knownTech = packet.PDAData.KnownTechTypes.Select(techType => techType.ToUnity()).ToList();
|
||||
HashSet<TechType> analyzedTech = new(packet.PDAData.AnalyzedTechTypes.Select(techType => techType.ToUnity()));
|
||||
Log.Info($"Received initial sync packet with {knownTech.Count} KnownTech.knownTech types and {analyzedTech.Count} KnownTech.analyzedTech types.");
|
||||
|
||||
using (PacketSuppressor<KnownTechEntryAdd>.Suppress())
|
||||
{
|
||||
KnownTech.Deserialize(knownTech, analyzedTech);
|
||||
}
|
||||
yield break;
|
||||
}
|
||||
|
||||
private static IEnumerator RestorePDALog(InitialPlayerSync packet)
|
||||
{
|
||||
List<PDALogEntry> logEntries = packet.PDAData.PDALogEntries;
|
||||
Log.Info($"Received initial sync packet with {logEntries.Count} pda log entries");
|
||||
|
||||
using (PacketSuppressor<PDALogEntryAdd>.Suppress())
|
||||
{
|
||||
// We just need the timestamp and the key because everything else is provided by PDALog.InitDataForEntries
|
||||
PDALog.Deserialize(logEntries.ToDictionary(m => m.Key, m => new PDALog.Entry() { timestamp = m.Timestamp }));
|
||||
}
|
||||
yield break;
|
||||
}
|
||||
|
||||
private static IEnumerator RestoreEncyclopediaEntries(InitialPlayerSync packet)
|
||||
{
|
||||
List<string> entries = packet.PDAData.EncyclopediaEntries;
|
||||
Log.Info($"Received initial sync packet with {entries.Count} encyclopedia entries");
|
||||
|
||||
using (PacketSuppressor<PDAEncyclopediaEntryAdd>.Suppress())
|
||||
{
|
||||
// We don't do as in PDAEncyclopedia.Deserialize because we don't persist the entry's fields which are useless
|
||||
foreach (string entry in entries)
|
||||
{
|
||||
PDAEncyclopedia.Add(entry, false);
|
||||
}
|
||||
}
|
||||
yield break;
|
||||
}
|
||||
|
||||
private static IEnumerator RestorePDAScanner(InitialPlayerSync packet)
|
||||
{
|
||||
InitialPDAData pdaData = packet.PDAData;
|
||||
|
||||
PDAScanner.Data data = new()
|
||||
{
|
||||
fragments = pdaData.ScannerFragments.ToDictionary(m => m.ToString(), m => 1f),
|
||||
partial = pdaData.ScannerPartial.Select(entry => entry.ToUnity()).ToList(),
|
||||
complete = new HashSet<TechType>(pdaData.ScannerComplete.Select(techType => techType.ToUnity()))
|
||||
};
|
||||
PDAScanner.Deserialize(data);
|
||||
yield break;
|
||||
}
|
||||
}
|
162
NitroxClient/GameLogic/InitialSync/PlayerInitialSyncProcessor.cs
Normal file
162
NitroxClient/GameLogic/InitialSync/PlayerInitialSyncProcessor.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
using System.Collections;
|
||||
using System.Text;
|
||||
using NitroxClient.GameLogic.InitialSync.Abstract;
|
||||
using NitroxClient.MonoBehaviours;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.DataStructures.GameLogic;
|
||||
using NitroxModel.Server;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.InitialSync;
|
||||
|
||||
/// <summary>
|
||||
/// Makes sure the player is configured.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This allows the player to:<br/>
|
||||
/// - use equipment
|
||||
/// </remarks>
|
||||
public sealed class PlayerInitialSyncProcessor : InitialSyncProcessor
|
||||
{
|
||||
private readonly Items item;
|
||||
private readonly ItemContainers itemContainers;
|
||||
private readonly LocalPlayer localPlayer;
|
||||
|
||||
public PlayerInitialSyncProcessor(Items item, ItemContainers itemContainers, LocalPlayer localPlayer)
|
||||
{
|
||||
this.item = item;
|
||||
this.itemContainers = itemContainers;
|
||||
this.localPlayer = localPlayer;
|
||||
|
||||
AddStep(sync => SetupEscapePod(sync.FirstTimeConnecting));
|
||||
AddStep(sync => SetPlayerPermissions(sync.Permissions));
|
||||
AddStep(sync => SetPlayerIntroCinematicMode(sync.IntroCinematicMode));
|
||||
AddStep(sync => SetPlayerGameObjectId(sync.PlayerGameObjectId));
|
||||
AddStep(sync => AddStartingItemsToPlayer(sync.FirstTimeConnecting));
|
||||
AddStep(sync => SetPlayerStats(sync.PlayerStatsData));
|
||||
AddStep(sync => SetPlayerGameMode(sync.GameMode));
|
||||
AddStep(sync => ApplySettings(sync.KeepInventoryOnDeath, sync.SessionSettings.FastHatch, sync.SessionSettings.FastGrow));
|
||||
}
|
||||
|
||||
private void SetPlayerPermissions(Perms permissions)
|
||||
{
|
||||
localPlayer.Permissions = permissions;
|
||||
}
|
||||
|
||||
private void SetPlayerIntroCinematicMode(IntroCinematicMode introCinematicMode)
|
||||
{
|
||||
if (localPlayer.IntroCinematicMode < introCinematicMode)
|
||||
{
|
||||
localPlayer.IntroCinematicMode = introCinematicMode;
|
||||
Log.Info($"Received initial sync player IntroCinematicMode: {introCinematicMode}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetPlayerGameObjectId(NitroxId id)
|
||||
{
|
||||
EcoTarget playerEcoTarget = Player.mainObject.AddComponent<EcoTarget>();
|
||||
playerEcoTarget.SetTargetType(RemotePlayer.PLAYER_ECO_TARGET_TYPE);
|
||||
|
||||
NitroxEntity.SetNewId(Player.mainObject, id);
|
||||
Log.Info($"Received initial sync player GameObject Id: {id}");
|
||||
}
|
||||
|
||||
private void SetupEscapePod(bool firstTimeConnecting)
|
||||
{
|
||||
EscapePod escapePod = EscapePod.main;
|
||||
if (escapePod)
|
||||
{
|
||||
Log.Info($"Setting up escape pod, FirstTimeConnecting: {firstTimeConnecting}");
|
||||
|
||||
escapePod.bottomHatchUsed = !firstTimeConnecting;
|
||||
escapePod.topHatchUsed = !firstTimeConnecting;
|
||||
|
||||
// Call code we suppressed inside EscapePodFirstUseCinematicsController_OnSceneObjectsLoaded_Patch
|
||||
EscapePodFirstUseCinematicsController cinematicController = escapePod.GetComponentInChildren<EscapePodFirstUseCinematicsController>(true);
|
||||
if (cinematicController)
|
||||
{
|
||||
cinematicController.Initialize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator AddStartingItemsToPlayer(bool firstTimeConnecting)
|
||||
{
|
||||
if (firstTimeConnecting)
|
||||
{
|
||||
if (!Player.main.TryGetIdOrWarn(out NitroxId localPlayerId))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (TechType techType in LootSpawner.main.GetEscapePodStorageTechTypes())
|
||||
{
|
||||
TaskResult<GameObject> result = new();
|
||||
yield return CraftData.InstantiateFromPrefabAsync(techType, result);
|
||||
GameObject gameObject = result.Get();
|
||||
Pickupable pickupable = gameObject.GetComponent<Pickupable>();
|
||||
pickupable.Initialize();
|
||||
|
||||
item.PickedUp(gameObject, techType);
|
||||
itemContainers.AddItem(gameObject, localPlayerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetPlayerStats(PlayerStatsData statsData)
|
||||
{
|
||||
if (statsData != null)
|
||||
{
|
||||
Player.main.oxygenMgr.AddOxygen(statsData.Oxygen);
|
||||
// Spawning a player with 0 health makes them invincible so we'd rather set it to 1 HP
|
||||
Player.main.liveMixin.health = Mathf.Max(1f, statsData.Health);
|
||||
Survival survivalComponent = Player.main.GetComponent<Survival>();
|
||||
survivalComponent.food = statsData.Food;
|
||||
survivalComponent.water = statsData.Water;
|
||||
Player.main.infectedMixin.SetInfectedAmount(statsData.InfectionAmount);
|
||||
|
||||
//If InfectionAmount is at least 1f then the infection reveal should have happened already.
|
||||
//If InfectionAmount is below 1f then the reveal has not.
|
||||
if (statsData.InfectionAmount >= 1f)
|
||||
{
|
||||
Player.main.infectionRevealed = true;
|
||||
}
|
||||
|
||||
// We need to make the player invincible before he finishes loading because in some cases he will eventually die before loading
|
||||
Player.main.liveMixin.invincible = true;
|
||||
Player.main.FreezeStats();
|
||||
}
|
||||
|
||||
// We need to start it at least once for everything that's in the PDA to load
|
||||
Player.main.GetPDA().Open(PDATab.Inventory);
|
||||
Player.main.GetPDA().Close();
|
||||
}
|
||||
|
||||
private static void SetPlayerGameMode(NitroxGameMode gameMode)
|
||||
{
|
||||
Log.Info($"Received initial sync packet with gamemode {gameMode}");
|
||||
GameModeUtils.SetGameMode((GameModeOption)(int)gameMode, GameModeOption.None);
|
||||
}
|
||||
|
||||
private void ApplySettings(bool keepInventoryOnDeath, bool fastHatch, bool fastGrow)
|
||||
{
|
||||
localPlayer.KeepInventoryOnDeath = keepInventoryOnDeath;
|
||||
NoCostConsoleCommand.main.fastHatchCheat = fastHatch;
|
||||
NoCostConsoleCommand.main.fastGrowCheat = fastGrow;
|
||||
if (!fastHatch && !fastGrow)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StringBuilder cheatsEnabled = new("Cheats enabled:");
|
||||
if (fastHatch)
|
||||
{
|
||||
cheatsEnabled.Append(" fastHatch");
|
||||
}
|
||||
if (fastGrow)
|
||||
{
|
||||
cheatsEnabled.Append(" fastGrow");
|
||||
}
|
||||
Log.InGame(cheatsEnabled.ToString());
|
||||
}
|
||||
}
|
@@ -0,0 +1,120 @@
|
||||
using System.Collections;
|
||||
using NitroxClient.Communication;
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxClient.GameLogic.InitialSync.Abstract;
|
||||
using NitroxClient.MonoBehaviours;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.DataStructures.Util;
|
||||
using NitroxModel.Packets;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using UnityEngine;
|
||||
using Math = System.Math;
|
||||
|
||||
namespace NitroxClient.GameLogic.InitialSync;
|
||||
|
||||
public sealed class PlayerPositionInitialSyncProcessor : InitialSyncProcessor
|
||||
{
|
||||
private static readonly Vector3 spawnRelativeToEscapePod = new(0.9f, 2.1f, 0);
|
||||
|
||||
public PlayerPositionInitialSyncProcessor()
|
||||
{
|
||||
AddDependency<PlayerInitialSyncProcessor>();
|
||||
AddDependency<GlobalRootInitialSyncProcessor>();
|
||||
}
|
||||
|
||||
public override IEnumerator Process(InitialPlayerSync packet, WaitScreen.ManualWaitItem waitScreenItem)
|
||||
{
|
||||
// We freeze the player so that he doesn't fall before the cells around him have loaded
|
||||
// Is disabled manually or in Terrain.WaitForWorldLoad()
|
||||
Player.main.cinematicModeActive = true;
|
||||
|
||||
AttachPlayerToEscapePod(packet.AssignedEscapePodId);
|
||||
|
||||
Vector3 position = packet.PlayerSpawnData.ToUnity();
|
||||
Quaternion rotation = packet.PlayerSpawnRotation.ToUnity();
|
||||
if (Math.Abs(position.x) < 0.0002 && Math.Abs(position.y) < 0.0002 && Math.Abs(position.z) < 0.0002)
|
||||
{
|
||||
position = Player.mainObject.transform.position;
|
||||
}
|
||||
Player.main.SetPosition(position, rotation);
|
||||
|
||||
// Player.ValidateEscapePod is setting currentEscapePod to null if player is not inside EscapePod
|
||||
using (PacketSuppressor<EscapePodChanged>.Suppress())
|
||||
{
|
||||
Player.main.ValidateEscapePod();
|
||||
}
|
||||
|
||||
Optional<NitroxId> subRootId = packet.PlayerSubRootId;
|
||||
if (!subRootId.HasValue)
|
||||
{
|
||||
yield return Terrain.WaitForWorldLoad();
|
||||
yield break;
|
||||
}
|
||||
|
||||
Optional<GameObject> sub = NitroxEntity.GetObjectFrom(subRootId.Value);
|
||||
if (!sub.HasValue)
|
||||
{
|
||||
Log.Error($"Could not spawn player into subroot with id: {subRootId.Value}");
|
||||
yield return Terrain.WaitForWorldLoad();
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (sub.Value.TryGetComponent(out SubRoot subRoot))
|
||||
{
|
||||
Player.main.SetCurrentSub(subRoot, true);
|
||||
if (subRoot.TryGetComponent(out Base @base))
|
||||
{
|
||||
SetupPlayerIfInWaterPark(@base);
|
||||
}
|
||||
}
|
||||
else if (sub.Value.GetComponent<EscapePod>())
|
||||
{
|
||||
Player.main.escapePod.Update(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error("SubRootId-GameObject has no SubRoot or EscapePod component");
|
||||
}
|
||||
|
||||
// If the player's in a base/cyclops we don't need to wait for the world to load
|
||||
Player.main.UpdateIsUnderwater();
|
||||
Player.main.cinematicModeActive = false;
|
||||
}
|
||||
|
||||
private static void AttachPlayerToEscapePod(NitroxId escapePodId)
|
||||
{
|
||||
GameObject escapePod = NitroxEntity.RequireObjectFrom(escapePodId);
|
||||
|
||||
EscapePod.main.transform.position = escapePod.transform.position;
|
||||
EscapePod.main.playerSpawn.position = escapePod.transform.position + spawnRelativeToEscapePod;
|
||||
|
||||
Player.main.transform.position = EscapePod.main.playerSpawn.position;
|
||||
Player.main.transform.rotation = EscapePod.main.playerSpawn.rotation;
|
||||
|
||||
Player.main.currentEscapePod = escapePod.GetComponent<EscapePod>();
|
||||
}
|
||||
|
||||
private static void SetupPlayerIfInWaterPark(Base @base)
|
||||
{
|
||||
foreach (Transform baseChild in @base.transform)
|
||||
{
|
||||
if (baseChild.TryGetComponent(out WaterPark waterPark))
|
||||
{
|
||||
if (waterPark is LargeRoomWaterPark)
|
||||
{
|
||||
// LargeRoomWaterPark.VerifyPlayerWaterPark sets Player.main.currentWaterPark to the right value
|
||||
waterPark.VerifyPlayerWaterPark(Player.main);
|
||||
}
|
||||
else if (waterPark.IsPointInside(Player.main.transform.position))
|
||||
{
|
||||
Player.main.currentWaterPark = waterPark;
|
||||
}
|
||||
}
|
||||
|
||||
if (Player.main.currentWaterPark)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,142 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NitroxClient.Communication;
|
||||
using NitroxClient.GameLogic.InitialSync.Abstract;
|
||||
using NitroxClient.MonoBehaviours;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel.DataStructures.GameLogic;
|
||||
using NitroxModel.Packets;
|
||||
|
||||
namespace NitroxClient.GameLogic.InitialSync;
|
||||
|
||||
public sealed class PlayerPreferencesInitialSyncProcessor : InitialSyncProcessor
|
||||
{
|
||||
public PlayerPreferencesInitialSyncProcessor()
|
||||
{
|
||||
// list of processors which may cause the spawn of Signal pings
|
||||
AddDependency<PlayerInitialSyncProcessor>();
|
||||
AddDependency<GlobalRootInitialSyncProcessor>();
|
||||
AddDependency<StoryGoalInitialSyncProcessor>();
|
||||
AddDependency<PdaInitialSyncProcessor>();
|
||||
AddDependency<RemotePlayerInitialSyncProcessor>();
|
||||
}
|
||||
|
||||
public override List<Func<InitialPlayerSync, IEnumerator>> Steps { get; } =
|
||||
[
|
||||
UpdatePins,
|
||||
UpdatePingInstancePreferences
|
||||
];
|
||||
|
||||
private static IEnumerator UpdatePins(InitialPlayerSync packet)
|
||||
{
|
||||
using (PacketSuppressor<RecipePinned>.Suppress())
|
||||
{
|
||||
PinManager.main.Deserialize(packet.Preferences.PinnedTechTypes.Select(techType => (TechType)techType).ToList());
|
||||
}
|
||||
yield break;
|
||||
}
|
||||
|
||||
private static IEnumerator UpdatePingInstancePreferences(InitialPlayerSync packet)
|
||||
{
|
||||
Dictionary<string, PingInstancePreference> pingPreferences = packet.Preferences.PingPreferences;
|
||||
void UpdateInstance(PingInstance instance)
|
||||
{
|
||||
ModifyPingInstanceIfPossible(instance, pingPreferences, () => UpdateInstance(instance));
|
||||
RefreshPingEntryInPDA(instance);
|
||||
}
|
||||
|
||||
PingManager.onAdd += UpdateInstance;
|
||||
UnityEngine.Object.FindObjectsOfType<PingInstance>().ForEach(UpdateInstance);
|
||||
yield break;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the given pingInstance if it has a specified preference
|
||||
/// </summary>
|
||||
private static void ModifyPingInstanceIfPossible(PingInstance pingInstance, Dictionary<string, PingInstancePreference> preferences, Action callback)
|
||||
{
|
||||
if (!TryGetKeyForPingInstance(pingInstance, out string pingKey, out bool isRemotePlayerPing, callback) ||
|
||||
!preferences.TryGetValue(pingKey, out PingInstancePreference preference))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (PacketSuppressor<SignalPingPreferenceChanged>.Suppress())
|
||||
{
|
||||
// We don't want to set the color for a remote player's signal
|
||||
if (!isRemotePlayerPing)
|
||||
{
|
||||
pingInstance.SetColor(preference.Color);
|
||||
}
|
||||
pingInstance.SetVisible(preference.Visible);
|
||||
}
|
||||
}
|
||||
|
||||
// Right after initial sync modifications, uGUI_PingEntry elements don't show their updated state
|
||||
private static void RefreshPingEntryInPDA(PingInstance pingInstance)
|
||||
{
|
||||
if (!uGUI_PDA.main || !uGUI_PDA.main.tabs.TryGetValue(PDATab.Ping, out uGUI_PDATab pdaTab))
|
||||
{
|
||||
return;
|
||||
}
|
||||
uGUI_PingTab pingTab = pdaTab as uGUI_PingTab;
|
||||
if (pingTab && pingTab.entries.TryGetValue(pingInstance.Id, out uGUI_PingEntry pingEntry))
|
||||
{
|
||||
pingEntry.SetColor(pingInstance.colorIndex);
|
||||
pingEntry.SetVisible(pingInstance.visible);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the identifier of a PingInstance depending on its type and container
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// We need to differentiate three types of pings, the "normal pings" from objects that emit a signal, these objects generally contain a NitroxEntity
|
||||
/// Another type is Signal pings that are generated by the story events, they are located in the Global Root and don't contain a NitroxEntity, to be identified, they have another object: a SignalPing which contains a description key
|
||||
/// The last type possible is RemotePlayers' pings which are located in a GameObject that is 2 steps under the main object
|
||||
/// </remarks>
|
||||
public static bool TryGetKeyForPingInstance(PingInstance pingInstance, out string pingKey, out bool isRemotePlayerPing, Action failCallback = null)
|
||||
{
|
||||
isRemotePlayerPing = false;
|
||||
if (pingInstance.TryGetComponent(out SignalPing signalPing))
|
||||
{
|
||||
pingKey = signalPing.descriptionKey;
|
||||
// Sometimes, the SignalPing will not have loaded properly so we need to postpone the key detection
|
||||
if (pingKey == null)
|
||||
{
|
||||
pingInstance.StartCoroutine(DelayPingKeyDetection(failCallback));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (pingInstance.TryGetComponent(out NitroxEntity nitroxEntity))
|
||||
{
|
||||
pingKey = nitroxEntity.Id.ToString();
|
||||
return true;
|
||||
}
|
||||
if (pingInstance.transform.TryGetComponentInAscendance(2, out nitroxEntity))
|
||||
{
|
||||
pingKey = nitroxEntity.Id.ToString();
|
||||
isRemotePlayerPing = true;
|
||||
return true;
|
||||
}
|
||||
// Known issue for a ping named "xSignal(Clone)" that appears temporarily when another player joins
|
||||
if (pingInstance.name.Equals("xSignal(Clone)"))
|
||||
{
|
||||
pingKey = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
Log.Warn($"Couldn't find PingInstance identifier for {pingInstance.name} under {pingInstance.transform.parent}");
|
||||
pingKey = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static IEnumerator DelayPingKeyDetection(Action delayedAction)
|
||||
{
|
||||
yield return Yielders.WaitForHalfSecond;
|
||||
delayedAction?.Invoke();
|
||||
}
|
||||
}
|
@@ -0,0 +1,62 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using NitroxClient.GameLogic.InitialSync.Abstract;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.DataStructures.Util;
|
||||
using NitroxModel.Packets;
|
||||
|
||||
namespace NitroxClient.GameLogic.InitialSync;
|
||||
|
||||
public sealed class QuickSlotInitialSyncProcessor : InitialSyncProcessor
|
||||
{
|
||||
public QuickSlotInitialSyncProcessor()
|
||||
{
|
||||
AddDependency<PlayerInitialSyncProcessor>(); // the player needs to be configured before we can set quick slots.
|
||||
AddDependency<EquippedItemInitialSyncProcessor>(); // we need to have the items spawned into our inventory before we can quick slot them.
|
||||
}
|
||||
|
||||
public override IEnumerator Process(InitialPlayerSync packet, WaitScreen.ManualWaitItem waitScreenItem)
|
||||
{
|
||||
int nonEmptySlots = 0;
|
||||
|
||||
Dictionary<NitroxId, InventoryItem> inventoryItemsById = GetItemsById();
|
||||
|
||||
for (int i = 0; i < packet.QuickSlotsBindingIds.Length; i++)
|
||||
{
|
||||
waitScreenItem.SetProgress(i, packet.QuickSlotsBindingIds.Length);
|
||||
|
||||
Optional<NitroxId> opId = packet.QuickSlotsBindingIds[i];
|
||||
|
||||
if (opId.HasValue && inventoryItemsById.TryGetValue(opId.Value, out InventoryItem inventoryItem) )
|
||||
{
|
||||
Inventory.main.quickSlots.binding[i] = inventoryItem;
|
||||
Inventory.main.quickSlots.NotifyBind(i, state: true);
|
||||
nonEmptySlots++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Unbind any default stuff from equipment addition.
|
||||
Inventory.main.quickSlots.Unbind(i);
|
||||
}
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
Log.Info($"Received initial sync with {nonEmptySlots} quick slots populated with items");
|
||||
}
|
||||
|
||||
private Dictionary<NitroxId, InventoryItem> GetItemsById()
|
||||
{
|
||||
Dictionary<NitroxId, InventoryItem> itemsById = new();
|
||||
|
||||
foreach (InventoryItem inventoryItem in Inventory.main.container)
|
||||
{
|
||||
if (inventoryItem.item.TryGetIdOrWarn(out NitroxId itemId))
|
||||
{
|
||||
itemsById.Add(itemId, inventoryItem);
|
||||
}
|
||||
}
|
||||
|
||||
return itemsById;
|
||||
}
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
using System.Collections;
|
||||
using NitroxClient.GameLogic.InitialSync.Abstract;
|
||||
using NitroxModel.MultiplayerSession;
|
||||
using NitroxModel.Packets;
|
||||
|
||||
namespace NitroxClient.GameLogic.InitialSync;
|
||||
|
||||
/// <summary>
|
||||
/// Makes sure the remote player object is loaded.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This allows for the remote player to:<br/>
|
||||
/// - use equipment
|
||||
/// </remarks>
|
||||
public sealed class RemotePlayerInitialSyncProcessor : InitialSyncProcessor
|
||||
{
|
||||
private readonly PlayerManager remotePlayerManager;
|
||||
|
||||
public RemotePlayerInitialSyncProcessor(PlayerManager remotePlayerManager)
|
||||
{
|
||||
this.remotePlayerManager = remotePlayerManager;
|
||||
}
|
||||
|
||||
public override IEnumerator Process(InitialPlayerSync packet, WaitScreen.ManualWaitItem waitScreenItem)
|
||||
{
|
||||
int remotePlayersSynced = 0;
|
||||
|
||||
foreach (PlayerContext otherPlayer in packet.OtherPlayers)
|
||||
{
|
||||
waitScreenItem.SetProgress(remotePlayersSynced, packet.OtherPlayers.Count);
|
||||
|
||||
remotePlayerManager.Create(otherPlayer);
|
||||
|
||||
remotePlayersSynced++;
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
using System.Collections;
|
||||
using NitroxClient.GameLogic.InitialSync.Abstract;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.Packets;
|
||||
|
||||
namespace NitroxClient.GameLogic.InitialSync;
|
||||
|
||||
public sealed class SimulationOwnershipInitialSyncProcessor : InitialSyncProcessor
|
||||
{
|
||||
private readonly SimulationOwnership simulationOwnership;
|
||||
|
||||
public SimulationOwnershipInitialSyncProcessor(SimulationOwnership simulationOwnership)
|
||||
{
|
||||
this.simulationOwnership = simulationOwnership;
|
||||
|
||||
AddDependency<GlobalRootInitialSyncProcessor>();
|
||||
}
|
||||
|
||||
public override IEnumerator Process(InitialPlayerSync packet, WaitScreen.ManualWaitItem waitScreenItem)
|
||||
{
|
||||
int entitiesSynced = 0;
|
||||
foreach (SimulatedEntity simulatedEntity in packet.InitialSimulationOwnerships)
|
||||
{
|
||||
simulationOwnership.TreatSimulatedEntity(simulatedEntity);
|
||||
|
||||
if (entitiesSynced++ % 5 == 0)
|
||||
{
|
||||
waitScreenItem.SetProgress(entitiesSynced, packet.InitialSimulationOwnerships.Count);
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
yield break;
|
||||
}
|
||||
}
|
@@ -0,0 +1,197 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NitroxClient.GameLogic.InitialSync.Abstract;
|
||||
using NitroxClient.MonoBehaviours;
|
||||
using NitroxModel.DataStructures.GameLogic;
|
||||
using NitroxModel.Packets;
|
||||
using Story;
|
||||
|
||||
namespace NitroxClient.GameLogic.InitialSync;
|
||||
|
||||
public sealed class StoryGoalInitialSyncProcessor : InitialSyncProcessor
|
||||
{
|
||||
private readonly TimeManager timeManager;
|
||||
|
||||
public StoryGoalInitialSyncProcessor(TimeManager timeManager)
|
||||
{
|
||||
this.timeManager = timeManager;
|
||||
|
||||
AddStep(SetTimeData);
|
||||
AddStep(SetupStoryGoalManager);
|
||||
AddStep(SetupTrackers);
|
||||
AddStep(SetupAuroraAndSunbeam);
|
||||
AddStep(SetScheduledGoals);
|
||||
}
|
||||
|
||||
private static void SetupStoryGoalManager(InitialPlayerSync packet)
|
||||
{
|
||||
List<string> completedGoals = packet.StoryGoalData.CompletedGoals;
|
||||
List<string> radioQueue = packet.StoryGoalData.RadioQueue;
|
||||
Dictionary<string, float> personalGoals = packet.StoryGoalData.PersonalCompletedGoalsWithTimestamp;
|
||||
StoryGoalManager storyGoalManager = StoryGoalManager.main;
|
||||
|
||||
storyGoalManager.completedGoals.AddRange(completedGoals);
|
||||
|
||||
storyGoalManager.pendingRadioMessages.AddRange(radioQueue);
|
||||
storyGoalManager.PulsePendingMessages();
|
||||
|
||||
// Restore states of GoalManager and the (tutorial) arrow system
|
||||
foreach (KeyValuePair<string, float> entry in personalGoals)
|
||||
{
|
||||
Goal entryGoal = GoalManager.main.goals.Find(goal => goal.customGoalName.Equals(entry.Key));
|
||||
if (entryGoal != null)
|
||||
{
|
||||
entryGoal.SetTimeCompleted(entry.Value);
|
||||
}
|
||||
}
|
||||
GoalManager.main.completedGoalNames.AddRange(personalGoals.Keys);
|
||||
PlayerWorldArrows.main.completedCustomGoals.AddRange(personalGoals.Keys);
|
||||
|
||||
// Deactivate the current arrow if it was completed
|
||||
if (personalGoals.Any(goal => goal.Key.Equals(WorldArrowManager.main.currentGoalText)))
|
||||
{
|
||||
WorldArrowManager.main.DeactivateArrow();
|
||||
}
|
||||
|
||||
Log.Info($"""
|
||||
Received initial sync packet with:
|
||||
- Completed story goals : {completedGoals.Count}
|
||||
- Personal goals : {personalGoals.Count}
|
||||
- Radio queue : {radioQueue.Count}
|
||||
""");
|
||||
}
|
||||
|
||||
private static void SetupTrackers(InitialPlayerSync packet)
|
||||
{
|
||||
List<string> completedGoals = packet.StoryGoalData.CompletedGoals;
|
||||
StoryGoalManager storyGoalManager = StoryGoalManager.main;
|
||||
OnGoalUnlockTracker onGoalUnlockTracker = storyGoalManager.onGoalUnlockTracker;
|
||||
CompoundGoalTracker compoundGoalTracker = storyGoalManager.compoundGoalTracker;
|
||||
|
||||
// Initializing CompoundGoalTracker and OnGoalUnlockTracker again (with OnSceneObjectsLoaded) requires us to
|
||||
// we first clear what was done in the first iteration of OnSceneObjectsLoaded
|
||||
onGoalUnlockTracker.goalUnlocks.Clear();
|
||||
compoundGoalTracker.goals.Clear();
|
||||
// we force initialized to false so OnSceneObjectsLoaded actually does something
|
||||
storyGoalManager.initialized = false;
|
||||
storyGoalManager.OnSceneObjectsLoaded();
|
||||
|
||||
// Clean LocationGoalTracker, BiomeGoalTracker and ItemGoalTracker already completed goals
|
||||
storyGoalManager.locationGoalTracker.goals.RemoveAll(goal => completedGoals.Contains(goal.key));
|
||||
storyGoalManager.biomeGoalTracker.goals.RemoveAll(goal => completedGoals.Contains(goal.key));
|
||||
|
||||
List<TechType> techTypesToRemove = new();
|
||||
foreach (KeyValuePair<TechType, List<ItemGoal>> entry in storyGoalManager.itemGoalTracker.goals)
|
||||
{
|
||||
// Goals are all triggered at the same time but we don't know if some entries share certain goals
|
||||
if (entry.Value.All(goal => completedGoals.Contains(goal.key)))
|
||||
{
|
||||
techTypesToRemove.Add(entry.Key);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
techTypesToRemove.ForEach(techType => storyGoalManager.itemGoalTracker.goals.Remove(techType));
|
||||
|
||||
// OnGoalUnlock might trigger the creation of a signal which is later on set to invisible when getting close to it
|
||||
// the invisibility is managed by PingInstance_Set_Patches and is restored during PlayerPreferencesInitialSyncProcessor
|
||||
// So we still need to recreate the signals at every game launch
|
||||
|
||||
// To avoid having the SignalPing play its sound we just make its notification null while triggering it
|
||||
// (the sound is something like "coordinates added to the gps" or something)
|
||||
SignalPing prefabSignalPing = onGoalUnlockTracker.signalPrefab.GetComponent<SignalPing>();
|
||||
PDANotification pdaNotification = prefabSignalPing.vo;
|
||||
prefabSignalPing.vo = null;
|
||||
|
||||
foreach (OnGoalUnlock onGoalUnlock in onGoalUnlockTracker.unlockData.onGoalUnlocks)
|
||||
{
|
||||
if (completedGoals.Contains(onGoalUnlock.goal))
|
||||
{
|
||||
// Code adapted from OnGoalUnlock.Trigger
|
||||
foreach (UnlockSignalData unlockSignalData in onGoalUnlock.signals)
|
||||
{
|
||||
unlockSignalData.Trigger(onGoalUnlockTracker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// recover the notification sound
|
||||
prefabSignalPing.vo = pdaNotification;
|
||||
}
|
||||
|
||||
// Must happen after CompletedGoals
|
||||
private static void SetupAuroraAndSunbeam(InitialPlayerSync packet)
|
||||
{
|
||||
TimeData timeData = packet.TimeData;
|
||||
|
||||
AuroraWarnings auroraWarnings = Player.mainObject.GetComponentInChildren<AuroraWarnings>(true);
|
||||
auroraWarnings.timeSerialized = DayNightCycle.main.timePassedAsFloat;
|
||||
auroraWarnings.OnProtoDeserialize(null);
|
||||
|
||||
CrashedShipExploder.main.version = 2;
|
||||
CrashedShipExploder.main.initialized = true;
|
||||
StoryManager.UpdateAuroraData(timeData.AuroraEventData);
|
||||
CrashedShipExploder.main.timeSerialized = DayNightCycle.main.timePassedAsFloat;
|
||||
CrashedShipExploder.main.OnProtoDeserialize(null);
|
||||
|
||||
// Sunbeam countdown is deducted from the scheduled goal PrecursorGunAimCheck
|
||||
NitroxScheduledGoal sunbeamCountdownGoal = packet.StoryGoalData.ScheduledGoals.Find(goal => string.Equals(goal.GoalKey, "PrecursorGunAimCheck", StringComparison.OrdinalIgnoreCase));
|
||||
if (sunbeamCountdownGoal != null)
|
||||
{
|
||||
StoryGoalCustomEventHandler.main.countdownActive = true;
|
||||
StoryGoalCustomEventHandler.main.countdownStartingTime = sunbeamCountdownGoal.TimeExecute - 2370;
|
||||
// See StoryGoalCustomEventHandler.endTime for calculation (endTime - 30 seconds)
|
||||
}
|
||||
}
|
||||
|
||||
// Must happen after CompletedGoals
|
||||
private static void SetScheduledGoals(InitialPlayerSync packet)
|
||||
{
|
||||
List<NitroxScheduledGoal> scheduledGoals = packet.StoryGoalData.ScheduledGoals;
|
||||
|
||||
// We don't want any scheduled goal we add now to be executed before initial sync has finished, else they might not get broadcasted
|
||||
StoryGoalScheduler.main.paused = true;
|
||||
Multiplayer.OnLoadingComplete += () => StoryGoalScheduler.main.paused = false;
|
||||
|
||||
foreach (NitroxScheduledGoal scheduledGoal in scheduledGoals)
|
||||
{
|
||||
// Clear duplicated goals that might have appeared during loading and before sync
|
||||
StoryGoalScheduler.main.schedule.RemoveAll(goal => goal.goalKey == scheduledGoal.GoalKey);
|
||||
|
||||
ScheduledGoal goal = new()
|
||||
{
|
||||
goalKey = scheduledGoal.GoalKey,
|
||||
goalType = (Story.GoalType)scheduledGoal.GoalType,
|
||||
timeExecute = scheduledGoal.TimeExecute,
|
||||
};
|
||||
if (!StoryGoalManager.main.completedGoals.Contains(goal.goalKey))
|
||||
{
|
||||
StoryGoalScheduler.main.schedule.Add(goal);
|
||||
}
|
||||
}
|
||||
|
||||
RefreshStoryWithLatestData();
|
||||
}
|
||||
|
||||
// Must happen after CompletedGoals
|
||||
private static void RefreshStoryWithLatestData()
|
||||
{
|
||||
// If those aren't set up yet, they'll initialize correctly in time
|
||||
// Else, we need to force them to acquire the right data
|
||||
if (StoryGoalCustomEventHandler.main)
|
||||
{
|
||||
StoryGoalCustomEventHandler.main.Awake();
|
||||
}
|
||||
if (PrecursorGunStoryEvents.main)
|
||||
{
|
||||
PrecursorGunStoryEvents.main.Start();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetTimeData(InitialPlayerSync packet)
|
||||
{
|
||||
timeManager.ProcessUpdate(packet.TimeData.TimePacket);
|
||||
timeManager.InitRealTimeElapsed(packet.TimeData.TimePacket.RealTimeElapsed, packet.TimeData.TimePacket.UpdateTime, packet.IsFirstPlayer);
|
||||
timeManager.AuroraRealExplosionTime = packet.TimeData.AuroraEventData.AuroraRealExplosionTime;
|
||||
}
|
||||
}
|
23
NitroxClient/GameLogic/Interior.cs
Normal file
23
NitroxClient/GameLogic/Interior.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.Packets;
|
||||
|
||||
namespace NitroxClient.GameLogic
|
||||
{
|
||||
public class Interior
|
||||
{
|
||||
private readonly IPacketSender packetSender;
|
||||
|
||||
public Interior(IPacketSender packetSender)
|
||||
{
|
||||
this.packetSender = packetSender;
|
||||
}
|
||||
|
||||
public void OpenableStateChanged(NitroxId id, bool isOpen, float animationDuration)
|
||||
{
|
||||
OpenableStateChanged stateChange = new OpenableStateChanged(id, isOpen, animationDuration);
|
||||
packetSender.Send(stateChange);
|
||||
Log.Debug(stateChange);
|
||||
}
|
||||
}
|
||||
}
|
112
NitroxClient/GameLogic/ItemContainers.cs
Normal file
112
NitroxClient/GameLogic/ItemContainers.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using NitroxClient.Communication;
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxClient.GameLogic.Helper;
|
||||
using NitroxClient.GameLogic.PlayerLogic;
|
||||
using NitroxClient.GameLogic.Spawning.Metadata;
|
||||
using NitroxClient.MonoBehaviours;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.DataStructures.GameLogic.Entities;
|
||||
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
|
||||
using NitroxModel.DataStructures.Util;
|
||||
using NitroxModel.Packets;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic;
|
||||
|
||||
public class ItemContainers
|
||||
{
|
||||
private readonly IPacketSender packetSender;
|
||||
private readonly EntityMetadataManager entityMetadataManager;
|
||||
private readonly Items items;
|
||||
|
||||
public ItemContainers(IPacketSender packetSender, EntityMetadataManager entityMetadataManager, Items items)
|
||||
{
|
||||
this.packetSender = packetSender;
|
||||
this.entityMetadataManager = entityMetadataManager;
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
public void BroadcastItemAdd(Pickupable pickupable, Transform containerTransform, ItemsContainer container)
|
||||
{
|
||||
// We don't want to broadcast that event if it's from another player's inventory
|
||||
if (containerTransform.GetComponentInParent<RemotePlayerIdentifier>(true))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!InventoryContainerHelper.TryGetOwnerId(containerTransform, out NitroxId ownerId))
|
||||
{
|
||||
// Error logging is done in the try function
|
||||
return;
|
||||
}
|
||||
|
||||
// For planters, we'll always forcefully recreate the entity to ensure there's no desync
|
||||
if (container.containerType == ItemsContainerType.LandPlants || container.containerType == ItemsContainerType.WaterPlants)
|
||||
{
|
||||
items.Planted(pickupable.gameObject, ownerId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pickupable.TryGetIdOrWarn(out NitroxId itemId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Calls from Inventory.Pickup are managed by Items.PickedUp
|
||||
if (items.IsInventoryPickingUp)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (packetSender.Send(new EntityReparented(itemId, ownerId)))
|
||||
{
|
||||
Log.Debug($"Sent: Added item ({itemId}) of type {pickupable.GetTechType()} to container {containerTransform.gameObject.GetFullHierarchyPath()}");
|
||||
}
|
||||
}
|
||||
|
||||
public void AddItem(GameObject item, NitroxId containerId)
|
||||
{
|
||||
Optional<GameObject> owner = NitroxEntity.GetObjectFrom(containerId);
|
||||
if (!owner.HasValue)
|
||||
{
|
||||
Log.Error($"Unable to find inventory container with id {containerId} for {item.name}");
|
||||
return;
|
||||
}
|
||||
Optional<ItemsContainer> opContainer = InventoryContainerHelper.TryGetContainerByOwner(owner.Value);
|
||||
if (!opContainer.HasValue)
|
||||
{
|
||||
Log.Error($"Could not find container field on GameObject {owner.Value.GetFullHierarchyPath()}");
|
||||
return;
|
||||
}
|
||||
|
||||
ItemsContainer container = opContainer.Value;
|
||||
Pickupable pickupable = item.RequireComponent<Pickupable>();
|
||||
|
||||
using (PacketSuppressor<EntityReparented>.Suppress())
|
||||
{
|
||||
container.UnsafeAdd(new InventoryItem(pickupable));
|
||||
Log.Debug($"Received: Added item {pickupable.GetTechType()} to container {owner.Value.GetFullHierarchyPath()}");
|
||||
}
|
||||
}
|
||||
|
||||
public void BroadcastBatteryAdd(GameObject gameObject, GameObject parent, TechType techType)
|
||||
{
|
||||
if (!gameObject.TryGetIdOrWarn(out NitroxId id))
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!parent.TryGetIdOrWarn(out NitroxId parentId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<EntityMetadata> metadata = entityMetadataManager.Extract(gameObject);
|
||||
|
||||
InstalledBatteryEntity installedBattery = new(id, techType.ToDto(), metadata.OrNull(), parentId, new());
|
||||
|
||||
EntitySpawnedByClient spawnedPacket = new EntitySpawnedByClient(installedBattery);
|
||||
packetSender.Send(spawnedPacket);
|
||||
}
|
||||
}
|
315
NitroxClient/GameLogic/Items.cs
Normal file
315
NitroxClient/GameLogic/Items.cs
Normal file
@@ -0,0 +1,315 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxClient.GameLogic.Helper;
|
||||
using NitroxClient.GameLogic.Spawning.Metadata;
|
||||
using NitroxClient.MonoBehaviours;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.DataStructures.GameLogic;
|
||||
using NitroxModel.DataStructures.GameLogic.Entities;
|
||||
using NitroxModel.DataStructures.GameLogic.Entities.Metadata;
|
||||
using NitroxModel.DataStructures.Util;
|
||||
using NitroxModel.Packets;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic;
|
||||
|
||||
public class Items
|
||||
{
|
||||
private readonly IPacketSender packetSender;
|
||||
private readonly Entities entities;
|
||||
public static GameObject PickingUpObject { get; private set; }
|
||||
private readonly EntityMetadataManager entityMetadataManager;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not <see cref="Inventory.Pickup"/> is running. It's useful to discriminate between Inventory.Pickup from
|
||||
/// a regular <see cref="Pickupable.Pickup"/>
|
||||
/// </summary>
|
||||
public bool IsInventoryPickingUp;
|
||||
|
||||
public Items(IPacketSender packetSender, Entities entities, EntityMetadataManager entityMetadataManager)
|
||||
{
|
||||
this.packetSender = packetSender;
|
||||
this.entities = entities;
|
||||
this.entityMetadataManager = entityMetadataManager;
|
||||
}
|
||||
|
||||
public void PickedUp(GameObject gameObject, TechType techType)
|
||||
{
|
||||
PickingUpObject = gameObject;
|
||||
|
||||
// Try catch to avoid blocking PickingUpObject with a non null value outside of the current context
|
||||
try
|
||||
{
|
||||
// Newly created objects are always placed into the player's inventory.
|
||||
if (!Player.main.TryGetNitroxId(out NitroxId playerId))
|
||||
{
|
||||
Log.ErrorOnce($"[{nameof(Items)}] Player has no id! Could not set parent of picked up item {gameObject.name}.");
|
||||
PickingUpObject = null;
|
||||
return;
|
||||
}
|
||||
|
||||
InventoryItemEntity inventoryItemEntity = ConvertToInventoryEntityUntracked(gameObject, playerId);
|
||||
|
||||
if (inventoryItemEntity.TechType.ToUnity() != techType)
|
||||
{
|
||||
Log.Warn($"Provided TechType: {techType} is different than the one automatically attributed to the item {inventoryItemEntity.TechType}");
|
||||
}
|
||||
|
||||
PickupItem pickupItem = new(inventoryItemEntity);
|
||||
|
||||
if (packetSender.Send(pickupItem))
|
||||
{
|
||||
Log.Debug($"Picked up item {inventoryItemEntity}");
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Log.Error(exception);
|
||||
}
|
||||
PickingUpObject = null;
|
||||
}
|
||||
|
||||
public void Planted(GameObject gameObject, NitroxId parentId)
|
||||
{
|
||||
InventoryItemEntity inventoryItemEntity = ConvertToInventoryEntityUntracked(gameObject, parentId);
|
||||
|
||||
if (packetSender.Send(new EntitySpawnedByClient(inventoryItemEntity, true)))
|
||||
{
|
||||
Log.Debug($"Planted item {inventoryItemEntity}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tracks the object (as dropped) and notifies the server to spawn the item for other players.
|
||||
/// </summary>
|
||||
public void Dropped(GameObject gameObject, TechType? techType = null)
|
||||
{
|
||||
techType ??= CraftData.GetTechType(gameObject);
|
||||
// there is a theoretical possibility of a stray remote tracking packet that re-adds the monobehavior, this is purely a safety call.
|
||||
RemoveAnyRemoteControl(gameObject);
|
||||
|
||||
// WaterParkCreatures need at least one ManagedUpdate to run so their data is correctly refreshed (isMature and timeNextBreed)
|
||||
if (gameObject.TryGetComponent(out WaterParkCreature waterParkCreature))
|
||||
{
|
||||
waterParkCreature.ManagedUpdate();
|
||||
}
|
||||
|
||||
NitroxId id = NitroxEntity.GetIdOrGenerateNew(gameObject);
|
||||
Optional<EntityMetadata> metadata = entityMetadataManager.Extract(gameObject);
|
||||
string classId = gameObject.GetComponent<PrefabIdentifier>().ClassId;
|
||||
|
||||
WorldEntity droppedItem;
|
||||
List<Entity> childrenEntities = GetPrefabChildren(gameObject, id, entityMetadataManager).ToList();
|
||||
|
||||
// If the item is dropped in a WaterPark we need to handle it differently
|
||||
NitroxId parentId = null;
|
||||
if (IsGlobalRootObject(gameObject) || (gameObject.GetComponent<Pickupable>() && TryGetParentWaterParkId(gameObject.transform.parent, out parentId)))
|
||||
{
|
||||
// We cast it to an entity type that is always seeable by clients
|
||||
// therefore, the packet will be redirected to everyone
|
||||
droppedItem = new GlobalRootEntity(gameObject.transform.ToLocalDto(), 0, classId, true, id, techType.Value.ToDto(), metadata.OrNull(), parentId, childrenEntities);
|
||||
}
|
||||
else if (gameObject.TryGetComponent(out OxygenPipe oxygenPipe))
|
||||
{
|
||||
// We can't spawn an OxygenPipe without its parent and root
|
||||
// Dropped patch is called in OxygenPipe.PlaceInWorld which is why OxygenPipe.ghostModel is valid
|
||||
IPipeConnection parentConnection = OxygenPipe.ghostModel.GetParent();
|
||||
if (parentConnection == null || !parentConnection.GetGameObject() ||
|
||||
!parentConnection.GetGameObject().TryGetNitroxId(out NitroxId parentPipeId))
|
||||
{
|
||||
Log.Error($"Couldn't find a valid reference to the OxygenPipe's parent pipe");
|
||||
return;
|
||||
}
|
||||
IPipeConnection rootConnection = parentConnection.GetRoot();
|
||||
if (rootConnection == null || !rootConnection.GetGameObject() ||
|
||||
!rootConnection.GetGameObject().TryGetNitroxId(out NitroxId rootPipeId))
|
||||
{
|
||||
Log.Error($"Couldn't find a valid reference to the OxygenPipe's root pipe");
|
||||
return;
|
||||
}
|
||||
|
||||
// Updating the local pipe's references to replace the UniqueIdentifier's id by their NitroxEntity's id
|
||||
oxygenPipe.rootPipeUID = rootPipeId.ToString();
|
||||
oxygenPipe.parentPipeUID = parentPipeId.ToString();
|
||||
|
||||
droppedItem = new OxygenPipeEntity(gameObject.transform.ToWorldDto(), 0, classId, false, id, techType.Value.ToDto(), metadata.OrNull(), null,
|
||||
childrenEntities, rootPipeId, parentPipeId, parentConnection.GetAttachPoint().ToDto());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Generic case
|
||||
droppedItem = new(gameObject.transform.ToWorldDto(), 0, classId, false, id, techType.Value.ToDto(), metadata.OrNull(), null, childrenEntities);
|
||||
}
|
||||
|
||||
if (packetSender.Send(new EntitySpawnedByClient(droppedItem, true)))
|
||||
{
|
||||
Log.Debug($"Dropping item: {droppedItem}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles objects placed as figures and posters, or LEDLights so that we can spawn them accordingly afterwards.
|
||||
/// </summary>
|
||||
public void Placed(GameObject gameObject, TechType techType)
|
||||
{
|
||||
RemoveAnyRemoteControl(gameObject);
|
||||
|
||||
NitroxId id = NitroxEntity.GetIdOrGenerateNew(gameObject);
|
||||
Optional<EntityMetadata> metadata = entityMetadataManager.Extract(gameObject);
|
||||
string classId = gameObject.GetComponent<PrefabIdentifier>().ClassId;
|
||||
|
||||
List<Entity> childrenEntities = GetPrefabChildren(gameObject, id, entityMetadataManager).ToList();
|
||||
WorldEntity placedItem;
|
||||
|
||||
// If the object is dropped in the water, it'll be parented to a CellRoot so we let it as WorldEntity (see Items.Dropped)
|
||||
// PlaceTool's object is located under GlobalRoot or under a CellRoot (we differentiate both by giving a different type)
|
||||
// Because objects under CellRoots must only spawn when visible while objects under GlobalRoot must be spawned at all times
|
||||
switch (gameObject.AliveOrNull())
|
||||
{
|
||||
case not null when IsGlobalRootObject(gameObject):
|
||||
placedItem = new GlobalRootEntity(gameObject.transform.ToWorldDto(), 0, classId, true, id, techType.ToDto(), metadata.OrNull(), null, childrenEntities);
|
||||
break;
|
||||
case not null when Player.main.AliveOrNull()?.GetCurrentSub().AliveOrNull()?.TryGetNitroxId(out NitroxId parentId) == true:
|
||||
placedItem = new GlobalRootEntity(gameObject.transform.ToLocalDto(), 0, classId, true, id, techType.ToDto(), metadata.OrNull(), parentId, childrenEntities);
|
||||
break;
|
||||
default:
|
||||
// If the object is not under a SubRoot nor in GlobalRoot, it'll be under a CellRoot but we still want to remember its state
|
||||
placedItem = new PlacedWorldEntity(gameObject.transform.ToWorldDto(), 0, classId, true, id, techType.ToDto(), metadata.OrNull(), null, childrenEntities);
|
||||
break;
|
||||
}
|
||||
|
||||
if (packetSender.Send(new EntitySpawnedByClient(placedItem, true)))
|
||||
{
|
||||
Log.Debug($"Placed object: {placedItem}");
|
||||
}
|
||||
}
|
||||
|
||||
// This function will record any notable children of the dropped item as a PrefabChildEntity. In this case, a 'notable'
|
||||
// child is one that UWE has tagged with a PrefabIdentifier (class id) and has entity metadata that can be extracted. An
|
||||
// example would be recording a Battery PrefabChild inside of a Flashlight WorldEntity.
|
||||
public static IEnumerable<Entity> GetPrefabChildren(GameObject gameObject, NitroxId parentId, EntityMetadataManager entityMetadataManager)
|
||||
{
|
||||
foreach (IGrouping<string, PrefabIdentifier> prefabGroup in gameObject.GetAllComponentsInChildren<PrefabIdentifier>()
|
||||
.Where(prefab => prefab.gameObject != gameObject)
|
||||
.GroupBy(prefab => prefab.classId))
|
||||
{
|
||||
int indexInGroup = 0;
|
||||
|
||||
foreach (PrefabIdentifier prefab in prefabGroup)
|
||||
{
|
||||
NitroxId id = NitroxEntity.GetIdOrGenerateNew(prefab.gameObject); // We do this here bc a MetadataExtractor could be requiring the id to increment or so
|
||||
Optional<EntityMetadata> metadata = entityMetadataManager.Extract(prefab.gameObject);
|
||||
|
||||
if (metadata.HasValue)
|
||||
{
|
||||
TechTag techTag = prefab.gameObject.GetComponent<TechTag>();
|
||||
TechType techType = (techTag) ? techTag.type : TechType.None;
|
||||
|
||||
yield return new PrefabChildEntity(id, prefab.classId, techType.ToDto(), indexInGroup, metadata.Value, parentId);
|
||||
|
||||
indexInGroup++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overloads <see cref="ConvertToInventoryItemEntity"/> and removes any tracking on <paramref name="gameObject"/>
|
||||
/// </summary>
|
||||
private InventoryItemEntity ConvertToInventoryEntityUntracked(GameObject gameObject, NitroxId parentId)
|
||||
{
|
||||
InventoryItemEntity inventoryItemEntity = ConvertToInventoryItemEntity(gameObject, parentId, entityMetadataManager);
|
||||
|
||||
// Some picked up entities are not known by the server for several reasons. First it can be picked up via a spawn item command. Another
|
||||
// example is that some obects are not 'real' objects until they are clicked and end up spawning a prefab. For example, the fire extinguisher
|
||||
// in the escape pod (mono: IntroFireExtinguisherHandTarget) or Creepvine seeds (mono: PickupPrefab). When clicked, these spawn new prefabs
|
||||
// directly into the player's inventory. These will ultimately be registered server side with the above inventoryItemEntity.
|
||||
entities.MarkAsSpawned(inventoryItemEntity);
|
||||
|
||||
// We want to remove any remote tracking immediately on pickup as it can cause weird behavior like holding a ghost item still in the world.
|
||||
RemoveAnyRemoteControl(gameObject);
|
||||
EntityPositionBroadcaster.StopWatchingEntity(inventoryItemEntity.Id);
|
||||
|
||||
return inventoryItemEntity;
|
||||
}
|
||||
|
||||
public static InventoryItemEntity ConvertToInventoryItemEntity(GameObject gameObject, NitroxId parentId, EntityMetadataManager entityMetadataManager)
|
||||
{
|
||||
NitroxId itemId = NitroxEntity.GetIdOrGenerateNew(gameObject); // id may not exist, create if missing
|
||||
string classId = gameObject.RequireComponent<PrefabIdentifier>().ClassId;
|
||||
TechType techType = gameObject.RequireComponent<Pickupable>().GetTechType();
|
||||
Optional<EntityMetadata> metadata = entityMetadataManager.Extract(gameObject);
|
||||
List<Entity> children = GetPrefabChildren(gameObject, itemId, entityMetadataManager).ToList();
|
||||
|
||||
InventoryItemEntity inventoryItemEntity = new(itemId, classId, techType.ToDto(), metadata.OrNull(), parentId, children);
|
||||
BatteryChildEntityHelper.TryPopulateInstalledBattery(gameObject, inventoryItemEntity.ChildEntities, itemId);
|
||||
|
||||
return inventoryItemEntity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Some items might be remotely simulated if they were dropped by other players. We'll want to remove
|
||||
/// any remote tracking when we actively handle the item.
|
||||
/// </summary>
|
||||
private void RemoveAnyRemoteControl(GameObject gameObject)
|
||||
{
|
||||
UnityEngine.Object.Destroy(gameObject.GetComponent<RemotelyControlled>());
|
||||
}
|
||||
|
||||
/// <param name="parent">Parent of the GameObject to check</param>
|
||||
public static bool TryGetParentWaterPark(Transform parent, out WaterPark waterPark)
|
||||
{
|
||||
// NB: When dropped in a WaterPark, items are placed under WaterPark/items_root/
|
||||
// So we need to search two steps higher to find the WaterPark
|
||||
if (parent && parent.parent && parent.parent.TryGetComponent(out waterPark))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
waterPark = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc cref="TryGetParentWaterPark" />
|
||||
private static bool TryGetParentWaterParkId(Transform parent, out NitroxId waterParkId)
|
||||
{
|
||||
if (TryGetParentWaterPark(parent, out WaterPark waterPark) && waterPark.TryGetNitroxId(out waterParkId))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
waterParkId = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static List<InstalledModuleEntity> GetEquipmentModuleEntities(Equipment equipment, NitroxId equipmentId, EntityMetadataManager entityMetadataManager)
|
||||
{
|
||||
List<InstalledModuleEntity> entities = new();
|
||||
foreach (KeyValuePair<string, InventoryItem> itemEntry in equipment.equipment)
|
||||
{
|
||||
InventoryItem item = itemEntry.Value;
|
||||
if (item != null)
|
||||
{
|
||||
Pickupable pickupable = item.item;
|
||||
string classId = pickupable.RequireComponent<PrefabIdentifier>().ClassId;
|
||||
NitroxId itemId = NitroxEntity.GetIdOrGenerateNew(pickupable.gameObject);
|
||||
Optional<EntityMetadata> metadata = entityMetadataManager.Extract(pickupable.gameObject);
|
||||
List<Entity> children = GetPrefabChildren(pickupable.gameObject, itemId, entityMetadataManager).ToList();
|
||||
|
||||
entities.Add(new(itemEntry.Key, classId, itemId, pickupable.GetTechType().ToDto(), metadata.OrNull(), equipmentId, children));
|
||||
}
|
||||
}
|
||||
return entities;
|
||||
}
|
||||
|
||||
private static bool IsGlobalRootObject(GameObject gameObject)
|
||||
{
|
||||
return gameObject.TryGetComponent(out LargeWorldEntity largeWorldEntity) &&
|
||||
largeWorldEntity.initialCellLevel == LargeWorldEntity.CellLevel.Global;
|
||||
}
|
||||
}
|
112
NitroxClient/GameLogic/LiveMixinManager.cs
Normal file
112
NitroxClient/GameLogic/LiveMixinManager.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NitroxModel.DataStructures;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic;
|
||||
|
||||
public class LiveMixinManager
|
||||
{
|
||||
private readonly SimulationOwnership simulationOwnership;
|
||||
private static readonly HashSet<string> broadcastDeathClassIdWhitelist = new()
|
||||
{
|
||||
"7d307502-46b7-4f86-afb0-65fe8867f893" // Crash (fish)
|
||||
};
|
||||
|
||||
public bool IsRemoteHealthChanging { get; private set; }
|
||||
|
||||
public LiveMixinManager(SimulationOwnership simulationOwnership)
|
||||
{
|
||||
this.simulationOwnership = simulationOwnership;
|
||||
}
|
||||
|
||||
// Currently, we only apply live mixin updates to vehicles as there is more work to implement
|
||||
// damage for regular entities like fish.
|
||||
public bool IsWhitelistedUpdateType(LiveMixin entity)
|
||||
{
|
||||
Vehicle vehicle = entity.GetComponent<Vehicle>();
|
||||
SubRoot subRoot = entity.GetComponent<SubRoot>();
|
||||
|
||||
return (vehicle || (subRoot && subRoot.isCyclops));
|
||||
}
|
||||
|
||||
public bool ShouldBroadcastDeath(LiveMixin liveMixin)
|
||||
{
|
||||
if (liveMixin.TryGetComponent(out UniqueIdentifier uniqueIdentifier) && !string.IsNullOrEmpty(uniqueIdentifier.classId))
|
||||
{
|
||||
return broadcastDeathClassIdWhitelist.Contains(uniqueIdentifier.classId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ShouldApplyNextHealthUpdate(LiveMixin receiver, GameObject dealer = null)
|
||||
{
|
||||
if (!receiver.TryGetNitroxId(out NitroxId id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!simulationOwnership.HasAnyLockType(id) && !IsRemoteHealthChanging)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Check to see if this health change is caused by docked vehicle collisions. If so, we don't want to apply it.
|
||||
if (!dealer)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
Vehicle dealerVehicle = dealer.GetComponent<Vehicle>();
|
||||
VehicleDockingBay vehicleDockingBay = receiver.GetComponentInChildren<VehicleDockingBay>();
|
||||
|
||||
if (vehicleDockingBay && dealerVehicle)
|
||||
{
|
||||
if (vehicleDockingBay.GetDockedVehicle() == dealerVehicle ||
|
||||
vehicleDockingBay.interpolatingVehicle == dealerVehicle ||
|
||||
vehicleDockingBay.nearbyVehicle == dealerVehicle)
|
||||
{
|
||||
Log.Debug($"Dealer {dealer} is vehicle and currently docked or nearby {receiver}, do not harm it!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void SyncRemoteHealth(LiveMixin liveMixin, float remoteHealth, Vector3 position = default, DamageType damageType = DamageType.Normal)
|
||||
{
|
||||
if (liveMixin.health == remoteHealth)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float difference = remoteHealth - liveMixin.health;
|
||||
|
||||
IsRemoteHealthChanging = true;
|
||||
|
||||
// We catch the exceptions here because we don't want IsRemoteHealthChanging to be stuck to true
|
||||
try
|
||||
{
|
||||
if (difference < 0)
|
||||
{
|
||||
liveMixin.TakeDamage(difference, position, damageType);
|
||||
}
|
||||
else
|
||||
{
|
||||
liveMixin.AddHealth(difference);
|
||||
}
|
||||
} catch (Exception e)
|
||||
{
|
||||
Log.Error(e, $"Encountered an expcetion while processing health update");
|
||||
}
|
||||
|
||||
IsRemoteHealthChanging = false;
|
||||
|
||||
// We mainly only do the above to trigger damage effects and sounds. After those, we sync the remote value
|
||||
// to ensure that any floating point discrepencies aren't an issue.
|
||||
liveMixin.health = remoteHealth;
|
||||
}
|
||||
}
|
145
NitroxClient/GameLogic/LocalPlayer.cs
Normal file
145
NitroxClient/GameLogic/LocalPlayer.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using System;
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract;
|
||||
using NitroxClient.MonoBehaviours;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.DataStructures.GameLogic;
|
||||
using NitroxModel.DataStructures.Util;
|
||||
using NitroxModel.MultiplayerSession;
|
||||
using NitroxModel.Packets;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace NitroxClient.GameLogic;
|
||||
|
||||
public class LocalPlayer : ILocalNitroxPlayer
|
||||
{
|
||||
private readonly IMultiplayerSession multiplayerSession;
|
||||
private readonly IPacketSender packetSender;
|
||||
private readonly ThrottledPacketSender throttledPacketSender;
|
||||
private readonly Lazy<GameObject> body;
|
||||
private readonly Lazy<GameObject> playerModel;
|
||||
private readonly Lazy<GameObject> bodyPrototype;
|
||||
|
||||
public GameObject Body => body.Value;
|
||||
|
||||
public GameObject PlayerModel => playerModel.Value;
|
||||
|
||||
public GameObject BodyPrototype => bodyPrototype.Value;
|
||||
|
||||
public string PlayerName => multiplayerSession.AuthenticationContext.Username;
|
||||
/// <summary>
|
||||
/// Gets the player id. The session is lost on disconnect so this can return null.
|
||||
/// </summary>
|
||||
public ushort? PlayerId => multiplayerSession?.Reservation?.PlayerId;
|
||||
public PlayerSettings PlayerSettings => multiplayerSession.PlayerSettings;
|
||||
|
||||
public Perms Permissions { get; set; }
|
||||
public IntroCinematicMode IntroCinematicMode { get; set; }
|
||||
public bool KeepInventoryOnDeath { get; set; }
|
||||
|
||||
public LocalPlayer(IMultiplayerSession multiplayerSession, IPacketSender packetSender, ThrottledPacketSender throttledPacketSender)
|
||||
{
|
||||
this.multiplayerSession = multiplayerSession;
|
||||
this.packetSender = packetSender;
|
||||
this.throttledPacketSender = throttledPacketSender;
|
||||
body = new Lazy<GameObject>(() => Player.main.RequireGameObject("body"));
|
||||
playerModel = new Lazy<GameObject>(() => Body.RequireGameObject("player_view"));
|
||||
bodyPrototype = new Lazy<GameObject>(CreateBodyPrototype);
|
||||
Permissions = Perms.PLAYER;
|
||||
IntroCinematicMode = IntroCinematicMode.NONE;
|
||||
KeepInventoryOnDeath = false;
|
||||
}
|
||||
|
||||
public void BroadcastLocation(Vector3 location, Vector3 velocity, Quaternion bodyRotation, Quaternion aimingRotation)
|
||||
{
|
||||
if (!PlayerId.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerMovement playerMovement = new(PlayerId.Value, location.ToDto(), velocity.ToDto(), bodyRotation.ToDto(), aimingRotation.ToDto());
|
||||
|
||||
packetSender.Send(playerMovement);
|
||||
}
|
||||
|
||||
public void AnimationChange(AnimChangeType type, AnimChangeState state)
|
||||
{
|
||||
if (PlayerId.HasValue)
|
||||
{
|
||||
packetSender.Send(new AnimationChangeEvent(PlayerId.Value, (int)type, (int)state));
|
||||
}
|
||||
}
|
||||
|
||||
public void BroadcastStats(float oxygen, float maxOxygen, float health, float food, float water, float infectionAmount)
|
||||
{
|
||||
if (PlayerId.HasValue)
|
||||
{
|
||||
packetSender.Send(new PlayerStats(PlayerId.Value, oxygen, maxOxygen, health, food, water, infectionAmount));
|
||||
}
|
||||
}
|
||||
|
||||
public void BroadcastDeath(Vector3 deathPosition)
|
||||
{
|
||||
if (PlayerId.HasValue)
|
||||
{
|
||||
packetSender.Send(new PlayerDeathEvent(PlayerId.Value, deathPosition.ToDto()));
|
||||
}
|
||||
}
|
||||
|
||||
public void BroadcastSubrootChange(Optional<NitroxId> subrootId)
|
||||
{
|
||||
if (PlayerId.HasValue)
|
||||
{
|
||||
packetSender.Send(new SubRootChanged(PlayerId.Value, subrootId));
|
||||
}
|
||||
}
|
||||
|
||||
public void BroadcastEscapePodChange(Optional<NitroxId> escapePodId)
|
||||
{
|
||||
if (PlayerId.HasValue)
|
||||
{
|
||||
packetSender.Send(new EscapePodChanged(PlayerId.Value, escapePodId));
|
||||
}
|
||||
}
|
||||
|
||||
public void BroadcastWeld(NitroxId id, float healthAdded) => packetSender.Send(new WeldAction(id, healthAdded));
|
||||
|
||||
public void BroadcastHeldItemChanged(NitroxId itemId, PlayerHeldItemChanged.ChangeType techType, NitroxTechType isFirstTime)
|
||||
{
|
||||
if (PlayerId.HasValue)
|
||||
{
|
||||
packetSender.Send(new PlayerHeldItemChanged(PlayerId.Value, itemId, techType, isFirstTime));
|
||||
}
|
||||
}
|
||||
|
||||
public void BroadcastQuickSlotsBindingChanged(Optional<NitroxId>[] slotItemIds) => throttledPacketSender.SendThrottled(new PlayerQuickSlotsBindingChanged(slotItemIds), (packet) => 1);
|
||||
|
||||
private GameObject CreateBodyPrototype()
|
||||
{
|
||||
GameObject prototype = Body;
|
||||
|
||||
// Cheap fix for showing head, much easier since male_geo contains many different heads
|
||||
prototype.GetComponentInParent<Player>().head.shadowCastingMode = ShadowCastingMode.On;
|
||||
GameObject clone = Object.Instantiate(prototype, Multiplayer.Main.transform, false);
|
||||
prototype.GetComponentInParent<Player>().head.shadowCastingMode = ShadowCastingMode.ShadowsOnly;
|
||||
|
||||
clone.SetActive(false);
|
||||
clone.name = "RemotePlayerPrototype";
|
||||
|
||||
// Removing items that are held in hand
|
||||
foreach (Transform child in clone.transform.Find($"player_view/{PlayerEquipmentConstants.ITEM_ATTACH_POINT_GAME_OBJECT_NAME}"))
|
||||
{
|
||||
if (!child.gameObject.name.Contains("attach1_"))
|
||||
{
|
||||
Object.DestroyImmediate(child.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
return clone;
|
||||
}
|
||||
}
|
27
NitroxClient/GameLogic/MedkitFabricator.cs
Normal file
27
NitroxClient/GameLogic/MedkitFabricator.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.Packets;
|
||||
|
||||
namespace NitroxClient.GameLogic
|
||||
{
|
||||
public class MedkitFabricator
|
||||
{
|
||||
private readonly IPacketSender packetSender;
|
||||
|
||||
public MedkitFabricator(IPacketSender packetSender)
|
||||
{
|
||||
this.packetSender = packetSender;
|
||||
}
|
||||
|
||||
public void Clicked(MedicalCabinet medicalCabinet)
|
||||
{
|
||||
if (!medicalCabinet.TryGetIdOrWarn(out NitroxId id))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MedicalCabinetClicked cabinetClicked = new(id, medicalCabinet.doorOpen, medicalCabinet.hasMedKit, medicalCabinet.timeSpawnMedKit);
|
||||
packetSender.Send(cabinetClicked);
|
||||
}
|
||||
}
|
||||
}
|
50
NitroxClient/GameLogic/MobileVehicleBay.cs
Normal file
50
NitroxClient/GameLogic/MobileVehicleBay.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxClient.MonoBehaviours;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.DataStructures.GameLogic.Entities;
|
||||
using NitroxModel.Packets;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic;
|
||||
|
||||
public class MobileVehicleBay
|
||||
{
|
||||
public static bool TransmitLocalSpawns { get; set; } = true;
|
||||
public static GameObject MostRecentlyCrafted { get; set; }
|
||||
|
||||
private readonly IPacketSender packetSender;
|
||||
private readonly Vehicles vehicles;
|
||||
|
||||
public MobileVehicleBay(IPacketSender packetSender, Vehicles vehicles)
|
||||
{
|
||||
this.packetSender = packetSender;
|
||||
this.vehicles = vehicles;
|
||||
}
|
||||
|
||||
public void BeginCrafting(ConstructorInput constructor, GameObject constructedObject, TechType techType, float duration)
|
||||
{
|
||||
MostRecentlyCrafted = constructedObject;
|
||||
|
||||
// Sometimes build templates, such as the cyclops, are already tagged with IDs. Remove any that exist to retag.
|
||||
// TODO: this seems to happen because various patches execute when the cyclops template loads (on game load).
|
||||
// This will leave vehicles with NitroxEntity but an empty NitroxId. We need to chase these down and only call
|
||||
// the code paths when the owner has a simulation lock.
|
||||
Vehicles.RemoveNitroxEntitiesTagging(constructedObject);
|
||||
|
||||
if (!TransmitLocalSpawns)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
constructor.constructor.TryGetIdOrWarn(out NitroxId constructorId);
|
||||
|
||||
NitroxId constructedObjectId = NitroxEntity.GenerateNewId(constructedObject);
|
||||
|
||||
VehicleWorldEntity vehicleEntity = Vehicles.BuildVehicleWorldEntity(constructedObject, constructedObjectId, techType, constructorId);
|
||||
|
||||
packetSender.Send(new EntitySpawnedByClient(vehicleEntity));
|
||||
// TODO: Fix remote players treating the SimulationOwnership change on the vehicle (they can't find it) even tho they're still in the
|
||||
// process of spawning the said vehicle because it's done over multiple frames, while the SimulationOwnership packet is received
|
||||
// right after the spawning started (so the processor won't find its target)
|
||||
}
|
||||
}
|
75
NitroxClient/GameLogic/MovementHelper.cs
Normal file
75
NitroxClient/GameLogic/MovementHelper.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic
|
||||
{
|
||||
public static class MovementHelper
|
||||
{
|
||||
public static void MoveRotateGameObject(GameObject go, Vector3 position, Quaternion rotation, float time)
|
||||
{
|
||||
MoveGameObject(go, position, time);
|
||||
RotateGameObject(go, rotation, time);
|
||||
}
|
||||
|
||||
public static void MoveGameObject(GameObject go, Vector3 position, float time)
|
||||
{
|
||||
iTween.MoveTo(go, iTween.Hash("position", position,
|
||||
"easetype", iTween.EaseType.easeInOutSine,
|
||||
"time", time));
|
||||
}
|
||||
|
||||
public static void RotateGameObject(GameObject go, Quaternion rotation, float time)
|
||||
{
|
||||
iTween.RotateTo(go, iTween.Hash("rotation", rotation.eulerAngles,
|
||||
"easetype", iTween.EaseType.easeInOutSine,
|
||||
"time", time));
|
||||
}
|
||||
|
||||
public static void Stop(GameObject go)
|
||||
{
|
||||
iTween.Stop(go);
|
||||
}
|
||||
|
||||
public static Vector3 GetCorrectedVelocity(Vector3 remotePosition, Vector3 remoteVelocity, GameObject gameObject, float correctionTime)
|
||||
{
|
||||
Vector3 difference = remotePosition - gameObject.transform.position;
|
||||
Vector3 velocityToMakeUpDifference = difference / correctionTime;
|
||||
|
||||
float distance = difference.magnitude;
|
||||
|
||||
if (distance > 20f)
|
||||
{
|
||||
// This should be a one-off teleport.
|
||||
gameObject.transform.position = remotePosition;
|
||||
}
|
||||
else
|
||||
{
|
||||
remoteVelocity = velocityToMakeUpDifference;
|
||||
}
|
||||
|
||||
return remoteVelocity;
|
||||
}
|
||||
|
||||
public static Vector3 GetCorrectedAngularVelocity(Quaternion remoteRotation, Vector3 angularVelocty, GameObject gameObject, float correctionTime)
|
||||
{
|
||||
Quaternion delta = remoteRotation * gameObject.transform.rotation.GetInverse();
|
||||
|
||||
delta.ToAngleAxis(out float angle, out Vector3 axis);
|
||||
|
||||
// We get an infinite axis in the event that our rotation is already aligned.
|
||||
if (float.IsInfinity(axis.x))
|
||||
{
|
||||
return angularVelocty;
|
||||
}
|
||||
|
||||
if (angle > 180f)
|
||||
{
|
||||
angle -= 360f;
|
||||
}
|
||||
|
||||
// Here I drop down to 0.9f times the desired movement,
|
||||
// since we'd rather undershoot and ease into the correct angle
|
||||
// than overshoot and oscillate around it in the event of errors.
|
||||
return (.9f * Mathf.Deg2Rad * angle / correctionTime) * axis + angularVelocty;
|
||||
}
|
||||
}
|
||||
}
|
83
NitroxClient/GameLogic/NitroxConsole.cs
Normal file
83
NitroxClient/GameLogic/NitroxConsole.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using System;
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxClient.MonoBehaviours;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.DataStructures.GameLogic.Entities;
|
||||
using NitroxModel.Packets;
|
||||
using NitroxModel_Subnautica.Helper;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic
|
||||
{
|
||||
public class NitroxConsole
|
||||
{
|
||||
public static bool DisableConsole { get; set; } = true;
|
||||
|
||||
private readonly IPacketSender packetSender;
|
||||
private readonly Items items;
|
||||
|
||||
public NitroxConsole(IPacketSender packetSender, Items items)
|
||||
{
|
||||
this.packetSender = packetSender;
|
||||
this.items = items;
|
||||
}
|
||||
|
||||
//List of things that can be spawned : https://subnauticacommands.com/items
|
||||
public void Spawn(GameObject gameObject)
|
||||
{
|
||||
TechType techType = GetObjectTechType(gameObject);
|
||||
|
||||
try
|
||||
{
|
||||
if (VehicleHelper.IsVehicle(techType))
|
||||
{
|
||||
SpawnVehicle(gameObject, techType);
|
||||
}
|
||||
else
|
||||
{
|
||||
DefaultSpawn(gameObject);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, $"Error while trying to spawn {techType} from devconsole");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns Seamoth, Exosuit or Cyclops
|
||||
/// </summary>
|
||||
private void SpawnVehicle(GameObject gameObject, TechType techType)
|
||||
{
|
||||
NitroxId id = NitroxEntity.GetIdOrGenerateNew(gameObject);
|
||||
|
||||
VehicleWorldEntity vehicleEntity = Vehicles.BuildVehicleWorldEntity(gameObject, id, techType);
|
||||
|
||||
packetSender.Send(new EntitySpawnedByClient(vehicleEntity));
|
||||
|
||||
Log.Debug($"Spawning vehicle {techType} with id {id} at {gameObject.transform.position}");
|
||||
}
|
||||
|
||||
private void DefaultSpawn(GameObject gameObject)
|
||||
{
|
||||
items.Dropped(gameObject);
|
||||
}
|
||||
|
||||
private static TechType GetObjectTechType(GameObject gameObject)
|
||||
{
|
||||
TechType techType = CraftData.GetTechType(gameObject);
|
||||
if (techType != TechType.None)
|
||||
{
|
||||
return techType;
|
||||
}
|
||||
|
||||
// Cyclops' GameObject doesn't have a way to give its a TechType so we detect it differently
|
||||
if (gameObject.TryGetComponent(out SubRoot subRoot) && subRoot.isCyclops)
|
||||
{
|
||||
return TechType.Cyclops;
|
||||
}
|
||||
|
||||
return TechType.None;
|
||||
}
|
||||
}
|
||||
}
|
75
NitroxClient/GameLogic/PlayerLogic/PlayerCinematics.cs
Normal file
75
NitroxClient/GameLogic/PlayerLogic/PlayerCinematics.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System.Collections.Generic;
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxClient.MonoBehaviours;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.DataStructures.GameLogic;
|
||||
using NitroxModel.Packets;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic;
|
||||
|
||||
public class PlayerCinematics
|
||||
{
|
||||
private readonly IPacketSender packetSender;
|
||||
private readonly LocalPlayer localPlayer;
|
||||
|
||||
private IntroCinematicMode lastModeToSend = IntroCinematicMode.NONE;
|
||||
|
||||
public ushort? IntroCinematicPartnerId = null;
|
||||
|
||||
/// <summary>
|
||||
/// Some cinematics should not be played. Example the intro as it's completely handled by a dedicated system.
|
||||
/// </summary>
|
||||
private readonly HashSet<string> blacklistedKeys = ["escapepod_intro", "reaper_attack"];
|
||||
|
||||
public PlayerCinematics(IPacketSender packetSender, LocalPlayer localPlayer)
|
||||
{
|
||||
this.packetSender = packetSender;
|
||||
this.localPlayer = localPlayer;
|
||||
}
|
||||
|
||||
public void StartCinematicMode(ushort playerId, NitroxId controllerID, int controllerNameHash, string key)
|
||||
{
|
||||
if (!blacklistedKeys.Contains(key))
|
||||
{
|
||||
packetSender.Send(new PlayerCinematicControllerCall(playerId, controllerID, controllerNameHash, key, true));
|
||||
}
|
||||
}
|
||||
|
||||
public void EndCinematicMode(ushort playerId, NitroxId controllerID, int controllerNameHash, string key)
|
||||
{
|
||||
if (!blacklistedKeys.Contains(key))
|
||||
{
|
||||
packetSender.Send(new PlayerCinematicControllerCall(playerId, controllerID, controllerNameHash, key, false));
|
||||
}
|
||||
}
|
||||
|
||||
public void SetLocalIntroCinematicMode(IntroCinematicMode introCinematicMode)
|
||||
{
|
||||
if (!localPlayer.PlayerId.HasValue)
|
||||
{
|
||||
Log.Error($"PlayerId was null while setting IntroCinematicMode to {introCinematicMode}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (localPlayer.IntroCinematicMode == introCinematicMode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
localPlayer.IntroCinematicMode = introCinematicMode;
|
||||
|
||||
// This method can be called before client is joined. To prevent sending as an unauthenticated packet we delay it.
|
||||
if (Multiplayer.Joined)
|
||||
{
|
||||
packetSender.Send(new SetIntroCinematicMode(localPlayer.PlayerId.Value, introCinematicMode));
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastModeToSend == IntroCinematicMode.NONE)
|
||||
{
|
||||
Multiplayer.OnLoadingComplete += () => SetLocalIntroCinematicMode(lastModeToSend);
|
||||
}
|
||||
|
||||
lastModeToSend = introCinematicMode;
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract
|
||||
{
|
||||
public interface IColorSwapManager
|
||||
{
|
||||
Action<ColorSwapAsyncOperation> CreateColorSwapTask(INitroxPlayer nitroxPlayer);
|
||||
void ApplyPlayerColor(Dictionary<string, Color[]> pixelIndex, INitroxPlayer nitroxPlayer);
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract
|
||||
{
|
||||
public interface IColorSwapStrategy
|
||||
{
|
||||
Color SwapColor(Color originalColor);
|
||||
}
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract
|
||||
{
|
||||
public interface ILocalNitroxPlayer : INitroxPlayer
|
||||
{
|
||||
//This serves as a "clean" player model to clone from. The root player model is going to be recolored as well
|
||||
//which would change our HSV filter parameters. Who wants to hit a moving target?
|
||||
GameObject BodyPrototype { get; }
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
using NitroxModel.MultiplayerSession;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract
|
||||
{
|
||||
public interface INitroxPlayer
|
||||
{
|
||||
GameObject Body { get; }
|
||||
GameObject PlayerModel { get; }
|
||||
string PlayerName { get; }
|
||||
PlayerSettings PlayerSettings { get; }
|
||||
}
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract
|
||||
{
|
||||
public interface IPlayerModelBuilder
|
||||
{
|
||||
void Build(INitroxPlayer player);
|
||||
}
|
||||
}
|
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap
|
||||
{
|
||||
public class ColorSwapAsyncOperation
|
||||
{
|
||||
private readonly INitroxPlayer nitroxPlayer;
|
||||
private readonly IEnumerable<IColorSwapManager> colorSwapManagers;
|
||||
private readonly Dictionary<string, Color[]> texturePixelIndexes;
|
||||
private int taskCount = -1;
|
||||
|
||||
public ColorSwapAsyncOperation(INitroxPlayer nitroxPlayer, IEnumerable<IColorSwapManager> colorSwapManagers)
|
||||
{
|
||||
this.nitroxPlayer = nitroxPlayer;
|
||||
this.colorSwapManagers = colorSwapManagers;
|
||||
|
||||
texturePixelIndexes = new Dictionary<string, Color[]>();
|
||||
}
|
||||
|
||||
public void UpdateIndex(string indexKey, Color[] pixels)
|
||||
{
|
||||
lock (texturePixelIndexes)
|
||||
{
|
||||
if (texturePixelIndexes.ContainsKey(indexKey))
|
||||
{
|
||||
throw new ArgumentException($"Texture index key {indexKey} already exists.");
|
||||
}
|
||||
|
||||
texturePixelIndexes.Add(indexKey, pixels);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsColorSwapComplete()
|
||||
{
|
||||
return taskCount == 0;
|
||||
}
|
||||
|
||||
public ColorSwapAsyncOperation BeginColorSwap()
|
||||
{
|
||||
if (taskCount >= 0)
|
||||
{
|
||||
throw new InvalidOperationException("This operation has already been started.");
|
||||
}
|
||||
|
||||
List<Action<ColorSwapAsyncOperation>> tasks = colorSwapManagers
|
||||
.Select(configuration => configuration.CreateColorSwapTask(nitroxPlayer))
|
||||
.ToList();
|
||||
|
||||
taskCount = tasks.Count;
|
||||
tasks.ForEach(task => ThreadPool.QueueUserWorkItem(ExecuteTask, task));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public void ApplySwappedColors()
|
||||
{
|
||||
if (taskCount != 0)
|
||||
{
|
||||
throw new InvalidOperationException("Colors must be swapped before the changes can be applied to the player model.");
|
||||
}
|
||||
|
||||
colorSwapManagers.ForEach(manager => manager.ApplyPlayerColor(texturePixelIndexes, nitroxPlayer));
|
||||
}
|
||||
|
||||
private void ExecuteTask(object state)
|
||||
{
|
||||
if (state is not Action<ColorSwapAsyncOperation> task)
|
||||
{
|
||||
//TODO: We need to handle job cancellation during stabilization to ensure that the client shuts down gracefully.
|
||||
throw new ArgumentException("Cannot execute a null task.", nameof(state));
|
||||
}
|
||||
|
||||
task.Invoke(this);
|
||||
Interlocked.Decrement(ref taskCount);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap
|
||||
{
|
||||
public class ColorValueRange
|
||||
{
|
||||
private readonly float min;
|
||||
private readonly float max;
|
||||
|
||||
public ColorValueRange(float min, float max)
|
||||
{
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
}
|
||||
|
||||
public bool Covers(float targetValue)
|
||||
{
|
||||
return min <= targetValue && targetValue <= max;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap.Strategy;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using UnityEngine;
|
||||
using static NitroxClient.GameLogic.PlayerLogic.PlayerModel.PlayerEquipmentConstants;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap
|
||||
{
|
||||
public class DiveSuitColorSwapManager : IColorSwapManager
|
||||
{
|
||||
public Action<ColorSwapAsyncOperation> CreateColorSwapTask(INitroxPlayer nitroxPlayer)
|
||||
{
|
||||
GameObject playerModel = nitroxPlayer.PlayerModel;
|
||||
Color playerColor = nitroxPlayer.PlayerSettings.PlayerColor.ToUnity();
|
||||
IColorSwapStrategy colorSwapStrategy = new HueSwapper(playerColor);
|
||||
|
||||
SkinnedMeshRenderer diveSuitRenderer = playerModel.GetRenderer(DIVE_SUIT_GAME_OBJECT_NAME);
|
||||
diveSuitRenderer.material.ApplyClonedTexture();
|
||||
diveSuitRenderer.materials[1].ApplyClonedTexture();
|
||||
|
||||
Color[] bodyTexturePixels = diveSuitRenderer.material.GetMainTexturePixels();
|
||||
Color[] armTexturePixels = diveSuitRenderer.materials[1].GetMainTexturePixels();
|
||||
|
||||
return operation =>
|
||||
{
|
||||
HsvSwapper diveSuitFilter = new HsvSwapper(colorSwapStrategy);
|
||||
diveSuitFilter.SetHueRange(5f, 45f);
|
||||
|
||||
diveSuitFilter.SwapColors(bodyTexturePixels);
|
||||
diveSuitFilter.SwapColors(armTexturePixels);
|
||||
|
||||
operation.UpdateIndex(DIVE_SUIT_INDEX_KEY, bodyTexturePixels);
|
||||
operation.UpdateIndex(DIVE_SUIT_ARMS_INDEX_KEY, armTexturePixels);
|
||||
};
|
||||
}
|
||||
|
||||
public void ApplyPlayerColor(Dictionary<string, Color[]> pixelIndex, INitroxPlayer nitroxPlayer)
|
||||
{
|
||||
Color[] bodyPixels = pixelIndex[DIVE_SUIT_INDEX_KEY];
|
||||
Color[] armSleevesPixels = pixelIndex[DIVE_SUIT_ARMS_INDEX_KEY];
|
||||
|
||||
GameObject playerModel = nitroxPlayer.PlayerModel;
|
||||
SkinnedMeshRenderer renderer = playerModel.GetRenderer(DIVE_SUIT_GAME_OBJECT_NAME);
|
||||
|
||||
Material torsoMaterial = renderer.material;
|
||||
torsoMaterial.UpdateMainTextureColors(bodyPixels);
|
||||
|
||||
Material armsMaterial = renderer.materials[1];
|
||||
armsMaterial.UpdateMainTextureColors(armSleevesPixels);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap.Strategy;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using UnityEngine;
|
||||
using static NitroxClient.GameLogic.PlayerLogic.PlayerModel.PlayerEquipmentConstants;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap
|
||||
{
|
||||
public class FinColorSwapManager : IColorSwapManager
|
||||
{
|
||||
public Action<ColorSwapAsyncOperation> CreateColorSwapTask(INitroxPlayer nitroxPlayer)
|
||||
{
|
||||
GameObject playerModel = nitroxPlayer.PlayerModel;
|
||||
Color playerColor = nitroxPlayer.PlayerSettings.PlayerColor.ToUnity();
|
||||
IColorSwapStrategy colorSwapStrategy = new HueSwapper(playerColor);
|
||||
|
||||
SkinnedMeshRenderer basicFinRenderer = playerModel.GetRenderer(FINS_GAME_OBJECT_NAME);
|
||||
basicFinRenderer.material.ApplyClonedTexture();
|
||||
|
||||
SkinnedMeshRenderer chargedFinRenderer = playerModel.GetRenderer(CHARGED_FINS_GAME_OBJECT_NAME);
|
||||
chargedFinRenderer.material.ApplyClonedTexture();
|
||||
|
||||
SkinnedMeshRenderer glideFinRenderer = playerModel.GetRenderer(GLIDE_FINS_GAME_OBJECT_NAME);
|
||||
glideFinRenderer.material.ApplyClonedTexture();
|
||||
|
||||
//All fin models use the same texture.
|
||||
Color[] texturePixels = basicFinRenderer.material.GetMainTexturePixels();
|
||||
|
||||
return operation =>
|
||||
{
|
||||
HsvSwapper finFilter = new HsvSwapper(colorSwapStrategy);
|
||||
finFilter.SetHueRange(0f, 35f);
|
||||
|
||||
finFilter.SwapColors(texturePixels);
|
||||
|
||||
operation.UpdateIndex(FINS_INDEX_KEY, texturePixels);
|
||||
};
|
||||
}
|
||||
|
||||
public void ApplyPlayerColor(Dictionary<string, Color[]> pixelIndex, INitroxPlayer nitroxPlayer)
|
||||
{
|
||||
Color[] pixels = pixelIndex[FINS_INDEX_KEY];
|
||||
|
||||
GameObject playerModel = nitroxPlayer.PlayerModel;
|
||||
|
||||
SkinnedMeshRenderer basicFinRenderer = playerModel.GetRenderer(FINS_GAME_OBJECT_NAME);
|
||||
basicFinRenderer.material.UpdateMainTextureColors(pixels);
|
||||
basicFinRenderer.material.SetTexture("_MainTex", basicFinRenderer.material.mainTexture);
|
||||
basicFinRenderer.material.SetTexture("_SpecTex", basicFinRenderer.material.mainTexture);
|
||||
|
||||
SkinnedMeshRenderer chargedFinRenderer = playerModel.GetRenderer(CHARGED_FINS_GAME_OBJECT_NAME);
|
||||
chargedFinRenderer.material.UpdateMainTextureColors(pixels);
|
||||
chargedFinRenderer.material.SetTexture("_MainTex", chargedFinRenderer.material.mainTexture);
|
||||
chargedFinRenderer.material.SetTexture("_SpecTex", chargedFinRenderer.material.mainTexture);
|
||||
|
||||
SkinnedMeshRenderer glideFinRenderer = playerModel.GetRenderer(GLIDE_FINS_GAME_OBJECT_NAME);
|
||||
glideFinRenderer.material.UpdateMainTextureColors(pixels);
|
||||
glideFinRenderer.material.SetTexture("_MainTex", glideFinRenderer.material.mainTexture);
|
||||
glideFinRenderer.material.SetTexture("_SpecTex", glideFinRenderer.material.mainTexture);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,75 @@
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap
|
||||
{
|
||||
public class HsvSwapper
|
||||
{
|
||||
private readonly IColorSwapStrategy colorSwapStrategy;
|
||||
|
||||
private ColorValueRange alphaValueRange;
|
||||
private ColorValueRange hueValueRange;
|
||||
private ColorValueRange saturationValueRange;
|
||||
private ColorValueRange vibrancyValueRange;
|
||||
|
||||
public HsvSwapper(IColorSwapStrategy colorSwapStrategy)
|
||||
{
|
||||
this.colorSwapStrategy = colorSwapStrategy;
|
||||
hueValueRange = new ColorValueRange(0f, 1f);
|
||||
saturationValueRange = new ColorValueRange(0f, 1f);
|
||||
vibrancyValueRange = new ColorValueRange(0f, 1f);
|
||||
alphaValueRange = new ColorValueRange(0f, 1f);
|
||||
}
|
||||
|
||||
public void SetHueRange(float minHue, float maxHue)
|
||||
{
|
||||
float newFilterMinValue = minHue < 1f ? minHue : minHue / 360f;
|
||||
float newFilterMaxValue = maxHue < 1f ? maxHue : maxHue / 360f;
|
||||
|
||||
hueValueRange = new ColorValueRange(newFilterMinValue, newFilterMaxValue);
|
||||
}
|
||||
|
||||
public void SetSaturationRange(float minSaturation, float maxSaturation)
|
||||
{
|
||||
float newFilterMinValue = minSaturation <= 1f ? minSaturation : minSaturation / 100f;
|
||||
float newFilterMaxValue = maxSaturation <= 1f ? maxSaturation : maxSaturation / 100f;
|
||||
|
||||
saturationValueRange = new ColorValueRange(newFilterMinValue, newFilterMaxValue);
|
||||
}
|
||||
|
||||
public void SetVibrancyRange(float minVibrancy, float maxVibrancy)
|
||||
{
|
||||
float newFilterMinValue = minVibrancy <= 1f ? minVibrancy : minVibrancy / 100f;
|
||||
float newFilterMaxValue = maxVibrancy <= 1f ? maxVibrancy : maxVibrancy / 100f;
|
||||
|
||||
vibrancyValueRange = new ColorValueRange(newFilterMinValue, newFilterMaxValue);
|
||||
}
|
||||
|
||||
public void SetAlphaRange(float minAlpha, float maxAlpha)
|
||||
{
|
||||
float newFilterMinValue = minAlpha < 1f ? minAlpha : minAlpha / 255f;
|
||||
float newFilterMaxValue = maxAlpha < 1f ? maxAlpha : maxAlpha / 255f;
|
||||
|
||||
alphaValueRange = new ColorValueRange(newFilterMinValue, newFilterMaxValue);
|
||||
}
|
||||
|
||||
public void SwapColors(Color[] texturePixels)
|
||||
{
|
||||
for (int pixelIndex = 0; pixelIndex < texturePixels.Length; pixelIndex++)
|
||||
{
|
||||
Color pixel = texturePixels[pixelIndex];
|
||||
float alpha = pixel.a;
|
||||
|
||||
Color.RGBToHSV(pixel, out float hue, out float saturation, out float vibrancy);
|
||||
|
||||
if (hueValueRange.Covers(hue) &&
|
||||
saturationValueRange.Covers(saturation) &&
|
||||
vibrancyValueRange.Covers(vibrancy) &&
|
||||
alphaValueRange.Covers(alpha))
|
||||
{
|
||||
texturePixels[pixelIndex] = colorSwapStrategy.SwapColor(pixel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap.Strategy;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using UnityEngine;
|
||||
using static NitroxClient.GameLogic.PlayerLogic.PlayerModel.PlayerEquipmentConstants;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap
|
||||
{
|
||||
public class RadiationHelmetColorSwapManager : IColorSwapManager
|
||||
{
|
||||
public Action<ColorSwapAsyncOperation> CreateColorSwapTask(INitroxPlayer nitroxPlayer)
|
||||
{
|
||||
GameObject playerModel = nitroxPlayer.PlayerModel;
|
||||
Color playerColor = nitroxPlayer.PlayerSettings.PlayerColor.ToUnity();
|
||||
IColorSwapStrategy colorSwapStrategy = new HueSaturationVibrancySwapper(playerColor);
|
||||
|
||||
SkinnedMeshRenderer radiationHelmetRenderer = playerModel.GetRenderer(RADIATION_HELMET_GAME_OBJECT_NAME);
|
||||
radiationHelmetRenderer.material.ApplyClonedTexture();
|
||||
|
||||
SkinnedMeshRenderer radiationSuitNeckClaspRenderer = playerModel.GetRenderer(RADIATION_SUIT_NECK_CLASP_GAME_OBJECT_NAME);
|
||||
radiationSuitNeckClaspRenderer.material.ApplyClonedTexture();
|
||||
|
||||
Color[] helmetPixels = radiationHelmetRenderer.material.GetMainTexturePixels();
|
||||
Color[] neckClaspPixels = radiationSuitNeckClaspRenderer.material.GetMainTexturePixels();
|
||||
|
||||
return operation =>
|
||||
{
|
||||
HsvSwapper radiationHelmetFilter = new HsvSwapper(colorSwapStrategy);
|
||||
radiationHelmetFilter.SetSaturationRange(0f, 35f);
|
||||
radiationHelmetFilter.SetVibrancyRange(30f, 100f);
|
||||
|
||||
radiationHelmetFilter.SwapColors(helmetPixels);
|
||||
radiationHelmetFilter.SwapColors(neckClaspPixels);
|
||||
|
||||
operation.UpdateIndex(RADIATION_HELMET_INDEX_KEY, helmetPixels);
|
||||
operation.UpdateIndex(RADIATION_SUIT_NECK_CLASP_INDEX_KEY, helmetPixels);
|
||||
};
|
||||
}
|
||||
|
||||
public void ApplyPlayerColor(Dictionary<string, Color[]> pixelIndex, INitroxPlayer nitroxPlayer)
|
||||
{
|
||||
Color[] helmetPixels = pixelIndex[RADIATION_HELMET_INDEX_KEY];
|
||||
Color[] neckClaspPixels = pixelIndex[RADIATION_SUIT_NECK_CLASP_INDEX_KEY];
|
||||
|
||||
GameObject playerModel = nitroxPlayer.PlayerModel;
|
||||
|
||||
SkinnedMeshRenderer radiationHelmetRenderer = playerModel.GetRenderer(RADIATION_HELMET_GAME_OBJECT_NAME);
|
||||
radiationHelmetRenderer.material.UpdateMainTextureColors(helmetPixels);
|
||||
|
||||
SkinnedMeshRenderer radiationSuitNeckClaspRenderer = playerModel.GetRenderer(RADIATION_SUIT_NECK_CLASP_GAME_OBJECT_NAME);
|
||||
radiationSuitNeckClaspRenderer.material.UpdateMainTextureColors(neckClaspPixels);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap.Strategy;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using UnityEngine;
|
||||
using static NitroxClient.GameLogic.PlayerLogic.PlayerModel.PlayerEquipmentConstants;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap
|
||||
{
|
||||
public class RadiationSuitColorSwapManager : IColorSwapManager
|
||||
{
|
||||
private readonly TextureBlock beltTextureBlock;
|
||||
private readonly TextureBlock feetTextureBlock;
|
||||
private readonly TextureBlock legTextureBlock;
|
||||
|
||||
public RadiationSuitColorSwapManager()
|
||||
{
|
||||
legTextureBlock = new TextureBlock(700, 484, 130, 155);
|
||||
feetTextureBlock = new TextureBlock(525, 324, 250, 325);
|
||||
beltTextureBlock = new TextureBlock(570, 0, 454, 1024);
|
||||
}
|
||||
|
||||
public Action<ColorSwapAsyncOperation> CreateColorSwapTask(INitroxPlayer nitroxPlayer)
|
||||
{
|
||||
GameObject playerModel = nitroxPlayer.PlayerModel;
|
||||
Color playerColor = nitroxPlayer.PlayerSettings.PlayerColor.ToUnity();
|
||||
|
||||
HueSaturationVibrancySwapper hueSaturationVibrancySwapper = new HueSaturationVibrancySwapper(playerColor);
|
||||
HueSwapper hueSwapper = new HueSwapper(playerColor);
|
||||
|
||||
SkinnedMeshRenderer radiationSuitRenderer = playerModel.GetRenderer(RADIATION_SUIT_GAME_OBJECT_NAME);
|
||||
radiationSuitRenderer.material.ApplyClonedTexture();
|
||||
radiationSuitRenderer.materials[1].ApplyClonedTexture();
|
||||
|
||||
Color[] legPixelBlock = radiationSuitRenderer.material.GetMainTexturePixelBlock(legTextureBlock);
|
||||
Color[] feetPixelBlock = radiationSuitRenderer.material.GetMainTexturePixelBlock(feetTextureBlock);
|
||||
Color[] beltPixelBlock = radiationSuitRenderer.material.GetMainTexturePixelBlock(beltTextureBlock);
|
||||
Color[] armSleevesPixels = radiationSuitRenderer.materials[1].GetMainTexturePixels();
|
||||
|
||||
return operation =>
|
||||
{
|
||||
HsvSwapper radiationSuitLegFilter = new HsvSwapper(hueSaturationVibrancySwapper);
|
||||
radiationSuitLegFilter.SetSaturationRange(0f, 35f);
|
||||
radiationSuitLegFilter.SetVibrancyRange(40f, 100f);
|
||||
|
||||
HsvSwapper radiationSuitArmAndFeetFilter = new HsvSwapper(hueSwapper);
|
||||
radiationSuitArmAndFeetFilter.SetHueRange(0f, 100f);
|
||||
radiationSuitArmAndFeetFilter.SetVibrancyRange(30f, 100f);
|
||||
|
||||
HsvSwapper radiationSuitBeltFilter = new HsvSwapper(hueSwapper);
|
||||
radiationSuitBeltFilter.SetVibrancyRange(3f, 100f);
|
||||
radiationSuitBeltFilter.SetHueRange(0f, 90f);
|
||||
|
||||
radiationSuitLegFilter.SwapColors(legPixelBlock);
|
||||
radiationSuitArmAndFeetFilter.SwapColors(feetPixelBlock);
|
||||
radiationSuitArmAndFeetFilter.SwapColors(armSleevesPixels);
|
||||
radiationSuitBeltFilter.SwapColors(beltPixelBlock);
|
||||
|
||||
operation.UpdateIndex(RADIATION_SUIT_ARMS_INDEX_KEY, armSleevesPixels);
|
||||
operation.UpdateIndex(RADIATION_SUIT_LEG_INDEX_KEY, legPixelBlock);
|
||||
operation.UpdateIndex(RADIATION_SUIT_FEET_INDEX_KEY, feetPixelBlock);
|
||||
operation.UpdateIndex(RADIATION_SUIT_BELT_INDEX_KEY, beltPixelBlock);
|
||||
};
|
||||
}
|
||||
|
||||
public void ApplyPlayerColor(Dictionary<string, Color[]> pixelIndex, INitroxPlayer nitroxPlayer)
|
||||
{
|
||||
Color[] armSleevesPixels = pixelIndex[RADIATION_SUIT_ARMS_INDEX_KEY];
|
||||
Color[] legPixels = pixelIndex[RADIATION_SUIT_LEG_INDEX_KEY];
|
||||
Color[] feetPixels = pixelIndex[RADIATION_SUIT_FEET_INDEX_KEY];
|
||||
Color[] beltPixels = pixelIndex[RADIATION_SUIT_BELT_INDEX_KEY];
|
||||
|
||||
GameObject playerModel = nitroxPlayer.PlayerModel;
|
||||
|
||||
SkinnedMeshRenderer radiationSuitRenderer = playerModel.GetRenderer(RADIATION_SUIT_GAME_OBJECT_NAME);
|
||||
|
||||
radiationSuitRenderer.material.UpdateMainTextureColors(legPixels, legTextureBlock);
|
||||
radiationSuitRenderer.material.UpdateMainTextureColors(feetPixels, feetTextureBlock);
|
||||
radiationSuitRenderer.material.UpdateMainTextureColors(beltPixels, beltTextureBlock);
|
||||
radiationSuitRenderer.materials[1].UpdateMainTextureColors(armSleevesPixels);
|
||||
|
||||
radiationSuitRenderer.material.SetTexture("_MainText", radiationSuitRenderer.material.mainTexture);
|
||||
radiationSuitRenderer.material.SetTexture("_SpecTex", radiationSuitRenderer.material.mainTexture);
|
||||
radiationSuitRenderer.materials[1].SetTexture("_MainText", radiationSuitRenderer.materials[1].mainTexture);
|
||||
radiationSuitRenderer.materials[1].SetTexture("_SpecTex", radiationSuitRenderer.materials[1].mainTexture);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap.Strategy;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using UnityEngine;
|
||||
using static NitroxClient.GameLogic.PlayerLogic.PlayerModel.PlayerEquipmentConstants;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap
|
||||
{
|
||||
public class RadiationSuitVestColorSwapManager : IColorSwapManager
|
||||
{
|
||||
public Action<ColorSwapAsyncOperation> CreateColorSwapTask(INitroxPlayer nitroxPlayer)
|
||||
{
|
||||
GameObject playerModel = nitroxPlayer.PlayerModel;
|
||||
Color playerColor = nitroxPlayer.PlayerSettings.PlayerColor.ToUnity();
|
||||
IColorSwapStrategy colorSwapStrategy = new HueSaturationVibrancySwapper(playerColor);
|
||||
|
||||
SkinnedMeshRenderer radiationVestRenderer = playerModel.GetRenderer(RADIATION_SUIT_VEST_GAME_OBJECT_NAME);
|
||||
radiationVestRenderer.material.ApplyClonedTexture();
|
||||
|
||||
Color[] texturePixels = radiationVestRenderer.material.GetMainTexturePixels();
|
||||
|
||||
return operation =>
|
||||
{
|
||||
HsvSwapper radiationSuitVestFilter = new HsvSwapper(colorSwapStrategy);
|
||||
radiationSuitVestFilter.SetSaturationRange(0f, 35f);
|
||||
radiationSuitVestFilter.SetVibrancyRange(12f, 100f);
|
||||
|
||||
radiationSuitVestFilter.SwapColors(texturePixels);
|
||||
|
||||
operation.UpdateIndex(RADIATION_SUIT_VEST_INDEX_KEY, texturePixels);
|
||||
};
|
||||
}
|
||||
|
||||
public void ApplyPlayerColor(Dictionary<string, Color[]> pixelIndex, INitroxPlayer nitroxPlayer)
|
||||
{
|
||||
Color[] helmetPixels = pixelIndex[RADIATION_SUIT_VEST_INDEX_KEY];
|
||||
|
||||
GameObject playerModel = nitroxPlayer.PlayerModel;
|
||||
SkinnedMeshRenderer radiationHelmetRenderer = playerModel.GetRenderer(RADIATION_SUIT_VEST_GAME_OBJECT_NAME);
|
||||
radiationHelmetRenderer.material.UpdateMainTextureColors(helmetPixels);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap.Strategy;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using UnityEngine;
|
||||
using static NitroxClient.GameLogic.PlayerLogic.PlayerModel.PlayerEquipmentConstants;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap
|
||||
{
|
||||
public class RadiationTankColorSwapManager : IColorSwapManager
|
||||
{
|
||||
public Action<ColorSwapAsyncOperation> CreateColorSwapTask(INitroxPlayer nitroxPlayer)
|
||||
{
|
||||
GameObject playerModel = nitroxPlayer.PlayerModel;
|
||||
Color playerColor = nitroxPlayer.PlayerSettings.PlayerColor.ToUnity();
|
||||
IColorSwapStrategy colorSwapStrategy = new HueSwapper(playerColor);
|
||||
|
||||
SkinnedMeshRenderer radiationTankRenderer = playerModel.GetRenderer(RADIATION_TANK_GAME_OBJECT_NAME);
|
||||
radiationTankRenderer.material.ApplyClonedTexture();
|
||||
|
||||
Color[] texturePixels = radiationTankRenderer.material.GetMainTexturePixels();
|
||||
|
||||
return operation =>
|
||||
{
|
||||
HsvSwapper radiationTankFilter = new HsvSwapper(colorSwapStrategy);
|
||||
radiationTankFilter.SetHueRange(0f, 85f);
|
||||
|
||||
radiationTankFilter.SwapColors(texturePixels);
|
||||
|
||||
operation.UpdateIndex(RADIATION_SUIT_TANK_INDEX_KEY, texturePixels);
|
||||
};
|
||||
}
|
||||
|
||||
public void ApplyPlayerColor(Dictionary<string, Color[]> pixelIndex, INitroxPlayer nitroxPlayer)
|
||||
{
|
||||
Color[] tankPixels = pixelIndex[RADIATION_SUIT_TANK_INDEX_KEY];
|
||||
|
||||
GameObject playerModel = nitroxPlayer.PlayerModel;
|
||||
SkinnedMeshRenderer radiationTankRenderer = playerModel.GetRenderer(RADIATION_TANK_GAME_OBJECT_NAME);
|
||||
|
||||
radiationTankRenderer.material.UpdateMainTextureColors(tankPixels);
|
||||
radiationTankRenderer.material.SetTexture("_MainTex", radiationTankRenderer.material.mainTexture);
|
||||
radiationTankRenderer.material.SetTexture("_SpecTex", radiationTankRenderer.material.mainTexture);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap.Strategy;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using UnityEngine;
|
||||
using static NitroxClient.GameLogic.PlayerLogic.PlayerModel.PlayerEquipmentConstants;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap
|
||||
{
|
||||
public class RebreatherColorSwapManager : IColorSwapManager
|
||||
{
|
||||
public Action<ColorSwapAsyncOperation> CreateColorSwapTask(INitroxPlayer nitroxPlayer)
|
||||
{
|
||||
GameObject playerModel = nitroxPlayer.PlayerModel;
|
||||
Color playerColor = nitroxPlayer.PlayerSettings.PlayerColor.ToUnity();
|
||||
IColorSwapStrategy colorSwapStrategy = new HueSwapper(playerColor);
|
||||
|
||||
SkinnedMeshRenderer rebreatherRenderer = playerModel.GetRenderer(REBREATHER_GAME_OBJECT_NAME);
|
||||
rebreatherRenderer.material.ApplyClonedTexture();
|
||||
|
||||
FixRebreatherMaterials(playerModel, rebreatherRenderer);
|
||||
|
||||
Color[] texturePixels = rebreatherRenderer.material.GetMainTexturePixels();
|
||||
|
||||
return operation =>
|
||||
{
|
||||
HsvSwapper rebreatherFilter = new HsvSwapper(colorSwapStrategy);
|
||||
rebreatherFilter.SetHueRange(0f, 25f);
|
||||
|
||||
rebreatherFilter.SwapColors(texturePixels);
|
||||
|
||||
operation.UpdateIndex(REBREATHER_INDEX_KEY, texturePixels);
|
||||
};
|
||||
}
|
||||
|
||||
public void ApplyPlayerColor(Dictionary<string, Color[]> pixelIndex, INitroxPlayer nitroxPlayer)
|
||||
{
|
||||
Color[] rebreatherPixelIndexes = pixelIndex[REBREATHER_INDEX_KEY];
|
||||
|
||||
GameObject playerModel = nitroxPlayer.PlayerModel;
|
||||
SkinnedMeshRenderer rebreatherRenderer = playerModel.GetRenderer(REBREATHER_GAME_OBJECT_NAME);
|
||||
rebreatherRenderer.material.UpdateMainTextureColors(rebreatherPixelIndexes);
|
||||
}
|
||||
|
||||
//Clean up of UWE's tech debt from when they gave up on rendering player equipment on the avatar during normal play. Probably best not to read too much into it...
|
||||
private static void FixRebreatherMaterials(GameObject playerModel, SkinnedMeshRenderer rebreatherRenderer)
|
||||
{
|
||||
Shader marmosetShader = playerModel.GetRenderer(NORMAL_HEAD_GAME_OBJECT_NAME).material.shader;
|
||||
rebreatherRenderer.material.shader = marmosetShader;
|
||||
rebreatherRenderer.material.SetOverrideTag("RenderType", "TransparentAdditive");
|
||||
rebreatherRenderer.material.SetOverrideTag("Queue", "Deferred");
|
||||
rebreatherRenderer.material.shaderKeywords = new List<string>
|
||||
{"MARMO_ALPHA", "MARMO_PREMULT_ALPHA", "MARMO_SIMPLE_GLASS", "UWE_DITHERALPHA", "MARMO_SPECMAP", "WBOIT", "_NORMALMAP", "_ZWRITE_ON"}.ToArray();
|
||||
|
||||
rebreatherRenderer.material.SetTexture("_MainTex", rebreatherRenderer.material.mainTexture);
|
||||
rebreatherRenderer.material.SetTexture("_SpecTex", rebreatherRenderer.material.mainTexture);
|
||||
rebreatherRenderer.material.SetTexture("_BumpMap", rebreatherRenderer.material.GetTexture("_BumpMap"));
|
||||
|
||||
rebreatherRenderer.materials[2].shader = marmosetShader;
|
||||
rebreatherRenderer.materials[2].shaderKeywords = new List<string>
|
||||
{"MARMO_SPECMAP", "_ZWRITE_ON"}.ToArray();
|
||||
rebreatherRenderer.materials[2].SetTexture("_MainTex", rebreatherRenderer.materials[2].mainTexture);
|
||||
rebreatherRenderer.materials[2].SetTexture("_SpecTex", rebreatherRenderer.materials[2].mainTexture);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap.Strategy;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using UnityEngine;
|
||||
using static NitroxClient.GameLogic.PlayerLogic.PlayerModel.PlayerEquipmentConstants;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap
|
||||
{
|
||||
public class ReinforcedSuitColorSwapManager : IColorSwapManager
|
||||
{
|
||||
public void PrepareMaterials(GameObject playerModel)
|
||||
{
|
||||
SkinnedMeshRenderer reinforcedSuitRenderer = playerModel.GetRenderer(REINFORCED_SUIT_GAME_OBJECT_NAME);
|
||||
|
||||
SkinnedMeshRenderer reinforcedSuitGlovesRenderer = playerModel.GetRenderer(REINFORCED_GLOVES_GAME_OBJECT_NAME);
|
||||
}
|
||||
|
||||
public Action<ColorSwapAsyncOperation> CreateColorSwapTask(INitroxPlayer nitroxPlayer)
|
||||
{
|
||||
GameObject playerModel = nitroxPlayer.PlayerModel;
|
||||
Color playerColor = nitroxPlayer.PlayerSettings.PlayerColor.ToUnity();
|
||||
IColorSwapStrategy colorSwapStrategy = new HueSwapper(playerColor);
|
||||
|
||||
SkinnedMeshRenderer reinforcedSuitRenderer = playerModel.GetRenderer(REINFORCED_SUIT_GAME_OBJECT_NAME);
|
||||
reinforcedSuitRenderer.material.ApplyClonedTexture();
|
||||
reinforcedSuitRenderer.materials[1].ApplyClonedTexture();
|
||||
|
||||
SkinnedMeshRenderer reinforcedGloveRenderer = playerModel.GetRenderer(REINFORCED_GLOVES_GAME_OBJECT_NAME);
|
||||
reinforcedGloveRenderer.material.ApplyClonedTexture();
|
||||
|
||||
Color[] suitTexturePixels = reinforcedSuitRenderer.material.GetMainTexturePixels();
|
||||
Color[] armsTexturePixels = reinforcedSuitRenderer.materials[1].GetMainTexturePixels();
|
||||
Color[] gloveTexturePixels = reinforcedGloveRenderer.material.GetMainTexturePixels();
|
||||
|
||||
return operation =>
|
||||
{
|
||||
HsvSwapper reinforcedSuitFilter = new HsvSwapper(colorSwapStrategy);
|
||||
reinforcedSuitFilter.SetHueRange(0f, 20f);
|
||||
reinforcedSuitFilter.SetSaturationRange(45f, 100f);
|
||||
|
||||
reinforcedSuitFilter.SwapColors(suitTexturePixels);
|
||||
reinforcedSuitFilter.SwapColors(armsTexturePixels);
|
||||
reinforcedSuitFilter.SwapColors(gloveTexturePixels);
|
||||
|
||||
operation.UpdateIndex(REINFORCED_SUIT_INDEX_KEY, suitTexturePixels);
|
||||
operation.UpdateIndex(REINFORCED_SUIT_ARMS_INDEX_KEY, armsTexturePixels);
|
||||
operation.UpdateIndex(REINFORCED_GLOVES_INDEX_KEY, gloveTexturePixels);
|
||||
};
|
||||
}
|
||||
|
||||
public void ApplyPlayerColor(Dictionary<string, Color[]> pixelIndex, INitroxPlayer nitroxPlayer)
|
||||
{
|
||||
Color[] suitPixelIndexes = pixelIndex[REINFORCED_SUIT_INDEX_KEY];
|
||||
Color[] armsTexturePixels = pixelIndex[REINFORCED_SUIT_ARMS_INDEX_KEY];
|
||||
Color[] glovePixelIndexes = pixelIndex[REINFORCED_GLOVES_INDEX_KEY];
|
||||
|
||||
GameObject playerModel = nitroxPlayer.PlayerModel;
|
||||
Color playerColor = nitroxPlayer.PlayerSettings.PlayerColor.ToUnity();
|
||||
IColorSwapStrategy colorSwapStrategy = new HueSwapper(playerColor);
|
||||
|
||||
SkinnedMeshRenderer reinforcedSuitRenderer = playerModel.GetRenderer(REINFORCED_SUIT_GAME_OBJECT_NAME);
|
||||
reinforcedSuitRenderer.material.UpdateMainTextureColors(suitPixelIndexes);
|
||||
reinforcedSuitRenderer.material.SetTexture("_MainTex", reinforcedSuitRenderer.material.mainTexture);
|
||||
reinforcedSuitRenderer.material.SetTexture("_SpecTex", reinforcedSuitRenderer.material.mainTexture);
|
||||
|
||||
reinforcedSuitRenderer.materials[1].UpdateMainTextureColors(armsTexturePixels);
|
||||
reinforcedSuitRenderer.materials[1].SetTexture("_MainTex", reinforcedSuitRenderer.materials[1].mainTexture);
|
||||
reinforcedSuitRenderer.materials[1].SetTexture("_SpecTex", reinforcedSuitRenderer.materials[1].mainTexture);
|
||||
|
||||
SkinnedMeshRenderer reinforcedGlovesRenderer = playerModel.GetRenderer(REINFORCED_GLOVES_GAME_OBJECT_NAME);
|
||||
reinforcedGlovesRenderer.material.UpdateMainTextureColors(glovePixelIndexes);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap.Strategy;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using UnityEngine;
|
||||
using static NitroxClient.GameLogic.PlayerLogic.PlayerModel.PlayerEquipmentConstants;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap
|
||||
{
|
||||
public class ScubaTankColorSwapManager : IColorSwapManager
|
||||
{
|
||||
public Action<ColorSwapAsyncOperation> CreateColorSwapTask(INitroxPlayer nitroxPlayer)
|
||||
{
|
||||
GameObject playerModel = nitroxPlayer.PlayerModel;
|
||||
Color playerColor = nitroxPlayer.PlayerSettings.PlayerColor.ToUnity();
|
||||
IColorSwapStrategy colorSwapStrategy = new HueSwapper(playerColor);
|
||||
|
||||
SkinnedMeshRenderer scubaTankRenderer = playerModel.GetRenderer(SCUBA_TANK_GAME_OBJECT_NAME);
|
||||
scubaTankRenderer.material.ApplyClonedTexture();
|
||||
|
||||
Color[] texturePixels = scubaTankRenderer.material.GetMainTexturePixels();
|
||||
|
||||
return operation =>
|
||||
{
|
||||
HsvSwapper scubaTankFilter = new HsvSwapper(colorSwapStrategy);
|
||||
scubaTankFilter.SetHueRange(0f, 30f);
|
||||
|
||||
scubaTankFilter.SwapColors(texturePixels);
|
||||
|
||||
operation.UpdateIndex(SCUBA_TANK_INDEX_KEY, texturePixels);
|
||||
};
|
||||
}
|
||||
|
||||
public void ApplyPlayerColor(Dictionary<string, Color[]> pixelIndex, INitroxPlayer nitroxPlayer)
|
||||
{
|
||||
Color[] scubaTankPixelIndexes = pixelIndex[SCUBA_TANK_INDEX_KEY];
|
||||
|
||||
GameObject playerModel = nitroxPlayer.PlayerModel;
|
||||
SkinnedMeshRenderer scubaTankRenderer = playerModel.GetRenderer(SCUBA_TANK_GAME_OBJECT_NAME);
|
||||
|
||||
scubaTankRenderer.material.UpdateMainTextureColors(scubaTankPixelIndexes);
|
||||
scubaTankRenderer.material.SetTexture("_MainTex", scubaTankRenderer.material.mainTexture);
|
||||
scubaTankRenderer.material.SetTexture("_SpecTex", scubaTankRenderer.material.mainTexture);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap.Strategy;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using UnityEngine;
|
||||
using static NitroxClient.GameLogic.PlayerLogic.PlayerModel.PlayerEquipmentConstants;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap
|
||||
{
|
||||
public class StillSuitColorSwapManager : IColorSwapManager
|
||||
{
|
||||
public Action<ColorSwapAsyncOperation> CreateColorSwapTask(INitroxPlayer nitroxPlayer)
|
||||
{
|
||||
GameObject playerModel = nitroxPlayer.PlayerModel;
|
||||
Color playerColor = nitroxPlayer.PlayerSettings.PlayerColor.ToUnity();
|
||||
IColorSwapStrategy colorSwapStrategy = new HueSwapper(playerColor);
|
||||
|
||||
SkinnedMeshRenderer stillSuitRenderer = playerModel.GetRenderer(STILL_SUIT_GAME_OBJECT_NAME);
|
||||
stillSuitRenderer.material.ApplyClonedTexture();
|
||||
stillSuitRenderer.materials[1].ApplyClonedTexture();
|
||||
|
||||
Color[] bodyTexturePixels = stillSuitRenderer.material.GetMainTexturePixels();
|
||||
Color[] armsTexturePixels = stillSuitRenderer.materials[1].GetMainTexturePixels();
|
||||
|
||||
return operation =>
|
||||
{
|
||||
HsvSwapper stillSuitFilter = new HsvSwapper(colorSwapStrategy);
|
||||
stillSuitFilter.SetHueRange(0f, 75f);
|
||||
|
||||
stillSuitFilter.SwapColors(bodyTexturePixels);
|
||||
stillSuitFilter.SwapColors(armsTexturePixels);
|
||||
|
||||
operation.UpdateIndex(STILL_SUIT_INDEX_KEY, bodyTexturePixels);
|
||||
operation.UpdateIndex(STILL_SUIT_ARMS_INDEX_KEY, armsTexturePixels);
|
||||
};
|
||||
}
|
||||
|
||||
public void ApplyPlayerColor(Dictionary<string, Color[]> pixelIndex, INitroxPlayer nitroxPlayer)
|
||||
{
|
||||
Color[] bodyPixelIndexes = pixelIndex[STILL_SUIT_INDEX_KEY];
|
||||
Color[] armsPixelIndexes = pixelIndex[STILL_SUIT_ARMS_INDEX_KEY];
|
||||
|
||||
GameObject playerModel = nitroxPlayer.PlayerModel;
|
||||
|
||||
SkinnedMeshRenderer stillSuitRenderer = playerModel.GetRenderer(STILL_SUIT_GAME_OBJECT_NAME);
|
||||
stillSuitRenderer.material.UpdateMainTextureColors(bodyPixelIndexes);
|
||||
stillSuitRenderer.material.SetTexture("_MainTex", stillSuitRenderer.material.mainTexture);
|
||||
stillSuitRenderer.material.SetTexture("_SpecTex", stillSuitRenderer.material.mainTexture);
|
||||
|
||||
stillSuitRenderer.materials[1].UpdateMainTextureColors(armsPixelIndexes);
|
||||
stillSuitRenderer.materials[1].SetTexture("_MainTex", stillSuitRenderer.materials[1].mainTexture);
|
||||
stillSuitRenderer.materials[1].SetTexture("_SpecTex", stillSuitRenderer.materials[1].mainTexture);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap.Strategy
|
||||
{
|
||||
public class AlphaChannelSwapper : IColorSwapStrategy
|
||||
{
|
||||
private readonly float replacementAlpha;
|
||||
|
||||
public AlphaChannelSwapper(float replacementAlpha)
|
||||
{
|
||||
this.replacementAlpha = replacementAlpha;
|
||||
}
|
||||
|
||||
public Color SwapColor(Color originalColor)
|
||||
{
|
||||
return originalColor.WithAlpha(replacementAlpha);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap.Strategy
|
||||
{
|
||||
public class HueSaturationVibrancySwapper : IColorSwapStrategy
|
||||
{
|
||||
private readonly float replacementHue;
|
||||
private readonly float replacementSaturation;
|
||||
private readonly float replacementVibrancy;
|
||||
|
||||
public HueSaturationVibrancySwapper(Color playerColor)
|
||||
{
|
||||
Color.RGBToHSV(playerColor, out replacementHue, out replacementSaturation, out replacementVibrancy);
|
||||
}
|
||||
|
||||
public Color SwapColor(Color originalColor)
|
||||
{
|
||||
float currentAlpha = originalColor.a;
|
||||
Color.RGBToHSV(originalColor, out float currentHue, out float currentSaturation, out float currentVibrancy);
|
||||
|
||||
return Color
|
||||
.HSVToRGB(replacementHue, replacementSaturation, replacementVibrancy)
|
||||
.WithAlpha(currentAlpha);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,25 @@
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap.Strategy
|
||||
{
|
||||
public class HueSwapper : IColorSwapStrategy
|
||||
{
|
||||
private readonly float replacementHue;
|
||||
|
||||
public HueSwapper(Color playerColor)
|
||||
{
|
||||
Color.RGBToHSV(playerColor, out replacementHue, out float saturation, out float vibrancy);
|
||||
}
|
||||
|
||||
public Color SwapColor(Color originalColor)
|
||||
{
|
||||
float currentAlpha = originalColor.a;
|
||||
Color.RGBToHSV(originalColor, out float currentHue, out float currentSaturation, out float currentVibrancy);
|
||||
|
||||
return Color
|
||||
.HSVToRGB(replacementHue, currentSaturation, currentVibrancy)
|
||||
.WithAlpha(currentAlpha);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap
|
||||
{
|
||||
public class TextureBlock
|
||||
{
|
||||
public int X { get; }
|
||||
public int Y { get; }
|
||||
public int BlockWidth { get; }
|
||||
public int BlockHeight { get; }
|
||||
|
||||
public TextureBlock(int x, int y, int blockWidth, int blockHeight)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
BlockWidth = blockWidth;
|
||||
BlockHeight = blockHeight;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel.Equipment.Abstract
|
||||
{
|
||||
public interface IEquipmentVisibilityHandler
|
||||
{
|
||||
void UpdateEquipmentVisibility(ReadOnlyCollection<TechType> currentEquipment);
|
||||
}
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Equipment.Abstract;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel.Equipment
|
||||
{
|
||||
public class DiveSuitVisibilityHandler : IEquipmentVisibilityHandler
|
||||
{
|
||||
private readonly GameObject head;
|
||||
private readonly GameObject body;
|
||||
private readonly GameObject hands;
|
||||
|
||||
public DiveSuitVisibilityHandler(GameObject playerModel)
|
||||
{
|
||||
head = playerModel.transform.Find(PlayerEquipmentConstants.NORMAL_HEAD_GAME_OBJECT_NAME).gameObject;
|
||||
body = playerModel.transform.Find(PlayerEquipmentConstants.DIVE_SUIT_GAME_OBJECT_NAME).gameObject;
|
||||
hands = playerModel.transform.Find(PlayerEquipmentConstants.NORMAL_HANDS_GAME_OBJECT_NAME).gameObject;
|
||||
}
|
||||
|
||||
public void UpdateEquipmentVisibility(ReadOnlyCollection<TechType> currentEquipment)
|
||||
{
|
||||
bool headVisible = !currentEquipment.Contains(TechType.RadiationHelmet) && !currentEquipment.Contains(TechType.Rebreather);
|
||||
bool bodyVisible = !currentEquipment.Contains(TechType.RadiationSuit) &&
|
||||
!currentEquipment.Contains(TechType.WaterFiltrationSuit) &&
|
||||
!currentEquipment.Contains(TechType.ReinforcedDiveSuit);
|
||||
bool handsVisible = !currentEquipment.Contains(TechType.RadiationGloves) && !currentEquipment.Contains(TechType.ReinforcedGloves);
|
||||
|
||||
head.SetActive(headVisible);
|
||||
body.gameObject.SetActive(bodyVisible);
|
||||
hands.SetActive(handsVisible);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,40 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Equipment.Abstract;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel.Equipment
|
||||
{
|
||||
public class FinsVisibilityHandler : IEquipmentVisibilityHandler
|
||||
{
|
||||
private readonly GameObject fins;
|
||||
private readonly GameObject finsRoot;
|
||||
private readonly GameObject chargedFins;
|
||||
private readonly GameObject chargedFinsRoot;
|
||||
private readonly GameObject glideFins;
|
||||
private readonly GameObject glideFinsRoot;
|
||||
|
||||
public FinsVisibilityHandler(GameObject playerModel)
|
||||
{
|
||||
fins = playerModel.transform.Find(PlayerEquipmentConstants.FINS_GAME_OBJECT_NAME).gameObject;
|
||||
finsRoot = playerModel.transform.Find(PlayerEquipmentConstants.FINS_ROOT_GAME_OBJECT_NAME).gameObject;
|
||||
chargedFins = playerModel.transform.Find(PlayerEquipmentConstants.CHARGED_FINS_GAME_OBJECT_NAME).gameObject;
|
||||
chargedFinsRoot = playerModel.transform.Find(PlayerEquipmentConstants.CHARGED_FINS_ROOT_GAME_OBJECT_NAME).gameObject;
|
||||
glideFins = playerModel.transform.Find(PlayerEquipmentConstants.GLIDE_FINS_GAME_OBJECT_NAME).gameObject;
|
||||
glideFinsRoot = playerModel.transform.Find(PlayerEquipmentConstants.GLIDE_FINS_ROOT_GAME_OBJECT_NAME).gameObject;
|
||||
}
|
||||
|
||||
public void UpdateEquipmentVisibility(ReadOnlyCollection<TechType> currentEquipment)
|
||||
{
|
||||
bool basicFinsVisible = currentEquipment.Contains(TechType.Fins);
|
||||
bool chargedFinsVisible = currentEquipment.Contains(TechType.SwimChargeFins);
|
||||
bool glideFinsVisible = currentEquipment.Contains(TechType.UltraGlideFins);
|
||||
|
||||
fins.SetActive(basicFinsVisible);
|
||||
finsRoot.SetActive(basicFinsVisible);
|
||||
chargedFins.SetActive(chargedFinsVisible);
|
||||
chargedFinsRoot.SetActive(chargedFinsVisible);
|
||||
glideFins.SetActive(glideFinsVisible);
|
||||
glideFinsRoot.SetActive(glideFinsVisible);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,55 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Equipment.Abstract;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel.Equipment
|
||||
{
|
||||
public class RadiationSuitVisibilityHandler : IEquipmentVisibilityHandler
|
||||
{
|
||||
private readonly GameObject head;
|
||||
private readonly GameObject helmet;
|
||||
private readonly GameObject gloves;
|
||||
private readonly GameObject suit;
|
||||
private readonly GameObject suitNeck;
|
||||
private readonly GameObject suitVest;
|
||||
private readonly GameObject tank;
|
||||
private readonly GameObject tankTubes;
|
||||
|
||||
public RadiationSuitVisibilityHandler(GameObject playerModel)
|
||||
{
|
||||
head = playerModel.transform.Find(PlayerEquipmentConstants.RADIATION_HEAD_GAME_OBJECT_NAME).gameObject;
|
||||
helmet = playerModel.transform.Find(PlayerEquipmentConstants.RADIATION_HELMET_GAME_OBJECT_NAME).gameObject;
|
||||
gloves = playerModel.transform.Find(PlayerEquipmentConstants.RADIATION_GLOVES_GAME_OBJECT_NAME).gameObject;
|
||||
suit = playerModel.transform.Find(PlayerEquipmentConstants.RADIATION_SUIT_GAME_OBJECT_NAME).gameObject;
|
||||
suitNeck = playerModel.transform.Find(PlayerEquipmentConstants.RADIATION_SUIT_NECK_CLASP_GAME_OBJECT_NAME).gameObject;
|
||||
suitVest = playerModel.transform.Find(PlayerEquipmentConstants.RADIATION_SUIT_VEST_GAME_OBJECT_NAME).gameObject;
|
||||
tank = playerModel.transform.Find(PlayerEquipmentConstants.RADIATION_TANK_GAME_OBJECT_NAME).gameObject;
|
||||
tankTubes = playerModel.transform.Find(PlayerEquipmentConstants.RADIATION_TANK_TUBES_GAME_OBJECT_NAME).gameObject;
|
||||
|
||||
}
|
||||
|
||||
public void UpdateEquipmentVisibility(ReadOnlyCollection<TechType> currentEquipment)
|
||||
{
|
||||
bool tankEquipped = currentEquipment.Contains(TechType.Tank) ||
|
||||
currentEquipment.Contains(TechType.DoubleTank) ||
|
||||
currentEquipment.Contains(TechType.HighCapacityTank) ||
|
||||
currentEquipment.Contains(TechType.PlasteelTank);
|
||||
|
||||
bool helmetVisible = currentEquipment.Contains(TechType.RadiationHelmet);
|
||||
bool glovesVisible = currentEquipment.Contains(TechType.RadiationGloves);
|
||||
bool bodyVisible = currentEquipment.Contains(TechType.RadiationSuit);
|
||||
bool vestVisible = bodyVisible || helmetVisible;
|
||||
bool tankVisible = tankEquipped && vestVisible;
|
||||
bool tubesVisible = tankVisible && helmetVisible;
|
||||
|
||||
head.SetActive(helmetVisible);
|
||||
helmet.SetActive(helmetVisible);
|
||||
gloves.SetActive(glovesVisible);
|
||||
suit.SetActive(bodyVisible);
|
||||
suitNeck.SetActive(helmetVisible);
|
||||
suitVest.SetActive(vestVisible);
|
||||
tank.SetActive(tankVisible);
|
||||
tankTubes.SetActive(tubesVisible);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Equipment.Abstract;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel.Equipment
|
||||
{
|
||||
public class ReinforcedSuitVisibilityHandler : IEquipmentVisibilityHandler
|
||||
{
|
||||
private readonly GameObject gloves;
|
||||
private readonly GameObject suit;
|
||||
|
||||
public ReinforcedSuitVisibilityHandler(GameObject playerModel)
|
||||
{
|
||||
gloves = playerModel.transform.Find(PlayerEquipmentConstants.REINFORCED_GLOVES_GAME_OBJECT_NAME).gameObject;
|
||||
suit = playerModel.transform.Find(PlayerEquipmentConstants.REINFORCED_SUIT_GAME_OBJECT_NAME).gameObject;
|
||||
}
|
||||
public void UpdateEquipmentVisibility(ReadOnlyCollection<TechType> currentEquipment)
|
||||
{
|
||||
bool glovesVisible = currentEquipment.Contains(TechType.ReinforcedGloves);
|
||||
bool bodyVisible = currentEquipment.Contains(TechType.ReinforcedDiveSuit);
|
||||
|
||||
gloves.SetActive(glovesVisible);
|
||||
suit.SetActive(bodyVisible);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Equipment.Abstract;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel.Equipment
|
||||
{
|
||||
public class ScubaSuitVisibilityHandler : IEquipmentVisibilityHandler
|
||||
{
|
||||
private readonly GameObject rebreather;
|
||||
private readonly GameObject scuba;
|
||||
private readonly GameObject scubaTank;
|
||||
private readonly GameObject scubaTankTubes;
|
||||
|
||||
public ScubaSuitVisibilityHandler(GameObject playerModel)
|
||||
{
|
||||
rebreather = playerModel.transform.Find(PlayerEquipmentConstants.REBREATHER_GAME_OBJECT_NAME).gameObject;
|
||||
scuba = playerModel.transform.Find(PlayerEquipmentConstants.SCUBA_ROOT_GAME_OBJECT_NAME).gameObject;
|
||||
scubaTank = playerModel.transform.Find(PlayerEquipmentConstants.SCUBA_TANK_GAME_OBJECT_NAME).gameObject;
|
||||
scubaTankTubes = playerModel.transform.Find(PlayerEquipmentConstants.SCUBA_TANK_TUBES_GAME_OBJECT_NAME).gameObject;
|
||||
}
|
||||
|
||||
public void UpdateEquipmentVisibility(ReadOnlyCollection<TechType> currentEquipment)
|
||||
{
|
||||
bool tankEquipped = currentEquipment.Contains(TechType.Tank) ||
|
||||
currentEquipment.Contains(TechType.DoubleTank) ||
|
||||
currentEquipment.Contains(TechType.HighCapacityTank) ||
|
||||
currentEquipment.Contains(TechType.PlasteelTank);
|
||||
|
||||
bool rebreatherVisible = currentEquipment.Contains(TechType.Rebreather);
|
||||
bool radiationHelmetVisible = currentEquipment.Contains(TechType.RadiationHelmet);
|
||||
bool tankVisible = tankEquipped && !currentEquipment.Contains(TechType.RadiationSuit);
|
||||
bool tubesVisible = (rebreatherVisible || radiationHelmetVisible) && tankVisible;
|
||||
bool rootVisible = rebreatherVisible || tankVisible;
|
||||
|
||||
rebreather.SetActive(rebreatherVisible);
|
||||
scuba.SetActive(rootVisible);
|
||||
scubaTank.SetActive(tankVisible);
|
||||
scubaTankTubes.SetActive(tubesVisible);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Equipment.Abstract;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel.Equipment
|
||||
{
|
||||
public class StillSuitVisibilityHandler : IEquipmentVisibilityHandler
|
||||
{
|
||||
private readonly GameObject stillSuit;
|
||||
|
||||
public StillSuitVisibilityHandler(GameObject playerModel)
|
||||
{
|
||||
stillSuit = playerModel.transform.Find(PlayerEquipmentConstants.STILL_SUIT_GAME_OBJECT_NAME).gameObject;
|
||||
}
|
||||
public void UpdateEquipmentVisibility(ReadOnlyCollection<TechType> currentEquipment)
|
||||
{
|
||||
bool bodyVisible = currentEquipment.Contains(TechType.WaterFiltrationSuit);
|
||||
|
||||
stillSuit.SetActive(bodyVisible);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,51 @@
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel
|
||||
{
|
||||
public static class PlayerEquipmentConstants
|
||||
{
|
||||
public const string NORMAL_HEAD_GAME_OBJECT_NAME = "male_geo/diveSuit/diveSuit_head_geo";
|
||||
public const string NORMAL_HANDS_GAME_OBJECT_NAME = "male_geo/diveSuit/diveSuit_hands_geo";
|
||||
public const string DIVE_SUIT_GAME_OBJECT_NAME = "male_geo/diveSuit/diveSuit_body_geo";
|
||||
public const string FINS_ROOT_GAME_OBJECT_NAME = "male_geo/generalSuit";
|
||||
public const string FINS_GAME_OBJECT_NAME = "male_geo/generalSuit/flippers_basic_geo";
|
||||
public const string CHARGED_FINS_ROOT_GAME_OBJECT_NAME = "male_geo/SwimChargeFins";
|
||||
public const string CHARGED_FINS_GAME_OBJECT_NAME = "male_geo/SwimChargeFins/flippers_basic_geo";
|
||||
public const string GLIDE_FINS_ROOT_GAME_OBJECT_NAME = "male_geo/UltraGlideFins";
|
||||
public const string GLIDE_FINS_GAME_OBJECT_NAME = "male_geo/UltraGlideFins/flippers_basic_geo";
|
||||
public const string STILL_SUIT_GAME_OBJECT_NAME = "male_geo/stillSuit/still_suit_01_body_geo";
|
||||
public const string SCUBA_ROOT_GAME_OBJECT_NAME = "male_geo/scubaSuit";
|
||||
public const string REBREATHER_GAME_OBJECT_NAME = "male_geo/scubaSuit/scuba_head";
|
||||
public const string SCUBA_TANK_GAME_OBJECT_NAME = "male_geo/scubaSuit/scuba_vest";
|
||||
public const string SCUBA_TANK_TUBES_GAME_OBJECT_NAME = "male_geo/scubaSuit/scuba_vest/scuba_breatherTubes";
|
||||
public const string REINFORCED_SUIT_GAME_OBJECT_NAME = "male_geo/reinforcedSuit/reinforced_suit_01_body_geo";
|
||||
public const string REINFORCED_GLOVES_GAME_OBJECT_NAME = "male_geo/reinforcedSuit/reinforced_suit_01_glove_geo";
|
||||
public const string RADIATION_SUIT_GAME_OBJECT_NAME = "male_geo/radiationSuit/radiationSuit_body_geo";
|
||||
public const string RADIATION_GLOVES_GAME_OBJECT_NAME = "male_geo/radiationSuit/radiationSuit_gloves_geo";
|
||||
public const string RADIATION_SUIT_VEST_GAME_OBJECT_NAME = "male_geo/radiationSuit/radiationSuit_vest_reducedNeck_geo";
|
||||
public const string RADIATION_HEAD_GAME_OBJECT_NAME = "male_geo/radiationSuit/radiationSuit_head_geo";
|
||||
public const string RADIATION_HELMET_GAME_OBJECT_NAME = "male_geo/radiationSuit/radiationSuit_vest_reducedNeck_geo/radiationSuit_vest_neckExtension_geo/radiationSuit_helmet_geo 1";
|
||||
public const string RADIATION_SUIT_NECK_CLASP_GAME_OBJECT_NAME = "male_geo/radiationSuit/radiationSuit_vest_reducedNeck_geo/radiationSuit_vest_neckExtension_geo";
|
||||
public const string RADIATION_TANK_GAME_OBJECT_NAME = "male_geo/radiationSuit/radiationSuit_vest_reducedNeck_geo/radiationSuit_tank_geo 1";
|
||||
public const string RADIATION_TANK_TUBES_GAME_OBJECT_NAME = "male_geo/radiationSuit/radiationSuit_vest_reducedNeck_geo/radiationSuit_tank_geo 1/radiationSuit_tubes_geo 1";
|
||||
public const string ITEM_ATTACH_POINT_GAME_OBJECT_NAME = "export_skeleton/head_rig/neck/chest/clav_R/clav_R_aim/shoulder_R/elbow_R/hand_R/attach1";
|
||||
|
||||
|
||||
public const string DIVE_SUIT_INDEX_KEY = "dive-suit";
|
||||
public const string DIVE_SUIT_ARMS_INDEX_KEY = "dive-suit-arms";
|
||||
public const string STILL_SUIT_INDEX_KEY = "still-suit";
|
||||
public const string STILL_SUIT_ARMS_INDEX_KEY = "still-suit-arms";
|
||||
public const string REBREATHER_INDEX_KEY = "rebreather";
|
||||
public const string SCUBA_TANK_INDEX_KEY = "scuba-tank";
|
||||
public const string REINFORCED_SUIT_INDEX_KEY = "reinforced-suit";
|
||||
public const string REINFORCED_SUIT_ARMS_INDEX_KEY = "reinforced-suit-arms";
|
||||
public const string REINFORCED_GLOVES_INDEX_KEY = "reinforced-gloves";
|
||||
public const string RADIATION_SUIT_ARMS_INDEX_KEY = "radiation-suit-arms";
|
||||
public const string RADIATION_SUIT_LEG_INDEX_KEY = "radiation-suit-leg";
|
||||
public const string RADIATION_SUIT_FEET_INDEX_KEY = "radiation-suit-feet";
|
||||
public const string RADIATION_SUIT_BELT_INDEX_KEY = "radiation-suit-belt";
|
||||
public const string RADIATION_SUIT_VEST_INDEX_KEY = "radiation-suit-vest";
|
||||
public const string RADIATION_HELMET_INDEX_KEY = "radiation-helmet";
|
||||
public const string RADIATION_SUIT_NECK_CLASP_INDEX_KEY = "radiation-suit-neck-clasp";
|
||||
public const string RADIATION_SUIT_TANK_INDEX_KEY = "radiation-suit-tank";
|
||||
public const string FINS_INDEX_KEY = "fins";
|
||||
}
|
||||
}
|
@@ -0,0 +1,103 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.ColorSwap;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Equipment;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Equipment.Abstract;
|
||||
using NitroxClient.MonoBehaviours;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerModel;
|
||||
|
||||
public class PlayerModelManager
|
||||
{
|
||||
private readonly IEnumerable<IColorSwapManager> colorSwapManagers;
|
||||
private List<IEquipmentVisibilityHandler> equipmentVisibilityHandlers;
|
||||
|
||||
public PlayerModelManager(IEnumerable<IColorSwapManager> colorSwapManagers)
|
||||
{
|
||||
this.colorSwapManagers = colorSwapManagers;
|
||||
}
|
||||
|
||||
public void BeginApplyPlayerColor(INitroxPlayer player)
|
||||
{
|
||||
Multiplayer.Main.StartCoroutine(ApplyPlayerColor(player, colorSwapManagers));
|
||||
}
|
||||
|
||||
public void RegisterEquipmentVisibilityHandler(GameObject playerModel)
|
||||
{
|
||||
equipmentVisibilityHandlers = new List<IEquipmentVisibilityHandler>
|
||||
{
|
||||
new DiveSuitVisibilityHandler(playerModel),
|
||||
new ScubaSuitVisibilityHandler(playerModel),
|
||||
new FinsVisibilityHandler(playerModel),
|
||||
new RadiationSuitVisibilityHandler(playerModel),
|
||||
new ReinforcedSuitVisibilityHandler(playerModel),
|
||||
new StillSuitVisibilityHandler(playerModel)
|
||||
};
|
||||
}
|
||||
|
||||
public void UpdateEquipmentVisibility(ReadOnlyCollection<TechType> currentEquipment)
|
||||
{
|
||||
foreach (IEquipmentVisibilityHandler equipmentVisibilityHandler in equipmentVisibilityHandlers)
|
||||
{
|
||||
equipmentVisibilityHandler.UpdateEquipmentVisibility(currentEquipment);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator CreateSignalPrototype(IOut<GameObject> result)
|
||||
{
|
||||
CoroutineTask<GameObject> signalHandle = AddressablesUtility.InstantiateAsync("WorldEntities/Environment/Generated/Signal.prefab", Multiplayer.Main.transform, awake: false);
|
||||
yield return signalHandle;
|
||||
|
||||
GameObject go = signalHandle.GetResult();
|
||||
go.name = "RemotePlayerSignalPrototype";
|
||||
go.transform.localScale = new Vector3(.5f, .5f, .5f);
|
||||
go.transform.localPosition = new Vector3(0, 0.8f, 0);
|
||||
go.SetActive(false);
|
||||
|
||||
result.Set(go);
|
||||
}
|
||||
|
||||
public IEnumerator AttachPing(INitroxPlayer player)
|
||||
{
|
||||
TaskResult<GameObject> result = new();
|
||||
yield return CreateSignalPrototype(result);
|
||||
|
||||
GameObject signalBase = Object.Instantiate(result.value, player.PlayerModel.transform, false);
|
||||
signalBase.name = $"signal_{player.PlayerName}";
|
||||
signalBase.SetActive(true);
|
||||
|
||||
PingInstance ping = signalBase.GetComponent<PingInstance>();
|
||||
ping.Initialize();
|
||||
ping.SetLabel($"Player {player.PlayerName}");
|
||||
ping.pingType = PingType.Signal;
|
||||
// ping will be moved to the player list tab
|
||||
ping.displayPingInManager = false;
|
||||
|
||||
// SignalPing is not required for player as we don't need to display text or anchor to a specific world position
|
||||
// we also take a dependency on the lack of signalping later to differentiate remote player pings from others.
|
||||
Object.DestroyImmediate(signalBase.GetComponent<SignalPing>());
|
||||
|
||||
SetInGamePingColor(player, ping);
|
||||
}
|
||||
|
||||
private static void SetInGamePingColor(INitroxPlayer player, PingInstance ping)
|
||||
{
|
||||
uGUI_Pings pings = Object.FindObjectOfType<uGUI_Pings>();
|
||||
|
||||
pings.OnColor(ping.Id, player.PlayerSettings.PlayerColor.ToUnity());
|
||||
}
|
||||
|
||||
private static IEnumerator ApplyPlayerColor(INitroxPlayer player, IEnumerable<IColorSwapManager> colorSwapManagers)
|
||||
{
|
||||
ColorSwapAsyncOperation swapOperation = new(player, colorSwapManagers);
|
||||
|
||||
swapOperation.BeginColorSwap();
|
||||
yield return new WaitUntil(() => swapOperation.IsColorSwapComplete());
|
||||
swapOperation.ApplySwappedColors();
|
||||
}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerPreferences
|
||||
{
|
||||
public interface IPreferenceStateProvider
|
||||
{
|
||||
PlayerPreferenceState GetPreferenceState();
|
||||
void SavePreferenceState(PlayerPreferenceState preferenceState);
|
||||
}
|
||||
}
|
@@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerPreferences
|
||||
{
|
||||
[Serializable]
|
||||
public class PlayerPreference : IEquatable<PlayerPreference>
|
||||
{
|
||||
public string PlayerName { get; private set; }
|
||||
public float RedAdditive { get; private set; }
|
||||
public float GreenAdditive { get; private set; }
|
||||
public float BlueAdditive { get; private set; }
|
||||
|
||||
public PlayerPreference()
|
||||
{
|
||||
}
|
||||
|
||||
public PlayerPreference(Color playerColor)
|
||||
{
|
||||
RedAdditive = playerColor.r;
|
||||
GreenAdditive = playerColor.g;
|
||||
BlueAdditive = playerColor.b;
|
||||
}
|
||||
|
||||
public PlayerPreference(string playerName, Color playerColor)
|
||||
{
|
||||
PlayerName = playerName;
|
||||
RedAdditive = playerColor.r;
|
||||
GreenAdditive = playerColor.g;
|
||||
BlueAdditive = playerColor.b;
|
||||
}
|
||||
|
||||
public bool Equals(PlayerPreference other)
|
||||
{
|
||||
if (ReferenceEquals(null, other))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(this, other))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return string.Equals(PlayerName, other.PlayerName) && RedAdditive.Equals(other.RedAdditive) && GreenAdditive.Equals(other.GreenAdditive) && BlueAdditive.Equals(other.BlueAdditive);
|
||||
}
|
||||
|
||||
public PlayerPreference Clone()
|
||||
{
|
||||
return new PlayerPreference
|
||||
{
|
||||
PlayerName = PlayerName,
|
||||
RedAdditive = RedAdditive,
|
||||
GreenAdditive = GreenAdditive,
|
||||
BlueAdditive = BlueAdditive
|
||||
};
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (ReferenceEquals(null, obj))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(this, obj))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (obj.GetType() != GetType())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Equals((PlayerPreference)obj);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
int hashCode = PlayerName != null ? PlayerName.GetHashCode() : 0;
|
||||
hashCode = (hashCode * 397) ^ RedAdditive.GetHashCode();
|
||||
hashCode = (hashCode * 397) ^ GreenAdditive.GetHashCode();
|
||||
hashCode = (hashCode * 397) ^ BlueAdditive.GetHashCode();
|
||||
return hashCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//LitJson does not seem to be capable of ignoring certain properties.
|
||||
public static class PlayerPreferenceExtensions
|
||||
{
|
||||
public static Color PreferredColor(this PlayerPreference playerPreference)
|
||||
{
|
||||
return new Color(playerPreference.RedAdditive, playerPreference.GreenAdditive, playerPreference.BlueAdditive);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,72 @@
|
||||
using NitroxModel.Helper;
|
||||
using NitroxModel.MultiplayerSession;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerPreferences
|
||||
{
|
||||
public class PlayerPreferenceManager
|
||||
{
|
||||
private readonly PlayerPreferenceState state;
|
||||
private readonly IPreferenceStateProvider stateProvider;
|
||||
|
||||
public PlayerPreferenceManager(IPreferenceStateProvider stateProvider)
|
||||
{
|
||||
this.stateProvider = stateProvider;
|
||||
|
||||
state = stateProvider.GetPreferenceState();
|
||||
}
|
||||
|
||||
public void SetPreference(string ipAddress, PlayerPreference playerPreference)
|
||||
{
|
||||
Validate.NotNull(ipAddress);
|
||||
Validate.NotNull(playerPreference);
|
||||
|
||||
if (state.Preferences.ContainsKey(ipAddress))
|
||||
{
|
||||
PlayerPreference currentPreference = state.Preferences[ipAddress];
|
||||
|
||||
if (currentPreference.Equals(playerPreference))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
state.Preferences[ipAddress] = playerPreference;
|
||||
state.LastSetPlayerPreference = playerPreference;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
state.Preferences.Add(ipAddress, playerPreference);
|
||||
state.LastSetPlayerPreference = playerPreference;
|
||||
}
|
||||
|
||||
public PlayerPreference GetPreference(string ipAddress)
|
||||
{
|
||||
Validate.NotNull(ipAddress);
|
||||
|
||||
|
||||
if (state.Preferences.TryGetValue(ipAddress, out PlayerPreference preference))
|
||||
{
|
||||
return preference.Clone();
|
||||
}
|
||||
|
||||
if (state.LastSetPlayerPreference != null)
|
||||
{
|
||||
return state.LastSetPlayerPreference.Clone();
|
||||
}
|
||||
|
||||
Color playerColor = RandomColorGenerator.GenerateColor().ToUnity();
|
||||
PlayerPreference defaultPlayerPreference = new PlayerPreference(playerColor);
|
||||
|
||||
state.LastSetPlayerPreference = defaultPlayerPreference;
|
||||
|
||||
return defaultPlayerPreference;
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
stateProvider.SavePreferenceState(state);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerPreferences
|
||||
{
|
||||
[Serializable]
|
||||
public class PlayerPreferenceState
|
||||
{
|
||||
public PlayerPreference LastSetPlayerPreference;
|
||||
public Dictionary<string, PlayerPreference> Preferences;
|
||||
}
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using LitJson;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic.PlayerPreferences
|
||||
{
|
||||
/// <summary>
|
||||
/// This abstraction allows us to write tests against the preferences manager. Otherwise - we are unduly tied to Unity.
|
||||
/// </summary>
|
||||
public class UnityPreferenceStateProvider : IPreferenceStateProvider
|
||||
{
|
||||
private const string UNITY_PREF_KEY_NAME = "NITROX_PLAYER_PREFS";
|
||||
|
||||
public PlayerPreferenceState GetPreferenceState()
|
||||
{
|
||||
JsonMapper.RegisterImporter((double value) => Convert.ToSingle(value));
|
||||
JsonMapper.RegisterExporter<float>((value, writer) => writer.Write(Convert.ToDouble(value)));
|
||||
|
||||
string playerPreferencesJson = PlayerPrefs.GetString(UNITY_PREF_KEY_NAME);
|
||||
|
||||
if (string.IsNullOrEmpty(playerPreferencesJson) || playerPreferencesJson == "{}")
|
||||
{
|
||||
return new PlayerPreferenceState
|
||||
{
|
||||
Preferences = new Dictionary<string, PlayerPreference>()
|
||||
};
|
||||
}
|
||||
|
||||
return JsonMapper.ToObject<PlayerPreferenceState>(playerPreferencesJson);
|
||||
}
|
||||
|
||||
public void SavePreferenceState(PlayerPreferenceState preferenceState)
|
||||
{
|
||||
JsonMapper.RegisterImporter((double value) => Convert.ToSingle(value));
|
||||
JsonMapper.RegisterExporter<float>((value, writer) => writer.Write(Convert.ToDouble(value)));
|
||||
|
||||
string playerPreferencesJson = JsonMapper.ToJson(preferenceState);
|
||||
PlayerPrefs.SetString(UNITY_PREF_KEY_NAME, playerPreferencesJson);
|
||||
}
|
||||
}
|
||||
}
|
22
NitroxClient/GameLogic/PlayerLogic/RemotePlayerIdentifier.cs
Normal file
22
NitroxClient/GameLogic/PlayerLogic/RemotePlayerIdentifier.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.PlayerLogic;
|
||||
|
||||
/// <summary>
|
||||
/// Attached to a RemotePlayer. Useful to determine that this script's GameObject is in the EntityRoot of a RemotePlayer.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The EntityRoot of an object is defined as the top most GameObject as when an object hierarchy first was spawned in. Either from a prefab, or in Nitrox' case, a cloned root game object.
|
||||
/// </remarks>
|
||||
public class RemotePlayerIdentifier : MonoBehaviour, IObstacle
|
||||
{
|
||||
public RemotePlayer RemotePlayer;
|
||||
|
||||
public bool IsDeconstructionObstacle() => true;
|
||||
|
||||
public bool CanDeconstruct(out string reason)
|
||||
{
|
||||
reason = Language.main.Get("Nitrox_RemotePlayerObstacle");
|
||||
return false;
|
||||
}
|
||||
}
|
91
NitroxClient/GameLogic/PlayerManager.cs
Normal file
91
NitroxClient/GameLogic/PlayerManager.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NitroxClient.GameLogic.HUD;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel;
|
||||
using NitroxClient.MonoBehaviours.Discord;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.DataStructures.Util;
|
||||
using NitroxModel.GameLogic.FMOD;
|
||||
using NitroxModel.Helper;
|
||||
using NitroxModel.MultiplayerSession;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic;
|
||||
|
||||
public class PlayerManager
|
||||
{
|
||||
private readonly PlayerModelManager playerModelManager;
|
||||
private readonly PlayerVitalsManager playerVitalsManager;
|
||||
private readonly FMODWhitelist fmodWhitelist;
|
||||
private readonly Dictionary<ushort, RemotePlayer> playersById = new();
|
||||
|
||||
public OnCreateDelegate OnCreate;
|
||||
public OnRemoveDelegate OnRemove;
|
||||
|
||||
public PlayerManager(PlayerModelManager playerModelManager, PlayerVitalsManager playerVitalsManager, FMODWhitelist fmodWhitelist)
|
||||
{
|
||||
this.playerModelManager = playerModelManager;
|
||||
this.playerVitalsManager = playerVitalsManager;
|
||||
this.fmodWhitelist = fmodWhitelist;
|
||||
}
|
||||
|
||||
public Optional<RemotePlayer> Find(ushort playerId)
|
||||
{
|
||||
playersById.TryGetValue(playerId, out RemotePlayer player);
|
||||
return Optional.OfNullable(player);
|
||||
}
|
||||
|
||||
public bool TryFind(ushort playerId, out RemotePlayer remotePlayer) => playersById.TryGetValue(playerId, out remotePlayer);
|
||||
|
||||
public Optional<RemotePlayer> Find(NitroxId playerNitroxId)
|
||||
{
|
||||
RemotePlayer remotePlayer = playersById.Select(idToPlayer => idToPlayer.Value)
|
||||
.FirstOrDefault(player => player.PlayerContext.PlayerNitroxId == playerNitroxId);
|
||||
|
||||
return Optional.OfNullable(remotePlayer);
|
||||
}
|
||||
|
||||
public IEnumerable<RemotePlayer> GetAll()
|
||||
{
|
||||
return playersById.Values;
|
||||
}
|
||||
|
||||
public HashSet<GameObject> GetAllPlayerObjects()
|
||||
{
|
||||
HashSet<GameObject> remotePlayerObjects = GetAll().Select(player => player.Body).ToSet();
|
||||
remotePlayerObjects.Add(Player.mainObject);
|
||||
return remotePlayerObjects;
|
||||
}
|
||||
|
||||
public RemotePlayer Create(PlayerContext playerContext)
|
||||
{
|
||||
Validate.NotNull(playerContext);
|
||||
Validate.IsFalse(playersById.ContainsKey(playerContext.PlayerId));
|
||||
|
||||
RemotePlayer remotePlayer = new(playerContext, playerModelManager, playerVitalsManager, fmodWhitelist);
|
||||
|
||||
playersById.Add(remotePlayer.PlayerId, remotePlayer);
|
||||
OnCreate(remotePlayer.PlayerId, remotePlayer);
|
||||
|
||||
DiscordClient.UpdatePartySize(GetTotalPlayerCount());
|
||||
|
||||
return remotePlayer;
|
||||
}
|
||||
|
||||
public void RemovePlayer(ushort playerId)
|
||||
{
|
||||
if (playersById.TryGetValue(playerId, out RemotePlayer player))
|
||||
{
|
||||
player.Destroy();
|
||||
playersById.Remove(playerId);
|
||||
OnRemove(playerId, player);
|
||||
DiscordClient.UpdatePartySize(GetTotalPlayerCount());
|
||||
}
|
||||
}
|
||||
|
||||
/// <returns>Remote players + You => X + 1</returns>
|
||||
public int GetTotalPlayerCount() => playersById.Count + 1;
|
||||
|
||||
public delegate void OnCreateDelegate(ushort playerId, RemotePlayer remotePlayer);
|
||||
public delegate void OnRemoveDelegate(ushort playerId, RemotePlayer remotePlayer);
|
||||
}
|
567
NitroxClient/GameLogic/RemotePlayer.cs
Normal file
567
NitroxClient/GameLogic/RemotePlayer.cs
Normal file
@@ -0,0 +1,567 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using NitroxClient.GameLogic.HUD;
|
||||
using NitroxClient.GameLogic.PlayerLogic;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract;
|
||||
using NitroxClient.MonoBehaviours;
|
||||
using NitroxClient.MonoBehaviours.Cyclops;
|
||||
using NitroxClient.MonoBehaviours.Gui.HUD;
|
||||
using NitroxClient.MonoBehaviours.Vehicles;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel.GameLogic.FMOD;
|
||||
using NitroxModel.MultiplayerSession;
|
||||
using NitroxModel.Server;
|
||||
using UnityEngine;
|
||||
using UWE;
|
||||
|
||||
namespace NitroxClient.GameLogic;
|
||||
|
||||
public class RemotePlayer : INitroxPlayer
|
||||
{
|
||||
/// <summary>
|
||||
/// Marks <see cref="Player.mainObject"/> and every <see cref="Body"/> so they can be precisely queried (e.g. by sea dragons).
|
||||
/// The value (5050) is determined arbitrarily and should not be used already.
|
||||
/// </summary>
|
||||
public const EcoTargetType PLAYER_ECO_TARGET_TYPE = (EcoTargetType)5050;
|
||||
|
||||
private static readonly int animatorPlayerIn = Animator.StringToHash("player_in");
|
||||
|
||||
private readonly PlayerModelManager playerModelManager;
|
||||
private readonly PlayerVitalsManager playerVitalsManager;
|
||||
private readonly FMODWhitelist fmodWhitelist;
|
||||
|
||||
public PlayerContext PlayerContext { get; }
|
||||
public GameObject Body { get; private set; }
|
||||
public GameObject PlayerModel { get; private set; }
|
||||
public Rigidbody RigidBody { get; private set; }
|
||||
public CapsuleCollider Collider { get; private set; }
|
||||
public ArmsController ArmsController { get; private set; }
|
||||
public AnimationController AnimationController { get; private set; }
|
||||
public ItemsContainer Inventory { get; private set; }
|
||||
public Transform ItemAttachPoint { get; private set; }
|
||||
public RemotePlayerVitals vitals { get; private set; }
|
||||
|
||||
public ushort PlayerId => PlayerContext.PlayerId;
|
||||
public string PlayerName => PlayerContext.PlayerName;
|
||||
public PlayerSettings PlayerSettings => PlayerContext.PlayerSettings;
|
||||
|
||||
public Vehicle Vehicle { get; private set; }
|
||||
public SubRoot SubRoot { get; private set; }
|
||||
public EscapePod EscapePod { get; private set; }
|
||||
public PilotingChair PilotingChair { get; private set; }
|
||||
public InfectedMixin InfectedMixin { get; private set; }
|
||||
public LiveMixin LiveMixin { get; private set; }
|
||||
|
||||
public readonly Event<RemotePlayer> PlayerDeathEvent = new();
|
||||
|
||||
public readonly Event<RemotePlayer> PlayerDisconnectEvent = new();
|
||||
|
||||
public CyclopsPawn Pawn { get; set; }
|
||||
|
||||
public RemotePlayer(PlayerContext playerContext, PlayerModelManager playerModelManager, PlayerVitalsManager playerVitalsManager, FMODWhitelist fmodWhitelist)
|
||||
{
|
||||
PlayerContext = playerContext;
|
||||
this.playerModelManager = playerModelManager;
|
||||
this.playerVitalsManager = playerVitalsManager;
|
||||
this.fmodWhitelist = fmodWhitelist;
|
||||
}
|
||||
|
||||
public void InitializeGameObject(GameObject playerBody)
|
||||
{
|
||||
Body = playerBody;
|
||||
Body.name = PlayerName;
|
||||
|
||||
RigidBody = Body.AddComponent<Rigidbody>();
|
||||
RigidBody.useGravity = false;
|
||||
RigidBody.interpolation = RigidbodyInterpolation.Interpolate;
|
||||
|
||||
NitroxEntity.SetNewId(Body, PlayerContext.PlayerNitroxId);
|
||||
|
||||
// Get player
|
||||
PlayerModel = Body.RequireGameObject("player_view");
|
||||
// Move variables to keep player animations from mirroring and for identification
|
||||
ArmsController = PlayerModel.GetComponent<ArmsController>();
|
||||
ArmsController.smoothSpeedUnderWater = 0;
|
||||
ArmsController.smoothSpeedAboveWater = 0;
|
||||
|
||||
// ConditionRules has Player.Main based conditions from ArmsController
|
||||
PlayerModel.GetComponent<ConditionRules>().enabled = false;
|
||||
|
||||
AnimationController = PlayerModel.AddComponent<AnimationController>();
|
||||
|
||||
Transform inventoryTransform = new GameObject("Inventory").transform;
|
||||
inventoryTransform.SetParent(Body.transform);
|
||||
Inventory = new ItemsContainer(6, 8, inventoryTransform, $"NitroxInventoryStorage_{PlayerName}", null);
|
||||
|
||||
ItemAttachPoint = PlayerModel.transform.Find(PlayerEquipmentConstants.ITEM_ATTACH_POINT_GAME_OBJECT_NAME);
|
||||
|
||||
CoroutineUtils.StartCoroutineSmart(playerModelManager.AttachPing(this));
|
||||
playerModelManager.BeginApplyPlayerColor(this);
|
||||
playerModelManager.RegisterEquipmentVisibilityHandler(PlayerModel);
|
||||
SetupBody();
|
||||
SetupSkyAppliers();
|
||||
SetupPlayerSounds();
|
||||
SetupMixins();
|
||||
|
||||
vitals = playerVitalsManager.CreateOrFindForPlayer(this);
|
||||
RefreshVitalsVisibility();
|
||||
|
||||
PlayerDisconnectEvent.AddHandler(Body, _ =>
|
||||
{
|
||||
Pawn?.Unregister();
|
||||
Pawn = null;
|
||||
});
|
||||
|
||||
PlayerDeathEvent.AddHandler(Body, _ =>
|
||||
{
|
||||
ResetStates();
|
||||
});
|
||||
}
|
||||
|
||||
public void Attach(Transform transform, bool keepWorldTransform = false)
|
||||
{
|
||||
Body.transform.SetParent(transform);
|
||||
|
||||
if (!keepWorldTransform)
|
||||
{
|
||||
UWE.Utils.ZeroTransform(Body);
|
||||
}
|
||||
|
||||
SkyEnvironmentChanged.Broadcast(Body, transform);
|
||||
}
|
||||
|
||||
public void Detach()
|
||||
{
|
||||
Body.transform.SetParent(null);
|
||||
SkyEnvironmentChanged.Broadcast(Body, (GameObject)null);
|
||||
}
|
||||
|
||||
public void UpdatePosition(Vector3 position, Vector3 velocity, Quaternion bodyRotation, Quaternion aimingRotation)
|
||||
{
|
||||
// It might happen that we get movement packets before the body is actually initialized which is not too bad
|
||||
if (!Body)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Body.SetActive(true);
|
||||
|
||||
// When receiving movement packets, a player can not be controlling a vehicle (they can walk through subroots though).
|
||||
SetVehicle(null);
|
||||
SetPilotingChair(null);
|
||||
|
||||
AnimationController.AimingRotation = aimingRotation;
|
||||
AnimationController.UpdatePlayerAnimations = true;
|
||||
AnimationController.Velocity = MovementHelper.GetCorrectedVelocity(position, velocity, Body, Time.fixedDeltaTime);
|
||||
|
||||
// If in a subroot the position will be relative to the subroot
|
||||
if (SubRoot && SubRoot.isBase)
|
||||
{
|
||||
Quaternion vehicleAngle = SubRoot.transform.rotation;
|
||||
position = vehicleAngle * position;
|
||||
position += SubRoot.transform.position;
|
||||
bodyRotation = vehicleAngle * bodyRotation;
|
||||
aimingRotation = vehicleAngle * aimingRotation;
|
||||
}
|
||||
|
||||
RigidBody.velocity = AnimationController.Velocity;
|
||||
RigidBody.angularVelocity = MovementHelper.GetCorrectedAngularVelocity(bodyRotation, Vector3.zero, Body, Time.fixedDeltaTime);
|
||||
}
|
||||
|
||||
public void UpdatePositionInCyclops(Vector3 localPosition, Quaternion localRotation)
|
||||
{
|
||||
if (Pawn == null || PilotingChair)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetVehicle(null);
|
||||
|
||||
AnimationController.AimingRotation = localRotation;
|
||||
AnimationController.UpdatePlayerAnimations = true;
|
||||
AnimationController.Velocity = (localPosition - Pawn.Handle.transform.localPosition) / Time.fixedDeltaTime;
|
||||
|
||||
Pawn.Handle.transform.localPosition = localPosition;
|
||||
Pawn.Handle.transform.localRotation = localRotation;
|
||||
}
|
||||
|
||||
public void SetPilotingChair(PilotingChair newPilotingChair)
|
||||
{
|
||||
if (PilotingChair != newPilotingChair)
|
||||
{
|
||||
PilotingChair = newPilotingChair;
|
||||
|
||||
CyclopsMovementReplicator cyclopsMovementReplicator = null;
|
||||
|
||||
// For unexpected and expected cases, for example when a player is driving a cyclops but the cyclops is destroyed
|
||||
if (!SubRoot)
|
||||
{
|
||||
Log.Error("Player changed PilotingChair but is not in SubRoot!");
|
||||
}
|
||||
else
|
||||
{
|
||||
cyclopsMovementReplicator = SubRoot.GetComponent<CyclopsMovementReplicator>();
|
||||
}
|
||||
|
||||
if (PilotingChair)
|
||||
{
|
||||
Attach(PilotingChair.sittingPosition.transform);
|
||||
ArmsController.SetWorldIKTarget(PilotingChair.leftHandPlug, PilotingChair.rightHandPlug);
|
||||
|
||||
if (cyclopsMovementReplicator)
|
||||
{
|
||||
cyclopsMovementReplicator.Enter(this);
|
||||
}
|
||||
|
||||
if (SubRoot)
|
||||
{
|
||||
SkyEnvironmentChanged.Broadcast(Body, SubRoot);
|
||||
}
|
||||
|
||||
AnimationController.UpdatePlayerAnimations = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
SetSubRoot(SubRoot, true);
|
||||
ArmsController.SetWorldIKTarget(null, null);
|
||||
|
||||
if (cyclopsMovementReplicator)
|
||||
{
|
||||
cyclopsMovementReplicator.Exit();
|
||||
}
|
||||
}
|
||||
|
||||
bool isKinematic = newPilotingChair;
|
||||
UWE.Utils.SetIsKinematicAndUpdateInterpolation(RigidBody, isKinematic, true);
|
||||
AnimationController["cyclops_steering"] = newPilotingChair;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetSubRoot(SubRoot newSubRoot, bool force = false)
|
||||
{
|
||||
if (SubRoot != newSubRoot || force)
|
||||
{
|
||||
// Unregister from previous cyclops
|
||||
Pawn?.Unregister();
|
||||
Pawn = null;
|
||||
|
||||
if (newSubRoot)
|
||||
{
|
||||
Attach(newSubRoot.transform, true);
|
||||
|
||||
// Register in new cyclops
|
||||
if (newSubRoot.TryGetComponent(out NitroxCyclops nitroxCyclops))
|
||||
{
|
||||
nitroxCyclops.OnPlayerEnter(this);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Detach();
|
||||
}
|
||||
|
||||
SubRoot = newSubRoot;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetEscapePod(EscapePod newEscapePod)
|
||||
{
|
||||
if (EscapePod != newEscapePod)
|
||||
{
|
||||
if (newEscapePod)
|
||||
{
|
||||
Attach(newEscapePod.transform, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
Detach();
|
||||
}
|
||||
|
||||
EscapePod = newEscapePod;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetVehicle(Vehicle newVehicle)
|
||||
{
|
||||
if (Vehicle != newVehicle)
|
||||
{
|
||||
if (Vehicle)
|
||||
{
|
||||
Vehicle.mainAnimator.SetBool(animatorPlayerIn, false);
|
||||
|
||||
Detach();
|
||||
ArmsController.SetWorldIKTarget(null, null);
|
||||
|
||||
if (Vehicle.TryGetComponent(out VehicleMovementReplicator vehicleMovementReplicator))
|
||||
{
|
||||
vehicleMovementReplicator.Exit();
|
||||
}
|
||||
}
|
||||
|
||||
if (newVehicle)
|
||||
{
|
||||
newVehicle.mainAnimator.SetBool(animatorPlayerIn, true);
|
||||
|
||||
Attach(newVehicle.playerPosition.transform);
|
||||
ArmsController.SetWorldIKTarget(newVehicle.leftHandPlug, newVehicle.rightHandPlug);
|
||||
|
||||
// From here, a basic issue can happen.
|
||||
// When a vehicle is docked since we joined a game and another player undocks him before the local player does,
|
||||
// no VehicleMovementReplicator can be found on the vehicle because they are only created when receiving SimulationOwnership packets
|
||||
// Therefore we need to make sure that the VehicleMovementReplicator component exists before using it
|
||||
switch (newVehicle)
|
||||
{
|
||||
case SeaMoth:
|
||||
newVehicle.gameObject.EnsureComponent<SeamothMovementReplicator>().Enter(this);
|
||||
break;
|
||||
case Exosuit:
|
||||
newVehicle.gameObject.EnsureComponent<ExosuitMovementReplicator>().Enter(this);
|
||||
break;
|
||||
}
|
||||
|
||||
AnimationController.UpdatePlayerAnimations = false;
|
||||
}
|
||||
|
||||
bool isKinematic = newVehicle;
|
||||
UWE.Utils.SetIsKinematicAndUpdateInterpolation(RigidBody, isKinematic, true);
|
||||
|
||||
Vehicle = newVehicle;
|
||||
|
||||
AnimationController["in_seamoth"] = newVehicle is SeaMoth;
|
||||
AnimationController["in_exosuit"] = AnimationController["using_mechsuit"] = newVehicle is Exosuit;
|
||||
|
||||
// In case we are dismissing the current seamoth to enter the cyclops through a docking,
|
||||
// we need to setup the player back in the cyclops
|
||||
if (!newVehicle && SubRoot)
|
||||
{
|
||||
SetSubRoot(SubRoot, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drops the remote player, swimming where he is. Resets its animator.
|
||||
/// </summary>
|
||||
public void ResetStates()
|
||||
{
|
||||
SetPilotingChair(null);
|
||||
SetVehicle(null);
|
||||
SetSubRoot(null);
|
||||
AnimationController.UpdatePlayerAnimations = true;
|
||||
AnimationController.Reset();
|
||||
ArmsController.SetWorldIKTarget(null, null);
|
||||
}
|
||||
|
||||
public void Destroy()
|
||||
{
|
||||
Log.Info($"{PlayerName} left the game");
|
||||
Log.InGame(Language.main.Get("Nitrox_PlayerLeft").Replace("{PLAYER}", PlayerName));
|
||||
NitroxEntity.RemoveFrom(Body);
|
||||
Object.DestroyImmediate(Body);
|
||||
}
|
||||
|
||||
public void UpdateAnimationAndCollider(AnimChangeType type, AnimChangeState state)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case AnimChangeType.UNDERWATER:
|
||||
AnimationController["is_underwater"] = state != AnimChangeState.OFF;
|
||||
break;
|
||||
case AnimChangeType.BENCH:
|
||||
AnimationController["cinematics_enabled"] = state != AnimChangeState.UNSET;
|
||||
AnimationController["bench_sit"] = state == AnimChangeState.ON;
|
||||
AnimationController["bench_stand_up"] = state == AnimChangeState.OFF;
|
||||
break;
|
||||
case AnimChangeType.INFECTION_REVEAL:
|
||||
AnimationController["player_infected"] = state != AnimChangeState.UNSET;
|
||||
break;
|
||||
}
|
||||
|
||||
// Rough estimation for different collider boxes in different animation stages
|
||||
if (AnimationController["is_underwater"])
|
||||
{
|
||||
Collider.center = new(0f, -0.3f, 0f);
|
||||
Collider.height = 0.5f;
|
||||
}
|
||||
else
|
||||
{
|
||||
Collider.center = new(0f, -0.8f, 0f);
|
||||
Collider.height = 1.5f;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateEquipmentVisibility(List<TechType> equippedItems)
|
||||
{
|
||||
playerModelManager.UpdateEquipmentVisibility(new ReadOnlyCollection<TechType>(equippedItems));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes the RemotePlayer recognizable as an obstacle for buildings, and as a target for creatures
|
||||
/// </summary>
|
||||
private void SetupBody()
|
||||
{
|
||||
// set as a target for reapers
|
||||
EcoTarget sharkEcoTarget = Body.AddComponent<EcoTarget>();
|
||||
sharkEcoTarget.SetTargetType(EcoTargetType.Shark);
|
||||
|
||||
EcoTarget playerEcoTarget = Body.AddComponent<EcoTarget>();
|
||||
playerEcoTarget.SetTargetType(PLAYER_ECO_TARGET_TYPE);
|
||||
|
||||
TechTag techTag = Body.AddComponent<TechTag>();
|
||||
techTag.type = TechType.Player;
|
||||
|
||||
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;
|
||||
Collider.contactOffset = refCollider.contactOffset;
|
||||
Collider.isTrigger = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warn("The main collider of the main Player couldn't be found or is not a CapsuleCollider. Collisions for the RemotePlayer won't be created");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allows the remote player model to have its lighting dynamically adjusted
|
||||
/// </summary>
|
||||
private void SetupSkyAppliers()
|
||||
{
|
||||
// SkyAppliers apply the light effects of a lighting source on a set of renderers
|
||||
SkyApplier skyApplier = Body.AddComponent<SkyApplier>();
|
||||
skyApplier.anchorSky = Skies.Auto;
|
||||
skyApplier.emissiveFromPower = false;
|
||||
skyApplier.dynamic = true;
|
||||
skyApplier.renderers = Body.GetComponentsInChildren<SkinnedMeshRenderer>(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up all bubbles, breathing and diving sounds to be multiplayer ready
|
||||
/// </summary>
|
||||
private void SetupPlayerSounds()
|
||||
{
|
||||
GameObject remotePlayerSoundsRoot = new("RemotePlayerSounds");
|
||||
remotePlayerSoundsRoot.transform.SetParent(Body.transform);
|
||||
FMODEmitterController emitterController = Body.AddComponent<FMODEmitterController>();
|
||||
|
||||
static void CopyEmitter(FMOD_CustomEmitter src, FMOD_CustomEmitter dst)
|
||||
{
|
||||
dst.asset = src.asset;
|
||||
dst.playOnAwake = src.playOnAwake;
|
||||
dst.stopImmediatelyOnDisable = src.stopImmediatelyOnDisable;
|
||||
dst.followParent = src.followParent;
|
||||
dst.restartOnPlay = src.restartOnPlay;
|
||||
}
|
||||
|
||||
// Bubbles
|
||||
PlayerBreathBubbles localPlayerBubbles = Player.main.GetComponentInChildren<PlayerBreathBubbles>(true);
|
||||
FMOD_CustomEmitter bubblesCustomEmitter = remotePlayerSoundsRoot.AddComponent<FMOD_CustomEmitter>();
|
||||
CopyEmitter(localPlayerBubbles.bubbleSound, bubblesCustomEmitter);
|
||||
|
||||
if (fmodWhitelist.IsWhitelisted(bubblesCustomEmitter.asset.path, out float bubblesSoundRadius))
|
||||
{
|
||||
emitterController.AddEmitter(bubblesCustomEmitter.asset.path, bubblesCustomEmitter, bubblesSoundRadius);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"[{nameof(RemotePlayer)}] Manual created FMOD emitter for {nameof(PlayerBreathBubbles)} but linked sound is not whitelisted: ({bubblesCustomEmitter.asset.path})");
|
||||
}
|
||||
|
||||
// Breathing
|
||||
BreathingSound breathingSound = Player.main.GetComponentInChildren<BreathingSound>(true);
|
||||
FMOD_CustomEmitter breathingSoundCustomEmitter = remotePlayerSoundsRoot.AddComponent<FMOD_CustomEmitter>();
|
||||
breathingSoundCustomEmitter.asset = breathingSound.loopingBreathingSound.asset;
|
||||
|
||||
if (fmodWhitelist.IsWhitelisted(breathingSoundCustomEmitter.asset.path, out float breathingSoundRadius))
|
||||
{
|
||||
emitterController.AddEmitter(breathingSoundCustomEmitter.asset.path, breathingSoundCustomEmitter, breathingSoundRadius);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"[{nameof(RemotePlayer)}] Manual created FMOD emitter for {nameof(BreathingSound)} but linked sound is not whitelisted: ({breathingSoundCustomEmitter.asset.path})");
|
||||
}
|
||||
|
||||
// Diving
|
||||
WaterAmbience waterAmbience = Player.main.GetComponentInChildren<WaterAmbience>(true);
|
||||
FMOD_CustomEmitter diveStartCustomEmitter = remotePlayerSoundsRoot.AddComponent<FMOD_CustomEmitter>();
|
||||
CopyEmitter(waterAmbience.diveStartSplash, diveStartCustomEmitter);
|
||||
|
||||
if (fmodWhitelist.IsWhitelisted(diveStartCustomEmitter.asset.path, out float diveSoundRadius))
|
||||
{
|
||||
emitterController.AddEmitter(diveStartCustomEmitter.asset.path, diveStartCustomEmitter, diveSoundRadius);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"[{nameof(RemotePlayer)}] Manual created FMOD emitter for {nameof(WaterAmbience)} but linked sound is not whitelisted: ({diveStartCustomEmitter.asset.path})");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An InfectedMixin is required for behaviours like <see cref="AggressiveWhenSeeTarget"/> which look for this on the target they find
|
||||
/// </summary>
|
||||
private void SetupMixins()
|
||||
{
|
||||
InfectedMixin = Body.AddComponent<InfectedMixin>();
|
||||
InfectedMixin.shaderKeyWord = InfectedMixin.uwe_playerinfection;
|
||||
Renderer renderer = PlayerModel.transform.Find("male_geo/diveSuit/diveSuit_hands_geo").GetComponent<Renderer>();
|
||||
InfectedMixin.renderers = [renderer];
|
||||
|
||||
LiveMixin = Body.AddComponent<LiveMixin>();
|
||||
LiveMixin.data = new()
|
||||
{
|
||||
maxHealth = 100,
|
||||
broadcastKillOnDeath = false
|
||||
};
|
||||
LiveMixin.health = 100;
|
||||
// We set the remote player to invincible because we only want this component to be detectable but not to work
|
||||
LiveMixin.invincible = true;
|
||||
}
|
||||
|
||||
public void UpdateHealthAndInfection(float health, float infection)
|
||||
{
|
||||
if (LiveMixin)
|
||||
{
|
||||
LiveMixin.health = health;
|
||||
}
|
||||
|
||||
if (InfectedMixin)
|
||||
{
|
||||
InfectedMixin.infectedAmount = infection;
|
||||
InfectedMixin.UpdateInfectionShading();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetGameMode(NitroxGameMode gameMode)
|
||||
{
|
||||
PlayerContext.GameMode = gameMode;
|
||||
RefreshVitalsVisibility();
|
||||
}
|
||||
|
||||
private void RefreshVitalsVisibility()
|
||||
{
|
||||
if (vitals)
|
||||
{
|
||||
bool visible = PlayerContext.GameMode != NitroxGameMode.CREATIVE;
|
||||
vitals.SetStatsVisible(visible);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adaptation of <see cref="Player.CanBeAttacked"/> for remote players.
|
||||
/// NB: This doesn't check for other player's use of 'invisible' command
|
||||
/// </summary>
|
||||
public bool CanBeAttacked()
|
||||
{
|
||||
return !SubRoot && !EscapePod && PlayerContext.GameMode != NitroxGameMode.CREATIVE;
|
||||
}
|
||||
}
|
74
NitroxClient/GameLogic/Rockets.cs
Normal file
74
NitroxClient/GameLogic/Rockets.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxClient.MonoBehaviours;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.DataStructures.Unity;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using NitroxModel_Subnautica.Packets;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic
|
||||
{
|
||||
public class Rockets
|
||||
{
|
||||
private readonly IPacketSender packetSender;
|
||||
private readonly Vehicles vehicles;
|
||||
private readonly PlayerManager playerManager;
|
||||
|
||||
public Rockets(IPacketSender packetSender, Vehicles vehicles, PlayerManager playerManager)
|
||||
{
|
||||
this.packetSender = packetSender;
|
||||
this.vehicles = vehicles;
|
||||
this.playerManager = playerManager;
|
||||
}
|
||||
|
||||
public void RequestRocketLaunch(Rocket rocket)
|
||||
{
|
||||
if (rocket.TryGetNitroxEntity(out NitroxEntity entity))
|
||||
{
|
||||
packetSender.Send(new RocketLaunch(entity.Id));
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"{nameof(Rockets.RequestRocketLaunch)}: Can't find a NitroxEntity attached to the Rocket: {rocket.name}");
|
||||
}
|
||||
}
|
||||
|
||||
public void RocketLaunch(NitroxId rocketId)
|
||||
{
|
||||
// Avoid useless calculations
|
||||
if (LaunchRocket.launchStarted)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject rocketObject = NitroxEntity.RequireObjectFrom(rocketId);
|
||||
GameObject sphereCenter = rocketObject.FindChild("AtmosphereVolume");
|
||||
LaunchRocket launchRocket = rocketObject.RequireComponentInChildren<LaunchRocket>(true);
|
||||
|
||||
// Only launch if you're in the rocket so
|
||||
// verify if the distance to a centered point in the middle of the stage 3 of the rocket is inferior to 5.55 (pre-calculated radius)
|
||||
if (Player.main.IsUnderwater() ||
|
||||
Player.main.currentSub ||
|
||||
NitroxVector3.Distance(Player.main.transform.position.ToDto(), sphereCenter.transform.position.ToDto()) > 5.55f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// When the server sends this to the client, he should execute the rocket launch
|
||||
// Code extracted from LaunchRocket::OnHandClick
|
||||
LaunchRocket.SetLaunchStarted();
|
||||
PlayerTimeCapsule.main.Submit(null);
|
||||
launchRocket.StartCoroutine(launchRocket.StartEndCinematic());
|
||||
HandReticle.main.RequestCrosshairHide();
|
||||
|
||||
// We also need to hide the other players
|
||||
foreach (RemotePlayer player in playerManager.GetAll())
|
||||
{
|
||||
player.PlayerModel.SetActive(false);
|
||||
}
|
||||
|
||||
Log.InGame(Language.main.Get("Nitrox_ThankForPlaying"));
|
||||
}
|
||||
}
|
||||
}
|
33
NitroxClient/GameLogic/SeamothModulesEvent.cs
Normal file
33
NitroxClient/GameLogic/SeamothModulesEvent.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using NitroxClient.Communication.Abstract;
|
||||
using NitroxModel.DataStructures;
|
||||
using NitroxModel.Packets;
|
||||
using NitroxModel_Subnautica.DataStructures;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic
|
||||
{
|
||||
public class SeamothModulesEvent
|
||||
{
|
||||
private readonly IPacketSender packetSender;
|
||||
|
||||
public SeamothModulesEvent(IPacketSender packetSender)
|
||||
{
|
||||
this.packetSender = packetSender;
|
||||
}
|
||||
|
||||
public void BroadcastElectricalDefense(TechType techType, int slotID, SeaMoth instance)
|
||||
{
|
||||
if (!instance.TryGetIdOrWarn(out NitroxId id))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (techType == TechType.SeamothElectricalDefense)
|
||||
{
|
||||
Transform aimingTransform = Player.main.camRoot.GetAimingTransform();
|
||||
SeamothModulesAction changed = new SeamothModulesAction(techType.ToDto(), slotID, id, aimingTransform.forward.ToDto(), aimingTransform.rotation.ToDto());
|
||||
packetSender.Send(changed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
77
NitroxClient/GameLogic/Settings/NitroxPrefs.cs
Normal file
77
NitroxClient/GameLogic/Settings/NitroxPrefs.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NitroxClient.GameLogic.Settings
|
||||
{
|
||||
public class NitroxPrefs
|
||||
{
|
||||
// Add new fields here, you can use bool, float and int as type
|
||||
public static readonly NitroxPref<bool> HideIp = new("Nitrox.hideIp");
|
||||
public static readonly NitroxPref<bool> SilenceChat = new("Nitrox.silenceChat");
|
||||
public static readonly NitroxPref<bool> ChatUsed = new("Nitrox.chatUsed");
|
||||
public static readonly NitroxPref<bool> SafeBuilding = new("Nitrox.safeBuilding", true);
|
||||
public static readonly NitroxPref<bool> SafeBuildingLog = new("Nitrox.safeBuildingLog", true);
|
||||
/// <summary>
|
||||
/// In seconds. <see cref="MonoBehaviours.Vehicles.MovementReplicator"/>
|
||||
/// </summary>
|
||||
public static readonly NitroxPref<float> LatencyUpdatePeriod = new("Nitrox.latencyUpdatePeriod", 10);
|
||||
/// <summary>
|
||||
/// In milliseconds. <see cref="MonoBehaviours.Vehicles.MovementReplicator"/>
|
||||
/// </summary>
|
||||
public static readonly NitroxPref<float> SafetyLatencyMargin = new("Nitrox.safetyLatencyMargin", 0.05f);
|
||||
/// <summary>
|
||||
/// In seconds.
|
||||
/// </summary>
|
||||
public static readonly NitroxPref<float> OfflineClockSyncDuration = new("Nitrox.offlineClockSyncDuration", 5);
|
||||
}
|
||||
|
||||
public abstract class NitroxPref { }
|
||||
|
||||
public class NitroxPref<T> : NitroxPref where T : IConvertible
|
||||
{
|
||||
public string Key { get; }
|
||||
public T DefaultValue { get; }
|
||||
|
||||
public NitroxPref(string key, T defaultValue = default)
|
||||
{
|
||||
Key = key;
|
||||
DefaultValue = defaultValue;
|
||||
}
|
||||
|
||||
public T Value
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (DefaultValue)
|
||||
{
|
||||
case bool defaultBool:
|
||||
return (T)Convert.ChangeType(PlayerPrefs.GetInt(Key, defaultBool ? 1 : 0), typeof(T));
|
||||
case float defaultFloat:
|
||||
return (T)Convert.ChangeType(PlayerPrefs.GetFloat(Key, defaultFloat), typeof(T));
|
||||
case int defaultInt:
|
||||
return (T)Convert.ChangeType(PlayerPrefs.GetInt(Key, defaultInt), typeof(T));
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case bool boolValue:
|
||||
PlayerPrefs.SetInt(Key, boolValue ? 1 : 0);
|
||||
break;
|
||||
case float floatValue:
|
||||
PlayerPrefs.SetFloat(Key, floatValue);
|
||||
break;
|
||||
case int intValue:
|
||||
PlayerPrefs.SetInt(Key, intValue);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
145
NitroxClient/GameLogic/Settings/NitroxSettingsManager.cs
Normal file
145
NitroxClient/GameLogic/Settings/NitroxSettingsManager.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NitroxClient.GameLogic.Bases;
|
||||
using NitroxClient.MonoBehaviours.Gui.MainMenu.ServersList;
|
||||
using UnityEngine.Events;
|
||||
|
||||
namespace NitroxClient.GameLogic.Settings;
|
||||
|
||||
public class NitroxSettingsManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Settings grouped by their headings
|
||||
/// </summary>
|
||||
public readonly Dictionary<string, List<Setting>> NitroxSettings;
|
||||
|
||||
public NitroxSettingsManager()
|
||||
{
|
||||
NitroxSettings = new Dictionary<string, List<Setting>>();
|
||||
MakeSettings();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allows to create new settings
|
||||
///
|
||||
/// Available types : TOGGLE, SLIDER, LIST, BUTTON
|
||||
///
|
||||
/// <example>
|
||||
/// <para>Examples :</para>
|
||||
/// <code>
|
||||
/// AddSetting("Subtitles", new Setting("Test Slidder", NitroxPrefs.SliderPref, newValue => NitroxPrefs.SliderPref.Value = newValue, 0.1f, 1f, 0.4f));
|
||||
/// AddSetting("Advanced", new Setting("Test list", NitroxPrefs.ListPref, newIndex => NitroxPrefs.ListPref.Value = newIndex, new string[] { "option 1", "option 2", "option 3" }));
|
||||
/// </code>
|
||||
/// </example>
|
||||
/// </summary>
|
||||
private void MakeSettings()
|
||||
{
|
||||
AddSetting("Nitrox_StreamerSettings", new Setting("Nitrox_SilenceChat", NitroxPrefs.SilenceChat, silence => NitroxPrefs.SilenceChat.Value = silence));
|
||||
AddSetting("Nitrox_StreamerSettings", new Setting("Nitrox_HideIp", NitroxPrefs.HideIp, hide =>
|
||||
{
|
||||
NitroxPrefs.HideIp.Value = hide;
|
||||
MainMenuServerListPanel.Main.RefreshServerEntries();
|
||||
}));
|
||||
|
||||
AddSetting("Nitrox_ResyncSettings", new Setting("Nitrox_ResyncBuildings", () =>
|
||||
{
|
||||
if (BuildingHandler.Main)
|
||||
{
|
||||
BuildingHandler.Main.AskForResync();
|
||||
}
|
||||
}));
|
||||
|
||||
AddSetting("Nitrox_BuildingSettings", new Setting("Nitrox_SafeBuilding", NitroxPrefs.SafeBuilding, safe => NitroxPrefs.SafeBuilding.Value = safe));
|
||||
AddSetting("Nitrox_BuildingSettings", new Setting("Nitrox_SafeBuildingLog", NitroxPrefs.SafeBuildingLog, safeLog => NitroxPrefs.SafeBuildingLog.Value = safeLog));
|
||||
|
||||
AddSetting("Nitrox_Settings_Bandwidth", new Setting("Nitrox_Settings_LatencyUpdatePeriod", NitroxPrefs.LatencyUpdatePeriod, latencyUpdatePeriod => NitroxPrefs.LatencyUpdatePeriod.Value = (int)latencyUpdatePeriod, 1, 60, NitroxPrefs.LatencyUpdatePeriod.DefaultValue, 1, SliderLabelMode.Int, tooltip: "Nitrox_Settings_HigherForUnstable_Tooltip"));
|
||||
AddSetting("Nitrox_Settings_Bandwidth", new Setting("Nitrox_Settings_SafetyLatencyMargin", NitroxPrefs.SafetyLatencyMargin, safetyLatencyMargin => NitroxPrefs.SafetyLatencyMargin.Value = safetyLatencyMargin, 0.01f, 0.5f, NitroxPrefs.SafetyLatencyMargin.DefaultValue, 0.01f, SliderLabelMode.Float, "0.00", "Nitrox_Settings_HigherForUnstable_Tooltip"));
|
||||
AddSetting("Nitrox_Settings_Bandwidth", new Setting("Nitrox_Settings_OfflineClockSyncDuration", NitroxPrefs.OfflineClockSyncDuration, offlineClockSyncDuration => NitroxPrefs.OfflineClockSyncDuration.Value = (int)offlineClockSyncDuration, 3, 15, NitroxPrefs.OfflineClockSyncDuration.DefaultValue, 1, SliderLabelMode.Int, tooltip: "Nitrox_Settings_HigherForUnstable_Tooltip"));
|
||||
}
|
||||
|
||||
/// <summary>Adds a setting to the list under a certain heading</summary>
|
||||
public void AddSetting(string heading, Setting setting)
|
||||
{
|
||||
if (NitroxSettings.TryGetValue(heading, out List<Setting> settings))
|
||||
{
|
||||
settings.Add(setting);
|
||||
}
|
||||
else
|
||||
{
|
||||
NitroxSettings.Add(heading, new List<Setting> { setting });
|
||||
}
|
||||
}
|
||||
|
||||
public class Setting
|
||||
{
|
||||
// These fields are used by each type of setting
|
||||
// To get the value, you need to type setting.GetValue<type>() or (type)NitroxPrefs.MyPref.Value when you don't have the setting
|
||||
public readonly SettingType SettingType;
|
||||
public readonly string Label;
|
||||
public readonly NitroxPref NitroxPref;
|
||||
public readonly Delegate Callback;
|
||||
|
||||
// Slider specifics
|
||||
public readonly float SliderMinValue;
|
||||
public readonly float SliderMaxValue;
|
||||
public readonly float SliderDefaultValue;
|
||||
public readonly float SliderStep;
|
||||
public readonly SliderLabelMode LabelMode;
|
||||
/// <summary>
|
||||
/// Examples: "0", "0.00"
|
||||
/// </summary>
|
||||
public string FloatFormat;
|
||||
public readonly string Tooltip;
|
||||
|
||||
// List specifics
|
||||
public readonly string[] ListItems;
|
||||
|
||||
/// <summary>Base constructor for the class</summary>
|
||||
private Setting(SettingType settingType, string label, NitroxPref nitroxPref, Delegate callback)
|
||||
{
|
||||
SettingType = settingType;
|
||||
Label = label;
|
||||
NitroxPref = nitroxPref;
|
||||
Callback = callback;
|
||||
}
|
||||
|
||||
/// <summary>Constructor for buttons (doesn't need a NitroxPref)</summary>
|
||||
public Setting(string label, UnityAction callback)
|
||||
{
|
||||
SettingType = SettingType.BUTTON;
|
||||
Label = label;
|
||||
Callback = callback;
|
||||
}
|
||||
|
||||
/// <summary>Constructor for a Toggle setting</summary>
|
||||
public Setting(string label, NitroxPref nitroxPref, UnityAction<bool> callback) : this(SettingType.TOGGLE, label, nitroxPref, callback) { }
|
||||
|
||||
/// <summary>Constructor for a Slider setting</summary>
|
||||
public Setting(string label, NitroxPref nitroxPref, UnityAction<float> callback, float sliderMinValue, float sliderMaxValue, float sliderDefaultValue, float sliderStep, SliderLabelMode labelMode, string floatFormat = "0", string tooltip = null) : this(SettingType.SLIDER, label, nitroxPref, callback)
|
||||
{
|
||||
SliderMinValue = sliderMinValue;
|
||||
SliderMaxValue = sliderMaxValue;
|
||||
SliderDefaultValue = sliderDefaultValue;
|
||||
SliderStep = sliderStep;
|
||||
LabelMode = labelMode;
|
||||
FloatFormat = floatFormat;
|
||||
Tooltip = tooltip;
|
||||
}
|
||||
|
||||
/// <summary>Constructor for a List setting</summary>
|
||||
public Setting(string label, NitroxPref nitroxPref, UnityAction<int> callback, string[] listItems) : this(SettingType.LIST, label, nitroxPref, callback)
|
||||
{
|
||||
ListItems = listItems;
|
||||
}
|
||||
|
||||
public T GetValue<T>() where T : IConvertible
|
||||
{
|
||||
return ((NitroxPref<T>)NitroxPref).Value;
|
||||
}
|
||||
}
|
||||
|
||||
public enum SettingType
|
||||
{
|
||||
TOGGLE, SLIDER, LIST, BUTTON
|
||||
}
|
||||
}
|
14
NitroxClient/GameLogic/Simulation/HandInteraction.cs
Normal file
14
NitroxClient/GameLogic/Simulation/HandInteraction.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace NitroxClient.GameLogic.Simulation
|
||||
{
|
||||
public class HandInteraction<T> : LockRequestContext where T : IHandTarget
|
||||
{
|
||||
public T Target { get; }
|
||||
public GUIHand GuiHand { get; }
|
||||
|
||||
public HandInteraction(T Target, GUIHand GuiHand)
|
||||
{
|
||||
this.Target = Target;
|
||||
this.GuiHand = GuiHand;
|
||||
}
|
||||
}
|
||||
}
|
27
NitroxClient/GameLogic/Simulation/LockRequest.cs
Normal file
27
NitroxClient/GameLogic/Simulation/LockRequest.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using NitroxModel.DataStructures;
|
||||
|
||||
namespace NitroxClient.GameLogic.Simulation
|
||||
{
|
||||
public class LockRequest<T> : LockRequestBase where T : LockRequestContext
|
||||
{
|
||||
public delegate void LockRequestCompleted(NitroxId id, bool lockAquired, T context);
|
||||
|
||||
private LockRequestCompleted onComplete;
|
||||
private T context { get; }
|
||||
|
||||
public LockRequest(NitroxId id, SimulationLockType lockType, LockRequestCompleted onComplete, T context) : base(id, lockType)
|
||||
{
|
||||
this.onComplete = onComplete;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public override void LockRequestComplete(NitroxId id, bool lockAquired)
|
||||
{
|
||||
if (onComplete != null)
|
||||
{
|
||||
onComplete(id, lockAquired, (T)context);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
18
NitroxClient/GameLogic/Simulation/LockRequestBase.cs
Normal file
18
NitroxClient/GameLogic/Simulation/LockRequestBase.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using NitroxModel.DataStructures;
|
||||
|
||||
namespace NitroxClient.GameLogic.Simulation
|
||||
{
|
||||
public abstract class LockRequestBase
|
||||
{
|
||||
public NitroxId Id { get; }
|
||||
public SimulationLockType LockType { get; }
|
||||
|
||||
public abstract void LockRequestComplete(NitroxId id, bool lockAquired);
|
||||
|
||||
public LockRequestBase(NitroxId Id, SimulationLockType LockType) : base()
|
||||
{
|
||||
this.Id = Id;
|
||||
this.LockType = LockType;
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user