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,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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!;
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user