// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Text.Json.Serialization; using EpicGames.Core; using EpicGames.Perforce; using Microsoft.Extensions.Logging; namespace UnrealGameSync { public enum BuildConfig { Debug, DebugGame, Development, } public enum TabLabels { Stream, WorkspaceName, WorkspaceRoot, ProjectFile, } public enum BisectState { Include, Exclude, Pass, Fail, } public enum UserSelectedProjectType { Client, Local } public enum FilterType { None, Code, Content } /// /// Config specified class to add a lockable editor argument to a project /// public class LockableEditorArgument { public string Name { get; set; } = ""; public bool Enabled { get; set; } = false; public bool Locked { get; set; } = false; public LockableEditorArgument( string name, bool enabled, bool locked = false ) { Name = name; Enabled = enabled; Locked = locked; } public LockableEditorArgument(LockableEditorArgument lockableEditorArgument) { Name = lockableEditorArgument.Name; Enabled = lockableEditorArgument.Enabled; Locked = lockableEditorArgument.Locked; } public static bool TryParseConfigEntry(string text, [NotNullWhen(true)] out LockableEditorArgument? editorArgument) { ConfigObject definitionObject = new ConfigObject(text); string editorArgumentName = definitionObject.GetValue("Name", ""); if (editorArgumentName.Length > 0) { editorArgument = new LockableEditorArgument( editorArgumentName, definitionObject.GetValue("Enabled", false), definitionObject.GetValue("Locked", false) ); return true; } else { editorArgument = null; } return false; } } /// /// Config specified class to determine what is the Latest Change to Sync /// Can be configured using badges, good and starred CLs /// public class LatestChangeType { // Display Specifiers public string Name { get; set; } = ""; // Name will the saved ID for this LatestChangeType public string Description { get; set; } = ""; public int OrderIndex { get; set; } = Int32.MaxValue; // What Rules to Check for. public bool Good { get; set; } = false; public bool Starred { get; set; } = false; public bool FindNewestGoodContent { get; set; } = false; public List RequiredBadges { get; init; } = new List(); // Depot path to read for the latest change number public string? ReadFrom { get; set; } public static bool TryParseConfigEntry(string text, [NotNullWhen(true)] out LatestChangeType? changeType) { ConfigObject definitionObject = new ConfigObject(text); string latestChangeTypeName = definitionObject.GetValue("Name", ""); if (latestChangeTypeName.Length > 0) { changeType = new LatestChangeType(); changeType.Name = latestChangeTypeName; changeType.Description = definitionObject.GetValue("Description", "No \"Description\" for LatestChangeType given."); changeType.OrderIndex = definitionObject.GetValue("OrderIndex", Int32.MaxValue); changeType.Good = definitionObject.GetValue("bGood", false); changeType.Starred = definitionObject.GetValue("bStarred", false); changeType.FindNewestGoodContent = definitionObject.GetValue("bFindNewestGoodContent", false); changeType.RequiredBadges.AddRange(definitionObject.GetValue("RequiredBadges", "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)); changeType.ReadFrom = definitionObject.GetValue("ReadFrom", null); return true; } else { changeType = null; } return false; } // Default Template for Latest Change public static LatestChangeType LatestChange() { LatestChangeType latest = new LatestChangeType(); latest.Name = "LatestChange"; latest.Description = "Latest Change"; latest.OrderIndex = -3; latest.FindNewestGoodContent = true; return latest; } // Default Template for Latest Good Change public static LatestChangeType LatestGoodChange() { LatestChangeType latestGood = new LatestChangeType(); latestGood.Name = "LatestGoodChange"; latestGood.Description = "Latest Good Change"; latestGood.OrderIndex = -2; latestGood.Good = true; latestGood.FindNewestGoodContent = true; return latestGood; } // Default Template for Latest Starred Change public static LatestChangeType LatestStarredChange() { LatestChangeType latestStarred = new LatestChangeType(); latestStarred.Name = "LatestStarredChange"; latestStarred.Description = "Latest Starred Change"; latestStarred.OrderIndex = -1; latestStarred.Starred = true; latestStarred.FindNewestGoodContent = true; return latestStarred; } } #pragma warning disable CA1027 // Mark enums with FlagsAttribute public enum UserSettingsVersion { Initial = 0, DefaultServerSettings = 1, XgeShaderCompilation = 2, DefaultNumberOfThreads = 3, Latest = DefaultNumberOfThreads } #pragma warning restore CA1027 // Mark enums with FlagsAttribute public class ArchiveSettings { public bool Enabled { get; set; } public string Type { get; set; } public List Order { get; init; } public ArchiveSettings(bool enabled, string type, IEnumerable order) { Enabled = enabled; Type = type; Order = new List(order); } public static bool TryParseConfigEntry(string text, [NotNullWhen(true)] out ArchiveSettings? settings) { ConfigObject obj = new ConfigObject(text); string? type = obj.GetValue("Type", null); if (type == null) { settings = null; return false; } else { string[] order = obj.GetValue("Order", "").Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); bool enabled = obj.GetValue("Enabled", 0) != 0; settings = new ArchiveSettings(enabled, type, order); return true; } } public string ToConfigEntry() { ConfigObject obj = new ConfigObject(); obj.SetValue("Enabled", Enabled ? 1 : 0); obj.SetValue("Type", Type); obj.SetValue("Order", String.Join(";", Order)); return obj.ToString(); } public override string ToString() { return ToConfigEntry(); } } public class SnapshotSettings { public string Runtime { get; set; } = ""; public string Platform { get; set; } = ""; } public class UserSelectedProjectSettings { public string? ServerAndPort { get; set; } public string? UserName { get; set; } public UserSelectedProjectType Type { get; set; } public string? ClientPath { get; set; } public string? LocalPath { get; set; } public string? ScheduledSyncTypeId { get; set; } public UserSelectedProjectSettings(string? serverAndPort, string? userName, UserSelectedProjectType type, string? clientPath, string? localPath) { ServerAndPort = serverAndPort; UserName = userName; Type = type; ClientPath = clientPath; LocalPath = localPath; } public static bool TryParseConfigEntry(string text, [NotNullWhen(true)] out UserSelectedProjectSettings? project) { ConfigObject obj = new ConfigObject(text); UserSelectedProjectType type; if (Enum.TryParse(obj.GetValue("Type", ""), out type)) { string? serverAndPort = obj.GetValue("ServerAndPort", null); if (String.IsNullOrWhiteSpace(serverAndPort)) { serverAndPort = null; } // Fixup for code that was saving server host name rather than DNS entry if (serverAndPort != null && serverAndPort.Equals("p4-nodeb.epicgames.net:1666", StringComparison.OrdinalIgnoreCase)) { serverAndPort = "perforce:1666"; } string? userName = obj.GetValue("UserName", null); if (String.IsNullOrWhiteSpace(userName)) { userName = null; } string? localPath = obj.GetValue("LocalPath", null); if (String.IsNullOrWhiteSpace(localPath)) { localPath = null; } string? clientPath = obj.GetValue("ClientPath", null); if (String.IsNullOrWhiteSpace(clientPath)) { clientPath = null; } string? scheduledSyncTypeId = obj.GetValue("ScheduledSyncTypeID", null); if (String.IsNullOrWhiteSpace(scheduledSyncTypeId)) { scheduledSyncTypeId = null; } if ((type == UserSelectedProjectType.Client && clientPath != null) || (type == UserSelectedProjectType.Local && localPath != null)) { project = new UserSelectedProjectSettings(serverAndPort, userName, type, clientPath, localPath); project.ScheduledSyncTypeId = scheduledSyncTypeId; return true; } } project = null; return false; } public string ToConfigEntry() { ConfigObject obj = new ConfigObject(); if (ServerAndPort != null) { obj.SetValue("ServerAndPort", ServerAndPort); } if (UserName != null) { obj.SetValue("UserName", UserName); } obj.SetValue("Type", Type.ToString()); if (ClientPath != null) { obj.SetValue("ClientPath", ClientPath); } if (LocalPath != null) { obj.SetValue("LocalPath", LocalPath); } if (ScheduledSyncTypeId != null) { obj.SetValue("ScheduledSyncTypeID", ScheduledSyncTypeId); } return obj.ToString(); } public override string? ToString() { return LocalPath ?? ClientPath; } } public class SyncCategory { public Guid Id { get; set; } public bool Enable { get; set; } } public class BisectEntry { public int Change { get; set; } public BisectState State { get; set; } } public class UserWorkspaceSettings { [JsonIgnore] public DirectoryReference RootDir { get; set; } = null!; [JsonIgnore] public long LastModifiedTimeUtc { get; set; } // Connection settings public string? ServerAndPort { get; set; } public string? UserName { get; set; } public string ClientName { get; set; } = String.Empty; // Path to the root of the branch within this client, with a trailing slash if non-empty public string BranchPath { get; set; } = String.Empty; // The currently selected project, relative to the root directory public string ProjectPath { get; set; } = String.Empty; // The currently selected preset public string Preset { get; set; } = String.Empty; // Workspace specific SyncFilters public FilterSettings Filter { get; set; } = new FilterSettings(); [JsonIgnore] public FileReference ConfigFile => GetConfigFile(RootDir); [JsonIgnore] public string ClientProjectPath => $"//{ClientName}{BranchPath}{ProjectPath}"; [JsonIgnore] public FileReference LocalProjectPath => new FileReference(RootDir.FullName + ProjectPath); public void Init(string? serverAndPort, string? userName, string clientName, string branchPath, string projectPath) { ProjectInfo.ValidateBranchPath(branchPath); ProjectInfo.ValidateProjectPath(projectPath); ServerAndPort = serverAndPort; UserName = userName; ClientName = clientName; BranchPath = branchPath; ProjectPath = projectPath; } public static bool TryLoad(DirectoryReference rootDir, [NotNullWhen(true)] out UserWorkspaceSettings? settings) { FileReference configFile = GetConfigFile(rootDir); if (Utility.TryLoadJson(configFile, out settings)) { settings.RootDir = rootDir; settings.LastModifiedTimeUtc = FileReference.GetLastWriteTimeUtc(configFile).Ticks; return true; } else { settings = null; return false; } } static readonly object _syncRoot = new object(); public bool Save(ILogger logger) { try { SaveInternal(); return true; } catch (Exception ex) { logger.LogError(ex, "Unable to save {ConfigFile}: {Message}", ConfigFile, ex.Message); return false; } } private void SaveInternal() { lock (_syncRoot) { UserSettings.CreateConfigDir(ConfigFile.Directory); Utility.SaveJson(ConfigFile, this); LastModifiedTimeUtc = FileReference.GetLastWriteTimeUtc(ConfigFile).Ticks; } } public static FileReference GetConfigFile(DirectoryReference rootDir) { return FileReference.Combine(UserSettings.GetConfigDir(rootDir), "settings.json"); } } public class UserProjectSettings { [JsonIgnore] public FileReference ConfigFile { get; private set; } = null!; public List BuildSteps { get; init; } = new List(); public FilterType FilterType { get; set; } public HashSet FilterBadges { get; init; } = new HashSet(StringComparer.OrdinalIgnoreCase); [JsonIgnore] public List RequiredBadges { get; init; } = new List(); static readonly object _syncRoot = new object(); private UserProjectSettings() { } public UserProjectSettings(FileReference configFile) { ConfigFile = configFile; } public static bool TryLoad(FileReference configFile, [NotNullWhen(true)] out UserProjectSettings? settings) { if (Utility.TryLoadJson(configFile, out settings)) { settings.ConfigFile = configFile; return true; } else { settings = null; return false; } } public bool Save(ILogger logger) { try { SaveInternal(); return true; } catch (Exception ex) { logger.LogError(ex, "Unable to save {ConfigFile}: {Message}", ConfigFile, ex.Message); return false; } } void SaveInternal() { lock (_syncRoot) { Utility.SaveJson(ConfigFile, this); } } } public class UserSettings : GlobalSettingsFile { /// /// Enum that decribes which robomerge changes to show /// public enum RobomergeShowChangesOption { All, // Show all changes from robomerge Badged, // Show only robomerge changes that have an associated badge None // Show no robomerge changes }; readonly FileReference _fileName; readonly ConfigFile _configFile; // General settings public UserSettingsVersion Version { get; set; } = UserSettingsVersion.Latest; public bool GenerateProjectAfterSync { get; set; } public bool BuildAfterSync { get; set; } public bool RunAfterSync { get; set; } public bool OpenSolutionAfterSync { get; set; } public bool ShowLogWindow { get; set; } public bool ShowUnreviewedChanges { get; set; } public bool ShowAutomatedChanges { get; set; } public RobomergeShowChangesOption ShowRobomerge { get; set; } public bool AnnotateRobmergeChanges { get; set; } public bool ShowLocalTimes { get; set; } public bool KeepInTray { get; set; } public HashSet EnabledTools { get; init; } = new HashSet(); public int FilterIndex { get; set; } public UserSelectedProjectSettings? LastProject { get; set; } public List OpenProjects { get; init; } public List RecentProjects { get; init; } public string SyncTypeId { get; set; } public BuildConfig CompiledEditorBuildConfig { get; set; } // NB: This assumes not using precompiled editor. See CurrentBuildConfig. public TabLabels TabLabels { get; set; } public long NextLauncherVersionCheck { get; set; } public ApplicationTheme? Theme { get; set; } public Dictionary ThemeList { get; init; } // Precompiled binaries public List Archives { get; init; } = new List(); public List Snapshots { get; init; } = new List(); public string SnapshotHost { get; set; } = ""; public string SnapshotNamespace { get; set; } = ""; // OIDC Settings public Dictionary ProviderToRefreshTokens { get; init; } = new Dictionary(); // Window settings public bool WindowVisible { get; set; } public string WindowState { get; set; } public Rectangle? WindowBounds { get; set; } // Schedule settings public bool ScheduleEnabled { get; set; } public TimeSpan ScheduleTime { get; set; } public bool ScheduleAnyOpenProject { get; set; } public List ScheduleProjects { get; init; } = new List(); // Run configuration public List EditorArguments { get; init; } = new List(); public bool EditorArgumentsPrompt { get; set; } // Notification settings public List NotifyProjects { get; init; } = new List(); public int NotifyUnassignedMinutes { get; set; } public int NotifyUnacknowledgedMinutes { get; set; } public int NotifyUnresolvedMinutes { get; set; } // Project settings readonly Dictionary _workspaceDirToSettings = new Dictionary(); readonly Dictionary _projectKeyToSettings = new Dictionary(); // Perforce settings public PerforceSyncOptions SyncOptions => Global.Perforce; public ClientSubmitOptions ClientSubmitOptions { get; set; } private List ReadProjectList(string settingName, string legacySettingName) { List projects = new List(); string[]? projectStrings = _configFile.GetValues(settingName, null); if (projectStrings != null) { foreach (string projectString in projectStrings) { UserSelectedProjectSettings? project; if (UserSelectedProjectSettings.TryParseConfigEntry(projectString, out project)) { projects.Add(project); } } } else if (legacySettingName != null) { string[]? legacyProjectStrings = _configFile.GetValues(legacySettingName, null); if (legacyProjectStrings != null) { foreach (string legacyProjectString in legacyProjectStrings) { if (!String.IsNullOrWhiteSpace(legacyProjectString)) { projects.Add(new UserSelectedProjectSettings(null, null, UserSelectedProjectType.Local, null, legacyProjectString)); } } } } return projects; } public static UserSettings Create(DirectoryReference settingsDir, ILogger logger) { return Create(FileReference.Combine(settingsDir, "UnrealGameSyncV2.ini"), FileReference.Combine(settingsDir, "UnrealGameSync.ini"), FileReference.Combine(settingsDir, "Global.json"), logger); } public static UserSettings Create(FileReference fileName, FileReference legacyFileName, FileReference coreFileName, ILogger logger) { ConfigFile configFile = new ConfigFile(); if (FileReference.Exists(fileName)) { configFile.TryLoad(fileName, logger); } else { configFile.TryLoad(legacyFileName, logger); } ApplicationTheme.AddDefaultThemesToConfig(configFile); GlobalSettings? coreSettingsData = null; if (FileReference.Exists(coreFileName)) { try { coreSettingsData = Utility.LoadJson(coreFileName); } catch (Exception ex) { logger.LogError(ex, "Error while reading {File}.", coreFileName); } } if (coreSettingsData == null) { coreSettingsData = new GlobalSettings(); coreSettingsData.Filter.View.AddRange(configFile.GetValues("General.SyncFilter", Array.Empty())); coreSettingsData.Filter.SetCategories(GetCategorySettings(configFile.FindSection("General"), "SyncIncludedCategories", "SyncExcludedCategories")); coreSettingsData.Filter.AllProjects = configFile.GetValue("General.SyncAllProjects", false); coreSettingsData.Filter.AllProjectsInSln = configFile.GetValue("General.IncludeAllProjectsInSolution", false); coreSettingsData.Filter.UprojectSpecificSln = configFile.GetValue("General.UprojectSpecificSolution", false); } return new UserSettings(fileName, configFile, coreFileName, coreSettingsData); } public UserSettings(FileReference inFileName, ConfigFile inConfigFile, FileReference inCoreFileName, GlobalSettings inCoreSettingsData) : base(inCoreFileName, inCoreSettingsData) { _fileName = inFileName; _configFile = inConfigFile; // General settings Version = (UserSettingsVersion)_configFile.GetValue("General.Version", (int)UserSettingsVersion.Initial); GenerateProjectAfterSync = (_configFile.GetValue("General.GenerateProjectAfterSync", "1") != "0"); BuildAfterSync = (_configFile.GetValue("General.BuildAfterSync", "1") != "0"); RunAfterSync = (_configFile.GetValue("General.RunAfterSync", "1") != "0"); bool syncPrecompiledEditor = (_configFile.GetValue("General.SyncPrecompiledEditor", "0") != "0"); OpenSolutionAfterSync = (_configFile.GetValue("General.OpenSolutionAfterSync", "0") != "0"); ShowLogWindow = (_configFile.GetValue("General.ShowLogWindow", false)); string? autoResolveConflicts = _configFile.GetValue("General.AutoResolveConflicts", ""); if (!String.IsNullOrEmpty(autoResolveConflicts)) { Global.AutoResolveConflicts = (autoResolveConflicts != "0"); } ShowUnreviewedChanges = _configFile.GetValue("General.ShowUnreviewed", true); ShowAutomatedChanges = _configFile.GetValue("General.ShowAutomated", false); NextLauncherVersionCheck = _configFile.GetValue("General.NextLauncherVersionCheck", 0); ShowRobomerge = _configFile.GetEnumValue("General.RobomergeFilter", RobomergeShowChangesOption.All); AnnotateRobmergeChanges = _configFile.GetValue("General.AnnotateRobomerge", true); ShowLocalTimes = _configFile.GetValue("General.ShowLocalTimes", false); KeepInTray = _configFile.GetValue("General.KeepInTray", true); string themeName = _configFile.GetValue("General.Theme", ApplicationTheme.DefaultThemeId); Theme = ApplicationTheme.LoadThemeFromConfig(themeName, _configFile); ThemeList = ApplicationTheme.GetThemeList(_configFile); EnabledTools.Clear(); EnabledTools.UnionWith(_configFile.GetGuidValues("General.EnabledTools", Array.Empty())); if (_configFile.GetValue("General.EnableP4VExtensions", false)) { EnabledTools.Add(new Guid("963850A0-BF63-4E0E-B903-1C5954C7DCF8")); } if (_configFile.GetValue("General.EnableUshell", false)) { EnabledTools.Add(new Guid("922EED87-E732-464C-92DC-5A8F7ED955E2")); } FilterIndex = _configFile.GetValue("General.FilterIndex", 0); string? lastProjectString = _configFile.GetValue("General.LastProject", null); if (lastProjectString != null) { UserSelectedProjectSettings? lastProject; if (!UserSelectedProjectSettings.TryParseConfigEntry(lastProjectString, out lastProject)) { lastProject = null; } LastProject = lastProject; } else { string? lastProjectFileName = _configFile.GetValue("General.LastProjectFileName", null); if (lastProjectFileName != null) { LastProject = new UserSelectedProjectSettings(null, null, UserSelectedProjectType.Local, null, lastProjectFileName); } } OpenProjects = ReadProjectList("General.OpenProjects", "General.OpenProjectFileNames"); RecentProjects = ReadProjectList("General.RecentProjects", "General.OtherProjectFileNames"); SyncTypeId = _configFile.GetValue("General.SyncTypeID", ""); ClientSubmitOptions = _configFile.GetEnumValue("General.ClientSubmitOptions", ClientSubmitOptions.RevertUnchanged); string? oldSyncTye = _configFile.GetValue("General.SyncType", null); if (oldSyncTye != null) { if (oldSyncTye == "Any") { SyncTypeId = LatestChangeType.LatestChange().Name; } else if (oldSyncTye == "Good") { SyncTypeId = LatestChangeType.LatestGoodChange().Name; } else if (oldSyncTye == "Starred") { SyncTypeId = LatestChangeType.LatestStarredChange().Name; } } // Build configuration CompiledEditorBuildConfig = _configFile.GetEnumValue("General.BuildConfig", BuildConfig.Development); // Tab names TabLabels = _configFile.GetEnumValue("General.TabLabels", TabLabels.Stream); // Editor arguments string[] arguments = _configFile.GetValues("General.EditorArguments", Array.Empty()); if (Version < UserSettingsVersion.XgeShaderCompilation) { arguments = Enumerable.Concat(arguments, new string[] { "0:-noxgeshadercompile" }).ToArray(); } foreach (string argument in arguments) { if (argument.StartsWith("0:", StringComparison.Ordinal)) { EditorArguments.Add(new LockableEditorArgument(argument.Substring(2), /* Enabled = */ false)); } else if (argument.StartsWith("1:", StringComparison.Ordinal)) { EditorArguments.Add(new LockableEditorArgument(argument.Substring(2), /* Enabled = */ true)); } else { EditorArguments.Add(new LockableEditorArgument(argument, /* Enabled = */ true)); } } EditorArgumentsPrompt = _configFile.GetValue("General.EditorArgumentsPrompt", false); // Precompiled binaries string[] archiveValues = _configFile.GetValues("PrecompiledBinaries.Archives", Array.Empty()); foreach (string archiveValue in archiveValues) { ArchiveSettings? settings; if (ArchiveSettings.TryParseConfigEntry(archiveValue, out settings)) { Archives.Add(settings); } } if (syncPrecompiledEditor) { Archives.Add(new ArchiveSettings(true, "Editor", Array.Empty())); } // OIDC Settings string[] tokens = _configFile.GetValues("OIDCProviders.Tokens", Array.Empty()); foreach (string tokenValue in tokens) { ConfigObject o = new ConfigObject(tokenValue); string? provider = o.GetValue("Provider"); string? token = o.GetValue("Token"); if (provider != null && token != null) { ProviderToRefreshTokens.TryAdd(provider, token); } } // Window settings WindowVisible = _configFile.GetValue("Window.Visible", true); WindowState = _configFile.GetValue("Window.State", ""); WindowBounds = ParseRectangleValue(_configFile.GetValue("Window.Bounds", "")); // Schedule settings ScheduleEnabled = _configFile.GetValue("Schedule.Enabled", false); TimeSpan scheduleTime; if (!TimeSpan.TryParse(_configFile.GetValue("Schedule.Time", ""), out scheduleTime)) { scheduleTime = new TimeSpan(6, 0, 0); } ScheduleTime = scheduleTime; ScheduleAnyOpenProject = _configFile.GetValue("Schedule.AnyOpenProject", true); ScheduleProjects = ReadProjectList("Schedule.Projects", "Schedule.ProjectFileNames"); // Notification settings NotifyProjects = _configFile.GetValues("Notifications.NotifyProjects", Array.Empty()).ToList(); NotifyUnassignedMinutes = _configFile.GetValue("Notifications.NotifyUnassignedMinutes", -1); NotifyUnacknowledgedMinutes = _configFile.GetValue("Notifications.NotifyUnacknowledgedMinutes", -1); NotifyUnresolvedMinutes = _configFile.GetValue("Notifications.NotifyUnresolvedMinutes", -1); // Perforce settings string? numThreadsStr = _configFile.GetValue("Perforce.NumThreads", null); if (numThreadsStr != null) { int numThreads; if (Int32.TryParse(numThreadsStr, out numThreads) && numThreads > 0) { if (Version >= UserSettingsVersion.DefaultNumberOfThreads || numThreads > 1) { SyncOptions.NumThreads = numThreads; } } } SyncOptions.MaxCommandsPerBatch = _configFile.GetOptionalIntValue("Perforce.MaxCommandsPerBatch", SyncOptions.MaxCommandsPerBatch); SyncOptions.MaxSizePerBatch = _configFile.GetOptionalIntValue("Perforce.MaxSizePerBatch", SyncOptions.MaxSizePerBatch); SyncOptions.NumSyncErrorRetries = _configFile.GetOptionalIntValue("Perforce.NumSyncErrorRetries", SyncOptions.NumSyncErrorRetries); SyncOptions.SyncErrorRetryDelay = _configFile.GetOptionalIntValue("Perforce.SyncErrorRetryDelay", SyncOptions.SyncErrorRetryDelay); } static Dictionary GetCategorySettings(ConfigSection? section, string includedKey, string excludedKey) { Dictionary result = new Dictionary(); if (section != null) { foreach (Guid uniqueId in section.GetValues(includedKey, Array.Empty())) { result[uniqueId] = true; } foreach (Guid uniqueId in section.GetValues(excludedKey, Array.Empty())) { result[uniqueId] = false; } } return result; } static Rectangle? ParseRectangleValue(string text) { ConfigObject obj = new ConfigObject(text); int x = obj.GetValue("X", -1); int y = obj.GetValue("Y", -1); int w = obj.GetValue("W", -1); int h = obj.GetValue("H", -1); if (x == -1 || y == -1 || w == -1 || h == -1) { return null; } else { return new Rectangle(x, y, w, h); } } static string FormatRectangleValue(Rectangle value) { ConfigObject obj = new ConfigObject(); obj.SetValue("X", value.X); obj.SetValue("Y", value.Y); obj.SetValue("W", value.Width); obj.SetValue("H", value.Height); return obj.ToString(); } public static DirectoryReference GetConfigDir(DirectoryReference workspaceDir) { DirectoryReference configDir = DirectoryReference.Combine(workspaceDir, ".ugs"); return configDir; } public static void CreateConfigDir(DirectoryReference configDir) { DirectoryInfo configDirInfo = configDir.ToDirectoryInfo(); if (!configDirInfo.Exists) { configDirInfo.Create(); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { configDirInfo.Attributes = FileAttributes.Directory | FileAttributes.Hidden; } } } public ApplicationTheme? LoadTheme(string themeId) { return ApplicationTheme.LoadThemeFromConfig(themeId, _configFile); } protected override void ImportWorkspaceState(DirectoryReference rootDir, string clientName, string branchPath, WorkspaceState currentWorkspace) { // Read the workspace settings ConfigSection? workspaceSection = _configFile.FindSection(clientName + branchPath); if (workspaceSection == null) { string legacyBranchAndClientKey = clientName + branchPath; int slashIdx = legacyBranchAndClientKey.IndexOf('/', StringComparison.Ordinal); if (slashIdx != -1) { legacyBranchAndClientKey = legacyBranchAndClientKey.Substring(0, slashIdx) + "$" + legacyBranchAndClientKey.Substring(slashIdx + 1); } string? currentSync = _configFile.GetValue("Clients." + legacyBranchAndClientKey, null); if (currentSync != null) { int atIdx = currentSync.LastIndexOf('@'); if (atIdx != -1) { int changeNumber; if (Int32.TryParse(currentSync.Substring(atIdx + 1), out changeNumber)) { currentWorkspace.ProjectIdentifier = currentSync.Substring(0, atIdx); currentWorkspace.CurrentChangeNumber = changeNumber; } } } string? lastUpdateResultText = _configFile.GetValue("Clients." + legacyBranchAndClientKey + "$LastUpdate", null); if (lastUpdateResultText != null) { int colonIdx = lastUpdateResultText.LastIndexOf(':'); if (colonIdx != -1) { int changeNumber; if (Int32.TryParse(lastUpdateResultText.Substring(0, colonIdx), out changeNumber)) { WorkspaceUpdateResult result; if (Enum.TryParse(lastUpdateResultText.Substring(colonIdx + 1), out result)) { currentWorkspace.LastSyncChangeNumber = changeNumber; currentWorkspace.LastSyncResult = result; } } } } } else { currentWorkspace.ProjectIdentifier = workspaceSection.GetValue("CurrentProjectPath", ""); currentWorkspace.CurrentChangeNumber = workspaceSection.GetValue("CurrentChangeNumber", -1); currentWorkspace.CurrentSyncFilterHash = workspaceSection.GetValue("CurrentSyncFilterHash", null); foreach (string additionalChangeNumberString in workspaceSection.GetValues("AdditionalChangeNumbers", Array.Empty())) { int additionalChangeNumber; if (Int32.TryParse(additionalChangeNumberString, out additionalChangeNumber)) { currentWorkspace.AdditionalChangeNumbers.Add(additionalChangeNumber); } } currentWorkspace.LastSyncResult = workspaceSection.GetEnumValue("LastSyncResult", WorkspaceUpdateResult.Canceled); currentWorkspace.LastSyncResultMessage = UnescapeText(workspaceSection.GetValue("LastSyncResultMessage")); currentWorkspace.LastSyncChangeNumber = workspaceSection.GetValue("LastSyncChangeNumber", -1); DateTime lastSyncTime; if (DateTime.TryParse(workspaceSection.GetValue("LastSyncTime", ""), out lastSyncTime)) { currentWorkspace.LastSyncTime = lastSyncTime; } currentWorkspace.LastSyncDurationSeconds = workspaceSection.GetValue("LastSyncDuration", 0); currentWorkspace.LastBuiltChangeNumber = workspaceSection.GetValue("LastBuiltChangeNumber", 0); currentWorkspace.LastSyncEditorArchive = workspaceSection.GetValue("LastSyncEditorArchive", "0"); currentWorkspace.ExpandedArchiveTypes.Clear(); currentWorkspace.ExpandedArchiveTypes.UnionWith(workspaceSection.GetValues("ExpandedArchiveName", Array.Empty())); string[] bisectEntries = workspaceSection.GetValues("Bisect", Array.Empty()); foreach (string bisectEntry in bisectEntries) { ConfigObject bisectEntryObject = new ConfigObject(bisectEntry); int changeNumber = bisectEntryObject.GetValue("Change", -1); if (changeNumber != -1) { BisectState state; if (Enum.TryParse(bisectEntryObject.GetValue("State", ""), out state)) { BisectEntry entry = new BisectEntry(); entry.Change = changeNumber; entry.State = state; currentWorkspace.BisectChanges.Add(entry); } } } } } protected override void ImportWorkspaceSettings(DirectoryReference rootDir, string clientName, string branchPath, UserWorkspaceSettings currentWorkspace) { ConfigSection? workspaceSection = _configFile.FindSection(clientName + branchPath); if (workspaceSection != null) { currentWorkspace.Filter.View.Clear(); currentWorkspace.Filter.View.AddRange(workspaceSection.GetValues("SyncFilter", Array.Empty())); currentWorkspace.Filter.SetCategories(GetCategorySettings(workspaceSection, "SyncIncludedCategories", "SyncExcludedCategories")); int syncAllProjects = workspaceSection.GetValue("SyncAllProjects", -1); currentWorkspace.Filter.AllProjects = (syncAllProjects == 0) ? (bool?)false : (syncAllProjects == 1) ? (bool?)true : (bool?)null; int includeAllProjectsInSolution = workspaceSection.GetValue("IncludeAllProjectsInSolution", -1); currentWorkspace.Filter.AllProjectsInSln = (includeAllProjectsInSolution == 0) ? (bool?)false : (includeAllProjectsInSolution == 1) ? (bool?)true : (bool?)null; int uprojectSpecificSolution = workspaceSection.GetValue("UprojectSpecificSolution", -1); currentWorkspace.Filter.UprojectSpecificSln = (uprojectSpecificSolution == 0) ? (bool?)false : (uprojectSpecificSolution == 1) ? (bool?)true : (bool?)null; } } protected override void ImportProjectSettings(ProjectInfo projectInfo, UserProjectSettings currentProject) { string clientProjectFileName = projectInfo.ClientFileName; // Read the project settings ConfigSection projectSection = _configFile.FindOrAddSection(clientProjectFileName); currentProject.BuildSteps.AddRange(projectSection.GetValues("BuildStep", Array.Empty()).Select(x => new ConfigObject(x))); currentProject.FilterType = projectSection.GetEnumValue("FilterType", FilterType.None); currentProject.FilterBadges.UnionWith(projectSection.GetValues("FilterBadges", Array.Empty())); currentProject.RequiredBadges.AddRange(projectSection.GetValues("RequiredBadges", Array.Empty())); } public override bool Save(ILogger logger) { if (!base.Save(logger)) { return false; } // General settings ConfigSection generalSection = _configFile.FindOrAddSection("General"); generalSection.Clear(); generalSection.SetValue("Version", (int)Version); generalSection.SetValue("GenerateProjectAfterSync", GenerateProjectAfterSync); generalSection.SetValue("BuildAfterSync", BuildAfterSync); generalSection.SetValue("RunAfterSync", RunAfterSync); generalSection.SetValue("OpenSolutionAfterSync", OpenSolutionAfterSync); generalSection.SetValue("ShowLogWindow", ShowLogWindow); generalSection.SetValue("ShowUnreviewed", ShowUnreviewedChanges); generalSection.SetValue("ShowAutomated", ShowAutomatedChanges); generalSection.SetValue("RobomergeFilter", ShowRobomerge.ToString()); generalSection.SetValue("AnnotateRobomerge", AnnotateRobmergeChanges); generalSection.SetValue("ShowLocalTimes", ShowLocalTimes); if (LastProject != null) { generalSection.SetValue("LastProject", LastProject.ToConfigEntry()); } generalSection.SetValues("OpenProjects", OpenProjects.Select(x => x.ToConfigEntry()).ToArray()); generalSection.SetValue("KeepInTray", KeepInTray); generalSection.SetValues("EnabledTools", EnabledTools.ToArray()); generalSection.SetValue("FilterIndex", FilterIndex); generalSection.SetValues("RecentProjects", RecentProjects.Select(x => x.ToConfigEntry()).ToArray()); generalSection.SetValue("SyncTypeID", SyncTypeId); generalSection.SetValue("ClientSubmitOptions", ClientSubmitOptions.ToString()); generalSection.SetValue("NextLauncherVersionCheck", NextLauncherVersionCheck); generalSection.SetValue("Theme", Theme?.ThemeId ?? ApplicationTheme.DefaultThemeId); // Build configuration generalSection.SetValue("BuildConfig", CompiledEditorBuildConfig.ToString()); // Tab labels generalSection.SetValue("TabLabels", TabLabels.ToString()); // Editor arguments List editorArgumentList = new List(); foreach (LockableEditorArgument editorArgument in EditorArguments) { editorArgumentList.Add(String.Format("{0}:{1}", editorArgument.Enabled ? 1 : 0, editorArgument.Name)); } generalSection.SetValues("EditorArguments", editorArgumentList.ToArray()); generalSection.SetValue("EditorArgumentsPrompt", EditorArgumentsPrompt); // Schedule settings ConfigSection scheduleSection = _configFile.FindOrAddSection("Schedule"); scheduleSection.Clear(); scheduleSection.SetValue("Enabled", ScheduleEnabled); scheduleSection.SetValue("Time", ScheduleTime.ToString()); scheduleSection.SetValue("AnyOpenProject", ScheduleAnyOpenProject); scheduleSection.SetValues("Projects", ScheduleProjects.Select(x => x.ToConfigEntry()).ToArray()); // Precompiled binaries ConfigSection archivesSection = _configFile.FindOrAddSection("PrecompiledBinaries"); archivesSection.SetValues("Archives", Archives.Select(x => x.ToConfigEntry()).ToArray()); // OIDC Settings ConfigSection oidcSection = _configFile.FindOrAddSection("OIDCProviders"); List tokenObjects = new List(); foreach (KeyValuePair pair in ProviderToRefreshTokens) { ConfigObject tokenEntryObject = new ConfigObject(); tokenEntryObject.SetValue("Provider", pair.Key); tokenEntryObject.SetValue("Token", pair.Value); tokenObjects.Add(tokenEntryObject); } oidcSection.SetValues("Tokens", tokenObjects.Select(x => x.ToString()).ToArray()); // Window settings ConfigSection windowSection = _configFile.FindOrAddSection("Window"); windowSection.Clear(); windowSection.SetValue("Visible", WindowVisible); windowSection.SetValue("State", WindowState); if (WindowBounds != null) { windowSection.SetValue("Bounds", FormatRectangleValue(WindowBounds.Value)); } // Notification settings ConfigSection notificationSection = _configFile.FindOrAddSection("Notifications"); notificationSection.Clear(); if (NotifyProjects.Count > 0) { notificationSection.SetValues("NotifyProjects", NotifyProjects.ToArray()); } if (NotifyUnassignedMinutes != -1) { notificationSection.SetValue("NotifyUnassignedMinutes", NotifyUnassignedMinutes); } if (NotifyUnacknowledgedMinutes != -1) { notificationSection.SetValue("NotifyUnacknowledgedMinutes", NotifyUnacknowledgedMinutes); } if (NotifyUnresolvedMinutes != -1) { notificationSection.SetValue("NotifyUnresolvedMinutes", NotifyUnresolvedMinutes); } // Perforce settings _configFile.RemoveSection("Perforce"); // Save the file try { _configFile.Save(_fileName); return true; } catch (Exception ex) { logger.LogError(ex, "Unable to save config file {FileName}: {Message}", _fileName, ex.Message); return false; } } [return: NotNullIfNotNull("text")] static string? UnescapeText(string? text) { if (text == null) { return null; } StringBuilder result = new StringBuilder(); for (int idx = 0; idx < text.Length; idx++) { if (text[idx] == '\\' && idx + 1 < text.Length) { switch (text[++idx]) { case 't': result.Append('\t'); break; case 'r': result.Append('\r'); break; case 'n': result.Append('\n'); break; case '\'': result.Append('\''); break; case '\"': result.Append('\"'); break; default: result.Append(text[idx]); break; } } else { result.Append(text[idx]); } } return result.ToString(); } public IEnumerable GetCachedFilePaths() { List files = new List(); files.AddRange(_workspaceDirToSettings.Values.Select(x => x.ConfigFile)); files.AddRange(_projectKeyToSettings.Keys); return files; } } }