Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New NPC Api #241

Merged
merged 15 commits into from
Dec 9, 2024
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