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.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Content.Server.GameObjects;
|
using Content.Server.GameObjects;
|
||||||
|
using Content.Server.GameObjects.Components.Access;
|
||||||
using Content.Server.GameObjects.Components.Markers;
|
using Content.Server.GameObjects.Components.Markers;
|
||||||
using Content.Server.GameObjects.Components.Mobs;
|
using Content.Server.GameObjects.Components.Mobs;
|
||||||
using Content.Server.GameTicking.GamePresets;
|
using Content.Server.GameTicking.GamePresets;
|
||||||
@@ -41,7 +42,7 @@ using Robust.Shared.ViewVariables;
|
|||||||
|
|
||||||
namespace Content.Server.GameTicking
|
namespace Content.Server.GameTicking
|
||||||
{
|
{
|
||||||
public class GameTicker : SharedGameTicker, IGameTicker
|
public partial class GameTicker : SharedGameTicker, IGameTicker
|
||||||
{
|
{
|
||||||
private const string PlayerPrototypeName = "HumanMob_Content";
|
private const string PlayerPrototypeName = "HumanMob_Content";
|
||||||
private const string ObserverPrototypeName = "MobObserver";
|
private const string ObserverPrototypeName = "MobObserver";
|
||||||
@@ -51,6 +52,7 @@ namespace Content.Server.GameTicking
|
|||||||
private const float LobbyDuration = 20;
|
private const float LobbyDuration = 20;
|
||||||
|
|
||||||
[ViewVariables] private readonly List<GameRule> _gameRules = new List<GameRule>();
|
[ViewVariables] private readonly List<GameRule> _gameRules = new List<GameRule>();
|
||||||
|
[ViewVariables] private readonly List<ManifestEntry> _manifest = new List<ManifestEntry>();
|
||||||
|
|
||||||
// Value is whether they're ready.
|
// Value is whether they're ready.
|
||||||
[ViewVariables]
|
[ViewVariables]
|
||||||
@@ -88,7 +90,7 @@ namespace Content.Server.GameTicking
|
|||||||
{
|
{
|
||||||
DebugTools.Assert(!_initialized);
|
DebugTools.Assert(!_initialized);
|
||||||
|
|
||||||
_configurationManager.RegisterCVar("game.lobbyenabled", false, CVar.ARCHIVE);
|
_configurationManager.RegisterCVar("game.lobbyenabled", true, CVar.ARCHIVE);
|
||||||
_playerManager.PlayerStatusChanged += _handlePlayerStatusChanged;
|
_playerManager.PlayerStatusChanged += _handlePlayerStatusChanged;
|
||||||
|
|
||||||
_netManager.RegisterNetMessage<MsgTickerJoinLobby>(nameof(MsgTickerJoinLobby));
|
_netManager.RegisterNetMessage<MsgTickerJoinLobby>(nameof(MsgTickerJoinLobby));
|
||||||
@@ -99,6 +101,8 @@ namespace Content.Server.GameTicking
|
|||||||
RestartRound();
|
RestartRound();
|
||||||
|
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
|
|
||||||
|
JobControllerInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Update(FrameEventArgs frameEventArgs)
|
public void Update(FrameEventArgs frameEventArgs)
|
||||||
@@ -143,16 +147,48 @@ namespace Content.Server.GameTicking
|
|||||||
var preset = MakeGamePreset();
|
var preset = MakeGamePreset();
|
||||||
preset.Start();
|
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();
|
_sendStatusToAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private HumanoidCharacterProfile GetPlayerProfile(IPlayerSession p) =>
|
||||||
|
(HumanoidCharacterProfile) _prefsManager.GetPreferences(p.SessionId.Username).SelectedCharacter;
|
||||||
|
|
||||||
public void EndRound()
|
public void EndRound()
|
||||||
{
|
{
|
||||||
DebugTools.Assert(RunLevel == GameRunLevel.InRound);
|
DebugTools.Assert(RunLevel == GameRunLevel.InRound);
|
||||||
@@ -168,7 +204,7 @@ namespace Content.Server.GameTicking
|
|||||||
if (LobbyEnabled)
|
if (LobbyEnabled)
|
||||||
_playerJoinLobby(targetPlayer);
|
_playerJoinLobby(targetPlayer);
|
||||||
else
|
else
|
||||||
_spawnPlayer(targetPlayer);
|
SpawnPlayer(targetPlayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void MakeObserve(IPlayerSession player)
|
public void MakeObserve(IPlayerSession player)
|
||||||
@@ -182,7 +218,7 @@ namespace Content.Server.GameTicking
|
|||||||
{
|
{
|
||||||
if (!_playersInLobby.ContainsKey(player)) return;
|
if (!_playersInLobby.ContainsKey(player)) return;
|
||||||
|
|
||||||
_spawnPlayer(player);
|
SpawnPlayer(player);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ToggleReady(IPlayerSession player, bool ready)
|
public void ToggleReady(IPlayerSession player, bool ready)
|
||||||
@@ -308,6 +344,9 @@ namespace Content.Server.GameTicking
|
|||||||
|
|
||||||
_playerJoinLobby(player);
|
_playerJoinLobby(player);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_spawnedPositions.Clear();
|
||||||
|
_manifest.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void _preRoundSetup()
|
private void _preRoundSetup()
|
||||||
@@ -359,13 +398,13 @@ namespace Content.Server.GameTicking
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_spawnPlayer(session);
|
SpawnPlayer(session);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (data.Mind.CurrentEntity == null)
|
if (data.Mind.CurrentEntity == null)
|
||||||
{
|
{
|
||||||
_spawnPlayer(session);
|
SpawnPlayer(session);
|
||||||
}
|
}
|
||||||
else
|
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);
|
_playerJoinGame(session);
|
||||||
|
|
||||||
var data = session.ContentData();
|
var data = session.ContentData();
|
||||||
data.WipeMind();
|
data.WipeMind();
|
||||||
data.Mind = new Mind(session.SessionId);
|
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);
|
data.Mind.AddRole(job);
|
||||||
|
|
||||||
var mob = _spawnPlayerMob(job);
|
var mob = _spawnPlayerMob(job);
|
||||||
data.Mind.TransferTo(mob);
|
data.Mind.TransferTo(mob);
|
||||||
var character = _prefsManager
|
|
||||||
.GetPreferences(session.SessionId.Username)
|
|
||||||
.SelectedCharacter;
|
|
||||||
ApplyCharacterProfile(mob, character);
|
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)
|
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