Extracts reward config from bot and rewarder

This commit is contained in:
benbierens 2024-01-29 11:02:47 -05:00
parent 43a160a9cc
commit acc9526cd5
No known key found for this signature in database
GPG Key ID: FE44815D96D0A1AA
24 changed files with 294 additions and 203 deletions

View File

@ -1,50 +0,0 @@
using Utils;
namespace TestNetRewarder
{
public class RewardConfig
{
public RewardConfig(ulong rewardId, ICheck check)
{
RewardId = rewardId;
Check = check;
}
public ulong RewardId { get; }
public ICheck Check { get; }
}
public class RewardRepo
{
public RewardConfig[] Rewards { get; } = new RewardConfig[]
{
// Filled any slot
new RewardConfig(123, new FilledAnySlotCheck()),
// Finished any slot
new RewardConfig(124, new FinishedSlotCheck(
minSize: 0.Bytes(),
minDuration: TimeSpan.Zero)),
// Finished a sizable slot
new RewardConfig(125, new FinishedSlotCheck(
minSize: 1.GB(),
minDuration: TimeSpan.FromHours(24.0))),
// Posted any contract
new RewardConfig(126, new PostedContractCheck()),
// Started any contract
new RewardConfig(127, new StartedContractCheck(
minNumberOfHosts: 1,
minSlotSize: 0.Bytes(),
minDuration: TimeSpan.Zero)),
// Started a sizable contract
new RewardConfig(127, new StartedContractCheck(
minNumberOfHosts: 4,
minSlotSize: 1.GB(),
minDuration: TimeSpan.FromHours(24.0)))
};
}
}

View File

@ -1,16 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Framework\ArgsUniform\ArgsUniform.csproj" />
<ProjectReference Include="..\Framework\Logging\Logging.csproj" />
<ProjectReference Include="..\Tools\BiblioTech\BiblioTech.csproj" />
</ItemGroup>
</Project>

View File

@ -10,8 +10,9 @@
<ItemGroup>
<PackageReference Include="Discord.Net" Version="3.12.0" />
<ProjectReference Include="..\..\Framework\ArgsUniform\ArgsUniform.csproj" />
<ProjectReference Include="..\..\GethConnector\GethConnector.csproj" />
<ProjectReference Include="..\..\ProjectPlugins\CodexPlugin\CodexPlugin.csproj" />
<ProjectReference Include="..\DiscordRewards\DiscordRewards.csproj" />
<ProjectReference Include="..\GethConnector\GethConnector.csproj" />
</ItemGroup>
</Project>

View File

