diff --git a/xcom2-launcher/xcom2-launcher/Classes/Mod/ModList.cs b/xcom2-launcher/xcom2-launcher/Classes/Mod/ModList.cs index 086f85d..8cf8d3a 100644 --- a/xcom2-launcher/xcom2-launcher/Classes/Mod/ModList.cs +++ b/xcom2-launcher/xcom2-launcher/Classes/Mod/ModList.cs @@ -111,9 +111,9 @@ private void UpdateModsConflictState(IEnumerable activeConflicts) /// /// /// - public void UpdatedModDependencyState(ModEntry mod) + public async Task UpdatedModDependencyStateAsync(ModEntry mod) { - var requiredMods = GetRequiredMods(mod, true, true); + var requiredMods = await GetRequiredModsAsync(mod, true, true).ConfigureAwait(false); var allRequiredModsAvailable = requiredMods.All(m => m.WorkshopID != 0 && m.isActive && !m.State.HasFlag(ModState.NotInstalled) && !m.State.HasFlag(ModState.NotLoaded)); if (allRequiredModsAvailable) @@ -314,10 +314,13 @@ public async Task> UpdateModAsync(ModEntry m, Settings settings) steamModsCopy = steamModsCopy.Skip(Workshop.MAX_UGC_RESULTS).ToList(); Log.Debug($"Creating SteamUGCDetails_t batch request for {batchQueryModList.Count} mods."); - - getDetailsTasks.Add(Task.Run(() => + + getDetailsTasks.Add(GetDetailsTask()); + continue; + + async Task> GetDetailsTask() { - var details = Workshop.GetDetails(batchQueryModList.ConvertAll(mod => (ulong) mod.WorkshopID), true); + var details = await Workshop.GetDetailsAsync(batchQueryModList.ConvertAll(mod => (ulong)mod.WorkshopID), true).ConfigureAwait(false); if (details == null) { @@ -336,7 +339,10 @@ public async Task> UpdateModAsync(ModEntry m, Settings settings) return null; } - updateTasks.Add(Task.Run(() => + updateTasks.Add(UpdateTask()); + continue; + + async Task UpdateTask() { // A requested workshop detail may match more than one mod (having the same mod installed from Steam and locally for example). var matchingMods = batchQueryModList.FindAll(mod => (ulong)mod.WorkshopID == workshopDetails.m_nPublishedFileId.m_PublishedFileId); @@ -350,15 +356,12 @@ public async Task> UpdateModAsync(ModEntry m, Settings settings) return; } - lock (_ModUpdateLock) - { - progress?.Report(new ModUpdateProgress($"Updating mods {steamProgress}/{totalModCount}...", steamProgress, totalModCount)); - Interlocked.Increment(ref steamProgress); - } + progress?.Report(new ModUpdateProgress($"Updating mods {steamProgress}/{totalModCount}...", steamProgress, totalModCount)); + Interlocked.Increment(ref steamProgress); try { - UpdateSteamMod(m, workshopDetails); + await UpdateSteamModAsync(m, workshopDetails).ConfigureAwait(false); } catch (Exception ex) { @@ -367,14 +370,13 @@ public async Task> UpdateModAsync(ModEntry m, Settings settings) throw; } } - - }, cancelToken)); + } } try { Log.Debug($"Waiting for {updateTasks.Count} UpdateSteamMod tasks to complete."); - Task.WaitAll(updateTasks.ToArray(), cancelToken); + await Task.WhenAny(Task.WhenAll(updateTasks), Task.Delay(Timeout.Infinite, cancelToken)).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -384,11 +386,11 @@ public async Task> UpdateModAsync(ModEntry m, Settings settings) Log.Debug("UpdateSteamMod tasks completed."); return details; - }, cancelToken)); + } } Log.Debug($"Waiting for {getDetailsTasks.Count} GetDetails tasks to complete."); - await Task.WhenAll(getDetailsTasks); + await Task.WhenAll(getDetailsTasks).ConfigureAwait(false); Log.Debug("GetDetails tasks completed."); var totalProgress = steamProgress; @@ -481,7 +483,7 @@ void UpdateLocalMod(ModEntry m) // Update directory size // slow, but necessary ? m.RealizeSize(Directory.EnumerateFiles(m.Path, "*", SearchOption.AllDirectories).Sum(fileName => new FileInfo(fileName).Length)); - + // Update Name and Description // look for .XComMod file try @@ -503,7 +505,7 @@ void UpdateLocalMod(ModEntry m) } - void UpdateSteamMod(ModEntry m, SteamUGCDetails_t workshopDetails) + async Task UpdateSteamModAsync(ModEntry m, SteamUGCDetails_t workshopDetails) { if (m == null || m.WorkshopID <= 0) { @@ -558,12 +560,12 @@ void UpdateSteamMod(ModEntry m, SteamUGCDetails_t workshopDetails) // If the mod has dependencies, request the workshop id's of those mods. if (workshopDetails.m_unNumChildren > 0) { - var dependencies = Workshop.GetDependencies(workshopDetails); - + var dependencies = await Workshop.GetDependenciesAsync(workshopDetails).ConfigureAwait(false); + if (dependencies != null) { m.Dependencies.Clear(); - m.Dependencies.AddRange(dependencies.ConvertAll(val => (long) val)); + m.Dependencies.AddRange(dependencies.Select(x => (long)x)); } else { @@ -578,6 +580,8 @@ void UpdateSteamMod(ModEntry m, SteamUGCDetails_t workshopDetails) m.AddState(ModState.UpdateAvailable); } + await UpdatedModDependencyStateAsync(m).ConfigureAwait(false); + // Check if it is built for WOTC try { @@ -590,7 +594,6 @@ void UpdateSteamMod(ModEntry m, SteamUGCDetails_t workshopDetails) Log.Error("Failed parsing XComMod file for " + m.ID, ex); Debug.Fail(ex.Message); } - } public string GetCategory(ModEntry mod) @@ -604,10 +607,22 @@ public string GetCategory(ModEntry mod) /// /// If set to false, dependencies are checked against the workshop ID. Otherwise, the mod ID is used which also matches for duplicates. /// - public List GetDependentMods(ModEntry mod, bool compareModId = true) + public async Task> GetDependentModsAsync(ModEntry mod, bool compareModId = true) { + var result = new List(); if (compareModId) - return All.Where(m => GetRequiredMods(m).Select(requiredMod => requiredMod.ID).Contains(mod.ID)).ToList(); + { + foreach (var modEntry in All) + { + var requiredMods = await GetRequiredModsAsync(modEntry).ConfigureAwait(false); + if (requiredMods.Any(x => x.ID == modEntry.ID)) + { + result.Add(modEntry); + } + } + + return result; + } return All.Where(m => m.Dependencies.Contains(mod.WorkshopID)).ToList(); } @@ -619,7 +634,7 @@ public List GetDependentMods(ModEntry mod, bool compareModId = true) /// If set to true, the primary duplicate will be returned if the real dependency is a disabled duplicate. /// If set to true, dependencies that have been set to be ignored are not returned. /// - public List GetRequiredMods(ModEntry mod, bool substituteDuplicates = true, bool checkIgnoredDependencies = false) + public async Task> GetRequiredModsAsync(ModEntry mod, bool substituteDuplicates = true, bool checkIgnoredDependencies = false) { List requiredMods = new List(); var installedSteamMods = All.Where(m => m.WorkshopID != 0).ToList(); @@ -663,7 +678,7 @@ public List GetRequiredMods(ModEntry mod, bool substituteDuplicates = } else { - var details = Workshop.GetDetails((ulong)id); + var details = await Workshop.GetDetailsAsync((ulong)id).ConfigureAwait(false); if (details.m_eResult == EResult.k_EResultOK) { diff --git a/xcom2-launcher/xcom2-launcher/Classes/Steam/DownloadItemRequest.cs b/xcom2-launcher/xcom2-launcher/Classes/Steam/DownloadItemRequest.cs deleted file mode 100644 index 0c64551..0000000 --- a/xcom2-launcher/xcom2-launcher/Classes/Steam/DownloadItemRequest.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Steamworks; -using XCOM2Launcher.Classes.Steam; - -namespace XCOM2Launcher.Steam -{ - public class DownloadItemRequest - { - // ReSharper disable once NotAccessedField.Local - private readonly Callback _callback; - private readonly ulong _id; - - public DownloadItemRequest(ulong id, Callback.DispatchDelegate callback) - { - _id = id; - _callback = Callback.Create(callback); - } - - public void Send() - { - SteamAPIWrapper.Init(); - SteamUGC.DownloadItem(new PublishedFileId_t(_id), true); - } - } -} \ No newline at end of file diff --git a/xcom2-launcher/xcom2-launcher/Classes/Steam/ItemDetailsRequest.cs b/xcom2-launcher/xcom2-launcher/Classes/Steam/ItemDetailsRequest.cs deleted file mode 100644 index 6d16087..0000000 --- a/xcom2-launcher/xcom2-launcher/Classes/Steam/ItemDetailsRequest.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using Microsoft.VisualBasic.Logging; -using Newtonsoft.Json; -using Steamworks; -using XCOM2Launcher.Classes.Steam; - -namespace XCOM2Launcher.Steam -{ - public class ItemDetailsRequest - { - [JsonIgnore] - private static readonly log4net.ILog Log = log4net.LogManager.GetLogger(nameof(ItemDetailsRequest)); - - private CallResult _onQueryCompleted; - private UGCQueryHandle_t _queryHandle; - - public List Identifiers { get; } - public bool GetFullDescription { get; } - public bool Success { get; private set; } - public bool Finished { get; private set; } - public bool Cancelled { get; private set; } - public List Result { get; private set; } - - public ItemDetailsRequest(ulong id, bool GetDesc = false) : this(new List() { id }, GetDesc) { - } - - public ItemDetailsRequest(List identifiers, bool GetDesc = false) - { - // Only pass distinct entries so that Identifiers.Count matches the number of returned queries when processing the result. - Identifiers = new List(); - Identifiers.AddRange(identifiers.Distinct()); - GetFullDescription = GetDesc; - } - - public ItemDetailsRequest Send() - { - SteamAPIWrapper.Init(); - - PublishedFileId_t[] idList = Identifiers.ConvertAll(id => new PublishedFileId_t(id)).ToArray(); - - _onQueryCompleted = CallResult.Create(QueryCompleted); - _queryHandle = SteamUGC.CreateQueryUGCDetailsRequest(idList, (uint)idList.Length); - - SteamUGC.SetReturnLongDescription(_queryHandle, GetFullDescription); - SteamUGC.SetReturnChildren(_queryHandle, true); // required, otherwise m_unNumChildren will always be 0 - - var apiCall = SteamUGC.SendQueryUGCRequest(_queryHandle); - _onQueryCompleted.Set(apiCall); - - return this; - } - - public bool Cancel() - { - Cancelled = true; - return !Finished; - } - - public ItemDetailsRequest WaitForResult() - { - // Wait for Response - while (!Finished && !Cancelled) - { - Thread.Sleep(10); - SteamAPIWrapper.RunCallbacks(); - } - - if (Cancelled) - return this; - - Result = new List(); - - for (uint i = 0; i < Identifiers.Count; i++) - { - // Retrieve Value - if (SteamUGC.GetQueryUGCResult(_queryHandle, i, out var result)) - { - Result.Add(result); - } - } - - SteamUGC.ReleaseQueryUGCRequest(_queryHandle); - return this; - } - - public string GetPreviewURL() - { - if (!Finished) - return null; - - SteamUGC.GetQueryUGCPreviewURL(_queryHandle, 0, out var url, 1000); - return url; - } - - private void QueryCompleted(SteamUGCQueryCompleted_t pCallback, bool bIOFailure) - { - Success = pCallback.m_eResult == EResult.k_EResultOK; - - if (!Success) - { - Log.Warn("SendQueryUGCRequest result was " + pCallback.m_eResult); - } - - Finished = true; - } - } -} \ No newline at end of file diff --git a/xcom2-launcher/xcom2-launcher/Classes/Steam/QueryUGCChildren.cs b/xcom2-launcher/xcom2-launcher/Classes/Steam/QueryUGCChildren.cs deleted file mode 100644 index d6d9f96..0000000 --- a/xcom2-launcher/xcom2-launcher/Classes/Steam/QueryUGCChildren.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using Steamworks; -using XCOM2Launcher.Classes.Steam; - -namespace XCOM2Launcher.Steam -{ - public class QueryUGCChildren - { - private CallResult _onQueryCompleted; - private UGCQueryHandle_t _queryHandle; - private readonly uint DependencyCount; - - public ulong Id { get; } - public bool Success { get; private set; } - public bool Finished { get; private set; } - public bool Cancelled { get; private set; } - public List Result { get; private set; } - - public QueryUGCChildren(ulong parentId, uint dependencyCount) - { - Id = parentId; - DependencyCount = dependencyCount; - Result = new List(); - } - - public QueryUGCChildren Send() - { - if (DependencyCount == 0) - return this; - - SteamAPIWrapper.Init(); - - _onQueryCompleted = CallResult.Create(QueryCompleted); - _queryHandle = SteamUGC.CreateQueryUGCDetailsRequest(new[] {Id.ToPublishedFileID()}, 1); - - //SteamUGC.SetReturnLongDescription(_queryHandle, GetFullDescription); - SteamUGC.SetReturnChildren(_queryHandle, true); - - var apiCall = SteamUGC.SendQueryUGCRequest(_queryHandle); - _onQueryCompleted.Set(apiCall); - - return this; - } - - public bool Cancel() - { - Cancelled = true; - return !Finished; - } - - public QueryUGCChildren WaitForResult() - { - if (DependencyCount == 0) - return this; - - // Wait for Response - while (!Finished && !Cancelled) - { - Thread.Sleep(10); - SteamAPIWrapper.RunCallbacks(); - } - - if (Cancelled) - return this; - - var idList = new PublishedFileId_t[DependencyCount]; - Success = SteamUGC.GetQueryUGCChildren(_queryHandle, 0, idList, (uint)idList.Length); - SteamUGC.ReleaseQueryUGCRequest(_queryHandle); - Result = idList.ToList().ConvertAll(item => item.m_PublishedFileId); - - return this; - } - - private void QueryCompleted(SteamUGCQueryCompleted_t pCallback, bool bIOFailure) - { - Finished = true; - } - } -} \ No newline at end of file diff --git a/xcom2-launcher/xcom2-launcher/Classes/Steam/SteamAPIWrapper.cs b/xcom2-launcher/xcom2-launcher/Classes/Steam/SteamAPIWrapper.cs deleted file mode 100644 index 6219457..0000000 --- a/xcom2-launcher/xcom2-launcher/Classes/Steam/SteamAPIWrapper.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Steamworks; - -namespace XCOM2Launcher.Classes.Steam -{ - /// - /// A hacky attempt to avoid race conditions caused by accessing the native Steam API from multiple threads by serializing access to it - /// - public static class SteamAPIWrapper - { - private static readonly object Mutex = new object(); - - public static void RunCallbacks() - { - lock (Mutex) - { - SteamAPI.RunCallbacks(); - } - } - - public static void Shutdown() - { - lock (Mutex) - { - SteamAPI.Shutdown(); - } - } - - public static bool Init() - { - lock (Mutex) - { - return SteamAPI.Init(); - } - } - - public static bool RestartAppIfNecessary(AppId_t unOwnAppID) - { - lock (Mutex) - { - return SteamAPI.RestartAppIfNecessary(unOwnAppID); - } - } - - - } -} diff --git a/xcom2-launcher/xcom2-launcher/Classes/Steam/SteamManager.cs b/xcom2-launcher/xcom2-launcher/Classes/Steam/SteamManager.cs new file mode 100644 index 0000000..75f283b --- /dev/null +++ b/xcom2-launcher/xcom2-launcher/Classes/Steam/SteamManager.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Steamworks; + +namespace XCOM2Launcher.Steam +{ + public static class SteamManager + { + private static bool _initialized; + private static readonly object _initlock = new object(); + private static readonly System.Timers.Timer _timer = new System.Timers.Timer(5); + + static SteamManager() + { + _timer.Elapsed += (sender, args) => + { + SteamAPI.RunCallbacks(); + }; + } + + public static bool IsSteamRunning() + { + return SteamAPI.IsSteamRunning(); + } + + public static bool EnsureInitialized() + { + if (_initialized) return true; + if (!SteamAPI.IsSteamRunning()) return false; + + lock (_initlock) + { + if (_initialized) return true; + + SteamAPI.Init(); + _timer.Start(); + _initialized = true; + } + + return true; + } + + public static void Shutdown() + { + if (!_initialized) return; + + lock (_initlock) + { + if (!_initialized) return; + + // stop timer before shutdown to avoid exceptions from steam + _timer.Stop(); + + SteamAPI.Shutdown(); + _initialized = false; + } + } + + public static Task QueryResultAsync(SteamAPICall_t apiCall, Func onCompleted) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var callResult = CallResult.Create(OnCompleted); + callResult.Set(apiCall); + + return tcs.Task; + + void OnCompleted(TCompleted completed, bool bIoFailure) + { + try + { + var result = onCompleted(completed, bIoFailure); + tcs.SetResult(result); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + } + } + } +} \ No newline at end of file diff --git a/xcom2-launcher/xcom2-launcher/Classes/Steam/Workshop.cs b/xcom2-launcher/xcom2-launcher/Classes/Steam/Workshop.cs index fdf8536..b24ea70 100644 --- a/xcom2-launcher/xcom2-launcher/Classes/Steam/Workshop.cs +++ b/xcom2-launcher/xcom2-launcher/Classes/Steam/Workshop.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Steamworks; -using XCOM2Launcher.Classes.Steam; namespace XCOM2Launcher.Steam { @@ -16,10 +16,14 @@ public static class Workshop /// public const int MAX_UGC_RESULTS = 50; // according to + static Workshop() + { + SteamManager.EnsureInitialized(); + _downloadItemCallback = Callback.Create(result => OnItemDownloaded?.Invoke(null, new DownloadItemEventArgs() { Result = result})); + } + public static ulong[] GetSubscribedItems() { - SteamAPIWrapper.Init(); - var num = SteamUGC.GetNumSubscribedItems(); var ids = new PublishedFileId_t[num]; SteamUGC.GetSubscribedItems(ids, num); @@ -29,15 +33,11 @@ public static ulong[] GetSubscribedItems() public static void Subscribe(ulong id) { - SteamAPIWrapper.Init(); - SteamUGC.SubscribeItem(id.ToPublishedFileID()); } public static void Unsubscribe(ulong id) { - SteamAPIWrapper.Init(); - SteamUGC.UnsubscribeItem(id.ToPublishedFileID()); } @@ -47,9 +47,9 @@ public static void Unsubscribe(ulong id) /// Workshop id /// Sets whether to return the full description for the item. If set to false, the description is truncated at 255 bytes. /// The requested data or the default struct (check for m_eResult == EResultNone), if the request failed. - public static SteamUGCDetails_t GetDetails(ulong id, bool getFullDescription = false) + public static async Task GetDetailsAsync(ulong id, bool getFullDescription = false) { - var result = GetDetails(new List {id}, getFullDescription); + var result = await GetDetailsAsync(new List {id}, getFullDescription); return result?.FirstOrDefault() ?? new SteamUGCDetails_t(); } @@ -59,7 +59,7 @@ public static SteamUGCDetails_t GetDetails(ulong id, bool getFullDescription = f /// Workshop id's /// Sets whether to return the full description for the item. If set to false, the description is truncated at 255 bytes. /// The requested data or null, if the request failed. - public static List GetDetails(List identifiers, bool getFullDescription = false) + public static async Task> GetDetailsAsync(List identifiers, bool getFullDescription = false) { if (identifiers == null) throw new ArgumentNullException(nameof(identifiers)); @@ -67,40 +67,79 @@ public static List GetDetails(List identifiers, bool g if (identifiers.Count > MAX_UGC_RESULTS) throw new ArgumentException($"Max allowed number of identifiers is {MAX_UGC_RESULTS}."); - var request = new ItemDetailsRequest(identifiers, getFullDescription); - request.Send().WaitForResult(); + if (!SteamManager.IsSteamRunning()) return null; - return request.Success ? request.Result : null; + var idList = identifiers + .Where(x => x > 0) + .Distinct() + .Select(x => new PublishedFileId_t(x)) + .ToArray(); + if (idList.Length == 0) return new List(); + + var queryHandle = SteamUGC.CreateQueryUGCDetailsRequest(idList, (uint)idList.Length); + SteamUGC.SetReturnLongDescription(queryHandle, getFullDescription); + SteamUGC.SetReturnChildren(queryHandle, true); // required, otherwise m_unNumChildren will always be 0 + + var apiCall = SteamUGC.SendQueryUGCRequest(queryHandle); + + var results = await SteamManager.QueryResultAsync>(apiCall, + (result, ioFailure) => + { + var details = new List(); + + for (uint i = 0; i < result.m_unNumResultsReturned; i++) + { + // Retrieve Value + if (SteamUGC.GetQueryUGCResult(queryHandle, i, out var detail)) + { + details.Add(detail); + } + } + + SteamUGC.ReleaseQueryUGCRequest(queryHandle); + return details; + }); + return results; } - public static List GetDependencies(ulong workShopId, uint dependencyCount) + private static async Task GetDependenciesAsync(ulong workShopId, uint dependencyCount) { - if (dependencyCount <= 0) + if (dependencyCount <= 0) return Array.Empty(); + if (workShopId <= 0) return Array.Empty(); + if (!SteamManager.IsSteamRunning()) return Array.Empty(); + + var queryHandle = SteamUGC.CreateQueryUGCDetailsRequest(new[] { workShopId.ToPublishedFileID() }, 1); + SteamUGC.SetReturnChildren(queryHandle, true); + var apiCall = SteamUGC.SendQueryUGCRequest(queryHandle); + + var results = await SteamManager.QueryResultAsync(apiCall, (result, ioFailure) => { - return new List(); - } + // todo: implement paged results, max is 50, see: https://partner.steamgames.com/doc/api/ISteamUGC#kNumUGCResultsPerPage + var idList = new PublishedFileId_t[dependencyCount]; + var success = SteamUGC.GetQueryUGCChildren(queryHandle, 0, idList, (uint)idList.Length); - QueryUGCChildren request = new QueryUGCChildren(workShopId, dependencyCount); - request.Send().WaitForResult(); - - return request.Success ? request.Result : null; + if (!success) return Array.Empty(); + + var resultIds = idList.Select(item => item.m_PublishedFileId).ToArray(); + return resultIds; + + }); + + return results; } - public static List GetDependencies(SteamUGCDetails_t details) + public static Task GetDependenciesAsync(SteamUGCDetails_t details) { - return GetDependencies(details.m_nPublishedFileId.m_PublishedFileId, details.m_unNumChildren); + return GetDependenciesAsync(details.m_nPublishedFileId.m_PublishedFileId, details.m_unNumChildren); } public static EItemState GetDownloadStatus(ulong id) { - SteamAPIWrapper.Init(); return (EItemState)SteamUGC.GetItemState(new PublishedFileId_t(id)); } public static InstallInfo GetInstallInfo(ulong id) { - SteamAPIWrapper.Init(); - ulong punSizeOnDisk; string pchFolder; uint punTimeStamp; @@ -118,8 +157,6 @@ public static InstallInfo GetInstallInfo(ulong id) public static UpdateInfo GetDownloadInfo(ulong id) { - SteamAPIWrapper.Init(); - ulong punBytesProcessed; ulong punBytesTotal; @@ -146,37 +183,26 @@ public class DownloadItemEventArgs : EventArgs public static event DownloadItemHandler OnItemDownloaded; public static void DownloadItem(ulong id) { - _downloadItemCallback = Callback.Create(ItemDownloaded); SteamUGC.DownloadItem(new PublishedFileId_t(id), true); } - private static void ItemDownloaded(DownloadItemResult_t result) - { - // Make sure someone is listening to event - if (OnItemDownloaded == null) return; - - DownloadItemEventArgs args = new DownloadItemEventArgs { Result = result }; - OnItemDownloaded(null, args); - } - #endregion public static string GetUsername(ulong steamID) { - System.Threading.ManualResetEvent work_done = new System.Threading.ManualResetEvent(false); - var _profileCallback = Callback.Create(delegate (PersonaStateChange_t result) - { - work_done.Set(); - }); - bool success = SteamFriends.RequestUserInformation(new CSteamID(steamID), true); - - if (success) + var work_done = new System.Threading.ManualResetEventSlim(false); + using (Callback.Create(result => { work_done.Set(); })) { - work_done.WaitOne(5000); - work_done.Reset(); + bool success = SteamFriends.RequestUserInformation(new CSteamID(steamID), true); + if (success) + { + work_done.Wait(5000); + work_done.Reset(); + return SteamFriends.GetFriendPersonaName(new CSteamID(steamID)) ?? string.Empty; + } + + return string.Empty; } - - return SteamFriends.GetFriendPersonaName(new CSteamID(steamID)); } } } \ No newline at end of file diff --git a/xcom2-launcher/xcom2-launcher/Classes/XCOM/Xcom2Env.cs b/xcom2-launcher/xcom2-launcher/Classes/XCOM/Xcom2Env.cs index 72b4d72..497b279 100644 --- a/xcom2-launcher/xcom2-launcher/Classes/XCOM/Xcom2Env.cs +++ b/xcom2-launcher/xcom2-launcher/Classes/XCOM/Xcom2Env.cs @@ -3,7 +3,7 @@ using System.Diagnostics; using System.IO; using System.Windows.Forms; -using XCOM2Launcher.Classes.Steam; +using XCOM2Launcher.Steam; namespace XCOM2Launcher.XCOM { @@ -56,7 +56,7 @@ private void RunVanilla(string gameDir, string args) { Log.Info("Starting XCOM 2 (vanilla)"); - if (!SteamAPIWrapper.Init()) + if (!SteamManager.IsSteamRunning()) MessageBox.Show("Could not connect to steam.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); var p = new Process @@ -78,8 +78,6 @@ private void RunVanilla(string gameDir, string args) Log.Warn("Failed to start game process", ex); MessageBox.Show("An error occured while trying to run the game. " + ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } - - SteamAPIWrapper.Shutdown(); } /// @@ -91,7 +89,7 @@ private void RunWotC(string gameDir, string args) { Log.Info("Starting WotC"); - if (!SteamAPIWrapper.Init()) + if (!SteamManager.IsSteamRunning()) MessageBox.Show("Could not connect to steam.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); var p = new Process @@ -113,10 +111,6 @@ private void RunWotC(string gameDir, string args) Log.Warn("Failed to start game process", ex); MessageBox.Show("An error occured while trying to run the game. " + ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } - - SteamAPIWrapper.Shutdown(); } - - } } \ No newline at end of file diff --git a/xcom2-launcher/xcom2-launcher/Classes/XCOM/XcomChimeraSquadEnv.cs b/xcom2-launcher/xcom2-launcher/Classes/XCOM/XcomChimeraSquadEnv.cs index 076d2e4..32ef0a4 100644 --- a/xcom2-launcher/xcom2-launcher/Classes/XCOM/XcomChimeraSquadEnv.cs +++ b/xcom2-launcher/xcom2-launcher/Classes/XCOM/XcomChimeraSquadEnv.cs @@ -3,7 +3,7 @@ using System.Diagnostics; using System.IO; using System.Windows.Forms; -using XCOM2Launcher.Classes.Steam; +using XCOM2Launcher.Steam; using XCOM2Launcher.XCOM; namespace XCOM2Launcher.Classes @@ -30,7 +30,7 @@ public override void RunGame(string gameDir, string args) { Log.Info("Starting XCOM Chimera Squad"); - if (!SteamAPIWrapper.Init()) + if (!SteamManager.IsSteamRunning()) MessageBox.Show("Could not connect to steam.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); var p = new Process @@ -52,8 +52,6 @@ public override void RunGame(string gameDir, string args) Log.Warn("Failed to start game process", ex); MessageBox.Show("An error occured while trying to run the game. " + ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } - - SteamAPIWrapper.Shutdown(); } } diff --git a/xcom2-launcher/xcom2-launcher/Forms/MainForm.Events.cs b/xcom2-launcher/xcom2-launcher/Forms/MainForm.Events.cs index 9134e3c..3bd6dc1 100644 --- a/xcom2-launcher/xcom2-launcher/Forms/MainForm.Events.cs +++ b/xcom2-launcher/xcom2-launcher/Forms/MainForm.Events.cs @@ -37,7 +37,7 @@ internal void RegisterEvents() Save(Settings.Instance.LastLaunchedWotC); }; - reloadToolStripMenuItem.Click += delegate + reloadToolStripMenuItem.Click += async delegate { Log.Info("Menu->File->Reset settings"); // Confirmation dialog @@ -886,11 +886,11 @@ private void bClearStateFilter_Click(object sender, EventArgs e) cFilterNotLoaded.Checked = false; } - private void cShowPrimaryDuplicates_CheckedChanged(object sender, EventArgs e) + private async void cShowPrimaryDuplicates_CheckedChanged(object sender, EventArgs e) { if (modlist_ListObjectListView.SelectedObject is ModEntry mod) { - UpdateDependencyInformation(mod); + await UpdateDependencyInformationAsync(mod); } } diff --git a/xcom2-launcher/xcom2-launcher/Forms/MainForm.ModList.cs b/xcom2-launcher/xcom2-launcher/Forms/MainForm.ModList.cs index 0347c41..e7b51bf 100644 --- a/xcom2-launcher/xcom2-launcher/Forms/MainForm.ModList.cs +++ b/xcom2-launcher/xcom2-launcher/Forms/MainForm.ModList.cs @@ -383,7 +383,7 @@ private void UpdateMods(List mods, Action afterUpdateAction = null) ModUpdateCancelSource = new CancellationTokenSource(); ModUpdateTask = Settings.Mods.UpdateModsAsync(mods, Settings, reporter, ModUpdateCancelSource.Token); - ModUpdateTask.ContinueWith(e => + ModUpdateTask.ContinueWith(async e => { switch (e.Status) { @@ -420,12 +420,13 @@ private void UpdateMods(List mods, Action afterUpdateAction = null) throw new ArgumentOutOfRangeException(); } }, TaskScheduler.FromCurrentSynchronizationContext()); + + return; void PostProcessModUpdateTask() { Cursor.Current = Cursors.WaitCursor; - // After an update refresh all mods that depend on this one - mods.ForEach(updatedMod => Mods.GetDependentMods(updatedMod).ForEach(dependentMod => Mods.UpdatedModDependencyState(dependentMod))); + modlist_ListObjectListView.RefreshObjects(mods); afterUpdateAction?.Invoke(); @@ -435,7 +436,7 @@ void PostProcessModUpdateTask() } } - void DeleteMods(List mods, bool keepEntries) + async Task DeleteMods(List mods, bool keepEntries) { // Delete / unsubscribe foreach (var mod in mods) @@ -443,7 +444,7 @@ void DeleteMods(List mods, bool keepEntries) Log.Info("Deleting mod " + mod.ID); // Set State for all mods that depend on this one to MissingDependencies - var dependentMods = Mods.GetDependentMods(mod); + var dependentMods = await Mods.GetDependentModsAsync(mod); dependentMods.ForEach(m => { m.SetState(ModState.MissingDependencies); @@ -521,7 +522,7 @@ private void ResubscribeToMods(List mods) MessageBox.Show($"You will have to wait for the download{plural} to finish in order to use the mod{plural}.", "Info", MessageBoxButtons.OK, MessageBoxIcon.Information); } - private void ConfirmDeleteMods(List mods) + private async Task ConfirmDeleteMods(List mods) { if (mods == null || !mods.Any()) { @@ -538,7 +539,7 @@ private void ConfirmDeleteMods(List mods) if (result != DialogResult.OK) return; - DeleteMods(mods, false); + await DeleteMods(mods, false); } private void ConfirmUnsubscribeMods(List mods) @@ -867,7 +868,7 @@ private ContextMenuStrip CreateModListContextMenu(ModEntry m, OLVListItem curren if (!m.State.HasFlag(ModState.DuplicatePrimary)) { disableDuplicates = new ToolStripMenuItem("Prefer this duplicate"); - disableDuplicates.Click += delegate + disableDuplicates.Click += async delegate { // disable all other duplicates foreach (var duplicate in duplicateMods) @@ -887,14 +888,14 @@ private ContextMenuStrip CreateModListContextMenu(ModEntry m, OLVListItem curren m.AddState(ModState.DuplicatePrimary); m.isActive = true; modlist_ListObjectListView.RefreshObject(m); - ProcessModListItemCheckChanged(m); + await ProcessModListItemCheckChangedAsync (m); }; } if (m.State.HasFlag(ModState.DuplicatePrimary) || m.State.HasFlag(ModState.DuplicateDisabled)) { restoreDuplicates = new ToolStripMenuItem("Restore duplicates"); - restoreDuplicates.Click += delegate + restoreDuplicates.Click += async delegate { // restore normal duplicate state foreach (var duplicate in duplicateMods) @@ -914,7 +915,7 @@ private ContextMenuStrip CreateModListContextMenu(ModEntry m, OLVListItem curren m.AddState(ModState.DuplicateID); m.isActive = false; modlist_ListObjectListView.RefreshObject(m); - ProcessModListItemCheckChanged(m); + await ProcessModListItemCheckChangedAsync(m); }; } } @@ -1208,7 +1209,7 @@ bool ProcessNewModState(ModEntry mod, bool newState) return newState; } - void ProcessModListItemCheckChanged(ModEntry modChecked) + async Task ProcessModListItemCheckChangedAsync(ModEntry modChecked) { //Debug.WriteLine("ProcessModListItemCheckChanged " + modChecked.Name); @@ -1216,7 +1217,8 @@ void ProcessModListItemCheckChanged(ModEntry modChecked) if (modChecked.isActive && Settings.OnlyUpdateEnabledOrNewModsOnStartup && !_CheckTriggeredFromContextMenu) { Log.Info($"Updating mod before enabling because {nameof(Settings.OnlyUpdateEnabledOrNewModsOnStartup)} is enabled"); - Task.Run(() => Mods.UpdateModAsync(modChecked, Settings)).Wait(); + + await Task.Run(() => Mods.UpdateModAsync(modChecked, Settings)); } _CheckTriggeredFromContextMenu = false; @@ -1248,14 +1250,17 @@ void ProcessModListItemCheckChanged(ModEntry modChecked) modlist_ListObjectListView.RefreshObject(mod); // refresh dependent mods - var dependentMods = Mods.GetDependentMods(mod); - dependentMods.ForEach(m => Mods.UpdatedModDependencyState(m)); + var dependentMods = await Mods.GetDependentModsAsync(mod); + foreach (var m in dependentMods) + { + await Mods.UpdatedModDependencyStateAsync(m); + } modlist_ListObjectListView.RefreshObjects(dependentMods); } - UpdateDependencyInformation(ModList.SelectedObject); UpdateStateFilterLabels(); UpdateLabels(); + await UpdateDependencyInformationAsync(ModList.SelectedObject); } #region Events @@ -1314,16 +1319,16 @@ private bool ModListBooleanCheckStatePutter(object rowobject, bool newValue) // will not fire and we have to process the new state manually if (!ModList.Objects.Contains(mod)) { - ProcessModListItemCheckChanged(mod); + _ = ProcessModListItemCheckChangedAsync(mod); } return newValue; } - private void ModListItemChecked(object sender, ItemCheckedEventArgs e) + private async void ModListItemChecked(object sender, ItemCheckedEventArgs e) { var mod = ModList.GetModelObject(e.Item.Index); - ProcessModListItemCheckChanged(mod); + await ProcessModListItemCheckChangedAsync(mod); } private void ModListItemCheck(object sender, ItemCheckEventArgs e) @@ -1346,11 +1351,11 @@ private void ModListItemCheck(object sender, ItemCheckEventArgs e) } } - private void ModListSelectionChanged(object sender, EventArgs e) + private async void ModListSelectionChanged(object sender, EventArgs e) { CurrentMod = ModList.SelectedObjects.Count != 1 ? null : ModList.SelectedObject; - UpdateModInfo(CurrentMod); + await UpdateModInfoAsync(CurrentMod); if (CurrentMod != null) { diff --git a/xcom2-launcher/xcom2-launcher/Forms/MainForm.cs b/xcom2-launcher/xcom2-launcher/Forms/MainForm.cs index 0ac5376..ea9e2ba 100644 --- a/xcom2-launcher/xcom2-launcher/Forms/MainForm.cs +++ b/xcom2-launcher/xcom2-launcher/Forms/MainForm.cs @@ -8,11 +8,10 @@ using System.Threading.Tasks; using System.Windows.Forms; using BrightIdeasSoftware; -using XCOM2Launcher.Classes.Steam; using XCOM2Launcher.Mod; using XCOM2Launcher.XCOM; using JR.Utils.GUI.Forms; -using Timer = System.Windows.Forms.Timer; +using XCOM2Launcher.Steam; namespace XCOM2Launcher.Forms { @@ -36,7 +35,6 @@ public MainForm(Settings settings) aboutToolStripMenuItem.DropDownDirection = ToolStripDropDownDirection.BelowLeft; // Settings - SteamAPIWrapper.Init(); Settings = settings; // Restore states @@ -45,7 +43,7 @@ public MainForm(Settings settings) // Init interface InitModListView(); InitDependencyListViews(); - UpdateInterface(); + RegisterEvents(); // Other intialization @@ -69,12 +67,6 @@ public MainForm(Settings settings) }); #endif - // Run callbacks - var t1 = new Timer(); - t1.Tick += (sender, e) => { SteamAPIWrapper.RunCallbacks(); }; - t1.Interval = 10; - t1.Start(); - /* // Check for running downloads #if DEBUG @@ -94,6 +86,7 @@ public MainForm(Settings settings) private void MainForm_Load(object sender, EventArgs e) { Text += " " + Program.GetCurrentVersionString(true); + _ = UpdateInterfaceAsync(); } private void InitializeTabImages() @@ -233,6 +226,7 @@ private void SetStatusIdle() } status_toolstrip_label.Text = "Ready."; + progress_toolstrip_progressbar.Style = ProgressBarStyle.Continuous; progress_toolstrip_progressbar.Visible = false; } @@ -453,14 +447,23 @@ private void RunChallengeMode() #region Interface updates - private void UpdateInterface() + private async Task UpdateInterfaceAsync() { + SetStatus("Loading mod information..."); + progress_toolstrip_progressbar.Visible = true; + progress_toolstrip_progressbar.Style = ProgressBarStyle.Marquee; + error_provider.Clear(); - + UpdateConflictInfo(); - UpdateModInfo(modlist_ListObjectListView.SelectedObject as ModEntry); + await UpdateModInfoAsync(modlist_ListObjectListView.SelectedObject as ModEntry); UpdateLabels(); UpdateStateFilterLabels(); + + var updateDepsTasks = Settings.Mods.All.Select(mod => Settings.Mods.UpdatedModDependencyStateAsync(mod)); + await Task.WhenAll(updateDepsTasks); + + SetStatusIdle(); } private void UpdateLabels() @@ -675,23 +678,23 @@ private void UpdateModDescription(ModEntry m) btnDescUndo.Enabled = false; } - private void UpdateDependencyInformation(ModEntry m) + private async Task UpdateDependencyInformationAsync(ModEntry m) { if (m == null) return; // update dependency information olvRequiredMods.ClearObjects(); - olvRequiredMods.AddObjects(Mods.GetRequiredMods(m, cShowPrimaryDuplicates.Checked)); + olvRequiredMods.AddObjects(await Mods.GetRequiredModsAsync(m, cShowPrimaryDuplicates.Checked)); olvDependentMods.ClearObjects(); - olvDependentMods.AddObjects(Mods.GetDependentMods(m, false)); + olvDependentMods.AddObjects(await Mods.GetRequiredModsAsync(m, false)); } /// /// Update mod information panel with data from specified mod. /// /// - private void UpdateModInfo(ModEntry m) + private async Task UpdateModInfoAsync(ModEntry m) { if (m == null) { @@ -726,8 +729,7 @@ private void UpdateModInfo(ModEntry m) UpdateModChangeLog(m); modinfo_readme_RichTextBox.Text = m.GetReadMe(); modinfo_image_picturebox.ImageLocation = m.Image; - UpdateDependencyInformation(m); - + // Init handler for property changes var sel_obj = m.GetProperty(); @@ -759,6 +761,8 @@ private void UpdateModInfo(ModEntry m) } #endregion + + await UpdateDependencyInformationAsync(m); } /// @@ -801,7 +805,7 @@ private void InitDependencyListViews() return CurrentMod.IgnoredDependencies.Contains(mod.WorkshopID); }; - olvColReqModsIgnore.AspectPutter += (rowObject, value) => + olvColReqModsIgnore.AspectPutter += async (rowObject, value) => { if (CurrentMod == null || !(rowObject is ModEntry mod) || !(value is bool checkState)) return; @@ -822,7 +826,7 @@ private void InitDependencyListViews() } } - Mods.UpdatedModDependencyState(CurrentMod); + await Mods.UpdatedModDependencyStateAsync(CurrentMod); modlist_ListObjectListView.RefreshObject(CurrentMod); }; diff --git a/xcom2-launcher/xcom2-launcher/Program.cs b/xcom2-launcher/xcom2-launcher/Program.cs index 0c224bc..0e1b439 100644 --- a/xcom2-launcher/xcom2-launcher/Program.cs +++ b/xcom2-launcher/xcom2-launcher/Program.cs @@ -6,13 +6,13 @@ using System.Reflection; using System.Text; using System.Threading; +using System.Threading.Tasks; using System.Windows.Forms; using Newtonsoft.Json; using Semver; using Sentry; using XCOM2Launcher.Classes; using XCOM2Launcher.Classes.Helper; -using XCOM2Launcher.Classes.Steam; using XCOM2Launcher.Forms; using XCOM2Launcher.Helper; using XCOM2Launcher.Mod; @@ -94,7 +94,7 @@ private static void Main() return; } - if (!SteamAPIWrapper.Init()) { + if (!SteamManager.EnsureInitialized()) { Log.Warn("Failed to detect Steam"); StringBuilder message = new StringBuilder(); @@ -107,6 +107,8 @@ private static void Main() } // Load settings + // Cannot make 'Main' async, Winforms doesn't support that well, and this code should probably run after + // we show the main window so we can show some progress var settings = InitializeSettings(); if (settings == null) { @@ -135,10 +137,10 @@ private static void Main() } Application.Run(new MainForm(settings)); - SteamAPIWrapper.Shutdown(); } finally { + SteamManager.Shutdown(); Log.Info("Shutting down..."); sentrySdkInstance?.Dispose(); GlobalSettings.Instance.Save(); @@ -421,8 +423,6 @@ public static Settings InitializeSettings() { mod.EnableModFile(); } - - settings.Mods.UpdatedModDependencyState(mod); } var newMissingMods = settings.Mods.All.Where(m => (m.State.HasFlag(ModState.NotLoaded) || m.State.HasFlag(ModState.NotInstalled)) && diff --git a/xcom2-launcher/xcom2-launcher/Steamworks.NET.dll b/xcom2-launcher/xcom2-launcher/Steamworks.NET.dll index 937f1b4..49b51c6 100644 Binary files a/xcom2-launcher/xcom2-launcher/Steamworks.NET.dll and b/xcom2-launcher/xcom2-launcher/Steamworks.NET.dll differ diff --git a/xcom2-launcher/xcom2-launcher/steam_api64.dll b/xcom2-launcher/xcom2-launcher/steam_api64.dll index c6e55cf..2b42812 100644 Binary files a/xcom2-launcher/xcom2-launcher/steam_api64.dll and b/xcom2-launcher/xcom2-launcher/steam_api64.dll differ diff --git a/xcom2-launcher/xcom2-launcher/xcom2-launcher.csproj b/xcom2-launcher/xcom2-launcher/xcom2-launcher.csproj index 6e974c2..d205bdf 100644 --- a/xcom2-launcher/xcom2-launcher/xcom2-launcher.csproj +++ b/xcom2-launcher/xcom2-launcher/xcom2-launcher.csproj @@ -86,6 +86,7 @@ ..\packages\Microsoft-WindowsAPICodePack-Shell.1.1.4\lib\net472\Microsoft.WindowsAPICodePack.Shell.dll + ..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll @@ -189,10 +190,7 @@ - - - - +