Merge branch 'main' into user-pfp-discord-rpc

This commit is contained in:
axell 2024-09-03 07:24:13 +02:00 committed by GitHub
commit 77262dc6e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 712 additions and 155 deletions

View File

@ -17,7 +17,7 @@ namespace Bloxstrap
public const string ProjectName = "Bloxstrap"; public const string ProjectName = "Bloxstrap";
public const string ProjectOwner = "pizzaboxer"; public const string ProjectOwner = "pizzaboxer";
public const string ProjectRepository = "pizzaboxer/bloxstrap"; public const string ProjectRepository = "pizzaboxer/bloxstrap";
public const string ProjectDownloadLink = "https://bloxstrap.pizzaboxer.xyz"; public const string ProjectDownloadLink = "https://bloxstraplabs.com";
public const string ProjectHelpLink = "https://github.com/pizzaboxer/bloxstrap/wiki"; public const string ProjectHelpLink = "https://github.com/pizzaboxer/bloxstrap/wiki";
public const string ProjectSupportLink = "https://github.com/pizzaboxer/bloxstrap/issues/new"; public const string ProjectSupportLink = "https://github.com/pizzaboxer/bloxstrap/issues/new";

View File

@ -209,12 +209,9 @@ namespace Bloxstrap
if (_latestVersionGuid != _versionGuid || !File.Exists(_playerLocation)) if (_latestVersionGuid != _versionGuid || !File.Exists(_playerLocation))
await InstallLatestVersion(); await InstallLatestVersion();
MigrateIntegrations();
if (_installWebView2) if (_installWebView2)
await InstallWebView2(); await InstallWebView2();
App.FastFlags.Save();
await ApplyModifications(); await ApplyModifications();
// TODO: move this to install/upgrade flow // TODO: move this to install/upgrade flow
@ -224,7 +221,6 @@ namespace Bloxstrap
CheckInstall(); CheckInstall();
// at this point we've finished updating our configs // at this point we've finished updating our configs
App.Settings.Save();
App.State.Save(); App.State.Save();
await mutex.ReleaseAsync(); await mutex.ReleaseAsync();
@ -328,18 +324,26 @@ namespace Bloxstrap
return; return;
} }
using var startEvent = new EventWaitHandle(false, EventResetMode.ManualReset, AppData.StartEvent); int gameClientPid;
bool startEventSignalled;
// TODO: figure out why this is causing roblox to block for some users
using (var startEvent = new EventWaitHandle(false, EventResetMode.ManualReset, AppData.StartEvent))
{
startEvent.Reset();
// v2.2.0 - byfron will trip if we keep a process handle open for over a minute, so we're doing this now // v2.2.0 - byfron will trip if we keep a process handle open for over a minute, so we're doing this now
int gameClientPid; using (var process = Process.Start(startInfo)!)
using (var gameClient = Process.Start(startInfo)!)
{ {
gameClientPid = gameClient.Id; gameClientPid = process.Id;
} }
App.Logger.WriteLine(LOG_IDENT, $"Started Roblox (PID {gameClientPid}), waiting for start event"); App.Logger.WriteLine(LOG_IDENT, $"Started Roblox (PID {gameClientPid}), waiting for start event");
if (!startEvent.WaitOne(TimeSpan.FromSeconds(10))) startEventSignalled = startEvent.WaitOne(TimeSpan.FromSeconds(10));
}
if (!startEventSignalled)
{ {
Frontend.ShowPlayerErrorDialog(); Frontend.ShowPlayerErrorDialog();
return; return;
@ -382,8 +386,10 @@ namespace Bloxstrap
if (autoclosePids.Any()) if (autoclosePids.Any())
args += $";{String.Join(',', autoclosePids)}"; args += $";{String.Join(',', autoclosePids)}";
using (var ipl = new InterProcessLock("Watcher")) if (App.Settings.Prop.EnableActivityTracking || autoclosePids.Any())
{ {
using var ipl = new InterProcessLock("Watcher", TimeSpan.FromSeconds(5));
if (ipl.IsAcquired) if (ipl.IsAcquired)
Process.Start(Paths.Process, $"-watcher \"{args}\""); Process.Start(Paths.Process, $"-watcher \"{args}\"");
} }
@ -766,32 +772,6 @@ namespace Bloxstrap
App.Logger.WriteLine(LOG_IDENT, "Finished installing runtime"); App.Logger.WriteLine(LOG_IDENT, "Finished installing runtime");
} }
public static void MigrateIntegrations()
{
// v2.2.0 - remove rbxfpsunlocker
string rbxfpsunlocker = Path.Combine(Paths.Integrations, "rbxfpsunlocker");
if (Directory.Exists(rbxfpsunlocker))
Directory.Delete(rbxfpsunlocker, true);
// v2.3.0 - remove reshade
string injectorLocation = Path.Combine(Paths.Modifications, "dxgi.dll");
string configLocation = Path.Combine(Paths.Modifications, "ReShade.ini");
if (File.Exists(injectorLocation))
{
Frontend.ShowMessageBox(
Strings.Bootstrapper_HyperionUpdateInfo,
MessageBoxImage.Warning
);
File.Delete(injectorLocation);
}
if (File.Exists(configLocation))
File.Delete(configLocation);
}
private async Task ApplyModifications() private async Task ApplyModifications()
{ {
const string LOG_IDENT = "Bootstrapper::ApplyModifications"; const string LOG_IDENT = "Bootstrapper::ApplyModifications";

View File

@ -4,9 +4,9 @@
{ {
public static string ToTranslatedString(this ServerType value) => value switch public static string ToTranslatedString(this ServerType value) => value switch
{ {
ServerType.Public => Resources.Strings.Enums_ServerType_Public, ServerType.Public => Strings.Enums_ServerType_Public,
ServerType.Private => Resources.Strings.Enums_ServerType_Private, ServerType.Private => Strings.Enums_ServerType_Private,
ServerType.Reserved => Resources.Strings.Enums_ServerType_Reserved, ServerType.Reserved => Strings.Enums_ServerType_Reserved,
_ => "?" _ => "?"
}; };
} }

View File

@ -4,6 +4,10 @@ namespace Bloxstrap
{ {
public class FastFlagManager : JsonManager<Dictionary<string, object>> public class FastFlagManager : JsonManager<Dictionary<string, object>>
{ {
public override string ClassName => nameof(FastFlagManager);
public override string LOG_IDENT_CLASS => ClassName;
public override string FileLocation => Path.Combine(Paths.Modifications, "ClientSettings\\ClientAppSettings.json"); public override string FileLocation => Path.Combine(Paths.Modifications, "ClientSettings\\ClientAppSettings.json");
public bool Changed => !OriginalProp.SequenceEqual(Prop); public bool Changed => !OriginalProp.SequenceEqual(Prop);
@ -48,7 +52,7 @@ namespace Bloxstrap
{ "UI.Menu.GraphicsSlider", "FFlagFixGraphicsQuality" }, { "UI.Menu.GraphicsSlider", "FFlagFixGraphicsQuality" },
{ "UI.FullscreenTitlebarDelay", "FIntFullscreenTitleBarTriggerDelayMillis" }, { "UI.FullscreenTitlebarDelay", "FIntFullscreenTitleBarTriggerDelayMillis" },
{ "UI.Menu.Style.DisableV2", "FFlagDisableNewIGMinDUA" }, { "UI.Menu.Style.V2Rollout", "FIntNewInGameMenuPercentRollout3" },
{ "UI.Menu.Style.EnableV4.1", "FFlagEnableInGameMenuControls" }, { "UI.Menu.Style.EnableV4.1", "FFlagEnableInGameMenuControls" },
{ "UI.Menu.Style.EnableV4.2", "FFlagEnableInGameMenuModernization" }, { "UI.Menu.Style.EnableV4.2", "FFlagEnableInGameMenuModernization" },
{ "UI.Menu.Style.EnableV4Chrome", "FFlagEnableInGameMenuChrome" }, { "UI.Menu.Style.EnableV4Chrome", "FFlagEnableInGameMenuChrome" },
@ -99,7 +103,7 @@ namespace Bloxstrap
InGameMenuVersion.Default, InGameMenuVersion.Default,
new Dictionary<string, string?> new Dictionary<string, string?>
{ {
{ "DisableV2", null }, { "V2Rollout", null },
{ "EnableV4", null }, { "EnableV4", null },
{ "EnableV4Chrome", null }, { "EnableV4Chrome", null },
{ "ABTest", null } { "ABTest", null }
@ -110,7 +114,7 @@ namespace Bloxstrap
InGameMenuVersion.V1, InGameMenuVersion.V1,
new Dictionary<string, string?> new Dictionary<string, string?>
{ {
{ "DisableV2", "True" }, { "V2Rollout", "0" },
{ "EnableV4", "False" }, { "EnableV4", "False" },
{ "EnableV4Chrome", "False" }, { "EnableV4Chrome", "False" },
{ "ABTest", "False" } { "ABTest", "False" }
@ -121,7 +125,7 @@ namespace Bloxstrap
InGameMenuVersion.V2, InGameMenuVersion.V2,
new Dictionary<string, string?> new Dictionary<string, string?>
{ {
{ "DisableV2", "False" }, { "V2Rollout", "100" },
{ "EnableV4", "False" }, { "EnableV4", "False" },
{ "EnableV4Chrome", "False" }, { "EnableV4Chrome", "False" },
{ "ABTest", "False" } { "ABTest", "False" }
@ -132,7 +136,7 @@ namespace Bloxstrap
InGameMenuVersion.V4, InGameMenuVersion.V4,
new Dictionary<string, string?> new Dictionary<string, string?>
{ {
{ "DisableV2", "True" }, { "V2Rollout", "0" },
{ "EnableV4", "True" }, { "EnableV4", "True" },
{ "EnableV4Chrome", "False" }, { "EnableV4Chrome", "False" },
{ "ABTest", "False" } { "ABTest", "False" }
@ -143,7 +147,7 @@ namespace Bloxstrap
InGameMenuVersion.V4Chrome, InGameMenuVersion.V4Chrome,
new Dictionary<string, string?> new Dictionary<string, string?>
{ {
{ "DisableV2", "True" }, { "V2Rollout", "0" },
{ "EnableV4", "True" }, { "EnableV4", "True" },
{ "EnableV4Chrome", "True" }, { "EnableV4Chrome", "True" },
{ "ABTest", "False" } { "ABTest", "False" }
@ -238,9 +242,9 @@ namespace Bloxstrap
OriginalProp = new(Prop); OriginalProp = new(Prop);
} }
public override void Load() public override void Load(bool alertFailure = true)
{ {
base.Load(); base.Load(alertFailure);
// clone the dictionary // clone the dictionary
OriginalProp = new(Prop); OriginalProp = new(Prop);
@ -248,10 +252,6 @@ namespace Bloxstrap
// TODO - remove when activity tracking has been revamped // TODO - remove when activity tracking has been revamped
if (GetPreset("Network.Log") != "7") if (GetPreset("Network.Log") != "7")
SetPreset("Network.Log", "7"); SetPreset("Network.Log", "7");
string? val = GetPreset("UI.Menu.Style.EnableV4.1");
if (GetPreset("UI.Menu.Style.EnableV4.2") != val)
SetPreset("UI.Menu.Style.EnableV4.2", val);
} }
} }
} }

View File

@ -11,6 +11,8 @@ namespace Bloxstrap
public string InstallLocation = Path.Combine(Paths.LocalAppData, "Bloxstrap"); public string InstallLocation = Path.Combine(Paths.LocalAppData, "Bloxstrap");
public bool ExistingDataPresent => File.Exists(Path.Combine(InstallLocation, "Settings.json"));
public bool CreateDesktopShortcuts = true; public bool CreateDesktopShortcuts = true;
public bool CreateStartMenuShortcuts = true; public bool CreateStartMenuShortcuts = true;
@ -21,6 +23,10 @@ namespace Bloxstrap
public void DoInstall() public void DoInstall()
{ {
const string LOG_IDENT = "Installer::DoInstall";
App.Logger.WriteLine(LOG_IDENT, "Beginning installation");
// should've been created earlier from the write test anyway // should've been created earlier from the write test anyway
Directory.CreateDirectory(InstallLocation); Directory.CreateDirectory(InstallLocation);
@ -69,9 +75,11 @@ namespace Bloxstrap
Shortcut.Create(Paths.Application, "", StartMenuShortcut); Shortcut.Create(Paths.Application, "", StartMenuShortcut);
// existing configuration persisting from an earlier install // existing configuration persisting from an earlier install
App.Settings.Load(); App.Settings.Load(false);
App.State.Load(); App.State.Load(false);
App.FastFlags.Load(); App.FastFlags.Load(false);
App.Logger.WriteLine(LOG_IDENT, "Installation finished");
} }
private bool ValidateLocation() private bool ValidateLocation()
@ -400,6 +408,41 @@ namespace Bloxstrap
if (existingVer is not null) if (existingVer is not null)
{ {
if (Utilities.CompareVersions(existingVer, "2.2.0") == VersionComparison.LessThan)
{
string path = Path.Combine(Paths.Integrations, "rbxfpsunlocker");
try
{
if (Directory.Exists(path))
Directory.Delete(path, true);
}
catch (Exception ex)
{
App.Logger.WriteException(LOG_IDENT, ex);
}
}
if (Utilities.CompareVersions(existingVer, "2.3.0") == VersionComparison.LessThan)
{
string injectorLocation = Path.Combine(Paths.Modifications, "dxgi.dll");
string configLocation = Path.Combine(Paths.Modifications, "ReShade.ini");
if (File.Exists(injectorLocation))
{
Frontend.ShowMessageBox(
Strings.Bootstrapper_HyperionUpdateInfo,
MessageBoxImage.Warning
);
File.Delete(injectorLocation);
}
if (File.Exists(configLocation))
File.Delete(configLocation);
}
if (Utilities.CompareVersions(existingVer, "2.5.0") == VersionComparison.LessThan) if (Utilities.CompareVersions(existingVer, "2.5.0") == VersionComparison.LessThan)
{ {
App.FastFlags.SetValue("DFFlagDisableDPIScale", null); App.FastFlags.SetValue("DFFlagDisableDPIScale", null);
@ -414,6 +457,13 @@ namespace Bloxstrap
App.FastFlags.SetPreset("UI.Menu.Style.ABTest", false); App.FastFlags.SetPreset("UI.Menu.Style.ABTest", false);
} }
if (Utilities.CompareVersions(existingVer, "2.5.3") == VersionComparison.LessThan)
{
string? val = App.FastFlags.GetPreset("UI.Menu.Style.EnableV4.1");
if (App.FastFlags.GetPreset("UI.Menu.Style.EnableV4.2") != val)
App.FastFlags.SetPreset("UI.Menu.Style.EnableV4.2", val);
}
if (Utilities.CompareVersions(existingVer, "2.6.0") == VersionComparison.LessThan) if (Utilities.CompareVersions(existingVer, "2.6.0") == VersionComparison.LessThan)
{ {
if (App.Settings.Prop.UseDisableAppPatch) if (App.Settings.Prop.UseDisableAppPatch)
@ -435,10 +485,8 @@ namespace Bloxstrap
_ = int.TryParse(App.FastFlags.GetPreset("Rendering.Framerate"), out int x); _ = int.TryParse(App.FastFlags.GetPreset("Rendering.Framerate"), out int x);
if (x == 0) if (x == 0)
{
App.FastFlags.SetPreset("Rendering.Framerate", null); App.FastFlags.SetPreset("Rendering.Framerate", null);
} }
}
if (Utilities.CompareVersions(existingVer, "2.8.0") == VersionComparison.LessThan) if (Utilities.CompareVersions(existingVer, "2.8.0") == VersionComparison.LessThan)
{ {
@ -466,6 +514,18 @@ namespace Bloxstrap
ProtocolHandler.Register("roblox", "Roblox", Paths.Application, "-player \"%1\""); ProtocolHandler.Register("roblox", "Roblox", Paths.Application, "-player \"%1\"");
ProtocolHandler.Register("roblox-player", "Roblox", Paths.Application, "-player \"%1\""); ProtocolHandler.Register("roblox-player", "Roblox", Paths.Application, "-player \"%1\"");
string? oldV2Val = App.FastFlags.GetValue("FFlagDisableNewIGMinDUA");
if (oldV2Val is not null)
{
if (oldV2Val == "True")
App.FastFlags.SetPreset("UI.Menu.Style.V2Rollout", "0");
else
App.FastFlags.SetPreset("UI.Menu.Style.V2Rollout", "100");
App.FastFlags.SetValue("FFlagDisableNewIGMinDUA", null);
}
} }
App.Settings.Save(); App.Settings.Save();

View File

@ -9,6 +9,7 @@ namespace Bloxstrap.Integrations
private const string GameJoiningEntry = "[FLog::Output] ! Joining game"; private const string GameJoiningEntry = "[FLog::Output] ! Joining game";
private const string GameJoiningPrivateServerEntry = "[FLog::GameJoinUtil] GameJoinUtil::joinGamePostPrivateServer"; private const string GameJoiningPrivateServerEntry = "[FLog::GameJoinUtil] GameJoinUtil::joinGamePostPrivateServer";
private const string GameJoiningReservedServerEntry = "[FLog::GameJoinUtil] GameJoinUtil::initiateTeleportToReservedServer"; private const string GameJoiningReservedServerEntry = "[FLog::GameJoinUtil] GameJoinUtil::initiateTeleportToReservedServer";
private const string GameJoiningUniverseEntry = "[FLog::GameJoinLoadTime] Report game_join_loadtime:";
private const string GameJoiningUDMUXEntry = "[FLog::Network] UDMUX Address = "; private const string GameJoiningUDMUXEntry = "[FLog::Network] UDMUX Address = ";
private const string GameJoinedEntry = "[FLog::Network] serverId:"; private const string GameJoinedEntry = "[FLog::Network] serverId:";
private const string GameDisconnectedEntry = "[FLog::Network] Time to disconnect replication data:"; private const string GameDisconnectedEntry = "[FLog::Network] Time to disconnect replication data:";
@ -19,6 +20,7 @@ namespace Bloxstrap.Integrations
private const string GameJoinLoadTimeEntryPattern = ", userid:([0-9]+)"; private const string GameJoinLoadTimeEntryPattern = ", userid:([0-9]+)";
private const string GameJoiningEntryPattern = @"! Joining game '([0-9a-f\-]{36})' place ([0-9]+) at ([0-9\.]+)"; private const string GameJoiningEntryPattern = @"! Joining game '([0-9a-f\-]{36})' place ([0-9]+) at ([0-9\.]+)";
private const string GameJoiningUniversePattern = @"universeid:([0-9]+)";
private const string GameJoiningUDMUXPattern = @"UDMUX Address = ([0-9\.]+), Port = [0-9]+ \| RCC Server Address = ([0-9\.]+), Port = [0-9]+"; private const string GameJoiningUDMUXPattern = @"UDMUX Address = ([0-9\.]+), Port = [0-9]+ \| RCC Server Address = ([0-9\.]+), Port = [0-9]+";
private const string GameJoinedEntryPattern = @"serverId: ([0-9\.]+)\|[0-9]+"; private const string GameJoinedEntryPattern = @"serverId: ([0-9\.]+)\|[0-9]+";
private const string GameMessageEntryPattern = @"\[BloxstrapRPC\] (.*)"; private const string GameMessageEntryPattern = @"\[BloxstrapRPC\] (.*)";
@ -41,8 +43,10 @@ namespace Bloxstrap.Integrations
// these are values to use assuming the player isn't currently in a game // these are values to use assuming the player isn't currently in a game
// hmm... do i move this to a model? // hmm... do i move this to a model?
public DateTime ActivityTimeJoined;
public bool ActivityInGame = false; public bool ActivityInGame = false;
public long ActivityPlaceId = 0; public long ActivityPlaceId = 0;
public long ActivityUniverseId = 0;
public string ActivityJobId = ""; public string ActivityJobId = "";
public string ActivityUserId = ""; public string ActivityUserId = "";
public string ActivityMachineAddress = ""; public string ActivityMachineAddress = "";
@ -51,6 +55,8 @@ namespace Bloxstrap.Integrations
public string ActivityLaunchData = ""; public string ActivityLaunchData = "";
public ServerType ActivityServerType = ServerType.Public; public ServerType ActivityServerType = ServerType.Public;
public List<ActivityHistoryEntry> ActivityHistory = new();
public bool IsDisposed = false; public bool IsDisposed = false;
public async void Start() public async void Start()
@ -92,7 +98,6 @@ namespace Bloxstrap.Integrations
if (logFileInfo.CreationTime.AddSeconds(15) > DateTime.Now) if (logFileInfo.CreationTime.AddSeconds(15) > DateTime.Now)
break; break;
// TODO: report failure after 10 seconds of no log file
App.Logger.WriteLine(LOG_IDENT, $"Could not find recent enough log file, waiting... (newest is {logFileInfo.Name})"); App.Logger.WriteLine(LOG_IDENT, $"Could not find recent enough log file, waiting... (newest is {logFileInfo.Name})");
await Task.Delay(1000); await Task.Delay(1000);
} }
@ -135,6 +140,7 @@ namespace Bloxstrap.Integrations
return deeplink; return deeplink;
} }
// TODO: i need to double check how this handles failed game joins (connection error, invalid permissions, etc)
private void ReadLogEntry(string entry) private void ReadLogEntry(string entry)
{ {
const string LOG_IDENT = "ActivityWatcher::ReadLogEntry"; const string LOG_IDENT = "ActivityWatcher::ReadLogEntry";
@ -168,6 +174,8 @@ namespace Bloxstrap.Integrations
} }
if (!ActivityInGame && ActivityPlaceId == 0) if (!ActivityInGame && ActivityPlaceId == 0)
{ {
// We are not in a game, nor are in the process of joining one
if (entry.Contains(GameJoiningPrivateServerEntry)) if (entry.Contains(GameJoiningPrivateServerEntry))
{ {
// we only expect to be joining a private server if we're not already in a game // we only expect to be joining a private server if we're not already in a game
@ -206,13 +214,28 @@ namespace Bloxstrap.Integrations
} }
else if (!ActivityInGame && ActivityPlaceId != 0) else if (!ActivityInGame && ActivityPlaceId != 0)
{ {
if (entry.Contains(GameJoiningUDMUXEntry)) // We are not confirmed to be in a game, but we are in the process of joining one
if (entry.Contains(GameJoiningUniverseEntry))
{
var match = Regex.Match(entry, GameJoiningUniversePattern);
if (match.Groups.Count != 2)
{
App.Logger.WriteLine(LOG_IDENT, "Failed to assert format for game join universe entry");
App.Logger.WriteLine(LOG_IDENT, entry);
return;
}
ActivityUniverseId = long.Parse(match.Groups[1].Value);
}
else if (entry.Contains(GameJoiningUDMUXEntry))
{ {
Match match = Regex.Match(entry, GameJoiningUDMUXPattern); Match match = Regex.Match(entry, GameJoiningUDMUXPattern);
if (match.Groups.Count != 3 || match.Groups[2].Value != ActivityMachineAddress) if (match.Groups.Count != 3 || match.Groups[2].Value != ActivityMachineAddress)
{ {
App.Logger.WriteLine(LOG_IDENT, $"Failed to assert format for game join UDMUX entry"); App.Logger.WriteLine(LOG_IDENT, "Failed to assert format for game join UDMUX entry");
App.Logger.WriteLine(LOG_IDENT, entry); App.Logger.WriteLine(LOG_IDENT, entry);
return; return;
} }
@ -236,17 +259,35 @@ namespace Bloxstrap.Integrations
App.Logger.WriteLine(LOG_IDENT, $"Joined Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); App.Logger.WriteLine(LOG_IDENT, $"Joined Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})");
ActivityInGame = true; ActivityInGame = true;
ActivityTimeJoined = DateTime.Now;
OnGameJoin?.Invoke(this, new EventArgs()); OnGameJoin?.Invoke(this, new EventArgs());
} }
} }
else if (ActivityInGame && ActivityPlaceId != 0) else if (ActivityInGame && ActivityPlaceId != 0)
{ {
// We are confirmed to be in a game
if (entry.Contains(GameDisconnectedEntry)) if (entry.Contains(GameDisconnectedEntry))
{ {
App.Logger.WriteLine(LOG_IDENT, $"Disconnected from Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})"); App.Logger.WriteLine(LOG_IDENT, $"Disconnected from Game ({ActivityPlaceId}/{ActivityJobId}/{ActivityMachineAddress})");
// TODO: should this be including launchdata?
if (ActivityServerType != ServerType.Reserved)
{
ActivityHistory.Insert(0, new ActivityHistoryEntry
{
PlaceId = ActivityPlaceId,
UniverseId = ActivityUniverseId,
JobId = ActivityJobId,
TimeJoined = ActivityTimeJoined,
TimeLeft = DateTime.Now
});
}
ActivityInGame = false; ActivityInGame = false;
ActivityPlaceId = 0; ActivityPlaceId = 0;
ActivityUniverseId = 0;
ActivityJobId = ""; ActivityJobId = "";
ActivityMachineAddress = ""; ActivityMachineAddress = "";
ActivityMachineUDMUX = false; ActivityMachineUDMUX = false;

View File

@ -207,15 +207,8 @@ namespace Bloxstrap.Integrations
// TODO: move this to its own function under the activity watcher? // TODO: move this to its own function under the activity watcher?
// TODO: show error if information cannot be queried instead of silently failing // TODO: show error if information cannot be queried instead of silently failing
var universeIdResponse = await Http.GetJson<UniverseIdResponse>($"https://apis.roblox.com/universes/v1/places/{placeId}/universe");
if (universeIdResponse is null)
{
App.Logger.WriteLine(LOG_IDENT, "Could not get Universe ID!");
return false;
}
long universeId = universeIdResponse.UniverseId; long universeId = _activityWatcher.ActivityUniverseId;
App.Logger.WriteLine(LOG_IDENT, $"Got Universe ID as {universeId}");
// preserve time spent playing if we're teleporting between places in the same universe // preserve time spent playing if we're teleporting between places in the same universe
if (_timeStartedUniverse is null || !_activityWatcher.ActivityIsTeleport || universeId != _currentUniverseId) if (_timeStartedUniverse is null || !_activityWatcher.ActivityIsTeleport || universeId != _currentUniverseId)
@ -277,7 +270,7 @@ namespace Bloxstrap.Integrations
buttons.Add(new Button buttons.Add(new Button
{ {
Label = "Join server", Label = "Join server",
Url = $"roblox://experiences/start?placeId={placeId}&gameInstanceId={_activityWatcher.ActivityJobId}" Url = _activityWatcher.GetActivityDeeplink()
}); });
} }

View File

@ -8,11 +8,13 @@ namespace Bloxstrap
public T Prop { get; set; } = new(); public T Prop { get; set; } = new();
public virtual string FileLocation => Path.Combine(Paths.Base, $"{typeof(T).Name}.json"); public virtual string ClassName => typeof(T).Name;
private string LOG_IDENT_CLASS => $"JsonManager<{typeof(T).Name}>"; public virtual string FileLocation => Path.Combine(Paths.Base, $"{ClassName}.json");
public virtual void Load() public virtual string LOG_IDENT_CLASS => $"JsonManager<{ClassName}>";
public virtual void Load(bool alertFailure = true)
{ {
string LOG_IDENT = $"{LOG_IDENT_CLASS}::Load"; string LOG_IDENT = $"{LOG_IDENT_CLASS}::Load";
@ -32,7 +34,22 @@ namespace Bloxstrap
catch (Exception ex) catch (Exception ex)
{ {
App.Logger.WriteLine(LOG_IDENT, "Failed to load!"); App.Logger.WriteLine(LOG_IDENT, "Failed to load!");
App.Logger.WriteLine(LOG_IDENT, $"{ex.Message}"); App.Logger.WriteException(LOG_IDENT, ex);
if (alertFailure)
{
string message = "";
if (ClassName == nameof(Settings))
message = Strings.JsonManager_SettingsLoadFailed;
else if (ClassName == nameof(FastFlagManager))
message = Strings.JsonManager_FastFlagsLoadFailed;
if (!String.IsNullOrEmpty(message))
Frontend.ShowMessageBox($"{message}\n\n{ex.GetType()}: {ex.Message}", System.Windows.MessageBoxImage.Warning);
}
Save();
} }
} }

View File

@ -215,7 +215,7 @@ namespace Bloxstrap
App.Logger.WriteLine(LOG_IDENT, "An exception occurred when running the bootstrapper"); App.Logger.WriteLine(LOG_IDENT, "An exception occurred when running the bootstrapper");
if (t.Exception is not null) if (t.Exception is not null)
App.FinalizeExceptionHandling(t.Exception, false); App.FinalizeExceptionHandling(t.Exception);
} }
App.Terminate(); App.Terminate();

View File

@ -16,8 +16,7 @@
{ {
const string LOG_IDENT = "Logger::Initialize"; const string LOG_IDENT = "Logger::Initialize";
// TODO: <Temp>/Bloxstrap/Logs/ string directory = useTempDir ? Path.Combine(Paths.TempLogs) : Path.Combine(Paths.Base, "Logs");
string directory = useTempDir ? Path.Combine(Paths.LocalAppData, "Temp") : Path.Combine(Paths.Base, "Logs");
string timestamp = DateTime.UtcNow.ToString("yyyyMMdd'T'HHmmss'Z'"); string timestamp = DateTime.UtcNow.ToString("yyyyMMdd'T'HHmmss'Z'");
string filename = $"{App.ProjectName}_{timestamp}.log"; string filename = $"{App.ProjectName}_{timestamp}.log";
string location = Path.Combine(directory, filename); string location = Path.Combine(directory, filename);

View File

@ -0,0 +1,38 @@
using CommunityToolkit.Mvvm.Input;
using System.Windows.Input;
namespace Bloxstrap.Models
{
public class ActivityHistoryEntry
{
public long UniverseId { get; set; }
public long PlaceId { get; set; }
public string JobId { get; set; } = String.Empty;
public DateTime TimeJoined { get; set; }
public DateTime TimeLeft { get; set; }
public string TimeJoinedFriendly => String.Format("{0} - {1}", TimeJoined.ToString("h:mm tt"), TimeLeft.ToString("h:mm tt"));
public bool DetailsLoaded = false;
public string GameName { get; set; } = String.Empty;
public string GameThumbnail { get; set; } = String.Empty;
public ICommand RejoinServerCommand => new RelayCommand(RejoinServer);
private void RejoinServer()
{
string playerPath = Path.Combine(Paths.Versions, App.State.Prop.PlayerVersionGuid, "RobloxPlayerBeta.exe");
string deeplink = $"roblox://experiences/start?placeId={PlaceId}&gameInstanceId={JobId}";
// start RobloxPlayerBeta.exe directly since Roblox can reuse the existing window
// ideally, i'd like to find out how roblox is doing it
Process.Start(playerPath, deeplink);
}
}
}

View File

@ -0,0 +1,13 @@
namespace Bloxstrap.Models
{
public class Supporter
{
[JsonPropertyName("imageAsset")]
public string ImageAsset { get; set; } = null!;
[JsonPropertyName("name")]
public string Name { get; set; } = null!;
public string Image => $"https://raw.githubusercontent.com/bloxstraplabs/config/main/assets/{ImageAsset}";
}
}

View File

@ -0,0 +1,11 @@
namespace Bloxstrap.Models
{
public class SupporterData
{
[JsonPropertyName("columns")]
public int Columns { get; set; }
[JsonPropertyName("supporters")]
public List<Supporter> Supporters { get; set; } = null!;
}
}

View File

@ -69,6 +69,24 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to These are the people currently supporting Bloxstrap through [Ko-fi]({0}). A massive thank you to everyone here!.
/// </summary>
public static string About_Supporters_Description {
get {
return ResourceManager.GetString("About.Supporters.Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Supporters.
/// </summary>
public static string About_Supporters_Title {
get {
return ResourceManager.GetString("About.Supporters.Title", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to About Bloxstrap. /// Looks up a localized string similar to About Bloxstrap.
/// </summary> /// </summary>
@ -449,6 +467,15 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Loading, please wait....
/// </summary>
public static string Common_Loading {
get {
return ResourceManager.GetString("Common.Loading", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Miscellaneous. /// Looks up a localized string similar to Miscellaneous.
/// </summary> /// </summary>
@ -485,6 +512,15 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Could not load data because of a network error..
/// </summary>
public static string Common_NetworkError {
get {
return ResourceManager.GetString("Common.NetworkError", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to New. /// Looks up a localized string similar to New.
/// </summary> /// </summary>
@ -629,6 +665,24 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Rejoin.
/// </summary>
public static string ContextMenu_GameHistory_Rejoin {
get {
return ResourceManager.GetString("ContextMenu.GameHistory.Rejoin", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Game history.
/// </summary>
public static string ContextMenu_GameHistory_Title {
get {
return ResourceManager.GetString("ContextMenu.GameHistory.Title", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Roblox is still launching. A log file will only be available once Roblox launches.. /// Looks up a localized string similar to Roblox is still launching. A log file will only be available once Roblox launches..
/// </summary> /// </summary>
@ -638,15 +692,6 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to See server details.
/// </summary>
public static string ContextMenu_SeeServerDetails {
get {
return ResourceManager.GetString("ContextMenu.SeeServerDetails", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Copy Instance ID. /// Looks up a localized string similar to Copy Instance ID.
/// </summary> /// </summary>
@ -1422,7 +1467,7 @@ namespace Bloxstrap.Resources {
/// <summary> /// <summary>
/// Looks up a localized string similar to Thank you for downloading Bloxstrap. /// Looks up a localized string similar to Thank you for downloading Bloxstrap.
/// ///
///You should have gotten it from either {0} or {1}. Those are the only official websites to get it from. ///You should have downloaded it from either {0} or {1}. Those are the only official websites to get it from. It is your responsibility to ensure you download from an official source.
/// ///
///This installation process will be quick and simple, and you will be able to configure any of Bloxstrap&apos;s settings after installation.. ///This installation process will be quick and simple, and you will be able to configure any of Bloxstrap&apos;s settings after installation..
/// </summary> /// </summary>
@ -1459,6 +1504,24 @@ namespace Bloxstrap.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Your Fast Flags could not be loaded. They have been reset to the default configuration..
/// </summary>
public static string JsonManager_FastFlagsLoadFailed {
get {
return ResourceManager.GetString("JsonManager.FastFlagsLoadFailed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Your Settings could not be loaded. They have been reset to the default configuration..
/// </summary>
public static string JsonManager_SettingsLoadFailed {
get {
return ResourceManager.GetString("JsonManager.SettingsLoadFailed", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Configure settings. /// Looks up a localized string similar to Configure settings.
/// </summary> /// </summary>

View File

@ -268,9 +268,6 @@ Your ReShade configuration files will still be saved, and you can locate them by
<data name="ContextMenu.CopyDeeplinkInvite" xml:space="preserve"> <data name="ContextMenu.CopyDeeplinkInvite" xml:space="preserve">
<value>Copy invite deeplink</value> <value>Copy invite deeplink</value>
</data> </data>
<data name="ContextMenu.SeeServerDetails" xml:space="preserve">
<value>See server details</value>
</data>
<data name="ContextMenu.ServerInformation.CopyInstanceId" xml:space="preserve"> <data name="ContextMenu.ServerInformation.CopyInstanceId" xml:space="preserve">
<value>Copy Instance ID</value> <value>Copy Instance ID</value>
</data> </data>
@ -999,7 +996,7 @@ Selecting 'No' will ignore this warning and continue installation.</value>
<data name="Installer.Welcome.MainText" xml:space="preserve"> <data name="Installer.Welcome.MainText" xml:space="preserve">
<value>Thank you for downloading Bloxstrap. <value>Thank you for downloading Bloxstrap.
You should have gotten it from either {0} or {1}. Those are the only official websites to get it from. You should have downloaded it from either {0} or {1}. Those are the only official websites to get it from. It is your responsibility to ensure you download from an official source.
This installation process will be quick and simple, and you will be able to configure any of Bloxstrap's settings after installation.</value> This installation process will be quick and simple, and you will be able to configure any of Bloxstrap's settings after installation.</value>
</data> </data>
@ -1156,4 +1153,28 @@ Are you sure you want to continue?</value>
<data name="Dialog.PlayerError.HelpInformation" xml:space="preserve"> <data name="Dialog.PlayerError.HelpInformation" xml:space="preserve">
<value>Please read the following help information, which will open in your web browser when you close this dialog.</value> <value>Please read the following help information, which will open in your web browser when you close this dialog.</value>
</data> </data>
<data name="Common.NetworkError" xml:space="preserve">
<value>Could not load data because of a network error.</value>
</data>
<data name="Common.Loading" xml:space="preserve">
<value>Loading, please wait...</value>
</data>
<data name="About.Supporters.Title" xml:space="preserve">
<value>Supporters</value>
</data>
<data name="About.Supporters.Description" xml:space="preserve">
<value>These are the people currently supporting Bloxstrap through [Ko-fi]({0}). A massive thank you to everyone here!</value>
</data>
<data name="JsonManager.SettingsLoadFailed" xml:space="preserve">
<value>Your Settings could not be loaded. They have been reset to the default configuration.</value>
</data>
<data name="JsonManager.FastFlagsLoadFailed" xml:space="preserve">
<value>Your Fast Flags could not be loaded. They have been reset to the default configuration.</value>
</data>
<data name="ContextMenu.GameHistory.Title" xml:space="preserve">
<value>Game history</value>
</data>
<data name="ContextMenu.GameHistory.Rejoin" xml:space="preserve">
<value>Rejoin</value>
</data>
</root> </root>

View File

@ -32,11 +32,11 @@ namespace Bloxstrap.UI.Converters
return attribute.StaticName; return attribute.StaticName;
if (attribute.FromTranslation is not null) if (attribute.FromTranslation is not null)
return Resources.Strings.ResourceManager.GetStringSafe(attribute.FromTranslation); return Strings.ResourceManager.GetStringSafe(attribute.FromTranslation);
} }
} }
return Resources.Strings.ResourceManager.GetStringSafe(String.Format( return Strings.ResourceManager.GetStringSafe(String.Format(
"{0}.{1}", "{0}.{1}",
typeName.Substring(typeName.IndexOf('.', StringComparison.Ordinal) + 1), typeName.Substring(typeName.IndexOf('.', StringComparison.Ordinal) + 1),
stringVal stringVal

View File

@ -3,7 +3,9 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:enums="clr-namespace:Bloxstrap.Enums"
xmlns:models="clr-namespace:Bloxstrap.UI.ViewModels" xmlns:models="clr-namespace:Bloxstrap.UI.ViewModels"
xmlns:dmodels="clr-namespace:Bloxstrap.UI.ViewModels.About"
xmlns:controls="clr-namespace:Bloxstrap.UI.Elements.Controls" xmlns:controls="clr-namespace:Bloxstrap.UI.Elements.Controls"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:resources="clr-namespace:Bloxstrap.Resources" xmlns:resources="clr-namespace:Bloxstrap.Resources"
@ -11,7 +13,13 @@
d:DesignHeight="1500" d:DesignWidth="800" d:DesignHeight="1500" d:DesignWidth="800"
Title="AboutPage" Title="AboutPage"
Scrollable="True"> Scrollable="True">
<!--d:DataContext="{d:DesignInstance dmodels:AboutViewModel, IsDesignTimeCreatable=True}"-->
<StackPanel Margin="0,0,14,14"> <StackPanel Margin="0,0,14,14">
<StackPanel.Resources>
<FrameworkElement x:Key="ProxyElement" DataContext="{Binding}"/>
</StackPanel.Resources>
<Grid Margin="0,0,0,24" HorizontalAlignment="Center"> <Grid Margin="0,0,0,24" HorizontalAlignment="Center">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
@ -73,6 +81,85 @@
</Grid> </Grid>
</StackPanel> </StackPanel>
<TextBlock Text="{x:Static resources:Strings.About_Supporters_Title}" FontWeight="Medium" FontSize="20" Margin="0,16,0,0" />
<controls:MarkdownTextBlock MarkdownText="{Binding Source={x:Static resources:Strings.About_Supporters_Description}, Converter={StaticResource StringFormatConverter}, ConverterParameter='https://ko-fi.com/boxerpizza'}" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
<Grid Margin="0,8,0,0">
<Grid.Style>
<Style TargetType="Grid">
<Style.Triggers>
<DataTrigger Binding="{Binding SupportersLoadedState, Mode=OneWay}" Value="{x:Static enums:GenericTriState.Unknown}">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
<Setter Property="Visibility" Value="Collapsed" />
</Style>
</Grid.Style>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:ProgressRing Grid.Column="0" IsIndeterminate="True" />
<TextBlock Grid.Column="1" Margin="16,0,0,0" Text="{x:Static resources:Strings.Common_Loading}" VerticalAlignment="Center" />
</Grid>
<Grid Margin="0,8,0,0">
<Grid.Style>
<Style TargetType="Grid">
<Style.Triggers>
<DataTrigger Binding="{Binding SupportersLoadedState, Mode=OneWay}" Value="{x:Static enums:GenericTriState.Failed}">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
<Setter Property="Visibility" Value="Collapsed" />
</Style>
</Grid.Style>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Source="pack://application:,,,/Resources/MessageBox/Error.png" Width="60" Height="60" RenderOptions.BitmapScalingMode="HighQuality" />
<StackPanel Grid.Column="1" Margin="16,0,0,0" VerticalAlignment="Center">
<TextBlock Text="{x:Static resources:Strings.Common_NetworkError}" />
<TextBlock Text="{Binding SupportersLoadError, Mode=OneWay}" />
</StackPanel>
</Grid>
<ListView ItemsSource="{Binding Supporters, Mode=OneWay}" Margin="0,8,0,0" ScrollViewer.CanContentScroll="False" IsEnabled="False">
<ListView.Style>
<Style TargetType="ListView" BasedOn="{StaticResource {x:Type ListView}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SupportersLoadedState, Mode=OneWay}" Value="{x:Static enums:GenericTriState.Successful}">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
<Setter Property="Visibility" Value="Collapsed" />
</Style>
</ListView.Style>
<ListView.ItemTemplate>
<DataTemplate>
<ui:Card Padding="8">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Ellipse Grid.Column="0" Height="32" Width="32" VerticalAlignment="Center">
<Ellipse.Fill>
<ImageBrush ImageSource="{Binding Image, IsAsync=True}" />
</Ellipse.Fill>
</Ellipse>
<TextBlock Grid.Column="1" Margin="8,0,2,0" VerticalAlignment="Center" Text="{Binding Name}" />
</Grid>
</ui:Card>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="{Binding SupporterColumns}" Margin="-4" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
</ListView>
<TextBlock Text="{x:Static resources:Strings.Menu_About_Contributors}" FontWeight="Medium" FontSize="20" Margin="0,16,0,0" /> <TextBlock Text="{x:Static resources:Strings.Menu_About_Contributors}" FontWeight="Medium" FontSize="20" Margin="0,16,0,0" />
<TextBlock Text="{x:Static resources:Strings.Menu_About_Contributors_Description}" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorTertiaryBrush}" /> <TextBlock Text="{x:Static resources:Strings.Menu_About_Contributors_Description}" TextWrapping="Wrap" Foreground="{DynamicResource TextFillColorTertiaryBrush}" />
<Grid Margin="0,8,0,0"> <Grid Margin="0,8,0,0">

View File

@ -1,4 +1,4 @@
using Bloxstrap.UI.ViewModels.Settings; using Bloxstrap.UI.ViewModels.About;
namespace Bloxstrap.UI.Elements.About.Pages namespace Bloxstrap.UI.Elements.About.Pages
{ {

View File

@ -43,7 +43,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
{ {
InitializeComponent(); InitializeComponent();
this.buttonCancel.Text = Resources.Strings.Common_Cancel; this.buttonCancel.Text = Strings.Common_Cancel;
ScaleWindow(); ScaleWindow();
SetupDialog(); SetupDialog();

View File

@ -43,7 +43,7 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
InitializeComponent(); InitializeComponent();
this.IconBox.BackgroundImage = App.Settings.Prop.BootstrapperIcon.GetIcon().ToBitmap(); this.IconBox.BackgroundImage = App.Settings.Prop.BootstrapperIcon.GetIcon().ToBitmap();
this.buttonCancel.Text = Resources.Strings.Common_Cancel; this.buttonCancel.Text = Strings.Common_Cancel;
ScaleWindow(); ScaleWindow();
SetupDialog(); SetupDialog();

View File

@ -52,8 +52,8 @@ namespace Bloxstrap.UI.Elements.Bootstrapper
this.BackColor = Color.FromArgb(25, 27, 29); this.BackColor = Color.FromArgb(25, 27, 29);
} }
this.labelMessage.Text = Resources.Strings.Bootstrapper_StylePreview_TextCancel; this.labelMessage.Text = Strings.Bootstrapper_StylePreview_TextCancel;
this.buttonCancel.Text = Resources.Strings.Common_Cancel; this.buttonCancel.Text = Strings.Common_Cancel;
this.IconBox.BackgroundImage = App.Settings.Prop.BootstrapperIcon.GetIcon().GetSized(128, 128).ToBitmap(); this.IconBox.BackgroundImage = App.Settings.Prop.BootstrapperIcon.GetIcon().GetSized(128, 128).ToBitmap();
SetupDialog(); SetupDialog();

View File

@ -57,7 +57,19 @@
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<ui:SymbolIcon Grid.Column="0" Symbol="Info28"/> <ui:SymbolIcon Grid.Column="0" Symbol="Info28"/>
<TextBlock Grid.Column="1" VerticalAlignment="Center" Margin="4,0,0,0" Text="{x:Static resources:Strings.ContextMenu_SeeServerDetails}" /> <TextBlock Grid.Column="1" VerticalAlignment="Center" Margin="4,0,0,0" Text="{x:Static resources:Strings.ContextMenu_ServerInformation_Title}" />
</Grid>
</MenuItem.Header>
</MenuItem>
<MenuItem x:Name="JoinLastServerMenuItem" Visibility="Collapsed" Click="JoinLastServerMenuItem_Click">
<MenuItem.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="24" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ui:SymbolIcon Grid.Column="0" Symbol="History24"/>
<TextBlock Grid.Column="1" VerticalAlignment="Center" Margin="4,0,0,0" Text="{x:Static resources:Strings.ContextMenu_GameHistory_Title}" />
</Grid> </Grid>
</MenuItem.Header> </MenuItem.Header>
</MenuItem> </MenuItem>

View File

@ -80,6 +80,7 @@ namespace Bloxstrap.UI.Elements.ContextMenu
public void ActivityWatcher_OnGameLeave(object? sender, EventArgs e) public void ActivityWatcher_OnGameLeave(object? sender, EventArgs e)
{ {
Dispatcher.Invoke(() => { Dispatcher.Invoke(() => {
JoinLastServerMenuItem.Visibility = Visibility.Visible;
InviteDeeplinkMenuItem.Visibility = Visibility.Collapsed; InviteDeeplinkMenuItem.Visibility = Visibility.Collapsed;
ServerDetailsMenuItem.Visibility = Visibility.Collapsed; ServerDetailsMenuItem.Visibility = Visibility.Collapsed;
@ -129,5 +130,13 @@ namespace Bloxstrap.UI.Elements.ContextMenu
_watcher.KillRobloxProcess(); _watcher.KillRobloxProcess();
} }
private void JoinLastServerMenuItem_Click(object sender, RoutedEventArgs e)
{
if (_activityWatcher is null)
throw new ArgumentNullException(nameof(_activityWatcher));
new ServerHistory(_activityWatcher).ShowDialog();
}
} }
} }

View File

@ -0,0 +1,89 @@
<base:WpfUiWindow x:Class="Bloxstrap.UI.Elements.ContextMenu.ServerHistory"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Bloxstrap.UI.Elements.ContextMenu"
xmlns:base="clr-namespace:Bloxstrap.UI.Elements.Base"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:models="clr-namespace:Bloxstrap.UI.ViewModels.ContextMenu"
xmlns:resources="clr-namespace:Bloxstrap.Resources"
d:DataContext="{d:DesignInstance Type=models:ServerHistoryViewModel}"
mc:Ignorable="d"
Title="{x:Static resources:Strings.ContextMenu_GameHistory_Title}"
MinWidth="420"
MinHeight="420"
Width="580"
Height="420"
Background="{ui:ThemeResource ApplicationBackgroundBrush}"
ExtendsContentIntoTitleBar="True"
WindowStartupLocation="CenterScreen">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ui:TitleBar Grid.Row="0" Grid.ColumnSpan="2" Padding="8" x:Name="RootTitleBar" Title="{x:Static resources:Strings.ContextMenu_GameHistory_Title}" ShowMinimize="False" ShowMaximize="False" CanMaximize="False" KeyboardNavigation.TabNavigation="None" Icon="pack://application:,,,/Bloxstrap.ico" />
<Border Grid.Row="1">
<Border.Style>
<Style TargetType="Border">
<Style.Triggers>
<DataTrigger Binding="{Binding ActivityHistory, Mode=OneWay}" Value="{x:Null}">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
<Setter Property="Visibility" Value="Collapsed" />
</Style>
</Border.Style>
<ui:ProgressRing Grid.Row="1" IsIndeterminate="True" />
</Border>
<ListView Grid.Row="1" ItemsSource="{Binding ActivityHistory, Mode=OneWay}" Margin="8">
<ListView.Style>
<Style TargetType="ListView" BasedOn="{StaticResource {x:Type ListView}}">
<Style.Triggers>
<DataTrigger Binding="{Binding ActivityHistory, Mode=OneWay}" Value="{x:Null}">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
<Setter Property="Visibility" Value="Visible" />
</Style>
</ListView.Style>
<ListView.ItemTemplate>
<DataTemplate>
<ui:Card Padding="12">
<Grid VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Width="84" Height="84" CornerRadius="4">
<Border.Background>
<ImageBrush ImageSource="{Binding GameThumbnail, IsAsync=True}" />
</Border.Background>
</Border>
<StackPanel Grid.Column="1" Margin="16,0,0,0" VerticalAlignment="Center">
<TextBlock Text="{Binding GameName}" FontSize="18" FontWeight="Medium" />
<TextBlock Text="{Binding TimeJoinedFriendly}" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<ui:Button Margin="0,8,0,0" Content="{x:Static resources:Strings.ContextMenu_GameHistory_Rejoin}" Command="{Binding RejoinServerCommand}" Appearance="Success" Icon="Play28" IconFilled="True" />
</StackPanel>
</Grid>
</ui:Card>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Border Grid.Row="2" Padding="15" Background="{ui:ThemeResource SolidBackgroundFillColorSecondaryBrush}">
<StackPanel Orientation="Horizontal" FlowDirection="LeftToRight" HorizontalAlignment="Right">
<Button Margin="12,0,0,0" MinWidth="100" Content="{x:Static resources:Strings.Common_Close}" IsCancel="True" />
</StackPanel>
</Border>
</Grid>
</base:WpfUiWindow>

View File

@ -0,0 +1,21 @@
using Bloxstrap.Integrations;
using Bloxstrap.UI.ViewModels.ContextMenu;
namespace Bloxstrap.UI.Elements.ContextMenu
{
/// <summary>
/// Interaction logic for ServerInformation.xaml
/// </summary>
public partial class ServerHistory
{
public ServerHistory(ActivityWatcher watcher)
{
var viewModel = new ServerHistoryViewModel(watcher);
viewModel.RequestCloseEvent += (_, _) => Close();
DataContext = viewModel;
InitializeComponent();
}
}
}

View File

@ -121,13 +121,13 @@ namespace Bloxstrap.UI.Elements.Dialogs
switch (result) switch (result)
{ {
case MessageBoxResult.OK: case MessageBoxResult.OK:
return Bloxstrap.Resources.Strings.Common_OK; return Strings.Common_OK;
case MessageBoxResult.Cancel: case MessageBoxResult.Cancel:
return Bloxstrap.Resources.Strings.Common_Cancel; return Strings.Common_Cancel;
case MessageBoxResult.Yes: case MessageBoxResult.Yes:
return Bloxstrap.Resources.Strings.Common_Yes; return Strings.Common_Yes;
case MessageBoxResult.No: case MessageBoxResult.No:
return Bloxstrap.Resources.Strings.Common_No; return Strings.Common_No;
default: default:
Debug.Assert(false); Debug.Assert(false);
return result.ToString(); return result.ToString();

View File

@ -24,7 +24,7 @@
<RowDefinition Height="*" /> <RowDefinition Height="*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<ui:TitleBar Grid.Row="0" Grid.ColumnSpan="2" Padding="8" ShowMinimize="False" ShowMaximize="False" CanMaximize="False" KeyboardNavigation.TabNavigation="None" /> <ui:TitleBar Grid.Row="0" Grid.ColumnSpan="2" Padding="8" ShowMinimize="False" ShowMaximize="False" Title="Bloxstrap" Icon="pack://application:,,,/Bloxstrap.ico" CanMaximize="False" KeyboardNavigation.TabNavigation="None" />
<Grid Grid.Row="1"> <Grid Grid.Row="1">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
@ -53,7 +53,7 @@
<StackPanel Grid.Row="1" HorizontalAlignment="Center"> <StackPanel Grid.Row="1" HorizontalAlignment="Center">
<ui:Hyperlink Icon="QuestionCircle48" Content="About Bloxstrap" Margin="0,0,0,8" Command="{Binding LaunchAboutCommand, Mode=OneTime}" /> <ui:Hyperlink Icon="QuestionCircle48" Content="About Bloxstrap" Margin="0,0,0,8" Command="{Binding LaunchAboutCommand, Mode=OneTime}" />
<ui:Hyperlink Icon="WindowNew16" Content="Support us on Ko-fi!" NavigateUri="https://ko-fi.com/boxerpizza" /> <ui:Hyperlink Icon="Heart48" Content="Support us on Ko-fi!" NavigateUri="https://ko-fi.com/boxerpizza" />
</StackPanel> </StackPanel>
</Grid> </Grid>

View File

@ -307,12 +307,10 @@ namespace Bloxstrap.UI.Elements.Settings.Pages
private void DataGrid_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e) private void DataGrid_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e)
{ {
int index = e.Row.GetIndex(); if (e.Row.DataContext is not FastFlag entry)
FastFlag entry = _fastFlagList[index]; return;
var textbox = e.EditingElement as TextBox; if (e.EditingElement is not TextBox textbox)
if (textbox is null)
return; return;
switch (e.Column.Header) switch (e.Column.Header)

View File

@ -86,7 +86,7 @@
Header="{x:Static resources:Strings.Menu_FastFlags_Presets_FPSLimit_Title}" Header="{x:Static resources:Strings.Menu_FastFlags_Presets_FPSLimit_Title}"
Description="{x:Static resources:Strings.Menu_FastFlags_Presets_FPSLimit_Description}" Description="{x:Static resources:Strings.Menu_FastFlags_Presets_FPSLimit_Description}"
HelpLink="https://github.com/pizzaboxer/bloxstrap/wiki/A-guide-to-FastFlags#framerate-limit"> HelpLink="https://github.com/pizzaboxer/bloxstrap/wiki/A-guide-to-FastFlags#framerate-limit">
<ui:TextBox Margin="5,0,0,0" Padding="10,5,10,5" Width="200" Text="{Binding FramerateLimit, Mode=TwoWay}" PreviewTextInput="ValidateInt32" /> <ui:TextBox Margin="5,0,0,0" Padding="10,5,10,5" Width="200" Text="{Binding FramerateLimit, Mode=TwoWay}" PreviewTextInput="ValidateUInt32" />
</controls:OptionControl> </controls:OptionControl>
<controls:OptionControl <controls:OptionControl

View File

@ -31,6 +31,8 @@ namespace Bloxstrap.UI.Elements.Settings.Pages
DataContext = new FastFlagsViewModel(this); DataContext = new FastFlagsViewModel(this);
} }
private void ValidateInt32(object sender, TextCompositionEventArgs e) => e.Handled = !Int32.TryParse(e.Text, out int _); private void ValidateInt32(object sender, TextCompositionEventArgs e) => e.Handled = e.Text != "-" && !Int32.TryParse(e.Text, out int _);
private void ValidateUInt32(object sender, TextCompositionEventArgs e) => e.Handled = !UInt32.TryParse(e.Text, out uint _);
} }
} }

View File

@ -0,0 +1,63 @@
using System.Windows;
namespace Bloxstrap.UI.ViewModels.About
{
public class AboutViewModel : NotifyPropertyChangedViewModel
{
private SupporterData? _supporterData;
public string Version => string.Format(Strings.Menu_About_Version, App.Version);
public BuildMetadataAttribute BuildMetadata => App.BuildMetadata;
public string BuildTimestamp => BuildMetadata.Timestamp.ToFriendlyString();
public string BuildCommitHashUrl => $"https://github.com/{App.ProjectRepository}/commit/{BuildMetadata.CommitHash}";
public Visibility BuildInformationVisibility => App.IsProductionBuild ? Visibility.Collapsed : Visibility.Visible;
public Visibility BuildCommitVisibility => App.IsActionBuild ? Visibility.Visible : Visibility.Collapsed;
public List<Supporter> Supporters => _supporterData?.Supporters ?? Enumerable.Empty<Supporter>().ToList();
public int SupporterColumns => _supporterData?.Columns ?? 0;
public GenericTriState SupportersLoadedState { get; set; } = GenericTriState.Unknown;
public string SupportersLoadError { get; set; } = "";
public AboutViewModel()
{
// this will cause momentary freezes only when ran under the debugger
LoadSupporterData();
}
public async void LoadSupporterData()
{
const string LOG_IDENT = "AboutViewModel::LoadSupporterData";
try
{
_supporterData = await Http.GetJson<SupporterData>("https://raw.githubusercontent.com/bloxstraplabs/config/main/supporters.json");
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, "Could not load supporter data");
App.Logger.WriteException(LOG_IDENT, ex);
SupportersLoadedState = GenericTriState.Failed;
SupportersLoadError = ex.Message;
OnPropertyChanged(nameof(SupportersLoadError));
}
if (_supporterData is not null)
{
SupportersLoadedState = GenericTriState.Successful;
OnPropertyChanged(nameof(Supporters));
OnPropertyChanged(nameof(SupporterColumns));
}
OnPropertyChanged(nameof(SupportersLoadedState));
}
}
}

View File

@ -0,0 +1,60 @@
using System.Collections.ObjectModel;
using System.Windows.Data;
using System.Windows.Input;
using Bloxstrap.Integrations;
using CommunityToolkit.Mvvm.Input;
namespace Bloxstrap.UI.ViewModels.ContextMenu
{
internal class ServerHistoryViewModel : NotifyPropertyChangedViewModel
{
private readonly ActivityWatcher _activityWatcher;
public List<ActivityHistoryEntry>? ActivityHistory { get; private set; }
public ICommand CloseWindowCommand => new RelayCommand(RequestClose);
public EventHandler? RequestCloseEvent;
public ServerHistoryViewModel(ActivityWatcher activityWatcher)
{
_activityWatcher = activityWatcher;
_activityWatcher.OnGameLeave += (_, _) => LoadData();
LoadData();
}
private async void LoadData()
{
var entries = _activityWatcher.ActivityHistory.Where(x => !x.DetailsLoaded);
if (entries.Any())
{
string universeIds = String.Join(',', entries.Select(x => x.UniverseId));
var gameDetailResponse = await Http.GetJson<ApiArrayResponse<GameDetailResponse>>($"https://games.roblox.com/v1/games?universeIds={universeIds}");
if (gameDetailResponse is null || !gameDetailResponse.Data.Any())
return;
var universeThumbnailResponse = await Http.GetJson<ApiArrayResponse<ThumbnailResponse>>($"https://thumbnails.roblox.com/v1/games/icons?universeIds={universeIds}&returnPolicy=PlaceHolder&size=128x128&format=Png&isCircular=false");
if (universeThumbnailResponse is null || !universeThumbnailResponse.Data.Any())
return;
foreach (var entry in entries)
{
entry.GameName = gameDetailResponse.Data.Where(x => x.Id == entry.UniverseId).Select(x => x.Name).First();
entry.GameThumbnail = universeThumbnailResponse.Data.Where(x => x.TargetId == entry.UniverseId).Select(x => x.ImageUrl).First();
entry.DetailsLoaded = true;
}
}
ActivityHistory = new(_activityWatcher.ActivityHistory);
OnPropertyChanged(nameof(ActivityHistory));
}
private void RequestClose() => RequestCloseEvent?.Invoke(this, EventArgs.Empty);
}
}

View File

@ -32,11 +32,11 @@ namespace Bloxstrap.UI.ViewModels.Installer
} }
installer.InstallLocation = value; installer.InstallLocation = value;
CheckExistingData(); OnPropertyChanged(nameof(DataFoundMessageVisibility));
} }
} }
public Visibility DataFoundMessageVisibility { get; set; } = Visibility.Collapsed; public Visibility DataFoundMessageVisibility => installer.ExistingDataPresent ? Visibility.Visible : Visibility.Collapsed;
public string ErrorMessage => installer.InstallLocationError; public string ErrorMessage => installer.InstallLocationError;
@ -61,7 +61,6 @@ namespace Bloxstrap.UI.ViewModels.Installer
public InstallViewModel() public InstallViewModel()
{ {
_originalInstallLocation = installer.InstallLocation; _originalInstallLocation = installer.InstallLocation;
CheckExistingData();
} }
public bool DoInstall() public bool DoInstall()
@ -79,16 +78,6 @@ namespace Bloxstrap.UI.ViewModels.Installer
return true; return true;
} }
public void CheckExistingData()
{
if (File.Exists(Path.Combine(InstallLocation, "Settings.json")))
DataFoundMessageVisibility = Visibility.Visible;
else
DataFoundMessageVisibility = Visibility.Collapsed;
OnPropertyChanged(nameof(DataFoundMessageVisibility));
}
private void BrowseInstallLocation() private void BrowseInstallLocation()
{ {
using var dialog = new System.Windows.Forms.FolderBrowserDialog(); using var dialog = new System.Windows.Forms.FolderBrowserDialog();

View File

@ -5,9 +5,9 @@
{ {
// formatting is done here instead of in xaml, it's just a bit easier // formatting is done here instead of in xaml, it's just a bit easier
public string MainText => String.Format( public string MainText => String.Format(
Resources.Strings.Installer_Welcome_MainText, Strings.Installer_Welcome_MainText,
"[github.com/pizzaboxer/bloxstrap](https://github.com/pizzaboxer/bloxstrap)", "[github.com/pizzaboxer/bloxstrap](https://github.com/pizzaboxer/bloxstrap)",
"[bloxstrap.pizzaboxer.xyz](https://bloxstrap.pizzaboxer.xyz)" "[bloxstraplabs.com](https://bloxstraplabs.com)"
); );
public string VersionNotice { get; private set; } = ""; public string VersionNotice { get; private set; } = "";

View File

@ -1,17 +0,0 @@
using System.Windows;
namespace Bloxstrap.UI.ViewModels.Settings
{
public class AboutViewModel
{
public string Version => string.Format(Strings.Menu_About_Version, App.Version);
public BuildMetadataAttribute BuildMetadata => App.BuildMetadata;
public string BuildTimestamp => BuildMetadata.Timestamp.ToFriendlyString();
public string BuildCommitHashUrl => $"https://github.com/{App.ProjectRepository}/commit/{BuildMetadata.CommitHash}";
public Visibility BuildInformationVisibility => App.IsProductionBuild ? Visibility.Collapsed : Visibility.Visible;
public Visibility BuildCommitVisibility => App.IsActionBuild ? Visibility.Visible : Visibility.Collapsed;
}
}

View File

@ -23,9 +23,9 @@ namespace Bloxstrap.UI.ViewModels.Settings
IBootstrapperDialog dialog = App.Settings.Prop.BootstrapperStyle.GetNew(); IBootstrapperDialog dialog = App.Settings.Prop.BootstrapperStyle.GetNew();
if (App.Settings.Prop.BootstrapperStyle == BootstrapperStyle.ByfronDialog) if (App.Settings.Prop.BootstrapperStyle == BootstrapperStyle.ByfronDialog)
dialog.Message = Resources.Strings.Bootstrapper_StylePreview_ImageCancel; dialog.Message = Strings.Bootstrapper_StylePreview_ImageCancel;
else else
dialog.Message = Resources.Strings.Bootstrapper_StylePreview_TextCancel; dialog.Message = Strings.Bootstrapper_StylePreview_TextCancel;
dialog.CancelEnabled = true; dialog.CancelEnabled = true;
dialog.ShowBootstrapper(); dialog.ShowBootstrapper();
@ -35,7 +35,7 @@ namespace Bloxstrap.UI.ViewModels.Settings
{ {
var dialog = new OpenFileDialog var dialog = new OpenFileDialog
{ {
Filter = $"{Resources.Strings.Menu_IconFiles}|*.ico" Filter = $"{Strings.Menu_IconFiles}|*.ico"
}; };
if (dialog.ShowDialog() != true) if (dialog.ShowDialog() != true)

View File

@ -31,13 +31,13 @@ namespace Bloxstrap.UI.ViewModels.Settings
{ {
for (int i = 10; i > 0; i--) for (int i = 10; i > 0; i--)
{ {
ContinueButtonText = $"({i}) {Resources.Strings.Menu_FastFlagEditor_Warning_Continue}"; ContinueButtonText = $"({i}) {Strings.Menu_FastFlagEditor_Warning_Continue}";
OnPropertyChanged(nameof(ContinueButtonText)); OnPropertyChanged(nameof(ContinueButtonText));
await Task.Delay(1000); await Task.Delay(1000);
} }
ContinueButtonText = Resources.Strings.Menu_FastFlagEditor_Warning_Continue; ContinueButtonText = Strings.Menu_FastFlagEditor_Warning_Continue;
OnPropertyChanged(nameof(ContinueButtonText)); OnPropertyChanged(nameof(ContinueButtonText));
CanContinue = true; CanContinue = true;

View File

@ -19,7 +19,7 @@ namespace Bloxstrap.UI.ViewModels.Settings
{ {
CustomIntegrations.Add(new CustomIntegration() CustomIntegrations.Add(new CustomIntegration()
{ {
Name = Resources.Strings.Menu_Integrations_Custom_NewIntegration Name = Strings.Menu_Integrations_Custom_NewIntegration
}); });
SelectedCustomIntegrationIndex = CustomIntegrations.Count - 1; SelectedCustomIntegrationIndex = CustomIntegrations.Count - 1;

View File

@ -17,8 +17,16 @@ namespace Bloxstrap.Utility
public InterProcessLock(string name, TimeSpan timeout) public InterProcessLock(string name, TimeSpan timeout)
{ {
Mutex = new Mutex(false, "Bloxstrap-" + name); Mutex = new Mutex(false, "Bloxstrap-" + name);
try
{
IsAcquired = Mutex.WaitOne(timeout); IsAcquired = Mutex.WaitOne(timeout);
} }
catch (AbandonedMutexException)
{
IsAcquired = true;
}
}
public void Dispose() public void Dispose()
{ {

View File

@ -1,5 +1,5 @@
> [!CAUTION] > [!CAUTION]
> The only official places to download Bloxstrap are this GitHub repository and [bloxstrap.pizzaboxer.xyz](https://bloxstrap.pizzaboxer.xyz). Any other websites offering downloads or claiming to be us are not controlled by us. > The only official places to download Bloxstrap are this GitHub repository and [bloxstraplabs.com](https://bloxstraplabs.com). Any other websites offering downloads or claiming to be us are not controlled by us.
# <img src="https://github.com/pizzaboxer/bloxstrap/raw/main/Images/Bloxstrap.png" width="48"/> Bloxstrap # <img src="https://github.com/pizzaboxer/bloxstrap/raw/main/Images/Bloxstrap.png" width="48"/> Bloxstrap
@ -20,7 +20,7 @@ Bloxstrap is only supported for PCs running Windows.
**Q: Is this malware?** **Q: Is this malware?**
**A:** No. The source code here is viewable to all, and it'd be impossible for us to slip anything malicious into the downloads without anyone noticing. Just be sure you're downloading it from an official source. The only two official sources are this GitHub repository and [bloxstrap.pizzaboxer.xyz](https://bloxstrap.pizzaboxer.xyz). **A:** No. The source code here is viewable to all, and it'd be impossible for us to slip anything malicious into the downloads without anyone noticing. Just be sure you're downloading it from an official source. The only two official sources are this GitHub repository and [bloxstraplabs.com](https://bloxstraplabs.com).
**Q: Can using this get me banned?** **Q: Can using this get me banned?**