Skip to content

Commit c1db14a

Browse files
authored
Release 1.15.0
Release 1.15.0
2 parents 9834c0b + 3d4e584 commit c1db14a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+923
-172
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# GameVault App Changelog
22

3+
## 1.15.0
4+
Recommended Gamevault Server Version: `v14.0.0`
5+
### Changes
6+
7+
- Cloud Saves (+)
8+
- Added retrieval and installation of community themes
9+
- Added Community Themes Repository link to the settings
10+
- Bug fix: Youtube url not loading in gameview
11+
- Bug fix: Game window sometimes started in the background when launched via gamevault uri
12+
- Bug fix: Crash on saving user details
13+
- Bug fix: Removed all dotted keyboard focus lines
14+
- Bug fix: App protocol of the microsoft version won't start a install if a instance of gamevault is already running
15+
- Bug fix: The game executable selection was always empty if the ignore list had an empty string as an entry
16+
317
## 1.14.0
418
Recommended Gamevault Server Version: `v13.1.2`
519
### Changes

gamevault/App.xaml.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ private async void Application_Startup(object sender, StartupEventArgs e)
123123

124124
private void AppDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
125125
{
126-
ProcessShepherd.KillAllChildProcesses();
126+
ProcessShepherd.Instance.KillAllChildProcesses();
127127
#if DEBUG
128128
e.Handled = false;
129129
#else
@@ -280,7 +280,7 @@ private void Navigate_Tab_Click(Object sender, EventArgs e)
280280
private void ShutdownApp()
281281
{
282282
ShowToastMessage = false;
283-
ProcessShepherd.KillAllChildProcesses();
283+
ProcessShepherd.Instance.KillAllChildProcesses();
284284
if (m_Icon != null)
285285
{
286286
m_Icon.Icon.Dispose();

gamevault/AssemblyInfo.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
//(used if a resource is not found in the page,
1212
// app, or any theme specific resource dictionaries)
1313
)]
14-
[assembly: AssemblyVersion("1.14.0.0")]
14+
[assembly: AssemblyVersion("1.15.0.0")]
1515
[assembly: AssemblyCopyright("© Phalcode™. All Rights Reserved.")]
1616
#if DEBUG
1717
[assembly: XmlnsDefinition("debug-mode", "Namespace")]

gamevault/Helper/AnalyticsHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ public Dictionary<string, string> PrepareSettingsForAnalytics()
285285
{
286286
try
287287
{
288-
var propertiesToExclude = new[] { "Instance", "UserName", "RootPath", "ServerUrl", "License", "RegistrationUser", "SendAnonymousAnalytics", "IgnoreList", "Themes" };
288+
var propertiesToExclude = new[] { "Instance", "UserName", "RootPath", "ServerUrl", "License", "RegistrationUser", "SendAnonymousAnalytics", "IgnoreList", "Themes", "CommunityThemes" };
289289
var trimmedObject = SettingsViewModel.Instance.GetType()
290290
.GetProperties()
291291
.Where(prop => !propertiesToExclude.Contains(prop.Name))

gamevault/Helper/GameTimeTracker.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using gamevault.Models;
1+
using gamevault.Helper.Integrations;
2+
using gamevault.Models;
23
using gamevault.ViewModels;
34
using System;
45
using System.Collections.Generic;
@@ -89,7 +90,7 @@ private void TimerCallback(object sender, ElapsedEventArgs e)
8990
{
9091
try
9192
{
92-
if(AnyOfflineProgressToSend())
93+
if (AnyOfflineProgressToSend())
9394
{
9495
await SendOfflineProgess();
9596
}
@@ -98,6 +99,7 @@ private void TimerCallback(object sender, ElapsedEventArgs e)
9899
WebHelper.Put(@$"{SettingsViewModel.Instance.ServerUrl}/api/progresses/user/{LoginManager.Instance.GetCurrentUser().ID}/game/{gameid}/increment", string.Empty);
99100
}
100101
DiscordHelper.Instance.SyncGameWithDiscordPresence(gamesToCountUp, foundGames);
102+
await SaveGameHelper.Instance.BackupSaveGamesFromIds(gamesToCountUp);
101103
}
102104
catch (Exception ex)
103105
{
File renamed without changes.
File renamed without changes.
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
using gamevault.Models;
2+
using gamevault.UserControls;
3+
using gamevault.ViewModels;
4+
using gamevault.Windows;
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Diagnostics;
8+
using System.IO;
9+
using System.Linq;
10+
using System.Net.Http;
11+
using System.Net.Http.Headers;
12+
using System.Text;
13+
using System.Text.Json;
14+
using System.Text.Json.Serialization;
15+
using System.Threading.Tasks;
16+
using System.Windows.Media.Imaging;
17+
using Windows.Gaming.Input;
18+
using Windows.Gaming.Preview.GamesEnumeration;
19+
20+
namespace gamevault.Helper.Integrations
21+
{
22+
internal class SaveGameHelper
23+
{
24+
#region Singleton
25+
private static SaveGameHelper instance = null;
26+
private static readonly object padlock = new object();
27+
28+
public static SaveGameHelper Instance
29+
{
30+
get
31+
{
32+
lock (padlock)
33+
{
34+
if (instance == null)
35+
{
36+
instance = new SaveGameHelper();
37+
}
38+
return instance;
39+
}
40+
}
41+
}
42+
#endregion
43+
44+
private class SaveGameEntry
45+
{
46+
[JsonPropertyName("score")]
47+
public double Score { get; set; }
48+
}
49+
private List<int> runningGameIds = new List<int>();
50+
private SevenZipHelper zipHelper;
51+
internal SaveGameHelper()
52+
{
53+
zipHelper = new SevenZipHelper();
54+
}
55+
internal async Task<bool> RestoreBackup(int gameId, string installationDir)
56+
{
57+
if (!LoginManager.Instance.IsLoggedIn() || !SettingsViewModel.Instance.CloudSaves || !SettingsViewModel.Instance.License.IsActive())
58+
return false;
59+
60+
using (HttpClient client = new HttpClient())
61+
{
62+
try
63+
{
64+
client.Timeout = TimeSpan.FromSeconds(15);
65+
string installationId = GetGameInstallationId(installationDir);
66+
string[] auth = WebHelper.GetCredentials();
67+
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{auth[0]}:{auth[1]}")));
68+
client.DefaultRequestHeaders.Add("User-Agent", $"GameVault/{SettingsViewModel.Instance.Version}");
69+
string url = @$"{SettingsViewModel.Instance.ServerUrl}/api/savefiles/user/{LoginManager.Instance.GetCurrentUser()!.ID}/game/{gameId}";
70+
using (HttpResponseMessage response = await client.GetAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/savefiles/user/{LoginManager.Instance.GetCurrentUser()!.ID}/game/{gameId}", HttpCompletionOption.ResponseHeadersRead))
71+
{
72+
response.EnsureSuccessStatusCode();
73+
string fileName = response.Content.Headers.ContentDisposition.FileName.Split('_')[1].Split('.')[0];
74+
if (fileName != installationId)
75+
{
76+
string tempFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
77+
Directory.CreateDirectory(tempFolder);
78+
string archive = Path.Combine(tempFolder, "backup.zip");
79+
using (Stream contentStream = await response.Content.ReadAsStreamAsync(), fileStream = new FileStream(archive, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true))
80+
{
81+
await contentStream.CopyToAsync(fileStream);
82+
}
83+
84+
await zipHelper.ExtractArchive(archive, tempFolder);
85+
var mappingFile = Directory.GetFiles(tempFolder, "mapping.yaml", SearchOption.AllDirectories);
86+
string extractFolder = "";
87+
if (mappingFile.Length < 1)
88+
throw new Exception("no savegame extracted");
89+
90+
extractFolder = Path.GetDirectoryName(Path.GetDirectoryName(mappingFile[0]));
91+
Process process = new Process();
92+
ProcessShepherd.Instance.AddProcess(process);
93+
process.StartInfo = CreateProcessHeader();
94+
process.StartInfo.Arguments = $"restore --force --path \"{extractFolder}\"";
95+
process.Start();
96+
process.WaitForExit();
97+
ProcessShepherd.Instance.RemoveProcess(process);
98+
Directory.Delete(tempFolder, true);
99+
return true;
100+
}
101+
}
102+
}
103+
catch (Exception ex)
104+
{
105+
string statusCode = WebExceptionHelper.GetServerStatusCode(ex);
106+
if (statusCode == "405")
107+
{
108+
MainWindowViewModel.Instance.AppBarText = "Cloud Saves are not enabled on this Server.";
109+
}
110+
else if (statusCode != "404")
111+
{
112+
MainWindowViewModel.Instance.AppBarText = "Failed to restore cloud save";
113+
}
114+
}
115+
}
116+
return false;
117+
}
118+
private string GetGameInstallationId(string installationDir)
119+
{
120+
string metadataFile = Path.Combine(installationDir, "gamevault-exec");
121+
string installationId = Preferences.Get(AppConfigKey.InstallationId, metadataFile);
122+
if (string.IsNullOrWhiteSpace(installationId))
123+
{
124+
installationId = Guid.NewGuid().ToString();
125+
Preferences.Set(AppConfigKey.InstallationId, installationId, metadataFile);
126+
}
127+
return installationId;
128+
}
129+
internal async Task BackupSaveGamesFromIds(List<int> gameIds)
130+
{
131+
var removedIds = runningGameIds.Except(gameIds).ToList();
132+
133+
foreach (var removedId in removedIds)
134+
{
135+
if (!LoginManager.Instance.IsLoggedIn() || !SettingsViewModel.Instance.CloudSaves)
136+
{
137+
break;
138+
}
139+
try
140+
{
141+
MainWindowViewModel.Instance.AppBarText = "Uploading Savegame to the Server...";
142+
bool success = await BackupSaveGame(removedId);
143+
if (success)
144+
{
145+
MainWindowViewModel.Instance.AppBarText = "Successfully synchronized the cloud saves";
146+
}
147+
else
148+
{
149+
MainWindowViewModel.Instance.AppBarText = "Failed to upload your Savegame to the Server";
150+
}
151+
}
152+
catch (Exception ex)
153+
{
154+
}
155+
}
156+
157+
// Find IDs that are new and add them to the list
158+
var newIds = gameIds.Except(runningGameIds).ToList();
159+
runningGameIds.AddRange(newIds);
160+
161+
// Remove IDs that are no longer in the new list
162+
runningGameIds = runningGameIds.Intersect(gameIds).ToList();
163+
}
164+
internal async Task<bool> BackupSaveGame(int gameId)
165+
{
166+
if (!SettingsViewModel.Instance.License.IsActive())
167+
return false;
168+
169+
var installedGame = InstallViewModel.Instance?.InstalledGames?.FirstOrDefault(g => g.Key?.ID == gameId);
170+
string gameMetadataTitle = installedGame?.Key?.Metadata?.Title ?? "";
171+
string installationDir = installedGame?.Value ?? "";
172+
if (gameMetadataTitle != "" && installationDir != "")
173+
{
174+
string title = await SearchForLudusaviGameTitle(gameMetadataTitle);
175+
string tempFolder = await CreateBackup(title);
176+
string archive = Path.Combine(tempFolder, "backup.zip");
177+
if (Directory.GetFiles(tempFolder, "mapping.yaml", SearchOption.AllDirectories).Length == 0)
178+
{
179+
Directory.Delete(tempFolder, true);
180+
return false;
181+
}
182+
await zipHelper.PackArchive(tempFolder, archive);
183+
184+
bool success = await UploadSavegame(archive, gameId, installationDir);
185+
Directory.Delete(tempFolder, true);
186+
return success;
187+
}
188+
return false;
189+
}
190+
internal async Task<string> SearchForLudusaviGameTitle(string title)
191+
{
192+
return await Task.Run<string>(() =>
193+
{
194+
Process process = new Process();
195+
ProcessShepherd.Instance.AddProcess(process);
196+
process.StartInfo = CreateProcessHeader(true);
197+
process.StartInfo.Arguments = $"find \"{title}\" --fuzzy --api";//--normalized
198+
process.EnableRaisingEvents = true;
199+
200+
List<string> output = new List<string>();
201+
202+
process.ErrorDataReceived += (sender, e) =>
203+
{
204+
// Debug.WriteLine("ERROR:" + e.Data);
205+
};
206+
process.OutputDataReceived += (sender, e) =>
207+
{
208+
output.Add(e.Data);
209+
};
210+
process.Start();
211+
process.BeginOutputReadLine();
212+
process.BeginErrorReadLine();
213+
process.WaitForExit();
214+
ProcessShepherd.Instance.RemoveProcess(process);
215+
string jsonString = string.Join("", output).Trim();
216+
var entries = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, SaveGameEntry>>>(jsonString);
217+
if (entries?.Count > 0 && entries.Values.Count > 0 && entries.Values.First().Values.Count > 0 && entries.Values.First().Values.First().Score > 0.9d)//Make sure Score is set and over 0.9
218+
{
219+
string lunusaviTitle = entries.Values.First().Keys.First();
220+
return lunusaviTitle;
221+
222+
}
223+
return "";
224+
});
225+
}
226+
private async Task<string> CreateBackup(string lunusaviTitle)
227+
{
228+
return await Task.Run<string>(() =>
229+
{
230+
string tempFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
231+
232+
Process process = new Process();
233+
ProcessShepherd.Instance.AddProcess(process);
234+
process.StartInfo = CreateProcessHeader();
235+
process.StartInfo.Arguments = $"backup --force --format \"zip\" --path \"{tempFolder}\" \"{lunusaviTitle}\"";
236+
process.Start();
237+
process.WaitForExit();
238+
ProcessShepherd.Instance.RemoveProcess(process);
239+
return tempFolder;
240+
});
241+
}
242+
private async Task<bool> UploadSavegame(string saveFilePath, int gameId, string installationDir)
243+
{
244+
try
245+
{
246+
string installationId = GetGameInstallationId(installationDir);
247+
using (MemoryStream memoryStream = await FileToMemoryStreamAsync(saveFilePath))
248+
{
249+
await WebHelper.UploadFileAsync(@$"{SettingsViewModel.Instance.ServerUrl}/api/savefiles/user/{LoginManager.Instance.GetCurrentUser()!.ID}/game/{gameId}", memoryStream, "x.zip", new KeyValuePair<string, string>("X-Installation-Id", installationId));
250+
}
251+
}
252+
catch
253+
{
254+
return false;
255+
}
256+
return true;
257+
}
258+
private async Task<MemoryStream> FileToMemoryStreamAsync(string filePath)
259+
{
260+
if (!File.Exists(filePath))
261+
throw new FileNotFoundException("File not found", filePath);
262+
263+
MemoryStream memoryStream = new MemoryStream();
264+
using (FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
265+
{
266+
await fileStream.CopyToAsync(memoryStream);
267+
}
268+
memoryStream.Position = 0; // Reset position to beginning
269+
return memoryStream;
270+
}
271+
private ProcessStartInfo CreateProcessHeader(bool redirectConsole = false)
272+
{
273+
ProcessStartInfo info = new ProcessStartInfo();
274+
info.CreateNoWindow = true;
275+
info.RedirectStandardOutput = redirectConsole;
276+
info.RedirectStandardError = redirectConsole;
277+
info.UseShellExecute = false;
278+
info.FileName = $"{AppDomain.CurrentDomain.BaseDirectory}Lib\\savegame\\ludusavi.exe";
279+
return info;
280+
}
281+
}
282+
}

0 commit comments

Comments
 (0)