@ -1,4 +1,5 @@
using Newtonsoft.Json;
using DiscordRewards;
using Newtonsoft.Json;
using System.Net;
using TaskFactory = Utils.TaskFactory;
@ -79,31 +80,10 @@ namespace BiblioTech.Rewards
var rewards = JsonConvert.DeserializeObject<GiveRewardsCommand>(content);
if (rewards != null)
{
AttachUsers(rewards);
await ProcessRewards(rewards);
}
}
private void AttachUsers(GiveRewardsCommand rewards)
{
foreach (var reward in rewards.Rewards)
{
reward.Users = reward.UserAddresses.Select(GetUserFromAddress).Where(u => u != null).Cast<UserData>().ToArray();
}
}
private UserData? GetUserFromAddress(string address)
{
try
{
return Program.UserRepo.GetUserDataForAddress(new GethPlugin.EthAddress(address));
}
catch
{
return null;
}
}
private async Task ProcessRewards(GiveRewardsCommand rewards)
{
await roleController.GiveRewards(rewards);

View File

@ -1,48 +0,0 @@
namespace BiblioTech.Rewards
{
public class RewardsRepo
{
private static string Tag => RoleController.UsernameTag;
public RoleRewardConfig[] Rewards { get; }
public RewardsRepo()
{
Rewards = new[]
{
// Join reward
new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"),
//// Hosting:
//// Filled any slot:
//new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"),
//// Finished any slot:
//new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"),
//// Finished a min-256MB min-8h slot:
//new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"),
//// Finished a min-64GB min-24h slot:
//new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"),
//// Oops:
//// Missed a storage proof:
//new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"),
//// Clienting:
//// Posted any contract:
//new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"),
//// Posted any contract that reached started state:
//new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"),
//// Started a contract with min-4 hosts, min-256MB per host, min-8h duration:
//new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"),
//// Started a contract with min-4 hosts, min-64GB per host, min-24h duration:
//new RoleRewardConfig(1187039439558541498, $"Congratulations {Tag}, you got the test-reward!"),
};
}
}
}

View File

@ -1,14 +1,14 @@
using Discord;
using Discord.WebSocket;
using DiscordRewards;
namespace BiblioTech.Rewards
{
public class RoleController : IDiscordRoleController
{
public const string UsernameTag = "<USER>";
private readonly DiscordSocketClient client;
private readonly SocketTextChannel? rewardsChannel;
private readonly RewardsRepo repo = new RewardsRepo();
private readonly RewardRepo repo = new RewardRepo();
public RoleController(DiscordSocketClient client)
{
@ -30,7 +30,7 @@ namespace BiblioTech.Rewards
LookUpAllRoles(guild, rewards),
rewardsChannel);
await context.ProcessGiveRewardsCommand(rewards);
await context.ProcessGiveRewardsCommand(LookUpUsers(rewards));
}
private async Task<Dictionary<ulong, IGuildUser>> LoadAllUsers(SocketGuild guild)
@ -47,19 +47,19 @@ namespace BiblioTech.Rewards
return result;
}
private Dictionary<ulong, RoleRewardConfig> LookUpAllRoles(SocketGuild guild, GiveRewardsCommand rewards)
private Dictionary<ulong, RoleReward> LookUpAllRoles(SocketGuild guild, GiveRewardsCommand rewards)
{
var result = new Dictionary<ulong, RoleRewardConfig>();
var result = new Dictionary<ulong, RoleReward>();
foreach (var r in rewards.Rewards)
{
var role = repo.Rewards.SingleOrDefault(rr => rr.RoleId == r.RewardId);
if (role == null)
if (!result.ContainsKey(r.RewardId))
{
Program.Log.Log($"No RoleReward is configured for reward with id '{r.RewardId}'.");
}
else
{
if (role.SocketRole == null)
var rewardConfig = repo.Rewards.SingleOrDefault(rr => rr.RoleId == r.RewardId);
if (rewardConfig == null)
{
Program.Log.Log($"No Reward is configured for id '{r.RewardId}'.");
}
else
{
var socketRole = guild.GetRole(r.RewardId);
if (socketRole == null)
@ -68,48 +68,99 @@ namespace BiblioTech.Rewards
}
else
{
role.SocketRole = socketRole;
result.Add(r.RewardId, new RoleReward(socketRole, rewardConfig));
}
}
result.Add(role.RoleId, role);
}
}
return result;
}
private UserReward[] LookUpUsers(GiveRewardsCommand rewards)
{
return rewards.Rewards.Select(LookUpUserData).ToArray();
}
private UserReward LookUpUserData(RewardUsersCommand command)
{
return new UserReward(command,
command.UserAddresses
.Select(LookUpUserDataForAddress)
.Where(d => d != null)
.Cast<UserData>()
.ToArray());
}
private UserData? LookUpUserDataForAddress(string address)
{
try
{
return Program.UserRepo.GetUserDataForAddress(new GethPlugin.EthAddress(address));
}
catch (Exception ex)
{
Program.Log.Error("Error during UserData lookup: " + ex);
return null;
}
}
private SocketGuild GetGuild()
{
return client.Guilds.Single(g => g.Name == Program.Config.ServerName);
}
}
public class RoleReward
{
public RoleReward(SocketRole socketRole, RewardConfig reward)
{
SocketRole = socketRole;
Reward = reward;
}
public SocketRole SocketRole { get; }
public RewardConfig Reward { get; }
}
public class UserReward
{
public UserReward(RewardUsersCommand rewardCommand, UserData[] users)
{
RewardCommand = rewardCommand;
Users = users;
}
public RewardUsersCommand RewardCommand { get; }
public UserData[] Users { get; }
}
public class RewardContext
{
private readonly Dictionary<ulong, IGuildUser> users;
private readonly Dictionary<ulong, RoleRewardConfig> roles;
private readonly Dictionary<ulong, RoleReward> roles;
private readonly SocketTextChannel? rewardsChannel;
public RewardContext(Dictionary<ulong, IGuildUser> users, Dictionary<ulong, RoleRewardConfig> roles, SocketTextChannel? rewardsChannel)
public RewardContext(Dictionary<ulong, IGuildUser> users, Dictionary<ulong, RoleReward> roles, SocketTextChannel? rewardsChannel)
{
this.users = users;
this.roles = roles;
this.rewardsChannel = rewardsChannel;
}
public async Task ProcessGiveRewardsCommand(GiveRewardsCommand rewards)
public async Task ProcessGiveRewardsCommand(UserReward[] rewards)
{
foreach (var rewardCommand in rewards.Rewards)
foreach (var rewardCommand in rewards)
{
if (roles.ContainsKey(rewardCommand.RewardId))
if (roles.ContainsKey(rewardCommand.RewardCommand.RewardId))
{
var role = roles[rewardCommand.RewardId];
var role = roles[rewardCommand.RewardCommand.RewardId];
await ProcessRewardCommand(role, rewardCommand);
}
}
}
private async Task ProcessRewardCommand(RoleRewardConfig role, RewardUsersCommand reward)
private async Task ProcessRewardCommand(RoleReward role, UserReward reward)
{
foreach (var user in reward.Users)
{
@ -117,7 +168,7 @@ namespace BiblioTech.Rewards
}
}
private async Task GiveReward(RoleRewardConfig role, UserData user)
private async Task GiveReward(RoleReward role, UserData user)
{
if (!users.ContainsKey(user.DiscordId))
{
@ -128,9 +179,9 @@ namespace BiblioTech.Rewards
var guildUser = users[user.DiscordId];
var alreadyHas = guildUser.RoleIds.ToArray();
if (alreadyHas.Any(r => r == role.RoleId)) return;
if (alreadyHas.Any(r => r == role.Reward.RoleId)) return;
await GiveRole(guildUser, role.SocketRole!);
await GiveRole(guildUser, role.SocketRole);
await SendNotification(role, user, guildUser);
await Task.Delay(1000);
}
@ -148,33 +199,20 @@ namespace BiblioTech.Rewards
}
}
private async Task SendNotification(RoleRewardConfig reward, UserData userData, IGuildUser user)
private async Task SendNotification(RoleReward reward, UserData userData, IGuildUser user)
{
try
{
if (userData.NotificationsEnabled && rewardsChannel != null)
{
var msg = reward.Message.Replace(RoleController.UsernameTag, $"<@{user.Id}>");
var msg = reward.Reward.Message.Replace(RewardConfig.UsernameTag, $"<@{user.Id}>");
await rewardsChannel.SendMessageAsync(msg);
}
}
catch (Exception ex)
{
Program.Log.Error($"Failed to notify user '{user.DisplayName}' about role '{reward.SocketRole!.Name}': {ex}");
Program.Log.Error($"Failed to notify user '{user.DisplayName}' about role '{reward.SocketRole.Name}': {ex}");
}
}
}
public class RoleRewardConfig
{
public RoleRewardConfig(ulong roleId, string message)
{
RoleId = roleId;
Message = message;
}
public ulong RoleId { get; }
public string Message { get; }
public SocketRole? SocketRole { get; set; }
}
}

