fix(ui): bounds-guard out-of-range list access in pop-out and tabs UI

Two pre-existing upstream defects fixed in v1.0.0:

- Ui/Popout.cs PopOutDocked[Idx] now bounds-checks Idx against
  ChatLogWindow.PopOutDocked.Count before reading or writing. A
  popout instance can outlive a list resize when AddPopOutsToDraw()
  rebuilds the docked-state list while a draw frame is in flight,
  which previously produced an out-of-range crash on tab drop
- Ui/SettingsTabs/Tabs.cs guards against an empty worlds list before
  indexing worlds[selectedWorld]. Empty lists can occur briefly when
  switching characters or before the datacenter sheet finishes
  loading — the previous code would crash with an
  ArgumentOutOfRangeException
This commit is contained in:
2026-05-03 22:04:45 +02:00
parent a651b3b9ad
commit 6d49dbad3e
2 changed files with 34 additions and 19 deletions
+6 -2
View File
@@ -80,7 +80,10 @@ internal class Popout : Window
if (!Tab.CanResize) if (!Tab.CanResize)
Flags |= ImGuiWindowFlags.NoResize; Flags |= ImGuiWindowFlags.NoResize;
if (!ChatLogWindow.PopOutDocked[Idx]) // Idx may point past the end if PopOutDocked was resized (e.g., a tab
// dropped) between the AddPopOutsToDraw() snapshot and this frame.
// Guard the read so we don't index into stale state.
if (Idx >= 0 && Idx < ChatLogWindow.PopOutDocked.Count && !ChatLogWindow.PopOutDocked[Idx])
{ {
if (Tab.IndependentOpacity) if (Tab.IndependentOpacity)
{ {
@@ -195,7 +198,8 @@ internal class Popout : Window
public override void PostDraw() public override void PostDraw()
{ {
ChatLogWindow.PopOutDocked[Idx] = ImGui.IsWindowDocked(); if (Idx >= 0 && Idx < ChatLogWindow.PopOutDocked.Count)
ChatLogWindow.PopOutDocked[Idx] = ImGui.IsWindowDocked();
if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null }) if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null })
StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == Plugin.Config.ChosenStyle)?.Pop(); StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == Plugin.Config.ChosenStyle)?.Pop();
+28 -17
View File
@@ -181,28 +181,39 @@ internal sealed class Tabs : ISettingsTab
ImGui.SameLine(); ImGui.SameLine();
var selectedWorld = worlds.FindIndex(world => world.RowId == tab.TellTarget.World); // Guard against an empty worlds list — can happen briefly
if (selectedWorld == -1) // when switching characters or if the datacenter sheet
selectedWorld = 0; // has not yet populated. Without the guard the indexed
// access into worlds[selectedWorld] would crash.
using (var combo = ImRaii.Combo("###player-world", worlds[selectedWorld].Name.ToString())) if (worlds.Count == 0)
{ {
if (combo.Success) ImGui.TextDisabled("(no worlds available)");
}
else
{
var selectedWorld = worlds.FindIndex(world => world.RowId == tab.TellTarget.World);
if (selectedWorld == -1)
selectedWorld = 0;
using (var combo = ImRaii.Combo("###player-world", worlds[selectedWorld].Name.ToString()))
{ {
var lastDc = worlds.First().DataCenter.RowId; if (combo.Success)
foreach (var (idx, world) in worlds.Index())
{ {
if (ImGui.Selectable(world.Name.ToString(), selectedWorld == idx)) var lastDc = worlds.First().DataCenter.RowId;
foreach (var (idx, world) in worlds.Index())
{ {
selectedWorld = idx; if (ImGui.Selectable(world.Name.ToString(), selectedWorld == idx))
tab.TellTarget.World = worlds[selectedWorld].RowId; {
selectedWorld = idx;
tab.TellTarget.World = worlds[selectedWorld].RowId;
}
if (lastDc == world.DataCenter.RowId)
continue;
lastDc = world.DataCenter.RowId;
ImGui.Separator();
} }
if (lastDc == world.DataCenter.RowId)
continue;
lastDc = world.DataCenter.RowId;
ImGui.Separator();
} }
} }
} }