Game ticker now has a job assignment system.
This commit is contained in:
199
Content.Server/GameTicking/GameTicker.JobController.cs
Normal file
199
Content.Server/GameTicking/GameTicker.JobController.cs
Normal file
@@ -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<string, int> _spawnedPositions = new Dictionary<string, int>();
|
||||
|
||||
private Dictionary<IPlayerSession, string> AssignJobs(List<IPlayerSession> available,
|
||||
Dictionary<IPlayerSession, HumanoidCharacterProfile> profiles)
|
||||
{
|
||||
// Calculate positions available round-start for each job.
|
||||
var availablePositions = GetBasePositions(true);
|
||||
|
||||
// Output dictionary of assigned jobs.
|
||||
var assigned = new Dictionary<IPlayerSession, string>();
|
||||
|
||||
// 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<JobPrototype>(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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the available positions for all jobs, *not* accounting for the current crew manifest.
|
||||
/// </summary>
|
||||
private Dictionary<string, int> GetBasePositions(bool roundStart)
|
||||
{
|
||||
var availablePositions = _prototypeManager
|
||||
.EnumeratePrototypes<JobPrototype>()
|
||||
// -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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the remaining available job positions in the current round.
|
||||
/// </summary>
|
||||
private Dictionary<string, int> 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<JobPrototype>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<GameRule> _gameRules = new List<GameRule>();
|
||||
[ViewVariables] private readonly List<ManifestEntry> _manifest = new List<ManifestEntry>();
|
||||
|
||||
// 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<MsgTickerJoinLobby>(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<IPlayerSession> 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<JobPrototype>("Assistant"));
|
||||
|
||||
if (jobId == null)
|
||||
{
|
||||
// Pick best job best on prefs.
|
||||
jobId = PickBestAvailableJob(character);
|
||||
}
|
||||
|
||||
var jobPrototype = _prototypeManager.Index<JobPrototype>(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<InventoryComponent>();
|
||||
inventory.Equip(EquipmentSlotDefines.Slots.IDCARD, card.GetComponent<ClothingComponent>());
|
||||
|
||||
var cardComponent = card.GetComponent<IdCardComponent>();
|
||||
cardComponent.FullName = characterName;
|
||||
cardComponent.JobTitle = jobPrototype.Name;
|
||||
|
||||
var access = card.GetComponent<AccessComponent>();
|
||||
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)
|
||||
|
||||
28
Content.Server/GameTicking/ManifestEntry.cs
Normal file
28
Content.Server/GameTicking/ManifestEntry.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.GameTicking
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes an entry in the crew manifest.
|
||||
/// </summary>
|
||||
public sealed class ManifestEntry
|
||||
{
|
||||
public ManifestEntry(string characterName, string jobId)
|
||||
{
|
||||
CharacterName = characterName;
|
||||
JobId = jobId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The name of the character on the manifest.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public string CharacterName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the job they picked.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public string JobId { get; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user