Cloning error messages and prediction based timing (#4013)

* Cloning error messages and prediction based timing

* Cloning error messages & prediction based timing: Fix problems mentioned in reviews
This commit is contained in:
20kdc
2021-06-05 18:32:59 +01:00
committed by GitHub
parent 62ce603858
commit 561a5bc0f2
6 changed files with 178 additions and 38 deletions

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Robust.Client.UserInterface;
@@ -5,6 +6,8 @@ using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.IoC;
using Robust.Shared.Timing;
using static Content.Shared.GameObjects.Components.Medical.SharedCloningPodComponent;
namespace Content.Client.GameObjects.Components.CloningPod
@@ -59,7 +62,7 @@ namespace Content.Client.GameObjects.Components.CloningPod
{
MinSize = (200, 20),
MinValue = 0,
MaxValue = 120, // todo make this actually derive from cloning time
MaxValue = 120,
Page = 0,
Value = 0.5f,
Children =
@@ -100,17 +103,37 @@ namespace Content.Client.GameObjects.Components.CloningPod
{
_scanManager = state.MindIdName;
BuildCloneList();
_lastUpdate = state;
}
_lastUpdate = state;
var percentage = state.Progress / _cloningProgressBar.MaxValue * 100;
_progressLabel.Text = $"{percentage:0}%";
_cloningProgressBar.Value = state.Progress;
_cloningProgressBar.MaxValue = state.Maximum;
UpdateProgress();
_mindState.Text = Loc.GetString(state.MindPresent ? "Consciousness Detected" : "No Activity");
_mindState.FontColorOverride = state.MindPresent ? Color.Green : Color.Red;
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
UpdateProgress();
}
private void UpdateProgress()
{
if (_lastUpdate == null)
return;
float simulatedProgress = _lastUpdate.Progress;
if (_lastUpdate.Progressing)
{
TimeSpan sinceReference = IoCManager.Resolve<IGameTiming>().CurTime - _lastUpdate.ReferenceTime;
simulatedProgress += (float) sinceReference.TotalSeconds;
simulatedProgress = MathHelper.Clamp(simulatedProgress, 0f, _lastUpdate.Maximum);
}
var percentage = simulatedProgress / _cloningProgressBar.MaxValue * 100;
_progressLabel.Text = $"{percentage:0}%";
_cloningProgressBar.Value = simulatedProgress;
}
private void BuildCloneList()
{
_scanList.RemoveAllChildren();

View File

@@ -4,17 +4,18 @@ using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Observer;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.Interfaces;
using Content.Server.Mobs;
using Content.Server.Utility;
using Content.Shared.GameObjects.Components.Medical;
using Content.Shared.GameObjects.Components.Mobs.State;
using Content.Shared.Interfaces;
using Content.Shared.Preferences;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Network;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.ViewVariables;
@@ -24,7 +25,6 @@ namespace Content.Server.GameObjects.Components.Medical
[RegisterComponent]
public class CloningPodComponent : SharedCloningPodComponent
{
[Dependency] private readonly IServerPreferencesManager _prefsManager = null!;
[Dependency] private readonly IPlayerManager _playerManager = null!;
[Dependency] private readonly EuiManager _euiManager = null!;
@@ -40,6 +40,8 @@ namespace Content.Server.GameObjects.Components.Medical
[ViewVariables] public float CloningProgress = 0;
[DataField("cloningTime")]
[ViewVariables] public float CloningTime = 30f;
// Used to prevent as many duplicate UI messages as possible
[ViewVariables] public bool UiKnownPowerState = false;
[ViewVariables]
public CloningPodStatus Status;
@@ -56,8 +58,7 @@ namespace Content.Server.GameObjects.Components.Medical
BodyContainer = ContainerHelpers.EnsureContainer<ContainerSlot>(Owner, $"{Name}-bodyContainer");
//TODO: write this so that it checks for a change in power events for GORE POD cases
if (UserInterface != null)
EntitySystem.Get<CloningSystem>().UpdateUserInterface(this);
EntitySystem.Get<CloningSystem>().UpdateUserInterface(this);
}
public override void OnRemove()
@@ -85,16 +86,28 @@ namespace Content.Server.GameObjects.Components.Medical
switch (message.Button)
{
case UiButton.Clone:
if (message.ScanId == null || BodyContainer.ContainedEntity != null)
if (BodyContainer.ContainedEntity != null)
{
obj.Session.AttachedEntity?.PopupMessageCursor(Loc.GetString("cloning-pod-component-msg-occupied"));
return;
}
if (message.ScanId == null)
{
obj.Session.AttachedEntity?.PopupMessageCursor(Loc.GetString("cloning-pod-component-msg-no-selection"));
return;
}
var cloningSystem = EntitySystem.Get<CloningSystem>();
if (!cloningSystem.Minds.TryGetValue(message.ScanId.Value, out var mind))
if (!cloningSystem.IdToDNA.TryGetValue(message.ScanId.Value, out var dna))
{
obj.Session.AttachedEntity?.PopupMessageCursor(Loc.GetString("cloning-pod-component-msg-bad-selection"));
return; // ScanId is not in database
}
var mind = dna.Mind;
if (cloningSystem.ClonesWaitingForMind.TryGetValue(mind, out var cloneUid))
{
if (Owner.EntityManager.TryGetEntity(cloneUid, out var clone) &&
@@ -102,7 +115,10 @@ namespace Content.Server.GameObjects.Components.Medical
!cloneState.IsDead() &&
clone.TryGetComponent(out MindComponent? cloneMindComp) &&
(cloneMindComp.Mind == null || cloneMindComp.Mind == mind))
{
obj.Session.AttachedEntity?.PopupMessageCursor(Loc.GetString("cloning-pod-component-msg-already-cloning"));
return; // Mind already has clone
}
cloningSystem.ClonesWaitingForMind.Remove(mind);
}
@@ -110,17 +126,22 @@ namespace Content.Server.GameObjects.Components.Medical
if (mind.OwnedEntity != null &&
mind.OwnedEntity.TryGetComponent<IMobStateComponent>(out var state) &&
!state.IsDead())
{
obj.Session.AttachedEntity?.PopupMessageCursor(Loc.GetString("cloning-pod-component-msg-already-alive"));
return; // Body controlled by mind is not dead
}
// TODO: Implement ClonerDNAEntry and get the profile appearance and name when scanned
// Yes, we still need to track down the client because we need to open the Eui
if (mind.UserId == null || !_playerManager.TryGetSessionById(mind.UserId.Value, out var client))
return;
{
obj.Session.AttachedEntity?.PopupMessageCursor(Loc.GetString("cloning-pod-component-msg-user-offline"));
return; // If we can't track down the client, we can't offer transfer. That'd be quite bad.
}
var mob = Owner.EntityManager.SpawnEntity("HumanMob_Content", Owner.Transform.MapPosition);
var profile = GetPlayerProfileAsync(client.UserId);
mob.GetComponent<HumanoidAppearanceComponent>().UpdateFromProfile(profile);
mob.Name = profile.Name;
mob.GetComponent<HumanoidAppearanceComponent>().UpdateFromProfile(dna.Profile);
mob.Name = dna.Profile.Name;
var cloneMindReturn = mob.AddComponent<BeingClonedComponent>();
cloneMindReturn.Mind = mind;
@@ -165,10 +186,5 @@ namespace Content.Server.GameObjects.Components.Medical
UpdateAppearance();
EntitySystem.Get<CloningSystem>().UpdateUserInterface(this);
}
private HumanoidCharacterProfile GetPlayerProfileAsync(NetUserId userId)
{
return (HumanoidCharacterProfile) _prefsManager.GetPreferences(userId).SelectedCharacter;
}
}
}

