Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
51 changes: 51 additions & 0 deletions SleepStrap/Services/MacroAutomationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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);

Expand Down
11 changes: 9 additions & 2 deletions SleepStrap/Services/SkyboxGalleryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SkyboxChoice> GetChoices()
{
Expand All @@ -27,7 +31,10 @@ public static IReadOnlyList<SkyboxChoice> 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;
}
Expand Down
90 changes: 84 additions & 6 deletions SleepStrap/Services/VisualModService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -426,16 +426,94 @@ private static IReadOnlyList<string> 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()
Expand Down
4 changes: 2 additions & 2 deletions SleepStrap/SleepStrap.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
<UseWPF>true</UseWPF>
<UseWindowsForms>True</UseWindowsForms>
<ApplicationIcon>SleepStrap.ico</ApplicationIcon>
<Version>6.9.1.0</Version>
<FileVersion>6.9.1.0</FileVersion>
<Version>6.9.3.0</Version>
<FileVersion>6.9.3.0</FileVersion>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Platforms>x64</Platforms>
Expand Down
8 changes: 1 addition & 7 deletions SleepStrap/UI/Elements/Settings/Pages/RivalsPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,7 @@
</ui:CardExpander.Header>
<Grid>
<Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions>
<StackPanel Margin="0,0,22,0">
<TextBlock Text="Strength" Margin="0,0,0,5" FontSize="12" FontWeight="SemiBold" />
<ComboBox Width="230" HorizontalAlignment="Left"
ItemsSource="{Binding StretchPresets}"
SelectedValuePath="Percent"
SelectedValue="{Binding SelectedStretchPercent, Mode=TwoWay}" />
</StackPanel>
<TextBlock Text="Stretch display" FontSize="14" VerticalAlignment="Center" />
<StackPanel Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Text="Off" HorizontalAlignment="Center" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<ui:ToggleSwitch IsChecked="{Binding StretchEnabled, Mode=TwoWay}" Margin="0,8" />
Expand Down
3 changes: 2 additions & 1 deletion SleepStrap/UI/ViewModels/Settings/RivalsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 3 additions & 1 deletion SleepStrap/UI/ViewModels/Settings/SkyboxViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}.";
Expand Down
Loading