Game ticker now has a job assignment system.

This commit is contained in:
Pieter-Jan Briers
2020-01-19 19:08:35 +01:00
parent e64d80d02e
commit aaf0b7a645
3 changed files with 315 additions and 15 deletions

View 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;
}
}
}

View File

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

View 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; }
}
}