Skip to content
286 changes: 145 additions & 141 deletions EXILED/Exiled.API/Features/Npc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ namespace Exiled.API.Features

using CentralAuth;
using CommandSystem;
using CommandSystem.Commands.RemoteAdmin.Dummies;
using Exiled.API.Enums;
using Exiled.API.Features.Components;
using Exiled.API.Features.Roles;
Expand Down Expand Up @@ -47,7 +48,7 @@ public Npc(GameObject gameObject)
/// <summary>
/// Gets a list of Npcs.
/// </summary>
public static new List<Npc> List => Player.List.OfType<Npc>().ToList();
public static new IReadOnlyCollection<Npc> List => Dictionary.Values.OfType<Npc>().ToList();

/// <summary>
/// Gets or sets the player's position.
Expand All @@ -63,6 +64,113 @@ public override Vector3 Position
}
}

/// <summary>
/// Gets or sets the player being followed.
/// </summary>
/// <remarks>The npc must have <see cref="PlayerFollower"/>.</remarks>
public Player? FollowedPlayer
{
get => !GameObject.TryGetComponent(out PlayerFollower follower) ? null : Player.Get(follower._hubToFollow);

set
{
if (!GameObject.TryGetComponent(out PlayerFollower follower))
{
GameObject.AddComponent<PlayerFollower>()._hubToFollow = value?.ReferenceHub;
return;
}

follower._hubToFollow = value?.ReferenceHub;
}
}

/// <summary>
/// Gets or sets the Max Distance of the npc.
/// </summary>
/// <remarks>The npc must have <see cref="PlayerFollower"/>.</remarks>
public float? MaxDistance
{
get
{
if (!GameObject.TryGetComponent(out PlayerFollower follower))
return null;

return follower._maxDistance;
}

set
{
if(!value.HasValue)
return;

if (!GameObject.TryGetComponent(out PlayerFollower follower))
{
GameObject.AddComponent<PlayerFollower>()._maxDistance = value.Value;
return;
}

follower._maxDistance = value.Value;
}
}

/// <summary>
/// Gets or sets the Min Distance of the npc.
/// </summary>
/// <remarks>The npc must have <see cref="PlayerFollower"/>.</remarks>
public float? MinDistance
{
get
{
if (!GameObject.TryGetComponent(out PlayerFollower follower))
return null;

return follower._minDistance;
}

set
{
if(!value.HasValue)
return;

if (!GameObject.TryGetComponent(out PlayerFollower follower))
{
GameObject.AddComponent<PlayerFollower>()._minDistance = value.Value;
return;
}

follower._minDistance = value.Value;
}
}

/// <summary>
/// Gets or sets the Speed of the npc.
/// </summary>
/// <remarks>The npc must have <see cref="PlayerFollower"/>.</remarks>
public float? Speed
{
get
{
if (!GameObject.TryGetComponent(out PlayerFollower follower))
return null;

return follower._speed;
}

set
{
if(!value.HasValue)
return;

if (!GameObject.TryGetComponent(out PlayerFollower follower))
{
GameObject.AddComponent<PlayerFollower>()._speed = value.Value;
return;
}

follower._speed = value.Value;
}
}

/// <summary>
/// Retrieves the NPC associated with the specified ReferenceHub.
/// </summary>
Expand Down Expand Up @@ -133,100 +241,24 @@ public override Vector3 Position
/// <returns>The NPC associated with the NetworkConnection, or <c>null</c> if not found.</returns>
public static new Npc? Get(NetworkConnection conn) => Player.Get(conn) as Npc;

/// <summary>
/// Docs.
/// </summary>
/// <param name="name">Docs1.</param>
/// <param name="role">Docs2.</param>
/// <param name="position">Docs3.</param>
/// <returns>Docs4.</returns>
public static Npc Create(string name, RoleTypeId role, Vector3 position)
{
// TODO: Test this.
Npc npc = new(DummyUtils.SpawnDummy(name))
{
IsNPC = true,
};

npc.Role.Set(role);
npc.Position = position;

return npc;
}

