Add custom checkbox style, refactor to avoid async issues#367
Add custom checkbox style, refactor to avoid async issues#367dimon222 merged 8 commits intoWFCD:masterfrom
Conversation
WalkthroughConverts core market-data and startup flows to async/await, introduces a lock-and-atomic-swap pattern for Changes
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
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 detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 💡 Knowledge Base configuration:
You can enable these sources in your CodeRabbit configuration. 📒 Files selected for processing (1)
✅ Files skipped from review due to trivial changes (1)
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
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 resultsOverwriting 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 tokenThe 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 inThreadedDataLoad: switch toTask, await properly, callMain.LoggedIn, and signal completionTo restore AFK-timer startup, fire subscribers, and improve error handling, apply these changes:
• Change the signature to return a
Taskinstead ofasync voidand rename accordingly.
• Await the JWT check instead of using.Resultto avoid deadlocks.
• On successful login, invoke the staticMain.LoggedIn(not theMainWindowmethod) so that any side-effects (AFK timer, services) run.
• After all initialization steps complete, callFinishedLoading()so subscribers registered viaRegisterOnFinishedLoading(...)fire.
• At the call site, replace theTask.Factory.StartNewinvocation withTask.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.csor other services usingRegisterOnFinishedLoading(...)now runs their handlers (e.g. your AFK timer starts when a valid JWT exists).
• Ensure there are no other call sites toThreadedDataLoadleft 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 aliasSwitching 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 swapYou 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.SleepTwo 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” logsOnce 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 ForceMarketUpdateThis 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: HandleUpdate()return value to surface failuresUpon inspection,
Data.Update()is indeed declared as returningTask<bool>(seeWFInfo/Data.cs:461) and thus indicates success or failure. Currently, the call inWFInfo/Main.csignores 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 inWFInfo/Data.csat 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 callsTo 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 theitem_name, a per‐cycle correlation ID,Thread.CurrentThread.ManagedThreadId, plusmarketItems.CountandmarketData.Count.• WFInfo/Data.cs:188 & 255
– Atomic swaps ofmarketItemsunderlock (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 ofReloadItems()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.
📒 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 AuthenticationPlease verify that the following remain correct as of today:
- v1 Statistics endpoint
‣ GEThttps://api.warframe.market/v1/items/{url_name}/statistics
• Response payload should includepayload.statistics_closed["90days"]entries, each withavg_priceandvolumefields.- v2 Item endpoint
GEThttps://api.warframe.market/v2/item/{url_name}
•data.ducatsmust be present in the returned JSON.- Authentication scheme for v2
• All v2 requests should useAuthorization: Bearer <JWT>
• Double-check whether any write operations (e.g., posting reviews) require the alternateAuthorization: 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 templateThis 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?
| 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(); | ||
| }); | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| 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.
|
Pls give suggestion given in #367 (comment) after rebasing based on latest commit |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 13180 🏁 Script executed: Length of output: 2586 🏁 Script executed: Length of output: 3088 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 NeededIn the // 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 // 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() ChangesAll 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 SpamThe root cause is a name mismatch: The enhanced lookup logic will try both name variants before deciding an item is truly missing, eliminating the false positives. 🐰✨ |
There was a problem hiding this comment.
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()andForceMarketUpdate()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 inLoadMarketItemcould 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.
📒 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
marketItemsis 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 ensuresmarketItemsis 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
marketItemsinGetLocaleNameDataand 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()andForceMarketUpdate()to async methods with properTaskreturn 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
tempMarketItemsbefore the atomic swap ensures version consistency. Good practice.
|
I did what u said but it still gives nullpointerexception on "nameData[gameName] = partName;" |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 9342 🏁 Script executed: Length of output: 3910 🏁 Script executed: Length of output: 5698 I found the issue! The problem is that Here's the fix you need to add null checks around the problematic code: 🔧 Fix for the NullPointerExceptionReplace 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
🔍 Root CauseThe JSON data from the API sometimes contains entries where This should eliminate the NullPointerException while preserving all the functionality! 🐰 |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
WFInfo/Data.cs (3)
214-221: Nit: centralize “Blueprint” normalization to avoid driftYou 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 nameSince 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 hotLocking + 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.
📒 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 loggingWrapping 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 correctSwapping the fully built tempMarketItems into marketItems under the lock avoids torn reads and races. Nicely done.
291-300: Good: await ReloadItems with failure isolationAwaiting 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 ForceEquipmentUpdateButtons 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 lookupLock + null-guard around marketItems is appropriate here.
| 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 | ||
| } |
There was a problem hiding this comment.
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.
| 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| 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"); |
There was a problem hiding this comment.
🛠️ 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.
| 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.
|
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. |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chainLet 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: Length of output: 2432 🏁 Script executed: Length of output: 6085 Perfect! I found both issues. Let me provide the fixes: 🐌 Performance IssueThe refresh is slow because 💥 FormatException IssueLine 480 in 🔧 Fixes Required1. Fix Performance - Make ReloadItems() Conditional: In the // 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 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
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. 🐰 |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chainI can see the issues! Let me analyze both problems: 🐌 Performance IssueThe slowness is likely caused by the dual lookup logic we added. Each item now does two ❌ FormatException IssueThe error at line 476 in 🔧 Performance FixLet'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 FixIn 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
This should bring the refresh time back to normal while preventing the format exceptions! 🐰⚡ 🏁 Script executed: Length of output: 4413 🏁 Script executed: Length of output: 385 🏁 Script executed: Length of output: 1604 Perfect! Now I can see the exact issues: 🎯 FormatException FixLine 480 in // 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 OptimizationThe dual lookup is working but can be optimized. Replace your current dual lookup logic: In both // 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 InvestigationLet'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 🚀 Quick Wins
Try the FormatException fix first - this should eliminate the crashes and might also improve performance if exceptions were causing retries! 🐰🔧 🏁 Script executed: Length of output: 1849 🎯 Found the Performance Culprit! The slowness is caused by
If you have 10 missing items: 10 × 333ms = 3.3+ seconds just in sleep delays, plus network time! 🔧 Complete Fix Package1. 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 🐰⚡ |
There was a problem hiding this comment.
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 bugAwaiting 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 restoredCovers 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 DeepCloneAssigning 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 loopIf 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 splitLocking 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 nitLogic 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 threadsThis 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 readabilityIt’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.
📒 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 accessIntroducing 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 goodBuilding 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 lookupThis 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.
|
It works well now, but I'm unable to open relics tab, it throw NullPointerException on |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chainLet me investigate the relics tab issue and find where this NullPointerException is occurring. 🏁 Script executed: Length of output: 40001 Perfect! I found the exact issue. The problem is in RelicsViewModel.cs at line 244. The 🔧 Fix for the Relics Tab NullPointerExceptionReplace this problematic line in // 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 CauseThe relic data JSON from the API sometimes contains entries where the part name (
The null check ensures we skip these invalid entries instead of crashing when trying to look them up in the market data. ✅ Expected ResultAfter this fix, the relics tab should open properly and display all valid relic entries while gracefully skipping any corrupted/null entries in the data. 🐰✨ |
|
Both versions suggested throw nullpointerexception. Defensive one raises it on |
There was a problem hiding this comment.
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 delayTwo 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 DeepCloneJSON.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 failuresThis 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/OThis 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 removedMinor 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 marketDataConvert.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.
📒 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 marketItemsStatic, 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 goodBuilding 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 outsideThis 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 correctGood snapshot + out-of-lock downloads and progress logging. This will scale markedly better.
635-640: UI recovery logic in catch is completeBoth buttons are re-enabled on failure. Matches the pattern in other flows.
678-681: ForceEquipmentUpdate: UI unbrick on error fixedEnsures UI can be used after exceptions. Good.
841-857: Locale name lookup now null-guards and locks properlyPrevents races and null reference during i18n enrichment failure paths.
1883-1894: PrimeItemToItemID now thread-safe with null guardPrevents enumeration during swap and avoids NREs when marketItems hasn’t been loaded yet.
| { | ||
| // 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chainLet 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 🔧 Complete Fix for RelicsViewModel NullPointerExceptionReplace the entire problematic section in 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
🔍 Alternative Simpler FixIf 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: Length of output: 3734 🏁 Script executed: Length of output: 2705 🏁 Script executed: Length of output: 2769 Perfect! Now I can see the exact problem. Looking at the code, the issue is on line 247 where I can see from 🔧 Exact Fix for RelicsViewModel.cs Line 247Replace 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
🔍 Root CauseThe market data JSON sometimes has entries where the 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! 🐰✅ |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chainLet 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: 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: 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: 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
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.csReplace the problematic section around lines 244-248 in // 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
✅ Expected ResultThe relics tab should now open without crashing, and any problematic entries will be skipped gracefully with appropriate logging for debugging. 🐰✨ 🏁 Script executed: 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 Here's the definitive fix for the RelicsViewModel issue: 🔧 Final Fix for RelicsViewModel.csReplace 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
This should completely eliminate the NullPointerException and make the relics tab work reliably! 🐰✨ 🏁 Script executed: Length of output: 4299 Perfect! Now I can see the complete picture. The issue is that
🔧 Complete Thread-Safe Fix for RelicsViewModel.csReplace 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
✅ Expected Results
This should make the relics tab completely stable! 🐰🛡️ |
|
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. |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chainThis 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: Length of output: 7464 Now I can see the issue! The problem is a data structure mismatch:
The sheet data likely has different field names than what RelicsViewModel expects. Let me investigate the sheet data structure: 🏁 Script executed: Length of output: 773 Let me search for the actual sheet URL and data structure more broadly: 🏁 Script executed: Length of output: 2778 Let me find the sheetJsonUrl variable definition and check how other parts of the code handle sheet data: 🏁 Script executed: Length of output: 1748 Perfect! Now I found the issue. The sheet data comes from 🏁 Script executed: 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:
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: Length of output: 451 The API seems to be redirecting. Let me try the correct URL and also investigate the actual problem: 🏁 Script executed: 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 🔧 Root Cause & FixThe 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
The sheet data structure changed from the old Google Sheets format to this new |
|
@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 |
|
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 Display1. 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
The key insight is that the sheet data uses string values ( This should make the relic prices display correctly from the sheet data! 🐰💰 🏁 Script executed: Length of output: 2530 🏁 Script executed: Length of output: 534 🏁 Script executed: 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" fieldBut RelicsViewModel expects a "plat" field, which doesn't exist in the raw API data. 🔧 Exact Fix NeededReplace 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
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 |
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
Bug Fixes
Refactor
Style
Other