From f35bd20fbbf8745f2ee9545f562df9ad58555254 Mon Sep 17 00:00:00 2001 From: SleepyDebug <171101568+SleepyDebug@users.noreply.github.com> Date: Mon, 20 Jul 2026 22:38:28 -0400 Subject: [PATCH] Release SleepStrap 6.9.3 --- README.md | 4 +- SleepStrap/Services/MacroAutomationService.cs | 51 +++++++++++ SleepStrap/Services/SkyboxGalleryService.cs | 11 ++- SleepStrap/Services/VisualModService.cs | 90 +++++++++++++++++-- SleepStrap/SleepStrap.csproj | 4 +- .../Elements/Settings/Pages/RivalsPage.xaml | 8 +- .../UI/ViewModels/Settings/RivalsViewModel.cs | 3 +- .../UI/ViewModels/Settings/SkyboxViewModel.cs | 4 +- 8 files changed, 154 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index ea9d305..797371c 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# SleepStrap 6.9.1 +# SleepStrap 6.9.3 ![SleepStrap icon](SleepStrap/SleepStrap.png) SleepStrap is a purple Windows launcher for Roblox with skyboxes, texture presets, visual modes, fonts, and Rivals display tools. -[Download SleepStrap 6.9.1](https://github.com/SleepyDebug/SleepStrap/releases/tag/sleepstrap-6.9.1) · [Discord](https://discord.gg/W3CMjx8C7s) · [Report an issue](https://github.com/SleepyDebug/SleepStrap/issues) +[Download SleepStrap 6.9.3](https://github.com/SleepyDebug/SleepStrap/releases/tag/sleepstrap-6.9.3) · [Discord](https://discord.gg/W3CMjx8C7s) · [Report an issue](https://github.com/SleepyDebug/SleepStrap/issues) ## Repository layout diff --git a/SleepStrap/Services/MacroAutomationService.cs b/SleepStrap/Services/MacroAutomationService.cs index 5c8e581..6f090fd 100644 --- a/SleepStrap/Services/MacroAutomationService.cs +++ b/SleepStrap/Services/MacroAutomationService.cs @@ -15,6 +15,14 @@ public enum MacroWeaponCategory public static class MacroAutomationService { + // The supplied RIVALS recording was captured on a 1920x1080 monitor positioned + // immediately left of the primary display. Convert those desktop coordinates + // into the active Roblox client area so the macro works on any monitor layout. + private const int RecordedScreenLeft = -1920; + private const int RecordedScreenTop = 0; + private const int RecordedScreenWidth = 1920; + private const int RecordedScreenHeight = 1080; + private static readonly object AutomaticActionsLock = new(); private static Process? _automaticActionsProcess; private static string? _automaticActionsScriptPath; @@ -103,6 +111,7 @@ public static async Task RunLoadoutAsync( var selection = selections[index]; MacroPoint point = ResolveSlot(selection.Category, selection.OriginalIndex, selection.MissingIndices); + point = MapRecordedPointToWindow(point, robloxWindow); await MoveAndClickExactPointAsync(point, cancellationToken); await Task.Delay(index < 3 ? 180 : 250, cancellationToken); } @@ -387,6 +396,33 @@ private static async Task MoveAndClickExactPointAsync(MacroPoint point, Cancella } } + private static MacroPoint MapRecordedPointToWindow(MacroPoint recordedPoint, IntPtr window) + { + if (!GetClientRect(window, out RECT clientRect)) + throw new Win32Exception(Marshal.GetLastWin32Error(), "Windows could not read the Roblox window size."); + + POINT clientOrigin = new() { X = clientRect.Left, Y = clientRect.Top }; + if (!ClientToScreen(window, ref clientOrigin)) + throw new Win32Exception(Marshal.GetLastWin32Error(), "Windows could not locate the Roblox window."); + + int clientWidth = clientRect.Right - clientRect.Left; + int clientHeight = clientRect.Bottom - clientRect.Top; + if (clientWidth <= 0 || clientHeight <= 0) + throw new InvalidOperationException("The Roblox window has no usable client area."); + + double relativeX = (recordedPoint.X - RecordedScreenLeft) / (double)RecordedScreenWidth; + double relativeY = (recordedPoint.Y - RecordedScreenTop) / (double)RecordedScreenHeight; + int mappedX = clientOrigin.X + (int)Math.Round(relativeX * clientWidth); + int mappedY = clientOrigin.Y + (int)Math.Round(relativeY * clientHeight); + + mappedX = Math.Clamp(mappedX, clientOrigin.X, clientOrigin.X + clientWidth - 1); + mappedY = Math.Clamp(mappedY, clientOrigin.Y, clientOrigin.Y + clientHeight - 1); + App.Logger.WriteLine( + "MacroAutomationService", + $"Mapped recorded point {recordedPoint.X}, {recordedPoint.Y} to Roblox client point {mappedX}, {mappedY}"); + return new MacroPoint(mappedX, mappedY); + } + private static string? FindAutoHotkeyV2() { string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles); @@ -510,6 +546,15 @@ private struct POINT public int Y; } + [StructLayout(LayoutKind.Sequential)] + private struct RECT + { + public int Left; + public int Top; + public int Right; + public int Bottom; + } + [StructLayout(LayoutKind.Sequential)] private struct INPUT { @@ -568,6 +613,12 @@ private struct MOUSEINPUT [DllImport("user32.dll")] private static extern bool GetCursorPos(out POINT point); + [DllImport("user32.dll", SetLastError = true)] + private static extern bool GetClientRect(IntPtr window, out RECT rectangle); + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool ClientToScreen(IntPtr window, ref POINT point); + [DllImport("user32.dll", SetLastError = true)] private static extern uint SendInput(uint inputCount, INPUT[] inputs, int inputSize); diff --git a/SleepStrap/Services/SkyboxGalleryService.cs b/SleepStrap/Services/SkyboxGalleryService.cs index 25277fb..05be391 100644 --- a/SleepStrap/Services/SkyboxGalleryService.cs +++ b/SleepStrap/Services/SkyboxGalleryService.cs @@ -17,7 +17,11 @@ internal static class SkyboxGalleryService }; public static bool IsPreset(string name) => - PresetNames.Contains(name, StringComparer.OrdinalIgnoreCase); + PresetNames.Contains(name, StringComparer.OrdinalIgnoreCase) || + String.Equals(name, "Zoff", StringComparison.OrdinalIgnoreCase); + + public static string GetResourceFolder(string name) => + String.Equals(name, "Zoff", StringComparison.OrdinalIgnoreCase) ? "Pandora" : name; public static IReadOnlyList GetChoices() { @@ -27,7 +31,10 @@ public static IReadOnlyList GetChoices() }; foreach (string name in PresetNames) - choices.Add(new SkyboxChoice(name, name, LoadPreview(name))); + { + string displayName = String.Equals(name, "Pandora", StringComparison.OrdinalIgnoreCase) ? "Zoff" : name; + choices.Add(new SkyboxChoice(displayName, name, LoadPreview(name))); + } return choices; } diff --git a/SleepStrap/Services/VisualModService.cs b/SleepStrap/Services/VisualModService.cs index f319d28..9aa35a3 100644 --- a/SleepStrap/Services/VisualModService.cs +++ b/SleepStrap/Services/VisualModService.cs @@ -144,7 +144,7 @@ public static void EnsureSelectedSkyboxReady(bool applyRivalsFix = true) if (cacheIsReady) ApplyCachedSkybox(); else - ApplyEmbeddedSkybox(selected, false); + ApplyEmbeddedSkybox(SkyboxGalleryService.GetResourceFolder(selected), false); if (applyRivalsFix) ApplyRivalsSkyboxCompatibilityFix(); @@ -426,16 +426,94 @@ private static IReadOnlyList GetLegacyRtxTexturePaths() private static void ApplyDarkTextures() { - Assembly assembly = Assembly.GetExecutingAssembly(); foreach (var item in GetDarkTextureResources()) { string destination = GetTextureModPath(item.RelativePath); Directory.CreateDirectory(Path.GetDirectoryName(destination)!); - using Stream input = assembly.GetManifestResourceStream(item.ResourceName) - ?? throw new InvalidOperationException($"Missing embedded texture '{item.ResourceName}'."); - using FileStream output = File.Create(destination); - input.CopyTo(output); + byte[] texture = ReadEmbeddedResource(item.ResourceName); + + // The supplied dark diffuse maps contain only their largest mip level. + // Roblox selects smaller mips at lower graphics quality and otherwise + // falls back to its stock-looking material. Build the missing BC1 mip + // chain so the dark pack remains active at every quality level. + if (String.Equals(Path.GetFileName(item.RelativePath), "diffuse.dds", StringComparison.OrdinalIgnoreCase)) + texture = EnsureBc1MipChain(texture); + + File.WriteAllBytes(destination, texture); + } + } + + private static byte[] EnsureBc1MipChain(byte[] texture) + { + const int ddsHeaderSize = 128; + const int ddsMipMapCountFlag = 0x00020000; + const int ddsCapsComplex = 0x00000008; + const int ddsCapsMipMap = 0x00400000; + const int dxt1FourCc = 0x31545844; + + if (texture.Length < ddsHeaderSize || + texture[0] != (byte)'D' || texture[1] != (byte)'D' || + texture[2] != (byte)'S' || texture[3] != (byte)' ' || + BitConverter.ToInt32(texture, 84) != dxt1FourCc) + { + return texture; + } + + int width = BitConverter.ToInt32(texture, 16); + int height = BitConverter.ToInt32(texture, 12); + int existingMipCount = Math.Max(1, BitConverter.ToInt32(texture, 28)); + int expectedMipCount = 1 + (int)Math.Floor(Math.Log2(Math.Max(width, height))); + if (width <= 0 || height <= 0 || existingMipCount >= expectedMipCount) + return texture; + + int topBlockWidth = Math.Max(1, (width + 3) / 4); + int topBlockHeight = Math.Max(1, (height + 3) / 4); + int topLevelBytes = checked(topBlockWidth * topBlockHeight * 8); + if (texture.Length < ddsHeaderSize + topLevelBytes) + return texture; + + using MemoryStream output = new(texture.Length + (topLevelBytes / 3)); + output.Write(texture, 0, ddsHeaderSize + topLevelBytes); + + byte[] parentBlocks = texture.AsSpan(ddsHeaderSize, topLevelBytes).ToArray(); + int parentBlockWidth = topBlockWidth; + int parentBlockHeight = topBlockHeight; + int mipWidth = width; + int mipHeight = height; + + for (int mip = 1; mip < expectedMipCount; mip++) + { + mipWidth = Math.Max(1, mipWidth / 2); + mipHeight = Math.Max(1, mipHeight / 2); + int blockWidth = Math.Max(1, (mipWidth + 3) / 4); + int blockHeight = Math.Max(1, (mipHeight + 3) / 4); + byte[] mipBlocks = new byte[checked(blockWidth * blockHeight * 8)]; + + for (int y = 0; y < blockHeight; y++) + { + for (int x = 0; x < blockWidth; x++) + { + int sourceX = Math.Min(parentBlockWidth - 1, x * 2); + int sourceY = Math.Min(parentBlockHeight - 1, y * 2); + int sourceOffset = (sourceY * parentBlockWidth + sourceX) * 8; + int destinationOffset = (y * blockWidth + x) * 8; + Buffer.BlockCopy(parentBlocks, sourceOffset, mipBlocks, destinationOffset, 8); + } + } + + output.Write(mipBlocks); + parentBlocks = mipBlocks; + parentBlockWidth = blockWidth; + parentBlockHeight = blockHeight; } + + byte[] completeTexture = output.ToArray(); + int flags = BitConverter.ToInt32(completeTexture, 8) | ddsMipMapCountFlag; + int caps = BitConverter.ToInt32(completeTexture, 108) | ddsCapsComplex | ddsCapsMipMap; + BitConverter.GetBytes(flags).CopyTo(completeTexture, 8); + BitConverter.GetBytes(expectedMipCount).CopyTo(completeTexture, 28); + BitConverter.GetBytes(caps).CopyTo(completeTexture, 108); + return completeTexture; } private static void ApplyBasicTextures() diff --git a/SleepStrap/SleepStrap.csproj b/SleepStrap/SleepStrap.csproj index 232c746..3da28b0 100644 --- a/SleepStrap/SleepStrap.csproj +++ b/SleepStrap/SleepStrap.csproj @@ -7,8 +7,8 @@ true True SleepStrap.ico - 6.9.1.0 - 6.9.1.0 + 6.9.3.0 + 6.9.3.0 app.manifest true x64 diff --git a/SleepStrap/UI/Elements/Settings/Pages/RivalsPage.xaml b/SleepStrap/UI/Elements/Settings/Pages/RivalsPage.xaml index 4d527ce..f3427ce 100644 --- a/SleepStrap/UI/Elements/Settings/Pages/RivalsPage.xaml +++ b/SleepStrap/UI/Elements/Settings/Pages/RivalsPage.xaml @@ -32,13 +32,7 @@ - - - - + diff --git a/SleepStrap/UI/ViewModels/Settings/RivalsViewModel.cs b/SleepStrap/UI/ViewModels/Settings/RivalsViewModel.cs index 84c9a0c..de2f51c 100644 --- a/SleepStrap/UI/ViewModels/Settings/RivalsViewModel.cs +++ b/SleepStrap/UI/ViewModels/Settings/RivalsViewModel.cs @@ -9,6 +9,7 @@ public class RivalsViewModel : NotifyPropertyChangedViewModel private const string MissingFlagValue = "__SLEEPSTRAP_FLAG_WAS_MISSING__"; private const string TargetFpsFlag = "DFIntTaskSchedulerTargetFps"; private const string UnlockFpsFlag = "FFlagTaskSchedulerLimitTargetFpsTo2402"; + private const int StretchPercent = 75; public sealed record StretchPreset(string Name, int Percent) { @@ -161,7 +162,7 @@ private void EnableStretch() App.Settings.Prop.RivalsNativeFrequency = native.Frequency; DisplayStretchService.DisplayMode applied = DisplayStretchService.ApplyHorizontalStretch( - App.Settings.Prop.RivalsStretchPercent, + StretchPercent, native); App.Settings.Prop.RivalsStretchEnabled = true; diff --git a/SleepStrap/UI/ViewModels/Settings/SkyboxViewModel.cs b/SleepStrap/UI/ViewModels/Settings/SkyboxViewModel.cs index 468b6e2..1615c8f 100644 --- a/SleepStrap/UI/ViewModels/Settings/SkyboxViewModel.cs +++ b/SleepStrap/UI/ViewModels/Settings/SkyboxViewModel.cs @@ -26,7 +26,9 @@ public SkyboxViewModel() else { _selectedSkybox = SkyboxChoices.FirstOrDefault(x => - !x.IsNone && String.Equals(x.Name, App.Settings.Prop.CustomSkyboxSourceName, StringComparison.OrdinalIgnoreCase)); + !x.IsNone && + (String.Equals(x.Name, App.Settings.Prop.CustomSkyboxSourceName, StringComparison.OrdinalIgnoreCase) || + String.Equals(x.ResourceFolder, App.Settings.Prop.CustomSkyboxSourceName, StringComparison.OrdinalIgnoreCase))); _statusText = _selectedSkybox is null ? "A legacy imported sky is active. Pick a preset or None to replace it." : $"Selected: {_selectedSkybox.Name}.";