Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 36 additions & 7 deletions Content.Client/Examine/ExamineSystem.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Linq;
using System.Numerics;
using System.Threading;
using Content.Client._Scp.Knowledge; // Fire added - client-side SCP knowledge examine prediction
using Content.Client.Verbs;
using Content.Shared.Examine;
using Content.Shared.IdentityManagement;
Expand Down Expand Up @@ -32,6 +33,8 @@ public sealed class ExamineSystem : ExamineSystemShared
[Dependency] private readonly VerbSystem _verbSystem = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;

[Dependency] private readonly ScpKnowledgeSystem _scpKnowledge = default!; // Fire added - predict knowledge-gated examine identity and hint buttons

private List<Verb> _verbList = new();

public const string StyleClassEntityTooltip = "entity-tooltip";
Expand Down Expand Up @@ -158,15 +161,16 @@ private void OnExamineInfoResponse(ExamineSystemMessages.ExamineInfoResponseMess
// since there's probably one open already if it's coming in from the server.
var entity = GetEntity(ev.EntityUid);

// Fire added start
// Fire added start - ignore stale examine replies for entities that disappeared before the server answered
if (TerminatingOrDeleted(entity))
{
CloseTooltip();
return;
}
// Fire added end

OpenTooltip(player.Value, entity, ev.CenterAtCursor, ev.OpenAtOldTooltip, ev.KnowTarget);
// Fire edit - apply server-provided knowledge-based name override to the tooltip title
OpenTooltip(player.Value, entity, ev.CenterAtCursor, ev.OpenAtOldTooltip, ev.KnowTarget, ev.NameOverride);
UpdateTooltipInfo(player.Value, entity, ev.Message, ev.Verbs, getVerbs: false);
}

