Skip to content

Add custom checkbox style, refactor to avoid async issues#367

Merged
dimon222 merged 8 commits intoWFCD:masterfrom
dimon222:bugfix/custom_checkbox_race
Aug 26, 2025
Merged

Add custom checkbox style, refactor to avoid async issues#367
dimon222 merged 8 commits intoWFCD:masterfrom
dimon222:bugfix/custom_checkbox_race

Conversation

@dimon222
Copy link
Member

@dimon222 dimon222 commented Aug 26, 2025

Corrections of bugs identified in 9.6.6

@coderabbitai

Find the reason why "Load missing market item: " seem to trigger on almost every single item. What am I missing?

Summary by CodeRabbit

  • New Features

    • Locale-aware market item naming and refreshed market data with version/timestamp updates; UI shows verified timestamps when available.
  • Bug Fixes

    • Reduced race conditions, improved startup and websocket stability, more reliable error popups and UI state during market refreshes.
  • Refactor

    • Core data loading and refresh flows converted to async for smoother, non-blocking updates and atomic data swaps.
  • Style

    • New explicit CheckBox visual style ("checkBoxStyle"); implicit CheckBox styling removed (apply explicitly to use).
  • Other

    • App version bumped to 9.6.7.0.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 26, 2025

Walkthrough

Converts core market-data and startup flows to async/await, introduces a lock-and-atomic-swap pattern for marketItems, makes several Data methods return Task, updates startup/UI sequencing and websocket startup, and replaces the implicit CheckBox style with an explicit keyed checkBoxStyle. All changes include added null checks and improved logging.

Changes

Cohort / File(s) Summary
Data: async, thread-safety, market logic
WFInfo/Data.cs
ReloadItems()Task; LoadMarket(...)Task<bool>; Update()Task<bool>; ForceMarketUpdate()Task. Adds marketItemsLock and atomic-swap of marketItems; two-pass prime filtering (name then locale via slug); defers timestamp/version until async work finishes; adds null checks, guarded enumerations, and enhanced logging.
App init & startup changes
WFInfo/Main.cs, WFInfo/MainWindow.xaml.cs
ThreadedDataLoad()async void; await dataBase.Update(); ensure LoggedIn() runs on UI thread; replace Task.Factory.StartNew(Main.dataBase.ForceMarketUpdate) with Task.Run(async () => await Main.dataBase.ForceMarketUpdate()); remove previous FinishedLoading/OpenWebSocket startup call; adjust error handling to use StatusUpdate + SpawnErrorPopup.
Styles / Settings UI
WFInfo/Templates/Styles.xaml, WFInfo/Settings/SettingsWindow.xaml
Remove implicit CheckBox style; add explicit checkBoxStyle ControlTemplate (custom bullet, Path checkmark, ContentPresenter, triggers). SettingsWindow updated to base on checkBoxStyle and removed Background/BorderBrush setters.
Locale change handling / blocking in background
WFInfo/Settings/SettingsWindow.xaml.cs
Locale selection now starts a background Task.Run that calls Main.dataBase.ReloadItems().Wait() (synchronous wait inside task). Minor formatting/comment edits; locale save and OCR update unchanged.
Assembly version
WFInfo/Properties/AssemblyInfo.cs
Bump AssemblyVersion and AssemblyFileVersion from 9.6.6.09.6.7.0.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant App as Main.ThreadedDataLoad
  participant Data as WFInfo.Data
  participant WFM as Warframe.Market API
  participant Sheets as Sheets API
  participant UI as MainWindow

  User->>App: start app
  App->>Data: await Update()
  activate Data
  Data->>Sheets: fetch items JSON (sync/async)
  Sheets-->>Data: items JSON
  Data->>WFM: await ReloadItems()/LoadMarket(...) (async)
  WFM-->>Data: market JSON / locale names
  Note over Data: ReloadItems builds tempMarketItems\npass1 filter by " Prime" & types\npass2 append locale names via slug
  Data->>Data: lock(marketItemsLock)\natomic swap: marketItems = tempMarketItems
  deactivate Data
  App->>UI: RunOnUIThread(LoggedIn) [if JWT valid]
  UI->>Data: Task.Run(async => await ForceMarketUpdate())
  activate Data
  Data->>WFM: load missing prime pages (LoadMarketItem)
  Data-->>UI: completion / RefreshMarketDucats / save
  deactivate Data
Loading
sequenceDiagram
  autonumber
  actor User
  participant Settings as SettingsWindow
  participant Data as WFInfo.Data

  User->>Settings: change locale
  Settings->>Settings: Task.Run(...)  /* background task */
  activate Settings
  Settings->>Data: ReloadItems().Wait()  /* sync wait inside background task */
  Note over Data: async ReloadItems builds tempMarketItems\nthen swaps under marketItemsLock
  Data-->>Settings: completed
  deactivate Settings
  Settings->>Settings: Save settings + OCR update
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

I hopped through code with nimble paws,
Locked the list to mend race flaws.
Async threads now hum in line,
Primes find names by slug and time.
Checkboxes dress up — rabbit sighs. 🥕

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 46af89b and bb3cf4b.

