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/4] 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/4] 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/4]
---
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 f92b0dcf55ff2e86b7e7b410ec2e117b002012f9 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 18 Sep 2026 03:16:58 +0000
Subject: [PATCH 4/4] Bump System.Management from 10.0.11 to 10.0.12
---
updated-dependencies:
- dependency-name: System.Management
dependency-version: 10.0.12
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
---
Bloxstrap/Bloxstrap.csproj | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Bloxstrap/Bloxstrap.csproj b/Bloxstrap/Bloxstrap.csproj
index e63bc95..75d335d 100644
--- a/Bloxstrap/Bloxstrap.csproj
+++ b/Bloxstrap/Bloxstrap.csproj
@@ -86,7 +86,7 @@
-
+