View File

@ -0,0 +1,21 @@
using Utils;
namespace DiscordRewards
{
public class CheckConfig
{
public CheckType Type { get; set; }
public ulong MinNumberOfHosts { get; set; }
public ByteSize MinSlotSize { get; set; } = 0.Bytes();
public TimeSpan MinDuration { get; set; } = TimeSpan.Zero;
}
public enum CheckType
{
Uninitialized,
FilledSlot,
FinishedSlot,
PostedContract,
StartedContract,
}
}

View File

@ -7,8 +7,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Framework\Logging\Logging.csproj" />
<ProjectReference Include="..\ProjectPlugins\CodexContractsPlugin\CodexContractsPlugin.csproj" />
<ProjectReference Include="..\..\Framework\Utils\Utils.csproj" />
</ItemGroup>
</Project>

View File

@ -1,6 +1,4 @@
using Newtonsoft.Json;
namespace BiblioTech.Rewards
namespace DiscordRewards
{
public class GiveRewardsCommand
{
@ -11,8 +9,5 @@ namespace BiblioTech.Rewards
{
public ulong RewardId { get; set; }
public string[] UserAddresses { get; set; } = Array.Empty<string>();
[JsonIgnore]
public UserData[] Users { get; set; } = Array.Empty<UserData>();
}
}

View File

@ -0,0 +1,18 @@
namespace DiscordRewards
{
public class RewardConfig
{
public const string UsernameTag = "<USER>";
public RewardConfig(ulong roleId, string message, CheckConfig checkConfig)
{
RoleId = roleId;
Message = message;
CheckConfig = checkConfig;
}
public ulong RoleId { get; }
public string Message { get; }
public CheckConfig CheckConfig { get; }
}
}

View File

@ -0,0 +1,53 @@
using Utils;
namespace DiscordRewards
{
public class RewardRepo
{
private static string Tag => RewardConfig.UsernameTag;
public RewardConfig[] Rewards { get; } = new RewardConfig[]
{
// Filled any slot
new RewardConfig(123, "Filled a slot", new CheckConfig
{
Type = CheckType.FilledSlot
}),
// Finished any slot
new RewardConfig(124, "Finished any slot", new CheckConfig
{
Type = CheckType.FinishedSlot
}),
// Finished a sizable slot
new RewardConfig(125, "Finished sizable slot", new CheckConfig
{
Type = CheckType.FinishedSlot,
MinSlotSize = 1.GB(),
MinDuration = TimeSpan.FromHours(24.0),
}),
// Posted any contract
new RewardConfig(126, "Posted any contract", new CheckConfig
{
Type = CheckType.PostedContract
}),
// Started any contract
new RewardConfig(127, "Started any contract", new CheckConfig
{
Type = CheckType.StartedContract
}),
// Started a sizable contract
new RewardConfig(125, "Started sizable contract", new CheckConfig
{
Type = CheckType.FinishedSlot,
MinNumberOfHosts = 4,
MinSlotSize = 1.GB(),
MinDuration = TimeSpan.FromHours(24.0),
})
};
}
}

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Framework\Logging\Logging.csproj" />
<ProjectReference Include="..\..\ProjectPlugins\CodexContractsPlugin\CodexContractsPlugin.csproj" />
<ProjectReference Include="..\..\ProjectPlugins\GethPlugin\GethPlugin.csproj" />
</ItemGroup>
</Project>

View File

@ -1,4 +1,4 @@
using BiblioTech.Rewards;
using DiscordRewards;
using Logging;
using Newtonsoft.Json;

View File

@ -1,4 +1,5 @@
using GethPlugin;
using CodexContractsPlugin.Marketplace;
using GethPlugin;
using NethereumWorkflow;
using Utils;
@ -54,9 +55,44 @@ namespace TestNetRewarder
public class PostedContractCheck : ICheck
{
private readonly ulong minNumberOfHosts;
private readonly ByteSize minSlotSize;
private readonly TimeSpan minDuration;
public PostedContractCheck(ulong minNumberOfHosts, ByteSize minSlotSize, TimeSpan minDuration)
{
this.minNumberOfHosts = minNumberOfHosts;
this.minSlotSize = minSlotSize;
this.minDuration = minDuration;
}
public EthAddress[] Check(ChainState state)
{
return state.NewRequests.Select(r => r.ClientAddress).ToArray();
return state.NewRequests
.Where(r =>
MeetsNumSlotsRequirement(r) &&
MeetsSizeRequirement(r) &&
MeetsDurationRequirement(r))
.Select(r => r.ClientAddress)
.ToArray();
}
private bool MeetsNumSlotsRequirement(Request r)
{
return r.Ask.Slots >= minNumberOfHosts;
}
private bool MeetsSizeRequirement(Request r)
{
var slotSize = r.Ask.SlotSize.ToDecimal();
decimal min = minSlotSize.SizeInBytes;
return slotSize >= min;
}
private bool MeetsDurationRequirement(Request r)
{
var duration = TimeSpan.FromSeconds((double)r.Ask.Duration);
return duration >= minDuration;
}
}

View File

@ -1,4 +1,5 @@
using BiblioTech.Rewards;
using DiscordRewards;
using GethPlugin;
using Logging;
using Newtonsoft.Json;
using Utils;
@ -60,15 +61,38 @@ namespace TestNetRewarder
private void ProcessReward(List<RewardUsersCommand> outgoingRewards, RewardConfig reward, ChainState chainState)
{
var winningAddresses = reward.Check.Check(chainState);
var winningAddresses = PerformCheck(reward, chainState);
if (winningAddresses.Any())
{
outgoingRewards.Add(new RewardUsersCommand
{
RewardId = reward.RewardId,
RewardId = reward.RoleId,
UserAddresses = winningAddresses.Select(a => a.Address).ToArray()
});
}
}
private EthAddress[] PerformCheck(RewardConfig reward, ChainState chainState)
{
var check = GetCheck(reward.CheckConfig);
return check.Check(chainState);
}
private ICheck GetCheck(CheckConfig config)
{
switch (config.Type)
{
case CheckType.FilledSlot:
return new FilledAnySlotCheck();
case CheckType.FinishedSlot:
return new FinishedSlotCheck(config.MinSlotSize, config.MinDuration);
case CheckType.PostedContract:
return new PostedContractCheck(config.MinNumberOfHosts, config.MinSlotSize, config.MinDuration);
case CheckType.StartedContract:
return new StartedContractCheck(config.MinNumberOfHosts, config.MinSlotSize, config.MinDuration);
}
throw new Exception("Unknown check type: " + config.Type);
}
}
}

