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:
Moony
2022-05-10 13:43:30 -05:00
committed by GitHub
parent d234a79d28
commit 36181334b5
65 changed files with 2564 additions and 1368 deletions

View File

@@ -1,8 +1,7 @@
using Content.Server.Station;
using Content.Server.Station.Systems;
using Content.Shared.Administration;
using Content.Shared.GameTicking;
using Content.Shared.Roles;
using Content.Shared.Station;
using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.Prototypes;
@@ -39,6 +38,7 @@ namespace Content.Server.GameTicking.Commands
var ticker = EntitySystem.Get<GameTicker>();
var stationSystem = EntitySystem.Get<StationSystem>();
var stationJobs = EntitySystem.Get<StationJobsSystem>();
if (!ticker.PlayersInLobby.ContainsKey(player) || ticker.PlayersInLobby[player] == LobbyPlayerStatus.Observer)
{
@@ -56,23 +56,23 @@ namespace Content.Server.GameTicking.Commands
{
string id = args[0];
if (!uint.TryParse(args[1], out var sid))
if (!int.TryParse(args[1], out var sid))
{
shell.WriteError(Loc.GetString("shell-argument-must-be-number"));
}
var stationId = new StationId(sid);
var station = new EntityUid(sid);
var jobPrototype = _prototypeManager.Index<JobPrototype>(id);
if(!stationSystem.IsJobAvailableOnStation(stationId, jobPrototype))
if(stationJobs.TryGetJobSlot(station, jobPrototype, out var slots) == false || slots == 0)
{
shell.WriteLine($"{jobPrototype.Name} has no available slots.");
return;
}
ticker.MakeJoinGame(player, stationId, id);
ticker.MakeJoinGame(player, station, id);
return;
}
ticker.MakeJoinGame(player, StationId.Invalid);
ticker.MakeJoinGame(player, EntityUid.Invalid);
}
}
}

View File

@@ -38,7 +38,7 @@ namespace Content.Server.GameTicking
_configurationManager.OnValueChanged(CCVars.GameDummyTicker, value => DummyTicker = value, true);
_configurationManager.OnValueChanged(CCVars.GameLobbyDuration, value => LobbyDuration = TimeSpan.FromSeconds(value), true);
_configurationManager.OnValueChanged(CCVars.GameDisallowLateJoins,
value => { DisallowLateJoin = value; UpdateLateJoinStatus(); UpdateJobsAvailable(); }, true);
value => { DisallowLateJoin = value; UpdateLateJoinStatus(); }, true);
_configurationManager.OnValueChanged(CCVars.StationOffset, value => StationOffset = value, true);
_configurationManager.OnValueChanged(CCVars.StationRotation, value => StationRotation = value, true);
_configurationManager.OnValueChanged(CCVars.MaxStationOffset, value => MaxStationOffset = value, true);

View File