View File

@@ -1,10 +1,11 @@
#nullable enable
#nullable enable
using System;
using System.Collections.Generic;
using Content.Server.GameObjects.Components.Mobs;
using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Content.Server.GameObjects.EntitySystems;
using Content.Server.Utility;
using Content.Server.Interfaces;
using Content.Shared.Damage;
using Content.Shared.GameObjects.Components.Damage;
using Content.Shared.GameObjects.Components.Medical;
@@ -12,13 +13,17 @@ using Content.Shared.GameObjects.Components.Mobs.State;
using Content.Shared.GameObjects.EntitySystems;
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
using Content.Shared.GameObjects.Verbs;
using Content.Shared.Interfaces;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Preferences;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Containers;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Timing;
using Robust.Shared.ViewVariables;
@@ -29,6 +34,8 @@ namespace Content.Server.GameObjects.Components.Medical
[ComponentReference(typeof(SharedMedicalScannerComponent))]
public class MedicalScannerComponent : SharedMedicalScannerComponent, IActivate, IDestroyAct
{
[Dependency] private readonly IServerPreferencesManager _prefsManager = null!;
[Dependency] private readonly IPlayerManager _playerManager = null!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
private static readonly TimeSpan InternalOpenAttemptDelay = TimeSpan.FromSeconds(0.5);
@@ -269,13 +276,32 @@ namespace Content.Server.GameObjects.Components.Medical
case UiButton.ScanDNA:
if (_bodyContainer.ContainedEntity != null)
{
//TODO: Show a 'ERROR: Body is completely devoid of soul' if no Mind owns the entity.
var cloningSystem = EntitySystem.Get<CloningSystem>();
if (!_bodyContainer.ContainedEntity.TryGetComponent(out MindComponent? mind) || mind.Mind == null)
if (!_bodyContainer.ContainedEntity.TryGetComponent(out MindComponent? mindComp) || mindComp.Mind == null)
{
obj.Session.AttachedEntity?.PopupMessageCursor(Loc.GetString("medical-scanner-component-msg-no-soul"));
break;
}
cloningSystem.AddToDnaScans(mind.Mind);
// Null suppression based on above check. Yes, it's explicitly needed
var mind = mindComp.Mind!;
// We need the HumanoidCharacterProfile
// TODO: Move this further 'outwards' into a DNAComponent or somesuch.
// Ideally this ends with GameTicker & CloningSystem handing DNA to a function that sets up a body for that DNA.
var mindUser = mind.UserId;
if (mindUser == null)
{
// For now assume this means soul departed
obj.Session.AttachedEntity?.PopupMessageCursor(Loc.GetString("medical-scanner-component-msg-soul-broken"));
break;
}
// has to be explicit cast like this, IDK why, null suppression operators seem to not work
var profile = GetPlayerProfileAsync((NetUserId) mindUser);
cloningSystem.AddToDnaScans(new ClonerDNAEntry(mind, profile));
}
break;
@@ -294,5 +320,10 @@ namespace Content.Server.GameObjects.Components.Medical
{
EjectBody();
}
private HumanoidCharacterProfile GetPlayerProfileAsync(NetUserId userId)
{
return (HumanoidCharacterProfile) _prefsManager.GetPreferences(userId).SelectedCharacter;
}
}
}