View File

@ -1,4 +1,5 @@
using ArgsUniform;
using GethConnector;
using Logging;
using Utils;

View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Framework\ArgsUniform\ArgsUniform.csproj" />
<ProjectReference Include="..\..\Framework\Logging\Logging.csproj" />
<ProjectReference Include="..\DiscordRewards\DiscordRewards.csproj" />
<ProjectReference Include="..\GethConnector\GethConnector.csproj" />
</ItemGroup>
</Project>

View File

@ -53,9 +53,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DeployAndRunPlugin", "Proje
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FrameworkTests", "Tests\FrameworkTests\FrameworkTests.csproj", "{25E72004-4D71-4D1E-A193-FC125D12FF96}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestNetRewarder", "TestNetRewarder\TestNetRewarder.csproj", "{27B56A82-E8CE-4B50-9746-D574BAD646A2}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscordRewards", "Tools\DiscordRewards\DiscordRewards.csproj", "{497352BE-0A1D-4D83-8E21-A07B4A80F2F3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GethConnector", "GethConnector\GethConnector.csproj", "{04F2D26E-0768-4F93-9A1A-834089646B56}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GethConnector", "Tools\GethConnector\GethConnector.csproj", "{41811923-AF3A-4CC4-B508-D90EC2AFEC55}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestNetRewarder", "Tools\TestNetRewarder\TestNetRewarder.csproj", "{570C0DBE-0EF1-47B5-9A3B-E1F7895722A5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -147,14 +149,18 @@ Global
{25E72004-4D71-4D1E-A193-FC125D12FF96}.Debug|Any CPU.Build.0 = Debug|Any CPU
{25E72004-4D71-4D1E-A193-FC125D12FF96}.Release|Any CPU.ActiveCfg = Release|Any CPU
{25E72004-4D71-4D1E-A193-FC125D12FF96}.Release|Any CPU.Build.0 = Release|Any CPU
{27B56A82-E8CE-4B50-9746-D574BAD646A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{27B56A82-E8CE-4B50-9746-D574BAD646A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{27B56A82-E8CE-4B50-9746-D574BAD646A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{27B56A82-E8CE-4B50-9746-D574BAD646A2}.Release|Any CPU.Build.0 = Release|Any CPU
{04F2D26E-0768-4F93-9A1A-834089646B56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{04F2D26E-0768-4F93-9A1A-834089646B56}.Debug|Any CPU.Build.0 = Debug|Any CPU
{04F2D26E-0768-4F93-9A1A-834089646B56}.Release|Any CPU.ActiveCfg = Release|Any CPU
{04F2D26E-0768-4F93-9A1A-834089646B56}.Release|Any CPU.Build.0 = Release|Any CPU
{497352BE-0A1D-4D83-8E21-A07B4A80F2F3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{497352BE-0A1D-4D83-8E21-A07B4A80F2F3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{497352BE-0A1D-4D83-8E21-A07B4A80F2F3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{497352BE-0A1D-4D83-8E21-A07B4A80F2F3}.Release|Any CPU.Build.0 = Release|Any CPU
{41811923-AF3A-4CC4-B508-D90EC2AFEC55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{41811923-AF3A-4CC4-B508-D90EC2AFEC55}.Debug|Any CPU.Build.0 = Debug|Any CPU
{41811923-AF3A-4CC4-B508-D90EC2AFEC55}.Release|Any CPU.ActiveCfg = Release|Any CPU
{41811923-AF3A-4CC4-B508-D90EC2AFEC55}.Release|Any CPU.Build.0 = Release|Any CPU
{570C0DBE-0EF1-47B5-9A3B-E1F7895722A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{570C0DBE-0EF1-47B5-9A3B-E1F7895722A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{570C0DBE-0EF1-47B5-9A3B-E1F7895722A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{570C0DBE-0EF1-47B5-9A3B-E1F7895722A5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -181,8 +187,9 @@ Global
{3E38A906-C2FC-43DC-8CA2-FC07C79CF3CA} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3}
{1CC5AF82-8924-4C7E-BFF1-3125D86E53FB} = {8F1F1C2A-E313-4E0C-BE40-58FB0BA91124}
{25E72004-4D71-4D1E-A193-FC125D12FF96} = {88C2A621-8A98-4D07-8625-7900FC8EF89E}
{27B56A82-E8CE-4B50-9746-D574BAD646A2} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3}
{04F2D26E-0768-4F93-9A1A-834089646B56} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3}
{497352BE-0A1D-4D83-8E21-A07B4A80F2F3} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3}
{41811923-AF3A-4CC4-B508-D90EC2AFEC55} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3}
{570C0DBE-0EF1-47B5-9A3B-E1F7895722A5} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {237BF0AA-9EC4-4659-AD9A-65DEB974250C}