From 7110cace065bfa53d71e9ad98afe05fb2cf868d5 Mon Sep 17 00:00:00 2001 From: gradimaz <@gradinazz> Date: Wed, 18 Feb 2026 16:43:03 +0200 Subject: [PATCH] Implement new CS2 trade-up formula with pre-normalized floats --- FloatTool/Common/Calculations.cs | 20 ++--- FloatTool/Common/Skin.cs | 17 +++- FloatTool/ViewModels/MainViewModel.cs | 46 ++++++++-- FloatTool/Views/BenchmarkWindow.xaml.cs | 5 +- FloatTool/Views/MainWindow.xaml.cs | 112 +++++++++++++++++++----- 5 files changed, 154 insertions(+), 46 deletions(-) diff --git a/FloatTool/Common/Calculations.cs b/FloatTool/Common/Calculations.cs index ceb5486..2d850a6 100644 --- a/FloatTool/Common/Calculations.cs +++ b/FloatTool/Common/Calculations.cs @@ -21,18 +21,16 @@ namespace FloatTool.Common { static public class Calculations { - public static double Craft(InputSkin[] ingridients, double minFloat, double floatRange) + public static double Craft(InputSkin[] ingredients, double minFloat, double maxFloat) { - return floatRange * (ingridients[0].WearValue - + ingridients[1].WearValue - + ingridients[2].WearValue - + ingridients[3].WearValue - + ingridients[4].WearValue - + ingridients[5].WearValue - + ingridients[6].WearValue - + ingridients[7].WearValue - + ingridients[8].WearValue - + ingridients[9].WearValue) + minFloat; + double sumNormalized = 0.0; + + for (int i = 0; i < 10; i++) + sumNormalized += ingredients[i].NormalizedWear; + + double avgNormalized = sumNormalized / 10.0; + + return minFloat + avgNormalized * (maxFloat - minFloat); } public static bool NextCombination(int[] num, int n) diff --git a/FloatTool/Common/Skin.cs b/FloatTool/Common/Skin.cs index a9639ac..e5c4d06 100644 --- a/FloatTool/Common/Skin.cs +++ b/FloatTool/Common/Skin.cs @@ -58,7 +58,6 @@ public struct Skin public string Name; public double MinFloat; public double MaxFloat; - public double FloatRange; public Quality Rarity; public Skin(string name, double minWear, double maxWear, Quality rarity) @@ -66,7 +65,6 @@ public Skin(string name, double minWear, double maxWear, Quality rarity) Name = name; MinFloat = minWear; MaxFloat = maxWear; - FloatRange = (MaxFloat - MinFloat) / 10; Rarity = rarity; } @@ -142,15 +140,28 @@ public struct InputSkin public float Price; public Currency SkinCurrency; public string ListingID; + public double MinFloat; + public double MaxFloat; + public double NormalizedWear; public double GetWearValue => WearValue; - public InputSkin(double wear, float price, Currency currency, string listingId = "") + public InputSkin(double wear, float price, Currency currency, string listingId = "", + double minFloat = 0.0, double maxFloat = 1.0) { WearValue = wear; Price = price; SkinCurrency = currency; ListingID = listingId; + MinFloat = minFloat; + MaxFloat = maxFloat; + + // Pre-compute normalized wear value + double range = maxFloat - minFloat; + if (range < 0.0001) range = 0.0001; + NormalizedWear = (wear - minFloat) / range; + if (NormalizedWear < 0.0) NormalizedWear = 0.0; + if (NormalizedWear > 1.0) NormalizedWear = 1.0; } internal int CompareTo(InputSkin b) diff --git a/FloatTool/ViewModels/MainViewModel.cs b/FloatTool/ViewModels/MainViewModel.cs index 4babbb2..237e3bd 100644 --- a/FloatTool/ViewModels/MainViewModel.cs +++ b/FloatTool/ViewModels/MainViewModel.cs @@ -381,19 +381,51 @@ public void UpdateFloatRange() minCraftWear = (float)currentMinWear; maxCraftWear = (float)currentMaxWear; UpdateFloatError(); - FloatRange = $"{minCraftWear.ToString("0.00", CultureInfo.InvariantCulture)} - {maxCraftWear.ToString("0.00", CultureInfo.InvariantCulture)}"; + FloatRange = $"{minCraftWear.ToString("0.0000", CultureInfo.InvariantCulture)} - {maxCraftWear.ToString("0.0000", CultureInfo.InvariantCulture)}"; return; } - var range = Skin.GetFloatRangeForQuality(SkinQuality); + // Find input skin in database to get its actual MinWear/MaxWear + string inputSkinName = $"{WeaponName} | {SkinName}"; + SkinModel? inputSkinModel = null; + + if (SkinsDatabase != null) + { + foreach (var collection in SkinsDatabase) + { + foreach (var skin in collection.Skins) + { + if (skin.Name == inputSkinName) + { + inputSkinModel = skin; + break; + } + } + if (inputSkinModel != null) break; + } + } + + if (inputSkinModel == null) + { + FloatRange = "No data"; + return; + } + + var qualityRange = Skin.GetFloatRangeForQuality(SkinQuality); + double inputSkinMin = inputSkinModel.Value.MinWear; + double inputSkinMax = inputSkinModel.Value.MaxWear; List lowest = new(); for (int i = 0; i < 10; i++) - lowest.Add(new InputSkin(range.Min, 0, Currency.USD)); + lowest.Add(new InputSkin(qualityRange.Min, 0, Currency.USD, "", + minFloat: inputSkinMin, + maxFloat: inputSkinMax)); List highest = new(); for (int i = 0; i < 10; i++) - highest.Add(new InputSkin(range.Max, 0, Currency.USD)); + highest.Add(new InputSkin(qualityRange.Max, 0, Currency.USD, "", + minFloat: inputSkinMin, + maxFloat: inputSkinMax)); int index = 0; minCraftWear = 0; @@ -407,7 +439,7 @@ public void UpdateFloatRange() Calculations.Craft( lowest.ToArray(), currSkin.MinFloat, - currSkin.FloatRange + currSkin.MaxFloat ), CultureInfo.InvariantCulture ); @@ -415,7 +447,7 @@ public void UpdateFloatRange() Calculations.Craft( highest.ToArray(), currSkin.MinFloat, - currSkin.FloatRange + currSkin.MaxFloat ), CultureInfo.InvariantCulture ); @@ -424,7 +456,7 @@ public void UpdateFloatRange() } UpdateFloatError(); - FloatRange = $"{minCraftWear.ToString("0.00", CultureInfo.InvariantCulture)} - {maxCraftWear.ToString("0.00", CultureInfo.InvariantCulture)}"; + FloatRange = $"{minCraftWear.ToString("0.0000", CultureInfo.InvariantCulture)} - {maxCraftWear.ToString("0.0000", CultureInfo.InvariantCulture)}"; } public void UpdateFloatError() diff --git a/FloatTool/Views/BenchmarkWindow.xaml.cs b/FloatTool/Views/BenchmarkWindow.xaml.cs index b5beab3..9d670d2 100644 --- a/FloatTool/Views/BenchmarkWindow.xaml.cs +++ b/FloatTool/Views/BenchmarkWindow.xaml.cs @@ -97,7 +97,7 @@ private static void FloatCraftWorkerThread(CraftSearchSetup options) for (int i = 0; i < options.Outcomes.Length; ++i) { double resultFloat = Calculations.Craft( - resultList, options.Outcomes[i].MinFloat, options.Outcomes[i].FloatRange + resultList, options.Outcomes[i].MinFloat, options.Outcomes[i].MaxFloat ); bool gotResult = false; @@ -161,7 +161,8 @@ private void StartBenchmark_Click(object sender, RoutedEventArgs e) List inputSkinsList = new(); foreach (double f in pool) - inputSkinsList.Add(new InputSkin(f, 0.03f, Currency.USD)); + inputSkinsList.Add(new InputSkin(f, 0.03f, Currency.USD, "", + minFloat: 0.06, maxFloat: 0.8)); InputSkin[] inputSkins = inputSkinsList.ToArray(); diff --git a/FloatTool/Views/MainWindow.xaml.cs b/FloatTool/Views/MainWindow.xaml.cs index 67f8948..3cab638 100644 --- a/FloatTool/Views/MainWindow.xaml.cs +++ b/FloatTool/Views/MainWindow.xaml.cs @@ -182,6 +182,11 @@ private void SetStatus(string stringCode) Dispatcher.Invoke(new Action(() => { SetStatus(stringCode); })); } + private const int MAX_FOUND_RESULTS = 1000; + private static ConcurrentBag PendingResults; + private static long FoundResultCount; + + private void FloatCraftWorkerThread(CraftSearchSetup options) { int size = options.SkinPool.Length - 10; @@ -201,7 +206,7 @@ private void FloatCraftWorkerThread(CraftSearchSetup options) for (int i = 0; i < options.Outcomes.Length; ++i) { double resultFloat = Calculations.Craft( - resultList, options.Outcomes[i].MinFloat, options.Outcomes[i].FloatRange + resultList, options.Outcomes[i].MinFloat, options.Outcomes[i].MaxFloat ); bool gotResult = false; @@ -226,31 +231,32 @@ private void FloatCraftWorkerThread(CraftSearchSetup options) .StartsWith(options.SearchFilter, StringComparison.Ordinal) || options.SearchMode != SearchMode.Equal) { + // Check limit before doing expensive work + if (Interlocked.Read(ref FoundResultCount) >= MAX_FOUND_RESULTS) + break; + InputSkin[] result = (InputSkin[])resultList.Clone(); float price = 0; - float ieeesum = 0; + float ieeeNormSum = 0; foreach (var skin in result) { price += skin.Price; - ieeesum += (float)skin.WearValue; + ieeeNormSum += (float)skin.NormalizedWear; } - ieeesum /= 10; - float ieee = ((float)options.Outcomes[i].MaxFloat - (float)options.Outcomes[i].MinFloat) * ieeesum + (float)options.Outcomes[i].MinFloat; + ieeeNormSum /= 10; + float ieee = ((float)options.Outcomes[i].MaxFloat - (float)options.Outcomes[i].MinFloat) * ieeeNormSum + (float)options.Outcomes[i].MinFloat; - Dispatcher.Invoke(new Action(() => + // Add to pending results (non-blocking) + Interlocked.Increment(ref FoundResultCount); + PendingResults.Add(new Combination { - ViewModel.FoundCombinations.Add(new Combination - { - Wear = resultFloat, - OutcomeName = options.Outcomes[i].Name, - Inputs = result, - Currency = result[0].SkinCurrency, - Price = price, - Wear32Bit = ((double)ieee).ToString("0.000000000000000", CultureInfo.InvariantCulture), - }); - if (AppHelpers.Settings.Sound) - CombinationFoundSound.Play(); - })); + Wear = resultFloat, + OutcomeName = options.Outcomes[i].Name, + Inputs = result, + Currency = result[0].SkinCurrency, + Price = price, + Wear32Bit = ((double)ieee).ToString("0.000000000000000", CultureInfo.InvariantCulture), + }); } } } @@ -260,6 +266,10 @@ private void FloatCraftWorkerThread(CraftSearchSetup options) if (CancellationToken.IsCancellationRequested) break; + // Stop if we've found enough results + if (Interlocked.Read(ref FoundResultCount) >= MAX_FOUND_RESULTS) + break; + // Get next combination running = Calculations.NextCombination(numbers, size, options.ThreadCount); @@ -389,6 +399,29 @@ private void StartSearchButton_Click(object sender, RoutedEventArgs e) } } + // Look up input skin's own min/max float from database + string inputSkinFullName = $"{ViewModel.WeaponName} | {ViewModel.SkinName}"; + double skinMinFloat = 0.0, skinMaxFloat = 1.0; + if (ViewModel.SkinsDatabase != null) + { + foreach (var collection in ViewModel.SkinsDatabase) + { + bool skinFound = false; + foreach (var skinModel in collection.Skins) + { + if (skinModel.Name == inputSkinFullName) + { + skinMinFloat = skinModel.MinWear; + skinMaxFloat = skinModel.MaxWear; + skinFound = true; + break; + } + } + if (skinFound) break; + } + } + + foreach (var task in floatTasks.Keys) { try @@ -397,7 +430,9 @@ private void StartSearchButton_Click(object sender, RoutedEventArgs e) task.Result, floatTasks[task].Item2, AppHelpers.Settings.Currency, - floatTasks[task].Item1 + floatTasks[task].Item1, + minFloat: skinMinFloat, + maxFloat: skinMaxFloat )); ViewModel.ProgressPercentage = (float)inputSkinBag.Count * 100 / floatTasks.Count; @@ -474,8 +509,8 @@ private void StartSearchButton_Click(object sender, RoutedEventArgs e) InputSkin[] worst = sorted.TakeLast(10).ToArray(); // Get min and max floats by running the craft function - double minFloat = Calculations.Craft(best, outcomes[0].MinFloat, outcomes[0].FloatRange); - double maxFloat = Calculations.Craft(worst, outcomes[0].MinFloat, outcomes[0].FloatRange); + double minFloat = Calculations.Craft(best, outcomes[0].MinFloat, outcomes[0].MaxFloat); + double maxFloat = Calculations.Craft(worst, outcomes[0].MinFloat, outcomes[0].MaxFloat); Dispatcher.Invoke(new Action(() => { @@ -496,6 +531,8 @@ private void StartSearchButton_Click(object sender, RoutedEventArgs e) double precission = Math.Pow(0.1, searchFilter.Length - 2); // Create thread pool + PendingResults = new ConcurrentBag(); + FoundResultCount = 0; ThreadPool = new(); int threads = ViewModel.ThreadCount; @@ -593,8 +630,19 @@ private void StartSearchButton_Click(object sender, RoutedEventArgs e) ViewModel.ParsedCombinations = PassedCombinations; ViewModel.CombinationsLabel = string.Empty; - if (!isAnyRunning) - break; + // Batch flush pending results to UI (must be on UI thread) + if (!PendingResults.IsEmpty) + { + Dispatcher.Invoke(new Action(() => + { + while (PendingResults.TryTake(out var combo)) + { + ViewModel.FoundCombinations.Add(combo); + if (AppHelpers.Settings.Sound && ViewModel.FoundCombinations.Count == 1) + CombinationFoundSound.Play(); + } + })); + } if (stopAfterHit && ViewModel.FoundCombinations.Count >= 1) { @@ -602,9 +650,27 @@ private void StartSearchButton_Click(object sender, RoutedEventArgs e) break; } + // Limit max results to prevent memory exhaustion + if (Interlocked.Read(ref FoundResultCount) >= MAX_FOUND_RESULTS) + { + Logger.Info($"Max results limit ({MAX_FOUND_RESULTS}) reached, stopping search"); + TokenSource.Cancel(); + break; + } + + if (!isAnyRunning) + break; + Thread.Sleep(100); } + // Final flush of any remaining pending results + Dispatcher.Invoke(new Action(() => + { + while (PendingResults.TryTake(out var combo)) + ViewModel.FoundCombinations.Add(combo); + })); + UpdateRichPresence(true); Logger.Info("Finished searching"); Dispatcher.Invoke(