diff --git a/Content.Server/GameTicking/GameTicker.JobController.cs b/Content.Server/GameTicking/GameTicker.JobController.cs new file mode 100644 index 0000000000..c7d82c9072 --- /dev/null +++ b/Content.Server/GameTicking/GameTicker.JobController.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Content.Shared.Jobs; +using Content.Shared.Preferences; +using Robust.Server.Interfaces.Player; +using Robust.Shared.Localization; +using Robust.Shared.Random; +using Robust.Shared.Utility; +using Robust.Shared.ViewVariables; + +namespace Content.Server.GameTicking +{ + // This code is responsible for the assigning & picking of jobs. + public partial class GameTicker + { + [ViewVariables] + private readonly Dictionary _spawnedPositions = new Dictionary(); + + private Dictionary AssignJobs(List available, + Dictionary profiles) + { + // Calculate positions available round-start for each job. + var availablePositions = GetBasePositions(true); + + // Output dictionary of assigned jobs. + var assigned = new Dictionary(); + + // Go over each priority level top to bottom. + for (var i = JobPriority.High; i > JobPriority.Never; i--) + { + void ProcessJobs(bool heads) + { + // Get all candidates for this priority & heads combo. + // That is all people with at LEAST one job at this priority & heads level, + // and the jobs they have selected here. + var candidates = available + .Select(player => + { + var profile = profiles[player]; + + var availableJobs = profile.JobPriorities + .Where(j => + { + var (jobId, priority) = j; + var job = _prototypeManager.Index(jobId); + if (job.IsHead != heads) + { + return false; + } + + return priority == i; + }) + .Select(j => j.Key) + .ToList(); + + return (player, availableJobs); + }) + .Where(p => p.availableJobs.Count != 0) + .ToList(); + + _robustRandom.Shuffle(candidates); + + 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); + break; + } + } + + available.RemoveAll(a => assigned.ContainsKey(a)); + } + + // Process heads FIRST. + // This means that if you have head and non-head roles on the same priority level, + // you will always get picked as head. + // Unless of course somebody beats you to those head roles. + ProcessJobs(true); + ProcessJobs(false); + } + + return assigned; + } + + /// + /// Gets the available positions for all jobs, *not* accounting for the current crew manifest. + /// + private Dictionary GetBasePositions(bool roundStart) + { + var availablePositions = _prototypeManager + .EnumeratePrototypes() + // -1 is treated as infinite slots. + .ToDictionary(job => job.ID, job => + { + if (job.SpawnPositions < 0) + { + return int.MaxValue; + } + + if (roundStart) + { + return job.SpawnPositions; + } + + return job.TotalPositions; + }); + + return availablePositions; + } + + /// + /// Gets the remaining available job positions in the current round. + /// + private Dictionary GetAvailablePositions() + { + var basePositions = GetBasePositions(false); + + foreach (var (jobId, count) in _spawnedPositions) + { + basePositions[jobId] = Math.Max(0, basePositions[jobId] - count); + } + + return basePositions; + } + + private string PickBestAvailableJob(HumanoidCharacterProfile profile) + { + var available = GetAvailablePositions(); + + bool TryPick(JobPriority priority, out string jobId) + { + var filtered = profile.JobPriorities + .Where(p => p.Value == priority) + .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; + } + + return OverflowJob; + } + + [Conditional("DEBUG")] + private void JobControllerInit() + { + // Verify that the overflow role exists and has the correct name. + var role = _prototypeManager.Index(OverflowJob); + DebugTools.Assert(role.Name == Loc.GetString(OverflowJobName), + "Overflow role does not have the correct name!"); + + DebugTools.Assert(role.SpawnPositions < 0, "Overflow role must have infinite spawn positions!"); + } + + private void AddSpawnedPosition(string jobId) + { + _spawnedPositions[jobId] = _spawnedPositions.GetValueOrDefault(jobId, 0) + 1; + } + } +} diff --git a/Content.Server/GameTicking/GameTicker.cs b/Content.Server/GameTicking/GameTicker.cs index a4749c4da6..841bacee35 100644 --- a/Content.Server/GameTicking/GameTicker.cs +++ b/Content.Server/GameTicking/GameTicker.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Content.Server.GameObjects; +using Content.Server.GameObjects.Components.Access; using Content.Server.GameObjects.Components.Markers; using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameTicking.GamePresets; @@ -41,7 +42,7 @@ using Robust.Shared.ViewVariables; namespace Content.Server.GameTicking { - public class GameTicker : SharedGameTicker, IGameTicker + public partial class GameTicker : SharedGameTicker, IGameTicker { private const string PlayerPrototypeName = "HumanMob_Content"; private const string ObserverPrototypeName = "MobObserver"; @@ -51,6 +52,7 @@ namespace Content.Server.GameTicking private const float LobbyDuration = 20; [ViewVariables] private readonly List _gameRules = new List(); + [ViewVariables] private readonly List _manifest = new List(); // Value is whether they're ready. [ViewVariables] @@ -88,7 +90,7 @@ namespace Content.Server.GameTicking { DebugTools.Assert(!_initialized); - _configurationManager.RegisterCVar("game.lobbyenabled", false, CVar.ARCHIVE); + _configurationManager.RegisterCVar("game.lobbyenabled", true, CVar.ARCHIVE); _playerManager.PlayerStatusChanged += _handlePlayerStatusChanged; _netManager.RegisterNetMessage(nameof(MsgTickerJoinLobby)); @@ -99,6 +101,8 @@ namespace Content.Server.GameTicking RestartRound(); _initialized = true; + + JobControllerInit(); } public void Update(FrameEventArgs frameEventArgs) @@ -143,16 +147,48 @@ namespace Content.Server.GameTicking var preset = MakeGamePreset(); preset.Start(); - foreach (var (playerSession, ready) in _playersInLobby.ToList()) + List readyPlayers; + if (LobbyEnabled) { - if (LobbyEnabled && !ready) continue; + readyPlayers = _playersInLobby.Where(p => p.Value).Select(p => p.Key).ToList(); + } + else + { + readyPlayers = _playersInLobby.Keys.ToList(); + } - _spawnPlayer(playerSession); + // Get the profiles for each player for easier lookup. + var profiles = readyPlayers.ToDictionary(p => p, GetPlayerProfile); + + var assignedJobs = AssignJobs(readyPlayers, profiles); + + // For players without jobs, give them the overflow job if they have that set... + foreach (var player in readyPlayers) + { + if (assignedJobs.ContainsKey(player)) + { + continue; + } + + var profile = profiles[player]; + if (profile.PreferenceUnavailable == PreferenceUnavailableMode.SpawnAsOverflow) + { + assignedJobs.Add(player, OverflowJob); + } + } + + // Spawn everybody in! + foreach (var (player, job) in assignedJobs) + { + SpawnPlayer(player, job); } _sendStatusToAll(); } + private HumanoidCharacterProfile GetPlayerProfile(IPlayerSession p) => + (HumanoidCharacterProfile) _prefsManager.GetPreferences(p.SessionId.Username).SelectedCharacter; + public void EndRound() { DebugTools.Assert(RunLevel == GameRunLevel.InRound); @@ -168,7 +204,7 @@ namespace Content.Server.GameTicking if (LobbyEnabled) _playerJoinLobby(targetPlayer); else - _spawnPlayer(targetPlayer); + SpawnPlayer(targetPlayer); } public void MakeObserve(IPlayerSession player) @@ -182,7 +218,7 @@ namespace Content.Server.GameTicking { if (!_playersInLobby.ContainsKey(player)) return; - _spawnPlayer(player); + SpawnPlayer(player); } public void ToggleReady(IPlayerSession player, bool ready) @@ -308,6 +344,9 @@ namespace Content.Server.GameTicking _playerJoinLobby(player); } + + _spawnedPositions.Clear(); + _manifest.Clear(); } private void _preRoundSetup() @@ -359,13 +398,13 @@ namespace Content.Server.GameTicking return; } - _spawnPlayer(session); + SpawnPlayer(session); } else { if (data.Mind.CurrentEntity == null) { - _spawnPlayer(session); + SpawnPlayer(session); } else { @@ -387,22 +426,56 @@ namespace Content.Server.GameTicking } } - private void _spawnPlayer(IPlayerSession session) + private void SpawnPlayer(IPlayerSession session, string jobId = null) { + var character = (HumanoidCharacterProfile) _prefsManager + .GetPreferences(session.SessionId.Username) + .SelectedCharacter; + _playerJoinGame(session); + var data = session.ContentData(); data.WipeMind(); data.Mind = new Mind(session.SessionId); - //TODO Replace "Assistant" with the job when char preference are done - var job = new Job(data.Mind, _prototypeManager.Index("Assistant")); + + if (jobId == null) + { + // Pick best job best on prefs. + jobId = PickBestAvailableJob(character); + } + + var jobPrototype = _prototypeManager.Index(jobId); + var job = new Job(data.Mind, jobPrototype); data.Mind.AddRole(job); var mob = _spawnPlayerMob(job); data.Mind.TransferTo(mob); - var character = _prefsManager - .GetPreferences(session.SessionId.Username) - .SelectedCharacter; ApplyCharacterProfile(mob, character); + + AddManifestEntry(character.Name, jobId); + AddSpawnedPosition(jobId); + EquipIdCard(mob, character.Name, jobPrototype); + } + + private void EquipIdCard(IEntity mob, string characterName, JobPrototype jobPrototype) + { + var card = _entityManager.SpawnEntity("IDCardStandard", mob.Transform.GridPosition); + + var inventory = mob.GetComponent(); + inventory.Equip(EquipmentSlotDefines.Slots.IDCARD, card.GetComponent()); + + var cardComponent = card.GetComponent(); + cardComponent.FullName = characterName; + cardComponent.JobTitle = jobPrototype.Name; + + var access = card.GetComponent(); + access.Tags.Clear(); + access.Tags.AddRange(jobPrototype.Access); + } + + private void AddManifestEntry(string characterName, string jobId) + { + _manifest.Add(new ManifestEntry(characterName, jobId)); } private void _spawnObserver(IPlayerSession session) diff --git a/Content.Server/GameTicking/ManifestEntry.cs b/Content.Server/GameTicking/ManifestEntry.cs new file mode 100644 index 0000000000..efc6d63fa4 --- /dev/null +++ b/Content.Server/GameTicking/ManifestEntry.cs @@ -0,0 +1,28 @@ +using Robust.Shared.ViewVariables; + +namespace Content.Server.GameTicking +{ + /// + /// Describes an entry in the crew manifest. + /// + public sealed class ManifestEntry + { + public ManifestEntry(string characterName, string jobId) + { + CharacterName = characterName; + JobId = jobId; + } + + /// + /// The name of the character on the manifest. + /// + [ViewVariables] + public string CharacterName { get; } + + /// + /// The ID of the job they picked. + /// + [ViewVariables] + public string JobId { get; } + } +}