View File

@@ -1,3 +1,4 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using Content.Server.GameObjects.Components.Medical;
@@ -6,16 +7,23 @@ using Content.Server.GameObjects.Components.Power.ApcNetComponents;
using Content.Server.Mobs;
using Content.Shared.GameTicking;
using Content.Shared.Interfaces.GameObjects.Components;
using Content.Shared.Preferences;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using Robust.Shared.IoC;
using static Content.Shared.GameObjects.Components.Medical.SharedCloningPodComponent;
namespace Content.Server.GameObjects.EntitySystems
{
internal sealed class CloningSystem : EntitySystem, IResettingEntitySystem
{
public readonly Dictionary<int, Mind> Minds = new();
[Dependency] private readonly IGameTiming _timing = default!;
public readonly Dictionary<Mind, int> MindToId = new();
public readonly Dictionary<int, ClonerDNAEntry> IdToDNA = new();
private int _nextAllocatedMindId = 0;
private float _quickAndDirtyUserUpdatePreventerTimer = 0.0f;
public readonly Dictionary<Mind, EntityUid> ClonesWaitingForMind = new();
public override void Initialize()
@@ -42,8 +50,9 @@ namespace Content.Server.GameObjects.EntitySystems
mindComp.Mind != null)
return;
mind?.TransferTo(entity);
mind?.UnVisit();
mind.TransferTo(entity);
mind.UnVisit();
ClonesWaitingForMind.Remove(mind);
}
private void HandleActivate(EntityUid uid, CloningPodComponent component, ActivateInWorldEvent args)
@@ -75,6 +84,13 @@ namespace Content.Server.GameObjects.EntitySystems
{
foreach (var (cloning, power) in ComponentManager.EntityQuery<CloningPodComponent, PowerReceiverComponent>(true))
{
if (cloning.UiKnownPowerState != power.Powered)
{
// Must be *before* update
cloning.UiKnownPowerState = power.Powered;
UpdateUserInterface(cloning);
}
if (!power.Powered)
return;
@@ -88,8 +104,6 @@ namespace Content.Server.GameObjects.EntitySystems
{
cloning.Eject();
}
UpdateUserInterface(cloning);
}
}
@@ -99,32 +113,65 @@ namespace Content.Server.GameObjects.EntitySystems
comp.UserInterface?.SetState(
new CloningPodBoundUserInterfaceState(
idToUser,
// now
_timing.CurTime,
// progress, time, progressing
comp.CloningProgress,
comp.CloningTime,
// this is duplicate w/ the above check that actually updates progress
// better here than on client though
comp.UiKnownPowerState && (comp.BodyContainer.ContainedEntity != null),
comp.Status == CloningPodStatus.Cloning));
}
public void AddToDnaScans(Mind mind)
public void AddToDnaScans(ClonerDNAEntry dna)
{
if (!Minds.ContainsValue(mind))
if (!MindToId.ContainsKey(dna.Mind))
{
Minds.Add(Minds.Count, mind);
int id = _nextAllocatedMindId++;
MindToId.Add(dna.Mind, id);
IdToDNA.Add(id, dna);
}
OnChangeMadeToDnaScans();
}
public void OnChangeMadeToDnaScans()
{
foreach (var cloning in ComponentManager.EntityQuery<CloningPodComponent>(true))
UpdateUserInterface(cloning);
}
public bool HasDnaScan(Mind mind)
{
return Minds.ContainsValue(mind);
return MindToId.ContainsKey(mind);
}
public Dictionary<int, string?> GetIdToUser()
{
return Minds.ToDictionary(m => m.Key, m => m.Value.CharacterName);
return IdToDNA.ToDictionary(m => m.Key, m => m.Value.Mind.CharacterName);
}
public void Reset()
{
Minds.Clear();
MindToId.Clear();
IdToDNA.Clear();
ClonesWaitingForMind.Clear();
_nextAllocatedMindId = 0;
// We PROBABLY don't need to send out UI interface updates for the dna scan changes during a reset
}
}
// TODO: This needs to be moved to Content.Server.Mobs and made a global point of reference.
// For example, GameTicker should be using this, and this should be using ICharacterProfile rather than HumanoidCharacterProfile.
// It should carry a reference or copy of itself with the mobs that it affects.
// See TODO in MedicalScannerComponent.
struct ClonerDNAEntry {
public Mind Mind;
public HumanoidCharacterProfile Profile;
public ClonerDNAEntry(Mind m, HumanoidCharacterProfile hcp)
{
Mind = m;
Profile = hcp;
}
}
}