@@ -1,196 +0,0 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared.GameTicking;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Content.Shared.Station;
using Robust.Server.Player;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.GameTicking
{
// This code is responsible for the assigning & picking of jobs.
public sealed partial class GameTicker
{
[ViewVariables]
private readonly List<ManifestEntry> _manifest = new();
[ViewVariables]
private readonly Dictionary<string, int> _spawnedPositions = new();
private Dictionary<IPlayerSession, (string, StationId)> AssignJobs(List<IPlayerSession> availablePlayers,
Dictionary<NetUserId, HumanoidCharacterProfile> profiles)
{
var assigned = new Dictionary<IPlayerSession, (string, StationId)>();
List<(IPlayerSession, List<string>)> GetPlayersJobCandidates(bool heads, JobPriority i)
{
return availablePlayers.Select(player =>
{
var profile = profiles[player.UserId];
var roleBans = _roleBanManager.GetJobBans(player.UserId);
var availableJobs = profile.JobPriorities
.Where(j =>
{
var (jobId, priority) = j;
if (!_prototypeManager.TryIndex(jobId, out JobPrototype? job))
{
// Job doesn't exist, probably old data?
return false;
}
if (job.IsHead != heads)
{
return false;
}
return priority == i;
})
.Where(p => roleBans != null && !roleBans.Contains(p.Key))
.Select(j => j.Key)
.ToList();
return (player, availableJobs);
})
.Where(p => p.availableJobs.Count != 0)
.ToList();
}
void ProcessJobs(bool heads, Dictionary<string, int> availablePositions, StationId id, JobPriority i)
{
var candidates = GetPlayersJobCandidates(heads, i);
foreach (var (candidate, jobs) in candidates)
{
while (jobs.Count != 0)
{
var picked = _robustRandom.Pick(jobs);
var openPositions = availablePositions.GetValueOrDefault(picked, 0);
if (openPositions == 0)
{
jobs.Remove(picked);
continue;
}
availablePositions[picked] -= 1;
assigned.Add(candidate, (picked, id));
break;
}
}
availablePlayers.RemoveAll(a => assigned.ContainsKey(a));
}
// Current strategy is to fill each station one by one.
foreach (var (id, station) in _stationSystem.StationInfo)
{
// Get the ROUND-START job list.
var availablePositions = station.MapPrototype.AvailableJobs.ToDictionary(x => x.Key, x => x.Value[0]);
for (var i = JobPriority.High; i > JobPriority.Never; i--)
{
// Process jobs possible for heads...
ProcessJobs(true, availablePositions, id, i);
// and then jobs that are not heads.
ProcessJobs(false, availablePositions, id, i);
}
}
return assigned;
}
private string? PickBestAvailableJob(IPlayerSession playerSession, HumanoidCharacterProfile profile,
StationId station)
{
if (station == StationId.Invalid)
return null;
var available = _stationSystem.StationInfo[station].JobList;
bool TryPick(JobPriority priority, [NotNullWhen(true)] out string? jobId)
{
var roleBans = _roleBanManager.GetJobBans(playerSession.UserId);
var filtered = profile.JobPriorities
.Where(p => p.Value == priority)
.Where(p => roleBans != null && !roleBans.Contains(p.Key))
.Select(p => p.Key)
.ToList();
while (filtered.Count != 0)
{
jobId = _robustRandom.Pick(filtered);
if (available.GetValueOrDefault(jobId, 0) > 0)
{
return true;
}
filtered.Remove(jobId);
}
jobId = default;
return false;
}
if (TryPick(JobPriority.High, out var picked))
{
return picked;
}
if (TryPick(JobPriority.Medium, out picked))
{
return picked;
}
if (TryPick(JobPriority.Low, out picked))
{
return picked;
}
var overflows = _stationSystem.StationInfo[station].MapPrototype.OverflowJobs.Clone().ToList();
return overflows.Count != 0 ? _robustRandom.Pick(overflows) : null;
}
[Conditional("DEBUG")]
private void InitializeJobController()
{
// Verify that the overflow role exists and has the correct name.
var role = _prototypeManager.Index<JobPrototype>(FallbackOverflowJob);
DebugTools.Assert(role.Name == Loc.GetString(FallbackOverflowJobName),
"Overflow role does not have the correct name!");
}
private void AddSpawnedPosition(string jobId)
{
_spawnedPositions[jobId] = _spawnedPositions.GetValueOrDefault(jobId, 0) + 1;
}
private TickerJobsAvailableEvent GetJobsAvailable()
{
// If late join is disallowed, return no available jobs.
if (DisallowLateJoin)
return new TickerJobsAvailableEvent(new Dictionary<StationId, string>(), new Dictionary<StationId, Dictionary<string, int>>());
var jobs = new Dictionary<StationId, Dictionary<string, int>>();
var stationNames = new Dictionary<StationId, string>();
foreach (var (id, station) in _stationSystem.StationInfo)
{
var list = station.JobList.ToDictionary(x => x.Key, x => x.Value);
jobs.Add(id, list);
stationNames.Add(id, station.Name);
}
return new TickerJobsAvailableEvent(stationNames, jobs);
}
public void UpdateJobsAvailable()
{
RaiseNetworkEvent(GetJobsAvailable(), Filter.Empty().AddPlayers(_playersInLobby.Keys));
}
}
}

View File

@@ -1,16 +1,10 @@
using System;
using Content.Server.Players;
using Content.Server.Roles;
using Content.Server.Station;
using Content.Shared.GameTicking;
using Content.Shared.GameWindow;
using Content.Shared.Preferences;
using Content.Shared.Station;
using JetBrains.Annotations;
using Robust.Server.Player;
using Robust.Shared.Enums;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -112,7 +106,7 @@ namespace Content.Server.GameTicking
async void SpawnWaitPrefs()
{
await _prefsManager.WaitPreferencesLoaded(session);
SpawnPlayer(session, StationId.Invalid);
SpawnPlayer(session, EntityUid.Invalid);
}
async void AddPlayerToDb(Guid id)
@@ -151,7 +145,7 @@ namespace Content.Server.GameTicking
RaiseNetworkEvent(GetStatusMsg(session), client);
RaiseNetworkEvent(GetInfoMsg(), client);
RaiseNetworkEvent(GetPlayerStatus(), client);
RaiseNetworkEvent(GetJobsAvailable(), client);
RaiseLocalEvent(new PlayerJoinedLobbyEvent(session));
}
private void ReqWindowAttentionAll()
@@ -159,4 +153,14 @@ namespace Content.Server.GameTicking
RaiseNetworkEvent(new RequestWindowAttentionEvent());
}
}
public sealed class PlayerJoinedLobbyEvent : EntityEventArgs
{
public readonly IPlayerSession PlayerSession;
public PlayerJoinedLobbyEvent(IPlayerSession playerSession)
{
PlayerSession = playerSession;
}
}
}

