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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
## Custom ignores
# Static icon data
# Static icon data (downloaded from RatScannerData releases for local dev)
RatScanner/[Dd]ata/*
# Shipped with the app: updated magnifier template for BSG's post-patch inspect UI.
# RatEye 4.x no longer embeds this asset; Name Scan loads it from Data/icon_search.png.
!RatScanner/[Dd]ata/icon_search.png

# publish folder
publish/
Expand Down
16 changes: 16 additions & 0 deletions .vscode/launch.json

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

the .vscode/ dir is not on the .gitignore normally?

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "RatScanner (Debug)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/RatScanner/bin/Debug/net10.0-windows10.0.22621.0/RatScanner.exe",
"args": [],
"cwd": "${workspaceFolder}/RatScanner/bin/Debug/net10.0-windows10.0.22621.0",
"console": "integratedTerminal",
"stopAtEntry": false
}
]
}
40 changes: 40 additions & 0 deletions .vscode/tasks.json

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same for this one

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/RatScanner/RatScanner.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/RatScanner/RatScanner.csproj",
"-c",
"Release",
"-o",
"${workspaceFolder}/publish",
"--runtime",
"win-x64",
"-p:PublishSingleFile=true",
"--self-contained",
"true"
],
"problemMatcher": "$msCompile"
}
]
}
Binary file added RatScanner/Data/icon_search.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions RatScanner/RatConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ internal static class Paths {
internal static string ItemData = Path.Combine(Data, "items.json");
internal static string TrainedData = Path.Combine(Data, "traineddata");
internal static string UnknownIcon = Path.Combine(Data, "unknown.png");
internal static string SearchIcon = Path.Combine(Data, "icon_search.png");
internal static string ConfigFile = Path.Combine(Base, "config.cfg");
internal static string Debug = Path.Combine(Base, "Debug");
internal static string Updater = Path.Combine(Base, "RatUpdater.exe");
Expand Down
2 changes: 1 addition & 1 deletion RatScanner/RatScanner.csproj

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

there is no automatic CI/CD to upgrade version and create release?

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<UseWindowsForms>True</UseWindowsForms>
<ApplicationIcon>Resources\RatLogoSmall.ico</ApplicationIcon>
<StartupObject>RatScanner.App</StartupObject>
<Version>3.9.3</Version>
<Version>3.9.4</Version>
<ImplicitUsings>disable</ImplicitUsings>
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
<RootNamespace>RatScanner</RootNamespace>
Expand Down
54 changes: 42 additions & 12 deletions RatScanner/RatScannerMain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ public class RatScannerMain : INotifyPropertyChanged {

internal RatEyeEngine RatEyeEngine;

private Bitmap? _searchMarker;

public event PropertyChangedEventHandler? PropertyChanged;

internal ItemQueue ItemScans = new();
Expand Down Expand Up @@ -205,10 +207,31 @@ private RatEye.Config GetRatEyeConfig(bool highlighted = true) {
InventoryConfig = new Config.Processing.Inventory() {
OptimizeHighlighted = highlighted,
},
InspectionConfig = CreateInspectionConfig(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: CreateInspectionConfig() allocates a new Bitmap each time RatEye is reconfigured, but no disposal path is introduced for previously created marker bitmaps, which can leak GDI handles/memory over repeated SetupRatEye calls. Ensure old inspection marker resources are disposed when replacing the engine/config. [resource leak]

Severity Level: Major ⚠️
⚠️ RatEye reconfiguration accumulates GDI handles over time.
⚠️ Long-running sessions risk memory/handle exhaustion during scans.
Steps of Reproduction ✅
1. Start the application so that `RatScannerMain` is constructed
(`RatScanner/RatScannerMain.cs:19-83`), which logs "Initializing RatEye..." and calls
`SetupRatEye()` at line 58-59.

2. In `SetupRatEye()` (`RatScanner/RatScannerMain.cs:35-41`), a new `RatEyeEngine`
instance is created with `GetRatEyeConfig()`, which in turn sets
`ProcessingConfig.InspectionConfig = CreateInspectionConfig()`
(`RatScanner/RatScannerMain.cs:43-60).

3. `CreateInspectionConfig()` (`RatScanner/RatScannerMain.cs:64-70`) allocates a new
`Bitmap` from `RatConfig.Paths.SearchIcon` and assigns it to the `Marker` property of the
`Config.Processing.Inspection` object; no disposal of previous markers or the created
bitmap is performed here.

4. Later, when the item cache becomes ready, `WaitForItemsAndSeedAsync()`
(`RatScanner/RatScannerMain.cs:101-114`) calls `SetupRatEye()` again at line 107, creating
a second `RatEyeEngine` and a new inspection config marker bitmap; the previous
`RatEyeEngine` and its `InspectionConfig.Marker` are replaced without an explicit
`Dispose` call, so repeated reconfiguration within a long-lived process will continually
allocate new marker bitmaps, relying only on GC/finalizers and effectively leaking GDI
handles until collection.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** RatScanner/RatScannerMain.cs
**Line:** 208:208
**Comment:**
	*Resource Leak: `CreateInspectionConfig()` allocates a new `Bitmap` each time RatEye is reconfigured, but no disposal path is introduced for previously created marker bitmaps, which can leak GDI handles/memory over repeated `SetupRatEye` calls. Ensure old inspection marker resources are disposed when replacing the engine/config.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

},
};
}

private Config.Processing.Inspection CreateInspectionConfig() {
if (_searchMarker == null) {
string path = RatConfig.Paths.SearchIcon;
if (!File.Exists(path)) {
Logger.LogError(
$"Search icon not found at: {path}\n\n" +
"Name scan requires Data/icon_search.png. Reinstall or update RatScanner so the full package is present.");
}

_searchMarker = new Bitmap(path);
Logger.LogInfo($"Loaded name-scan marker {_searchMarker.Width}x{_searchMarker.Height} from {path}");
}

// MarkerItemScale is calibrated to template width: in-game icon is ~16px at 1080p
return new Config.Processing.Inspection() {
Marker = _searchMarker,
MarkerItemScale = 16f / _searchMarker.Width,
};
}
Comment on lines +215 to +233

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Missing-file check logs but still crashes with an unhelpful exception.

The File.Exists check at line 218 logs a helpful message, but execution falls through to new Bitmap(path) at line 224, which throws FileNotFoundException with a generic .NET message. The user sees an unhandled exception dialog with no actionable guidance — the helpful text is only in the log file. This partially addresses the prior review's concern but leaves the user-facing crash experience unimproved.

🛡️ Proposed fix: throw with the custom message
 private Config.Processing.Inspection CreateInspectionConfig() {
 	if (_searchMarker == null) {
 		string path = RatConfig.Paths.SearchIcon;
 		if (!File.Exists(path)) {
 			Logger.LogError(
 				$"Search icon not found at: {path}\n\n" +
 				"Name scan requires Data/icon_search.png. Reinstall or update RatScanner so the full package is present.");
-		}
-
-		_searchMarker = new Bitmap(path);
+			throw new FileNotFoundException(
+				"Search icon file is missing. Name scan requires Data/icon_search.png. Reinstall or update RatScanner so the full package is present.",
+				path);
+		}
+
+		_searchMarker = new Bitmap(path);
 		Logger.LogInfo($"Loaded name-scan marker {_searchMarker.Width}x{_searchMarker.Height} from {path}");
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@RatScanner/RatScannerMain.cs` around lines 215 - 233, Update
CreateInspectionConfig so a missing RatConfig.Paths.SearchIcon does not continue
to new Bitmap(path). After logging the existing actionable guidance, throw an
exception using that same custom message so the user-facing failure includes
reinstall/update instructions and avoids the generic FileNotFoundException.


private Database RatStashDatabaseFromTarkovDev() {
List<Item> rsItems = new();
if (!TarkovDevAPI.TryGetCachedItems(out TarkovDev.GraphQL.Item[] items) || items.Length == 0) {
Expand Down Expand Up @@ -298,17 +321,25 @@ internal void NameScan(Vector2 position) {
toolTipPosition += new Vector2(-(int)(marker.Width * scale), (int)(marker.Height * scale));
toolTipPosition += position;

ItemNameScan tempNameScan = new(
inspection,
toolTipPosition,
RatConfig.ToolTip.Duration);

ItemScans.Enqueue(tempNameScan);
if (!TryEnqueueNameScan(inspection, toolTipPosition)) return;

RefreshOverlay();
}
}

private bool TryEnqueueNameScan(RatEye.Processing.Inspection inspection, Vector2 toolTipPosition) {
if (!TarkovDevAPI.TryGetItemById(inspection.Item!.Id, out _)) {
Logger.LogWarning($"Skipping name scan: item {inspection.Item.Id} is not in cache yet.");
return false;
}
Comment on lines +331 to +334

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: Add an explicit null guard for inspection.Item at the start of the helper and avoid relying on the null-forgiving operator when reading the item id. [custom_rule]

Severity Level: Major ⚠️

Why it matters? ⭐

The helper dereferences inspection.Item with the null-forgiving operator instead of explicitly guarding it. This is a changed C# nullable-reference usage without explicit null handling, so the suggestion matches the custom rule.

Rule source 📖

=== AGENTS.md === (line 66)

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** RatScanner/RatScannerMain.cs
**Line:** 318:321
**Comment:**
	*Custom Rule: Add an explicit null guard for `inspection.Item` at the start of the helper and avoid relying on the null-forgiving operator when reading the item id.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎


ItemScans.Enqueue(new ItemNameScan(
inspection,
toolTipPosition,
RatConfig.ToolTip.Duration));
return true;
}

/// <summary>
/// Perform a name scan over the entire active screen
/// </summary>
Expand All @@ -333,12 +364,7 @@ internal void NameScanScreen(object? _ = null) {
Bitmap marker = RatEyeEngine.Config.ProcessingConfig.InspectionConfig.Marker;
toolTipPosition += new Vector2(0, (int)(marker.Height * scale));

ItemNameScan tempNameScan = new(
inspection,
toolTipPosition,
RatConfig.ToolTip.Duration);

ItemScans.Enqueue(tempNameScan);
TryEnqueueNameScan(inspection, toolTipPosition);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: Add an explicit null check for the nullable inspection value in the loop before passing it to the enqueue helper. [custom_rule]

Severity Level: Major ⚠️

Why it matters? ⭐

The loop variable is declared as Inspection? inspection, but the code passes it directly to TryEnqueueNameScan without an explicit null check. That is a nullable-reference usage introduced in changed code and is not explicitly handled, so the rule applies.

Rule source 📖

=== AGENTS.md === (line 66)

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** RatScanner/RatScannerMain.cs
**Line:** 354:354
**Comment:**
	*Custom Rule: Add an explicit null check for the nullable `inspection` value in the loop before passing it to the enqueue helper.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

}
RefreshOverlay();
}
Expand All @@ -364,6 +390,10 @@ internal void IconScan(Vector2 position) {
RatEye.Processing.Icon? icon = inventory.LocateIcon();

if (icon?.DetectionConfidence <= 0 || icon?.Item == null) return;
if (!TarkovDevAPI.TryGetItemById(icon.Item.Id, out _)) {
Logger.LogWarning($"Skipping icon scan: item {icon.Item.Id} is not in cache yet.");
return;
}

Vector2 toolTipPosition = position;
toolTipPosition += icon.Position + icon.ItemPosition;
Expand Down
8 changes: 6 additions & 2 deletions RatScanner/Scan/ItemIconScan.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using RatEye;
using RatStash;
using System;
using System.Linq;
using Icon = RatEye.Processing.Icon;
using TarkovItem = RatScanner.TarkovDev.GraphQL.Item;

namespace RatScanner.Scan;

Expand All @@ -16,7 +16,11 @@ public class ItemIconScan : ItemScan {
public ItemIconScan(Icon icon, Vector2 toolTipPosition, int duration) {
Icon = icon;
RatStash.Item iconItem = icon.Item;
Item = TarkovDevAPI.GetItems().FirstOrDefault(item => item.Id == iconItem.Id) ?? throw new Exception($"Unknown item: {icon.Item.Id}");
if (!TarkovDevAPI.TryGetItemById(iconItem.Id, out TarkovItem? tarkovItem) || tarkovItem is null) {
throw new Exception($"Unknown item: {iconItem.Id}");
}

Item = tarkovItem;
ItemExtraInfo = icon.ItemExtraInfo;
Confidence = icon.DetectionConfidence;
Rotated = icon.Rotated;
Expand Down
8 changes: 6 additions & 2 deletions RatScanner/Scan/ItemNameScan.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using RatEye;
using RatEye.Processing;
using System;
using System.Linq;
using TarkovItem = RatScanner.TarkovDev.GraphQL.Item;

namespace RatScanner.Scan;

Expand All @@ -10,7 +10,11 @@ public class ItemNameScan : ItemScan {

public ItemNameScan(Inspection inspection, Vector2 toolTipPosition, int duration) {
RatStash.Item inspectionItem = inspection.Item;
Item = TarkovDevAPI.GetItems().FirstOrDefault(item => item.Id == inspectionItem.Id) ?? throw new Exception($"Unknown item: {inspection.Item.Id}");
if (!TarkovDevAPI.TryGetItemById(inspectionItem.Id, out TarkovItem? tarkovItem) || tarkovItem is null) {
throw new Exception($"Unknown item: {inspectionItem.Id}");
}

Item = tarkovItem;
Confidence = inspection.MarkerConfidence;
IconPath = inspection.IconPath;
_toolTipPosition = toolTipPosition;
Expand Down
36 changes: 36 additions & 0 deletions RatScanner/TarkovDevAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
Expand Down Expand Up @@ -306,6 +307,12 @@ private static bool IsCacheExpired(string baseQueryKey, long time) {
/// Full cache initialization - waits for all requests to complete
/// </summary>
public static async Task InitializeCache() {
// Preload offline cache for the current language so scans do not fail during fetch
TryLoadFromOfflineCache<Item>(ItemsQueryKey(), RatConfig.MediumTTL);
TryLoadFromOfflineCache<TTask>(TasksQueryKey(), RatConfig.LongTTL);
TryLoadFromOfflineCache<HideoutStation>(HideoutStationsQueryKey(), RatConfig.LongTTL);
TryLoadFromOfflineCache<Map>(MapsQueryKey(), RatConfig.LongTTL);

await Task.WhenAll(
QueueRequest<Item>(ItemsQueryKey(), ItemsQuery, RatConfig.MediumTTL),
QueueRequest<TTask>(TasksQueryKey(), TasksQuery, RatConfig.LongTTL),
Expand All @@ -316,6 +323,35 @@ await Task.WhenAll(

public static Item[] GetItems(LanguageCode language, GameMode gameMode) => GetCached<Item>(ItemsQueryKey(language, gameMode), () => ItemsQuery(language, gameMode), RatConfig.MediumTTL);
public static Item[] GetItems() => GetCached<Item>(ItemsQueryKey(), ItemsQuery, RatConfig.MediumTTL);

/// <summary>
/// Looks up an item by id in loaded caches for the current game mode (no network).
/// Prefers the active language, then other languages of the same mode.
/// </summary>
public static bool TryGetItemById(string id, out Item? item) {
item = null;
if (string.IsNullOrEmpty(id)) return false;

if (Cache.TryGetValue(ItemsQueryKey(), out (long expire, object response) current)
&& current.response is Item[] currentItems) {
item = currentItems.FirstOrDefault(i => i.Id == id);
if (item != null) return true;
}

string modeSuffix = $"_{RatConfig.GameMode}";
foreach (KeyValuePair<string, (long expire, object response)> entry in Cache) {
if (!entry.Key.StartsWith("items_", StringComparison.Ordinal)) continue;
if (!entry.Key.EndsWith(modeSuffix, StringComparison.Ordinal)) continue;
if (entry.Value.response is not Item[] items) continue;

item = items.FirstOrDefault(i => i.Id == id);
if (item != null) return true;
}

item = null;
return false;
}

public static bool TryGetCachedItems(out Item[] items) {
if (Cache.TryGetValue(ItemsQueryKey(), out (long expire, object response) cached)) {
items = (Item[])cached.response;
Expand Down
Loading