using Content.Server.Access.Systems;
using Content.Server.DetailExaminable;
using Content.Server.Humanoid;
using Content.Server.IdentityManagement;
using Content.Server.Mind.Commands;
using Content.Server.PDA;
using Content.Server.Shuttles.Systems;
using Content.Server.Spawners.EntitySystems;
using Content.Server.Station.Components;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.CCVar;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.PDA;
using Content.Shared.Preferences;
using Content.Shared.Random;
using Content.Shared.Random.Helpers;
using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
using Content.Shared.Station;
using Content.Shared.StatusIcon;
using JetBrains.Annotations;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Station.Systems;
///
/// Manages spawning into the game, tracking available spawn points.
/// Also provides helpers for spawning in the player's mob.
///
[PublicAPI]
public sealed class StationSpawningSystem : SharedStationSpawningSystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!;
[Dependency] private readonly IdCardSystem _cardSystem = default!;
[Dependency] private readonly PdaSystem _pdaSystem = default!;
[Dependency] private readonly SharedAccessSystem _accessSystem = default!;
[Dependency] private readonly IdentitySystem _identity = default!;
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
[Dependency] private readonly ArrivalsSystem _arrivalsSystem = default!;
[Dependency] private readonly ContainerSpawnPointSystem _containerSpawnPointSystem = default!;
private bool _randomizeCharacters;
private Dictionary> _spawnerCallbacks = new();
///
public override void Initialize()
{
_configurationManager.OnValueChanged(CCVars.ICRandomCharacters, e => _randomizeCharacters = e, true);
_spawnerCallbacks = new Dictionary>()
{
{ SpawnPriorityPreference.Arrivals, _arrivalsSystem.HandlePlayerSpawning },
{ SpawnPriorityPreference.Cryosleep, _containerSpawnPointSystem.HandlePlayerSpawning }
};
}
///
/// Attempts to spawn a player character onto the given station.
///
/// Station to spawn onto.
/// The job to assign, if any.
/// The character profile to use, if any.
/// Resolve pattern, the station spawning component for the station.
/// The resulting player character, if any.
/// Thrown when the given station is not a station.
///
/// This only spawns the character, and does none of the mind-related setup you'd need for it to be playable.
///
public EntityUid? SpawnPlayerCharacterOnStation(EntityUid? station, JobComponent? job, HumanoidCharacterProfile? profile, StationSpawningComponent? stationSpawning = null)
{
if (station != null && !Resolve(station.Value, ref stationSpawning))
throw new ArgumentException("Tried to use a non-station entity as a station!", nameof(station));
var ev = new PlayerSpawningEvent(job, profile, station);
if (station != null && profile != null)
{
/// Try to call the character's preferred spawner first.
if (_spawnerCallbacks.TryGetValue(profile.SpawnPriority, out var preferredSpawner))
{
preferredSpawner(ev);
foreach (var (key, remainingSpawner) in _spawnerCallbacks)
{
if (key == profile.SpawnPriority)
continue;
remainingSpawner(ev);
}
}
else
{
/// Call all of them in the typical order.
foreach (var typicalSpawner in _spawnerCallbacks.Values)
typicalSpawner(ev);
}
}
RaiseLocalEvent(ev);
DebugTools.Assert(ev.SpawnResult is { Valid: true } or null);
return ev.SpawnResult;
}
//TODO: Figure out if everything in the player spawning region belongs somewhere else.
#region Player spawning helpers
///
/// Spawns in a player's mob according to their job and character information at the given coordinates.
/// Used by systems that need to handle spawning players.
///
/// Coordinates to spawn the character at.
/// Job to assign to the character, if any.
/// Appearance profile to use for the character.
/// The station this player is being spawned on.
/// The entity to use, if one already exists.
/// The spawned entity
public EntityUid SpawnPlayerMob(
EntityCoordinates coordinates,
JobComponent? job,
HumanoidCharacterProfile? profile,
EntityUid? station,
EntityUid? entity = null)
{
_prototypeManager.TryIndex(job?.Prototype ?? string.Empty, out JobPrototype? prototype);
// If we're not spawning a humanoid, we're gonna exit early without doing all the humanoid stuff.
if (prototype?.JobEntity != null)
{
DebugTools.Assert(entity is null);
var jobEntity = EntityManager.SpawnEntity(prototype.JobEntity, coordinates);
MakeSentientCommand.MakeSentient(jobEntity, EntityManager);
DoJobSpecials(job, jobEntity);
_identity.QueueIdentityUpdate(jobEntity);
return jobEntity;
}
string speciesId;
if (_randomizeCharacters)
{
var weightId = _configurationManager.GetCVar(CCVars.ICRandomSpeciesWeights);
var weights = _prototypeManager.Index(weightId);
speciesId = weights.Pick(_random);
}
else if (profile != null)
{
speciesId = profile.Species;
}
else
{
speciesId = SharedHumanoidAppearanceSystem.DefaultSpecies;
}
if (!_prototypeManager.TryIndex(speciesId, out var species))
throw new ArgumentException($"Invalid species prototype was used: {speciesId}");
entity ??= Spawn(species.Prototype, coordinates);
if (_randomizeCharacters)
{
profile = HumanoidCharacterProfile.RandomWithSpecies(speciesId);
}
if (prototype?.StartingGear != null)
{
var startingGear = _prototypeManager.Index(prototype.StartingGear);
EquipStartingGear(entity.Value, startingGear, profile);
if (profile != null)
EquipIdCard(entity.Value, profile.Name, prototype, station);
}
if (profile != null)
{
_humanoidSystem.LoadProfile(entity.Value, profile);
_metaSystem.SetEntityName(entity.Value, profile.Name);
if (profile.FlavorText != "" && _configurationManager.GetCVar(CCVars.FlavorText))
{
AddComp(entity.Value).Content = profile.FlavorText;
}
}
DoJobSpecials(job, entity.Value);
_identity.QueueIdentityUpdate(entity.Value);
return entity.Value;
}
private void DoJobSpecials(JobComponent? job, EntityUid entity)
{
if (!_prototypeManager.TryIndex(job?.Prototype ?? string.Empty, out JobPrototype? prototype))
return;
foreach (var jobSpecial in prototype.Special)
{
jobSpecial.AfterEquip(entity);
}
}
///
/// Equips an ID card and PDA onto the given entity.
///
/// Entity to load out.
/// Character name to use for the ID.
/// Job prototype to use for the PDA and ID.
/// The station this player is being spawned on.
public void EquipIdCard(EntityUid entity, string characterName, JobPrototype jobPrototype, EntityUid? station)
{
if (!InventorySystem.TryGetSlotEntity(entity, "id", out var idUid))
return;
var cardId = idUid.Value;
if (TryComp(idUid, out var pdaComponent) && pdaComponent.ContainedId != null)
cardId = pdaComponent.ContainedId.Value;
if (!TryComp(cardId, out var card))
return;
_cardSystem.TryChangeFullName(cardId, characterName, card);
_cardSystem.TryChangeJobTitle(cardId, jobPrototype.LocalizedName, card);
if (_prototypeManager.TryIndex(jobPrototype.Icon, out var jobIcon))
{
_cardSystem.TryChangeJobIcon(cardId, jobIcon, card);
}
var extendedAccess = false;
if (station != null)
{
var data = Comp(station.Value);
extendedAccess = data.ExtendedAccess;
}
_accessSystem.SetAccessToJob(cardId, jobPrototype, extendedAccess);
if (pdaComponent != null)
_pdaSystem.SetOwner(idUid.Value, pdaComponent, characterName);
}
#endregion Player spawning helpers
}
///
/// Ordered broadcast event fired on any spawner eligible to attempt to spawn a player.
/// This event's success is measured by if SpawnResult is not null.
/// You should not make this event's success rely on random chance.
/// This event is designed to use ordered handling. You probably want SpawnPointSystem to be the last handler.
///
[PublicAPI]
public sealed class PlayerSpawningEvent : EntityEventArgs
{
///
/// The entity spawned, if any. You should set this if you succeed at spawning the character, and leave it alone if it's not null.
///
public EntityUid? SpawnResult;
///
/// The job to use, if any.
///
public readonly JobComponent? Job;
///
/// The profile to use, if any.
///
public readonly HumanoidCharacterProfile? HumanoidCharacterProfile;
///
/// The target station, if any.
///
public readonly EntityUid? Station;
public PlayerSpawningEvent(JobComponent? job, HumanoidCharacterProfile? humanoidCharacterProfile, EntityUid? station)
{
Job = job;
HumanoidCharacterProfile = humanoidCharacterProfile;
Station = station;
}
}