-
Notifications
You must be signed in to change notification settings - Fork 4.7k
Fix SSD text delay on examine #39241
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix SSD text delay on examine #39241
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not very familiar with mind code so I can't comment much on the approach, but I am pretty confident that it would be better to avoid using an update loop here: it's possible this will mean you need to add some events to MindSystem for this.
| SubscribeLocalEvent<MindStatusComponent, ExaminedEvent>(OnExamined); | ||
| } | ||
|
|
||
| private void OnExamined(EntityUid uid, MindStatusComponent component, ExaminedEvent args) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general using the Entity<T> variant of event handlers is preferred—here the signature would be OnExamined(Entity<MindStatusComponent> ent, ref ExaminedEvent args).
| [Dependency] private readonly INetManager _net = default!; | ||
|
|
||
| private TimeSpan _nextUpdate = TimeSpan.Zero; | ||
| private const float UpdateInterval = 1.0f; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These should be part of the component to ensure it gets serialized correctly, and the update interval should be a TimeSpan. I'm also not positive that this needs an update loop at all—it'd be better if this is something that could just be updated in response to events. You'd then want to make _nextUpdate is an [AutoPausedField], and for it to get set to CurTime on MapInit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I agree this sussed me out - I think the solution is to just not do it at all because even having it on the component is a little odd. Events is right way I think
| SubscribeLocalEvent<MindContainerComponent, MindRemovedMessage>(OnMindRemoved); | ||
| SubscribeLocalEvent<ForceUpdateMindStatusEvent>(OnForceUpdate); | ||
|
|
||
| if (_net.IsServer) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of having all these _net.IsClient / _net.IsServer, it'd probably be better to 1) make MindStatusSystem abstract and rename it to SharedMindStatusSystem, 2) in both Content.Client and Content.Server, create their own MindStatusSystems which inherit from SharedMindStatusSystem, and then 3) keep all the server-specific code in the version that's in Content.Server.
That being said, I'm not sure if this is needed, as it looks like nearly all of these events are only raised by the server, meaning you don't need the checks at all—it just won't ever run on the client.
| if (_timing.CurTime < _nextUpdate) // Periodic update for edge cases | ||
| return; | ||
|
|
||
| _nextUpdate = _timing.CurTime + TimeSpan.FromSeconds(UpdateInterval); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even though precision is not important here, it's good practice to increment via _nextUpdate += UpdateInterval. Otherwise each time between updates will be slightly longer than the UpdateInterval, since Update isn't called every single (arbitrary small unit of time).
| } | ||
| } | ||
|
|
||
| private void OnStartup(EntityUid uid, MindStatusComponent component, ComponentStartup args) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of these should use the Entity<T> form as mentioned earlier.
| { | ||
| var oldStatus = status.Status; | ||
| var dead = _mobState.IsDead(uid); | ||
| var mind = container.Mind != null ? CompOrNull<MindComponent>(container.Mind) : null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| var mind = container.Mind != null ? CompOrNull<MindComponent>(container.Mind) : null; | |
| var mind = CompOrNull<MindComponent>(container.Mind); |
| var hasActiveSession = hasUserId != null && _playerManager.ValidSessionId(hasUserId.Value); | ||
|
|
||
| // Determine new status based on the scenarios | ||
| if (dead && hasUserId == null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you wanted to be super funky you could make this a switch expression over dead, hasUserId, and hasActiveSession and pattern match everything. (This is fine though.)
| [Dependency] private readonly IConfigurationManager _cfg = default!; | ||
| [Dependency] private readonly IGameTiming _timing = default!; | ||
| [Dependency] private readonly StatusEffectsSystem _statusEffects = default!; | ||
| [Dependency] private readonly EntityManager _entityManager = default!; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is already available as an EntitySystem field as just EntityManager.
| public sealed class ForceUpdateMindStatusEvent : EntityEventArgs | ||
| { | ||
| public EntityUid Entity { get; } | ||
|
|
||
| public ForceUpdateMindStatusEvent(EntityUid entity) | ||
| { | ||
| Entity = entity; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Structs are preferred:
| public sealed class ForceUpdateMindStatusEvent : EntityEventArgs | |
| { | |
| public EntityUid Entity { get; } | |
| public ForceUpdateMindStatusEvent(EntityUid entity) | |
| { | |
| Entity = entity; | |
| } | |
| } | |
| [ByRefEvent] | |
| public readonly record struct ForceUpdateMindStatusEvent(EntityUid Entity); |
| } | ||
|
|
||
| Dirty(uid, component); | ||
| _entityManager.EventBus.RaiseEvent(EventSource.Local, new ForceUpdateMindStatusEvent(uid)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same for other, but this should be:
| _entityManager.EventBus.RaiseEvent(EventSource.Local, new ForceUpdateMindStatusEvent(uid)); | |
| var ev = new ForceUpdateMindStatusEvent(uid); | |
| RaiseLocalEvent(uid, ref ev); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure this is the correct approach 🤔
Its pretty complex and I worry it'll eventually break
| namespace Content.Shared.Mind.Components; | ||
|
|
||
| [RegisterComponent, NetworkedComponent, AutoGenerateComponentState] | ||
| public sealed partial class MindStatusComponent : Component |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add comment
| DeadAndIrrecoverable | ||
| } | ||
|
|
||
| public sealed class ForceUpdateMindStatusEvent : EntityEventArgs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment
| MindStatus.Catatonic => Loc.GetString("comp-mind-examined-catatonic", ("ent", uid)), | ||
| MindStatus.SSD => Loc.GetString("comp-mind-examined-ssd", ("ent", uid)), | ||
| MindStatus.Active => null, // No special text for active players | ||
| _ => null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Throw an error here instead
| } | ||
| } | ||
|
|
||
| private static string GetColorForStatus(MindStatus status) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it makes more sense to put this with the LOC strings themselves
| } | ||
| } | ||
|
|
||
| public override void Update(float frameTime) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Put the update at the bottom of all the functions!
Also this function gives me sus vibes - does the system not work without it?
| SubscribeLocalEvent<MindContainerComponent, MindRemovedMessage>(OnMindRemoved); | ||
| SubscribeLocalEvent<ForceUpdateMindStatusEvent>(OnForceUpdate); | ||
|
|
||
| if (_net.IsServer) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In 90% of situations you shouldn't be using IsClient / IsServer, its usually best to just split the function into a server and client version. Do it like this:
In Shared make a SharedMindStatusSystem that is abstract. Put all subscriptions and functions that need to run in both client and shared there.
In Server make a MindStatusSystem that is NOT abstract. Extend the shared abstract system and then add any new functions subscriptions, etc... that can't be run in the client.
Repeat for Client but stuff that only the client can run not the server
| } | ||
|
|
||
| Dirty(uid, component); | ||
| _entityManager.EventBus.RaiseEvent(EventSource.Local, new ForceUpdateMindStatusEvent(uid)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use RaiseLocalEvent
Welp perry you sniped me... Double review double the fun I guess 😆 I wish github would give a big warning like: WARNING SOMEONE POSTED A REVIEW WHILE YOU WERE MAKING COMMENTS!! |
|
Huge thanks for the detailed review, a lot of stuff to unpack but already a big help in understanding Robust Toolbox practices, I really appreciate it. I had a simpler solution that I liked a lot more but it kept saying that I had a duplicate subscription that I just couldn't pinpoint so I went for this as a workaround last night and I can see all the problems with it now. Will probably not have another solution as my weekend is over now but I can take another shot at it later if it hasn't been addressed by then. Thanks again folks! |
yeah your PR is solid and def has the right core idea! If you see both of the comments on the same thing, it looks like perry has a more detailed answers so go with those |
I was able to implement yours and perry's suggestions for the most part, but in diving deeper and attempting to move away from the update loop, I'm noticing some inconsistencies between the current status descriptions (without any of my changes) and when they're actually used: "Catatonic" status - Current description is "{ent} is totally catatonic. The stresses of life in deep-space must have been too much for {ent}. Any recovery is impossible." When it's used - Living entities that have never had a mind (admin-spawned mobs, test dummies on test maps). The description says "recovery is impossible", but if these entities are often specifically spawned to be possessed by ghosts, players might examine them, see "recovery is impossible", then moments later see them walking around controlled by a player. Then there's the "Dead and Irrecoverable" status. Current description is "{ent} soul has departed and moved on. Any recovery is impossible." The issue is that it seems like this should apply to players who have died and rotted beyond revival, but an unrevivable player corpse still just shows as the "dead" or "dead and SSD" statuses rather than "dead and irrecoverable", even if they are truly irrecoverable at that point. I'm unable to determine when exactly "Dead and Irrecoverable" ever happens in its current state. I'm able to match the functionality of the current system (but with predicted text) using an update loop, but it's difficult to move to an event-driven update system because the actual application of these different statuses seems to have some issues. Or I'm just misunderstanding the intentions, which is totally possible! Can you clarify the intention of the "Catatonic" and "Dead and Irrecoverable" descriptions? |
About the PR
This PR refactors the way mind status text on examine is handled to use a client-predicted component.
Why / Balance
Removes the delay on SSD/death text populating when examining entities that is caused by waiting for the server to provide mind status information. This fix was asked for in the SharedMindSystem code by a TODO comment. See Media for comparison.
Technical details
Mind status OnExamine logic removed from SharedMindSystem and handled by a new component and systems - MindStatusComponent, MindExamineSystem, and MindStatusSystem. Tested on localhost and no issues found, but you never know. Might contain some strange or unnecessary code, I'm coming from a C++ background, so feel free to ask for revisions.
Media
mind-status-examine0001-0563.mp4
Requirements
Breaking changes
Content.Shared.SharedMindSystem.cs - Removed OnExamine method and refactored into MindStatusSystem.cs
Changelog
🆑