View File

@@ -6,13 +6,11 @@ using Content.Server.Ghost;
using Content.Server.Maps;
using Content.Server.Mind;
using Content.Server.Players;
using Content.Server.Station;
using Content.Shared.CCVar;
using Content.Shared.Coordinates;
using Content.Shared.GameTicking;
using Content.Shared.Preferences;
using Content.Shared.Sound;
using Content.Shared.Station;
using JetBrains.Annotations;
using Prometheus;
using Robust.Server.Maps;
@@ -207,7 +205,7 @@ namespace Content.Server.GameTicking
// MapInitialize *before* spawning players, our codebase is too shit to do it afterwards...
_mapManager.DoMapInitialize(DefaultMap);
SpawnPlayers(readyPlayers, origReadyPlayers, profiles, force);
SpawnPlayers(readyPlayers, origReadyPlayers.Select(x => x.UserId), profiles, force);
_roundStartDateTime = DateTime.UtcNow;
RunLevel = GameRunLevel.InRound;
@@ -216,7 +214,6 @@ namespace Content.Server.GameTicking
SendStatusToAll();
ReqWindowAttentionAll();
UpdateLateJoinStatus();
UpdateJobsAvailable();
AnnounceRound();
#if EXCEPTION_TOLERANCE
@@ -428,8 +425,6 @@ namespace Content.Server.GameTicking
// So clients' entity systems can clean up too...
RaiseNetworkEvent(ev, Filter.Broadcast());
_spawnedPositions.Clear();
_manifest.Clear();
DisallowLateJoin = false;
}

View File

@@ -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;

View File

@@ -7,10 +7,11 @@ using Content.Server.Ghost;
using Content.Server.Maps;
using Content.Server.PDA;
using Content.Server.Preferences.Managers;
using Content.Server.Station;
using Content.Server.Station.Systems;
using Content.Shared.Chat;
using Content.Shared.Damage;
using Content.Shared.GameTicking;
using Content.Shared.Roles;
using Robust.Server;
using Robust.Server.Maps;
using Robust.Server.ServerStatus;
@@ -53,8 +54,9 @@ namespace Content.Server.GameTicking
InitializeLobbyMusic();
InitializeLobbyBackground();
InitializeGamePreset();
DebugTools.Assert(_prototypeManager.Index<JobPrototype>(FallbackOverflowJob).Name == Loc.GetString(FallbackOverflowJobName),
"Overflow role does not have the correct name!");
InitializeGameRules();
InitializeJobController();
InitializeUpdates();
_initialized = true;
@@ -111,6 +113,8 @@ namespace Content.Server.GameTicking
[Dependency] private readonly IRuntimeLog _runtimeLog = default!;
#endif
[Dependency] private readonly StationSystem _stationSystem = default!;
[Dependency] private readonly StationSpawningSystem _stationSpawning = default!;
[Dependency] private readonly StationJobsSystem _stationJobs = default!;
[Dependency] private readonly AdminLogSystem _adminLogSystem = default!;
[Dependency] private readonly HumanoidAppearanceSystem _humanoidAppearanceSystem = default!;
[Dependency] private readonly PDASystem _pdaSystem = default!;

View File

@@ -3,7 +3,7 @@ using System.Threading;
using Content.Server.Chat.Managers;
using Content.Server.Players;
using Content.Server.Roles;
using Content.Server.Station;
using Content.Server.Station.Components;
using Content.Server.Suspicion;
using Content.Server.Suspicion.Roles;
using Content.Server.Traitor.Uplink;
@@ -14,7 +14,6 @@ using Content.Shared.EntityList;
using Content.Shared.GameTicking;
using Content.Shared.Maps;
using Content.Shared.MobState.Components;
using Content.Shared.Random.Helpers;
using Content.Shared.Roles;
using Content.Shared.Sound;
using Content.Shared.Suspicion;
@@ -222,7 +221,7 @@ public sealed class SuspicionRuleSystem : GameRuleSystem
var susLoot = _prototypeManager.Index<EntityLootTablePrototype>(SuspicionLootTable);
foreach (var (_, mapGrid) in EntityManager.EntityQuery<StationComponent, IMapGridComponent>(true))
foreach (var (_, mapGrid) in EntityManager.EntityQuery<StationMemberComponent, IMapGridComponent>(true))
{
// I'm so sorry.
var tiles = mapGrid.Grid.GetAllTiles().ToArray();