diff --git a/Mimante/App.xaml b/Mimante/App.xaml new file mode 100644 index 0000000..8a3dd23 --- /dev/null +++ b/Mimante/App.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/Mimante/App.xaml.cs b/Mimante/App.xaml.cs new file mode 100644 index 0000000..ec7348a --- /dev/null +++ b/Mimante/App.xaml.cs @@ -0,0 +1,14 @@ +using System.Configuration; +using System.Data; +using System.Windows; + +namespace Mimante +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + } + +} diff --git a/Mimante/AssemblyInfo.cs b/Mimante/AssemblyInfo.cs new file mode 100644 index 0000000..b0ec827 --- /dev/null +++ b/Mimante/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/Mimante/MainWindow.xaml b/Mimante/MainWindow.xaml new file mode 100644 index 0000000..dd894de --- /dev/null +++ b/Mimante/MainWindow.xaml @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Max Clicks + + + + + Max Resets + + + + + + + Min Price + + + + + Max Price + + + + + Prezzo corrente + + + + + + + + + + + + + + + + + + + + + diff --git a/Mimante/MainWindow.xaml.cs b/Mimante/MainWindow.xaml.cs new file mode 100644 index 0000000..694f711 --- /dev/null +++ b/Mimante/MainWindow.xaml.cs @@ -0,0 +1,636 @@ +using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using Microsoft.Web.WebView2.Core; + +namespace AutoBidder +{ + /// + /// Interaction logic for MainWindow.xaml + /// + public partial class MainWindow : Window + { + private CancellationTokenSource? _cts; + private Task? _automationTask; + + public MainWindow() + { + InitializeComponent(); + // create a simple programmatic icon (circle with AB) and assign to window + try + { + const int size = 64; + var dv = new DrawingVisual(); + using (var dc = dv.RenderOpen()) + { + dc.DrawRoundedRectangle(new SolidColorBrush(Color.FromRgb(16,163,74)), null, new Rect(0, 0, size, size), 8, 8); + var ft = new FormattedText("AB", System.Globalization.CultureInfo.InvariantCulture, FlowDirection.LeftToRight, + new Typeface("Segoe UI"), 28, Brushes.White, 1.0); + var pt = new Point((size - ft.Width) / 2, (size - ft.Height) / 2 - 2); + dc.DrawText(ft, pt); + } + var rtb = new RenderTargetBitmap(size, size, 96, 96, PixelFormats.Pbgra32); + rtb.Render(dv); + this.Icon = rtb; // set window and taskbar icon + } + catch + { + // ignore icon errors + } + + // initial visual states: start is primary action + StartButton.IsEnabled = true; + StartButton.Opacity = 1.0; + StopButton.IsEnabled = false; + StopButton.Opacity = 0.5; + BackButton.IsEnabled = false; + + // navigation completed -> update back button state + webView.NavigationCompleted += WebView_NavigationCompleted; + } + + private void WebView_NavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs e) + { + UpdateBackButton(); + } + + private void UpdateBackButton() + { + try + { + Dispatcher.Invoke(() => + { + BackButton.IsEnabled = webView.CoreWebView2 != null && webView.CoreWebView2.CanGoBack; + try + { + string? url = null; + if (webView.CoreWebView2 != null) + { + url = webView.CoreWebView2.Source; + } + else if (webView.Source != null) + { + url = webView.Source.ToString(); + } + + if (!string.IsNullOrEmpty(url)) SiteLinkText.Text = url; + } + catch { } + }); + } + catch { } + } + + private async void StartButton_Click(object sender, RoutedEventArgs e) + { + StartButton.IsEnabled = false; + StopButton.IsEnabled = true; + // visual feedback: running -> stop is primary + StartButton.Opacity = 0.5; + StopButton.Opacity = 1.0; + + Log("Inizializzazione web..."); + + try + { + // Ensure WebView2 is initialized + if (webView.CoreWebView2 == null) + { + await webView.EnsureCoreWebView2Async(); + } + UpdateBackButton(); + } + catch (Exception ex) + { + var msg = "Errore inizializzazione WebView2: " + ex.Message; + Log(msg); + StartButton.IsEnabled = true; + StopButton.IsEnabled = false; + StartButton.Opacity = 1.0; + StopButton.Opacity = 0.5; + return; + } + + Log("Avviato: WebView inizializzato. Avvio monitoraggio pagina."); + + _cts = new CancellationTokenSource(); + var token = _cts.Token; + _automationTask = Task.Run(async () => await AutomationLoop(token), token); + } + + private void StopButton_Click(object sender, RoutedEventArgs e) + { + // visual feedback: stopped -> start is primary + StartButton.Opacity = 1.0; + StopButton.Opacity = 0.5; + StopAutomation("Arrestato dall'utente"); + } + + private void BackButton_Click(object sender, RoutedEventArgs e) + { + try + { + if (webView.CoreWebView2 == null) + { + // if not initialized, try ensuring + _ = webView.EnsureCoreWebView2Async(); + } + + if (webView.CoreWebView2 != null && webView.CoreWebView2.CanGoBack) + { + webView.CoreWebView2.GoBack(); + } + } + catch (Exception ex) + { + Log("Errore navigazione indietro: " + ex.Message); + } + } + + private async void RefreshButton_Click(object sender, RoutedEventArgs e) + { + try + { + if (webView.CoreWebView2 == null) + { + await webView.EnsureCoreWebView2Async(); + } + webView.CoreWebView2?.Reload(); + Log("Pagina aggiornata"); + } + catch (Exception ex) + { + Log("Errore aggiornamento pagina: " + ex.Message); + } + } + + private void StopAutomation(string reason) + { + try + { + _cts?.Cancel(); + } + catch { } + + StartButton.Dispatcher.Invoke(() => + { + StartButton.IsEnabled = true; + StopButton.IsEnabled = false; + // reset visual states: start primary + StartButton.Opacity = 1.0; + StopButton.Opacity = 0.5; + }); + + Log("STOP: " + reason); + } + + private void Log(string message) + { + var entry = $"{DateTime.Now:HH:mm:ss} - {message}{Environment.NewLine}"; + try + { + Dispatcher.Invoke(() => + { + if (LogBox != null) + { + LogBox.AppendText(entry); + LogBox.ScrollToEnd(); + } + }); + } + catch + { + // ignore logging errors + } + } + + private async Task AutomationLoop(CancellationToken token) + { + int clickCount = 0; + int resetCount = 0; + string? previousTimer = null; + + // read limits from UI + int maxClicks = 0; + int maxResets = 0; + double minPrice = 0.0; + double maxPrice = double.MaxValue; + try + { + Dispatcher.Invoke(() => + { + int.TryParse(MaxClicksBox.Text, out maxClicks); + int.TryParse(MaxResetsBox.Text, out maxResets); + if (maxClicks <= 0) maxClicks = int.MaxValue; + if (maxResets <= 0) maxResets = int.MaxValue; + + // price bounds: 0 = no limit + if (!double.TryParse(MinPriceBox.Text.Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out minPrice)) minPrice = 0.0; + if (!double.TryParse(MaxPriceBox.Text.Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var tmpMax)) tmpMax = 0.0; + if (tmpMax <= 0) maxPrice = double.MaxValue; else maxPrice = tmpMax; + }); + } + catch { maxClicks = int.MaxValue; maxResets = int.MaxValue; minPrice = 0.0; maxPrice = double.MaxValue; } + + // post-click delay to avoid duplicate actions + // increased delay to click as late as possible (milliseconds) + const int postClickDelayMs = 1200; + + // Improved JS snippet: find PUNTA anchor/button by class or text, find nearby numeric timer elements (including svg text) and price + const string findScript = @"(function(){ + function isVisible(el){ if(!el) return false; try{ var r=el.getBoundingClientRect(); var s=window.getComputedStyle(el); return r.width>0 && r.height>0 && s.visibility!=='hidden' && s.display!=='none'; }catch(e){ return false; } } + + // find price + var priceText = ''; + try{ var pEl = document.querySelector('.auction-action-price strong, .auction-action-price'); if(pEl) priceText = (pEl.textContent||pEl.innerText||'').trim(); }catch(e){} + var priceVal = null; + try{ if(priceText){ var p = priceText.replace('€','').replace(/\./g,'').replace(',','.').match(/\d+(?:\.\d+)?/); if(p) priceVal = p[0]; } }catch(e){} + + // Try to find a bid button that might say PUNTA or INIZIA + var btn = document.querySelector('a.auction-btn-bid, a.bid-button, .auction-btn-bid'); + if(btn && isVisible(btn)){ + var txt = (btn.textContent||btn.innerText||'').trim(); + if(/\bINIZIA\b/i.test(txt)){ + return JSON.stringify({status:'soon', debug: txt, price: priceVal}); + } + if(/\bPUNTA\b/i.test(txt)){ + // continue to find timer normally + } + } + + // Prefer direct countdown element by known class + try{ + var direct = document.querySelector('.text-countdown-progressbar'); + if(direct && isVisible(direct)){ + var t = (direct.textContent||'').trim(); + var m = t.match(/\d+/); + if(m) return JSON.stringify({status:'found', timer:m[0], price: priceVal, debug: direct.outerHTML}); + // if no digits, continue to fallback + } + }catch(err){ /* ignore */ } + + if(!btn || !isVisible(btn)){ + var candidates = Array.from(document.querySelectorAll('a, button, div, span')).filter(e=> e && (e.innerText||e.textContent) && /\bPUNTA\b/i.test((e.innerText||e.textContent)) && isVisible(e)); + if(candidates.length>0) btn = candidates[0]; + } + if(!btn) return JSON.stringify({status:'no-button', price: priceVal}); + + var btnRect; try{ btnRect = btn.getBoundingClientRect(); }catch(e){ btnRect = {top:0,left:0,right:0,bottom:0,width:0,height:0}; } + + // collect possible numeric-containing elements, including svg text + var nodeList = Array.from(document.querySelectorAll('div, span, p, strong, b, i, em, label, small, a, svg text')); + var nums = nodeList.map(e=>{ try{return {el:e, text:(e.textContent||'').trim(), rect:e.getBoundingClientRect(), html:(e.outerHTML||'')}; }catch(err){ return null; }}).filter(x=> x && /\d+/.test(x.text)).map(x=>({el:x.el, text:x.text, rect:x.rect, html:x.html})); + + if(nums.length==0) return JSON.stringify({status:'no-timer', price: priceVal}); + + // prefer elements that are visually near and above the button + function distanceRect(a,b){ var ax=(a.left+a.right)/2, ay=(a.top+a.bottom)/2; var bx=(b.left+b.right)/2, by=(b.top+b.bottom)/2; return Math.hypot(ax-bx, ay-by); } + + nums.sort(function(a,b){ + var da = distanceRect(a.rect, btnRect); + var db = distanceRect(b.rect, btnRect); + var ya = btnRect.top - a.rect.bottom; + var yb = btnRect.top - b.rect.bottom; + var pref = (ya>=0?0:200) - (yb>=0?0:200); + return (da - db) + pref; + }); + + var best = nums[0]; + var m = (best.text||'').match(/\d+/); + if(!m) return JSON.stringify({status:'no-timer-extracted', debug: best.html, price: priceVal}); + return JSON.stringify({status:'found', timer:m[0], price: priceVal, debug: best.html}); +})();"; + + const string clickScript = @"(function(){ + var btn = document.querySelector('a.auction-btn-bid, a.bid-button, .auction-btn-bid'); + if(!btn){ var candidates = Array.from(document.querySelectorAll('a, button, div, span')).filter(e=> e && (e.innerText||e.textContent) && /\bPUNTA\b/i.test((e.innerText||e.textContent))); if(candidates.length>0) btn=candidates[0]; } + if(!btn) return 'no-button'; + try{ btn.click(); return 'clicked'; }catch(e){ try{ var evt = document.createEvent('MouseEvents'); evt.initEvent('click', true, true); btn.dispatchEvent(evt); return 'dispatched'; }catch(ex){ return 'error:'+ (ex && ex.message?ex.message:ex); } } +})();"; + + try + { + while (!token.IsCancellationRequested) + { + string? result = null; + try + { + var op = Dispatcher.InvokeAsync(() => webView.ExecuteScriptAsync(findScript)); + var innerTask = await op.Task.ConfigureAwait(false); + result = await innerTask.ConfigureAwait(false); + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + Log("Errore JS/interop: " + ex.Message); + StopAutomation("Errore JS/interop: " + ex.Message); + return; + } + + string json = result ?? ""; + if (json.Length >= 2 && json[0] == '"' && json[^1] == '"') + { + json = System.Text.Json.JsonSerializer.Deserialize(result) ?? json; + } + + try + { + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + var status = root.GetProperty("status").GetString(); + + // price may be present + string? priceStr = null; + double? priceVal = null; + if (root.TryGetProperty("price", out var priceEl) && priceEl.ValueKind != JsonValueKind.Null) + { + priceStr = priceEl.GetString(); + if (!string.IsNullOrEmpty(priceStr)) + { + if (double.TryParse(priceStr.Replace(',', '.').Trim(), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var pval)) + { + priceVal = pval; + Dispatcher.Invoke(() => CurrentPriceText.Text = pval.ToString("0.##") + " €"); + } + } + } + + if (status == "soon") + { + Log("Stato: INIZIA TRA POCO. In attesa che appaia il pulsante PUNTA..."); + // poll until PUNTA button appears (or cancellation) + while (!token.IsCancellationRequested) + { + string? chk = null; + try + { + var op2 = Dispatcher.InvokeAsync(() => webView.ExecuteScriptAsync("(function(){ var b=document.querySelector('a.auction-btn-bid, a.bid-button'); if(!b) return 'no'; var t=(b.textContent||b.innerText||'').trim(); return /PUNTA/i.test(t)?'yes':'no'; })();")); + var inner2 = await op2.Task.ConfigureAwait(false); + chk = await inner2.ConfigureAwait(false); + } + catch { chk = null; } + + if (!string.IsNullOrEmpty(chk)) + { + if (chk.Length >= 2 && chk[0] == '"' && chk[^1] == '"') chk = JsonSerializer.Deserialize(chk) ?? chk; + if (chk == "yes") + { + Log("Pulsante PUNTA trovato. Riprendo esecuzione."); + break; + } + } + + await Task.Delay(700, token).ConfigureAwait(false); + } + + continue; // next main loop iteration + } + + if (status == "no-button") + { + Log("Nessun pulsante PUNTA trovato; arresto"); + StopAutomation("Nessun pulsante PUNTA trovato; arresto"); + return; + } + + if (status == "no-timer" || status == "no-timer-extracted") + { + // Pause: wait until timer reappears + Log("Timer non trovato: pausa in corso, verrà ripreso quando il timer ricompare"); + bool resumed = false; + while (!token.IsCancellationRequested) + { + string? pollResult = null; + try + { + var op3 = Dispatcher.InvokeAsync(() => webView.ExecuteScriptAsync(findScript)); + var inner3 = await op3.Task.ConfigureAwait(false); + pollResult = await inner3.ConfigureAwait(false); + } + catch { pollResult = null; } + + if (!string.IsNullOrEmpty(pollResult)) + { + if (pollResult.Length >= 2 && pollResult[0] == '"' && pollResult[^1] == '"') pollResult = JsonSerializer.Deserialize(pollResult) ?? pollResult; + try + { + using var doc2 = JsonDocument.Parse(pollResult); + var st2 = doc2.RootElement.GetProperty("status").GetString(); + if (st2 == "found" || st2 == "soon") + { + Log("Timer ricomparso, ripresa."); + resumed = true; + break; + } + if (st2 == "no-button") + { + Log("Nessun pulsante durante pausa; arresto"); + StopAutomation("Nessun pulsante durante pausa"); + return; + } + } + catch { } + } + + await Task.Delay(800, token).ConfigureAwait(false); + } + + if (!resumed) break; + + continue; + } + + if (status == "found") + { + var timerValue = root.GetProperty("timer").GetString(); + + // only log when timer value actually changes + if (timerValue != previousTimer) + { + Log("Timer rilevato: " + timerValue); + + // detect resets: if price available, enforce min/max bounds + try + { + if (previousTimer != null && int.TryParse(previousTimer, out var prev) && int.TryParse(timerValue, out var curr)) + { + if (curr > prev) + { + resetCount++; + Dispatcher.Invoke(() => ResetCountText.Text = resetCount.ToString()); + Log("Timer resettato (contatore): " + resetCount); + // stop if reached limit + if (resetCount >= maxResets) + { + StopAutomation($"Limite reset raggiunto: {resetCount}"); + return; + } + } + } + } + catch { } + + previousTimer = timerValue; + } + + // price check: if price available, enforce min/max bounds + if (priceVal.HasValue) + { + if (priceVal.Value < minPrice) + { + Log($"Prezzo {priceVal.Value:0.##}€ sotto limite minimo ({minPrice:0.##}). Pausa."); + // wait until price >= minPrice + while (!token.IsCancellationRequested) + { + // poll price only + string? pricePoll = null; + try + { + var opP = Dispatcher.InvokeAsync(() => webView.ExecuteScriptAsync(@"(function(){ var p=document.querySelector('.auction-action-price strong, .auction-action-price'); if(!p) return null; var t=(p.textContent||p.innerText||'').trim(); var num = t.replace('€','').replace(/\./g,'').replace(',','.').match(/\d+(?:\.\d+)?/); return num?num[0]:null; })();")); + var innerP = await opP.Task.ConfigureAwait(false); + pricePoll = await innerP.ConfigureAwait(false); + } + catch { pricePoll = null; } + + if (!string.IsNullOrEmpty(pricePoll)) + { + if (pricePoll.Length >= 2 && pricePoll[0] == '"' && pricePoll[^1] == '"') pricePoll = JsonSerializer.Deserialize(pricePoll) ?? pricePoll; + if (double.TryParse(pricePoll.Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var pp)) + { + Dispatcher.Invoke(() => CurrentPriceText.Text = pp.ToString("0.##") + " €"); + if (pp >= minPrice) + { + Log("Prezzo salito sopra il minimo; ripresa esecuzione."); + break; + } + } + } + + await Task.Delay(700, token).ConfigureAwait(false); + } + } + + if (priceVal.Value > maxPrice) + { + Log($"Prezzo {priceVal.Value:0.##}€ sopra limite massimo ({maxPrice:0.##}). Pausa."); + while (!token.IsCancellationRequested) + { + string? pricePoll = null; + try + { + var opP = Dispatcher.InvokeAsync(() => webView.ExecuteScriptAsync(@"(function(){ var p=document.querySelector('.auction-action-price strong, .auction-action-price'); if(!p) return null; var t=(p.textContent||p.innerText||'').trim(); var num = t.replace('€','').replace(/\./g,'').replace(',','.').match(/\d+(?:\.\d+)?/); return num?num[0]:null; })();")); + var innerP = await opP.Task.ConfigureAwait(false); + pricePoll = await innerP.ConfigureAwait(false); + } + catch { pricePoll = null; } + + if (!string.IsNullOrEmpty(pricePoll)) + { + if (pricePoll.Length >= 2 && pricePoll[0] == '"' && pricePoll[^1] == '"') pricePoll = JsonSerializer.Deserialize(pricePoll) ?? pricePoll; + if (double.TryParse(pricePoll.Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var pp)) + { + Dispatcher.Invoke(() => CurrentPriceText.Text = pp.ToString("0.##") + " €"); + if (pp <= maxPrice) + { + Log("Prezzo sceso sotto il massimo; ripresa esecuzione."); + break; + } + } + } + + await Task.Delay(700, token).ConfigureAwait(false); + } + } + } + + if (timerValue == "0") + { + // immediate click when timer reaches 0 (extreme test) + string clickResult; + try + { + var op2 = Dispatcher.InvokeAsync(() => webView.ExecuteScriptAsync(clickScript)); + var inner2 = await op2.Task.ConfigureAwait(false); + clickResult = await inner2.ConfigureAwait(false); + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + Log("Errore durante click JS: " + ex.Message); + StopAutomation("Errore durante click JS: " + ex.Message); + return; + } + + if (clickResult.Length >= 2 && clickResult[0] == '"' && clickResult[^1] == '"') + { + clickResult = JsonSerializer.Deserialize(clickResult) ?? clickResult; + } + + // increment click counter and update UI + clickCount++; + Dispatcher.Invoke(() => ClickCountText.Text = clickCount.ToString()); + + Log("Click eseguito: " + clickResult + " (totale: " + clickCount + ")"); + + // stop if reached max clicks + if (clickCount >= maxClicks) + { + StopAutomation($"Limite click raggiunto: {clickCount}"); + return; + } + + await Task.Delay(postClickDelayMs, token).ConfigureAwait(false); + continue; + } + + await Task.Delay(200, token).ConfigureAwait(false); + } + + await Task.Delay(200, token).ConfigureAwait(false); + } + catch (JsonException ex) + { + Log("Errore parsing JSON: " + ex.Message); + await Task.Delay(300, token).ConfigureAwait(false); + } + } + } + catch (OperationCanceledException) + { + } + catch (Exception ex) + { + Log("Errore loop: " + ex.Message); + StopAutomation("Errore loop: " + ex.Message); + } + finally + { + StartButton.Dispatcher.Invoke(() => + { + StartButton.IsEnabled = true; + StopButton.IsEnabled = false; + StartButton.Opacity = 1.0; + StopButton.Opacity = 0.5; + }); + + Log("Automazione terminata"); + } + } + + protected override void OnClosed(EventArgs e) + { + try { _cts?.Cancel(); } catch { } + base.OnClosed(e); + } + } +} \ No newline at end of file diff --git a/Mimante/Mimante.csproj b/Mimante/Mimante.csproj new file mode 100644 index 0000000..f60561e --- /dev/null +++ b/Mimante/Mimante.csproj @@ -0,0 +1,17 @@ + + + + WinExe + net8.0-windows + enable + enable + true + AutoBidder + AutoBidder + + + + + + + diff --git a/Mimante/Mimante.sln b/Mimante/Mimante.sln new file mode 100644 index 0000000..9ba9265 --- /dev/null +++ b/Mimante/Mimante.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36511.14 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mimante", "Mimante.csproj", "{9BBAEF93-DF66-432C-9349-459E272D6538}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9BBAEF93-DF66-432C-9349-459E272D6538}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9BBAEF93-DF66-432C-9349-459E272D6538}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9BBAEF93-DF66-432C-9349-459E272D6538}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9BBAEF93-DF66-432C-9349-459E272D6538}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1C55CA56-D270-4D9A-91DA-410BF131E905} + EndGlobalSection +EndGlobal