Skip to content
Merged
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
37 changes: 37 additions & 0 deletions S1API.Tests/Entities/DealerLifecyclePolicyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,43 @@ public void DealerHomeEventNeverAliasesAConsumerScheduleAction(string name, bool
Assert.Equal(expected, NPCPrefabBuilder.IsDealerHomeEventName(name));
}

[Theory]
[InlineData(false, true, 0, 0, 0, true)]
[InlineData(false, false, 0, 0, 0, false)]
[InlineData(true, true, 0, 0, 0, false)]
[InlineData(false, true, 1, 0, 0, false)]
[InlineData(false, true, 0, 1, 0, false)]
[InlineData(false, true, 0, 0, 1, false)]
public void OnlyCreatedEmptyLockedDealerConversationsAreHidden(
bool relationshipUnlocked,
bool uiCreated,
int messageCount,
int messageChainCount,
int responseCount,
bool expected)
{
Assert.Equal(
expected,
NPCDealer.ShouldHideLockedConversation(
relationshipUnlocked,
uiCreated,
messageCount,
messageChainCount,
responseCount));
}

[Theory]
[InlineData(false, false)]
[InlineData(true, true)]
public void DealerConversationUiIsCreatedOnlyForUnlockedRelationships(
bool relationshipUnlocked,
bool expected)
{
Assert.Equal(
expected,
NPCDealer.ShouldEnsureConversationUi(relationshipUnlocked));
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
[Fact]
public void ConnectionIdsAreStableAcrossSpawnOrderReconciliation()
{
Expand Down
1 change: 1 addition & 0 deletions S1API/Entities/NPC.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5014,6 +5014,7 @@ internal void CleanupRuntimeHooks()
{
ClearDealerRecommendationHooks();
CleanupAwarenessEventHooks();
_dealer?.Cleanup();
_messaging?.Cleanup();
CleanupVehicleLifecycleHooks();
}
Expand Down
110 changes: 109 additions & 1 deletion S1API/Entities/NPCDealer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ public sealed class NPCDealer
private readonly ManagedEventRegistrationTracker<NativeDealerRecruitedAction> _dealerRecruitedHandlers = new ManagedEventRegistrationTracker<NativeDealerRecruitedAction>();
private Action? _contractAcceptedHandlers;
private bool _contractAcceptedHooked;
private Action<NPCRelationship.UnlockType, bool>? _relationshipUnlockedHandler;
private S1Messaging.MSGConversation? _conversationUiRefreshHooked;

internal NPCDealer(NPC npc)
{
Expand Down Expand Up @@ -138,10 +140,16 @@ private void EnsureDealerCategory()
{
try
{
NPC.SetConversationCategory(S1Messaging.EConversationCategory.Dealer);
bool isUnlocked = NPC.Relationship.IsUnlocked;
NPC.SetConversationCategory(
S1Messaging.EConversationCategory.Dealer,
ensureUi: ShouldEnsureConversationUi(isUnlocked));
EnsureRelationshipUnlockHook();

if (NPC.S1NPC.MSGConversation != null)
{
TryHookConversationUIRefresh(NPC.S1NPC.MSGConversation);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
HideLockedEmptyConversation();
RefreshDealerCategoryBadge();
}
}
Expand Down Expand Up @@ -171,11 +179,15 @@ private void TryHookConversationUIRefresh(object convoObj)
RefreshDealerCategoryBadge();
}

if (ReferenceEquals(_conversationUiRefreshHooked, convo))
return;

// Hook onLoaded (called after UI is loaded from save)
var prevLoaded = convo.onLoaded;
convo.onLoaded = new System.Action(() =>
{
try { prevLoaded?.Invoke(); } catch { }
HideLockedEmptyConversation();
RefreshDealerCategoryBadge();
});

Expand All @@ -186,13 +198,109 @@ private void TryHookConversationUIRefresh(object convoObj)
try { prevOpened?.Invoke(); } catch { }
RefreshDealerCategoryBadge();
});

_conversationUiRefreshHooked = convo;
}
catch (Exception ex)
{
Logger.Warning($"Exception in TryHookConversationUIRefresh: {ex.Message}");
}
}

private void EnsureRelationshipUnlockHook()
{
_relationshipUnlockedHandler ??= (_, _) => ShowConversationAfterUnlock();

NPC.Relationship.OnUnlocked -= _relationshipUnlockedHandler;
NPC.Relationship.OnUnlocked += _relationshipUnlockedHandler;
}

private void ShowConversationAfterUnlock()
{
try
{
NPC.SetConversationCategory(
S1Messaging.EConversationCategory.Dealer,
ensureUi: true);

var conversation = NPC.S1NPC.MSGConversation;
if (conversation == null)
return;

conversation.SetIsKnown(true);
conversation.SetEntryVisibility(true);
TryHookConversationUIRefresh(conversation);
RefreshDealerCategoryBadge();
}
catch (Exception ex)
{
Logger.Warning($"Exception showing dealer conversation after unlock for {NPC.ID}: {ex.Message}");
}
}

private void HideLockedEmptyConversation()
{
var conversation = NPC.S1NPC.MSGConversation;
if (conversation == null)
return;

bool uiCreated =
Internal.Utils.ReflectionUtils.TryGetFieldOrProperty(conversation, "uiCreated") is bool created
&& created;
int messageCount = conversation.messageHistory?.Count ?? 0;
int messageChainCount = conversation.messageChainHistory?.Count ?? 0;
int responseCount = conversation.currentResponses?.Count ?? 0;
if (!ShouldHideLockedConversation(
NPC.Relationship.IsUnlocked,
uiCreated,
messageCount,
messageChainCount,
responseCount))
{
return;
}

bool conversationCanBeHidden = NPC.ConversationCanBeHidden;
try
{
if (!conversationCanBeHidden)
NPC.ConversationCanBeHidden = true;

conversation.SetEntryVisibility(false);
}
finally
{
if (!conversationCanBeHidden)
NPC.ConversationCanBeHidden = false;
}
}

internal static bool ShouldHideLockedConversation(
bool relationshipUnlocked,
bool uiCreated,
int messageCount,
int messageChainCount,
int responseCount) =>
!relationshipUnlocked
&& uiCreated
&& messageCount == 0
&& messageChainCount == 0
&& responseCount == 0;

internal static bool ShouldEnsureConversationUi(bool relationshipUnlocked) =>
relationshipUnlocked;

internal void Cleanup()
{
if (_relationshipUnlockedHandler != null)
{
NPC.Relationship.OnUnlocked -= _relationshipUnlockedHandler;
_relationshipUnlockedHandler = null;
}

_conversationUiRefreshHooked = null;
}

/// <summary>
/// Refresh the category badge on the existing conversation entry to ensure Dealer icon/label/color are shown.
/// Mirrors MessagesApp.CreateConversationUI category setup.
Expand Down
Loading