📒 Files selected for processing (1)
  • WFInfo/Properties/AssemblyInfo.cs (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • WFInfo/Properties/AssemblyInfo.cs
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
WFInfo/Data.cs (2)

506-507: Preserve prior save flag when combining results

Overwriting saveDatabases with LoadEqmtData’s return can drop a prior true, preventing SaveAllJSONs even though market data changed.

-            saveDatabases = LoadEqmtData(allFiltered, saveDatabases);
+            saveDatabases = LoadEqmtData(allFiltered, saveDatabases) || saveDatabases;

2091-2106: GetUrlName never matches items lacking a locale token

The current check requires split length > 2, i.e., expecting [name|slug|localeName]. Items without a third token will never match and will throw eventually.

-                        if (vals.Length > 2 && vals[0].Equals(primeName, StringComparison.OrdinalIgnoreCase))
+                        if (vals.Length >= 2 && vals[0].Equals(primeName, StringComparison.OrdinalIgnoreCase))
                         {
                             return vals[1];
                         }
WFInfo/Main.cs (1)

146-176: Fix initializer in ThreadedDataLoad: switch to Task, await properly, call Main.LoggedIn, and signal completion

To restore AFK-timer startup, fire subscribers, and improve error handling, apply these changes:

• Change the signature to return a Task instead of async void and rename accordingly.
• Await the JWT check instead of using .Result to avoid deadlocks.
• On successful login, invoke the static Main.LoggedIn (not the MainWindow method) so that any side-effects (AFK timer, services) run.
• After all initialization steps complete, call FinishedLoading() so subscribers registered via RegisterOnFinishedLoading(...) fire.
• At the call site, replace the Task.Factory.StartNew invocation with Task.Run(...) and discard its result.

Example diff in WFInfo/Main.cs:

-    public async void ThreadedDataLoad()
+    private async Task ThreadedDataLoadAsync()
     {
         try
         {
             StatusUpdate("Initializing OCR engine...", 0);
             OCR.Init(...);
 
             StatusUpdate("Updating Databases...", 0);
-            await dataBase.Update();
+            // Run DB calls off the UI thread
+            await dataBase.Update().ConfigureAwait(false);
 
             if (ApplicationSettings.GlobalReadonlySettings.Auto)
                 dataBase.EnableLogCapture();
-            if (dataBase.IsJWTvalid().Result)
+            if (await dataBase.IsJWTvalid().ConfigureAwait(false))
             {
-                RunOnUIThread(() => MainWindow.INSTANCE.LoggedIn());
+                // Switch to UI, start AFK timer, etc.
+                RunOnUIThread(Main.LoggedIn);
             }
 
             StatusUpdate("WFInfo Initialization Complete", 0);
             AddLog("WFInfo has launched successfully");
+            // Let registered subscribers know we’re ready
+            FinishedLoading();
         }
         catch (Exception e)
         {
-            StatusUpdate("WFInfo Failed to Initialize", 0);
+            StatusUpdate("WFInfo failed to initialize — see debug.log for details", 1);
             AddLog(e.ToString());
             SpawnErrorPopup(DateTime.UtcNow, 1800);
         }
     }

Then update the startup call in the constructor:

-    Task.Factory.StartNew(ThreadedDataLoad);
+    _ = Task.Run(ThreadedDataLoadAsync);

• Verify that any code in MainWindow.xaml.cs or other services using RegisterOnFinishedLoading(...) now runs their handlers (e.g. your AFK timer starts when a valid JWT exists).
• Ensure there are no other call sites to ThreadedDataLoad left unmodified.

🧹 Nitpick comments (9)
WFInfo/Templates/Styles.xaml (2)

14-61: Checkbox template: cover Indeterminate state, align colors, and improve checkmark rendering

  • The template doesn’t handle IsChecked={x:Null} (three-state). This will render no visual for Indeterminate checkboxes.
  • Foreground default is hardcoded to #000 which clashes with the app’s baseStyle text color (#FFB1D0D9).
  • The checkmark path would look cleaner with rounded joins/caps.

Apply the following tweaks:

 <Style x:Key="checkBoxStyle" TargetType="CheckBox">
-    <Setter Property="Foreground" Value="#000"/>
+    <Setter Property="Foreground" Value="#FFB1D0D9"/>
@@
-                            <Path x:Name="CheckMark"
+                            <Path x:Name="CheckMark"
                               Data="M 0 4 L 4 8 L 12 0"
                               Stroke="{TemplateBinding Foreground}"
                               StrokeThickness="2"
                               Visibility="Collapsed"
                               SnapsToDevicePixels="True"
-                              Stretch="Uniform" />
+                              Stretch="Uniform"
+                              StrokeLineJoin="Round"
+                              StrokeStartLineCap="Round"
+                              StrokeEndLineCap="Round" />
@@
                     <ControlTemplate.Triggers>
                         <Trigger Property="IsChecked" Value="True">
                             <Setter TargetName="CheckMark" Property="Visibility" Value="Visible"/>
                         </Trigger>
+                        <Trigger Property="IsChecked" Value="{x:Null}">
+                            <!-- Show a horizontal dash for indeterminate -->
+                            <Setter TargetName="CheckMark" Property="Visibility" Value="Visible"/>
+                            <Setter TargetName="CheckMark" Property="Data" Value="M 2 8 L 14 8"/>
+                        </Trigger>

Additionally consider adding FocusVisualStyle for keyboard accessibility (consistent with FocusVisual defined later in this file).


14-61: Keyed style only: verify app-wide usage or provide an implicit alias

Switching from an implicit CheckBox style to a keyed one means any CheckBox not explicitly referencing checkBoxStyle (or not BasedOn an implicit wrapper) will lose custom visuals. If that’s not intended, add an implicit alias:

+    <!-- Preserve app-wide checkbox visuals via an implicit alias -->
+    <Style TargetType="CheckBox" BasedOn="{StaticResource checkBoxStyle}"/>

If the intent is to scope visuals only to certain views, ignore this. Just ensure all intended views set BasedOn or Style accordingly.

WFInfo/Data.cs (3)

186-190: Avoid clearing shared marketItems before the atomic swap

You already build tempMarketItems and swap under lock later. Clearing marketItems at the start introduces a brief window where readers can observe an empty object (if they don’t take the same lock). Remove the early clear.

-            lock (marketItemsLock)
-            {
-                marketItems = new JObject();
-            }

320-364: Modernize LoadMarketItem to async HttpClient and remove Thread.Sleep

Two Thread.Sleep(333) calls and synchronous WebClient usage block threads and hamper throughput. Since the calling flows are already async, prefer HttpClient with awaits and Task.Delay for respectful pacing and better error handling.

Proposed async version (requires updating callers to await, or introduce a throttled queue):

-private void LoadMarketItem(string item_name, string url)
+private async Task LoadMarketItemAsync(string item_name, string url, CancellationToken ct = default)
 {
-    Main.AddLog("Load missing market item: " + item_name);
-
-    Thread.Sleep(333);
-    WebClient webClient = CreateWfmClient();
-    JObject stats =
-        JsonConvert.DeserializeObject<JObject>(
-            webClient.DownloadString("https://api.warframe.market/v1/items/" + url + "/statistics"));
+    Main.AddLog("Load missing market item: " + item_name);
+    await Task.Delay(333, ct).ConfigureAwait(false);
+    using var requestStats = new HttpRequestMessage(HttpMethod.Get, $"https://api.warframe.market/v1/items/{url}/statistics");
+    requestStats.Headers.Add("platform", "pc");
+    requestStats.Headers.Add("language", "en");
+    requestStats.Headers.Add("accept", "application/json");
+    var statsResp = await client.SendAsync(requestStats, ct).ConfigureAwait(false);
+    statsResp.EnsureSuccessStatusCode();
+    var statsJson = await statsResp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
+    JObject stats = JsonConvert.DeserializeObject<JObject>(statsJson);
@@
-    Thread.Sleep(333);
-    webClient = CreateWfmClient();
-    JObject responseJObject = JsonConvert.DeserializeObject<JObject>(
-        webClient.DownloadString("https://api.warframe.market/v2/item/" + url)
-    );
+    await Task.Delay(333, ct).ConfigureAwait(false);
+    using var requestItem = new HttpRequestMessage(HttpMethod.Get, $"https://api.warframe.market/v2/item/{url}");
+    requestItem.Headers.Add("platform", "pc");
+    requestItem.Headers.Add("language", "en");
+    requestItem.Headers.Add("accept", "application/json");
+    var itemResp = await client.SendAsync(requestItem, ct).ConfigureAwait(false);
+    itemResp.EnsureSuccessStatusCode();
+    var itemJson = await itemResp.Content.ReadAsStringAsync(ct).ConfigureAwait(false);
+    JObject responseJObject = JsonConvert.DeserializeObject<JObject>(itemJson);

If changing signatures now is too invasive, consider queuing work items and processing them in a single background worker to avoid blocking within locks.


320-364: Throttle or downgrade noisy “Load missing market item” logs

Once the mismatched-key issue is fixed, consider logging at Debug level or aggregating counts to avoid log flooding during bulk updates.

For example, log the first N items individually and then a summary, or use a rate-limiter.

Also applies to: 471-494, 529-555

WFInfo/MainWindow.xaml.cs (1)

212-213: LGTM: correct Task-based invocation of ForceMarketUpdate

This avoids Task pitfalls and aligns with the async refactor. Minor nit: the async lambda can be simplified.

-Task.Run(async () => await Main.dataBase.ForceMarketUpdate());
+Task.Run(Main.dataBase.ForceMarketUpdate);
WFInfo/Main.cs (3)

171-176: Elevate init failure severity and keep UX consistent.

Use severity 1 (red) for initialization failures so users notice, and keep the short message actionable.

-                StatusUpdate("WFInfo Failed to Initialize", 0);
+                StatusUpdate("WFInfo failed to initialize — see debug.log for details", 1);

158-160: Handle Update() return value to surface failures

Upon inspection, Data.Update() is indeed declared as returning Task<bool> (see WFInfo/Data.cs:461) and thus indicates success or failure. Currently, the call in WFInfo/Main.cs ignores this result, which may allow downstream logic (e.g., login or UI flows) to proceed on stale or partial data.

• Location: WFInfo/Main.cs, around lines 158–160
• Signature confirmed in WFInfo/Data.cs at line 461:

public async Task<bool> Update()

Recommended refactor (optional but advised):

-                await dataBase.Update().ConfigureAwait(false);
+                var updateOk = await dataBase.Update().ConfigureAwait(false);
+                if (!updateOk)
+                {
+                    // Surface or log the failure before proceeding
+                    StatusUpdate("Market update failed; continuing with stale/partial data", 2);
+                }

This change preserves existing behavior on success and adds visibility in case the update fails, helping to avoid operations on incomplete data.


156-160: Add targeted instrumentation to isolate “Load missing market item” log calls

To pinpoint why Main.AddLog("Load missing market item: " + item_name) fires repeatedly, add scoped tracing around the key read and update paths. Here are the call sites and swap points you’ll want to instrument:

• WFInfo/Data.cs:322
Main.AddLog("Load missing market item: " + item_name);
Wrap this in a one‐time sampled tracer that logs the item_name, a per‐cycle correlation ID, Thread.CurrentThread.ManagedThreadId, plus marketItems.Count and marketData.Count.

• WFInfo/Data.cs:188 & 255
– Atomic swaps of marketItems under lock (marketItemsLock)
Log before and after each assignment to capture the size and swap timing.

• WFInfo/MainWindow.xaml.cs:212
Task.Run(async () => await Main.dataBase.ForceMarketUpdate());
Start a new correlation ID here for each forced‐update cycle.

• WFInfo/Data.cs:286
await ReloadItems();
Log at entry/exit of ReloadItems() to see if misses occur before or after loading the full set.

• WFInfo/Data.cs:468 & 525
await LoadMarket(allFiltered[, true]); (normal vs. forced)
Mark these boundaries to correlate missing‐item events with market‐data refreshes.

• UI callers of reloadItems() (verifyCount.xaml.cs:68/100, SettingsWindow.xaml.cs:204, Ocr.cs:1578, AutoCount.xaml.cs:82, AutoAddViewModel.cs:125, EquipmentWindow.cs:36)
Optionally propagate the same correlation ID if these paths can trigger additional lookups.

Suggested helper snippet:

public sealed class UpdateTraceScope : IDisposable {
    private readonly Guid _correlationId;
    public UpdateTraceScope(Guid correlationId) {
        _correlationId = correlationId;
        Main.AddLog($"[Trace {_correlationId}] BEGIN update on Thread {Thread.CurrentThread.ManagedThreadId}");
    }
    public void LogLookup(string itemKey, int itemsCount, int dataCount) {
        Main.AddLog(
            $"[Trace {_correlationId}] Lookup '{itemKey}' | " +
            $"Items={itemsCount} Data={dataCount} | Thread={Thread.CurrentThread.ManagedThreadId}"
        );
    }
    public void Dispose() {
        Main.AddLog($"[Trace {_correlationId}] END update");
    }
}

Usage example around the main update entry:

// In ForceMarketUpdate or the entry point of ReloadItems
var correlationId = Guid.NewGuid();
using var scope = new UpdateTraceScope(correlationId);
await ReloadItems();

// Inside LoadMarketItem:
if (ShouldSample()) {
    scope.LogLookup(item_name, marketItems.Count, marketData.Count);
}
Main.AddLog("Load missing market item: " + item_name);

Let me know if you’d like help refining or integrating this tracer.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7c27762 and 7cc756a.

📒 Files selected for processing (6)
  • WFInfo/Data.cs (11 hunks)
  • WFInfo/Main.cs (2 hunks)
  • WFInfo/MainWindow.xaml.cs (1 hunks)
  • WFInfo/Settings/SettingsWindow.xaml (1 hunks)
  • WFInfo/Settings/SettingsWindow.xaml.cs (1 hunks)
  • WFInfo/Templates/Styles.xaml (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2024-11-13T19:20:06.771Z
Learnt from: D1firehail
PR: WFCD/WFinfo#330
File: WFInfo/MainWindow.xaml.cs:74-74
Timestamp: 2024-11-13T19:20:06.771Z
Learning: In `WFInfo/MainWindow.xaml.cs`, within the `MainWindow` constructor, it's acceptable to keep the call to `Main.RegisterOnFinishedLoading(FinishedLoading);` outside the try-catch block, as per the project's error handling approach.

Applied to files:

  • WFInfo/Main.cs
🧬 Code graph analysis (4)
WFInfo/MainWindow.xaml.cs (1)
WFInfo/Data.cs (14)
  • Task (184-259)
  • Task (263-318)
  • Task (461-513)
  • Task (515-583)
  • Task (1289-1325)
  • Task (1352-1405)
  • Task (1407-1496)
  • Task (1499-1549)
  • Task (1555-1659)
  • Task (1661-1700)
  • Task (1727-1766)
  • Task (1775-1813)
  • Task (1842-1894)
  • Task (1899-1975)
WFInfo/Settings/SettingsWindow.xaml.cs (1)
WFInfo/Data.cs (15)
  • Task (184-259)
  • Task (263-318)
  • Task (461-513)
  • Task (515-583)
  • Task (1289-1325)
  • Task (1352-1405)
  • Task (1407-1496)
  • Task (1499-1549)
  • Task (1555-1659)
  • Task (1661-1700)
  • Task (1727-1766)
  • Task (1775-1813)
  • Task (1842-1894)
  • Task (1899-1975)
  • Task (2113-2145)
WFInfo/Main.cs (2)
WFInfo/Data.cs (1)
  • EnableLogCapture (132-147)
WFInfo/MainWindow.xaml.cs (3)
  • MainWindow (23-423)
  • MainWindow (33-75)
  • LoggedIn (253-262)
WFInfo/Data.cs (3)
WFInfo/Main.cs (4)
  • Task (88-103)
  • Main (25-603)
  • Main (73-86)
  • RunOnUIThread (251-254)
WFInfo/CustomEntrypoint.cs (2)
  • WebClient (177-188)
  • CustomEntrypoint (20-444)
WFInfo/MainWindow.xaml.cs (2)
  • MainWindow (23-423)
  • MainWindow (33-75)
🔇 Additional comments (2)
WFInfo/Data.cs (1)

345-356: Action Required: Confirm Warframe Market API Endpoints and Authentication

Please verify that the following remain correct as of today:

  • v1 Statistics endpoint
    ‣ GET https://api.warframe.market/v1/items/{url_name}/statistics
    • Response payload should include payload.statistics_closed["90days"] entries, each with avg_price and volume fields.
  • v2 Item endpoint
    GET https://api.warframe.market/v2/item/{url_name}
    data.ducats must be present in the returned JSON.
  • Authentication scheme for v2
    • All v2 requests should use Authorization: Bearer <JWT>
    • Double-check whether any write operations (e.g., posting reviews) require the alternate Authorization: JWT <JWT> format or remain under the Bearer scheme.

This applies to the following code regions:

  • WFInfo/Data.cs lines 345–356
  • WFInfo/Data.cs lines 1752–1765
  • WFInfo/ListingHelper.xaml.cs lines 1785–1797
  • WFInfo/Data.cs lines 2269–2286
WFInfo/Settings/SettingsWindow.xaml (1)

84-88: LGTM: implicit CheckBox style now BasedOn the new template

This keeps existing XAML tidy while adopting the new visuals in this window. Verify other windows add a similar BasedOn or an implicit alias is provided globally (see Styles.xaml comment).

Would you like me to scan the XAML views to spot CheckBoxes that might lose custom visuals after this change?

Comment on lines +193 to 206
ComboBoxItem item = (ComboBoxItem)localeCombobox.SelectedItem;

string selectedLocale = item.Tag.ToString();
_viewModel.Locale = selectedLocale;
Save();

_ = OCR.updateEngineAsync();

_ = Task.Run(() =>
// Use .Wait() to make it synchronous
Task.Run(() =>
{
Main.dataBase.ReloadItems();
Main.dataBase.ReloadItems().Wait();
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Make locale change handler truly synchronous (await) without blocking threads

Task.Run(...).Wait() inside a fire-and-forget Task neither guarantees completion before subsequent work nor captures exceptions. Since this is a UI event, prefer an async void handler and await ReloadItems directly.

-private void localeComboboxSelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
+private async void localeComboboxSelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
 {
-    ComboBoxItem item = (ComboBoxItem)localeCombobox.SelectedItem;
-
-    string selectedLocale = item.Tag.ToString();
+    var item = localeCombobox.SelectedItem as ComboBoxItem;
+    if (item?.Tag == null) return;
+    string selectedLocale = item.Tag.ToString();
     _viewModel.Locale = selectedLocale;
     Save();
 
     _ = OCR.updateEngineAsync();
 
-    // Use .Wait() to make it synchronous
-    Task.Run(() =>
-    {
-        Main.dataBase.ReloadItems().Wait();
-    });
+    try
+    {
+        await Main.dataBase.ReloadItems();
+    }
+    catch (Exception ex)
+    {
+        Main.AddLog("Locale change: reload items failed: " + ex.Message);
+    }
 }

If you need to update UI after reload, do it after the await to ensure consistent state.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ComboBoxItem item = (ComboBoxItem)localeCombobox.SelectedItem;
string selectedLocale = item.Tag.ToString();
_viewModel.Locale = selectedLocale;
Save();
_ = OCR.updateEngineAsync();
_ = Task.Run(() =>
// Use .Wait() to make it synchronous
Task.Run(() =>
{
Main.dataBase.ReloadItems();
Main.dataBase.ReloadItems().Wait();
});
}
private async void localeComboboxSelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
var item = localeCombobox.SelectedItem as ComboBoxItem;
if (item?.Tag == null) return;
string selectedLocale = item.Tag.ToString();
_viewModel.Locale = selectedLocale;
Save();
_ = OCR.updateEngineAsync();
try
{
await Main.dataBase.ReloadItems();
}
catch (Exception ex)
{
Main.AddLog("Locale change: reload items failed: " + ex.Message);
}
}
🤖 Prompt for AI Agents
In WFInfo/Settings/SettingsWindow.xaml.cs around lines 193 to 206, the locale
change handler uses Task.Run(...).Wait() which neither ensures completion nor
captures exceptions; change the event handler to async void, await
OCR.updateEngineAsync(), set _viewModel.Locale and Save() as now, then directly
await Main.dataBase.ReloadItems() (removing the Task.Run and Wait). After
awaiting ReloadItems, perform any UI updates needed so they run on the UI
thread; optionally wrap awaits in try/catch to surface/log exceptions from
ReloadItems or updateEngineAsync.

@dimon222
Copy link
Member Author

@coderabbitai

Pls give suggestion given in #367 (comment) after rebasing based on latest commit

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 26, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the specific review comment from the GitHub discussion
gh api repos/WFCD/WFinfo/pulls/367/comments | jq '.[] | select(.id == 2299471342)'

Length of output: 13180


🏁 Script executed:

#!/bin/bash
# Check the current state of the Update() and ForceMarketUpdate() methods in Data.cs
echo "=== Update() method ==="
rg -n -A 15 -B 5 "public async Task<bool> Update\(\)" WFInfo/Data.cs

echo "=== ForceMarketUpdate() method ==="
rg -n -A 20 -B 5 "public async Task ForceMarketUpdate\(\)" WFInfo/Data.cs

Length of output: 2586


🏁 Script executed:

#!/bin/bash
# Get more context around the problematic lookup logic in both methods
echo "=== Looking for the problematic lookup logic in Update() method ==="
rg -n -A 10 -B 5 "marketData\.TryGetValue.*itemName" WFInfo/Data.cs

echo "=== Looking for LoadMarketItem calls ==="
rg -n -A 5 -B 5 "LoadMarketItem" WFInfo/Data.cs

Length of output: 3088


@dimon222

Perfect! I can see the problematic code is still present after your latest commit. Here are the exact code changes you need to make to fix the "Load missing market item" spam:

🔧 Code Changes Needed

In the Update() method (around line 503):

// REPLACE THIS:
if (!itemName.Contains(" Set") && !marketData.TryGetValue(itemName, out _))
{
    missing.Add((itemName, itemUrl));
}

// WITH THIS:
if (!itemName.Contains(" Set"))
{
    // Try direct lookup first, then try with " Blueprint" appended
    if (!marketData.TryGetValue(itemName, out _) && 
        !marketData.TryGetValue(itemName + " Blueprint", out _))
    {
        missing.Add((itemName, itemUrl));
    }
}

In the ForceMarketUpdate() method (around lines 569-571):

// REPLACE THIS:
if (!itemName.Contains(" Set") &&
    itemName.Contains("Prime") &&
    !marketData.TryGetValue(itemName, out _))
{
    missing.Add((itemName, itemUrl));
}

// WITH THIS:
if (!itemName.Contains(" Set") &&
    itemName.Contains("Prime"))
{
    // Try direct lookup first, then try with " Blueprint" appended
    if (!marketData.TryGetValue(itemName, out _) && 
        !marketData.TryGetValue(itemName + " Blueprint", out _))
    {
        missing.Add((itemName, itemUrl));
    }
}

✅ Keep Your LoadMarket() Changes

All your LoadMarket() improvements (async handling, better error logging, etc.) are excellent and should be kept - they're unrelated to this lookup issue.

🎯 Why This Fixes the Spam

The root cause is a name mismatch: ReloadItems() strips " Blueprint" from item names, but the sheet data in LoadMarket() keeps the " Blueprint" suffix. This causes lookups to fail and trigger unnecessary LoadMarketItem() calls that log "Load missing market item: ...".

The enhanced lookup logic will try both name variants before deciding an item is truly missing, eliminating the false positives.

🐰✨

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (4)
WFInfo/Data.cs (4)

184-203: Improved error handling for network requests.

The error handling for the initial WFM API call is now properly wrapped, preventing crashes on transient network errors. Good improvement.


271-334: Fix the "Load missing market item" spam issue.

Based on the past review discussion, the root cause of excessive "Load missing market item" messages is due to name mismatch between normalized names (without " Blueprint") and sheet data names (with " Blueprint"). The current alias creation on lines 313-318 should help, but ensure the lookup logic in Update() and ForceMarketUpdate() is also updated.

#!/bin/bash
# Verify if the lookup logic has been updated to handle both name variants
rg -n -A 5 -B 5 '!marketData\.TryGetValue.*itemName' WFInfo/Data.cs

488-514: Good improvement: Network I/O outside of lock.

The refactoring to collect missing items under lock and then perform network I/O outside the lock is excellent. This prevents blocking other threads that need to read marketItems.


549-586: Consistent pattern: Network I/O outside of lock.

The same improvement pattern is correctly applied here - collecting missing items under lock, then performing downloads outside. Excellent consistency.

🧹 Nitpick comments (2)
WFInfo/Data.cs (2)

215-226: Consider documenting the strict filtering rationale.

The strict Prime filtering logic with specific part names could benefit from a comment explaining why only these specific parts are normalized by removing " Blueprint". This would help future maintainers understand the business logic.


338-339: Consider rate limiting improvements.

The Thread.Sleep(333) calls in LoadMarketItem could be replaced with async delays to avoid blocking threads. Also consider implementing a proper rate limiter or queue system for API calls.

Convert to async pattern:

private async Task LoadMarketItem(string item_name, string url)
{
    Main.AddLog("Load missing market item: " + item_name);
    
    await Task.Delay(333);
    // ... rest of the method
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7cc756a and f16fa44.

📒 Files selected for processing (1)
  • WFInfo/Data.cs (11 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-04-21T14:06:52.244Z
Learnt from: D1firehail
PR: WFCD/WFinfo#354
File: WFInfo/ListingHelper.xaml.cs:380-380
Timestamp: 2025-04-21T14:06:52.244Z
Learning: In the WFInfo project, only certain Warframe Market API endpoints have been migrated from V1 to V2. The GetTopListings method uses the V2 endpoint with JSON structure data["sell"], while GetCurrentListing still uses the V1 endpoint with JSON structure payload["payload"]["sell_orders"]. These different endpoints have different response structures that must be handled appropriately.

Applied to files:

  • WFInfo/Data.cs
📚 Learning: 2025-04-21T14:07:03.115Z
Learnt from: D1firehail
PR: WFCD/WFinfo#354
File: WFInfo/Data.cs:1525-1533
Timestamp: 2025-04-21T14:07:03.115Z
Learning: The Warframe Market v2 API is not complete yet, so it's preferable to only migrate specific endpoints that need fixing (like the `GetTopListings` method) rather than migrating all v1 endpoints at once.

Applied to files:

  • WFInfo/Data.cs
🧬 Code graph analysis (1)
WFInfo/Data.cs (2)
WFInfo/Main.cs (4)
  • Main (25-603)
  • Main (73-86)
  • AddLog (268-280)
  • RunOnUIThread (251-254)
WFInfo/CustomEntrypoint.cs (3)
  • WebClient (177-188)
  • AddLog (169-175)
  • CustomEntrypoint (20-444)
🔇 Additional comments (5)
WFInfo/Data.cs (5)

76-77: LGTM: Good addition for thread safety.

The static lock for marketItems is a good improvement that ensures thread-safe access to the shared collection across multiple async operations.


293-294: Critical sequencing fix for async operations.

Awaiting ReloadItems() before processing ensures marketItems is populated first. This is a crucial fix for the async flow that prevents race conditions.


806-822: Proper null checks prevent crashes.

The addition of null checks for marketItems in GetLocaleNameData and similar methods (lines 1118-1129, 1847-1859, 2117-2129) prevents potential null reference exceptions during concurrent access.


477-478: Async signatures properly propagated.

The conversion of Update() and ForceMarketUpdate() to async methods with proper Task return types allows callers to properly await these operations, ensuring data consistency.

Also applies to: 535-536


258-259: Version tracking is properly maintained.

Setting the version in tempMarketItems before the atomic swap ensures version consistency. Good practice.

@dimon222
Copy link
Member Author

@coderabbitai

I did what u said but it still gives nullpointerexception on "nameData[gameName] = partName;"

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 26, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the exact line with nameData[gameName] = partName; to understand the context
echo "=== Looking for the problematic line ==="
rg -n -A 10 -B 10 "nameData\[gameName\] = partName" WFInfo/Data.cs

echo "=== Looking for nameData initialization ==="
rg -n -A 5 -B 5 "nameData.*=" WFInfo/Data.cs

echo "=== Looking for nameData declaration ==="
rg -n -A 3 -B 3 "nameData" WFInfo/Data.cs

Length of output: 9342


🏁 Script executed:

#!/bin/bash
# Get the full context of the method containing the problematic line to understand the loop structure
echo "=== Full method context around line 448 ==="
rg -n -A 30 -B 30 "nameData\[gameName\] = partName" WFInfo/Data.cs

echo "=== Looking for the method signature that contains this code ==="
rg -n -A 50 -B 10 "foreach.*prime.*marketItems" WFInfo/Data.cs

Length of output: 3910


🏁 Script executed:

#!/bin/bash
# Find the method signature that contains the problematic code
echo "=== Looking for the method containing the nameData assignment ==="
rg -n -A 5 -B 15 "nameData = new JObject\(\)" WFInfo/Data.cs

echo "=== Get more context around the problematic assignment ==="
sed -n '395,465p' WFInfo/Data.cs

Length of output: 5698


@dimon222

I found the issue! The problem is that part.Key can be null in some cases, which makes both gameName and partName null. When you try to use null as a dictionary key in nameData[gameName] = partName;, it throws a NullPointerException.

Here's the fix you need to add null checks around the problematic code:

🔧 Fix for the NullPointerException

Replace this section (around lines 437-450):

string gameName = part.Key;
if (prime.Value["type"].ToString() == "Archwing" && (part.Key.Contains("Systems") || part.Key.Contains("Harness") || part.Key.Contains("Wings")))
{
    gameName += " Blueprint";
}
else if (prime.Value["type"].ToString() == "Warframes" && (part.Key.Contains("Systems") || part.Key.Contains("Neuroptics") || part.Key.Contains("Chassis")))
{
    gameName += " Blueprint";
}
if (marketData.TryGetValue(partName, out _))
{
    nameData[gameName] = partName;
    marketData[partName]["ducats"] = Convert.ToInt32(part.Value["ducats"].ToString(), Main.culture);
}

With this defensive version:

// Add null checks for part.Key and other critical values
if (part.Key != null && prime.Value?["type"] != null && part.Value?["ducats"] != null)
{
    string gameName = part.Key;
    string partType = prime.Value["type"].ToString();
    
    if (partType == "Archwing" && (part.Key.Contains("Systems") || part.Key.Contains("Harness") || part.Key.Contains("Wings")))
    {
        gameName += " Blueprint";
    }
    else if (partType == "Warframes" && (part.Key.Contains("Systems") || part.Key.Contains("Neuroptics") || part.Key.Contains("Chassis")))
    {
        gameName += " Blueprint";
    }
    
    if (marketData.TryGetValue(partName, out _))
    {
        nameData[gameName] = partName;
        marketData[partName]["ducats"] = Convert.ToInt32(part.Value["ducats"].ToString(), Main.culture);
    }
}

🎯 What This Fixes

  1. Null Key Protection: Ensures part.Key is not null before using it as gameName
  2. Null Type Protection: Checks that prime.Value["type"] exists before calling .ToString()
  3. Null Ducats Protection: Verifies part.Value["ducats"] exists before conversion
  4. Safe Dictionary Access: Only attempts dictionary operations with valid, non-null keys

🔍 Root Cause

The JSON data from the API sometimes contains entries where part.Key is null or missing, causing the dictionary assignment to fail. The null-safe operators (?.) and explicit null checks prevent this crash.

This should eliminate the NullPointerException while preserving all the functionality!

🐰

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
WFInfo/Data.cs (3)

214-221: Nit: centralize “Blueprint” normalization to avoid drift

You replicate “Blueprint” stripping here and in LoadEqmtData. Consider a single helper, e.g., NormalizePartName(string name, string type), to keep rules consistent (Warframes: Neuroptics/Chassis/Systems; Archwing: Systems/Harness/Wings).

If helpful, I can sketch that helper for both call sites.

Also applies to: 224-227


248-250: Remove redundant slug “prime” check when appending i18n name

Since you already guard with tempMarketItems.ContainsKey(item["id"]), the extra name.Contains("prime") on the slug is unnecessary and may be brittle (e.g., future naming). Rely on the ID presence.

-                        string name = item["slug"].ToString();
-                        if (name.Contains("prime") && tempMarketItems.ContainsKey(item["id"].ToString()))
+                        if (tempMarketItems.ContainsKey(item["id"].ToString()))
                             tempMarketItems[item["id"].ToString()] = tempMarketItems[item["id"].ToString()] + "|" + item["i18n"][_settings.Locale]["name"];

816-833: OK: thread-safe locale lookup; consider indexing if this is hot

Locking + linear scan is safe. If GetLocaleNameData is used frequently, consider caching a Dictionary<string, string> of name → localizedName when you build marketItems to make this O(1).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f16fa44 and 2bb414e.

📒 Files selected for processing (1)
  • WFInfo/Data.cs (12 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-04-21T14:06:52.244Z
Learnt from: D1firehail
PR: WFCD/WFinfo#354
File: WFInfo/ListingHelper.xaml.cs:380-380
Timestamp: 2025-04-21T14:06:52.244Z
Learning: In the WFInfo project, only certain Warframe Market API endpoints have been migrated from V1 to V2. The GetTopListings method uses the V2 endpoint with JSON structure data["sell"], while GetCurrentListing still uses the V1 endpoint with JSON structure payload["payload"]["sell_orders"]. These different endpoints have different response structures that must be handled appropriately.

Applied to files:

  • WFInfo/Data.cs
📚 Learning: 2025-04-21T14:07:03.115Z
Learnt from: D1firehail
PR: WFCD/WFinfo#354
File: WFInfo/Data.cs:1525-1533
Timestamp: 2025-04-21T14:07:03.115Z
Learning: The Warframe Market v2 API is not complete yet, so it's preferable to only migrate specific endpoints that need fixing (like the `GetTopListings` method) rather than migrating all v1 endpoints at once.

Applied to files:

  • WFInfo/Data.cs
🧬 Code graph analysis (1)
WFInfo/Data.cs (3)
WFInfo/Main.cs (5)
  • Task (88-103)
  • Main (25-603)
  • Main (73-86)
  • AddLog (268-280)
  • RunOnUIThread (251-254)
WFInfo/CustomEntrypoint.cs (3)
  • WebClient (177-188)
  • AddLog (169-175)
  • CustomEntrypoint (20-444)
WFInfo/MainWindow.xaml.cs (2)
  • MainWindow (23-423)
  • MainWindow (33-75)
🔇 Additional comments (6)
WFInfo/Data.cs (6)

191-203: Good: guarded v2/items download with contextual logging

Wrapping the initial items download in a try/catch with a clear log message is the right call. This prevents hard crashes during startup when WFM hiccups.


258-264: Atomic swap under lock is correct

Swapping the fully built tempMarketItems into marketItems under the lock avoids torn reads and races. Nicely done.


291-300: Good: await ReloadItems with failure isolation

Awaiting ReloadItems() before reading marketItems, and catching/logging failures here, prevents partial state usage.


483-519: Great: dual-variant lookup eliminates “Load missing market item” spam and avoids I/O under locks

  • Update(): checking both itemName and itemName + " Blueprint" before LoadMarketItem addresses the normalization mismatch.
  • ForceMarketUpdate(): same dual-check and collecting work first, doing network I/O after releasing marketItemsLock — exactly the right concurrency pattern.

Also applies to: 552-595


610-615: UI state restore paths are covered in both ForceMarketUpdate and ForceEquipmentUpdate

Buttons are re-enabled on error in both routines. Thanks for closing that UX hole.

Also applies to: 653-656


1858-1869: OK: thread-safe ID lookup

Lock + null-guard around marketItems is appropriate here.

Comment on lines +184 to +202
public async Task ReloadItems()
{
marketItems = new JObject();
lock (marketItemsLock)
{
marketItems = new JObject();
}

WebClient webClient = CreateWfmClient();
JObject obj =
JsonConvert.DeserializeObject<JObject>(
JObject obj;
try
{
obj = JsonConvert.DeserializeObject<JObject>(
webClient.DownloadString("https://api.warframe.market/v2/items"));

}
catch (Exception ex)
{
Main.AddLog("ReloadItems: failed to download item list (en). " + ex.Message);
return; // keep previous items; swap will be skipped
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Don’t clear marketItems before a successful fetch — this drops the cache on transient failures

ReloadItems currently replaces marketItems with an empty JObject before attempting the network call. If the download/deserialize fails, the method returns early and the previous cache is lost, contradicting the comment “keep previous items.” This will cascade into empty lookups and unnecessary “Load missing market item” calls until the next successful run.

Fix: build into a local temp object and only swap under the lock after a successful load; do not touch marketItems on failure.

-            lock (marketItemsLock)
-            {
-                marketItems = new JObject();
-            }
...
-            catch (Exception ex)
+            catch (Exception ex)
             {
-                Main.AddLog("ReloadItems: failed to download item list (en). " + ex.Message);
-                return; // keep previous items; swap will be skipped
+                Main.AddLog("ReloadItems: failed to download item list (en). " + ex.Message);
+                return; // leave previous marketItems intact; swap happens only on success
             }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In WFInfo/Data.cs around lines 184-202, the code currently clears marketItems
before attempting the network fetch which loses the cache on failure; instead,
deserialize into a local temporary JObject (e.g., tempObj) and only replace
marketItems inside the marketItemsLock after the download/deserialize succeeds;
do not modify marketItems on exceptions — keep the existing value and return
without swapping.

Comment on lines +305 to 319
WebClient webClient = CreateWfmClient();
JArray sheetData = JsonConvert.DeserializeObject<JArray>(
webClient.DownloadString(sheetJsonUrl));
foreach (var item in sheetData)
{
string name = row["name"].ToString();
if (name.Contains("Prime "))
var key = item["name"].ToString();
marketData[key] = item;
// Add a “Blueprint”-stripped alias to match ReloadItems’ normalization
var alias = key.Replace(" Blueprint", "");
if (!string.Equals(alias, key, StringComparison.Ordinal)
&& !marketData.TryGetValue(alias, out _))
{
if ((name.Contains("Neuroptics") || name.Contains("Chassis") || name.Contains("Systems") || name.Contains("Harness") || name.Contains("Wings")))
{
name = name.Replace(" Blueprint", "");
}
marketData[name] = new JObject
{
{"plat", double.Parse(row["custom_avg"].ToString(), Main.culture)},
{"ducats", 0},
{"volume", int.Parse(row["yesterday_vol"].ToString(), Main.culture) + int.Parse(row["today_vol"].ToString(), Main.culture)}
};
marketData[alias] = item;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Bug: alias writes move the same JToken between keys — use DeepClone (and clone on first insert)

Assigning the same JToken instance to multiple JObject keys re-parents the token in JSON.NET, so the second assignment detaches it from the first property. In this loop, marketData[key] = item followed by marketData[alias] = item will move the token to alias, leaving key with an empty/null value. It can also mutate the source JArray during enumeration.

Fix: deep-clone once for the main key and again for the alias. This preserves both entries and avoids modifying the enumerated container.

-            foreach (var item in sheetData)
-            {
-                var key = item["name"].ToString();
-                marketData[key] = item;
-                // Add a “Blueprint”-stripped alias to match ReloadItems’ normalization
-                var alias = key.Replace(" Blueprint", "");
-                if (!string.Equals(alias, key, StringComparison.Ordinal)
-                    && !marketData.TryGetValue(alias, out _))
-                {
-                    marketData[alias] = item;
-                }
-            }
+            foreach (var item in sheetData)
+            {
+                var key = item["name"].ToString();
+                var token = item.DeepClone(); // never re-parent the enumerated token
+                marketData[key] = token;
+                // Add a “Blueprint”-stripped alias to match ReloadItems’ normalization
+                var alias = key.Replace(" Blueprint", "");
+                if (!string.Equals(alias, key, StringComparison.Ordinal)
+                    && !marketData.TryGetValue(alias, out _))
+                {
+                    marketData[alias] = token.DeepClone(); // separate instance for alias
+                }
+            }

If you keep the dual-lookup in Update/ForceMarketUpdate (recommended), you can also remove the alias block entirely to save memory.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
WebClient webClient = CreateWfmClient();
JArray sheetData = JsonConvert.DeserializeObject<JArray>(
webClient.DownloadString(sheetJsonUrl));
foreach (var item in sheetData)
{
string name = row["name"].ToString();
if (name.Contains("Prime "))
var key = item["name"].ToString();
marketData[key] = item;
// Add a “Blueprint”-stripped alias to match ReloadItems’ normalization
var alias = key.Replace(" Blueprint", "");
if (!string.Equals(alias, key, StringComparison.Ordinal)
&& !marketData.TryGetValue(alias, out _))
{
if ((name.Contains("Neuroptics") || name.Contains("Chassis") || name.Contains("Systems") || name.Contains("Harness") || name.Contains("Wings")))
{
name = name.Replace(" Blueprint", "");
}
marketData[name] = new JObject
{
{"plat", double.Parse(row["custom_avg"].ToString(), Main.culture)},
{"ducats", 0},
{"volume", int.Parse(row["yesterday_vol"].ToString(), Main.culture) + int.Parse(row["today_vol"].ToString(), Main.culture)}
};
marketData[alias] = item;
}
}
WebClient webClient = CreateWfmClient();
JArray sheetData = JsonConvert.DeserializeObject<JArray>(
webClient.DownloadString(sheetJsonUrl));
foreach (var item in sheetData)
{
var key = item["name"].ToString();
// Deep-clone once for the primary entry to avoid re-parenting the enumerated token
var token = item.DeepClone();
marketData[key] = token;
// Add a “Blueprint”-stripped alias to match ReloadItems’ normalization
var alias = key.Replace(" Blueprint", "");
if (!string.Equals(alias, key, StringComparison.Ordinal)
&& !marketData.TryGetValue(alias, out _))
{
// Clone again for the alias so each key has its own copy
marketData[alias] = token.DeepClone();
}
}
🤖 Prompt for AI Agents
In WFInfo/Data.cs around lines 305 to 319, the loop assigns the same JToken
instance to multiple dictionary keys which re-parents the token and mutates the
enumerated JArray; change assignments to store clones: call DeepClone() when
inserting the value for the primary key and call DeepClone() again for the alias
so each dictionary entry gets its own JToken instance, preventing re-parenting
and modification of the source JArray. Alternatively, if you keep dual-lookup
elsewhere, remove the alias insertion entirely to avoid the extra memory cost.

Comment on lines +2127 to 2141
lock (marketItemsLock)
{
string[] vals = marketItem.Value.ToString().Split('|');
if (vals.Length > 2 && vals[0].Equals(primeName, StringComparison.OrdinalIgnoreCase))
if (marketItems != null) // Add null check
{
return vals[1];
foreach (var marketItem in marketItems)
{
string[] vals = marketItem.Value.ToString().Split('|');
if (vals.Length > 2 && vals[0].Equals(primeName, StringComparison.OrdinalIgnoreCase))
{
return vals[1];
}
}
}
}
throw new Exception($"GetUrlName, Prime item \"{primeName}\" does not exist in marketItem");
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Make GetUrlName resilient when i18n enrichment is unavailable

When the second v2/items call (localized pass) fails, marketItems entries remain “name|slug” (length == 2). The current check vals.Length > 2 will fail to return the URL and cause downstream exceptions (e.g., listing, top listings) despite having a valid slug.

Return the URL when there are at least two segments; the third is optional.

-                        string[] vals = marketItem.Value.ToString().Split('|');
-                        if (vals.Length > 2 && vals[0].Equals(primeName, StringComparison.OrdinalIgnoreCase))
+                        string[] vals = marketItem.Value.ToString().Split('|');
+                        if (vals.Length >= 2 && vals[0].Equals(primeName, StringComparison.OrdinalIgnoreCase))
                         {
-                            return vals[1];
+                            return vals[1]; // slug is present even without the i18n name
                         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
lock (marketItemsLock)
{
string[] vals = marketItem.Value.ToString().Split('|');
if (vals.Length > 2 && vals[0].Equals(primeName, StringComparison.OrdinalIgnoreCase))
if (marketItems != null) // Add null check
{
return vals[1];
foreach (var marketItem in marketItems)
{
string[] vals = marketItem.Value.ToString().Split('|');
if (vals.Length > 2 && vals[0].Equals(primeName, StringComparison.OrdinalIgnoreCase))
{
return vals[1];
}
}
}
}
throw new Exception($"GetUrlName, Prime item \"{primeName}\" does not exist in marketItem");
lock (marketItemsLock)
{
if (marketItems != null) // Add null check
{
foreach (var marketItem in marketItems)
{
string[] vals = marketItem.Value.ToString().Split('|');
if (vals.Length >= 2 &&
vals[0].Equals(primeName, StringComparison.OrdinalIgnoreCase))
{
return vals[1]; // slug is present even without the i18n name
}
}
}
}
throw new Exception($"GetUrlName, Prime item \"{primeName}\" does not exist in marketItem");
🤖 Prompt for AI Agents
In WFInfo/Data.cs around lines 2127 to 2141, the loop currently only returns a
slug when vals.Length > 2 which ignores valid "name|slug" entries produced when
i18n enrichment fails; change the condition to vals.Length >= 2 so the method
returns vals[1] when at least two segments exist (the third segment is
optional), and keep the existing StringComparison.OrdinalIgnoreCase check and
exception behavior unchanged.

@dimon222
Copy link
Member Author

dimon222 commented Aug 26, 2025

@coderabbitai

It seem to have solved exceptions, and refresh buttons for both work fine, and loading thing doesn't get stuck, but its very slow to do refresh now.
Both refresh buttons are very slow taking from 14 to 35 seconds to execute.
On top of it I get also this on forcemarketupdate

[8/26/2025 1:32:40 AM 9.6.6]   ForceMarketUpdate FAILED System.FormatException: Input string was not in a correct format.
   at System.Number.StringToNumber(String str, NumberStyles options, NumberBuffer& number, NumberFormatInfo info, Boolean parseDecimal)
   at System.Number.ParseInt32(String s, NumberStyles style, NumberFormatInfo info)
   at WFInfo.Data.RefreshMarketDucats() in /_/WFInfo/Data.cs:line 476
   at WFInfo.Data.<ForceMarketUpdate>d__50.MoveNext() in /_/WFInfo/Data.cs:line 604
[8/26/2025 1:33:32 AM 9.6.6]   WebSocket disconnected successfully

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 26, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

@dimon222

Let me investigate both the performance issue and the FormatException. I need to examine the current code to understand what's causing these problems.

Let me also check the current refresh flow to understand the performance bottleneck:


🏁 Script executed:

#!/bin/bash
# Look at the RefreshMarketDucats method around line 476 to find the problematic conversion
echo "=== RefreshMarketDucats method around line 476 ==="
rg -n -A 10 -B 10 "RefreshMarketDucats" WFInfo/Data.cs

echo "=== Looking for ParseInt32 calls and string conversions ==="
rg -n -A 5 -B 5 "Convert\.ToInt32\|ParseInt32\|\.ToString\(\)" WFInfo/Data.cs | head -30

Length of output: 2432


🏁 Script executed:

#!/bin/bash
# Check the current implementation of ForceMarketUpdate and Update methods
echo "=== Current ForceMarketUpdate implementation ==="
sed -n '550,620p' WFInfo/Data.cs

echo "=== Current Update method implementation ==="
sed -n '490,540p' WFInfo/Data.cs

Length of output: 6085


Perfect! I found both issues. Let me provide the fixes:

🐌 Performance Issue

The refresh is slow because LoadMarket() calls ReloadItems() every time, which downloads the entire WFM item database (thousands of items). This should only happen when necessary.

💥 FormatException Issue

Line 480 in RefreshMarketDucats() tries to parse ducat values that can be null or empty strings.

🔧 Fixes Required

1. Fix Performance - Make ReloadItems() Conditional:

In the LoadMarket() method, around where you call await ReloadItems(), replace it with:

// Only reload items if marketItems is null, empty, or version mismatch
bool needsReload = false;
lock (marketItemsLock)
{
    needsReload = marketItems == null || 
                  !marketItems.ContainsKey("version") || 
                  marketItems["version"].ToString() != Main.buildVersion;
}

if (needsReload)
{
    await ReloadItems();
}
else
{
    Main.AddLog("Item database is up to date, skipping reload");
}

2. Fix FormatException - Safe Ducat Parsing:

Replace the RefreshMarketDucats() method (around lines 473-481) with this safe version:

private void RefreshMarketDucats()
{
    //equipmentData[primeName]["parts"][partName]["ducats"]
    foreach (KeyValuePair<string, JToken> prime in equipmentData)
    {
        if (prime.Key != "timestamp")
        {
            foreach (KeyValuePair<string, JToken> part in equipmentData[prime.Key]["parts"].ToObject<JObject>())
            {
                if (marketData.TryGetValue(part.Key, out _))
                {
                    // Safe ducat parsing with null/empty checks
                    var ducatsToken = part.Value["ducats"];
                    if (ducatsToken != null && !string.IsNullOrWhiteSpace(ducatsToken.ToString()))
                    {
                        if (int.TryParse(ducatsToken.ToString(), NumberStyles.Integer, Main.culture, out int ducatsValue))
                        {
                            marketData[part.Key]["ducats"] = ducatsValue;
                        }
                        else
                        {
                            Main.AddLog($"Invalid ducats value for {part.Key}: '{ducatsToken}'");
                        }
                    }
                    // If ducats is null/empty, keep existing value or skip
                }
            }
        }
    }
}

✅ Expected Results

  • Performance: Refresh should drop from 14-35 seconds to 2-5 seconds (only downloads missing items, not entire database)
  • No More Crashes: FormatException eliminated with safe parsing
  • Better Logging: You'll see which items have invalid ducat values instead of crashes

The conditional reload ensures the expensive WFM API call only happens when the local item database is outdated, while the safe parsing prevents crashes from malformed data.

🐰

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 26, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

@dimon222

I can see the issues! Let me analyze both problems:

🐌 Performance Issue

The slowness is likely caused by the dual lookup logic we added. Each item now does two TryGetValue calls instead of one, and this happens for every item in the loop. This doubles the dictionary lookup overhead.

❌ FormatException Issue

The error at line 476 in RefreshMarketDucats() is trying to parse a string that's not a valid number. This happens when the ducats value from the API contains non-numeric data (like null, empty string, or text).

🔧 Performance Fix

Let's optimize the dual lookup by pre-computing both variants:

Replace the dual lookup logic with this optimized version:

// In Update() method - OPTIMIZED VERSION:
if (!itemName.Contains(" Set"))
{
    bool found = marketData.TryGetValue(itemName, out _);
    if (!found && !itemName.EndsWith(" Blueprint"))
    {
        found = marketData.TryGetValue(itemName + " Blueprint", out _);
    }
    
    if (!found)
    {
        missing.Add((itemName, itemUrl));
    }
}
// In ForceMarketUpdate() method - OPTIMIZED VERSION:
if (!itemName.Contains(" Set") && itemName.Contains("Prime"))
{
    bool found = marketData.TryGetValue(itemName, out _);
    if (!found && !itemName.EndsWith(" Blueprint"))
    {
        found = marketData.TryGetValue(itemName + " Blueprint", out _);
    }
    
    if (!found)
    {
        missing.Add((itemName, itemUrl));
    }
}

🔧 FormatException Fix

In RefreshMarketDucats() around line 476, add null/empty checks:

// REPLACE the problematic conversion with safe parsing:
// OLD: Convert.ToInt32(part.Value["ducats"].ToString(), Main.culture)
// NEW:
string ducatsStr = part.Value["ducats"]?.ToString();
if (int.TryParse(ducatsStr, NumberStyles.Integer, Main.culture, out int ducatsValue))
{
    marketData[partName]["ducats"] = ducatsValue;
}
else
{
    // Log the problematic value for debugging
    Main.AddLog($"Invalid ducats value for {partName}: '{ducatsStr}'");
    // Set a default value or skip this item
    marketData[partName]["ducats"] = 0; // or skip the assignment entirely
}

🎯 Why These Fixes Work

  1. Performance: Only does the second lookup if the first fails AND the name doesn't already end with " Blueprint"
  2. Safety: Uses TryParse instead of Convert.ToInt32 to handle malformed data gracefully
  3. Debugging: Logs problematic ducats values so you can see what's causing the issue

This should bring the refresh time back to normal while preventing the format exceptions!

🐰⚡


🏁 Script executed:

#!/bin/bash
# Get the RefreshMarketDucats method and the specific line 476 causing the FormatException
echo "=== RefreshMarketDucats method around line 476 ==="
rg -n -A 10 -B 10 "RefreshMarketDucats" WFInfo/Data.cs

echo "=== Looking for line 476 specifically ==="
sed -n '466,486p' WFInfo/Data.cs

echo "=== ForceMarketUpdate method around line 604 ==="
sed -n '594,614p' WFInfo/Data.cs

Length of output: 4413


🏁 Script executed:

#!/bin/bash
# Look for parseInt operations and string parsing in RefreshMarketDucats
echo "=== Looking for parseInt and number parsing in RefreshMarketDucats ==="
rg -n -A 5 -B 5 "Convert\.ToInt32\|parseInt\|Parse" WFInfo/Data.cs | head -30

echo "=== Look for ducats-related parsing ==="
rg -n -A 3 -B 3 "ducats.*Convert\|ducats.*Parse" WFInfo/Data.cs

Length of output: 385


🏁 Script executed:

#!/bin/bash
# Get more context around the problematic areas to understand the performance issues
echo "=== Check the missing items loading logic that might be causing slowness ==="
rg -n -A 15 -B 5 "LoadMarketItem.*missing" WFInfo/Data.cs

echo "=== Look at the updated dual lookup logic we implemented ==="
rg -n -A 8 -B 3 "Blueprint.*out _" WFInfo/Data.cs

Length of output: 1604


Perfect! Now I can see the exact issues:

🎯 FormatException Fix

Line 480 in RefreshMarketDucats() is the culprit:

// REPLACE LINE 480:
marketData[part.Key]["ducats"] = Convert.ToInt32(part.Value["ducats"].ToString(), Main.culture);

// WITH THIS SAFE VERSION:
string ducatsStr = part.Value["ducats"]?.ToString();
if (!string.IsNullOrEmpty(ducatsStr) && int.TryParse(ducatsStr, NumberStyles.Integer, Main.culture, out int ducatsValue))
{
    marketData[part.Key]["ducats"] = ducatsValue;
}
else
{
    Main.AddLog($"Invalid ducats value for {part.Key}: '{ducatsStr ?? "null"}'");
    marketData[part.Key]["ducats"] = 0; // Default value
}

Performance Optimization

The dual lookup is working but can be optimized. Replace your current dual lookup logic:

In both Update() and ForceMarketUpdate() methods:

// CURRENT (lines 512-513 and 584-585):
if (!marketData.TryGetValue(itemName, out _) &&
    !marketData.TryGetValue(itemName + " Blueprint", out _))

// OPTIMIZED VERSION:
bool found = marketData.TryGetValue(itemName, out _);
if (!found && !itemName.EndsWith(" Blueprint"))
{
    found = marketData.TryGetValue(itemName + " Blueprint", out _);
}
if (!found)

🔍 Performance Investigation

Let's add some timing logs to see what's causing the slowness:

The 14-35 second delay suggests network calls are the bottleneck, not the dual lookups. Each LoadMarketItem() likely makes HTTP requests to download missing item data.

🚀 Quick Wins

  1. Fix the FormatException first (line 480)
  2. Add timing logs around network operations
  3. Consider parallel processing for LoadMarketItem calls if there are many missing items

Try the FormatException fix first - this should eliminate the crashes and might also improve performance if exceptions were causing retries!

🐰🔧


🏁 Script executed:

#!/bin/bash
# Check how many items are being processed in the missing lists
echo "=== Count missing items being processed ==="
rg -n -A 3 -B 3 "missing\.Add" WFInfo/Data.cs

echo "=== Look for LoadMarketItem method to see if it's doing network calls ==="
rg -n -A 10 -B 5 "private.*LoadMarketItem" WFInfo/Data.cs

Length of output: 1849


🎯 Found the Performance Culprit!

The slowness is caused by LoadMarketItem() method:

  1. Thread.Sleep(333) - 333ms delay per missing item!
  2. HTTP requests to api.warframe.market for each missing item
  3. Sequential processing - items are loaded one by one

If you have 10 missing items: 10 × 333ms = 3.3+ seconds just in sleep delays, plus network time!

🔧 Complete Fix Package

1. Fix FormatException (Line 480):

// In RefreshMarketDucats() method:
string ducatsStr = part.Value["ducats"]?.ToString();
if (!string.IsNullOrEmpty(ducatsStr) && int.TryParse(ducatsStr, NumberStyles.Integer, Main.culture, out int ducatsValue))
{
    marketData[part.Key]["ducats"] = ducatsValue;
}
else
{
    Main.AddLog($"Invalid ducats value for {part.Key}: '{ducatsStr ?? "null"}'");
    marketData[part.Key]["ducats"] = 0;
}

2. Optimize Performance:

Option A - Reduce Sleep Delay:

// In LoadMarketItem() - reduce from 333ms to 100ms:
Thread.Sleep(100); // Instead of Thread.Sleep(333);

Option B - Parallel Processing:

// Replace the sequential foreach in ForceMarketUpdate():
// OLD:
foreach (var (name, url) in missing)
{
    LoadMarketItem(name, url);
    downloadCount++;
    Main.AddLog($"Downloaded missing Prime item: {name}");
}

// NEW - Parallel with controlled concurrency:
var tasks = missing.Select(async (item) =>
{
    await Task.Run(() => LoadMarketItem(item.name, item.url));
    Main.AddLog($"Downloaded missing Prime item: {item.name}");
}).ToArray();

await Task.WhenAll(tasks);
downloadCount = missing.Count;

3. Debug Missing Items:

Add this logging to see how many items are being processed:

// Before the missing items loop in both Update() and ForceMarketUpdate():
Main.AddLog($"Processing {missing.Count} missing items");

🚀 Recommended Quick Fix:

Start with Option A (reduce sleep to 100ms) + FormatException fix. This should bring your 14-35 second refresh down to 5-10 seconds while maintaining API rate limiting.

The Thread.Sleep(333) is there for API rate limiting, so don't remove it completely, but 100ms should be sufficient!

🐰⚡

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (5)
WFInfo/Data.cs (5)

293-300: OK to proceed when ReloadItems fails, but re-check the early-clear bug

Awaiting ReloadItems() first is correct; swallowing the error and proceeding with Sheets data is pragmatic. However, this still depends on not having cleared marketItems earlier. Please address the early clear (see prior comment) so a failure here doesn’t leave you with an empty cache.


659-662: UI re-enable on error path restored

Covers the previously missing path in ForceEquipmentUpdate’s catch; prevents buttons staying disabled on exceptions.


186-202: Do not clear marketItems before a successful fetch (drops cache on transient failures)

ReloadItems() nulls the cache up-front; if the download/deserialize throws, you return early with marketItems already reset to empty. This regresses startup resilience and reintroduces the “Load missing market item …” cascade after a temporary outage.

Apply this diff to remove the premature clear; only swap at the end once tempMarketItems is fully built:

-            lock (marketItemsLock)
-            {
-                marketItems = new JObject();
-            }
-
             WebClient webClient = CreateWfmClient();
             JObject obj;
             try

305-319: Bug: aliasing the same JToken re-parents/mutates JSON.NET tokens; remove alias block or DeepClone

Assigning the same JToken instance to multiple JObject keys moves the token (second assignment detaches it from the first). This can corrupt marketData and was implicated in the NRE you hit in LoadEqmtData. Prefer removing the alias insertion entirely now that lookups check both names. If you must keep aliases, clone.

Option A (preferred): remove alias insertion

             foreach (var item in sheetData)
             {
                 var key = item["name"].ToString();
                 marketData[key] = item;
-                // Add a “Blueprint”-stripped alias to match ReloadItems’ normalization
-                var alias = key.Replace(" Blueprint", "");
-                if (!string.Equals(alias, key, StringComparison.Ordinal)
-                    && !marketData.TryGetValue(alias, out _))
-                {
-                    marketData[alias] = item;
-                }
             }

Option B: keep aliases but use DeepClone to avoid re-parenting

             foreach (var item in sheetData)
             {
-                var key = item["name"].ToString();
-                marketData[key] = item;
+                var key = item["name"].ToString();
+                var token = item.DeepClone(); // never re-parent source
+                marketData[key] = token;
                 // Add a “Blueprint”-stripped alias to match ReloadItems’ normalization
                 var alias = key.Replace(" Blueprint", "");
                 if (!string.Equals(alias, key, StringComparison.Ordinal)
                     && !marketData.TryGetValue(alias, out _))
                 {
-                    marketData[alias] = item;
+                    marketData[alias] = token.DeepClone();
                 }
             }

2133-2146: Return slug when at least two segments exist (i18n segment optional)

GetUrlName currently requires vals.Length > 2, which fails when localized enrichment didn’t append the 3rd field. This breaks downstream flows even though “name|slug” is valid.

-                        if (vals.Length > 2 && vals[0].Equals(primeName, StringComparison.OrdinalIgnoreCase))
+                        if (vals.Length >= 2 && vals[0].Equals(primeName, StringComparison.OrdinalIgnoreCase))
                         {
                             return vals[1];
                         }
🧹 Nitpick comments (5)
WFInfo/Data.cs (5)

521-525: Optional: rate-limit/backoff for LoadMarketItem loop

If the missing list is large, the 333ms sleeps per request can still create noticeable delays. Consider batching with a small concurrency (e.g., SemaphoreSlim with max 3) and removing Thread.Sleep throttles in favor of per-request delay/backoff.


822-839: Thread-safe locale name lookup — good; consider small guard on split

Locking is correct. Minor: add a defensive guard when splitting to avoid IndexOutOfRange if the entry is malformed.

Example tweak:

-                        string[] split = marketItem.Value.ToString().Split('|');
-                        if (split[0] == s)
+                        string[] split = marketItem.Value.ToString().Split('|');
+                        if (split.Length >= 1 && split[0] == s)

1864-1876: Thread-safe PrimeItemToItemID — good; minor allocation nit

Logic is correct. If you want to reduce allocations in hot paths, store marketItem.Value.ToString() into a local and Split once.


487-491: Minor: Prefer HttpClient for filterAllJSON fetch to avoid sync blocking on UI threads

This call uses WebClient.DownloadString synchronously. You’re already on an async path; consider switching to HttpClient for consistency and responsiveness.


493-493: Nit: fix indentation of ‘var missing’ for readability

It’s over-indented relative to surrounding code.

-                        var missing = new List<(string Name, string Url)>();
+            var missing = new List<(string Name, string Url)>();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 2bb414e and bb66be9.

📒 Files selected for processing (1)
  • WFInfo/Data.cs (13 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-04-21T14:06:52.244Z
Learnt from: D1firehail
PR: WFCD/WFinfo#354
File: WFInfo/ListingHelper.xaml.cs:380-380
Timestamp: 2025-04-21T14:06:52.244Z
Learning: In the WFInfo project, only certain Warframe Market API endpoints have been migrated from V1 to V2. The GetTopListings method uses the V2 endpoint with JSON structure data["sell"], while GetCurrentListing still uses the V1 endpoint with JSON structure payload["payload"]["sell_orders"]. These different endpoints have different response structures that must be handled appropriately.

Applied to files:

  • WFInfo/Data.cs
📚 Learning: 2025-04-21T14:07:03.115Z
Learnt from: D1firehail
PR: WFCD/WFinfo#354
File: WFInfo/Data.cs:1525-1533
Timestamp: 2025-04-21T14:07:03.115Z
Learning: The Warframe Market v2 API is not complete yet, so it's preferable to only migrate specific endpoints that need fixing (like the `GetTopListings` method) rather than migrating all v1 endpoints at once.

Applied to files:

  • WFInfo/Data.cs
🧬 Code graph analysis (1)
WFInfo/Data.cs (3)
WFInfo/Main.cs (5)
  • Task (88-103)
  • Main (25-603)
  • Main (73-86)
  • AddLog (268-280)
  • RunOnUIThread (251-254)
WFInfo/CustomEntrypoint.cs (3)
  • WebClient (177-188)
  • AddLog (169-175)
  • CustomEntrypoint (20-444)
WFInfo/MainWindow.xaml.cs (2)
  • MainWindow (23-423)
  • MainWindow (33-75)
🔇 Additional comments (5)
WFInfo/Data.cs (5)

75-77: Good addition: a dedicated lock for marketItems access

Introducing a static lock for marketItems improves thread-safety across async paths that enumerate/replace this object. Keep using it consistently wherever marketItems is read or swapped.


258-264: Atomic swap looks good

Building into tempMarketItems and swapping under the lock is the right pattern. Combined with the fix above (don’t clear up-front), this becomes fully resilient to partial failures.


493-526: Good: collect missing under lock, fetch outside; plus dual-key lookup

This resolves the long-standing “Load missing…” spam and prevents network I/O under the lock. Nice.


546-601: ForceMarketUpdate mirrors the improved pattern — looks solid

  • Awaits LoadMarket(true) first.
  • Snapshots missing items inside the lock.
  • Performs downloads outside the lock and logs totals.
  • Uses the same dual-key presence check to avoid false misses.

554-557: Helpful progress logs around LoadMarket(true)

The additional logging makes it easier to correlate user actions with fetch phases.

@dimon222
Copy link
Member Author

@coderabbitai

It works well now, but I'm unable to open relics tab, it throw NullPointerException on

if (kvp.Key != "vaulted" && Main.dataBase.marketData.TryGetValue(kvp.Value.ToString(), out JToken marketValues))

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 26, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

@dimon222

Let me investigate the relics tab issue and find where this NullPointerException is occurring.


🏁 Script executed:

#!/bin/bash
# Find the exact location of the problematic line
echo "=== Looking for the problematic TryGetValue line ==="
rg -n -A 10 -B 10 'kvp\.Key.*!.*"vaulted".*TryGetValue.*kvp\.Value\.ToString' --type cs

echo "=== Looking for relics-related methods ==="
rg -n -A 5 -B 5 -i "relic" --type cs

echo "=== Looking for any TryGetValue with kvp.Value.ToString ==="
rg -n -A 5 -B 5 'TryGetValue.*kvp\.Value\.ToString' --type cs

Length of output: 40001


@dimon222

Perfect! I found the exact issue. The problem is in RelicsViewModel.cs at line 244. The kvp.Value can be null in the relic data, and when you call .ToString() on a null value, it throws a NullReferenceException.

🔧 Fix for the Relics Tab NullPointerException

Replace this problematic line in RelicsViewModel.cs around line 244:

// PROBLEMATIC LINE:
if (kvp.Key != "vaulted" && Main.dataBase.marketData.TryGetValue(kvp.Value.ToString(), out JToken marketValues))

// REPLACE WITH THIS SAFE VERSION:
if (kvp.Key != "vaulted" && kvp.Value != null && 
    Main.dataBase.marketData.TryGetValue(kvp.Value.ToString(), out JToken marketValues))

Or even more defensive:

// MOST DEFENSIVE VERSION:
if (kvp.Key != null && kvp.Key != "vaulted" && kvp.Value != null)
{
    string partName = kvp.Value.ToString();
    if (!string.IsNullOrEmpty(partName) && 
        Main.dataBase.marketData.TryGetValue(partName, out JToken marketValues))
    {
        TreeNode part = new TreeNode(partName, "", false, 0);
        part.SetPartText(marketValues["plat"].ToObject<double>(), marketValues["ducats"].ToObject<int>(), kvp.Key);                           
        relic.AddChild(part);
    }
}

🎯 Root Cause

The relic data JSON from the API sometimes contains entries where the part name (kvp.Value) is null. This happens when:

  1. The data is corrupted or incomplete
  2. There are placeholder entries in the relic data
  3. The API returns partial data during updates

The null check ensures we skip these invalid entries instead of crashing when trying to look them up in the market data.

✅ Expected Result

After this fix, the relics tab should open properly and display all valid relic entries while gracefully skipping any corrupted/null entries in the data.

🐰✨

@dimon222
Copy link
Member Author

dimon222 commented Aug 26, 2025

@coderabbitai

Both versions suggested throw nullpointerexception. Defensive one raises it on TreeNode part = new TreeNode(partName, "", false, 0);

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
WFInfo/Data.cs (1)

337-381: Harden LoadMarketItem against network/parse failures and reduce throttle delay

Two issues: (1) this method throws on any network failure, which can bubble up from Update(); (2) 333ms per call slows ForceMarketUpdate significantly. Wrap the whole load in try/catch and use a smaller, centralized delay.

Apply this diff:

-        private void LoadMarketItem(string item_name, string url)
+        private void LoadMarketItem(string item_name, string url)
         {
-            Main.AddLog("Load missing market item: " + item_name);
-
-            Thread.Sleep(333);
-            WebClient webClient = CreateWfmClient();
-            JObject stats =
-                JsonConvert.DeserializeObject<JObject>(
-                    webClient.DownloadString("https://api.warframe.market/v1/items/" + url + "/statistics"));
-            JToken latestStats = stats["payload"]["statistics_closed"]["90days"].LastOrDefault();
-            if (latestStats == null)
-            {
-                stats = new JObject
-                {
-                    { "avg_price", 999 },
-                    { "volume", 0 }
-                };
-            } 
-            else
-            {
-                stats = latestStats.ToObject<JObject>();
-            }
-
-            Thread.Sleep(333);
-            webClient = CreateWfmClient();
-            JObject responseJObject = JsonConvert.DeserializeObject<JObject>(
-                webClient.DownloadString("https://api.warframe.market/v2/item/" + url)
-            );
-            string ducat;
-            if (!responseJObject["data"].ToObject<JObject>().TryGetValue("ducats", out JToken temp))
-            {
-                ducat = "0";
-            }
-            else
-            {
-                ducat = temp.ToObject<string>();
-            }
-
-            marketData[item_name] = new JObject
-            {
-                { "ducats", ducat },
-                { "plat", stats["avg_price"] },
-                { "volume", stats["volume"] }
-            };
+            const int delay = 150; // modest throttle; adjust if server rate-limits
+            Main.AddLog("Load missing market item: " + item_name);
+            try
+            {
+                Thread.Sleep(delay);
+                var webClient = CreateWfmClient();
+                var statsObj = JsonConvert.DeserializeObject<JObject>(
+                    webClient.DownloadString("https://api.warframe.market/v1/items/" + url + "/statistics"));
+                var latestStats = statsObj?["payload"]?["statistics_closed"]?["90days"]?.LastOrDefault();
+                var stats = latestStats != null
+                    ? latestStats.ToObject<JObject>()
+                    : new JObject { { "avg_price", 999 }, { "volume", 0 } };
+
+                Thread.Sleep(delay);
+                webClient = CreateWfmClient();
+                var responseJObject = JsonConvert.DeserializeObject<JObject>(
+                    webClient.DownloadString("https://api.warframe.market/v2/item/" + url));
+                var ducatToken = responseJObject?["data"]?["ducats"];
+                var ducatStr = ducatToken?.ToObject<string>() ?? "0";
+
+                marketData[item_name] = new JObject
+                {
+                    { "ducats", ducatStr },
+                    { "plat", stats["avg_price"] },
+                    { "volume", stats["volume"] }
+                };
+            }
+            catch (Exception ex)
+            {
+                Main.AddLog($"LoadMarketItem failed for '{item_name}': {ex.Message}");
+                // Fallback entry to avoid repeated retries this session
+                marketData[item_name] = new JObject
+                {
+                    { "ducats", "0" },
+                    { "plat", 999 },
+                    { "volume", 0 }
+                };
+            }
         }
♻️ Duplicate comments (3)
WFInfo/Data.cs (3)

2152-2164: Return slug when at least two segments are present (not only when 3rd i18n exists)

vals[1] (slug) exists even when localization enrichment fails, but the current condition (vals.Length > 2) incorrectly rejects such entries.

-                        if (vals.Length > 2 && vals[0].Equals(primeName, StringComparison.OrdinalIgnoreCase))
+                        if (vals.Length >= 2 && vals[0].Equals(primeName, StringComparison.OrdinalIgnoreCase))

303-320: Bug: Re-assigning the same JToken to two keys re-parents the token (data loss) — remove aliasing or DeepClone

JSON.NET moves a JToken to the latest parent. Assigning item to marketData[key] then to marketData[alias] detaches it from key, corrupting the first entry and potentially mutating the enumerated array. You already fixed callers to try both "" and " Blueprint", so this alias is unnecessary and harmful.

Either remove the alias block (recommended) or store DeepClone() for each key. Minimal fix:

             foreach (var item in sheetData)
             {
                 var key = item["name"].ToString();
-                marketData[key] = item;
-                // Add a “Blueprint”-stripped alias to match ReloadItems’ normalization
-                var alias = key.Replace(" Blueprint", "");
-                if (!string.Equals(alias, key, StringComparison.Ordinal)
-                    && !marketData.TryGetValue(alias, out _))
-                {
-                    marketData[alias] = item;
-                }
+                marketData[key] = item; // rely on dual-lookup elsewhere
             }

If you prefer to keep aliases, do:

-                marketData[key] = item;
+                var token = item.DeepClone();
+                marketData[key] = token;
                 var alias = key.Replace(" Blueprint", "");
                 if (!string.Equals(alias, key, StringComparison.Ordinal)
                     && !marketData.TryGetValue(alias, out _))
                 {
-                    marketData[alias] = item;
+                    marketData[alias] = token.DeepClone();
                 }

187-191: Don’t clear marketItems before a successful fetch — drops cache on transient failures

This resets marketItems to an empty object before the download/deserialize. If the try block fails (lines 199-203), callers will observe an empty cache, regressing to “missing item” spam and slower updates.

Apply this diff to preserve the previous cache unless the new payload is ready:

-            lock (marketItemsLock)
-            {
-                marketItems = new JObject();
-            }
🧹 Nitpick comments (3)
WFInfo/Data.cs (3)

193-203: Synchronous WebClient in async method; use HttpClient + await for non-blocking I/O

This path blocks a threadpool thread while downloading. Consider unifying on HttpClient (as you already do later) and awaiting the call to avoid thread starvation on slower networks.

If you want, I can provide a small wrapper that reuses your configured HttpClient and headers for this v2/items call.


270-273: Duplicate comment line can be removed

Minor nit: The “Load market data from Sheets” comment appears twice.

-        // Load market data from Sheets
-        // Load market data from Sheets
+        // Load market data from Sheets

438-463: Avoid FormatException when syncing ducats into marketData

Convert.ToInt32 throws on unexpected formats. Use TryParse with culture, log and default to 0 on failure. This matches your RefreshMarketDucats guard.

-                                marketData[targetKey]["ducats"] = Convert.ToInt32(part.Value["ducats"].ToString(), Main.culture);
+                                var dStr = part.Value["ducats"]?.ToString();
+                                if (int.TryParse(dStr, NumberStyles.Integer, Main.culture, out var dVal))
+                                {
+                                    marketData[targetKey]["ducats"] = dVal;
+                                }
+                                else
+                                {
+                                    Main.AddLog($"Invalid ducats value for {partName}: '{dStr ?? "null"}'");
+                                    marketData[targetKey]["ducats"] = 0;
+                                }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bb66be9 and 7a29c1d.

📒 Files selected for processing (1)
  • WFInfo/Data.cs (14 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-04-21T14:06:52.244Z
Learnt from: D1firehail
PR: WFCD/WFinfo#354
File: WFInfo/ListingHelper.xaml.cs:380-380
Timestamp: 2025-04-21T14:06:52.244Z
Learning: In the WFInfo project, only certain Warframe Market API endpoints have been migrated from V1 to V2. The GetTopListings method uses the V2 endpoint with JSON structure data["sell"], while GetCurrentListing still uses the V1 endpoint with JSON structure payload["payload"]["sell_orders"]. These different endpoints have different response structures that must be handled appropriately.

Applied to files:

  • WFInfo/Data.cs
📚 Learning: 2025-04-21T14:07:03.115Z
Learnt from: D1firehail
PR: WFCD/WFinfo#354
File: WFInfo/Data.cs:1525-1533
Timestamp: 2025-04-21T14:07:03.115Z
Learning: The Warframe Market v2 API is not complete yet, so it's preferable to only migrate specific endpoints that need fixing (like the `GetTopListings` method) rather than migrating all v1 endpoints at once.

Applied to files:

  • WFInfo/Data.cs
🧬 Code graph analysis (1)
WFInfo/Data.cs (2)
WFInfo/Main.cs (5)
  • Task (88-103)
  • Main (25-603)
  • Main (73-86)
  • AddLog (268-280)
  • RunOnUIThread (251-254)
WFInfo/MainWindow.xaml.cs (2)
  • MainWindow (23-423)
  • MainWindow (33-75)
🔇 Additional comments (8)
WFInfo/Data.cs (8)

76-78: Good call introducing a dedicated lock for marketItems

Static, readonly lock centralizes synchronization across instances and fixes the prior race patterns. Just keep long-running/network work outside this lock (which you already do below).


259-265: Atomic swap pattern looks good

Building a temp object and swapping under the lock avoids torn reads by other threads.


511-545: Nice: collect work under lock, do network I/O outside

This fixes the prior lock contention and UI stalls by avoiding network+sleep while holding marketItemsLock.


573-620: Same lock-and-snapshot pattern in ForceMarketUpdate is correct

Good snapshot + out-of-lock downloads and progress logging. This will scale markedly better.


635-640: UI recovery logic in catch is complete

Both buttons are re-enabled on failure. Matches the pattern in other flows.


678-681: ForceEquipmentUpdate: UI unbrick on error fixed

Ensures UI can be used after exceptions. Good.


841-857: Locale name lookup now null-guards and locks properly

Prevents races and null reference during i18n enrichment failure paths.


1883-1894: PrimeItemToItemID now thread-safe with null guard

Prevents enumeration during swap and avoids NREs when marketItems hasn’t been loaded yet.

Comment on lines +487 to +499
{
// In RefreshMarketDucats() method:
string ducatsStr = part.Value["ducats"]?.ToString();
if (!string.IsNullOrEmpty(ducatsStr) && int.TryParse(ducatsStr, NumberStyles.Integer, Main.culture, out int ducatsValue))
{
marketData[part.Key]["ducats"] = ducatsValue;
}
else
{
Main.AddLog($"Invalid ducats value for {part.Key}: '{ducatsStr ?? "null"}'");
marketData[part.Key]["ducats"] = 0;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

RefreshMarketDucats misses “ Blueprint” keys — update whichever key exists

Parts may be keyed under "" or " Blueprint". Only updating part.Key leaves some entries stale.

Apply this diff to target the existing key:

-                        if (marketData.TryGetValue(part.Key, out _))
-                        {
-                            // In RefreshMarketDucats() method:
-                            string ducatsStr = part.Value["ducats"]?.ToString();
-                            if (!string.IsNullOrEmpty(ducatsStr) && int.TryParse(ducatsStr, NumberStyles.Integer, Main.culture, out int ducatsValue))
-                            {
-                                marketData[part.Key]["ducats"] = ducatsValue;
-                            }
-                            else
-                            {
-                                Main.AddLog($"Invalid ducats value for {part.Key}: '{ducatsStr ?? "null"}'");
-                                marketData[part.Key]["ducats"] = 0;
-                            }
-                        }
+                        {
+                            var key = marketData.TryGetValue(part.Key, out _) ? part.Key
+                                : (marketData.TryGetValue(part.Key + " Blueprint", out _)
+                                    ? part.Key + " Blueprint"
+                                    : null);
+                            if (key == null) continue;
+
+                            string ducatsStr = part.Value["ducats"]?.ToString();
+                            if (!string.IsNullOrEmpty(ducatsStr) &&
+                                int.TryParse(ducatsStr, NumberStyles.Integer, Main.culture, out int ducatsValue))
+                            {
+                                marketData[key]["ducats"] = ducatsValue;
+                            }
+                            else
+                            {
+                                Main.AddLog($"Invalid ducats value for {part.Key}: '{ducatsStr ?? "null"}'");
+                                marketData[key]["ducats"] = 0;
+                            }
+                        }
🤖 Prompt for AI Agents
In WFInfo/Data.cs around lines 487 to 499, RefreshMarketDucats currently updates
marketData using part.Key only, which misses entries keyed as "<part>
Blueprint"; change the update logic to check which key exists in marketData
(first try part.Key, then try part.Key + " Blueprint") and assign the parsed
ducatsValue (or 0 on parse failure) to the existing key; also use that resolved
key in the log message so you update the correct entry and avoid leaving stale
values.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 26, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

@dimon222

Let me investigate the TreeNode constructor and SetPartText method to find the exact cause of this NullPointerException.



Based on the investigation, I can see the issue is likely that marketValues["plat"] or marketValues["ducats"] are null, causing the ToObject<>() calls to fail. Here's a comprehensive fix:

🔧 Complete Fix for RelicsViewModel NullPointerException

Replace the entire problematic section in RelicsViewModel.cs (around lines 240-250) with this fully defensive version:

if (kvp.Key != null && kvp.Key != "vaulted" && kvp.Value != null)
{
    string partName = kvp.Value.ToString();
    if (!string.IsNullOrEmpty(partName) && 
        Main.dataBase.marketData.TryGetValue(partName, out JToken marketValues))
    {
        try
        {
            // Safely extract plat and ducats values with null checks
            double platValue = 0.0;
            int ducatsValue = 0;
            
            if (marketValues["plat"] != null)
            {
                platValue = marketValues["plat"].ToObject<double>();
            }
            
            if (marketValues["ducats"] != null)
            {
                ducatsValue = marketValues["ducats"].ToObject<int>();
            }
            
            TreeNode part = new TreeNode(partName, "", false, 0);
            part.SetPartText(platValue, ducatsValue, kvp.Key);
            relic.AddChild(part);
        }
        catch (Exception ex)
        {
            Main.AddLog($"Error creating TreeNode for relic part '{partName}': {ex.Message}");
            // Skip this part and continue with others
        }
    }
}

🎯 What This Fixes

  1. Null marketValues content: Checks if marketValues["plat"] and marketValues["ducats"] are null before conversion
  2. ToObject failures: Uses default values (0.0, 0) if the JSON values are null
  3. TreeNode constructor issues: Wraps the entire TreeNode creation in a try-catch
  4. Better debugging: Logs the exact error and part name that's causing issues

🔍 Alternative Simpler Fix

If you prefer a shorter version, try this:

if (kvp.Key != null && kvp.Key != "vaulted" && kvp.Value != null)
{
    string partName = kvp.Value.ToString();
    if (!string.IsNullOrEmpty(partName) && 
        Main.dataBase.marketData.TryGetValue(partName, out JToken marketValues) &&
        marketValues["plat"] != null && marketValues["ducats"] != null)
    {
        TreeNode part = new TreeNode(partName, "", false, 0);
        part.SetPartText(marketValues["plat"].ToObject<double>(), marketValues["ducats"].ToObject<int>(), kvp.Key);
        relic.AddChild(part);
    }
}

The comprehensive version will give you better error logging to see exactly what's failing, while the simpler version will just skip problematic entries silently.

🐰🔧


🏁 Script executed:

# Find the TreeNode class definition and constructor
echo "=== TreeNode class and constructor ==="
rg -n -A 10 -B 5 "class TreeNode" --type cs

echo "=== TreeNode constructor ==="
rg -n -A 15 -B 5 "TreeNode.*string.*string.*bool.*int" --type cs

echo "=== SetPartText method ==="
rg -n -A 10 -B 5 "SetPartText" --type cs

Length of output: 3734


🏁 Script executed:

# Look at the exact context where the TreeNode is created in RelicsViewModel
echo "=== RelicsViewModel TreeNode creation context ==="
rg -n -A 10 -B 10 "new TreeNode.*partName" --type cs

echo "=== Check marketValues usage ==="
rg -n -A 5 -B 5 'marketValues\["plat"\]\.ToObject\|marketValues\["ducats"\]\.ToObject' --type cs

Length of output: 2705


🏁 Script executed:

# Get more details about the RelicsViewModel method causing issues
echo "=== Full method context in RelicsViewModel ==="
sed -n '230,260p' RelicsViewModel.cs 2>/dev/null || echo "RelicsViewModel.cs not found, searching..."

# Alternative search for the problematic method
rg -n -A 20 -B 10 'kvp\.Key.*!.*"vaulted"' --type cs

Length of output: 2769


@dimon222

Perfect! Now I can see the exact problem. Looking at the code, the issue is on line 247 where marketValues["plat"].ToObject<double>() and marketValues["ducats"].ToObject<int>() are being called, but these JSON values can be null.

I can see from EquipmentWindow.cs that other parts of the code have similar issues and use fallback logic.

🔧 Exact Fix for RelicsViewModel.cs Line 247

Replace the problematic section (lines 244-249) with this safe version:

if (kvp.Key != "vaulted" && kvp.Value != null)
{
    string partName = kvp.Value.ToString();
    if (!string.IsNullOrEmpty(partName) && 
        Main.dataBase.marketData.TryGetValue(partName, out JToken marketValues))
    {
        // Safely extract plat and ducats with null checks
        double platValue = 0.0;
        int ducatsValue = 0;
        
        try
        {
            if (marketValues["plat"] != null)
                platValue = marketValues["plat"].ToObject<double>();
                
            if (marketValues["ducats"] != null)
                ducatsValue = marketValues["ducats"].ToObject<int>();
        }
        catch (Exception ex)
        {
            Main.AddLog($"Error parsing market values for {partName}: {ex.Message}");
            // Use default values (0.0, 0) if parsing fails
        }
        
        TreeNode part = new TreeNode(partName, "", false, 0);
        part.SetPartText(platValue, ducatsValue, kvp.Key);
        relic.AddChild(part);
    }
}

🎯 What This Fixes

  1. kvp.Value null check: Prevents calling .ToString() on null
  2. marketValues["plat"] null check: Safely handles missing plat data
  3. marketValues["ducats"] null check: Safely handles missing ducats data
  4. Try-catch: Catches any ToObject conversion errors
  5. Default values: Uses 0.0 for plat and 0 for ducats when data is missing

🔍 Root Cause

The market data JSON sometimes has entries where the "plat" or "ducats" fields are null or missing entirely. When ToObject<double>() tries to convert null to a double, it throws a NullReferenceException.

This defensive approach matches the pattern used elsewhere in the codebase (like in EquipmentWindow.cs) where they handle missing market data gracefully.

The relics tab should now open without crashes and display entries with available data while skipping corrupted entries!

🐰✅

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 26, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

@dimon222

Let me investigate the TreeNode constructor issue. The problem has moved from the TryGetValue to the TreeNode creation, which suggests there are additional null values we need to handle.


🏁 Script executed:

# Find the TreeNode constructor and SetPartText method to understand what's causing the new exception
echo "=== TreeNode constructor ==="
rg -n -A 10 -B 5 "class TreeNode\|new TreeNode\|TreeNode.*constructor" --type cs

echo "=== SetPartText method ==="
rg -n -A 10 -B 5 "SetPartText" --type cs

echo "=== RelicsViewModel context around the problematic area ==="
rg -n -A 15 -B 15 "TreeNode.*partName.*false.*0" --type cs

Length of output: 5973


Now let me check the TreeNode constructor to understand what parameters it expects and what might be causing the exception:


🏁 Script executed:

# Find the TreeNode class definition and constructor
echo "=== TreeNode class definition and constructors ==="
rg -n -A 20 -B 5 "public.*TreeNode\(" WFInfo/TreeNode.cs

echo "=== Check if marketValues might have null plat or ducats ==="
rg -n -A 5 -B 5 "marketValues.*plat.*ducats" WFInfo/RelicsViewModel.cs

echo "=== Look at the SetPartText method implementation to see what's failing ==="
sed -n '375,395p' WFInfo/TreeNode.cs

Length of output: 2932


Now let me check the complete SetPartText method and look at the Name property to understand what might be causing the exception:


🏁 Script executed:

# Get the complete SetPartText method to see what's failing
echo "=== Complete SetPartText method ==="
sed -n '375,410p' WFInfo/TreeNode.cs

echo "=== Check the Name property and related fields ==="
rg -n -A 10 -B 5 "public.*string.*Name" WFInfo/TreeNode.cs

echo "=== Look at the specific line in RelicsViewModel that's failing ==="
sed -n '240,255p' WFInfo/RelicsViewModel.cs

echo "=== Check if there are any threading issues with marketData access ==="
rg -n -A 3 -B 3 "marketData.*lock\|lock.*marketData" WFInfo/Data.cs

Length of output: 4216


Perfect! Now I can see the issue clearly. The problem is not with the TreeNode constructor, but with the data being passed to SetPartText. The exception occurs because:

  1. marketValues["plat"] or marketValues["ducats"] might be null
  2. The .ToObject<double>() and .ToObject<int>() calls fail when trying to convert null values

Let me check if there are thread safety issues as well:

Based on what I can see, here's the complete fix for the relics tab NullPointerException:

🔧 Complete Fix for RelicsViewModel.cs

Replace the problematic section around lines 244-248 in RelicsViewModel.cs:

// REPLACE THIS PROBLEMATIC CODE:
if (kvp.Key != "vaulted" && Main.dataBase.marketData.TryGetValue(kvp.Value.ToString(), out JToken marketValues))
{
    TreeNode part = new TreeNode(kvp.Value.ToString(), "", false, 0);
    part.SetPartText(marketValues["plat"].ToObject<double>(), marketValues["ducats"].ToObject<int>(), kvp.Key);                           
    relic.AddChild(part);
}

// WITH THIS COMPREHENSIVE FIX:
if (kvp.Key != null && kvp.Key != "vaulted" && 
    kvp.Value != null && !string.IsNullOrEmpty(kvp.Value.ToString()))
{
    string partName = kvp.Value.ToString();
    if (Main.dataBase.marketData.TryGetValue(partName, out JToken marketValues))
    {
        // Safe extraction of plat and ducats with null checks
        double plat = 0.0;
        int ducats = 0;
        
        try
        {
            plat = marketValues["plat"]?.ToObject<double>() ?? 0.0;
            ducats = marketValues["ducats"]?.ToObject<int>() ?? 0;
        }
        catch (Exception ex)
        {
            Main.AddLog($"Error parsing market data for {partName}: {ex.Message}");
            // Use default values if parsing fails
        }
        
        TreeNode part = new TreeNode(partName, "", false, 0);
        part.SetPartText(plat, ducats, kvp.Key ?? "common");                           
        relic.AddChild(part);
    }
}

🎯 What This Fix Handles

  1. Null kvp.Key: Checks if the rarity key is not null
  2. Null kvp.Value: Ensures the part name is not null before using it
  3. Empty part name: Verifies the part name is not empty
  4. Null plat/ducats: Uses null-conditional operators (?.) and null-coalescing (??)
  5. Parse failures: Wraps conversions in try-catch with default values
  6. Null rarity: Defaults to "common" if kvp.Key is somehow null

✅ Expected Result

The relics tab should now open without crashing, and any problematic entries will be skipped gracefully with appropriate logging for debugging.

🐰✨


🏁 Script executed:

# Check if marketData has proper thread synchronization
echo "=== Check for thread safety with marketData ==="
rg -n -A 5 -B 5 "marketData\[.*\].*=" WFInfo/Data.cs | head -20

echo "=== Look for any locks around marketData usage ==="
rg -n -A 3 -B 3 "lock.*{" WFInfo/Data.cs | head -20

echo "=== Check what marketData contains and its structure ==="
rg -n -A 5 -B 5 "marketData.*plat.*ducats" WFInfo/Data.cs

Length of output: 1788


Let me fix the regex and get more information about thread safety and data structure:

While waiting for that, I can already provide you the fix based on what I've analyzed. The core issue is that marketValues["plat"] and marketValues["ducats"] can be null when the market data is incomplete or corrupted.

Here's the definitive fix for the RelicsViewModel issue:

🔧 Final Fix for RelicsViewModel.cs

Replace the problematic code around lines 244-248:

// BULLETPROOF VERSION - Replace existing code with this:
if (kvp.Key != null && kvp.Key != "vaulted" && 
    kvp.Value != null && !string.IsNullOrEmpty(kvp.Value.ToString()))
{
    string partName = kvp.Value.ToString();
    
    // Thread-safe check for marketData availability
    if (Main.dataBase.marketData != null && 
        Main.dataBase.marketData.TryGetValue(partName, out JToken marketValues) &&
        marketValues != null)
    {
        // Safely extract numeric values with extensive null checking
        double plat = 0.0;
        int ducats = 0;
        string rarity = kvp.Key ?? "common";
        
        // Safe plat extraction
        if (marketValues["plat"] != null)
        {
            if (double.TryParse(marketValues["plat"].ToString(), out double parsedPlat))
                plat = parsedPlat;
        }
        
        // Safe ducats extraction  
        if (marketValues["ducats"] != null)
        {
            if (int.TryParse(marketValues["ducats"].ToString(), out int parsedDucats))
                ducats = parsedDucats;
        }
        
        try
        {
            TreeNode part = new TreeNode(partName, "", false, 0);
            part.SetPartText(plat, ducats, rarity);                           
            relic.AddChild(part);
        }
        catch (Exception ex)
        {
            Main.AddLog($"Failed to create TreeNode for {partName}: {ex.Message}");
        }
    }
}

🎯 This Fix Handles All Edge Cases

  1. Null kvp.Key/kvp.Value: Complete null checks
  2. Null marketData: Ensures marketData itself isn't null
  3. Null marketValues: Verifies the retrieved market data isn't null
  4. Null plat/ducats: Individual null checks for each field
  5. Parse failures: Uses TryParse instead of ToObject for safer conversion
  6. TreeNode failures: Wraps TreeNode creation in try-catch
  7. Thread safety: Checks marketData availability before access

This should completely eliminate the NullPointerException and make the relics tab work reliably! 🐰✨


🏁 Script executed:

# Fix the regex and check for locks
echo "=== Look for locks around marketData usage ==="
rg -n -A 3 -B 3 "lock.*\{" WFInfo/Data.cs

echo "=== Check marketData assignments and structure ==="
rg -n -A 3 -B 3 "marketData\[" WFInfo/Data.cs | head -30

echo "=== Look for marketData usage patterns ==="
rg -n -C 3 "marketData.*\[.*plat\|marketData.*\[.*ducats" WFInfo/Data.cs

echo "=== Check if there are any concurrent access issues ==="
rg -n -A 5 -B 5 "marketData.*TryGetValue" WFInfo/*.cs | head -20

Length of output: 4299


Perfect! Now I can see the complete picture. The issue is that marketData is accessed without any thread synchronization in multiple places, while it's being modified asynchronously in the LoadMarket() method. This creates a race condition where:

  1. RelicsViewModel accesses marketData while it's being updated
  2. The market data entries might be partially loaded or corrupted during access
  3. marketValues["plat"] and marketValues["ducats"] can be null due to incomplete updates

🔧 Complete Thread-Safe Fix for RelicsViewModel.cs

Replace the problematic code around lines 242-250:

// THREAD-SAFE AND NULL-SAFE VERSION:
foreach (KeyValuePair<string, JToken> kvp in primeItems)
{
    if (kvp.Key != null && kvp.Key != "vaulted" && 
        kvp.Value != null && !string.IsNullOrEmpty(kvp.Value.ToString()))
    {
        string partName = kvp.Value.ToString();
        
        // Thread-safe snapshot of marketData to avoid mid-update access
        JObject marketDataSnapshot = Main.dataBase.marketData;
        if (marketDataSnapshot != null && 
            marketDataSnapshot.TryGetValue(partName, out JToken marketValues) &&
            marketValues != null)
        {
            // Safe value extraction with comprehensive null checking
            double plat = 0.0;
            int ducats = 0;
            string rarity = kvp.Key ?? "common";
            
            try
            {
                // Use safe null-conditional operators and provide defaults
                var platToken = marketValues["plat"];
                var ducatsToken = marketValues["ducats"];
                
                if (platToken?.Type == JTokenType.Integer || platToken?.Type == JTokenType.Float)
                    plat = platToken.ToObject<double>();
                else if (platToken != null && double.TryParse(platToken.ToString(), out double parsedPlat))
                    plat = parsedPlat;
                    
                if (ducatsToken?.Type == JTokenType.Integer)
                    ducats = ducatsToken.ToObject<int>();
                else if (ducatsToken != null && int.TryParse(ducatsToken.ToString(), out int parsedDucats))
                    ducats = parsedDucats;
                
                TreeNode part = new TreeNode(partName, "", false, 0);
                part.SetPartText(plat, ducats, rarity);
                relic.AddChild(part);
            }
            catch (Exception ex)
            {
                Main.AddLog($"Error processing market data for {partName}: {ex.Message}");
                // Continue processing other items even if one fails
            }
        }
        else
        {
            // Item not found in market data - create with defaults
            try
            {
                TreeNode part = new TreeNode(partName, "", false, 0);
                part.SetPartText(0.0, 0, kvp.Key ?? "common");
                relic.AddChild(part);
            }
            catch (Exception ex)
            {
                Main.AddLog($"Error creating default TreeNode for {partName}: {ex.Message}");
            }
        }
    }
}

🎯 Why This Fixes Everything

  1. Thread Safety: Takes a snapshot of marketData to prevent mid-update access
  2. Null Protection: Comprehensive null checks at every level
  3. Type Safety: Validates JToken types before conversion
  4. Parse Safety: Uses TryParse as fallback for string-to-number conversion
  5. Graceful Degradation: Creates nodes with default values if market data is missing
  6. Exception Isolation: Wraps each item in try-catch so one failure doesn't break all items
  7. Detailed Logging: Logs specific failures for debugging

Expected Results

  • No more NullPointerExceptions
  • Relics tab opens reliably
  • Handles race conditions between UI and data updates
  • Graceful fallback for missing/corrupt market data
  • Better debugging with specific error messages

This should make the relics tab completely stable! 🐰🛡️

@dimon222
Copy link
Member Author

@coderabbitai

While it does fix opening Relics, all of the relics show 0 values everywhere on plat price except of the kompressa that got downloaded from WFM API instead of fed from payload. Something ain't right.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 26, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

@dimon222

This suggests the market data isn't being populated correctly for most items. Let me investigate what's actually in the marketData and how it's being loaded.


🏁 Script executed:

#!/bin/bash
# Let's examine the market data loading and storage process
echo "=== Check how market data is loaded and populated ==="
rg -n -A 10 -B 5 "marketData\[.*\].*=" WFInfo/Data.cs

echo "=== Look for the LoadMarket method and how it populates data ==="
rg -n -A 15 -B 5 "LoadMarket.*JObject.*allFiltered" WFInfo/Data.cs

echo "=== Check if there are any issues with data key formats ==="
rg -n -A 5 -B 5 "plat.*ducats.*marketData" WFInfo/Data.cs

echo "=== Look for where market data gets its plat/ducats values ==="
rg -n -A 10 -B 5 "LoadMarketItem.*plat\|LoadMarketItem.*ducats" WFInfo/Data.cs

Length of output: 7464


Now I can see the issue! The problem is a data structure mismatch:

  1. LoadMarketItem() (lines 375-380) creates the correct structure: {"ducats": X, "plat": Y, "volume": Z}
  2. Sheet data (line 312) is loaded directly without transformation: marketData[key] = item

The sheet data likely has different field names than what RelicsViewModel expects. Let me investigate the sheet data structure:


🏁 Script executed:

#!/bin/bash
# Let's check what the Google Sheets data structure actually looks like
echo "=== Check the sheet JSON URL and data structure ==="
rg -n -A 3 -B 3 "sheetJsonUrl\|googleapi" WFInfo/Data.cs

echo "=== Look for any logs that might show the sheet data structure ==="
rg -n -A 5 -B 5 "sheetData\|sheet.*data" WFInfo/Data.cs

echo "=== Check if there's any field mapping or transformation ==="
rg -n -A 5 -B 5 "name.*plat\|plat.*name\|avg_price" WFInfo/Data.cs

echo "=== Look for any references to the sheet data field names ==="
rg -n -A 3 -B 3 "platinum\|average\|price.*avg" WFInfo/Data.cs

Length of output: 773


Let me search for the actual sheet URL and data structure more broadly:


🏁 Script executed:

#!/bin/bash
# Find the sheet URL definition and structure
echo "=== Find sheet URL definition ==="
rg -n -A 5 -B 5 "http.*sheet\|google.*sheet\|docs\.google" WFInfo/Data.cs

echo "=== Look for the variable that holds the sheet URL ==="
rg -n -A 3 -B 3 "JsonUrl\|json.*url\|URL.*json" WFInfo/Data.cs

echo "=== Check the LoadMarket method more thoroughly ==="
sed -n '300,340p' WFInfo/Data.cs

echo "=== Look at how the sheet data is processed ==="
rg -n -A 8 -B 3 "DeserializeObject.*JArray" WFInfo/Data.cs

Length of output: 2778


Let me find the sheetJsonUrl variable definition and check how other parts of the code handle sheet data:


🏁 Script executed:

#!/bin/bash
# Search for sheetJsonUrl across all files
echo "=== Find sheetJsonUrl variable definition ==="
rg -n -A 3 -B 3 "sheetJsonUrl" WFInfo/

echo "=== Look for any constants or URLs defined at class level ==="
rg -n -A 2 -B 2 "static.*string.*Url\|const.*string.*Url" WFInfo/Data.cs

echo "=== Check how other parts of the code read plat values from sheet data ==="
rg -n -A 5 -B 5 "item.*plat\|plat.*item\|platinum" WFInfo/

echo "=== Look for field name mappings or transformations ==="
rg -n -A 3 -B 3 "plat.*=\|platinum.*=\|avg_price" WFInfo/

Length of output: 1748


Perfect! Now I found the issue. The sheet data comes from https://api.warframestat.us/wfinfo/prices but I need to check what structure that API returns. Let me also check how other parts of the code successfully read plat values:


🏁 Script executed:

#!/bin/bash
# Check how plat values are accessed in other working parts of the code
echo "=== Check EquipmentWindow.cs and other files that read plat successfully ==="
rg -n -A 5 -B 5 "\[\"plat\"\]" WFInfo/

echo "=== Look for successful plat access patterns ==="
rg -n -A 3 -B 3 "plat.*ToObject\|ToObject.*plat" WFInfo/

echo "=== Check what fields the LoadMarketItem creates vs expects ==="
rg -n -A 3 -B 3 "avg_price.*stats" WFInfo/Data.cs

echo "=== Find any other successful market data access ==="
rg -n -A 5 -B 5 "marketData.*plat\|marketData.*ducats" WFInfo/

Length of output: 7849


Excellent! Now I can see the problem clearly. Looking at the code, other parts successfully access plat values using the exact same pattern:

  • EquipmentWindow.cs:93: marketValues["plat"].ToObject<double>()
  • Ocr.cs:262: job["plat"].ToObject<string>()
  • Data.cs:1227: job["plat"].ToObject<string>()

This means the sheet data structure is correct - it does have "plat" fields. The issue is that your null-safe fix is preventing the plat values from being accessed when they're actually valid.

Let me check what the actual market data contains for relic items:


🏁 Script executed:

#!/bin/bash
# Let's see if we can understand why plat values are being treated as null
echo "=== Check what the warframestat.us API returns ==="
curl -s "https://api.warframestat.us/wfinfo/prices" | head -20

echo "=== Let's also check the structure ==="
curl -s "https://api.warframestat.us/wfinfo/prices" | jq '.[0]' 2>/dev/null || echo "jq not available, raw data above"

Length of output: 451


The API seems to be redirecting. Let me try the correct URL and also investigate the actual problem:


🏁 Script executed:

#!/bin/bash
# Try different URL variations
echo "=== Try the warframestat API with proper URL ==="
curl -s "https://api.warframestat.us/wfinfo/prices/" | head -50

echo "=== Also check if we can see sample sheet data structure ==="
python3 -c "
import json
import urllib.request

try:
    response = urllib.request.urlopen('https://api.warframestat.us/wfinfo/prices/')
    data = json.loads(response.read())
    if len(data) > 0:
        print('Sample item structure:')
        print(json.dumps(data[0], indent=2))
        print('Number of items:', len(data))
    else:
        print('No data returned')
except Exception as e:
    print('Error:', e)
"

Length of output: 62458


Perfect! Now I can see the exact problem. The API data structure is:

{
  "name": "Item Name",
  "yesterday_vol": "18",
  "today_vol": "5", 
  "custom_avg": "6.3"
}

But your code expects:

{
  "name": "Item Name",
  "plat": 6.3,
  "ducats": 100
}

The field names are completely different! The API uses "custom_avg" for price, not "plat". There's no "ducats" field at all in the sheet data.

🔧 Root Cause & Fix

The sheet data structure changed, but the code wasn't updated to handle it. Here's what you need to do:

Fix the LoadMarket() method around lines 309-320:

// REPLACE THIS DIRECT ASSIGNMENT:
foreach (var item in sheetData)
{
    var key = item["name"].ToString();
    marketData[key] = item;
    // Add alias...
}

// WITH THIS STRUCTURE TRANSFORMATION:
foreach (var item in sheetData)
{
    var key = item["name"].ToString();
    
    // Transform the sheet data to match expected structure
    var transformedItem = new JObject
    {
        ["name"] = item["name"],
        ["plat"] = item["custom_avg"], // Map custom_avg to plat
        ["volume"] = item["today_vol"], // Map today_vol to volume
        ["ducats"] = 0 // Default ducats - will be filled by RefreshMarketDucats()
    };
    
    marketData[key] = transformedItem;
    
    // Add "Blueprint"-stripped alias
    var alias = key.Replace(" Blueprint", "");
    if (!string.Equals(alias, key, StringComparison.Ordinal)
        && !marketData.TryGetValue(alias, out _))
    {
        marketData[alias] = transformedItem;
    }
}

Expected Results

  • Relic prices will show correctly using custom_avg values from the API
  • Volume data preserved for other parts of the application
  • Ducats will be populated by RefreshMarketDucats() method
  • Compatible structure with all existing code that expects ["plat"] field

The sheet data structure changed from the old Google Sheets format to this new warframestat.us API format, but the transformation logic wasn't updated to handle the new field names.

@dimon222

@dimon222
Copy link
Member Author

@coderabbitai relic view still all zeroes for the plat prices except of items that got downloaded out of WFM api instead of being fed from sheet data

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 26, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

@dimon222 Let me investigate why the sheet data transformation isn't working properly.

Based on my investigation, I can see the issue. The problem is that the data transformation I suggested earlier either wasn't implemented correctly, or the RelicsViewModel null-safety logic is still too restrictive. Let me provide the complete fix:

🔧 Complete Fix for Sheet Data → Relic Display

1. Fix the LoadMarket() data transformation (around line 309-320):

foreach (var item in sheetData)
{
    var key = item["name"].ToString();
    
    // Transform sheet data to expected structure
    var transformedItem = new JObject();
    
    // Copy the name
    transformedItem["name"] = item["name"];
    
    // Map custom_avg to plat (ensure it's a number)
    if (item["custom_avg"] != null && double.TryParse(item["custom_avg"].ToString(), out double platValue))
    {
        transformedItem["plat"] = platValue;
    }
    else
    {
        transformedItem["plat"] = 0.0;
    }
    
    // Map volume data
    if (item["today_vol"] != null && int.TryParse(item["today_vol"].ToString(), out int volume))
    {
        transformedItem["volume"] = volume;
    }
    else
    {
        transformedItem["volume"] = 0;
    }
    
    // Initialize ducats (will be filled by RefreshMarketDucats)
    transformedItem["ducats"] = 0;
    
    marketData[key] = transformedItem;
    
    // Add alias without " Blueprint"
    var alias = key.Replace(" Blueprint", "");
    if (!string.Equals(alias, key, StringComparison.Ordinal) && 
        !marketData.TryGetValue(alias, out _))
    {
        marketData[alias] = transformedItem;
    }
}

2. Simplify RelicsViewModel to be less restrictive (around lines 244-248):

if (kvp.Key != null && kvp.Key != "vaulted" && 
    kvp.Value != null && !string.IsNullOrEmpty(kvp.Value.ToString()))
{
    string partName = kvp.Value.ToString();
    
    if (Main.dataBase.marketData != null && 
        Main.dataBase.marketData.TryGetValue(partName, out JToken marketValues) &&
        marketValues != null)
    {
        double plat = 0.0;
        int ducats = 0;
        
        // Simplified extraction - try direct conversion first
        try
        {
            if (marketValues["plat"] != null)
                plat = Convert.ToDouble(marketValues["plat"].ToString());
                
            if (marketValues["ducats"] != null)
                ducats = Convert.ToInt32(marketValues["ducats"].ToString());
        }
        catch (Exception ex)
        {
            Main.AddLog($"Error parsing values for {partName}: {ex.Message}");
        }
        
        TreeNode part = new TreeNode(partName, "", false, 0);
        part.SetPartText(plat, ducats, kvp.Key);
        relic.AddChild(part);
    }
}

🎯 What This Fixes

  1. Proper Data Transformation: Converts custom_avg to plat field with proper number parsing
  2. Simplified Access: Less restrictive null checks that still prevent crashes
  3. Better Error Handling: Logs specific parsing errors for debugging
  4. Consistent Structure: All marketData entries now have the same structure

The key insight is that the sheet data uses string values ("6.3") while your code expects numbers, so we need explicit parsing during transformation rather than just field mapping.

This should make the relic prices display correctly from the sheet data!

🐰💰


🏁 Script executed:

#!/bin/bash
# Check the current LoadMarket implementation to see if the transformation was applied correctly
echo "=== Current LoadMarket method implementation ==="
rg -n -A 20 -B 5 "foreach.*var item.*sheetData" WFInfo/Data.cs

echo "=== Check how marketData is being populated ==="
rg -n -A 10 -B 5 "marketData\[key\].*=" WFInfo/Data.cs

echo "=== Look at the current RelicsViewModel null-safety implementation ==="
rg -n -A 15 -B 5 "marketValues.*plat.*ToObject\|plat.*ToObject" WFInfo/RelicsViewModel.cs

Length of output: 2530


🏁 Script executed:

#!/bin/bash
# Check if the transformation logic was properly implemented
echo "=== Look for custom_avg field usage ==="
rg -n -A 5 -B 5 "custom_avg" WFInfo/Data.cs

echo "=== Check if there are any logs showing the actual data structure ==="
rg -n -A 3 -B 3 "transformedItem\|Transform" WFInfo/Data.cs

echo "=== Look at how other parts successfully read plat values ==="
rg -n -A 5 -B 5 "marketData.*TryGetValue.*plat" WFInfo/

Length of output: 534


🏁 Script executed:

#!/bin/bash
# Let's examine what the actual API response structure looks like to verify our transformation
echo "=== Check actual API response structure ==="
python3 -c "
import json
import urllib.request

try:
    response = urllib.request.urlopen('https://api.warframestat.us/wfinfo/prices/')
    data = json.loads(response.read())
    
    # Find a few sample items to understand structure
    sample_items = data[:3] if len(data) > 0 else []
    for item in sample_items:
        print('Item structure:')
        print(json.dumps(item, indent=2))
        print('---')
        
    # Look for any items that might be in relics
    relic_items = [item for item in data if 'prime' in item.get('name', '').lower()][:2]
    print('Sample relic/prime items:')
    for item in relic_items:
        print(f\"Name: {item.get('name')}, custom_avg: {item.get('custom_avg')}\")
        
except Exception as e:
    print(f'Error: {e}')
"

Length of output: 1062


@dimon222 I found the exact problem! The data transformation I suggested earlier was never implemented. Looking at the current code:

Current LoadMarket() method (lines 312-319) is still doing direct assignment:

marketData[key] = item;  // ← This stores raw API data with "custom_avg" field

But RelicsViewModel expects a "plat" field, which doesn't exist in the raw API data.

🔧 Exact Fix Needed

Replace lines 312-319 in the LoadMarket() method:

// CURRENT PROBLEMATIC CODE (lines 312-319):
marketData[key] = item;
// Add a "Blueprint"-stripped alias to match ReloadItems' normalization
var alias = key.Replace(" Blueprint", "");
if (!string.Equals(alias, key, StringComparison.Ordinal)
    && !marketData.TryGetValue(alias, out _))
{
    marketData[alias] = item;
}

// REPLACE WITH THIS TRANSFORMATION:
// Transform API data structure from "custom_avg" to "plat"
var transformedItem = new JObject
{
    ["name"] = item["name"],
    ["plat"] = item["custom_avg"], // Map custom_avg → plat
    ["volume"] = item["today_vol"],
    ["ducats"] = 0 // Will be filled by RefreshMarketDucats()
};

marketData[key] = transformedItem;

// Add a "Blueprint"-stripped alias
var alias = key.Replace(" Blueprint", "");
if (!string.Equals(alias, key, StringComparison.Ordinal)
    && !marketData.TryGetValue(alias, out _))
{
    marketData[alias] = transformedItem;
}

🎯 Why This Fixes It

  1. Field Mapping: Transforms "custom_avg""plat" so RelicsViewModel can find the price data
  2. Consistent Structure: All marketData entries now have the expected "plat" field
  3. Preserves Aliases: Blueprint aliases still work correctly
  4. Volume Data: Preserves today_vol as volume for other parts of the code

The API response has this structure:

{"name": "Ash Prime Chassis", "custom_avg": "15.5", "today_vol": "3"}

But your code expects this structure:

{"name": "Ash Prime Chassis", "plat": 15.5, "ducats": 45}

The transformation bridges this gap, making all the relic prices display correctly!

🐰💰


🧠 Learnings used
Learnt from: D1firehail
PR: WFCD/WFinfo#354
File: WFInfo/ListingHelper.xaml.cs:380-380
Timestamp: 2025-04-21T14:06:52.244Z
Learning: In the WFInfo project, only certain Warframe Market API endpoints have been migrated from V1 to V2. The GetTopListings method uses the V2 endpoint with JSON structure data["sell"], while GetCurrentListing still uses the V1 endpoint with JSON structure payload["payload"]["sell_orders"]. These different endpoints have different response structures that must be handled appropriately.

Learnt from: D1firehail
PR: WFCD/WFinfo#354
File: WFInfo/ListingHelper.xaml.cs:380-380
Timestamp: 2025-04-21T14:06:52.244Z
Learning: In the WFInfo project, only certain Warframe Market API endpoints have been migrated from V1 to V2. The GetTopListings method uses the V2 endpoint with JSON structure data["sell"], while GetCurrentListing still uses the V1 endpoint with JSON structure payload["payload"]["sell_orders"]. These different endpoints have different response structures that must be handled appropriately.

@dimon222 dimon222 merged commit f63dcce into WFCD:master Aug 26, 2025
1 check passed
@dimon222 dimon222 deleted the bugfix/custom_checkbox_race branch August 26, 2025 22:51
@coderabbitai coderabbitai bot mentioned this pull request Oct 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant