From 9abe4db9c6e1b69cc16bb70e2646ea6deab7ea3a Mon Sep 17 00:00:00 2001 From: tsukiforge <70107300+tsukiforge@users.noreply.github.com> Date: Thu, 17 Sep 2026 23:43:04 +0700 Subject: [PATCH 1/6] v7.6.3: fix storage detection, game session suspend & crosshair centering - Storage: layered WMI detection (MSFT_PhysicalDisk/Win32_DiskDrive) with IOCTL seek-penalty fallback; all-SSD PCs no longer misdetected as HDD - Game Session: atomic NtSuspendProcess/NtResumeProcess suspend path, pending-session auto-recovery, WMI failure no longer aborts the session, per-process suspend isolation, relaxed process identity matching - Crosshair: position now stored as center point so resizing keeps the aim point centered on screen; one-time migration from old corner-based position --- Bloxstrap/Bloxstrap.csproj | 4 +- Bloxstrap/GameSession/GameSessionService.cs | 101 +++++++- Bloxstrap/GameSession/ProcessClassifier.cs | 21 +- Bloxstrap/GameSession/ProcessControl.cs | 135 ++++++++++- .../GameSession/ProcessSuspensionService.cs | 140 ++++++++++- Bloxstrap/Integrations/AutoOptimizeService.cs | 225 +++++++++++++++++- Bloxstrap/Models/Persistable/Settings.cs | 7 +- .../UI/Elements/CrosshairOverlay.xaml.cs | 54 ++++- CHANGELOG.md | 63 +++++ 9 files changed, 705 insertions(+), 45 deletions(-) diff --git a/Bloxstrap/Bloxstrap.csproj b/Bloxstrap/Bloxstrap.csproj index 77117ec..c299c5f 100644 --- a/Bloxstrap/Bloxstrap.csproj +++ b/Bloxstrap/Bloxstrap.csproj @@ -7,8 +7,8 @@ true True Bloxstrap.ico - 7.6.2 - 7.6.2 + 7.6.3 + 7.6.3 $(Version) $(Version) app.manifest diff --git a/Bloxstrap/GameSession/GameSessionService.cs b/Bloxstrap/GameSession/GameSessionService.cs index e816e63..184f458 100644 --- a/Bloxstrap/GameSession/GameSessionService.cs +++ b/Bloxstrap/GameSession/GameSessionService.cs @@ -61,6 +61,21 @@ public async Task BeginSessionAsync(CancellationToken cancell } } + /// + /// v7.6.3 — identitas proses dianggap cukup dikenal bila NAMA prosesnya + /// terbaca, walau path/StartTime tidak bisa dibaca (akibat handle akses + /// dibatasi pada proses tertentu, mis. browser Chromium terbaru, launcher + /// anti-cheat, atau aplikasi UWP). Dulu kondisi itu langsung diklasifikasi + /// Critical sehingga aplikasi yang justru paling ingin di-suspend tidak + /// pernah tersentuh (feedback: "aplikasi yang saya pilih tidak ikut suspend"). + /// Proses tanpa nama tetap ditolak — nama dipakai untuk mencocokkan rule + /// dan untuk me-restore. + /// + public static bool IsKnownProcess(ProcessSnapshot snapshot) + { + return !String.IsNullOrWhiteSpace(snapshot.ProcessName); + } + private async Task BeginSessionCoreAsync(CancellationToken cancellationToken) { const string LOG_IDENT_LOCAL = "GameSession::BeginSession"; @@ -71,19 +86,68 @@ private async Task BeginSessionCoreAsync(CancellationToken ca EndSessionCore(null); if (Store.ReadActive() is not null) - throw new InvalidOperationException("A previous Game Session still has processes pending restore."); + { + // v7.6.3 FIX — dulu kondisi ini melempar InvalidOperationException + // SETIAP kali user masuk game, sehingga Game Session tampak "tidak + // pernah berhasil sama sekali" setelah satu kegagalan restore. + // Sekarang record pending yang tidak bisa di-restore dipulihkan + // sekaligus: semua thread di-resume lewat rescue scan, record + // dibuang, lalu sesi baru dimulai normal. + App.Logger.WriteLine(LOG_IDENT_LOCAL, + "Sesi sebelumnya masih pending restore — memaksa pemulihan dan memulai sesi baru"); + + try + { + IReadOnlyList rescued = Suspension.RescueSuspendedProcesses(); + App.Logger.WriteLine(LOG_IDENT_LOCAL, + $"Pemulihan paksa selesai: {rescued.Count} proses di-resume (rescue scan)"); + } + catch (Exception ex) + { + App.Logger.WriteLine(LOG_IDENT_LOCAL, $"Rescue scan gagal (dilanjutkan): {ex.Message}"); + } + + EndSessionCore(null); + } + } + + SecurityDetectionState detectorState; + try + { + detectorState = await Detector.RefreshAsync(cancellationToken); + } + catch (Exception ex) + { + // v7.6.3 FIX — kegagalan WMI (Timeout/COMException, sering di PC dengan + // antivirus third-party atau WMI repository bermasalah) dulu membuat + // BeginSessionAsync gagal total sehingga TIDAK ADA aplikasi yang + // ter-suspend. Sekarang detector yang error diperlakukan seperti + // Degraded: proses tetap aman dari daftar proteksi statis, user tetap + // bisa suspend aplikasi pilihannya sendiri. + App.Logger.WriteLine(LOG_IDENT_LOCAL, $"Detector refresh gagal — dilanjutkan sebagai Degraded: {ex.Message}"); + detectorState = SecurityDetectionState.Degraded; } - SecurityDetectionState detectorState = await Detector.RefreshAsync(cancellationToken); List processes = _processSource().ToList(); // PID semua Windows service (SCM). Service = komponen sistem/vendor — // sinyal CRITICAL tambahan di classifier supaya audio stack, driver // companion, dan sync service (bahkan yang jalan di session user, // mis. OneDrive.Sync.Service) tidak pernah ter-suspend. - IReadOnlySet serviceProcessIds = ServiceProcessDetector.GetServiceProcessIds(cancellationToken); - if (serviceProcessIds.Count > 0) - App.Logger.WriteLine(LOG_IDENT_LOCAL, $"{serviceProcessIds.Count} Windows service PID terdaftar sebagai protected"); + // v7.6.3: kegagalan query WMI TIDAK lagi membatalkan seluruh sesi — + // cukup dengan daftar proteksi statis (ProcessClassifier) dijalankan. + IReadOnlySet serviceProcessIds; + try + { + serviceProcessIds = ServiceProcessDetector.GetServiceProcessIds(cancellationToken); + if (serviceProcessIds.Count > 0) + App.Logger.WriteLine(LOG_IDENT_LOCAL, $"{serviceProcessIds.Count} Windows service PID terdaftar sebagai protected"); + } + catch (Exception ex) + { + App.Logger.WriteLine(LOG_IDENT_LOCAL, $"Service PID enumeration gagal — lanjut tanpa daftar service: {ex.Message}"); + serviceProcessIds = new HashSet(); + } var session = new GameSessionRecord { @@ -164,8 +228,28 @@ private async Task BeginSessionCoreAsync(CancellationToken ca continue; } - ProcessSuspendResult result = Suspension.SuspendProcess(process.ProcessId, cancellationToken); - if (result.SuspendedThreadIds.Count == 0) + ProcessSuspendResult result; + try + { + result = Suspension.SuspendProcess(process.ProcessId, cancellationToken); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + // v7.6.3 FIX — satu proses bermasalah tidak boleh menggagalkan + // seluruh sesi (dulu exception dari SuspendProcess merambat ke + // try blok luar → EndSessionCore → SEMUA aplikasi yang sudah + // terlanjur disuspend ikut di-resume dan user kehilangan + // proteksi sepenuhnya). + App.Logger.WriteLine(LOG_IDENT_LOCAL, + $"Suspend PID={process.ProcessId} ({process.ProcessName}) gagal — dilewati: {ex.Message}"); + continue; + } + + if (result.SuspendedThreadIds.Count == 0 && !result.ProcessLevelSuspend) continue; session.AppliedRules.Add(RuleKey(rule)); @@ -179,7 +263,7 @@ private async Task BeginSessionCoreAsync(CancellationToken ca AppliedRule = RuleKey(rule), ThreadIds = result.SuspendedThreadIds, TotalThreadCount = result.TotalThreadCount, - SuspendedThreadCount = result.SuspendedThreadIds.Count, + SuspendedThreadCount = result.ProcessLevelSuspend ? result.TotalThreadCount : result.SuspendedThreadIds.Count, FailedThreadCount = result.FailedThreadCount, PartiallySuspended = result.PartiallySuspended }); @@ -188,6 +272,7 @@ private async Task BeginSessionCoreAsync(CancellationToken ca App.Logger.WriteLine( LOG_IDENT_LOCAL, $"{process.ProcessName} ter-suspend {result.SuspendedThreadIds.Count}/{result.TotalThreadCount} thread" + + (result.ProcessLevelSuspend ? " [process-level]" : "") + (result.PartiallySuspended ? " (PartiallySuspended)" : "")); } } diff --git a/Bloxstrap/GameSession/ProcessClassifier.cs b/Bloxstrap/GameSession/ProcessClassifier.cs index d9b1782..4a36a61 100644 --- a/Bloxstrap/GameSession/ProcessClassifier.cs +++ b/Bloxstrap/GameSession/ProcessClassifier.cs @@ -76,13 +76,14 @@ public static ProcessClassification Classify( if (detector.State != SecurityDetectionState.Ok) return ProcessClassification.Critical; - // A readable identity is required before a rule can mutate another process. - if (String.IsNullOrWhiteSpace(snapshot.ProcessName) - || String.IsNullOrWhiteSpace(snapshot.ExecutablePath) - || !snapshot.StartTimeUtc.HasValue) - { + // v7.6.3 FIX — dulu path executable DAN StartTime wajib terbaca, sehingga + // aplikasi yang handle aksesnya dibatasi (browser Chromium, launcher + // anti-cheat, UWP) tidak PERNAH bisa di-suspend meski user mencentangnya + // di UI (feedback: "tidak benar2 ke suspend dan masih berjalan"). + // Nama proses sekarang cukup: pencocokan rule dan label restore memakai + // nama, dan keamanan target tetap dijaga daftar proteksi IsAlwaysProtected. + if (!GameSessionService.IsKnownProcess(snapshot)) return ProcessClassification.Critical; - } return ProcessClassification.Safe; } @@ -97,10 +98,10 @@ public static bool IsCritical( if (IsAlwaysProtected(snapshot, detector, selfProcessId, gameProcessId, serviceProcessIds)) return true; - // Unknown identity is never safe to touch. - return String.IsNullOrWhiteSpace(snapshot.ProcessName) - || String.IsNullOrWhiteSpace(snapshot.ExecutablePath) - || !snapshot.StartTimeUtc.HasValue; + // v7.6.3 — proses tanpa nama tidak bisa dicocokkan ke rule maupun + // di-restore, jadi tetap ditolak. Path/StartTime yang tak terbaca tidak + // lagi dianggap critical (lihat Classify). + return String.IsNullOrWhiteSpace(snapshot.ProcessName); } public static bool IsAlwaysProtected( diff --git a/Bloxstrap/GameSession/ProcessControl.cs b/Bloxstrap/GameSession/ProcessControl.cs index c8a771b..a53aa79 100644 --- a/Bloxstrap/GameSession/ProcessControl.cs +++ b/Bloxstrap/GameSession/ProcessControl.cs @@ -12,20 +12,51 @@ public interface IProcessAccessor : IDisposable bool IsThreadSuspended(int threadId); DateTime? GetStartTimeUtc(); long GetProcessorTimeTicks(); + + // v7.6.3 — suspend/resume level proses (atomik, mencakup thread yang lahir + // di tengah operasi). Implementasi boleh tidak mendukung (returns false). + bool TrySuspendProcess(); + bool TryResumeProcess(); + bool SupportsProcessLevelControl { get; } } + /// + /// v7.6.3 FIX — feedback "aplikasi yang dipilih untuk di-suspend tidak benar-benar + /// ter-suspend dan masih berjalan seperti biasa". + /// + /// Dulu suspend hanya per-thread via SuspendThread dengan maksimal 5 sweep pass + /// dalam 2 detik. Aplikasi seperti browser/launcher terus menambah thread baru, + /// sehingga sweep tidak pernah menangkap semuanya — thread yang lolos membuat + /// proses tetap hidup. Selain itu OpenThread(THREAD_SUSPEND_RESUME) bisa gagal + /// untuk thread yang baru dibuat, dan proses modern yang diproteksi + /// (PROCESS_SUSPEND_RESUME butuh akses eksplisit) menolak handle Process.GetProcessById. + /// + /// Sekarang: NtSuspendProcess/NtResumeProcess (syscall ntdll yang sama dipakai + /// Process Explorer / pssuspend) men-suspend SELURUH proses secara atomik — + /// kernel menahan setiap thread termasuk yang baru lahir. Per-thread API tetap + /// ada sebagai fallback dan untuk verifikasi. + /// internal sealed class Win32ProcessAccessor : IProcessAccessor { private const uint THREAD_SUSPEND_RESUME = 0x0002; + // PROCESS_SUSPEND_RESUME (0x0800) — hak minimal untuk NtSuspendProcess/NtResumeProcess. + private const uint PROCESS_SUSPEND_RESUME = 0x0800; + + private const string LOG_IDENT = "GameSession::Win32ProcessAccessor"; + private readonly Process _process; + private readonly int _processId; public Win32ProcessAccessor(int processId) { + _processId = processId; _process = Process.GetProcessById(processId); } - public int ProcessId => _process.Id; + public int ProcessId => _processId; + + public bool SupportsProcessLevelControl => true; public bool IsAlive { @@ -51,6 +82,94 @@ public IReadOnlyCollection GetThreadIds() } } + public bool TrySuspendProcess() + { + if (TryProcessLevelSuspend()) + return true; + + // Fallback: sweep semua thread saat ini. Tidak sempurna (thread baru + // bisa lolos), tapi tetap lebih baik daripada gagal total. + int attempted = 0, succeeded = 0; + foreach (int threadId in GetThreadIds()) + { + attempted++; + if (TrySuspendThread(threadId)) + succeeded++; + } + + App.Logger.WriteLine(LOG_IDENT, + $"PID={_processId}: NtSuspendProcess unavailable — per-thread fallback {succeeded}/{attempted} threads"); + return attempted > 0 && succeeded == attempted; + } + + public bool TryResumeProcess() + { + if (TryProcessLevelResume()) + return true; + + int attempted = 0, succeeded = 0; + foreach (int threadId in GetThreadIds()) + { + attempted++; + if (TryResumeThread(threadId)) + succeeded++; + } + + App.Logger.WriteLine(LOG_IDENT, + $"PID={_processId}: NtResumeProcess unavailable — per-thread fallback {succeeded}/{attempted} threads"); + return attempted > 0 && succeeded == attempted; + } + + private bool TryProcessLevelSuspend() + { + IntPtr handle = OpenProcess(PROCESS_SUSPEND_RESUME, false, (uint)_processId); + if (handle == IntPtr.Zero) + { + App.Logger.WriteLine(LOG_IDENT, + $"PID={_processId}: OpenProcess(PROCESS_SUSPEND_RESUME) failed (error={Marshal.GetLastWin32Error()})"); + return false; + } + + try + { + NTSTATUS status = NtSuspendProcess(handle); + if (status != NTSTATUS.Success) + { + App.Logger.WriteLine(LOG_IDENT, $"PID={_processId}: NtSuspendProcess returned {status}"); + return false; + } + + return true; + } + finally + { + CloseHandle(handle); + } + } + + private bool TryProcessLevelResume() + { + IntPtr handle = OpenProcess(PROCESS_SUSPEND_RESUME, false, (uint)_processId); + if (handle == IntPtr.Zero) + return false; + + try + { + NTSTATUS status = NtResumeProcess(handle); + if (status != NTSTATUS.Success) + { + App.Logger.WriteLine(LOG_IDENT, $"PID={_processId}: NtResumeProcess returned {status}"); + return false; + } + + return true; + } + finally + { + CloseHandle(handle); + } + } + public bool TrySuspendThread(int threadId) { IntPtr handle = OpenThread(THREAD_SUSPEND_RESUME, false, (uint)threadId); @@ -130,6 +249,9 @@ public long GetProcessorTimeTicks() public void Dispose() => _process.Dispose(); + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr OpenProcess(uint desiredAccess, bool inheritHandle, uint processId); + [DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr OpenThread(uint desiredAccess, bool inheritHandle, uint threadId); @@ -142,5 +264,16 @@ public long GetProcessorTimeTicks() [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool CloseHandle(IntPtr handle); + + private enum NTSTATUS : uint + { + Success = 0x00000000 + } + + [DllImport("ntdll.dll")] + private static extern NTSTATUS NtSuspendProcess(IntPtr processHandle); + + [DllImport("ntdll.dll")] + private static extern NTSTATUS NtResumeProcess(IntPtr processHandle); } } diff --git a/Bloxstrap/GameSession/ProcessSuspensionService.cs b/Bloxstrap/GameSession/ProcessSuspensionService.cs index d5d148b..c5218a8 100644 --- a/Bloxstrap/GameSession/ProcessSuspensionService.cs +++ b/Bloxstrap/GameSession/ProcessSuspensionService.cs @@ -10,6 +10,13 @@ public sealed class ProcessSuspendResult public int FailedThreadCount { get; init; } public bool PartiallySuspended { get; init; } public int SweepPasses { get; init; } + + /// + /// v7.6.3 — true bila proses di-suspend secara atomik via NtSuspendProcess. + /// ThreadIds yang tercatat saat itu hanya snapshot untuk verifikasi; resume + /// memakai jalur level proses, bukan daftar thread ini. + /// + public bool ProcessLevelSuspend { get; init; } } public sealed class RescuedProcess @@ -62,7 +69,74 @@ private static IEnumerable DefaultProcessSource() return snapshots; } + /// + /// v7.6.3 FIX — jalur utama sekarang NtSuspendProcess (atomik). Versi lama + /// men-suspend per-thread dengan maksimal 5 sweep pass; aplikasi yang terus + /// menambah thread (browser, launcher, Discord) selalu punya thread baru yang + /// lolos antar sweep, sehingga proses "tetap berjalan seperti biasa" padahal + /// statusnya katanya suspended. NtSuspendProcess membuat kernel menahan semua + /// thread — termasuk yang lahir selama operasi — tanpa race. + /// public ProcessSuspendResult SuspendProcess(int processId, CancellationToken cancellationToken = default) + { + const string LOG_IDENT = "GameSession::SuspendProcess"; + var stopwatch = Stopwatch.StartNew(); + + try + { + using IProcessAccessor accessor = _accessorFactory(processId); + cancellationToken.ThrowIfCancellationRequested(); + + IReadOnlyCollection threadSnapshot = accessor.GetThreadIds(); + + if (accessor.TrySuspendProcess()) + { + // Verifikasi: probe beberapa thread untuk memastikan benar-benar + // tersuspend. Bila NtSuspendProcess sukses, semua thread pasti + // suspend count >= 1. + int verified = 0; + int probeBudget = Math.Min(threadSnapshot.Count, 8); + foreach (int threadId in threadSnapshot.Take(probeBudget)) + { + if (accessor.IsThreadSuspended(threadId)) + verified++; + } + + App.Logger.WriteLine( + LOG_IDENT, + $"PID={processId}: process-level suspend OK; threads~{threadSnapshot.Count}; verified={verified}/{probeBudget}"); + + return new ProcessSuspendResult + { + SuspendedThreadIds = threadSnapshot.ToList(), + TotalThreadCount = threadSnapshot.Count, + FailedThreadCount = 0, + PartiallySuspended = false, + SweepPasses = 1, + ProcessLevelSuspend = true + }; + } + + // Process-level gagal (mis. ntdll tidak tersedia) — jalur lama per-thread. + App.Logger.WriteLine(LOG_IDENT, $"PID={processId}: process-level suspend unavailable — falling back to per-thread sweep"); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + App.Logger.WriteLine(LOG_IDENT, $"PID={processId}: process-level suspend failed: {ex.Message} — falling back to per-thread sweep"); + } + + return SuspendProcessPerThread(processId, cancellationToken); + } + + /// + /// Jalur lama (v7.6.2 dan sebelumnya): suspend per-thread dengan sweep. + /// Dipertahankan sebagai fallback dan untuk test/diagnostik. + /// + public ProcessSuspendResult SuspendProcessPerThread(int processId, CancellationToken cancellationToken = default) { const string LOG_IDENT = "GameSession::SuspendProcess"; var result = new ProcessSuspendResultBuilder(); @@ -226,12 +300,58 @@ public RestoreResult RestoreProcess(SuspendedProcessRecord record) } } - // ── FIX: Enhanced verification logging + retry (v7.3.1) ────────── - // Pola SynTPEnh: thread State masih Suspended setelah 2x resume+sleep. - // Root cause dugaan: Windows thread scheduler belum update state setelah - // resume (timing-dependent, bukan resume yang benar-benar gagal). Retry - // tambahan (100ms delay) sebelum VerificationFailed memberi waktu OS - // untuk update thread state. + if (!accessor.IsAlive) + return Failed(record, RestoreStatus.NotFound, + "Proses sudah ditutup manual sebelum restore."); + + // ── v7.6.3 FIX — resume level proses dulu ─────────────────────────── + // NtResumeProcess membalikkan NtSuspendProcess secara atomik: kernel + // menurunkan suspend count SETIAP thread, termasuk thread yang lahir + // setelah suspend. Ini menutup celah lama di mana thread baru (spawn + // saat proses terlanjur disuspend per-thread, atau app yang restart + // subprocess-nya sendiri) tidak pernah masuk daftar ThreadIds dan + // tertinggal beku — serta pola "VerificationFailed" pada app yang + // spawn thread baru saat direstore. + if (accessor.SupportsProcessLevelControl && accessor.TryResumeProcess()) + { + // Verifikasi ringkas: thread snapshot lama tidak boleh ada yang + // masih tersuspend. Thread yang sudah mati dihitung sukses. + for (int attempt = 0; attempt < 2; attempt++) + { + if (!accessor.IsAlive) + return Failed(record, RestoreStatus.NotFound, + "Proses sudah ditutup manual saat verifikasi restore."); + + Thread.Sleep(100); + + IReadOnlyCollection currentThreadIds = accessor.GetThreadIds(); + bool stillSuspended = record.ThreadIds + .Distinct() + .Any(threadId => currentThreadIds.Contains(threadId) && accessor.IsThreadSuspended(threadId)); + + if (!stillSuspended) + { + App.Logger.WriteLine(LOG_IDENT, + $"PID={record.ProcessId} ({record.ProcessName}) restored via process-level resume and verified."); + return new RestoreResult + { + ProcessName = record.ProcessName, + Status = RestoreStatus.Restored, + Message = "Proses kembali berjalan dan terverifikasi." + }; + } + + // Thread yang masih tersuspend punya suspend count > 1 (pernah + // di-suspend dua kali) — resume sekali lagi. + foreach (int threadId in record.ThreadIds.Distinct()) + accessor.TryResumeThread(threadId); + } + + return Failed(record, RestoreStatus.VerificationFailed, + "Process-level resume dijalankan namun sebagian thread masih tersuspend."); + } + + // ── Fallback jalur lama: resume per-thread berdasarkan catatan ────── int resumeFailures = 0; int initialThreadCount = record.ThreadIds.Distinct().Count(); @@ -254,8 +374,8 @@ public RestoreResult RestoreProcess(SuspendedProcessRecord record) // Snapshot current thread count for logging (new threads spawned between // suspend and restore would explain SynTPEnh's VerificationFailed pattern). - IReadOnlyCollection currentThreadIds = accessor.GetThreadIds(); - int currentThreadCount = currentThreadIds.Count; + IReadOnlyCollection currentThreadIdsFallback = accessor.GetThreadIds(); + int currentThreadCount = currentThreadIdsFallback.Count; int newThreadCount = currentThreadCount - initialThreadCount; for (int attempt = 0; attempt < 2; attempt++) @@ -269,10 +389,10 @@ public RestoreResult RestoreProcess(SuspendedProcessRecord record) Thread.Sleep(100); // Re-read thread list — threads may have appeared/disappeared. - currentThreadIds = accessor.GetThreadIds(); + currentThreadIdsFallback = accessor.GetThreadIds(); bool stillSuspended = record.ThreadIds .Distinct() - .Any(threadId => currentThreadIds.Contains(threadId) && accessor.IsThreadSuspended(threadId)); + .Any(threadId => currentThreadIdsFallback.Contains(threadId) && accessor.IsThreadSuspended(threadId)); if (!stillSuspended && resumeFailures == 0) { diff --git a/Bloxstrap/Integrations/AutoOptimizeService.cs b/Bloxstrap/Integrations/AutoOptimizeService.cs index 883900e..79dfafe 100644 --- a/Bloxstrap/Integrations/AutoOptimizeService.cs +++ b/Bloxstrap/Integrations/AutoOptimizeService.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.Management; using System.Runtime.InteropServices; namespace Bloxstrap.Integrations @@ -279,10 +280,230 @@ public static void ForceRefreshHardwareCache() App.Logger.WriteLine(LOG_IDENT, $"Hardware detection manually refreshed: {(isSSD ? "SSD" : "HDD")} (persistent cache re-written)"); } + // ── v7.6.3 FIX: deteksi tipe storage via WMI ───────────────────────────── + // Dulu hanya memakai IOCTL_STORAGE_QUERY_PROPERTY (seek penalty) ke volume + // handle yang dibuka dengan GENERIC_READ — pada Windows modern handle volume + // butuh hak admin, jadi CreateFile sering gagal dan kode jatuh ke fallback + // "Assuming HDD". Akibatnya PC all-SSD terdeteksi sebagai HDD (feedback + // v7.6.0: "PC saya tidak ada HDD sama sekali, adanya tipe SSD saja"). + // + // Sekarang deteksi berlapis, layer pertama yang berhasil dipakai: + // 1. WMI MSFT_PhysicalDisk (MediaType + BusType NVMe) — akurat tanpa admin. + // 2. WMI Win32_DiskDrive — heuristik nama model untuk disk yang tidak + // terklasifikasi oleh MSFT_PhysicalDisk. + // 3. IOCTL seek-penalty (logika ASLI, nama diubah jadi + // DetectStorageTypeViaSeekPenalty) — fallback terakhir. + private static bool DetectStorageType() + { + bool? wmiResult = TryDetectStorageTypeViaWmi(); + if (wmiResult.HasValue) + return wmiResult.Value; + + App.Logger.WriteLine(LOG_IDENT, "WMI storage detection unavailable — falling back to IOCTL seek-penalty query"); + return DetectStorageTypeViaSeekPenalty(); + } + + private static bool? TryDetectStorageTypeViaWmi() + { + try + { + // Key = nomor physical disk; value = "SSD" / "HDD" (hanya diisi bila yakin). + var kindByDisk = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // 1) MSFT_PhysicalDisk — sumber paling akurat. + try + { + using var searcher = new ManagementObjectSearcher( + @"\\.\root\Microsoft\Windows\Storage", + "SELECT DeviceId, BusType, MediaType FROM MSFT_PhysicalDisk"); + + foreach (ManagementBaseObject disk in searcher.Get()) + { + try + { + string? diskNumber = disk["DeviceId"]?.ToString()?.Trim(); + if (String.IsNullOrWhiteSpace(diskNumber)) + continue; + + string? kind = ClassifyPhysicalDisk( + disk["MediaType"]?.ToString(), + disk["BusType"]?.ToString()); + + if (kind is not null) + kindByDisk[diskNumber] = kind; + } + finally { disk.Dispose(); } + } + } + catch (Exception ex) + { + App.Logger.WriteLine(LOG_IDENT, $"MSFT_PhysicalDisk query failed: {ex.Message}"); + } + + if (kindByDisk.Count > 0) + { + App.Logger.WriteLine(LOG_IDENT, + "MSFT_PhysicalDisk classified: " + + String.Join(", ", kindByDisk.Select(kv => $"disk #{kv.Key} = {kv.Value}"))); + } + + // 2) Win32_DiskDrive — heuristik nama model untuk disk yang belum + // terklasifikasi (MediaType sering "Unspecified" pada SATA lama). + try + { + using var searcher = new ManagementObjectSearcher( + @"\\.\root\cimv2", + "SELECT Index, Model FROM Win32_DiskDrive"); + + foreach (ManagementBaseObject disk in searcher.Get()) + { + try + { + string? diskNumber = disk["Index"]?.ToString()?.Trim(); + if (String.IsNullOrWhiteSpace(diskNumber) || kindByDisk.ContainsKey(diskNumber)) + continue; + + bool? guess = GuessSsdFromModel(disk["Model"]?.ToString()); + if (guess.HasValue) + kindByDisk[diskNumber] = guess.Value ? "SSD" : "HDD"; + } + finally { disk.Dispose(); } + } + } + catch (Exception ex) + { + App.Logger.WriteLine(LOG_IDENT, $"Win32_DiskDrive query failed: {ex.Message}"); + } + + if (kindByDisk.Count == 0) + { + App.Logger.WriteLine(LOG_IDENT, "WMI storage detection returned no classified disks"); + return null; + } + + bool anySsd = kindByDisk.Values.Any(kind => kind == "SSD"); + bool anyHdd = kindByDisk.Values.Any(kind => kind == "HDD"); + + // PC all-SSD (kasus feedback v7.6.0): tidak ada disk yang terklasifikasi + // HDD → pasti SSD, tidak perlu peta drive-letter → disk. + if (!anyHdd && anySsd) + { + App.Logger.WriteLine(LOG_IDENT, + $"Storage detection via WMI: all {kindByDisk.Count} classified disk(s) are SSD/NVMe"); + return true; + } + + // Mixed (SSD + HDD): tentukan disk yang menampung volume sistem. + string? systemDiskNumber = GetSystemPhysicalDiskNumber(); + if (!String.IsNullOrEmpty(systemDiskNumber) + && kindByDisk.TryGetValue(systemDiskNumber, out string? systemKind)) + { + App.Logger.WriteLine(LOG_IDENT, + $"Storage detection via WMI: system disk #{systemDiskNumber} is {systemKind}"); + return systemKind == "SSD"; + } + + App.Logger.WriteLine(LOG_IDENT, "Storage detection via WMI: system disk could not be classified — falling back to IOCTL"); + return null; + } + catch (Exception ex) + { + App.Logger.WriteLine(LOG_IDENT, $"WMI storage detection failed: {ex.Message}"); + return null; + } + } + + private static string? ClassifyPhysicalDisk(string? mediaTypeRaw, string? busTypeRaw) + { + // MSFT_PhysicalDisk.MediaType: 0 = Unspecified, 3 = HDD, 4 = SSD. + if (ushort.TryParse(mediaTypeRaw, out ushort mediaType)) + { + if (mediaType == 4) + return "SSD"; + if (mediaType == 3) + return "HDD"; + } + + // BusType 17 = NVMe → selalu SSD. Jenis lain ambigu (SATA/ATA bisa + // HDD maupun SSD), sedangkan USB/SD/virtual tidak relevan sebagai disk sistem. + if (ushort.TryParse(busTypeRaw, out ushort busType) && busType == 17) + return "SSD"; + + return null; + } + + private static bool? GuessSsdFromModel(string? model) + { + if (String.IsNullOrWhiteSpace(model)) + return null; + + string upper = model.ToUpperInvariant(); + + // Bias ke SSD: salah deteksi "SSD" hanya membuat preset HDD tidak aktif, + // sedangkan salah deteksi "HDD" membuat PC all-SSD dapat preset HDD. + if (upper.Contains("SSD") || upper.Contains("NVME") || upper.Contains("NVM EXPRESS") || upper.Contains("M.2")) + return true; + + // Tidak menebak HDD dari nama model — terlalu berisiko. + return null; + } + + /// + /// Petakan drive letter volume sistem (mis. "C:") ke nomor physical disk + /// melalui asosiasi WMI: LogicalDisk → Partition → DiskDrive. + /// + private static string? GetSystemPhysicalDiskNumber() + { + try + { + string driveLetter = (Path.GetPathRoot(Environment.SystemDirectory) ?? "C:").TrimEnd('\\'); + + string? partitionDeviceId = null; + using (var searcher = new ManagementObjectSearcher( + @"\\.\root\cimv2", + $"ASSOCIATORS OF {{Win32_LogicalDisk.DeviceID='{driveLetter}'}} WHERE AssocClass=Win32_LogicalDiskToPartition")) + { + foreach (ManagementBaseObject partition in searcher.Get()) + { + try { partitionDeviceId = partition["DeviceID"]?.ToString(); } + finally { partition.Dispose(); } + + if (!String.IsNullOrEmpty(partitionDeviceId)) + break; + } + } + + if (String.IsNullOrEmpty(partitionDeviceId)) + return null; + + using var diskSearcher = new ManagementObjectSearcher( + @"\\.\root\cimv2", + $"ASSOCIATORS OF {{Win32_DiskPartition.DeviceID='{partitionDeviceId}'}} WHERE AssocClass=Win32_DiskDriveToDiskPartition"); + + foreach (ManagementBaseObject disk in diskSearcher.Get()) + { + try + { + string? index = disk["Index"]?.ToString()?.Trim(); + if (!String.IsNullOrWhiteSpace(index)) + return index; + } + finally { disk.Dispose(); } + } + } + catch (Exception ex) + { + App.Logger.WriteLine(LOG_IDENT, $"System disk mapping failed: {ex.Message}"); + } + + return null; + } + // Query seek-penalty IOCTL — logika deteksi ASLI (tidak diubah), hanya // dipisah dari caching supaya IsSSD() bisa memotong query ini saat - // persistent cache masih valid. - private static bool DetectStorageType() + // persistent cache masih valid. Sejak v7.6.3 dipakai sebagai fallback + // terakhir bila kedua query WMI tidak tersedia. + private static bool DetectStorageTypeViaSeekPenalty() { try { diff --git a/Bloxstrap/Models/Persistable/Settings.cs b/Bloxstrap/Models/Persistable/Settings.cs index d18b741..67e2a4f 100644 --- a/Bloxstrap/Models/Persistable/Settings.cs +++ b/Bloxstrap/Models/Persistable/Settings.cs @@ -101,8 +101,13 @@ public class Settings public string CrosshairColor { get; set; } = "#00FF00"; // Lime green default public double CrosshairSize { get; set; } = 40; // 20-200px public double CrosshairOpacity { get; set; } = 0.8; // 0.1-1.0 - public double CrosshairX { get; set; } = 0; // screen position + public double CrosshairX { get; set; } = 0; // DEPRECATED (v7.6.3): dulu pojok kiri-atas window — bikin crosshair bergeser saat ukuran diubah. Tidak dipakai lagi. public double CrosshairY { get; set; } = 0; + // v7.6.3 FIX: posisi crosshair kini disimpan sebagai TITIK TENGAH crosshair di layar. + // Jadi apapun ukuran crosshair, titik bidik selalu tepat di posisi yang sama (tengah layar). + // (0, 0) = belum pernah digeser → otomatis di tengah layar. + public double CrosshairCenterX { get; set; } = 0; + public double CrosshairCenterY { get; set; } = 0; // wallpaper background (EnableWallpaperLauncher dihapus — FIX 3: background selalu aktif) diff --git a/Bloxstrap/UI/Elements/CrosshairOverlay.xaml.cs b/Bloxstrap/UI/Elements/CrosshairOverlay.xaml.cs index 1391deb..b17ce86 100644 --- a/Bloxstrap/UI/Elements/CrosshairOverlay.xaml.cs +++ b/Bloxstrap/UI/Elements/CrosshairOverlay.xaml.cs @@ -54,12 +54,23 @@ public void ApplyCurrentSettings() double gap = size * 0.25; double opacity = Math.Clamp(settings.CrosshairOpacity, 0.1, 1.0); + // v7.6.3 FIX — catat posisi TITIK TENGAH crosshair saat ini SEBELUM window + // di-resize, lalu kembalikan window sehingga titik tengahnya tetap di + // tempat yang sama. Dulu Left/Top tidak disentuh saat ukuran berubah, dan + // karena ukuran window mengikuti CrosshairSize, crosshair membesar ke + // kanan-bawah → pusatnya bergeser dan tidak lagi di tengah layar. + double anchorCenterX = Left + Width / 2; + double anchorCenterY = Top + Height / 2; + Width = size + 40; Height = size + 40; CrosshairCanvas.Width = Width; CrosshairCanvas.Height = Height; this.Opacity = opacity; + Left = anchorCenterX - Width / 2; + Top = anchorCenterY - Height / 2; + CrosshairCanvas.Children.Clear(); var brush = new SolidColorBrush(color); @@ -184,6 +195,7 @@ private void Window_MouseDown(object sender, MouseButtonEventArgs e) { _isDragging = true; _lastMousePos = PointToScreen(e.GetPosition(this)); + SavePosition(); } } @@ -208,8 +220,12 @@ private void SavePosition() { try { - App.Settings.Prop.CrosshairX = Left; - App.Settings.Prop.CrosshairY = Top; + // v7.6.3 FIX — simpan TITIK TENGAH crosshair, bukan pojok kiri-atas + // window. Dengan begitu perubahan ukuran (window membesar/mengecil) + // tidak menggeser titik bidik: LoadPosition selalu meletakkan window + // di sekitar titik tengah yang sama. + App.Settings.Prop.CrosshairCenterX = Left + Width / 2; + App.Settings.Prop.CrosshairCenterY = Top + Height / 2; App.Settings.Save(); } catch (Exception ex) @@ -222,17 +238,33 @@ private void LoadPosition() { try { - // Default: center of screen - if (App.Settings.Prop.CrosshairX == 0 && App.Settings.Prop.CrosshairY == 0) - { - Left = (SystemParameters.PrimaryScreenWidth - Width) / 2; - Top = (SystemParameters.PrimaryScreenHeight - Height) / 2; - } - else + double screenCenterX = SystemParameters.PrimaryScreenWidth / 2; + double screenCenterY = SystemParameters.PrimaryScreenHeight / 2; + + // Default: tengah layar. CenterX/CenterY == 0 berarti belum pernah + // digeser user (titik tengah sah tidak pernah 0 persis di layar riil). + double centerX = App.Settings.Prop.CrosshairCenterX != 0 || App.Settings.Prop.CrosshairCenterY != 0 + ? App.Settings.Prop.CrosshairCenterX + : screenCenterX; + double centerY = App.Settings.Prop.CrosshairCenterX != 0 || App.Settings.Prop.CrosshairCenterY != 0 + ? App.Settings.Prop.CrosshairCenterY + : screenCenterY; + + // Migrasi satu kali dari pengaturan lama (pojok kiri-atas window): + // jika ada nilai lama yang tidak nol, konversi ke titik tengah dengan + // ukuran window saat ini, simpan, lalu abaikan selamanya. + if ((App.Settings.Prop.CrosshairCenterX == 0 && App.Settings.Prop.CrosshairCenterY == 0) + && (App.Settings.Prop.CrosshairX != 0 || App.Settings.Prop.CrosshairY != 0)) { - Left = App.Settings.Prop.CrosshairX; - Top = App.Settings.Prop.CrosshairY; + centerX = App.Settings.Prop.CrosshairX + Width / 2; + centerY = App.Settings.Prop.CrosshairY + Height / 2; + App.Settings.Prop.CrosshairCenterX = centerX; + App.Settings.Prop.CrosshairCenterY = centerY; + try { App.Settings.Save(); } catch { } } + + Left = centerX - Width / 2; + Top = centerY - Height / 2; } catch { diff --git a/CHANGELOG.md b/CHANGELOG.md index 82cc6cb..88a7682 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,68 @@ # BoneFish Changelog +## v7.6.3 - Fix Deteksi Storage, Game Session Suspend & Crosshair Centering + +Release date: 2026-09-17 + +### 💾 FIX 1 — Deteksi tipe disk salah (PC all-SSD terdeteksi HDD) + +Deteksi lama hanya mengandalkan IOCTL seek-penalty ke volume handle yang dibuka +with `GENERIC_READ` — pada Windows modern ini butuh hak admin, sering gagal, dan +kode jatuh ke fallback "Assuming HDD". Akibatnya PC yang seluruh disk-nya SSD +terdeteksi sebagai HDD. + +- Deteksi sekarang berlapis: **WMI `MSFT_PhysicalDisk`** (MediaType + BusType + NVMe) → **heuristik model `Win32_DiskDrive`** → IOCTL seek-penalty (fallback + terakhir, logika lama tidak dibuang). +- PC yang semua disk terklasifikasi SSD/NVMe langsung dianggap SSD tanpa perlu + pemetaan drive-letter → disk. +- Pada sistem mixed (SSD + HDD), disk penampung Windows ditentukan lewat + asosiasi WMI LogicalDisk → Partition → DiskDrive. +- Hasil tetap di-cache persisten 30 hari; tombol "Deteksi Ulang Hardware" + tetap berfungsi untuk refresh paksa. + +### 🎮 FIX 2 — Game Session Manager: aplikasi tidak benar-benar ter-suspend + +Empat akar masalah diperbaiki sekaligus: + +- **Suspend per-thread sering bocor.** Sweep per-thread (maks 5 pass / 2 detik) + selalu ketinggalan thread baru yang lahir di tengah operasi — browser, + launcher, dan Discord terus menambah thread, sehingga proses tetap hidup. + Jalur utama kini **`NtSuspendProcess`/`NtResumeProcess`** (atomik, level + proses — sama seperti Process Explorer/pssuspend), dengan sweep per-thread + lama sebagai fallback dan untuk rescue scan. +- **Sesi pending memblokir sesi baru selamanya.** Satu record restore yang + gagal membuat `BeginSessionAsync` melempar `InvalidOperationException` setiap + kali user masuk game — fitur terlihat "tidak pernah berhasil". Sekarang record + pending dipulihkan paksa (rescue scan), dibuang, lalu sesi baru mulai normal. +- **Kegagalan WMI membatalkan seluruh sesi.** Kegagalan query Security Center / + service PID (umum pada PC dengan antivirus pihak ketiga) kini diperlakukan + seperti state Degraded — daftar proteksi statis tetap berjalan, aplikasi + pilihan user tetap di-suspend. +- **Identitas proses terlalu ketat.** Aplikasi yang path/StartTime-nya tidak + bisa dibaca (browser Chromium, launcher anti-cheat, UWP) dulu diklasifikasi + Critical sehingga tidak pernah tersentuh meski user mencentangnya. Nama proses + kini cukup; proteksi keamanan tetap dari daftar IsAlwaysProtected + service + PID + Session 0. Satu proses yang gagal di-suspend juga tidak lagi menggugurkan + sesi (dulu semua aplikasi yang sudah terlanjur disuspend ikut di-resume). + +### 🎯 FIX 3 — Crosshair bergeser saat ukuran diperbesar + +Posisi crosshair dulu disimpan sebagai pojok kiri-atas window, sementara ukuran +window mengikuti pengaturan CrosshairSize — jadi memperbesar crosshair +menggeser pusat bidik ke kanan-bawah, keluar dari tengah layar. + +- Posisi kini disimpan sebagai **titik tengah crosshair** (`CrosshairCenterX/Y`). + Pengubahan ukuran/style menggeser window, bukan pusatnya — titik bidik selalu + tepat di tengah layar dalam ukuran apapun. +- Posisi lama dimigrasi satu kali otomatis saat pertama kali dijalankan. + +### ✅ Verifikasi + +- `dotnet build` Release konfigurasi Debug berhasil (0 error). +- Perilaku restore lama tetap kompatibel dengan record `active.json` versi + sebelumnya (fallback jalur per-thread bila record tidak ber-flag process-level). + ## v7.6.2 - Bug Fix Update Check Transparency Release date: 2026-09-17 From 5795d119029f57c5e48ada2370f5d683a00c8628 Mon Sep 17 00:00:00 2001 From: "faiz.exe" <70107300+tsukiforge@users.noreply.github.com> Date: Thu, 17 Sep 2026 17:18:53 +0000 Subject: [PATCH 2/6] Fix build error: Add EnableWindowsTargeting for Linux compatibility --- Bloxstrap/Bloxstrap.csproj | 5 +++-- wpfui | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Bloxstrap/Bloxstrap.csproj b/Bloxstrap/Bloxstrap.csproj index c299c5f..cd1c8bf 100644 --- a/Bloxstrap/Bloxstrap.csproj +++ b/Bloxstrap/Bloxstrap.csproj @@ -7,8 +7,8 @@ true True Bloxstrap.ico - 7.6.3 - 7.6.3 + 7.7.0 + 7.7.3 $(Version) $(Version) app.manifest @@ -18,6 +18,7 @@ $(AssemblyName) Bloxstrap false + true diff --git a/wpfui b/wpfui index 25cee6b..e387e48 160000 --- a/wpfui +++ b/wpfui @@ -1 +1 @@ -Subproject commit 25cee6b4b3138722af020ebd257a59a0804302c9 +Subproject commit e387e48fae3baf838407d3ac56194a6d221b5cd6 From f3ae67a11b7f2e382838e3db54fca41ed711eede Mon Sep 17 00:00:00 2001 From: "faiz.exe" <70107300+tsukiforge@users.noreply.github.com> Date: Fri, 18 Sep 2026 03:13:46 +0000 Subject: [PATCH 3/6] --- Bloxstrap/Bloxstrap.csproj | 4 ++-- CHANGELOG.md | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Bloxstrap/Bloxstrap.csproj b/Bloxstrap/Bloxstrap.csproj index cd1c8bf..e63bc95 100644 --- a/Bloxstrap/Bloxstrap.csproj +++ b/Bloxstrap/Bloxstrap.csproj @@ -7,8 +7,8 @@ true True Bloxstrap.ico - 7.7.0 - 7.7.3 + 7.6.8 + 7.6.8 $(Version) $(Version) app.manifest diff --git a/CHANGELOG.md b/CHANGELOG.md index 88a7682..faadca7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # BoneFish Changelog +## v7.7.0 - Build Fix: EnableWindowsTargeting untuk Linux + +Release date: 2026-09-18 + +### 🛠️ FIX — Build Compatibility di Linux + +- Menambahkan `true` ke `Bloxstrap/Bloxstrap.csproj` dan `wpfui/src/Wpf.Ui/Wpf.Ui.csproj` agar proyek bisa build di environment non-Windows (Linux). +- Perubahan ini hanya affect kompilasi dan tidak mempengaruhi fitur pengguna. +- Build sekarang sukses dengan 0 error, 0 warning di platform Linux. + +### ✅ Verifikasi + +- `dotnet build` Build berhasil pada Linux dengan 0 error, 0 warning. +- `git diff` menampilkan perubahan di 2 file csproj yang sudah di-commit. + + ## v7.6.3 - Fix Deteksi Storage, Game Session Suspend & Crosshair Centering Release date: 2026-09-17 From ee127131c5ac5a0fb3bd44d5489f82b1a7ce4d01 Mon Sep 17 00:00:00 2001 From: "faiz.exe" <70107300+tsukiforge@users.noreply.github.com> Date: Fri, 18 Sep 2026 05:12:10 +0000 Subject: [PATCH 4/6] update --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index 2d4581e..dc8e210 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "wpfui"] path = wpfui - url = https://github.com/Kukki-Studio/wpfui.git + url = https://github.com/BoneFishStudio/wpfui.git [submodule "website"] path = website url = https://github.com/BoneFishStudio/BoneFishStudio.git From 7cb5a9e96d8ba3775cf255402dc228d8b8cfd85e Mon Sep 17 00:00:00 2001 From: "faiz.exe" <70107300+tsukiforge@users.noreply.github.com> Date: Fri, 18 Sep 2026 05:26:26 +0000 Subject: [PATCH 5/6] Fix CI: reset submodule wpfui ke commit remote & rapikan changelog v7.7.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pointer submodule wpfui (e387e48) tidak ada di remote sehingga checkout CI gagal ("not our ref"). Kembalikan ke 25cee6b (identik dengan commit v7.6.3 yang CI-nya hijau) dan kirim EnableWindowsTargeting via flag CI, sehingga submodule tidak perlu dimodifikasi. 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- CHANGELOG.md | 10 ++++++---- wpfui | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index faadca7..557a011 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,21 @@ # BoneFish Changelog -## v7.7.0 - Build Fix: EnableWindowsTargeting untuk Linux +## v7.7.0 - CI Fix: EnableWindowsTargeting & Perbaikan Submodule wpfui Release date: 2026-09-18 ### 🛠️ FIX — Build Compatibility di Linux -- Menambahkan `true` ke `Bloxstrap/Bloxstrap.csproj` dan `wpfui/src/Wpf.Ui/Wpf.Ui.csproj` agar proyek bisa build di environment non-Windows (Linux). +- Menambahkan `true` ke `Bloxstrap/Bloxstrap.csproj` agar proyek bisa di-compile di environment non-Windows (Linux/CI). +- Flag yang sama dikirim saat restore di CI (`-p:EnableWindowsTargeting=true`), sehingga proyek submodule `wpfui` tidak perlu dimodifikasi. +- Pointer submodule `wpfui` dikembalikan ke commit yang tersedia di remote, memperbaiki kegagalan checkout CI (`not our ref e387e48...`). - Perubahan ini hanya affect kompilasi dan tidak mempengaruhi fitur pengguna. - Build sekarang sukses dengan 0 error, 0 warning di platform Linux. ### ✅ Verifikasi -- `dotnet build` Build berhasil pada Linux dengan 0 error, 0 warning. -- `git diff` menampilkan perubahan di 2 file csproj yang sudah di-commit. +- `dotnet build BoneFish.sln` sukses di Linux dengan 0 error, 0 warning. +- Pointer submodule diverifikasi identik dengan commit v7.6.3 yang CI-nya hijau. ## v7.6.3 - Fix Deteksi Storage, Game Session Suspend & Crosshair Centering diff --git a/wpfui b/wpfui index e387e48..25cee6b 160000 --- a/wpfui +++ b/wpfui @@ -1 +1 @@ -Subproject commit e387e48fae3baf838407d3ac56194a6d221b5cd6 +Subproject commit 25cee6b4b3138722af020ebd257a59a0804302c9 From 9ed0c372a2229242ed8d763988622bb44f75b8e7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 05:28:30 +0000 Subject: [PATCH 6/6] Bump website from `2420679` to `8b79d91` Bumps [website](https://github.com/BoneFishStudio/BoneFishStudio) from `2420679` to `8b79d91`. - [Commits](https://github.com/BoneFishStudio/BoneFishStudio/compare/242067999607101620f14c06222bdd82035b3a46...8b79d919a20a05b5c832c9663a65ce400d784093) --- updated-dependencies: - dependency-name: website dependency-version: 8b79d919a20a05b5c832c9663a65ce400d784093 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- website | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website b/website index 2420679..8b79d91 160000 --- a/website +++ b/website @@ -1 +1 @@ -Subproject commit 242067999607101620f14c06222bdd82035b3a46 +Subproject commit 8b79d919a20a05b5c832c9663a65ce400d784093