Skip to content

Commit f48517a

Browse files
committed
Add Battle.net plugin
1 parent 56440cb commit f48517a

19 files changed

+1355
-7
lines changed

GameLib.Demo/GameLib.Demo.Console/GameLib.Demo.Console.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
</ItemGroup>
1515

1616
<ItemGroup>
17-
<ProjectReference Include="..\..\GameLib.Core\GameLib.Core.csproj" />
1817
<ProjectReference Include="..\..\GameLib\GameLib.csproj" />
18+
<ProjectReference Include="..\..\GameLib.Plugin\GameLib.Plugin.BattleNet\GameLib.Plugin.BattleNet.csproj" />
1919
<ProjectReference Include="..\..\GameLib.Plugin\GameLib.Plugin.Origin\GameLib.Plugin.Origin.csproj" />
2020
<ProjectReference Include="..\..\GameLib.Plugin\GameLib.Plugin.Ubisoft\GameLib.Plugin.Ubisoft.csproj" />
2121
<ProjectReference Include="..\..\GameLib.Plugin\GameLib.Plugin.Steam\GameLib.Plugin.Steam.csproj" />

GameLib.Demo/GameLib.Demo.Wpf/GameLib.Demo.Wpf.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
</ItemGroup>
6060

6161
<ItemGroup>
62+
<ProjectReference Include="..\..\GameLib.Plugin\GameLib.Plugin.BattleNet\GameLib.Plugin.BattleNet.csproj" />
6263
<ProjectReference Include="..\..\GameLib.Plugin\GameLib.Plugin.Epic\GameLib.Plugin.Epic.csproj" />
6364
<ProjectReference Include="..\..\GameLib.Plugin\GameLib.Plugin.Gog\GameLib.Plugin.Gog.csproj" />
6465
<ProjectReference Include="..\..\GameLib.Plugin\GameLib.Plugin.Origin\GameLib.Plugin.Origin.csproj" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
using Gamelib.Core.Util;
2+
using GameLib.Core;
3+
using GameLib.Plugin.BattleNet.Model;
4+
using Newtonsoft.Json;
5+
using System.Reflection;
6+
7+
namespace GameLib.Plugin.BattleNet;
8+
9+
public static class BattleNetGameFactory
10+
{
11+
public static IEnumerable<BattleNetGame> GetGames(ILauncher launcher, CancellationToken cancellationToken = default)
12+
{
13+
var catalog = GetCatalog();
14+
15+
return DeserializeProductInstalls()
16+
//.AsParallel()
17+
//.WithCancellation(cancellationToken)
18+
.Select(product => BattleNetGameBuiler(launcher, product))
19+
.Where(game => game is not null)
20+
.Select(game => AddLauncherId(launcher, game))
21+
.Select(game => AddCatalogData(launcher, game, catalog))
22+
.ToList()!;
23+
}
24+
25+
/// <summary>
26+
/// Add launcher ID to Game
27+
/// </summary>
28+
private static BattleNetGame AddLauncherId(ILauncher launcher, BattleNetGame game)
29+
{
30+
game.LauncherId = launcher.Id;
31+
return game;
32+
}
33+
34+
/// <summary>
35+
/// Load local catalog data
36+
/// </summary>
37+
private static BNetGames? GetCatalog()
38+
{
39+
var path = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? ".", "Resources", "BattleNetGames.json");
40+
41+
if (!File.Exists(path))
42+
{
43+
return null;
44+
}
45+
46+
try
47+
{
48+
var json = File.ReadAllText(path);
49+
var catalog = JsonConvert.DeserializeObject<BNetGames>(json);
50+
51+
if (catalog is null)
52+
{
53+
throw new ApplicationException("Cannot deserialize JSON stream");
54+
}
55+
56+
return catalog;
57+
}
58+
catch { return null; }
59+
}
60+
61+
/// <summary>
62+
/// Deserialize the BattlNet product.db
63+
/// </summary>
64+
private static IEnumerable<ProductInstall> DeserializeProductInstalls()
65+
{
66+
var productInstalls = new List<ProductInstall>();
67+
68+
var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Battle.net", "Agent", "product.db");
69+
70+
if (!File.Exists(path))
71+
{
72+
throw new FileNotFoundException("Configuration file not found, probably Battle.net client hasn't been started at least once.", path);
73+
}
74+
75+
using var file = File.OpenRead(path);
76+
productInstalls = ProtoBuf.Serializer.Deserialize<List<ProductInstall>>(file);
77+
78+
return productInstalls.Where(p => p.Uid is not "agent" and not "battle.net");
79+
}
80+
81+
/// <summary>
82+
/// Convert entry from product.db into a BattleNetGame object
83+
/// </summary>
84+
private static BattleNetGame BattleNetGameBuiler(ILauncher launcher, ProductInstall productInstall)
85+
{
86+
return new BattleNetGame()
87+
{
88+
Id = productInstall.Uid,
89+
Name = Path.GetFileName(PathUtil.Sanitize(productInstall.Settings.installPath)) ?? string.Empty,
90+
InstallDir = PathUtil.Sanitize(productInstall.Settings.installPath) ?? string.Empty,
91+
WorkingDir = PathUtil.Sanitize(productInstall.Settings.installPath) ?? string.Empty,
92+
InstallDate = PathUtil.GetCreationTime(productInstall.Settings.installPath) ?? DateTime.MinValue,
93+
LaunchString = $"\"{launcher.ExecutablePath}\" --game={productInstall.productCode.ToUpper()}",
94+
ProductCode = productInstall.productCode ?? string.Empty,
95+
PlayRegion = productInstall.Settings.playRegion ?? string.Empty,
96+
SpeechLanguage = productInstall.Settings.selectedSpeechLanguage ?? string.Empty,
97+
TextLanguage = productInstall.Settings.selectedTextLanguage ?? string.Empty,
98+
Version = productInstall.cachedProductState.baseProductState.currentVersionStr ?? string.Empty
99+
};
100+
}
101+
102+
/// <summary>
103+
/// Get the executable and game name from the catalog
104+
/// </summary>
105+
private static BattleNetGame AddCatalogData(ILauncher launcher, BattleNetGame game, BNetGames? catalog = null)
106+
{
107+
if (catalog?.Games
108+
.Where(g => g.InternalId == game.Id)
109+
.FirstOrDefault(defaultValue: null) is not BNetGame catalogItem)
110+
{
111+
return game;
112+
}
113+
114+
if (!string.IsNullOrEmpty(catalogItem.Name))
115+
{
116+
game.Name = catalogItem.Name;
117+
}
118+
119+
if (!string.IsNullOrEmpty(catalogItem.ProductId))
120+
{
121+
game.ProductCode = catalogItem.ProductId;
122+
game.LaunchString = $"\"{launcher.ExecutablePath}\" --exec=\"launch {game.ProductCode}\"";
123+
}
124+
125+
game.Executable = catalogItem.Executables.FirstOrDefault(defaultValue: string.Empty);
126+
if (!string.IsNullOrEmpty(game.Executable))
127+
{
128+
game.ExecutablePath = Path.Combine(game.InstallDir, game.Executable);
129+
}
130+
131+
return game;
132+
}
133+
134+
135+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
using Gamelib.Core.Util;
2+
using GameLib.Core;
3+
using System.ComponentModel.Composition;
4+
using System.Diagnostics;
5+
using System.Drawing;
6+
using System.Runtime.InteropServices;
7+
8+
namespace GameLib.Plugin.BattleNet;
9+
10+
[Guid("3BF9899A-AF88-4935-893C-3B99A577A565")]
11+
[Export(typeof(ILauncher))]
12+
public class BattleNetLauncher : ILauncher
13+
{
14+
[ImportingConstructor]
15+
public BattleNetLauncher(LauncherOptions? launcherOptions)
16+
{
17+
LauncherOptions = launcherOptions ?? new LauncherOptions();
18+
}
19+
20+
#region Interface implementations
21+
public LauncherOptions LauncherOptions { get; }
22+
23+
public Guid Id => GetType().GUID;
24+
25+
public string Name => "Battle.net";
26+
27+
public Image Logo => Properties.Resources.Logo128px;
28+
29+
public bool IsInstalled { get; private set; }
30+
31+
public bool IsRunning => ProcessUtil.IsProcessRunning(ExecutablePath);
32+
33+
public string InstallDir { get; private set; } = string.Empty;
34+
35+
public string ExecutablePath { get; private set; } = string.Empty;
36+
37+
public string Executable { get; private set; } = string.Empty;
38+
39+
public Icon? ExecutableIcon => PathUtil.GetFileIcon(ExecutablePath);
40+
41+
public IEnumerable<IGame> Games { get; private set; } = Enumerable.Empty<IGame>();
42+
43+
public void Refresh(CancellationToken cancellationToken = default)
44+
{
45+
ExecutablePath = string.Empty;
46+
Executable = string.Empty;
47+
InstallDir = string.Empty;
48+
IsInstalled = false;
49+
Games = Enumerable.Empty<IGame>();
50+
51+
ExecutablePath = GetExecutable() ?? string.Empty;
52+
if (!string.IsNullOrEmpty(ExecutablePath))
53+
{
54+
Executable = Path.GetFileName(ExecutablePath);
55+
InstallDir = Path.GetDirectoryName(ExecutablePath) ?? string.Empty;
56+
IsInstalled = File.Exists(ExecutablePath);
57+
Games = BattleNetGameFactory.GetGames(this, cancellationToken);
58+
}
59+
}
60+
61+
public bool Start() => IsRunning || ProcessUtil.StartProcess(ExecutablePath);
62+
63+
public void Stop()
64+
{
65+
if (IsRunning)
66+
{
67+
Process.Start(ExecutablePath, "--exec=shutdown");
68+
}
69+
}
70+
#endregion
71+
72+
#region Private methods
73+
private static string? GetExecutable()
74+
{
75+
string? executablePath = null;
76+
77+
executablePath ??= RegistryUtil.GetShellCommand("battlenet");
78+
executablePath ??= RegistryUtil.GetShellCommand("blizzard");
79+
executablePath ??= RegistryUtil.GetShellCommand("Blizzard.URI.Battlenet");
80+
executablePath ??= RegistryUtil.GetShellCommand("Blizzard.URI.Blizzard");
81+
82+
executablePath = PathUtil.Sanitize(executablePath);
83+
84+
if (!PathUtil.IsExecutable(executablePath))
85+
{
86+
executablePath = null;
87+
}
88+
89+
return executablePath;
90+
}
91+
#endregion
92+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net6.0-windows</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<IsPackable>false</IsPackable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<ProjectReference Include="..\..\GameLib.Core\GameLib.Core.csproj" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<PackageReference Include="System.ComponentModel.Composition" Version="6.0.0" />
16+
<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
17+
<PackageReference Include="System.Resources.Extensions" Version="6.0.0" />
18+
<PackageReference Include="protobuf-net" Version="3.1.17" />
19+
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
20+
</ItemGroup>
21+
22+
<ItemGroup>
23+
<Compile Update="Properties\Resources.Designer.cs">
24+
<DesignTime>True</DesignTime>
25+
<AutoGen>True</AutoGen>
26+
<DependentUpon>Resources.resx</DependentUpon>
27+
</Compile>
28+
</ItemGroup>
29+
30+
<ItemGroup>
31+
<EmbeddedResource Update="Properties\Resources.resx">
32+
<Generator>PublicResXFileCodeGenerator</Generator>
33+
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
34+
</EmbeddedResource>
35+
</ItemGroup>
36+
37+
<ItemGroup>
38+
<None Update="Resources\BattleNetGames.json">
39+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
40+
</None>
41+
</ItemGroup>
42+
43+
44+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using Newtonsoft.Json;
2+
3+
namespace GameLib.Plugin.BattleNet.Model;
4+
5+
internal class BNetGames
6+
{
7+
[JsonProperty("Games")]
8+
public List<BNetGame> Games { get; set; } = new();
9+
}
10+
11+
internal class BNetGame
12+
{
13+
[JsonProperty("InternalId")]
14+
public string? InternalId { get; set; }
15+
16+
[JsonProperty("ProductId")]
17+
public string? ProductId { get; set; }
18+
19+
[JsonProperty("Name")]
20+
public string? Name { get; set; }
21+
22+
[JsonProperty("Executables")]
23+
public List<string> Executables { get; set; } = new();
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using Gamelib.Core.Util;
2+
using GameLib.Core;
3+
using System.Drawing;
4+
5+
namespace GameLib.Plugin.BattleNet.Model;
6+
7+
public class BattleNetGame : IGame
8+
{
9+
#region Interface implementations
10+
public string Id { get; internal set; } = string.Empty;
11+
12+
public Guid LauncherId { get; internal set; } = Guid.Empty;
13+
14+
public string Name { get; internal set; } = string.Empty;
15+
16+
public string InstallDir { get; internal set; } = string.Empty;
17+
18+
public string ExecutablePath { get; internal set; } = string.Empty;
19+
20+
public string Executable { get; internal set; } = string.Empty;
21+
22+
public Icon? ExecutableIcon => PathUtil.GetFileIcon(ExecutablePath);
23+
24+
public string WorkingDir { get; internal set; } = string.Empty;
25+
26+
public string LaunchString { get; internal set; } = string.Empty;
27+
28+
public DateTime InstallDate { get; internal set; } = DateTime.MinValue;
29+
30+
public bool IsRunning => ProcessUtil.IsProcessRunning(Executable);
31+
#endregion
32+
33+
public string ProductCode { get; internal set; } = string.Empty;
34+
35+
public string SpeechLanguage { get; internal set; } = string.Empty;
36+
37+
public string TextLanguage { get; internal set; } = string.Empty;
38+
39+
public string PlayRegion { get; internal set; } = string.Empty;
40+
41+
public string Version { get; internal set; } = string.Empty;
42+
}

0 commit comments

Comments
 (0)