Expand All @@ -181,7 +185,7 @@ public override void SendExamineTooltip(EntityUid player, EntityUid target, Form
/// not fill it with information. This is done when the server sends examine info/verbs,
/// or immediately if it's entirely clientside.
/// </summary>
public void OpenTooltip(EntityUid player, EntityUid target, bool centeredOnCursor=true, bool openAtOldTooltip=true, bool knowTarget = true)
public void OpenTooltip(EntityUid player, EntityUid target, bool centeredOnCursor=true, bool openAtOldTooltip=true, bool knowTarget = true, string? nameOverride = null) // Fire edit - allow knowledge systems to override the displayed examine name
{
// Close any examine tooltip that might already be opened
// Before we do that, save its position. We'll prioritize opening any new popups there if
Expand Down Expand Up @@ -246,7 +250,8 @@ public void OpenTooltip(EntityUid player, EntityUid target, bool centeredOnCurso

if (knowTarget)
{
var itemName = FormattedMessage.EscapeText(Identity.Name(target, EntityManager, player));
// Fire edit - prefer knowledge-gated identity override when examine prediction/server response provides one
var itemName = FormattedMessage.EscapeText(nameOverride ?? Identity.Name(target, EntityManager, player));
var labelMessage = FormattedMessage.FromMarkupPermissive($"[bold]{itemName}[/bold]");
var label = new RichTextLabel();
label.SetMessage(labelMessage);
Expand Down Expand Up @@ -411,14 +416,38 @@ public void DoExamine(EntityUid entity, bool centeredOnCursor = true, EntityUid?
return;

FormattedMessage message;
var knowTarget = true;
string? nameOverride = null;
List<Verb>? predictedVerbs = null; // Fire added - predicted OOC knowledge hint buttons

// Fire added start - predict SCP knowledge-gated examine locally
if (_scpKnowledge.TryGetPredictedExamineData(entity, out var predictedKnowTarget, out var predictedNameOverride, out var predictedMessage))
{
knowTarget = predictedKnowTarget;
nameOverride = predictedNameOverride;
message = predictedMessage ?? GetExamineText(entity, playerEnt);
}
else
{
message = GetExamineText(entity, playerEnt);
}
// Fire added end

// Fire added start - predict SCP knowledge examine verbs locally
predictedVerbs = new List<Verb>();
_scpKnowledge.AddPredictedExamineVerbs(playerEnt.Value, entity, predictedVerbs);
if (predictedVerbs.Count == 0)
predictedVerbs = null;
// Fire added end
Comment on lines +436 to +441

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Избыточная аллокация списка для предсказанных глаголов.

Список создаётся всегда, даже если AddPredictedExamineVerbs не добавит ни одного элемента. Можно избежать аллокации, используя out параметр или проверяя наличие глаголов до создания списка.

♻️ Предлагаемое исправление
-            // Fire added start - predict SCP knowledge examine verbs locally
-            predictedVerbs = new List<Verb>();
-            _scpKnowledge.AddPredictedExamineVerbs(playerEnt.Value, entity, predictedVerbs);
-            if (predictedVerbs.Count == 0)
-                predictedVerbs = null;
-            // Fire added end
+            // Fire added start - predict SCP knowledge examine verbs locally
+            if (_scpKnowledge.TryGetPredictedExamineVerbs(playerEnt.Value, entity, out predictedVerbs) && predictedVerbs.Count == 0)
+                predictedVerbs = null;
+            // Fire added end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Content.Client/Examine/ExamineSystem.cs` around lines 436 - 441, Avoid always
allocating predictedVerbs by changing the call pattern with
_scpKnowledge.AddPredictedExamineVerbs: either modify AddPredictedExamineVerbs
to return a bool and an out List<Verb> (or return IEnumerable<Verb>/nullable
list) so the list is only created when there are verbs, or add a pre-check API
on _scpKnowledge (e.g. HasPredictedExamineVerbs) and only instantiate
predictedVerbs when that check returns true; update the call site in
ExamineSystem (predictedVerbs variable and the AddPredictedExamineVerbs
invocation) accordingly so you no longer allocate an empty List<Verb>
unconditionally.


OpenTooltip(playerEnt.Value, entity, centeredOnCursor, false);
// Fire edit start - apply predicted SCP identity and hint-button overrides before the server reply arrives
OpenTooltip(playerEnt.Value, entity, centeredOnCursor, false, knowTarget, nameOverride);

// Always update tooltip info from client first.
// If we get it wrong, server will correct us later anyway.
// This will usually be correct (barring server-only components, which generally only adds, not replaces text)
message = GetExamineText(entity, playerEnt);
UpdateTooltipInfo(playerEnt.Value, entity, message);
UpdateTooltipInfo(playerEnt.Value, entity, message, predictedVerbs);
// Fire edit end

if (!IsClientSide(entity))
{
Expand Down
30 changes: 29 additions & 1 deletion Content.Client/Paper/UI/PaperBoundUserInterface.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public sealed class PaperBoundUserInterface : BoundUserInterface
[ViewVariables]
private PaperWindow? _window;

private PaperKnowledgeHighlightMessage? _highlightMessage; // Fire added - cache server-provided knowledge highlights between UI messages

public PaperBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
Expand All @@ -37,8 +39,34 @@ protected override void Open()
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
_window?.Populate((PaperBoundUserInterfaceState) state);

// Fire added start - reuse the server-provided highlighted paper text when it still matches the current state
var paperState = (PaperBoundUserInterfaceState) state;
string? highlightedText = null;

if (_highlightMessage != null && paperState.Text == _highlightMessage.RawText)
highlightedText = _highlightMessage.HighlightedText;
else
_highlightMessage = null;
// Fire added end

_window?.Populate(paperState, highlightedText); // Fire edit - pass highlighted paper text for knowledge hint rendering
}

// Fire added start - receive out-of-band paper knowledge highlight updates without waiting for a full state resend
protected override void ReceiveMessage(BoundUserInterfaceMessage message)
{
base.ReceiveMessage(message);

if (message is not PaperKnowledgeHighlightMessage highlight)
return;

_highlightMessage = highlight;

if (State is PaperBoundUserInterfaceState state)
_window?.Populate(state, state.Text == highlight.RawText ? highlight.HighlightedText : null);
}
// Fire added end

private void InputOnTextEntered(string text)
{
Expand Down
4 changes: 2 additions & 2 deletions Content.Client/Paper/UI/PaperWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -246,15 +246,15 @@ protected override void Draw(DrawingHandleScreen handle)
/// Initialize the paper contents, i.e. the text typed by the
/// user and any stamps that have peen put on the page.
/// </summary>
public void Populate(PaperComponent.PaperBoundUserInterfaceState state)
public void Populate(PaperComponent.PaperBoundUserInterfaceState state, string? highlightedText = null) // Fire edit - allow paper knowledge highlights to override rendered text
{
bool isEditing = state.Mode == PaperComponent.PaperAction.Write;
bool wasEditing = InputContainer.Visible;
InputContainer.Visible = isEditing;
EditButtons.Visible = isEditing;

var msg = new FormattedMessage();
msg.AddMarkupPermissive(state.Text);
msg.AddMarkupPermissive(highlightedText ?? state.Text); // Fire edit - render highlighted paper text when knowledge hints are available

// For premade documents, we want to be able to edit them rather than
// replace them.
Expand Down
149 changes: 149 additions & 0 deletions Content.Client/_Scp/Knowledge/ScpKnowledgeSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
using System.Diagnostics.CodeAnalysis;
using Content.Client.Examine;
using Content.Shared._Scp.Knowledge;
using Content.Shared._Scp.Knowledge.Components;
using Content.Shared.Examine;
using Content.Shared.Verbs;
using Robust.Client.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;

namespace Content.Client._Scp.Knowledge;

public sealed class ScpKnowledgeSystem : EntitySystem
{
[Dependency] private readonly ExamineSystem _examine = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;

private readonly Dictionary<EntProtoId, List<ProtoId<ScpKnowledgePrototype>>> _knowledgeByEntityPrototype = new();

public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);

RebuildCaches();
}

public bool TryGetPredictedExamineData(
EntityUid target,
out bool knowTarget,
out string? nameOverride,
out FormattedMessage? message)
{
knowTarget = true;
nameOverride = null;
message = null;

if (!TryGetLocalKnowledgeState(out var knowledgeState) ||
!TryGetKnowledgeIdsForEntity(target, out var knowledgeIds))
{
return false;
}

var requiresKnowledge = false;
for (var i = 0; i < knowledgeIds.Count; i++)
{
var knowledgeId = knowledgeIds[i];
var knowledge = _prototype.Index(knowledgeId);
if (!knowledge.HideIdentityUntilKnown)
continue;

requiresKnowledge = true;
if (!ScpKnowledgeLogic.WillBeKnownAfterExamine(knowledgeState, knowledgeId, knowledge))
continue;

nameOverride = Loc.GetString(knowledge.DisplayName);
return true;
}

if (!requiresKnowledge)
return false;

knowTarget = false;
message = new FormattedMessage();
message.AddText(Loc.GetString("scp-knowledge-unknown-examine"));
return true;
}

public void AddPredictedExamineVerbs(EntityUid user, EntityUid target, List<Verb> verbs)
{
if (_player.LocalEntity != user ||
!TryComp(user, out ScpKnowledgeComponent? knowledgeState) ||
!TryGetKnowledgeIdsForEntity(target, out var knowledgeIds))
{
return;
}

for (var i = 0; i < knowledgeIds.Count; i++)
{
var knowledgeId = knowledgeIds[i];
var knowledge = _prototype.Index(knowledgeId);
if (!ScpKnowledgeLogic.WillBeKnownAfterExamine(knowledgeState, knowledgeId, knowledge) ||
knowledge.KnownExamineVerbText == null ||
knowledge.KnownExamineText == null)
{
continue;
}

var examineMessage = new FormattedMessage();
examineMessage.AddText(Loc.GetString(knowledge.KnownExamineText.Value));

var examineVerb = new ExamineVerb
{
Act = () => _examine.SendExamineTooltip(user, target, examineMessage, false, false),
Text = Loc.GetString(knowledge.KnownExamineVerbText.Value),
Category = VerbCategory.Examine,
Icon = new SpriteSpecifier.Texture(new(ExamineSystemShared.DefaultIconTexture)),
ClientExclusive = true,
};

verbs.Add(examineVerb);
}
}

private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
{
if (!args.WasModified<ScpKnowledgePrototype>())
return;

RebuildCaches();
}

private bool TryGetLocalKnowledgeState([NotNullWhen(true)] out ScpKnowledgeComponent? knowledgeState)
{
knowledgeState = null;
return _player.LocalEntity is { } localEntity && TryComp(localEntity, out knowledgeState);
}

private void RebuildCaches()
{
_knowledgeByEntityPrototype.Clear();

foreach (var knowledge in _prototype.EnumeratePrototypes<ScpKnowledgePrototype>())
{
for (var i = 0; i < knowledge.EntityPrototypes.Count; i++)
{
var entityPrototype = knowledge.EntityPrototypes[i];
if (!_knowledgeByEntityPrototype.TryGetValue(entityPrototype, out var knowledgeIds))
{
knowledgeIds = [];
_knowledgeByEntityPrototype[entityPrototype] = knowledgeIds;
}

knowledgeIds.Add(knowledge.ID);
}
}
}

private bool TryGetKnowledgeIdsForEntity(
EntityUid target,
[NotNullWhen(true)] out List<ProtoId<ScpKnowledgePrototype>>? knowledgeIds)
{
knowledgeIds = null;
var prototypeId = Prototype(target)?.ID;
return prototypeId != null && _knowledgeByEntityPrototype.TryGetValue(prototypeId, out knowledgeIds);
}
}
Loading
Loading