/// <summary>
/// Spawns an NPC based on the given parameters.
/// </summary>
/// <param name="name">The name of the NPC.</param>
/// <param name="role">The RoleTypeId of the NPC.</param>
/// <param name="id">The player ID of the NPC.</param>
/// <param name="userId">The userID of the NPC.</param>
/// <param name="position">The position to spawn the NPC.</param>
/// <returns>The <see cref="Npc"/> spawned.</returns>
[Obsolete("This method is marked as obsolete due to a bug that make player have the same id. Use Npc.Spawn(string) instead", true)]
public static Npc Spawn(string name, RoleTypeId role, int id = 0, string userId = PlayerAuthenticationManager.DedicatedId, Vector3? position = null)
/// <param name="position">The position where the NPC should spawn.</param>
/// <returns>Docs4.</returns>
public static Npc Spawn(string name, RoleTypeId role, Vector3 position)
{
GameObject newObject = UnityEngine.Object.Instantiate(Mirror.NetworkManager.singleton.playerPrefab);

Npc npc = new(newObject)
{
IsNPC = true,
};

if (!RecyclablePlayerId.FreeIds.Contains(id) && RecyclablePlayerId._autoIncrement >= id)
{
Log.Warn($"{Assembly.GetCallingAssembly().GetName().Name} tried to spawn an NPC with a duplicate PlayerID. Using auto-incremented ID instead to avoid an ID clash.");
id = new RecyclablePlayerId(true).Value;
}

try
{
if (userId == PlayerAuthenticationManager.DedicatedId)
{
npc.ReferenceHub.authManager.SyncedUserId = userId;
try
{
npc.ReferenceHub.authManager.InstanceMode = ClientInstanceMode.DedicatedServer;
}
catch (Exception e)
{
Log.Debug($"Ignore: {e.Message}");
}
}
else
{
npc.ReferenceHub.authManager.InstanceMode = ClientInstanceMode.Unverified;
npc.ReferenceHub.authManager._privUserId = userId == string.Empty ? $"Dummy@localhost" : userId;
}
}
catch (Exception e)
{
Log.Debug($"Ignore: {e.Message}");
}

try
{
npc.ReferenceHub.roleManager.InitializeNewRole(RoleTypeId.None, RoleChangeReason.None);
}
catch (Exception e)
{
Log.Debug($"Ignore: {e.Message}");
}

FakeConnection fakeConnection = new(id);
NetworkServer.AddPlayerForConnection(fakeConnection, newObject);

npc.ReferenceHub.nicknameSync.Network_myNickSync = name;
Dictionary.Add(newObject, npc);
Npc npc = new(DummyUtils.SpawnDummy(name));

Timing.CallDelayed(0.5f, () =>
{
npc.Role.Set(role, SpawnReason.RoundStart, position is null ? RoleSpawnFlags.All : RoleSpawnFlags.AssignInventory);

if (position is not null)
npc.Position = position.Value;
npc.Role.Set(role);
npc.Position = position;
});

Dictionary.Add(npc.GameObject, npc);
return npc;
}

