StationSystem/jobs/partial spawning refactor (#7580)
* Partial work on StationSystem refactor. * WIP station jobs API. * forgor to fire off grid events. * Partial implementation of StationSpawningSystem * whoops infinite loop. * Spawners should work now. * it compiles. * tfw * Vestigial code cleanup. * fix station deletion. * attempt to make tests go brr * add latejoin spawnpoints to test maps. * make sure the station still exists while destructing spawners. * forgot an exists check. * destruction order check. * hopefully fix final test. * fail-safe radstorm. * Deep-clean job code further. This is bugged!!!!! * Fix job bug. (init order moment) * whooo cleanup * New job selection algorithm that tries to distribute fairly across stations. * small nitpicks * Give the heads their weights to replace the head field. * make overflow assign take a station list. * moment * Fixes and test #1 of many. * please fix nullspace * AssignJobs should no longer even consider showing up on a trace. * add comment. * Introduce station configs, praying i didn't miss something. * in one small change stations are now fully serializable. * Further doc comments. * whoops. * Solve bug where assignjobs didn't account for roundstart. * Fix spawning, improve the API. Caught an oversight in stationsystem that should've broke everything but didn't, whoops. * Goodbye JobController. * minor fix.. * fix test fail, remove debug logs. * quick serialization fixes. * fixes.. * sus * partialing * Update Content.Server/Station/Systems/StationJobsSystem.Roundstart.cs Co-authored-by: Kara <lunarautomaton6@gmail.com> * Use dirtying to avoid rebuilding the list 2,100 times. * add a bajillion more lines of docs (mostly in AssignJobs so i don't ever forget how it works) * Update Content.IntegrationTests/Tests/Station/StationJobsTest.cs Co-authored-by: Kara <lunarautomaton6@gmail.com> * Add the Mysteriously Missing Captain Check. * Put maprender back the way it belongs. * I love addressing reviews. * Update Content.Server/Station/Systems/StationJobsSystem.cs Co-authored-by: Kara <lunarautomaton6@gmail.com> * doc cleanup. * Fix bureaucratic error, add job slot tests. * zero cost abstractions when * cri * saner error. * Fix spawning failing certain tests due to gameticker not handling falliability correctly. Can't fix this until I refactor the rest of spawning code. * submodule gaming * Packedenger. * Documentation consistency. Co-authored-by: Kara <lunarautomaton6@gmail.com>
This commit is contained in:
@@ -1,25 +1,17 @@
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Content.Server.Access.Systems;
|
||||
using Content.Server.Ghost;
|
||||
using Content.Server.Ghost.Components;
|
||||
using Content.Server.Hands.Components;
|
||||
using Content.Server.Players;
|
||||
using Content.Server.Roles;
|
||||
using Content.Server.Spawners.Components;
|
||||
using Content.Server.Speech.Components;
|
||||
using Content.Server.Station;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Ghost;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.PDA;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Species;
|
||||
using Content.Shared.Station;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
@@ -32,85 +24,35 @@ namespace Content.Server.GameTicking
|
||||
{
|
||||
private const string ObserverPrototypeName = "MobObserver";
|
||||
|
||||
[Dependency] private readonly IdCardSystem _cardSystem = default!;
|
||||
[Dependency] private readonly InventorySystem _inventorySystem = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Can't yet be removed because every test ever seems to depend on it. I'll make removing this a different PR.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[ViewVariables(VVAccess.ReadWrite), Obsolete("Due for removal when observer spawning is refactored.")]
|
||||
private EntityCoordinates _spawnPoint;
|
||||
|
||||
// Mainly to avoid allocations.
|
||||
private readonly List<EntityCoordinates> _possiblePositions = new();
|
||||
|
||||
private void SpawnPlayers(List<IPlayerSession> readyPlayers, IPlayerSession[] origReadyPlayers,
|
||||
private void SpawnPlayers(List<IPlayerSession> readyPlayers, IEnumerable<NetUserId> origReadyPlayers,
|
||||
Dictionary<NetUserId, HumanoidCharacterProfile> profiles, bool force)
|
||||
{
|
||||
// Allow game rules to spawn players by themselves if needed. (For example, nuke ops or wizard)
|
||||
RaiseLocalEvent(new RulePlayerSpawningEvent(readyPlayers, profiles, force));
|
||||
|
||||
var assignedJobs = AssignJobs(readyPlayers, profiles);
|
||||
var assignedJobs = _stationJobs.AssignJobs(profiles, _stationSystem.Stations.ToList());
|
||||
|
||||
AssignOverflowJobs(assignedJobs, origReadyPlayers, profiles);
|
||||
_stationJobs.AssignOverflowJobs(ref assignedJobs, origReadyPlayers, profiles, _stationSystem.Stations.ToList());
|
||||
|
||||
// Spawn everybody in!
|
||||
foreach (var (player, (job, station)) in assignedJobs)
|
||||
{
|
||||
SpawnPlayer(player, profiles[player.UserId], station, job, false);
|
||||
SpawnPlayer(_playerManager.GetSessionByUserId(player), profiles[player], station, job, false);
|
||||
}
|
||||
|
||||
RefreshLateJoinAllowed();
|
||||
|
||||
// Allow rules to add roles to players who have been spawned in. (For example, on-station traitors)
|
||||
RaiseLocalEvent(new RulePlayerJobsAssignedEvent(assignedJobs.Keys.ToArray(), profiles, force));
|
||||
RaiseLocalEvent(new RulePlayerJobsAssignedEvent(assignedJobs.Keys.Select(x => _playerManager.GetSessionByUserId(x)).ToArray(), profiles, force));
|
||||
}
|
||||
|
||||
private void AssignOverflowJobs(IDictionary<IPlayerSession, (string, StationId)> assignedJobs,
|
||||
IPlayerSession[] origReadyPlayers, IReadOnlyDictionary<NetUserId, HumanoidCharacterProfile> profiles)
|
||||
{
|
||||
// For players without jobs, give them the overflow job if they have that set...
|
||||
foreach (var player in origReadyPlayers)
|
||||
{
|
||||
if (assignedJobs.ContainsKey(player))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var profile = profiles[player.UserId];
|
||||
if (profile.PreferenceUnavailable != PreferenceUnavailableMode.SpawnAsOverflow)
|
||||
continue;
|
||||
|
||||
// Pick a random station
|
||||
var stations = _stationSystem.StationInfo.Keys.ToList();
|
||||
|
||||
if (stations.Count == 0)
|
||||
{
|
||||
assignedJobs.Add(player, (FallbackOverflowJob, StationId.Invalid));
|
||||
continue;
|
||||
}
|
||||
|
||||
_robustRandom.Shuffle(stations);
|
||||
|
||||
foreach (var station in stations)
|
||||
{
|
||||
// Pick a random overflow job from that station
|
||||
var overflows = _stationSystem.StationInfo[station].MapPrototype.OverflowJobs.Clone();
|
||||
_robustRandom.Shuffle(overflows);
|
||||
|
||||
// Stations with no overflow slots should simply get skipped over.
|
||||
if (overflows.Count == 0)
|
||||
continue;
|
||||
|
||||
// If the overflow exists, put them in as it.
|
||||
assignedJobs.Add(player, (overflows[0], stations[0]));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SpawnPlayer(IPlayerSession player, StationId station, string? jobId = null, bool lateJoin = true)
|
||||
private void SpawnPlayer(IPlayerSession player, EntityUid station, string? jobId = null, bool lateJoin = true)
|
||||
{
|
||||
var character = GetPlayerProfile(player);
|
||||
|
||||
@@ -118,21 +60,20 @@ namespace Content.Server.GameTicking
|
||||
if (jobBans == null || (jobId != null && jobBans.Contains(jobId)))
|
||||
return;
|
||||
SpawnPlayer(player, character, station, jobId, lateJoin);
|
||||
UpdateJobsAvailable();
|
||||
}
|
||||
|
||||
private void SpawnPlayer(IPlayerSession player, HumanoidCharacterProfile character, StationId station, string? jobId = null, bool lateJoin = true)
|
||||
private void SpawnPlayer(IPlayerSession player, HumanoidCharacterProfile character, EntityUid station, string? jobId = null, bool lateJoin = true)
|
||||
{
|
||||
// Can't spawn players with a dummy ticker!
|
||||
if (DummyTicker)
|
||||
return;
|
||||
|
||||
if (station == StationId.Invalid)
|
||||
if (station == EntityUid.Invalid)
|
||||
{
|
||||
var stations = _stationSystem.StationInfo.Keys.ToList();
|
||||
var stations = _stationSystem.Stations.ToList();
|
||||
_robustRandom.Shuffle(stations);
|
||||
if (stations.Count == 0)
|
||||
station = StationId.Invalid;
|
||||
station = EntityUid.Invalid;
|
||||
else
|
||||
station = stations[0];
|
||||
}
|
||||
@@ -155,7 +96,8 @@ namespace Content.Server.GameTicking
|
||||
}
|
||||
|
||||
// Pick best job best on prefs.
|
||||
jobId ??= PickBestAvailableJob(player, character, station);
|
||||
jobId ??= _stationJobs.PickBestAvailableJobWithPriority(station, character.JobPriorities, true,
|
||||
_roleBanManager.GetJobBans(player.UserId));
|
||||
// If no job available, stay in lobby, or if no lobby spawn as observer
|
||||
if (jobId is null)
|
||||
{
|
||||
@@ -194,7 +136,10 @@ namespace Content.Server.GameTicking
|
||||
playDefaultSound: false);
|
||||
}
|
||||
|
||||
var mob = SpawnPlayerMob(job, character, station, lateJoin);
|
||||
var mobMaybe = _stationSpawning.SpawnPlayerCharacterOnStation(station, job, character);
|
||||
DebugTools.AssertNotNull(mobMaybe);
|
||||
var mob = mobMaybe!.Value;
|
||||
|
||||
newMind.TransferTo(mob);
|
||||
|
||||
if (player.UserId == new Guid("{e887eb93-f503-4b65-95b6-2f282c014192}"))
|
||||
@@ -202,21 +147,12 @@ namespace Content.Server.GameTicking
|
||||
EntityManager.AddComponent<OwOAccentComponent>(mob);
|
||||
}
|
||||
|
||||
AddManifestEntry(character.Name, jobId);
|
||||
AddSpawnedPosition(jobId);
|
||||
EquipIdCard(mob, character.Name, jobPrototype);
|
||||
|
||||
foreach (var jobSpecial in jobPrototype.Special)
|
||||
{
|
||||
jobSpecial.AfterEquip(mob);
|
||||
}
|
||||
|
||||
_stationSystem.TryAssignJobToStation(station, jobPrototype);
|
||||
_stationJobs.TryAssignJob(station, jobPrototype);
|
||||
|
||||
if (lateJoin)
|
||||
_adminLogSystem.Add(LogType.LateJoin, LogImpact.Medium, $"Player {player.Name} late joined as {character.Name:characterName} on station {_stationSystem.StationInfo[station].Name:stationName} with {ToPrettyString(mob):entity} as a {job.Name:jobName}.");
|
||||
_adminLogSystem.Add(LogType.LateJoin, LogImpact.Medium, $"Player {player.Name} late joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {job.Name:jobName}.");
|
||||
else
|
||||
_adminLogSystem.Add(LogType.RoundStartJoin, LogImpact.Medium, $"Player {player.Name} joined as {character.Name:characterName} on station {_stationSystem.StationInfo[station].Name:stationName} with {ToPrettyString(mob):entity} as a {job.Name:jobName}.");
|
||||
_adminLogSystem.Add(LogType.RoundStartJoin, LogImpact.Medium, $"Player {player.Name} joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {job.Name:jobName}.");
|
||||
|
||||
// We raise this event directed to the mob, but also broadcast it so game rules can do something now.
|
||||
var aev = new PlayerSpawnCompleteEvent(mob, player, jobId, lateJoin, station, character);
|
||||
@@ -231,10 +167,10 @@ namespace Content.Server.GameTicking
|
||||
if (LobbyEnabled)
|
||||
PlayerJoinLobby(player);
|
||||
else
|
||||
SpawnPlayer(player, StationId.Invalid);
|
||||
SpawnPlayer(player, EntityUid.Invalid);
|
||||
}
|
||||
|
||||
public void MakeJoinGame(IPlayerSession player, StationId station, string? jobId = null)
|
||||
public void MakeJoinGame(IPlayerSession player, EntityUid station, string? jobId = null)
|
||||
{
|
||||
if (!_playersInLobby.ContainsKey(player)) return;
|
||||
|
||||
@@ -276,28 +212,6 @@ namespace Content.Server.GameTicking
|
||||
}
|
||||
|
||||
#region Mob Spawning Helpers
|
||||
private EntityUid SpawnPlayerMob(Job job, HumanoidCharacterProfile? profile, StationId station, bool lateJoin = true)
|
||||
{
|
||||
var coordinates = lateJoin ? GetLateJoinSpawnPoint(station) : GetJobSpawnPoint(job.Prototype.ID, station);
|
||||
var entity = EntityManager.SpawnEntity(
|
||||
_prototypeManager.Index<SpeciesPrototype>(profile?.Species ?? SpeciesManager.DefaultSpecies).Prototype,
|
||||
coordinates);
|
||||
|
||||
if (job.StartingGear != null)
|
||||
{
|
||||
var startingGear = _prototypeManager.Index<StartingGearPrototype>(job.StartingGear);
|
||||
EquipStartingGear(entity, startingGear, profile);
|
||||
}
|
||||
|
||||
if (profile != null)
|
||||
{
|
||||
_humanoidAppearanceSystem.UpdateFromProfile(entity, profile);
|
||||
EntityManager.GetComponent<MetaDataComponent>(entity).EntityName = profile.Name;
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
private EntityUid SpawnObserverMob()
|
||||
{
|
||||
var coordinates = GetObserverSpawnPoint();
|
||||
@@ -305,108 +219,7 @@ namespace Content.Server.GameTicking
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Equip Helpers
|
||||
public void EquipStartingGear(EntityUid entity, StartingGearPrototype startingGear, HumanoidCharacterProfile? profile)
|
||||
{
|
||||
if (_inventorySystem.TryGetSlots(entity, out var slotDefinitions))
|
||||
{
|
||||
foreach (var slot in slotDefinitions)
|
||||
{
|
||||
var equipmentStr = startingGear.GetGear(slot.Name, profile);
|
||||
if (!string.IsNullOrEmpty(equipmentStr))
|
||||
{
|
||||
var equipmentEntity = EntityManager.SpawnEntity(equipmentStr, EntityManager.GetComponent<TransformComponent>(entity).Coordinates);
|
||||
_inventorySystem.TryEquip(entity, equipmentEntity, slot.Name, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!TryComp(entity, out HandsComponent? handsComponent))
|
||||
return;
|
||||
|
||||
var inhand = startingGear.Inhand;
|
||||
var coords = EntityManager.GetComponent<TransformComponent>(entity).Coordinates;
|
||||
foreach (var (hand, prototype) in inhand)
|
||||
{
|
||||
var inhandEntity = EntityManager.SpawnEntity(prototype, coords);
|
||||
_handsSystem.TryPickup(entity, inhandEntity, hand, checkActionBlocker: false, handsComp: handsComponent);
|
||||
}
|
||||
}
|
||||
|
||||
public void EquipIdCard(EntityUid entity, string characterName, JobPrototype jobPrototype)
|
||||
{
|
||||
if (!_inventorySystem.TryGetSlotEntity(entity, "id", out var idUid))
|
||||
return;
|
||||
|
||||
if (!EntityManager.TryGetComponent(idUid, out PDAComponent? pdaComponent) || pdaComponent.ContainedID == null)
|
||||
return;
|
||||
|
||||
var card = pdaComponent.ContainedID;
|
||||
_cardSystem.TryChangeFullName(card.Owner, characterName, card);
|
||||
_cardSystem.TryChangeJobTitle(card.Owner, jobPrototype.Name, card);
|
||||
|
||||
var access = EntityManager.GetComponent<AccessComponent>(card.Owner);
|
||||
var accessTags = access.Tags;
|
||||
accessTags.UnionWith(jobPrototype.Access);
|
||||
_pdaSystem.SetOwner(pdaComponent, characterName);
|
||||
}
|
||||
#endregion
|
||||
|
||||
private void AddManifestEntry(string characterName, string jobId)
|
||||
{
|
||||
_manifest.Add(new ManifestEntry(characterName, jobId));
|
||||
}
|
||||
|
||||
#region Spawn Points
|
||||
public EntityCoordinates GetJobSpawnPoint(string jobId, StationId station)
|
||||
{
|
||||
var location = _spawnPoint;
|
||||
|
||||
_possiblePositions.Clear();
|
||||
|
||||
foreach (var (point, transform) in EntityManager.EntityQuery<SpawnPointComponent, TransformComponent>(true))
|
||||
{
|
||||
var matchingStation =
|
||||
EntityManager.TryGetComponent<StationComponent>(transform.ParentUid, out var stationComponent) &&
|
||||
stationComponent.Station == station;
|
||||
DebugTools.Assert(EntityManager.TryGetComponent<IMapGridComponent>(transform.ParentUid, out _));
|
||||
|
||||
if (point.SpawnType == SpawnPointType.Job && point.Job?.ID == jobId && matchingStation)
|
||||
_possiblePositions.Add(transform.Coordinates);
|
||||
}
|
||||
|
||||
if (_possiblePositions.Count != 0)
|
||||
location = _robustRandom.Pick(_possiblePositions);
|
||||
else
|
||||
location = GetLateJoinSpawnPoint(station); // We need a sane fallback here, so latejoin it is.
|
||||
|
||||
return location;
|
||||
}
|
||||
|
||||
public EntityCoordinates GetLateJoinSpawnPoint(StationId station)
|
||||
{
|
||||
var location = _spawnPoint;
|
||||
|
||||
_possiblePositions.Clear();
|
||||
|
||||
foreach (var (point, transform) in EntityManager.EntityQuery<SpawnPointComponent, TransformComponent>(true))
|
||||
{
|
||||
var matchingStation =
|
||||
EntityManager.TryGetComponent<StationComponent>(transform.ParentUid, out var stationComponent) &&
|
||||
stationComponent.Station == station;
|
||||
DebugTools.Assert(EntityManager.TryGetComponent<IMapGridComponent>(transform.ParentUid, out _));
|
||||
|
||||
if (point.SpawnType == SpawnPointType.LateJoin && matchingStation)
|
||||
_possiblePositions.Add(transform.Coordinates);
|
||||
}
|
||||
|
||||
if (_possiblePositions.Count != 0)
|
||||
location = _robustRandom.Pick(_possiblePositions);
|
||||
|
||||
return location;
|
||||
}
|
||||
|
||||
|
||||
public EntityCoordinates GetObserverSpawnPoint()
|
||||
{
|
||||
var location = _spawnPoint;
|
||||
@@ -432,15 +245,16 @@ namespace Content.Server.GameTicking
|
||||
/// You can use this event to spawn a player off-station on late-join but also at round start.
|
||||
/// When this event is handled, the GameTicker will not perform its own player-spawning logic.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public sealed class PlayerBeforeSpawnEvent : HandledEntityEventArgs
|
||||
{
|
||||
public IPlayerSession Player { get; }
|
||||
public HumanoidCharacterProfile Profile { get; }
|
||||
public string? JobId { get; }
|
||||
public bool LateJoin { get; }
|
||||
public StationId Station { get; }
|
||||
public EntityUid Station { get; }
|
||||
|
||||
public PlayerBeforeSpawnEvent(IPlayerSession player, HumanoidCharacterProfile profile, string? jobId, bool lateJoin, StationId station)
|
||||
public PlayerBeforeSpawnEvent(IPlayerSession player, HumanoidCharacterProfile profile, string? jobId, bool lateJoin, EntityUid station)
|
||||
{
|
||||
Player = player;
|
||||
Profile = profile;
|
||||
@@ -455,16 +269,17 @@ namespace Content.Server.GameTicking
|
||||
/// You can use this to handle people late-joining, or to handle people being spawned at round start.
|
||||
/// Can be used to give random players a role, modify their equipment, etc.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public sealed class PlayerSpawnCompleteEvent : EntityEventArgs
|
||||
{
|
||||
public EntityUid Mob { get; }
|
||||
public IPlayerSession Player { get; }
|
||||
public string? JobId { get; }
|
||||
public bool LateJoin { get; }
|
||||
public StationId Station { get; }
|
||||
public EntityUid Station { get; }
|
||||
public HumanoidCharacterProfile Profile { get; }
|
||||
|
||||
public PlayerSpawnCompleteEvent(EntityUid mob, IPlayerSession player, string? jobId, bool lateJoin, StationId station, HumanoidCharacterProfile profile)
|
||||
public PlayerSpawnCompleteEvent(EntityUid mob, IPlayerSession player, string? jobId, bool lateJoin, EntityUid station, HumanoidCharacterProfile profile)
|
||||
{
|
||||
Mob = mob;
|
||||
Player = player;
|
||||
|
||||
Reference in New Issue
Block a user