View File

@@ -14,13 +14,25 @@ namespace Content.Shared.GameObjects.Components.Medical
public class CloningPodBoundUserInterfaceState : BoundUserInterfaceState
{
public readonly Dictionary<int, string?> MindIdName;
// When this state was created.
// The reason this is used rather than a start time is because cloning can be interrupted.
public readonly TimeSpan ReferenceTime;
// Both of these are in seconds.
// They're not TimeSpans because of complicated reasons.
// CurTime of receipt is combined with Progress.
public readonly float Progress;
public readonly float Maximum;
// If true, cloning is progressing (predict clone progress)
public readonly bool Progressing;
public readonly bool MindPresent;
public CloningPodBoundUserInterfaceState(Dictionary<int, string?> mindIdName, float progress, bool mindPresent)
public CloningPodBoundUserInterfaceState(Dictionary<int, string?> mindIdName, TimeSpan refTime, float progress, float maximum, bool progressing, bool mindPresent)
{
MindIdName = mindIdName;
ReferenceTime = refTime;
Progress = progress;
Maximum = maximum;
Progressing = progressing;
MindPresent = mindPresent;
}
}

View File

@@ -0,0 +1,11 @@
### yup etc
cloning-pod-component-msg-occupied = ERROR: The pod already contains a clone
cloning-pod-component-msg-no-selection = ERROR: You didn't select someone to clone
cloning-pod-component-msg-bad-selection = ERROR: Entry Removed During Selection // System Error
cloning-pod-component-msg-already-cloning = ERROR: Pod Network Conflict
cloning-pod-component-msg-already-alive = ERROR: Metaphysical Conflict
cloning-pod-component-msg-user-offline = ERROR: Metaphysical Disturbance
medical-scanner-component-msg-no-soul = ERROR: Body is completely devoid of soul
medical-scanner-component-msg-soul-broken = ERROR: Soul present, but defunct / departed