Expand All @@ -236,58 +268,11 @@ public static Npc Spawn(string name, RoleTypeId role, int id = 0, string userId
/// <param name="name">The name of the NPC.</param>
/// <param name="role">The RoleTypeId of the NPC, defaulting to None.</param>
/// <param name="ignored">Whether the NPC should be ignored by round ending checks.</param>
/// <param name="userId">The userID of the NPC for authentication. Defaults to the Dedicated ID.</param>
/// <param name="position">The position where the NPC should spawn. If null, the default spawn location is used.</param>
/// <returns>The <see cref="Npc"/> spawned.</returns>
public static Npc Spawn(string name, RoleTypeId role = RoleTypeId.None, bool ignored = false, string userId = PlayerAuthenticationManager.DedicatedId, Vector3? position = null)
public static Npc Spawn(string name, RoleTypeId role = RoleTypeId.None, bool ignored = false, Vector3? position = null)
{
GameObject newObject = UnityEngine.Object.Instantiate(Mirror.NetworkManager.singleton.playerPrefab);

Npc npc = new(newObject)
{
IsNPC = true,
};

FakeConnection fakeConnection = new(npc.Id);

try
{
if (userId == PlayerAuthenticationManager.DedicatedId)
{
npc.ReferenceHub.authManager.SyncedUserId = userId;
try
{
npc.ReferenceHub.authManager.InstanceMode = ClientInstanceMode.DedicatedServer;
}
catch (Exception e)
{
Log.Debug($"Ignore: {e.Message}");
}
}
else
{
npc.ReferenceHub.authManager.InstanceMode = ClientInstanceMode.Unverified;
npc.ReferenceHub.authManager._privUserId = userId == string.Empty ? $"Dummy-{npc.Id}@localhost" : userId;
}
}
catch (Exception e)
{
Log.Debug($"Ignore: {e.Message}");
}

try
{
npc.ReferenceHub.roleManager.InitializeNewRole(RoleTypeId.None, RoleChangeReason.None);
}
catch (Exception e)
{
Log.Debug($"Ignore: {e.Message}");
}

NetworkServer.AddPlayerForConnection(fakeConnection, newObject);

npc.ReferenceHub.nicknameSync.Network_myNickSync = name;
Dictionary.Add(newObject, npc);
Npc npc = new(DummyUtils.SpawnDummy(name));

Timing.CallDelayed(0.5f, () =>
{
Expand All @@ -300,16 +285,38 @@ public static Npc Spawn(string name, RoleTypeId role = RoleTypeId.None, bool ign
if (ignored)
Round.IgnoredPlayers.Add(npc.ReferenceHub);

Dictionary.Add(npc.GameObject, npc);
return npc;
}

/// <summary>
/// Destroys all NPCs currently spawned.
/// </summary>
public static void DestroyAll()
public static void DestroyAll() => DummyUtils.DestroyAllDummies();

/// <summary>
/// Follow a specific player.
/// </summary>
/// <param name="player">the Player to follow.</param>
public void Follow(Player player)
{
PlayerFollower follow = !GameObject.TryGetComponent(out PlayerFollower follower) ? GameObject.AddComponent<PlayerFollower>() : follower;

follow.Init(player.ReferenceHub);
}

/// <summary>
/// Follow a specific player.
/// </summary>
/// <param name="player">the Player to follow.</param>
/// <param name="maxDistance">the max distance the npc will go.</param>
/// <param name="minDistance">the min distance the npc will go.</param>
/// <param name="speed">the speed the npc will go.</param>
public void Follow(Player player, float maxDistance, float minDistance, float speed = 30f)
{
foreach (Npc npc in List)
npc.Destroy();
PlayerFollower follow = !GameObject.TryGetComponent(out PlayerFollower follower) ? GameObject.AddComponent<PlayerFollower>() : follower;

follow.Init(player.ReferenceHub, maxDistance, minDistance, speed);
}

/// <summary>
Expand All @@ -320,11 +327,8 @@ public void Destroy()
try
{
Round.IgnoredPlayers.Remove(ReferenceHub);
NetworkConnectionToClient conn = ReferenceHub.connectionToClient;
ReferenceHub.OnDestroy();
CustomNetworkManager.TypedSingleton.OnServerDisconnect(conn);
Dictionary.Remove(GameObject);
Object.Destroy(GameObject);
Dictionary.Remove(ReferenceHub.gameObject);
NetworkServer.Destroy(ReferenceHub.gameObject);
}
catch (Exception e)
{
Expand Down
6 changes: 3 additions & 3 deletions EXILED/Exiled.API/Features/Player.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ public Player(GameObject gameObject)
/// <summary>
/// Gets a list of all <see cref="Player"/>'s on the server.
/// </summary>
public static IReadOnlyCollection<Player> List => Dictionary.Values;
public static IReadOnlyCollection<Player> List => Dictionary.Values.Where(x => !x.IsNPC).ToList();

/// <summary>
/// Gets a <see cref="Dictionary{TKey, TValue}"/> containing cached <see cref="Player"/> and their user ids.
Expand Down Expand Up @@ -301,9 +301,9 @@ public AuthenticationType AuthenticationType
public bool IsVerified { get; internal set; }

/// <summary>
/// Gets or sets a value indicating whether the player is a NPC.
/// Gets a value indicating whether the player is a NPC.
/// </summary>
public bool IsNPC { get; set; }
public bool IsNPC => ReferenceHub.IsDummy;

/// <summary>
/// Gets a value indicating whether the player has an active CustomName.
Expand Down