fish-station/Content.Server/Station/Systems/StationSpawningSystem.cs
Hannah Giovanna Dawson 3b3163c4f4
SS-28662 Add cvars to support forcing people to departures and making those at departures invincible (#28765)
* SS-28662 Add cvar to force spawn everyone at departures

This cvar means everyone must spawn at departures. This
could be handy for an admin event? But mostly it's so the
tutorial departures terminal can be seen by all newbies on
gateway servers.

* Small fix to ArrivalsSystem flow

* Remove incorrect todo

* Add godmode arrivals cvar
2024-06-14 04:15:42 -07:00

319 lines
12 KiB
C#

using System.Linq;
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.Clothing;
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.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;
/// <summary>
/// Manages spawning into the game, tracking available spawn points.
/// Also provides helpers for spawning in the player's mob.
/// </summary>
[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<SpawnPriorityPreference, Action<PlayerSpawningEvent>> _spawnerCallbacks = new();
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
Subs.CVar(_configurationManager, CCVars.ICRandomCharacters, e => _randomizeCharacters = e, true);
_spawnerCallbacks = new Dictionary<SpawnPriorityPreference, Action<PlayerSpawningEvent>>()
{
{ SpawnPriorityPreference.Arrivals, _arrivalsSystem.HandlePlayerSpawning },
{
SpawnPriorityPreference.Cryosleep, ev =>
{
if (_arrivalsSystem.Forced)
_arrivalsSystem.HandlePlayerSpawning(ev);
else
_containerSpawnPointSystem.HandlePlayerSpawning(ev);
}
}
};
}
/// <summary>
/// Attempts to spawn a player character onto the given station.
/// </summary>
/// <param name="station">Station to spawn onto.</param>
/// <param name="job">The job to assign, if any.</param>
/// <param name="profile">The character profile to use, if any.</param>
/// <param name="stationSpawning">Resolve pattern, the station spawning component for the station.</param>
/// <returns>The resulting player character, if any.</returns>
/// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception>
/// <remarks>
/// This only spawns the character, and does none of the mind-related setup you'd need for it to be playable.
/// </remarks>
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
/// <summary>
/// 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.
/// </summary>
/// <param name="coordinates">Coordinates to spawn the character at.</param>
/// <param name="job">Job to assign to the character, if any.</param>
/// <param name="profile">Appearance profile to use for the character.</param>
/// <param name="station">The station this player is being spawned on.</param>
/// <param name="entity">The entity to use, if one already exists.</param>
/// <returns>The spawned entity</returns>
public EntityUid SpawnPlayerMob(
EntityCoordinates coordinates,
JobComponent? job,
HumanoidCharacterProfile? profile,
EntityUid? station,
EntityUid? entity = null)
{
_prototypeManager.TryIndex(job?.Prototype ?? string.Empty, out var 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<WeightedRandomSpeciesPrototype>(weightId);
speciesId = weights.Pick(_random);
}
else if (profile != null)
{
speciesId = profile.Species;
}
else
{
speciesId = SharedHumanoidAppearanceSystem.DefaultSpecies;
}
if (!_prototypeManager.TryIndex<SpeciesPrototype>(speciesId, out var species))
throw new ArgumentException($"Invalid species prototype was used: {speciesId}");
entity ??= Spawn(species.Prototype, coordinates);
if (_randomizeCharacters)
{
profile = HumanoidCharacterProfile.RandomWithSpecies(speciesId);
}
var jobLoadout = LoadoutSystem.GetJobPrototype(prototype?.ID);
if (_prototypeManager.TryIndex(jobLoadout, out RoleLoadoutPrototype? roleProto))
{
RoleLoadout? loadout = null;
profile?.Loadouts.TryGetValue(jobLoadout, out loadout);
// Set to default if not present
if (loadout == null)
{
loadout = new RoleLoadout(jobLoadout);
loadout.SetDefault(_prototypeManager);
}
EquipRoleLoadout(entity.Value, loadout, roleProto);
}
if (prototype?.StartingGear != null)
{
var startingGear = _prototypeManager.Index<StartingGearPrototype>(prototype.StartingGear);
EquipStartingGear(entity.Value, startingGear, 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<DetailExaminableComponent>(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);
}
}
/// <summary>
/// Sets the ID card and PDA name, job, and access data.
/// </summary>
/// <param name="entity">Entity to load out.</param>
/// <param name="characterName">Character name to use for the ID.</param>
/// <param name="jobPrototype">Job prototype to use for the PDA and ID.</param>
/// <param name="station">The station this player is being spawned on.</param>
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<PdaComponent>(idUid, out var pdaComponent) && pdaComponent.ContainedId != null)
cardId = pdaComponent.ContainedId.Value;
if (!TryComp<IdCardComponent>(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<StationJobsComponent>(station.Value);
extendedAccess = data.ExtendedAccess;
}
_accessSystem.SetAccessToJob(cardId, jobPrototype, extendedAccess);
if (pdaComponent != null)
_pdaSystem.SetOwner(idUid.Value, pdaComponent, characterName);
}
#endregion Player spawning helpers
}
/// <summary>
/// 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.
/// </summary>
[PublicAPI]
public sealed class PlayerSpawningEvent : EntityEventArgs
{
/// <summary>
/// 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.
/// </summary>
public EntityUid? SpawnResult;
/// <summary>
/// The job to use, if any.
/// </summary>
public readonly JobComponent? Job;
/// <summary>
/// The profile to use, if any.
/// </summary>
public readonly HumanoidCharacterProfile? HumanoidCharacterProfile;
/// <summary>
/// The target station, if any.
/// </summary>
public readonly EntityUid? Station;
public PlayerSpawningEvent(JobComponent? job, HumanoidCharacterProfile? humanoidCharacterProfile, EntityUid? station)
{
Job = job;
HumanoidCharacterProfile = humanoidCharacterProfile;
Station = station;
}
}