| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- 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;
- /// <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 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;
- /// <inheritdoc/>
- public override void Initialize()
- {
- base.Initialize();
- Subs.CVar(_configurationManager, CCVars.ICRandomCharacters, e => _randomizeCharacters = e, true);
- }
- /// <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, ProtoId<JobPrototype>? 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
- /// <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,
- ProtoId<JobPrototype>? 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<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);
- }
- 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<DetailExaminableComponent>(entity.Value).Content = profile.FlavorText;
- }
- }
- DoJobSpecials(job, entity.Value);
- _identity.QueueIdentityUpdate(entity.Value);
- return entity.Value;
- }
- private void DoJobSpecials(ProtoId<JobPrototype>? job, EntityUid entity)
- {
- if (!_prototypeManager.TryIndex(job ?? 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, entity, 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 ProtoId<JobPrototype>? 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(ProtoId<JobPrototype>? job, HumanoidCharacterProfile? humanoidCharacterProfile, EntityUid? station)
- {
- Job = job;
- HumanoidCharacterProfile = humanoidCharacterProfile;
- Station = station;
- }
- }
|