using Content.Server.Access.Systems;
using Content.Server.Humanoid;
using Content.Server.IdentityManagement;
using Content.Server.Mind.Commands;
using Content.Server.PDA;
using Content.Server.Station.Components;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.CCVar;
using Content.Shared.Clothing;
using Content.Shared.DetailExaminable;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.PDA;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Random;
using Content.Shared.Random.Helpers;
using Content.Shared.Roles;
using Content.Shared.Station;
using JetBrains.Annotations;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
using Robust.Shared.Player;
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 SharedAccessSystem _accessSystem = default!;
[Dependency] private readonly ActorSystem _actors = default!;
[Dependency] private readonly IdCardSystem _cardSystem = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!;
[Dependency] private readonly IdentitySystem _identity = default!;
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
[Dependency] private readonly PdaSystem _pdaSystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
private bool _randomizeCharacters;
///
public override void Initialize()
{
base.Initialize();
Subs.CVar(_configurationManager, CCVars.ICRandomCharacters, e => _randomizeCharacters = e, true);
}
///
/// 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, ProtoId? 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);
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,
ProtoId? job,
HumanoidCharacterProfile? profile,
EntityUid? station,
EntityUid? entity = null)
{
_prototypeManager.TryIndex(job ?? string.Empty, out var prototype);
RoleLoadout? loadout = null;
// Need to get the loadout up-front to handle names if we use an entity spawn override.
var jobLoadout = LoadoutSystem.GetJobPrototype(prototype?.ID);
if (_prototypeManager.TryIndex(jobLoadout, out RoleLoadoutPrototype? roleProto))
{
profile?.Loadouts.TryGetValue(jobLoadout, out loadout);
// Set to default if not present
if (loadout == null)
{
loadout = new RoleLoadout(jobLoadout);
loadout.SetDefault(profile, _actors.GetSession(entity), _prototypeManager);
}
}
// 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);
// Make sure custom names get handled, what is gameticker control flow whoopy.
if (loadout != null)
{
EquipRoleName(jobEntity, loadout, roleProto!);
}
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 (loadout != null)
{
EquipRoleLoadout(entity.Value, loadout, roleProto!);
}
// Equip starting gear if specified in the job prototype
if (prototype != null)
{
StartingGearPrototype? startingGearProto = null;
// Check if random gears are available and populated
if (prototype.RandomStartingGears != null && prototype.RandomStartingGears.Count > 0)
{
var startingGearId = _random.Pick(prototype.RandomStartingGears); // Safe now
_prototypeManager.TryIndex(startingGearId, out startingGearProto);
}
// Otherwise, check if the single starting gear is specified
else if (prototype.StartingGear != null)
{
_prototypeManager.TryIndex(prototype.StartingGear.Value, out startingGearProto); // Safe now, using .Value for ProtoId?
}
// If we found a valid gear prototype (either random or specific), equip it
if (startingGearProto != null)
{
EquipStartingGear(entity.Value, startingGearProto, raiseEvent: false);
}
}
var gearEquippedEv = new StartingGearEquippedEvent(entity.Value);
RaiseLocalEvent(entity.Value, ref gearEquippedEv);
if (profile != null)
{
if (prototype != null)
SetPdaAndIdCardData(entity.Value, profile.Name, prototype, station);
_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(ProtoId? job, EntityUid entity)
{
if (!_prototypeManager.TryIndex(job ?? string.Empty, out JobPrototype? prototype))
return;
foreach (var jobSpecial in prototype.Special)
{
jobSpecial.AfterEquip(entity);
}
}
///
/// Sets the ID card and PDA name, job, and access data.
///
/// 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 SetPdaAndIdCardData(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, entity, 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 ProtoId? Job;
///
/// The profile to use, if any.
///
public readonly HumanoidCharacterProfile? HumanoidCharacterProfile;
///
/// The target station, if any.
///
public readonly EntityUid? Station;
public PlayerSpawningEvent(ProtoId? job, HumanoidCharacterProfile? humanoidCharacterProfile, EntityUid? station)
{
Job = job;
HumanoidCharacterProfile = humanoidCharacterProfile;
Station = station;
}
}