diff --git a/AcManager.Controls/AcManager.Controls.csproj b/AcManager.Controls/AcManager.Controls.csproj
index 1f185edba..549b4de5a 100644
--- a/AcManager.Controls/AcManager.Controls.csproj
+++ b/AcManager.Controls/AcManager.Controls.csproj
@@ -260,6 +260,12 @@
+
+ WorkshopAuthorized.xaml
+
+
+ WorkshopCurrentUserBlock.xaml
+
ModernPopup.xaml
@@ -405,6 +411,7 @@
MSBuild:Compile
Designer
+
MSBuild:Compile
Designer
@@ -556,6 +563,8 @@
Designer
MSBuild:Compile
+
+
@@ -580,6 +589,10 @@
AcManager.Tools
False
+
+ {abcd8f3f-5730-4fac-86ba-8a28086b75f8}
+ AcManager.Workshop
+
{38A04388-FC0C-4E96-BED5-1DC5C6C0B18F}
AcTools.LapTimes
@@ -612,6 +625,9 @@
+
+
+
diff --git a/AcManager.Controls/Assets/IconData.xaml b/AcManager.Controls/Assets/IconData.xaml
index e9d365d02..adb83b551 100644
--- a/AcManager.Controls/Assets/IconData.xaml
+++ b/AcManager.Controls/Assets/IconData.xaml
@@ -2,6 +2,9 @@
+ F1 M 26.9204,19.0027L 28.504,19.0027C 29.3785,19.0027 30.0875,19.7116 30.0875,20.5862L 30.0875,55.4244C 30.0875,56.299 29.3785,57.008 28.504,57.008L 26.9204,57.008C 26.0459,57.008 25.3369,56.299 25.3369,55.4244L 25.3369,20.5862C 25.3369,19.7116 26.0459,19.0027 26.9204,19.0027 Z M 31.6711,23.7533C 33.2546,22.6976 34.8382,21.6419 37.4775,21.9059C 40.1167,22.1698 43.8117,23.7535 46.7148,23.7535C 49.618,23.7535 51.7294,22.1698 53.8408,20.5862L 53.8408,34.838C 51.7294,36.4216 49.618,38.0052 46.7148,38.0052C 43.8117,38.0052 40.1167,36.4216 37.4775,36.1576C 34.8382,35.8937 33.2546,36.9494 31.6711,38.0051L 31.6711,23.7533 Z
+ F1 M 29.9796,22.2C 33.4365,22.2 36.2389,25.0024 36.2389,28.4593C 36.2389,31.9162 33.4365,34.7185 29.9796,34.7185C 26.5228,34.7185 23.7204,31.9162 23.7204,28.4593C 23.7204,25.0024 26.5228,22.2 29.9796,22.2 Z M 23.7204,43.2315C 22.8858,43.6488 21.2167,46.987 21.2167,46.987C 21.2167,46.987 20.3821,48.2388 19.9648,53.2462L 16.2093,51.9944L 17.4611,45.7352C 17.4611,45.7352 19.9648,36.9722 24.9722,36.9723L 34.9869,36.9723C 39.9944,36.9722 42.4981,45.7352 42.4981,45.7352L 43.75,51.9945L 39.9944,53.2463C 39.5772,48.2389 38.7427,46.987 38.7427,46.987C 38.7427,46.987 37.0735,43.6488 36.2389,43.2315L 36.7447,54.6004C 34.8417,55.3427 32.7712,55.75 30.6056,55.75C 27.9657,55.75 25.4672,55.1447 23.2413,54.0654L 23.7204,43.2315 Z M 39,24L 57,24L 57,27L 39,27L 39,24 Z M 39,30L 57,30L 57,33L 39,33L 39,30 Z M 41,36L 57,36L 57,39L 41,39L 41,36 Z M 44,42L 57,42L 57,45L 44,45L 44,42 Z M 46,48L 57,48L 57,51L 46,51L 46,48 Z
+ F1 M 49.0833,33.25C 53.4555,33.25 57,36.7944 57,41.1666C 57,45.5389 53.3723,48.9999 49,49L 23,49.0001C 20.8139,49 19,47.3111 19,45.125C 19,43.1866 20.3931,41.5737 22.2327,41.233C 22.1892,40.9533 22.1667,40.6667 22.1667,40.3749C 22.1667,37.3993 24.5122,34.9712 27.4553,34.8389C 28.7579,31.1462 32.2782,28.4999 36.4167,28.4999C 40.3458,28.4999 43.7179,30.8853 45.1637,34.2868C 46.3193,33.627 47.6573,33.25 49.0833,33.25 Z
F1 M 39,46L 46,46L 46,39L 51,39L 51,46L 58,46L 58,51L 51,51L 51,58L 46,58L 46,51L 39,51L 39,46 Z M 31,25L 38,25L 38,18L 43,18L 43,25L 50,25L 50,30L 43,30L 43,37L 38,37L 38,30L 31,30L 31,25 Z M 18,39L 25,39L 25,32L 30,32L 30,39L 37,39L 37,44L 30,44L 30,51L 25,51L 25,44L 18,44L 18,39 Z
F1 M 58.5833,45.9167L 58.5833,57L 19,57L 19,45.9167L 28.8998,45.9167C 31.0306,47.2793 33.6795,48.4476 36.6434,49.2418C 40.3467,50.2341 44.7079,50.4746 47.6249,50.0692L 47.6249,45.9167L 58.5833,45.9167 Z M 56.2083,48.2917L 53.4374,48.2917L 53.4374,53.0417L 56.2082,53.0417L 56.2083,48.2917 Z M 45.7433,48.2824C 43.1578,48.6418 39.9938,48.0327 36.7113,47.1532C 31.1866,45.6728 26.8965,42.7258 25.5163,39.7952L 32.2902,33.0213L 45.7433,38.0663L 45.7433,48.2824 Z M 39.2905,28.1719C 39.6351,28.1719 39.9737,28.1941 40.3047,28.2368L 46.0048,17.0498L 49.0712,18.6122L 43.5443,29.4594C 45.1454,30.5622 46.1735,32.2733 46.1735,34.1946C 46.1735,35.0197 45.9839,35.806 45.6407,36.5221L 33.4151,31.7762C 34.4796,29.6541 36.4722,28.1719 39.2905,28.1719 Z
F1 M 57,42L 57,34L 32.25,34L 42.25,24L 31.75,24L 17.75,38L 31.75,52L 42.25,52L 32.25,42L 57,42 Z
diff --git a/AcManager.Controls/Assets/ModernUI.Default.xaml b/AcManager.Controls/Assets/ModernUI.Default.xaml
index a32a37362..03d5b0261 100644
--- a/AcManager.Controls/Assets/ModernUI.Default.xaml
+++ b/AcManager.Controls/Assets/ModernUI.Default.xaml
@@ -42,7 +42,7 @@
-
+
diff --git a/AcManager.Controls/Assets/WorkshopResources.xaml b/AcManager.Controls/Assets/WorkshopResources.xaml
new file mode 100644
index 000000000..976070578
--- /dev/null
+++ b/AcManager.Controls/Assets/WorkshopResources.xaml
@@ -0,0 +1,380 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AcManager.Controls/Helpers/SharingUiHelper.cs b/AcManager.Controls/Helpers/SharingUiHelper.cs
index 24c3bd7ea..159d8f426 100644
--- a/AcManager.Controls/Helpers/SharingUiHelper.cs
+++ b/AcManager.Controls/Helpers/SharingUiHelper.cs
@@ -106,25 +106,39 @@ public static void ShowShared(string type, string link, bool showDialog) {
if (_custom?.ShowShared(type, link) == true) return;
if (SettingsHolder.Sharing.CopyLinkToClipboard) {
- ClipboardHelper.SetText(link);
+ try {
+ ClipboardHelper.SetText(link);
+ } catch (Exception e) {
+ Logging.Warning(e);
+ }
}
if (showDialog && SettingsHolder.Sharing.ShowSharedDialog) {
- var copied = SettingsHolder.Sharing.CopyLinkToClipboard && Clipboard.GetText() == link;
+ var copied = false;
+ try {
+ copied = SettingsHolder.Sharing.CopyLinkToClipboard && Clipboard.GetText() == link;
+ } catch (Exception e) {
+ Logging.Warning(e);
+ }
+
+ var buttons = copied ? new MessageDialogButton(MessageBoxButton.OKCancel) {
+ [MessageBoxResult.OK] = "Open link in browser",
+ [MessageBoxResult.Cancel] = "Close"
+ } : new MessageDialogButton(MessageBoxButton.YesNoCancel) {
+ [MessageBoxResult.Yes] = "Copy link to the clipboard",
+ [MessageBoxResult.No] = "Open link in browser",
+ [MessageBoxResult.Cancel] = "Close"
+ };
+
var response = MessageDialog.Show(copied
? $"Link has been copied to the clipboard:[br]{link}"
- : $"Here is the link:[br]{link}", type, new MessageDialogButton(copied ? MessageBoxButton.YesNoCancel : MessageBoxButton.OKCancel) {
- [MessageBoxResult.Yes] = "Copy link to the clipboard",
- [MessageBoxResult.No] = "Open link in browser",
- [MessageBoxResult.OK] = "Open link in browser",
- [MessageBoxResult.Cancel] = "Close"
- });
-
- if (response == MessageBoxResult.OK) {
+ : $"Here is the link:[br]{link}", type, buttons);
+
+ if (response == MessageBoxResult.OK || response == MessageBoxResult.Yes) {
ClipboardHelper.SetText(link);
}
- if (response == MessageBoxResult.Yes) {
+ if (response == MessageBoxResult.No) {
WindowsHelper.ViewInBrowser(link + "#noauto");
}
} else {
diff --git a/AcManager.Controls/UserControls/Workshop/WorkshopAuthorized.xaml b/AcManager.Controls/UserControls/Workshop/WorkshopAuthorized.xaml
new file mode 100644
index 000000000..86e89d2b0
--- /dev/null
+++ b/AcManager.Controls/UserControls/Workshop/WorkshopAuthorized.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AcManager.Controls/UserControls/Workshop/WorkshopAuthorized.xaml.cs b/AcManager.Controls/UserControls/Workshop/WorkshopAuthorized.xaml.cs
new file mode 100644
index 000000000..110b8d208
--- /dev/null
+++ b/AcManager.Controls/UserControls/Workshop/WorkshopAuthorized.xaml.cs
@@ -0,0 +1,7 @@
+namespace AcManager.Controls.UserControls.Workshop {
+ public partial class WorkshopAuthorized {
+ public WorkshopAuthorized() {
+ InitializeComponent();
+ }
+ }
+}
\ No newline at end of file
diff --git a/AcManager.Controls/UserControls/Workshop/WorkshopCurrentUserBlock.xaml b/AcManager.Controls/UserControls/Workshop/WorkshopCurrentUserBlock.xaml
new file mode 100644
index 000000000..5a2690fe2
--- /dev/null
+++ b/AcManager.Controls/UserControls/Workshop/WorkshopCurrentUserBlock.xaml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AcManager.Controls/UserControls/Workshop/WorkshopCurrentUserBlock.xaml.cs b/AcManager.Controls/UserControls/Workshop/WorkshopCurrentUserBlock.xaml.cs
new file mode 100644
index 000000000..79a4f7ac8
--- /dev/null
+++ b/AcManager.Controls/UserControls/Workshop/WorkshopCurrentUserBlock.xaml.cs
@@ -0,0 +1,9 @@
+using System.Windows.Controls;
+
+namespace AcManager.Controls.UserControls.Workshop {
+ public partial class WorkshopCurrentUserBlock : UserControl {
+ public WorkshopCurrentUserBlock() {
+ InitializeComponent();
+ }
+ }
+}
\ No newline at end of file
diff --git a/AcManager.Tools/AcObjectsNew/AcCommonObject.Pack.cs b/AcManager.Tools/AcObjectsNew/AcCommonObject.Pack.cs
index 697fa1218..e2cb5326a 100644
--- a/AcManager.Tools/AcObjectsNew/AcCommonObject.Pack.cs
+++ b/AcManager.Tools/AcObjectsNew/AcCommonObject.Pack.cs
@@ -35,6 +35,8 @@ public class AcCommonObjectPackerParams {
[CanBeNull]
public string Destination { get; set; }
+ public Func> Override { get; set; }
+
public bool ShowInExplorer { get; set; } = true;
[CanBeNull]
@@ -91,7 +93,13 @@ private bool Write(string name, Action fn) {
if (_added.Contains(lower)) return false;
_added.Add(lower);
_progress?.Report(key);
- fn?.Invoke(_writer, key);
+
+ var overrideFn = _packerParams?.Override?.Invoke(key);
+ if (overrideFn != null) {
+ overrideFn.Invoke(_writer, key);
+ } else {
+ fn?.Invoke(_writer, key);
+ }
return true;
}
@@ -112,7 +120,7 @@ private IEnumerable GetFiles(string mask) {
if (_subFiles == null) {
_subFiles = Directory.GetFiles(location, "*", SearchOption.AllDirectories)
- .Select(x => FileUtils.GetRelativePath(x, location).Replace('\\', '/')).ToArray();
+ .Select(x => FileUtils.GetRelativePath(x, location).Replace('\\', '/')).ToArray();
}
var f = RegexFromQuery.Create(mask.Replace('\\', '/'), StringMatchMode.CompleteMatch);
diff --git a/AcManager.Tools/GameProperties/WorldSimSeriesMark.cs b/AcManager.Tools/GameProperties/WorldSimSeriesMark.cs
index 70089f737..4c4c0a4f1 100644
--- a/AcManager.Tools/GameProperties/WorldSimSeriesMark.cs
+++ b/AcManager.Tools/GameProperties/WorldSimSeriesMark.cs
@@ -4,14 +4,28 @@
namespace AcManager.Tools.GameProperties {
public class WorldSimSeriesMark : Game.RaceIniProperties {
- public string Name;
+ public string Name { get; set; }
+ public string Nationality { get; set; }
+ public string NationCode { get; set; }
+ public string Team { get; set; }
public override void Set(IniFile file) {
- file["REMOTE"].Set("NAME", Name);
- file["REMOTE"].Set("GUID", SteamIdHelper.Instance.Value ?? "");
- file["CAR_0"].Set("DRIVER_NAME", Name);
- file["CAR_0"].Set("SETUP", "");
- file["CAR_0"].Remove(@"AI_LEVEL");
+ if (Name != null) {
+ file["REMOTE"].Set("NAME", Name);
+ file["REMOTE"].Set("GUID", SteamIdHelper.Instance.Value ?? "");
+ file["CAR_0"].Set("DRIVER_NAME", Name);
+ file["CAR_0"].Set("SETUP", "");
+ file["CAR_0"].Remove(@"AI_LEVEL");
+ }
+
+ if (Nationality != null) {
+ file["CAR_0"].Set("NATIONALITY", Nationality);
+ file["CAR_0"].Set("NATION_CODE", NationCode ?? NationCodeProvider.Instance.GetNationCode(Nationality));
+ }
+
+ if (Team != null) {
+ file["REMOTE"].Set("TEAM", Team);
+ }
}
}
}
\ No newline at end of file
diff --git a/AcManager.Tools/Helpers/AcLog/WhatsGoingOn.cs b/AcManager.Tools/Helpers/AcLog/WhatsGoingOn.cs
index db7521a75..ac92b674b 100644
--- a/AcManager.Tools/Helpers/AcLog/WhatsGoingOn.cs
+++ b/AcManager.Tools/Helpers/AcLog/WhatsGoingOn.cs
@@ -46,7 +46,7 @@ public string GetDescription() {
private LazierThis _solution;
public NonfatalErrorSolution Solution => _solution.Get(() => Fix == null ? null :
- new NonfatalErrorSolution(FixDisplayName, null, token => {
+ new NonfatalErrorSolution(FixDisplayName, token => {
if (FixAffectingDataOriginalLog != null) {
var carIds = GetCarsIds(FixAffectingDataOriginalLog).ToList();
if (!DataUpdateWarning.Warn(carIds.Select(CarsManager.Instance.GetById))) {
diff --git a/AcManager.Tools/Helpers/Api/HttpClientHolder.cs b/AcManager.Tools/Helpers/Api/HttpClientHolder.cs
index 35fb2f648..2d1722585 100644
--- a/AcManager.Tools/Helpers/Api/HttpClientHolder.cs
+++ b/AcManager.Tools/Helpers/Api/HttpClientHolder.cs
@@ -4,8 +4,9 @@
using AcManager.Internal;
namespace AcManager.Tools.Helpers.Api {
- public class HttpClientHolder {
+ public static class HttpClientHolder {
private static HttpClient _httpClient;
+
public static HttpClient Get() {
if (_httpClient == null) {
var handler = new HttpClientHandler {
diff --git a/AcManager.Tools/Helpers/Api/SteamWebProvider.cs b/AcManager.Tools/Helpers/Api/SteamWebProvider.cs
index 72f912d0e..fcbf6f43a 100644
--- a/AcManager.Tools/Helpers/Api/SteamWebProvider.cs
+++ b/AcManager.Tools/Helpers/Api/SteamWebProvider.cs
@@ -15,7 +15,7 @@ public static class SteamWebProvider {
[Localizable(false), NotNull]
public static string[] GetAchievments(string appId, string steamId) {
var requestUri = string.Format(RequestStatsUri, appId, steamId, InternalUtils.GetSteamApiCode());
-
+
var httpRequest = WebRequest.Create(requestUri);
httpRequest.Method = "GET";
@@ -62,7 +62,7 @@ public static string[] TryToGetAchievments(string appId, string steamId) {
}
}
- private const string RequestPlayerSummariesUri = "http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key={1}&steamids={0}";
+ private const string RequestPlayerSummariesUri = "https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key={1}&steamids={0}";
[Localizable(false), CanBeNull]
public static string TryToGetUserName(string steamId) {
diff --git a/AcManager.Tools/Helpers/FaultTolerantHeapFix.cs b/AcManager.Tools/Helpers/FaultTolerantHeapFix.cs
index 8523b5fd3..8f22023ab 100644
--- a/AcManager.Tools/Helpers/FaultTolerantHeapFix.cs
+++ b/AcManager.Tools/Helpers/FaultTolerantHeapFix.cs
@@ -17,7 +17,7 @@ private static IEnumerable GetRelatedValues(RegistryKey stateKey) {
return stateKey.GetValueNames().Where(name => string.Equals(Path.GetFileName(name), @"acs.exe", StringComparison.OrdinalIgnoreCase));
}
- private static bool Check() {
+ public static bool Check() {
try {
using (var localMachineKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine,
Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32))
@@ -62,10 +62,15 @@ private static async Task FixAsync() {
}
}
- if (await patch.ApplyAsync("Potential performance issue detected",
+ await patch.ApplyAsync("Potential performance issue detected",
"[url=\"https://docs.microsoft.com/en-us/windows/win32/win7appqual/fault-tolerant-heap\"]Fault Tolerant Heap[/url] might be considerably slowing down loading process of Assetto Corsa, but to disable it, a few values in Windows Registry have to be changed, and that would require administrator privilegies. Would you like for Content Manager to disable it automatically, or just prepare a .reg-file for you to inspect and import manually?[br][br]You might need to restart your Windows for changes to apply.",
- "FixFTH.reg")) {
- await ResetService();
+ "FixFTH.reg");
+ for (var i = 0; i < 120; ++i) {
+ await Task.Delay(TimeSpan.FromSeconds(1d));
+ if (!Check()) {
+ await ResetService();
+ break;
+ }
}
}
@@ -81,13 +86,15 @@ private static async Task ResetService() {
var filename = FilesStorage.Instance.GetTemporaryFilename("RunElevated", "FixFTH.bat");
File.WriteAllText(filename, @"@echo off
+:: More info: https://docs.microsoft.com/en-us/windows/win32/win7appqual/fault-tolerant-heap
echo Running rundll32.exe fthsvc.dll,FthSysprepSpecialize...
-%windir%\\system32\\rundll32.exe fthsvc.dll,FthSysprepSpecialize
+cd %windir%\system32
+%windir%\system32\rundll32.exe fthsvc.dll,FthSysprepSpecialize
echo Done
pause");
if (secondResponse == MessageBoxResult.Yes) {
- var procRunDll32 = ProcessExtension.Start(filename, new string[0], new ProcessStartInfo { Verb = "runas" });
+ var procRunDll32 = ProcessExtension.Start("explorer.exe", new[] { filename }, new ProcessStartInfo { Verb = "runas" });
await procRunDll32.WaitForExitAsync().ConfigureAwait(false);
Logging.Debug("Done: " + procRunDll32.ExitCode);
} else if (secondResponse == MessageBoxResult.No) {
@@ -95,8 +102,9 @@ echo Done
}
}
- public static async Task CheckAsync() {
+ public static async Task CheckAndFixAsync() {
if (Check()) {
+ ValuesStorage.Set(".fth.shown", true);
try {
await FixAsync();
} catch (Exception e) {
diff --git a/AcManager.Tools/Helpers/Loaders/DirectLoader.cs b/AcManager.Tools/Helpers/Loaders/DirectLoader.cs
index 10a6882f8..c16430c70 100644
--- a/AcManager.Tools/Helpers/Loaders/DirectLoader.cs
+++ b/AcManager.Tools/Helpers/Loaders/DirectLoader.cs
@@ -47,8 +47,8 @@ public virtual Task PrepareAsync(CookieAwareWebClient client, Cancellation
public Task DownloadAsync(CookieAwareWebClient client,
FlexibleLoaderGetPreferredDestinationCallback getPreferredDestination,
- FlexibleLoaderReportDestinationCallback reportDestination, Func checkIfPaused,
- IProgress progress, CancellationToken cancellation) {
+ FlexibleLoaderReportDestinationCallback reportDestination = null, Func checkIfPaused = null,
+ IProgress progress = null, CancellationToken cancellation = default) {
return DownloadAsyncInner(client, getPreferredDestination, reportDestination, checkIfPaused, progress, cancellation);
}
diff --git a/AcManager.Tools/Helpers/RegistryPatch.cs b/AcManager.Tools/Helpers/RegistryPatch.cs
index 942755f27..e48205f09 100644
--- a/AcManager.Tools/Helpers/RegistryPatch.cs
+++ b/AcManager.Tools/Helpers/RegistryPatch.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
-using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading.Tasks;
@@ -39,7 +38,7 @@ private static string ToRegValue(object v) {
void AddValue(int b) {
data.Append($@"{b:x2},");
if (--commasLeft == 0) {
- data.Append("\\\n ");
+ data.Append("\\\r\n ");
commasLeft = 25;
}
}
@@ -88,7 +87,7 @@ public async Task ApplyAsync(string title, string message, string fileName
foreach (var p in pair.Value) {
var commentValue = ToCommentValue(p.Value);
if (commentValue != null) {
- data.Append("; ").Append(commentValue).Append(Environment.NewLine);
+ data.Append("; ").Append(commentValue.Replace("\n", "\n; ")).Append(Environment.NewLine);
}
// ReSharper disable once MethodHasAsyncOverload
@@ -106,7 +105,7 @@ public async Task ApplyAsync(string title, string message, string fileName
}
try {
- var procRegEdit = ProcessExtension.Start("regedit.exe", new[] { filename }, new ProcessStartInfo { Verb = "runas" });
+ var procRegEdit = ProcessExtension.Start("explorer.exe", new[] { filename });
await procRegEdit.WaitForExitAsync().ConfigureAwait(false);
Logging.Debug("Done: " + procRegEdit.ExitCode);
return procRegEdit.ExitCode == 0;
diff --git a/AcManager.Tools/Helpers/SettingsHolder.Drive.cs b/AcManager.Tools/Helpers/SettingsHolder.Drive.cs
index c6496354c..c776b1ade 100644
--- a/AcManager.Tools/Helpers/SettingsHolder.Drive.cs
+++ b/AcManager.Tools/Helpers/SettingsHolder.Drive.cs
@@ -683,6 +683,19 @@ public bool QuickDriveAllowCustomData {
}
}
+ private bool? _quickDriveAllowExtendedPhysics;
+
+ public bool QuickDriveAllowExtendedPhysics {
+ get => _quickDriveAllowExtendedPhysics
+ ?? (_quickDriveAllowExtendedPhysics = ValuesStorage.Get("Settings.DriveSettings.QuickDriveAllowExtendedPhysics", false)).Value;
+ set {
+ if (Equals(value, _quickDriveAllowExtendedPhysics)) return;
+ _quickDriveAllowExtendedPhysics = value;
+ ValuesStorage.Set("Settings.DriveSettings.QuickDriveAllowExtendedPhysics", value);
+ OnPropertyChanged();
+ }
+ }
+
private bool? _quickDriveExpandBounds;
public bool QuickDriveExpandBounds {
@@ -816,6 +829,22 @@ public bool QuickSwitches {
}
}
+ private bool? _quickSwitchesRightMouseButton;
+
+ public bool QuickSwitchesRightMouseButton {
+ get
+ =>
+ _quickSwitchesRightMouseButton
+ ?? (_quickSwitchesRightMouseButton = ValuesStorage.Get("Settings.DriveSettings.QuickSwitchesRightMouseButton", true))
+ .Value;
+ set {
+ if (Equals(value, _quickSwitchesRightMouseButton)) return;
+ _quickSwitchesRightMouseButton = value;
+ ValuesStorage.Set("Settings.DriveSettings.QuickSwitchesRightMouseButton", value);
+ OnPropertyChanged();
+ }
+ }
+
private string[] _quickSwitchesList;
public string[] QuickSwitchesList {
@@ -1011,7 +1040,8 @@ public DelayEntry RhmKeepAlivePeriod {
private BeepingNoiseType? _crashBeepingNoise;
public BeepingNoiseType CrashBeepingNoise {
- get => _crashBeepingNoise ?? (_crashBeepingNoise = ValuesStorage.Get("Settings.DriveSettings.CrashBeepingNoise", BeepingNoiseType.System)).Value;
+ get => _crashBeepingNoise ?? (_crashBeepingNoise = ValuesStorage.Get("Settings.DriveSettings.CrashBeepingNoise", BeepingNoiseType.System)).Value
+ ;
set {
if (Equals(value, _crashBeepingNoise)) return;
_crashBeepingNoise = value;
diff --git a/AcManager.Tools/Helpers/SteamIdHelper.cs b/AcManager.Tools/Helpers/SteamIdHelper.cs
index 8a51a3c22..695e02e59 100644
--- a/AcManager.Tools/Helpers/SteamIdHelper.cs
+++ b/AcManager.Tools/Helpers/SteamIdHelper.cs
@@ -133,9 +133,12 @@ public static bool IsValidSteamId(string id) {
return id != null && Regex.IsMatch(id.Trim(), @"\d{10,30}");
}
+ private static Dictionary _steamNameCache = new Dictionary();
+
[ItemCanBeNull]
- public static async Task GetSteamName(string id) {
- return IsValidSteamId(id) ? await Task.Run(() => SteamWebProvider.TryToGetUserName(id)) : null;
+ public static async Task GetSteamNameAsync(string id) {
+ if (_steamNameCache.TryGetValue(id, out var name)) return name;
+ return _steamNameCache[id] = IsValidSteamId(id) ? await Task.Run(() => SteamWebProvider.TryToGetUserName(id)) : null;
}
}
diff --git a/AcManager.Tools/Helpers/VisualCppTool.cs b/AcManager.Tools/Helpers/VisualCppTool.cs
index 1b0c2c89a..695564b53 100644
--- a/AcManager.Tools/Helpers/VisualCppTool.cs
+++ b/AcManager.Tools/Helpers/VisualCppTool.cs
@@ -131,7 +131,7 @@ private static bool IsVisualCppInstalled() {
private static void ShowMessage(Exception e) {
NonfatalError.Notify("Looks like app can’t load native library",
"Visual C++ Redistributable might be missing or damaged. Would you like to install the package prepared specially for CM?", e, new[] {
- new NonfatalErrorSolution("Download and install", null, DownloadAndInstall, "DownloadIconData"),
+ new NonfatalErrorSolution("Download and install", DownloadAndInstall, "DownloadIconData"),
});
}
@@ -170,7 +170,7 @@ private static void ManualInstallation([CanBeNull] Exception e, string directory
NonfatalError.Notify("Can’t download the package",
$"You can try to [url={BbCodeBlock.EncodeAttribute(downloadUrl)}]download[/url] and install it manually. Don’t forget to restart CM after it was installed.",
e, new[] {
- new NonfatalErrorSolution("Open destination folder", null, t => {
+ new NonfatalErrorSolution("Open destination folder", t => {
WindowsHelper.ViewDirectory(directory);
return Task.Delay(0);
}, "FolderIconData"),
diff --git a/AcManager.Tools/Managers/Presets/PresetsManager.cs b/AcManager.Tools/Managers/Presets/PresetsManager.cs
index 0926b98c7..5f09529ad 100644
--- a/AcManager.Tools/Managers/Presets/PresetsManager.cs
+++ b/AcManager.Tools/Managers/Presets/PresetsManager.cs
@@ -125,6 +125,13 @@ public void RegisterBuiltInPreset(byte[] data, string categoryName, params strin
RegisterBuiltInPreset(data, new PresetsCategory(categoryName), localFilename);
}
+ public byte[] GetBuiltInPresetData(string categoryName, params string[] localFilename) {
+ var category = new PresetsCategory(categoryName);
+ var directory = GetDirectory(category);
+ var filename = Path.Combine(directory, Path.Combine(localFilename)) + category.Extension;
+ return GetBuiltInPresetsList(category).FirstOrDefault(x => x.VirtualFilename == filename)?.ReadBinaryData();
+ }
+
public void ClearBuiltInPresets(PresetsCategory category) {
_builtInPresets.Remove(category);
}
diff --git a/AcManager.Tools/Objects/CarObject.CustomData.cs b/AcManager.Tools/Objects/CarObject.CustomData.cs
index 307f87242..ab37069cf 100644
--- a/AcManager.Tools/Objects/CarObject.CustomData.cs
+++ b/AcManager.Tools/Objects/CarObject.CustomData.cs
@@ -55,7 +55,9 @@ public bool SetExtendedPhysics(bool allowCustom) {
var dataAcd = Path.Combine(Location, "data.acd");
if (File.Exists(dataAcd)) {
var backupAcd = Path.Combine(Location, "data.acd~cm_bak_ep");
- var custom = allowCustom && UseExtendedPhysics && PatchHelper.IsFeatureSupported(PatchHelper.FeatureFullDay);
+ var custom = allowCustom && UseExtendedPhysics
+ && PatchHelper.IsFeatureSupported(PatchHelper.FeatureFullDay)
+ && SettingsHolder.Drive.QuickDriveAllowExtendedPhysics;
if (custom) {
FileUtils.TryToDelete(backupAcd);
if (File.Exists(dataAcd) && !File.Exists(backupAcd)) {
diff --git a/AcManager.Tools/Objects/CarSkinObject.cs b/AcManager.Tools/Objects/CarSkinObject.cs
index d86763e46..2cbb17d6f 100644
--- a/AcManager.Tools/Objects/CarSkinObject.cs
+++ b/AcManager.Tools/Objects/CarSkinObject.cs
@@ -39,6 +39,8 @@ public CarSkinObject([NotNull] string carId, IFileAcManager manager, string id,
});
}
+ public string NameFromId => _nameFromId.Value;
+
protected override bool LoadJsonOrThrow() {
if (!File.Exists(JsonFilename)) {
ClearData();
@@ -337,7 +339,7 @@ protected override IEnumerable PackOverride(CarSkinObject t) {
yield return Add("cm_skin.json");
}
- if (t.CarId == _recentCarId) {
+ if (t.CarId == _recentCarId && _recentTextures != null) {
yield return Add(_recentTextures);
} else {
var car = CarsManager.Instance.GetById(t.CarId);
diff --git a/AcManager.Tools/Objects/PythonAppConfig.cs b/AcManager.Tools/Objects/PythonAppConfig.cs
index f351e34ba..30924fcc9 100644
--- a/AcManager.Tools/Objects/PythonAppConfig.cs
+++ b/AcManager.Tools/Objects/PythonAppConfig.cs
@@ -10,6 +10,7 @@
using AcTools.Utils;
using AcTools.Utils.Helpers;
using FirstFloor.ModernUI.Commands;
+using FirstFloor.ModernUI.Helpers;
using FirstFloor.ModernUI.Presentation;
using FirstFloor.ModernUI.Serialization;
using JetBrains.Annotations;
@@ -228,33 +229,38 @@ public static PythonAppConfig Create([NotNull] PythonAppConfigParams configParam
return null;
}
- const string extension = @".ini";
- string defaults;
+ try {
+ const string extension = @".ini";
+ string defaults;
- if (userEditedFile == null) {
- const string defaultsPostfix = @"_defaults" + extension;
+ if (userEditedFile == null) {
+ const string defaultsPostfix = @"_defaults" + extension;
- if (relative.EndsWith(defaultsPostfix, StringComparison.OrdinalIgnoreCase)) {
- var original = filename.ApartFromLast(defaultsPostfix) + extension;
- if (File.Exists(original)) return null;
- filename = original;
+ if (relative.EndsWith(defaultsPostfix, StringComparison.OrdinalIgnoreCase)) {
+ var original = filename.ApartFromLast(defaultsPostfix) + extension;
+ if (File.Exists(original)) return null;
+ filename = original;
+ }
+ defaults = filename.ApartFromLast(extension, StringComparison.OrdinalIgnoreCase) + defaultsPostfix;
+ } else {
+ defaults = filename;
+ filename = userEditedFile;
}
- defaults = filename.ApartFromLast(extension, StringComparison.OrdinalIgnoreCase) + defaultsPostfix;
- } else {
- defaults = filename;
- filename = userEditedFile;
- }
- var defaultsMode = File.Exists(defaults);
- var ini = defaultsMode ? new IniFile(defaults, IniFileMode.Comments) : new IniFile(filename, IniFileMode.Comments);
- if (!force && (!ini.Any() || ini.Any(x => !Regex.IsMatch(x.Key, @"^[\w -]+$")))) return null;
- return new PythonAppConfig(configParams, filename, ini,
- (ini.ContainsKey("ℹ") ? ini["ℹ"].GetNonEmpty("FULLNAME") : null)
- ?? relative.ApartFromLast(extension, StringComparison.OrdinalIgnoreCase)
- .Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries)
- .Select(AcStringValues.NameFromId).JoinToString('/'),
- PythonAppsManager.Instance.FirstOrDefault(x => x.Location == configParams.PythonAppLocation),
- defaultsMode ? new IniFile(filename) : null);
+ var defaultsMode = File.Exists(defaults);
+ var ini = defaultsMode ? new IniFile(defaults, IniFileMode.Comments) : new IniFile(filename, IniFileMode.Comments);
+ if (!force && (!ini.Any() || ini.Any(x => !Regex.IsMatch(x.Key, @"^[\w -]+$")))) return null;
+ return new PythonAppConfig(configParams, filename, ini,
+ (ini.ContainsKey("ℹ") ? ini["ℹ"].GetNonEmpty("FULLNAME") : null)
+ ?? relative.ApartFromLast(extension, StringComparison.OrdinalIgnoreCase)
+ .Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries)
+ .Select(AcStringValues.NameFromId).JoinToString('/'),
+ PythonAppsManager.Instance.FirstOrDefault(x => x.Location == configParams.PythonAppLocation),
+ defaultsMode ? new IniFile(filename) : null);
+ } catch (Exception e) when (!force) {
+ Logging.Warning(e);
+ return null;
+ }
}
internal static string CapitalizeFirst(string s) {
diff --git a/AcManager.Tools/Profile/LapTimesManager.cs b/AcManager.Tools/Profile/LapTimesManager.cs
index 97d7d204e..ae8595fad 100644
--- a/AcManager.Tools/Profile/LapTimesManager.cs
+++ b/AcManager.Tools/Profile/LapTimesManager.cs
@@ -315,7 +315,7 @@ public async Task RemoveEntryAsync(LapTimeEntry entry) {
if (EnabledSources.Any(x => x.ReadOnly)) {
NonfatalError.Notify("Can’t remove entry from read-only sources",
$"Please, disable {EnabledSources.Where(x => x.ReadOnly).Select(x => x.DisplayName).JoinToReadableString()}.", solutions: new[] {
- new NonfatalErrorSolution("Disable read-only sources", null, token => {
+ new NonfatalErrorSolution("Disable read-only sources", token => {
foreach (var source in EnabledSources) {
source.IsEnabled = false;
}
diff --git a/AcManager.Tools/Profile/RaceWebServer.cs b/AcManager.Tools/Profile/RaceWebServer.cs
index 6ef0723d4..9173b5399 100644
--- a/AcManager.Tools/Profile/RaceWebServer.cs
+++ b/AcManager.Tools/Profile/RaceWebServer.cs
@@ -62,7 +62,7 @@ public void Start(int port, [CanBeNull] string filename) {
} catch (HttpListenerException e) when (e.ToString().Contains("0x80004005")) {
NonfatalError.NotifyBackground("Can’t start web server",
$"Don’t forget to allow port’s usage with something like “netsh http add urlacl url=\"http://+:{port}/\" user=everyone”.", e, new[] {
- new NonfatalErrorSolution($"Use “netsh” to allow usage of port {port}", null, async token => {
+ new NonfatalErrorSolution($"Use “netsh” to allow usage of port {port}", async token => {
try {
var command = $"netsh http add urlacl url=\"http://+:{port}/\" user=everyone";
var proc = ProcessExtension.Start("cmd", new[] {
diff --git a/AcManager.Tools/SemiGui/GameWrapper.cs b/AcManager.Tools/SemiGui/GameWrapper.cs
index ff51bcad8..0ec7136fe 100644
--- a/AcManager.Tools/SemiGui/GameWrapper.cs
+++ b/AcManager.Tools/SemiGui/GameWrapper.cs
@@ -91,7 +91,10 @@ public bool Test(string obj, string key, ITestEntry value) {
}
private static void PrepareRaceDriverName(Game.StartProperties properties) {
- if (properties.HasAdditional()) return;
+ if (properties.HasAdditional()
+ || properties.HasAdditional()) {
+ return;
+ }
if (properties.BasicProperties?.DriverName != null) {
properties.SetAdditional(new DriverName(properties.BasicProperties.DriverName, properties.BasicProperties.DriverNationality));
diff --git a/AcManager.Workshop/AcManager.Workshop.csproj b/AcManager.Workshop/AcManager.Workshop.csproj
index c92cec004..21a4e8dad 100644
--- a/AcManager.Workshop/AcManager.Workshop.csproj
+++ b/AcManager.Workshop/AcManager.Workshop.csproj
@@ -66,6 +66,8 @@
..\Libraries\Newtonsoft.Json\Newtonsoft.Json.dll
+
+
@@ -73,14 +75,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/AcManager.Workshop/Data/ContentInfoBase.cs b/AcManager.Workshop/Data/ContentInfoBase.cs
new file mode 100644
index 000000000..79964fcf0
--- /dev/null
+++ b/AcManager.Workshop/Data/ContentInfoBase.cs
@@ -0,0 +1,95 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using AcManager.Tools.Helpers;
+using AcManager.Workshop.Providers;
+using AcTools.Utils;
+using AcTools.Utils.Helpers;
+using FirstFloor.ModernUI.Presentation;
+using Newtonsoft.Json;
+
+namespace AcManager.Workshop.Data {
+ public class VersionInfo : NotifyPropertyChanged {
+ [JsonProperty("date")]
+ public long Timestamp { get; set; }
+
+ [JsonIgnore]
+ public DateTime Date => Timestamp.ToDateTimeFromMilliseconds();
+
+ [JsonIgnore]
+ public string DisplayDate => Date.ToShortDateString();
+
+ [JsonProperty("version")]
+ public string Version { get; set; }
+
+ [JsonProperty("changelog")]
+ public string Changelog { get; set; }
+ }
+
+ public class ContentInfoBase : NotifyPropertyChanged {
+ public ContentInfoBase() {
+ UserInfo = Lazier.CreateAsync(() => UserInfoProvider.GetAsync(UserId));
+ }
+
+ [JsonProperty("entryID")]
+ public string Id { get; set; }
+
+ [JsonProperty("userID")]
+ public string UserId { get; set; }
+
+ [JsonIgnore]
+ public Lazier UserInfo { get; }
+
+ [JsonProperty("releaseDate")]
+ public long ReleaseTimestamp { get; set; }
+
+ [JsonIgnore]
+ public DateTime ReleaseDate => ReleaseTimestamp.ToDateTimeFromMilliseconds();
+
+ [JsonIgnore]
+ public string DisplayReleaseDate => ReleaseDate.ToShortDateString();
+
+ [JsonProperty("lastDate")]
+ public long LastTimestamp{ get; set; }
+
+ [JsonIgnore]
+ public DateTime LastDate => LastTimestamp.ToDateTimeFromMilliseconds();
+
+ [JsonIgnore]
+ public string DisplayLastDate => LastDate.ToShortDateString();
+
+ [JsonProperty("name")]
+ public string Name { get; set; }
+
+ [JsonIgnore]
+ public virtual string DisplayName => Name;
+
+ [JsonProperty("tags")]
+ public ObservableCollection Tags { get; set; }
+
+ [JsonProperty("description")]
+ public string Description { get; set; }
+
+ [JsonProperty("originality")]
+ public Originality Originality { get; set; }
+
+ [JsonProperty("version")]
+ public string Version { get; set; }
+
+ [JsonProperty("url")]
+ public string Url { get; set; }
+
+ [JsonProperty("size")]
+ public long Size { get; set; }
+
+ [JsonProperty("sizeFull")]
+ public long SizeFull { get; set; }
+
+ public long SizeToInstall => SizeFull != 0 ? SizeFull : Size;
+
+ [JsonProperty("versions")]
+ public List Versions { get; set; }
+
+ public bool IsNew => DateTime.Now - ReleaseDate < SettingsHolder.Content.NewContentPeriod.TimeSpan;
+ }
+}
\ No newline at end of file
diff --git a/AcManager.Workshop/Data/Originality.cs b/AcManager.Workshop/Data/Originality.cs
new file mode 100644
index 000000000..8170d8566
--- /dev/null
+++ b/AcManager.Workshop/Data/Originality.cs
@@ -0,0 +1,8 @@
+namespace AcManager.Workshop.Data {
+ public enum Originality {
+ Stolen = 0,
+ PortedIndy = 1,
+ Ported = 2,
+ Original = 3
+ };
+}
\ No newline at end of file
diff --git a/AcManager.Workshop/Data/UserFlags.cs b/AcManager.Workshop/Data/UserFlags.cs
new file mode 100644
index 000000000..a200cd6d1
--- /dev/null
+++ b/AcManager.Workshop/Data/UserFlags.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace AcManager.Workshop.Data {
+ [Flags]
+ public enum UserFlags {
+ None = 0,
+ Hidden = 1
+ }
+}
\ No newline at end of file
diff --git a/AcManager.Workshop/Data/UserInfo.cs b/AcManager.Workshop/Data/UserInfo.cs
index c5940de74..69603d4bd 100644
--- a/AcManager.Workshop/Data/UserInfo.cs
+++ b/AcManager.Workshop/Data/UserInfo.cs
@@ -1,27 +1,19 @@
-using System;
-using System.Collections.Generic;
using FirstFloor.ModernUI.Presentation;
using Newtonsoft.Json;
namespace AcManager.Workshop.Data {
- [Flags]
- public enum UserFlags {
- None = 0,
- Hidden = 1
- }
-
public class UserInfo : NotifyPropertyChanged {
[JsonProperty("userID")]
- public string Username { get; set; }
+ public string UserId { get; set; }
- [JsonProperty("flags")]
- public UserFlags Flags { get; set; }
+ [JsonProperty("isHidden")]
+ public bool IsHidden { get; set; }
- [JsonIgnore]
- public bool IsHidden {
- get => Flags.HasFlag(UserFlags.Hidden);
- set => Flags = (Flags & ~UserFlags.Hidden) | (value ? UserFlags.Hidden : UserFlags.None);
- }
+ [JsonProperty("isVirtual")]
+ public bool IsVirtual { get; set; }
+
+ [JsonProperty("username")]
+ public string Username { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
@@ -38,7 +30,7 @@ public bool IsHidden {
[JsonProperty("avatarImageLarge")]
public string AvatarLarge { get; set; }
- [JsonProperty("userURLs")]
- public Dictionary UserUrls { get; set; }
+ /*[JsonProperty("userURLs")]
+ public Dictionary UserUrls { get; set; }*/
}
}
\ No newline at end of file
diff --git a/AcManager.Workshop/Data/WorkshopContentCar.cs b/AcManager.Workshop/Data/WorkshopContentCar.cs
new file mode 100644
index 000000000..a3fcb4b51
--- /dev/null
+++ b/AcManager.Workshop/Data/WorkshopContentCar.cs
@@ -0,0 +1,99 @@
+using System.Collections.Generic;
+using System.Linq;
+using AcManager.Tools.Helpers;
+using AcManager.Workshop.Providers;
+using AcTools.Utils;
+using AcTools.Utils.Helpers;
+using JetBrains.Annotations;
+using Newtonsoft.Json;
+
+namespace AcManager.Workshop.Data {
+ public class WorkshopContentCar : ContentInfoBase {
+ public override string DisplayName {
+ get {
+ var name = Name;
+ if (name == null) return Id;
+
+ if (SettingsHolder.Content.CarsDisplayNameCleanUp) {
+ name = name.Replace(@"™", "");
+ }
+
+ var yearValue = Year;
+ if (yearValue > 1900 && SettingsHolder.Content.CarsYearPostfix) {
+ if (SettingsHolder.Content.CarsYearPostfixAlt) {
+ return $@"{name} ({yearValue})";
+ }
+ var year = yearValue.ToString();
+ var index = name.Length - year.Length - 1;
+ if ((!name.EndsWith(year) || index > 0 && char.IsLetterOrDigit(name[index]))
+ && !AcStringValues.GetYearFromName(name).HasValue) {
+ return $@"{name} ’{yearValue % 100:D2}";
+ }
+ }
+
+ return name;
+ }
+ }
+
+ public WorkshopContentCar() {
+ BrandBadge = Lazier.CreateAsync(() => BrandBadgeProvider.GetAsync(Brand));
+ }
+
+ [JsonProperty("parentID")]
+ public string ParentID { get; set; }
+
+ [JsonProperty("carBrand")]
+ public string Brand { get; set; }
+
+ public Lazier BrandBadge { get; }
+
+ [JsonProperty("carClass")]
+ public string CarClass { get; set; }
+
+ [JsonProperty("country")]
+ public string Country { get; set; }
+
+ [JsonProperty("year")]
+ public int Year { get; set; }
+
+ [JsonProperty("weight")]
+ public double Weight { get; set; }
+
+ [JsonProperty("power")]
+ public double Power { get; set; }
+
+ [JsonProperty("torque")]
+ public double Torque { get; set; }
+
+ [JsonProperty("speed")]
+ public double Speed { get; set; }
+
+ [JsonProperty("acceleration")]
+ public double Acceleration { get; set; }
+
+ [JsonProperty("previewImage")]
+ public string PreviewImage { get; set; }
+
+ [JsonProperty("upgradeIcon")]
+ public string UpgradeIcon { get; set; }
+
+ [JsonProperty("defaultSkinsCount")]
+ public int DefaultSkinsCount { get; set; }
+
+ [JsonProperty("skinsCount")]
+ public int SkinsCount { get; set; }
+
+ [JsonProperty("skins"), CanBeNull]
+ public List Skins { get; set; }
+
+ private WorkshopContentCarSkin _selectedSkin;
+
+ public WorkshopContentCarSkin SelectedSkin {
+ get => _selectedSkin ?? (_selectedSkin = Skins?.FirstOrDefault());
+ set => Apply(value, ref _selectedSkin);
+ }
+
+ [JsonIgnore, NotNull]
+ public string ShortName => DisplayName.ApartFromFirst(Brand).TrimStart();
+ }
+}
\ No newline at end of file
diff --git a/AcManager.Workshop/Data/WorkshopContentCarSkin.cs b/AcManager.Workshop/Data/WorkshopContentCarSkin.cs
new file mode 100644
index 000000000..d041843be
--- /dev/null
+++ b/AcManager.Workshop/Data/WorkshopContentCarSkin.cs
@@ -0,0 +1,23 @@
+using Newtonsoft.Json;
+
+namespace AcManager.Workshop.Data {
+ public class WorkshopContentCarSkin : ContentInfoBase {
+ [JsonProperty("skinIcon")]
+ public string LiveryImage { get; set; }
+
+ [JsonProperty("previewImage")]
+ public string PreviewImage { get; set; }
+
+ [JsonProperty("driverName")]
+ public string DriverName { get; set; }
+
+ [JsonProperty("country")]
+ public string Country { get; set; }
+
+ [JsonProperty("isDefault")]
+ public int IsDefaultNum { get; set; }
+
+ [JsonIgnore] // TODO: custom serializer
+ public bool IsDefault => IsDefaultNum == 1;
+ }
+}
\ No newline at end of file
diff --git a/AcManager.Workshop/Data/WorkshopContentCategory.cs b/AcManager.Workshop/Data/WorkshopContentCategory.cs
new file mode 100644
index 000000000..eaeb72bab
--- /dev/null
+++ b/AcManager.Workshop/Data/WorkshopContentCategory.cs
@@ -0,0 +1,20 @@
+using Newtonsoft.Json;
+
+namespace AcManager.Workshop.Data {
+ public class WorkshopContentCategory {
+ [JsonProperty("name")]
+ public string Name { get; set; }
+
+ [JsonProperty("icon")]
+ public string Icon { get; set; }
+
+ [JsonProperty("uses")]
+ public int Uses { get; set; }
+
+ [JsonProperty("new")]
+ public int NewItems { get; set; }
+
+ [JsonIgnore]
+ public bool HasNew => NewItems > 0;
+ }
+}
\ No newline at end of file
diff --git a/AcManager.Workshop/Data/WorkshopContentTag.cs b/AcManager.Workshop/Data/WorkshopContentTag.cs
new file mode 100644
index 000000000..ef8c951b8
--- /dev/null
+++ b/AcManager.Workshop/Data/WorkshopContentTag.cs
@@ -0,0 +1,23 @@
+using Newtonsoft.Json;
+
+namespace AcManager.Workshop.Data {
+ public class WorkshopContentTag {
+ [JsonProperty("tagID")]
+ public string Name { get; set; }
+
+ [JsonProperty("description")]
+ public string Description { get; set; }
+
+ [JsonProperty("uses")]
+ public int Uses { get; set; }
+
+ [JsonProperty("new")]
+ public int NewItems { get; set; }
+
+ [JsonIgnore]
+ public bool HasNew => NewItems > 0;
+
+ [JsonIgnore]
+ public bool Accented => Name?.StartsWith(@"#") == true;
+ }
+}
\ No newline at end of file
diff --git a/AcManager.Workshop/Properties/AssemblyInfo.cs b/AcManager.Workshop/Properties/AssemblyInfo.cs
index a05d4fc35..adc3567dd 100644
--- a/AcManager.Workshop/Properties/AssemblyInfo.cs
+++ b/AcManager.Workshop/Properties/AssemblyInfo.cs
@@ -33,12 +33,12 @@
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("1.0.1.219")]
-[assembly: AssemblyFileVersion("1.0.1.219")]
+[assembly: AssemblyVersion("1.0.1.337")]
+[assembly: AssemblyFileVersion("1.0.1.337")]
[assembly: XmlnsDefinition("http://acstuff.ru/app/workshop", "AcManager.Workshop")]
-[assembly: XmlnsDefinition("http://acstuff.ru/app/workshop", "AcManager.Workshop.Implementations")]
+[assembly: XmlnsDefinition("http://acstuff.ru/app/workshop", "AcManager.Workshop.Data")]
[assembly: XmlnsPrefix("http://acstuff.ru/app/workshop", "ws")]
[assembly: NeutralResourcesLanguage("en-US")]
-// Modified at: 10/9/2020 1:22:57 AM
\ No newline at end of file
+// Modified at: 10/24/2020 6:31:20 PM
\ No newline at end of file
diff --git a/AcManager.Workshop/Providers/BrandBadgeProvider.cs b/AcManager.Workshop/Providers/BrandBadgeProvider.cs
new file mode 100644
index 000000000..d280d0684
--- /dev/null
+++ b/AcManager.Workshop/Providers/BrandBadgeProvider.cs
@@ -0,0 +1,26 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using AcManager.Workshop.Data;
+using AcTools.Utils.Helpers;
+
+namespace AcManager.Workshop.Providers {
+ public class BrandBadgeProvider {
+ private static List _carBrands;
+ private static TaskCache _tasks = new TaskCache();
+
+ public static Task GetAsync(string badgeName) {
+ // TODO: error handling
+ if (_carBrands != null) {
+ return Task.FromResult(_carBrands.FirstOrDefault(x => x.Name == badgeName)?.Icon);
+ }
+
+ return _tasks.Get(async () => {
+ _carBrands = await WorkshopHolder.Client.GetAsync>("/car-brands");
+ return _carBrands;
+ }).ContinueWith(r => {
+ return r.Result.FirstOrDefault(x => x.Name == badgeName)?.Icon;
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/AcManager.Workshop/Providers/UserInfoProvider.cs b/AcManager.Workshop/Providers/UserInfoProvider.cs
new file mode 100644
index 000000000..d10fdf0ee
--- /dev/null
+++ b/AcManager.Workshop/Providers/UserInfoProvider.cs
@@ -0,0 +1,23 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using AcManager.Workshop.Data;
+using AcTools.Utils.Helpers;
+
+namespace AcManager.Workshop.Providers {
+ public class UserInfoProvider {
+ private static Dictionary _cache = new Dictionary();
+ private static TaskCache _tasks = new TaskCache();
+
+ public static Task GetAsync(string userId) {
+ // TODO: error handling
+ if (_cache.TryGetValue(userId, out var ret)) {
+ return Task.FromResult(ret);
+ }
+
+ return _tasks.Get(async () => {
+ _cache[userId] = await WorkshopHolder.Client.GetAsync($"/users/{userId}");
+ return _cache[userId];
+ }, userId);
+ }
+ }
+}
\ No newline at end of file
diff --git a/AcManager.Workshop/Uploaders/AcStuffWorkshopUploader.cs b/AcManager.Workshop/Uploaders/AcStuffWorkshopUploader.cs
new file mode 100644
index 000000000..2993e4779
--- /dev/null
+++ b/AcManager.Workshop/Uploaders/AcStuffWorkshopUploader.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Net;
+using System.Net.Http;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using AcManager.Tools.Helpers.Api;
+using AcTools.Utils.Helpers;
+using B2Net;
+using FirstFloor.ModernUI.Dialogs;
+using FirstFloor.ModernUI.Helpers;
+using Newtonsoft.Json.Linq;
+
+namespace AcManager.Workshop.Uploaders {
+ public class AcStuffWorkshopUploader : IWorkshopUploader {
+ private readonly string _endpoint;
+ private readonly string _checksum;
+
+ public AcStuffWorkshopUploader(JObject uploadParams) {
+ _endpoint = uploadParams["endpoint"].ToString();
+ _checksum = uploadParams["checksum"].ToString();
+ }
+
+ public async Task UploadAsync(byte[] data, string group, string name,
+ IProgress progress = null, CancellationToken cancellation = default) {
+ for (var i = 0; i < 3; ++i) {
+ progress?.Report(AsyncProgressEntry.FromStringIndetermitate(i == 0
+ ? "Starting upload…"
+ : $"Trying again, {(i + 1).ToOrdinal("attempt").ToSentenceMember()} attempt"));
+ try {
+ return await TryToUploadAsync();
+ } catch (HttpRequestException e) {
+ Logging.Warning(e);
+ cancellation.ThrowIfCancellationRequested();
+ progress?.Report(AsyncProgressEntry.FromStringIndetermitate("Upload is failed, waiting a bit before the next attempt…"));
+ await Task.Delay(TimeSpan.FromSeconds(i + 1d));
+ cancellation.ThrowIfCancellationRequested();
+ } catch (WebException e) {
+ Logging.Warning(e);
+ cancellation.ThrowIfCancellationRequested();
+ progress?.Report(AsyncProgressEntry.FromStringIndetermitate("Upload is failed, waiting a bit before the next attempt…"));
+ await Task.Delay(TimeSpan.FromSeconds(i + 1d));
+ cancellation.ThrowIfCancellationRequested();
+ }
+ }
+
+ cancellation.ThrowIfCancellationRequested();
+ progress?.Report(AsyncProgressEntry.FromStringIndetermitate($"Trying again, last attempt"));
+ return await TryToUploadAsync();
+
+ async Task TryToUploadAsync() {
+ var request = new HttpRequestMessage(HttpMethod.Post, _endpoint);
+ request.Headers.TryAddWithoutValidation("X-Data-File-Group", group);
+ request.Headers.TryAddWithoutValidation("X-Data-File-Name", name);
+ request.Headers.TryAddWithoutValidation("X-Data-Checksum", _checksum);
+ var stopwatch = new AsyncProgressBytesStopwatch();
+ request.Content = progress == null
+ ? (HttpContent)new ByteArrayContent(data)
+ : new ProgressableByteArrayContent(data, 8192,
+ new Progress(x => progress.Report(AsyncProgressEntry.CreateUploading(x, data.Length, stopwatch))));
+ using (var response = await HttpClientHolder.Get().SendAsync(request, cancellation).ConfigureAwait(false)) {
+ if (response.StatusCode != HttpStatusCode.OK) {
+ throw new WebException($"Failed to upload: {response.StatusCode}, response: {await LoadContent()}");
+ }
+ var result = JObject.Parse(await LoadContent());
+ return new WorkshopUploadResult {
+ Size = data.Length,
+ Tag = result["key"].ToString()
+ };
+
+ ConfiguredTaskAwaitable LoadContent() {
+ return response.Content.ReadAsStringAsync().WithCancellation(cancellation).ConfigureAwait(false);
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/AcManager.Workshop/Uploaders/B2WorkshopUploader.cs b/AcManager.Workshop/Uploaders/B2WorkshopUploader.cs
index 3bfe992ef..4f15a1b57 100644
--- a/AcManager.Workshop/Uploaders/B2WorkshopUploader.cs
+++ b/AcManager.Workshop/Uploaders/B2WorkshopUploader.cs
@@ -1,65 +1,126 @@
using System;
using System.Collections.Generic;
+using System.Net;
+using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
-using System.Web;
using AcManager.Tools.Helpers.Api;
+using AcTools.Utils.Helpers;
using B2Net;
using B2Net.Http;
using B2Net.Models;
using FirstFloor.ModernUI.Dialogs;
+using FirstFloor.ModernUI.Helpers;
+using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace AcManager.Workshop.Uploaders {
public class B2WorkshopUploader : IWorkshopUploader {
- private B2Client _b2Client;
- private string _b2ClientPrefix;
- private string _b2BucketName;
- private string _hostName;
- private string _currentGroup;
+ private static B2Client _b2ClientLast;
+ private static string _b2ClientConfig;
+
+ private readonly B2Client _b2Client;
+ private readonly string _b2ClientPrefix;
public B2WorkshopUploader(JObject uploadParams) {
HttpClientFactory.SetHttpClient(HttpClientHolder.Get());
- _b2Client = new B2Client(new B2Options {
- KeyId = uploadParams["keyID"].ToString(),
- ApplicationKey = uploadParams["keyValue"].ToString(),
- BucketId = uploadParams["bucketID"].ToString(),
- PersistBucket = true
- });
+
+ var config = uploadParams.ToString();
+ if (_b2ClientConfig == config) {
+ _b2Client = _b2ClientLast;
+ } else {
+ _b2Client = new B2Client(new B2Options {
+ KeyId = uploadParams["keyID"].ToString(),
+ ApplicationKey = uploadParams["keyValue"].ToString(),
+ BucketId = uploadParams["bucketID"].ToString(),
+ PersistBucket = true
+ });
+ _b2ClientLast = _b2Client;
+ _b2ClientConfig = config;
+ }
_b2ClientPrefix = uploadParams["prefix"].ToString();
- _b2BucketName = uploadParams["bucketName"].ToString();
- _hostName = uploadParams["hostName"].ToString();
}
- public void MarkNewGroup() {
- _currentGroup = Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16).ToLowerInvariant();
+ private Task _authorizing;
+ private object _authorizingLock = new object();
+
+ private async Task AuthorizeInner() {
+ await _b2Client.Authorize().ConfigureAwait(false);
+ lock (_authorizingLock) {
+ _authorizing = null;
+ }
}
- async Task IWorkshopUploader.UploadAsync(byte[] data, string downloadName, string origin, IProgress progress,
- CancellationToken cancellation) {
- await _b2Client.Authorize(cancellation);
- if (cancellation.IsCancellationRequested) {
- throw new TaskCanceledException();
+ private Task Authorize() {
+ lock (_authorizingLock) {
+ return _authorizing ?? (_authorizing = AuthorizeInner());
}
+ }
+
+ public async Task UploadAsync(byte[] data, string group, string name,
+ IProgress progress = null, CancellationToken cancellation = default) {
+ progress?.Report(AsyncProgressEntry.FromStringIndetermitate("Authorizing…"));
+ await Authorize().WithCancellation(cancellation).ConfigureAwait(false);
+ cancellation.ThrowIfCancellationRequested();
- if (_currentGroup == null) {
- MarkNewGroup();
+ progress?.Report(AsyncProgressEntry.FromStringIndetermitate("Finding a vault to upload…"));
+ var uploadUrl = await _b2Client.Files.GetUploadUrl(cancelToken: cancellation);
+ cancellation.ThrowIfCancellationRequested();
+
+ for (var i = 0; i < 4; ++i) {
+ progress?.Report(AsyncProgressEntry.FromStringIndetermitate(i == 0
+ ? "Starting upload…"
+ : $"Trying again, {(i + 1).ToOrdinal("attempt").ToSentenceMember()} attempt"));
+ try {
+ return await TryToUploadAsync(uploadUrl);
+ } catch (B2Exception e) when (e.Code == "bad_auth_token" || e.ShouldRetryRequest) {
+ cancellation.ThrowIfCancellationRequested();
+ uploadUrl = await _b2Client.Files.GetUploadUrl(cancelToken: cancellation);
+ } catch (HttpRequestException e) {
+ Logging.Warning(e);
+ cancellation.ThrowIfCancellationRequested();
+ progress?.Report(AsyncProgressEntry.FromStringIndetermitate("Target vault is not available, waiting a bit before the next attempt…"));
+ await Task.Delay(TimeSpan.FromSeconds(i + 1d));
+ cancellation.ThrowIfCancellationRequested();
+ } catch (WebException e) {
+ Logging.Warning(e);
+ cancellation.ThrowIfCancellationRequested();
+ progress?.Report(AsyncProgressEntry.FromStringIndetermitate("Target vault is not available, waiting a bit before the next attempt…"));
+ await Task.Delay(TimeSpan.FromSeconds(i + 1d));
+ cancellation.ThrowIfCancellationRequested();
+ } catch (B2Exception e) when (e.Status == "500" || e.Status == "503") {
+ Logging.Warning(e);
+ cancellation.ThrowIfCancellationRequested();
+ progress?.Report(AsyncProgressEntry.FromStringIndetermitate("Target vault is not full, waiting a bit before the next attempt…"));
+ await Task.Delay(TimeSpan.FromSeconds(i + 1d));
+ cancellation.ThrowIfCancellationRequested();
+ } catch (B2Exception e) {
+ Logging.Warning("B2Exception.Code=" + e.Code);
+ Logging.Warning("B2Exception.Status=" + e.Status);
+ Logging.Warning("B2Exception.Message=" + e.Message);
+ Logging.Warning("B2Exception.ShouldRetryRequest=" + e.ShouldRetryRequest);
+ throw;
+ }
}
- var fileName = _b2ClientPrefix + _currentGroup + "/" + downloadName;
- var uploadUrl = await _b2Client.Files.GetUploadUrl().ConfigureAwait(false);
- if (cancellation.IsCancellationRequested) throw new TaskCanceledException();
+ cancellation.ThrowIfCancellationRequested();
+ progress?.Report(AsyncProgressEntry.FromStringIndetermitate($"Trying again, last attempt"));
+ return await TryToUploadAsync(uploadUrl);
- var stopwatch = new AsyncProgressBytesStopwatch();
- var file = await _b2Client.Files.Upload(data, fileName, uploadUrl, "", "", new Dictionary {
- ["b2-content-disposition"] = Regex.IsMatch(downloadName, @"\.(png|jpe?g)$") ? "inline" : "attachment",
- ["b2-cache-control"] = "immutable",
- ["x-origin"] = origin == null ? "" :HttpUtility.UrlEncode(origin),
- }, progress == null ? null : new Progress(x => {
- progress.Report(AsyncProgressEntry.CreateUploading(x, data.Length, stopwatch));
- }), cancellation).ConfigureAwait(false);
- return $"{_hostName}/file/{_b2BucketName}/{file.FileName}";
+ async Task TryToUploadAsync(B2UploadUrl url) {
+ var fileName = _b2ClientPrefix + group + "/" + name;
+ var stopwatch = new AsyncProgressBytesStopwatch();
+ var file = await _b2Client.Files.Upload(data, fileName, url, "", "", new Dictionary {
+ ["b2-content-disposition"] = Regex.IsMatch(name, @"\.(png|jpg)$") ? "inline" : "attachment",
+ ["b2-cache-control"] = "immutable"
+ }, progress == null ? null : new Progress(x => progress.Report(AsyncProgressEntry.CreateUploading(x, data.Length, stopwatch))),
+ cancellation).ConfigureAwait(false);
+ return new WorkshopUploadResult {
+ Tag = JsonConvert.SerializeObject(new { fileName = file.FileName, fileID = file.FileId }),
+ Size = data.LongLength
+ };
+ }
}
}
}
\ No newline at end of file
diff --git a/AcManager.Workshop/Uploaders/IWorkshopUploader.cs b/AcManager.Workshop/Uploaders/IWorkshopUploader.cs
index b3c4baaf0..0062b5a46 100644
--- a/AcManager.Workshop/Uploaders/IWorkshopUploader.cs
+++ b/AcManager.Workshop/Uploaders/IWorkshopUploader.cs
@@ -5,19 +5,22 @@
using JetBrains.Annotations;
namespace AcManager.Workshop.Uploaders {
- public interface IWorkshopUploader {
- void MarkNewGroup();
+ public class WorkshopUploadResult {
+ public long Size { get; set; }
+ public string Tag { get; set; }
+ }
+ public interface IWorkshopUploader {
///
- /// Uploads a file online for it to be downloaded later, returns a direct link.
+ /// Uploads a file online for it to be downloaded later, returns a file tag to send to Workshop server to get a final link.
///
/// Data to upload
- /// File name for downloads
- /// Any tag for the file marking its origin, role or something like that
+ /// File group (aka name of its folder), to avoid collisions
+ /// File name for downloads
/// Progress callback
/// Cancellation token
/// Direct link to download or view content in browser
- Task UploadAsync([NotNull] byte[] data, [NotNull] string downloadName, [CanBeNull] string origin,
+ Task UploadAsync([NotNull] byte[] data, [NotNull] string group, [NotNull] string name,
[CanBeNull] IProgress progress = null, CancellationToken cancellation = default);
}
}
\ No newline at end of file
diff --git a/AcManager.Workshop/Uploaders/WorkshopUploaderFactory.cs b/AcManager.Workshop/Uploaders/WorkshopUploaderFactory.cs
index f5bd8e755..1ec2f4fa5 100644
--- a/AcManager.Workshop/Uploaders/WorkshopUploaderFactory.cs
+++ b/AcManager.Workshop/Uploaders/WorkshopUploaderFactory.cs
@@ -5,8 +5,11 @@
namespace AcManager.Workshop.Uploaders {
public static class WorkshopUploaderFactory {
[NotNull]
- public static IWorkshopUploader Create([NotNull] JObject uploadParams) {
- if (uploadParams["version"].ToString() == "B2/1") {
+ public static IWorkshopUploader Create(string uploaderId, [NotNull] JObject uploadParams) {
+ if (uploaderId == "AS/1") {
+ return new AcStuffWorkshopUploader(uploadParams);
+ }
+ if (uploaderId == "B2/1") {
return new B2WorkshopUploader(uploadParams);
}
throw new NotImplementedException("Unsupported upload parameters");
diff --git a/AcManager.Workshop/WorkshopClient.cs b/AcManager.Workshop/WorkshopClient.cs
index b82c34ad4..657cb9378 100644
--- a/AcManager.Workshop/WorkshopClient.cs
+++ b/AcManager.Workshop/WorkshopClient.cs
@@ -2,52 +2,145 @@
using System.ComponentModel;
using System.Net;
using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
+using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using AcManager.Tools.Helpers.Api;
using AcManager.Workshop.Uploaders;
using AcTools.Utils.Helpers;
using FirstFloor.ModernUI.Dialogs;
+using FirstFloor.ModernUI.Helpers;
+using FirstFloor.ModernUI.Presentation;
using JetBrains.Annotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace AcManager.Workshop {
- public class WorkshopClient {
+ public class WorkshopClient : NotifyPropertyChanged {
+ public static bool OptionUserAvailable = false;
+ public static bool OptionCreatorAvailable = false;
+
private const string Salt1 = "Mghf4ZTzPQFGYJMA";
private const string Salt2 = "5qzAfOyqBIgNHG0V";
private const string Salt3 = "h3y2zIonOlgf1AQS";
+ private const string Salt4 = "5wXRea0U5wXRea0U";
+ private const string Salt5 = "IARgcb0Jk3mksyfeQl3xUqHJnEKLmi8f";
+
+ [NotNull]
+ private readonly string _apiHost;
+
+ [CanBeNull]
+ private string _userId;
+
+ [CanBeNull]
+ internal string UserId {
+ get => _userId;
+ set {
+ _userId = value;
+ _pokeKey = null;
+ }
+ }
+
+ [CanBeNull]
+ private string _userPasswordChecksum;
+
+ [CanBeNull]
+ internal string UserPasswordChecksum {
+ get => _userPasswordChecksum;
+ set {
+ _userPasswordChecksum = value;
+ _pokeKey = null;
+ }
+ }
- private string ApiHost;
- private string UserId;
- private string UserPassword;
+ private string _pokeKey;
- public WorkshopClient([NotNull] string apiHost, [NotNull] string userId, [NotNull] string userPassword) {
- ApiHost = apiHost;
- UserId = userId;
- UserPassword = GetChecksum(GetChecksum(Salt3 + userId) + userPassword);
+ public WorkshopClient([NotNull] string apiHost) {
+ _apiHost = apiHost;
}
+ #region Basic stuff for sending requests
[NotNull]
private string GetFullUrl([NotNull] string path) {
- return ApiHost + path;
+ return $@"{_apiHost}/v1{path}";
+ }
+
+ internal static string GetUserId([NotNull] string steamId) {
+ if (steamId == null) throw new ArgumentNullException(nameof(steamId));
+ using (var sha = SHA256.Create()) {
+ return Regex.Replace(Convert.ToBase64String(sha.ComputeHash(Encoding.UTF8.GetBytes(Salt4 + steamId))), @"\W+|=+$", "").Substring(0, 16);
+ }
+ }
+
+ internal static string GetPasswordChecksum([NotNull] string userId, [NotNull] string userPassword) {
+ if (userId == null) throw new ArgumentNullException(nameof(userId));
+ if (userPassword == null) throw new ArgumentNullException(nameof(userPassword));
+ Logging.Debug($"userId={userId}, userPassword=`{userPassword}`, checksum={GetChecksum(GetChecksum(Salt3 + userId) + userPassword)}");
+ return GetChecksum(GetChecksum(Salt3 + userId) + userPassword);
+ }
+
+ private static string ToHexString([NotNull] byte[] data) {
+ const string lookup = "0123456789abcdef";
+ int i = -1, p = -1, l = data.Length;
+ var c = new char[l-- * 2];
+ while (i < l) {
+ var d = data[++i];
+ c[++p] = lookup[d >> 4];
+ c[++p] = lookup[d & 0xF];
+ }
+ return new string(c, 0, c.Length);
}
[NotNull]
- private string GetChecksum([NotNull] string s) {
- using (var sha1 = SHA256.Create()) {
- return sha1.ComputeHash(Encoding.UTF8.GetBytes(s)).ToHexString().ToLowerInvariant();
+ private static string GetChecksum([NotNull] string s) {
+ using (var sha = SHA256.Create()) {
+ return ToHexString(sha.ComputeHash(Encoding.UTF8.GetBytes(s)));
+ }
+ }
+
+ private static ConfiguredTaskAwaitable LoadBinaryContent(HttpResponseMessage response, CancellationToken cancellation) {
+ return response.Content.ReadAsByteArrayAsync().WithCancellation(cancellation).ConfigureAwait(false);
+ }
+
+ private static ConfiguredTaskAwaitable LoadContent(HttpResponseMessage response, CancellationToken cancellation) {
+ return response.Content.ReadAsStringAsync().WithCancellation(cancellation).ConfigureAwait(false);
+ }
+
+ private static async Task TestResponse(HttpResponseMessage response, CancellationToken cancellation) {
+ if (response.StatusCode >= (HttpStatusCode)400) {
+ var errorMessage = response.ReasonPhrase;
+ string remoteException = null;
+ string[] remoteStackTrace = null;
+ try {
+ var content = await LoadContent(response, cancellation);
+ var details = JObject.Parse(content);
+ errorMessage = details["error"].ToString();
+ var exception = details["exception"];
+ if (exception != null) {
+ remoteException = exception["message"].ToString();
+ remoteStackTrace = exception["stack"]?.ToObject();
+ if (remoteStackTrace?.Length == 0) {
+ remoteStackTrace = null;
+ }
+ }
+ } catch (Exception e) {
+ Logging.Warning(e);
+ // ignored
+ }
+ throw new WorkshopException(response.StatusCode, errorMessage, remoteException, remoteStackTrace);
}
}
[ItemNotNull]
- private async Task PokeAsync([NotNull] string url, CancellationToken cancellation) {
- var request = new HttpRequestMessage(HttpMethod.Get, GetFullUrl("/poke/" + GetChecksum(Salt1 + url)));
+ private async Task PokeAsync(CancellationToken cancellation) {
+ var request = new HttpRequestMessage(HttpMethod.Post, GetFullUrl("/poke/" + GetChecksum(Salt1 + UserId)));
try {
using (var response = await HttpClientHolder.Get().SendAsync(request, cancellation).ConfigureAwait(false)) {
- if (response.StatusCode != HttpStatusCode.OK) throw new WebException(response.ReasonPhrase);
+ await TestResponse(response, cancellation).ConfigureAwait(false);
return JObject.Parse(await response.Content.ReadAsStringAsync().WithCancellation(cancellation).ConfigureAwait(false))["poke"].ToString();
}
} catch (Exception e) when (e.IsCancelled()) {
@@ -56,73 +149,136 @@ private async Task PokeAsync([NotNull] string url, CancellationToken can
}
[ItemNotNull]
- private async Task CreateHttpRequestAsync(HttpMethod method, [NotNull] string url, [CanBeNull] string data,
+ private async Task CreateHttpRequestAsync([NotNull] HttpMethod method, [NotNull] string url, [CanBeNull] string data,
CancellationToken cancellation) {
- var poke = await PokeAsync(url, cancellation).ConfigureAwait(false);
- var randomLine = GetChecksum(Guid.NewGuid().ToString());
- var userIdShuffled = GetChecksum(Salt2 + UserId);
- var userPasswordShuffled = GetChecksum((method == HttpMethod.Get ? poke : poke + data) + userIdShuffled + UserPassword + randomLine);
var request = new HttpRequestMessage(method, GetFullUrl(url));
- request.Headers.Add("X-Validation", randomLine + userIdShuffled + userPasswordShuffled);
+ if (UserId != null && UserPasswordChecksum != null) {
+ var poke = _pokeKey ?? (_pokeKey = await PokeAsync(cancellation).ConfigureAwait(false));
+ var randomLine = GetChecksum(Guid.NewGuid().ToString());
+ var userIdShuffled = GetChecksum(Salt2 + UserId);
+ var userPasswordShuffled = GetChecksum((method == HttpMethod.Get ? poke : poke + data) + userIdShuffled + UserPasswordChecksum + randomLine);
+ Logging.Debug("Poke argument base: " + Salt1 + UserId);
+ Logging.Debug("Poke argument: " + GetChecksum(Salt1 + UserId));
+ Logging.Debug("Poke: " + poke);
+ Logging.Debug("Data: " + data);
+ Logging.Debug("UserIdShuffled: " + userIdShuffled);
+ Logging.Debug("UserPasswordChecksum: " + UserPasswordChecksum);
+ Logging.Debug("RandomLine: " + randomLine);
+ request.Headers.Add("X-Validation", randomLine + userIdShuffled + userPasswordShuffled);
+ }
return request;
}
- private static async Task RunRequest(HttpRequestMessage request, CancellationToken cancellation) {
+ private static async Task RunRequest([NotNull] HttpRequestMessage request, [CanBeNull] Action headersCallback,
+ CancellationToken cancellation) {
using (var response = await HttpClientHolder.Get().SendAsync(request, cancellation).ConfigureAwait(false)) {
- if (response.StatusCode >= (HttpStatusCode)400) {
- throw new WebException(
- (await response.Content.ReadAsStringAsync().WithCancellation(cancellation).ConfigureAwait(false)).Or(response.ReasonPhrase));
- }
+ await TestResponse(response, cancellation).ConfigureAwait(false);
+ headersCallback?.Invoke(response.Headers);
return typeof(T) == typeof(object)
? (T)(object)null
- : JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync().WithCancellation(cancellation).ConfigureAwait(false));
+ : typeof(T) == typeof(byte[])
+ ? (T)(object)await LoadBinaryContent(response, cancellation)
+ : JsonConvert.DeserializeObject(await LoadContent(response, cancellation));
}
}
- [CanBeNull]
- private JObject _uploaderParams;
-
- [CanBeNull]
- private IWorkshopUploader _uploader;
+ private string _currentGroup;
- public void MarkNewUploadGroup() {
- _uploader?.MarkNewGroup();
+ public void MarkNewUploadGroup(string groupId = null) {
+ _currentGroup = groupId.Or(Guid.NewGuid().ToString().Replace("-", "").Substring(0, 16).ToLowerInvariant());
}
- [ItemNotNull]
- public async Task UploadAsync([NotNull] byte[] data, [NotNull] string downloadName, [CanBeNull] string origin = null,
- IProgress progress = null, CancellationToken cancellation = default) {
- if (_uploader == null) {
- if (_uploaderParams == null) {
- _uploaderParams = await GetAsync("/manage/upload-params", cancellation).ConfigureAwait(false);
- if (_uploaderParams == null) {
- throw new Exception("Upload parameters are not available");
- }
- }
- _uploader = WorkshopUploaderFactory.Create(_uploaderParams);
+ private static string CalculateUploadChecksum(byte[] data) {
+ using (var sha1 = SHA1.Create()) {
+ return ToHexString(sha1.ComputeHash(Encoding.UTF8.GetBytes(Salt5 + ToHexString(sha1.ComputeHash(data)))));
}
- return await _uploader.UploadAsync(data, downloadName, origin, progress, cancellation).ConfigureAwait(false);
}
- public async Task GetAsync([Localizable(false), NotNull] string url, CancellationToken cancellation = default) {
- var request = await CreateHttpRequestAsync(HttpMethod.Get, url, null, cancellation).ConfigureAwait(false);
- return await RunRequest(request, cancellation).ConfigureAwait(false);
+ internal async Task RequestAsync(HttpMethod method, [Localizable(false), NotNull] string url,
+ [Localizable(false), CanBeNull] TData data, Action headersCallback = null,
+ CancellationToken cancellation = default) {
+ var serialized = data != null ? JsonConvert.SerializeObject(data) : null;
+ var request = await CreateHttpRequestAsync(method, url, serialized, cancellation).ConfigureAwait(false);
+ if (data != null) request.Content = new StringContent(serialized, Encoding.UTF8, "application/json");
+ return await RunRequest(request, headersCallback, cancellation).ConfigureAwait(false);
+ }
+ #endregion
+
+ #region Various public methods for REST API
+ public Task GetAsync([Localizable(false), NotNull] string url, CancellationToken cancellation = default) {
+ return RequestAsync