MUIP server, GM command system

This commit is contained in:
xeon 2024-01-21 18:34:19 +03:00
parent 8be42d2ffb
commit ace68414f7
22 changed files with 357 additions and 15 deletions

View file

@ -26,7 +26,7 @@ public class AvatarRow : ExcelRow
public uint MaxRank { get; set; } // 0x50 public uint MaxRank { get; set; } // 0x50
public string[] RankUpCostList { get; set; } = []; // 0x58 public string[] RankUpCostList { get; set; } = []; // 0x58
public uint MaxRankRepay { get; set; } // 0x60 public uint MaxRankRepay { get; set; } // 0x60
public uint[] SkillList { get; set; } // 0x68 public uint[] SkillList { get; set; } = []; // 0x68
[JsonConverter(typeof(JsonStringEnumConverter))] [JsonConverter(typeof(JsonStringEnumConverter))]
public AvatarBaseType AvatarBaseType { get; set; } // 0x70 public AvatarBaseType AvatarBaseType { get; set; } // 0x70
public string DefaultAvatarImagePath { get; set; } = string.Empty; // 0x78 public string DefaultAvatarImagePath { get; set; } = string.Empty; // 0x78

View file

@ -7,17 +7,26 @@ enum RPGServiceType
RPG_SERVICE_TYPE_SDK = 1; RPG_SERVICE_TYPE_SDK = 1;
RPG_SERVICE_TYPE_GATESERVER = 2; RPG_SERVICE_TYPE_GATESERVER = 2;
RPG_SERVICE_TYPE_GAMESERVER = 3; RPG_SERVICE_TYPE_GAMESERVER = 3;
RPG_SERVICE_TYPE_MUIPSERVER = 4;
} }
enum ServiceCommandType enum ServiceCommandType
{ {
SERVICE_COMMAND_TYPE_NONE = 0; SERVICE_COMMAND_TYPE_NONE = 0;
SERVICE_COMMAND_TYPE_GMTALK_BY_MUIP = 10;
SERVICE_COMMAND_TYPE_BIND_CONTAINER = 100; SERVICE_COMMAND_TYPE_BIND_CONTAINER = 100;
SERVICE_COMMAND_TYPE_BIND_CONTAINER_RESULT = 101; SERVICE_COMMAND_TYPE_BIND_CONTAINER_RESULT = 101;
SERVICE_COMMAND_TYPE_UNBIND_CONTAINER = 102; SERVICE_COMMAND_TYPE_UNBIND_CONTAINER = 102;
SERVICE_COMMAND_TYPE_FORWARD_GAME_MESSAGE = 103; SERVICE_COMMAND_TYPE_FORWARD_GAME_MESSAGE = 103;
} }
message CmdGmtalkByMuip
{
uint64 session_id = 1;
string msg = 2;
}
message CmdBindContainer message CmdBindContainer
{ {
uint64 session_id = 1; uint64 session_id = 1;

View file

@ -9,7 +9,7 @@ using RPG.Services.Core.Session;
namespace RPG.Services.Core.Extensions; namespace RPG.Services.Core.Extensions;
public static class HostApplicationBuilderExtensions public static class HostApplicationBuilderExtensions
{ {
public static HostApplicationBuilder SetupRPGService<TService, TCommandHandler>(this HostApplicationBuilder builder) public static IHostApplicationBuilder SetupRPGService<TService, TCommandHandler>(this IHostApplicationBuilder builder, bool stateless = false)
where TService : RPGServiceBase where TService : RPGServiceBase
where TCommandHandler : ServiceCommandHandler where TCommandHandler : ServiceCommandHandler
{ {
@ -21,10 +21,11 @@ public static class HostApplicationBuilderExtensions
builder.Services.AddHostedService<TService>() builder.Services.AddHostedService<TService>()
.AddSingleton<ServiceManager>() .AddSingleton<ServiceManager>()
.AddSingleton<SessionManager>()
.AddSingleton<ServiceBox>() .AddSingleton<ServiceBox>()
.AddSingleton<ServiceCommandHandler, TCommandHandler>(); .AddSingleton<ServiceCommandHandler, TCommandHandler>();
if (!stateless) builder.Services.AddSingleton<SessionManager>();
return builder; return builder;
} }
} }

View file

@ -4,6 +4,8 @@ using RPG.Services.Gameserver.Modules.Attributes;
using RPG.Services.Gameserver.Session; using RPG.Services.Gameserver.Session;
namespace RPG.Services.Gameserver.Modules; namespace RPG.Services.Gameserver.Modules;
[GMAlias("adventure")]
internal class AdventureModule : BaseModule internal class AdventureModule : BaseModule
{ {
private readonly ExcelTables _tables; private readonly ExcelTables _tables;
@ -13,6 +15,39 @@ internal class AdventureModule : BaseModule
_tables = excelTables; _tables = excelTables;
} }
[GMCommand("maze")]
public Task OnGmEnterMaze(PlayerSession session, string[] args)
{
if (args.Length != 1) return Task.CompletedTask;
if (!uint.TryParse(args[0], out uint entryId))
return Task.CompletedTask;
MapEntryRow? entry = _tables.GetExcelRow<MapEntryRow>(ExcelType.MapEntry, entryId);
if (entry == null) return Task.CompletedTask;
Send(session, CmdType.CmdEnterMazeByServerScNotify, new EnterMazeByServerScNotify
{
Maze = new Maze
{
MapEntryId = entry.ID,
Id = entry.PlaneID,
Floor = new MazeFloor
{
FloorId = entry.FloorID,
Scene = new SceneInfo
{
EntryId = entry.ID,
PlaneId = entry.PlaneID,
FloorId = entry.FloorID
}
}
},
});
return Task.CompletedTask;
}
[OnCommand(CmdType.CmdGetCurSceneInfoCsReq)] [OnCommand(CmdType.CmdGetCurSceneInfoCsReq)]
public Task OnCmdGetCurSceneInfoCsReq(PlayerSession session, ReadOnlyMemory<byte> _) public Task OnCmdGetCurSceneInfoCsReq(PlayerSession session, ReadOnlyMemory<byte> _)
{ {

View file

@ -0,0 +1,12 @@
namespace RPG.Services.Gameserver.Modules.Attributes;
[AttributeUsage(AttributeTargets.Class)]
internal class GMAliasAttribute : Attribute
{
public string Alias { get; }
public GMAliasAttribute(string alias)
{
Alias = alias;
}
}

View file

@ -0,0 +1,12 @@
namespace RPG.Services.Gameserver.Modules.Attributes;
[AttributeUsage(AttributeTargets.Method)]
internal class GMCommandAttribute : Attribute
{
public string Command { get; }
public GMCommandAttribute(string command)
{
Command = command;
}
}

View file

@ -11,14 +11,18 @@ namespace RPG.Services.Gameserver.Modules;
internal class ModuleManager internal class ModuleManager
{ {
private delegate Task ReqHandler(PlayerSession session, IServiceProvider serviceProvider, ReadOnlyMemory<byte> body); private delegate Task ReqHandler(PlayerSession session, IServiceProvider serviceProvider, ReadOnlyMemory<byte> body);
private static readonly ImmutableDictionary<CmdType, ReqHandler> s_handlers; private static readonly ImmutableDictionary<CmdType, ReqHandler> s_reqHandlers;
private delegate Task GmCommandHandler(IServiceProvider provider, PlayerSession session, string[] args);
private static readonly ImmutableDictionary<string, ImmutableDictionary<string, GmCommandHandler>> s_gmCommandMap;
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly ILogger _logger; private readonly ILogger _logger;
static ModuleManager() static ModuleManager()
{ {
s_handlers = MapHandlers(); s_reqHandlers = MapReqHandlers();
s_gmCommandMap = MapGmCommandHandlers();
} }
public ModuleManager(IServiceProvider serviceProvider, ILogger<ModuleManager> logger) public ModuleManager(IServiceProvider serviceProvider, ILogger<ModuleManager> logger)
@ -27,9 +31,27 @@ internal class ModuleManager
_logger = logger; _logger = logger;
} }
public TModule Get<TModule>() where TModule : BaseModule
{
return _serviceProvider.GetRequiredService<TModule>();
}
public async Task HandleGmCommandAsync(PlayerSession session, string[] args)
{
if (args.Length < 2) return;
if (s_gmCommandMap.TryGetValue(args[0], out var map))
{
if (map.TryGetValue(args[1], out GmCommandHandler? handler))
{
await handler(_serviceProvider, session, args[2..]);
}
}
}
public async Task HandleAsync(PlayerSession session, CmdType cmdType, ReadOnlyMemory<byte> body) public async Task HandleAsync(PlayerSession session, CmdType cmdType, ReadOnlyMemory<byte> body)
{ {
if (s_handlers.TryGetValue(cmdType, out var handler)) if (s_reqHandlers.TryGetValue(cmdType, out var handler))
{ {
await handler(session, _serviceProvider, body); await handler(session, _serviceProvider, body);
_logger.LogInformation("Successfully handled command of type {cmdType}", cmdType); _logger.LogInformation("Successfully handled command of type {cmdType}", cmdType);
@ -40,7 +62,7 @@ internal class ModuleManager
} }
} }
private static ImmutableDictionary<CmdType, ReqHandler> MapHandlers() private static ImmutableDictionary<CmdType, ReqHandler> MapReqHandlers()
{ {
var builder = ImmutableDictionary.CreateBuilder<CmdType, ReqHandler>(); var builder = ImmutableDictionary.CreateBuilder<CmdType, ReqHandler>();
@ -72,4 +94,43 @@ internal class ModuleManager
return builder.ToImmutable(); return builder.ToImmutable();
} }
private static ImmutableDictionary<string, ImmutableDictionary<string, GmCommandHandler>> MapGmCommandHandlers()
{
var builder = ImmutableDictionary.CreateBuilder<string, ImmutableDictionary<string, GmCommandHandler>>();
IEnumerable<Type> types = Assembly.GetExecutingAssembly().GetTypes()
.Where(type => type.IsAssignableTo(typeof(BaseModule)) && !type.IsAbstract);
MethodInfo getServiceMethod = typeof(ServiceProviderServiceExtensions).GetMethod("GetRequiredService", [typeof(IServiceProvider)])!;
foreach (Type type in types)
{
GMAliasAttribute? aliasAttribute = type.GetCustomAttribute<GMAliasAttribute>();
if (aliasAttribute == null) continue;
var commands = ImmutableDictionary.CreateBuilder<string, GmCommandHandler>();
IEnumerable<MethodInfo> methods = type.GetMethods()
.Where(method => method.GetCustomAttribute<GMCommandAttribute>() != null);
foreach (MethodInfo method in methods)
{
GMCommandAttribute attribute = method.GetCustomAttribute<GMCommandAttribute>()!;
ParameterExpression serviceProviderParam = Expression.Parameter(typeof(IServiceProvider));
ParameterExpression sessionParam = Expression.Parameter(typeof(PlayerSession));
ParameterExpression argsParam = Expression.Parameter(typeof(string[]));
MethodCallExpression getServiceCall = Expression.Call(getServiceMethod.MakeGenericMethod(type), serviceProviderParam);
MethodCallExpression handlerCall = Expression.Call(getServiceCall, method, sessionParam, argsParam);
Expression<GmCommandHandler> lambda = Expression.Lambda<GmCommandHandler>(handlerCall, serviceProviderParam, sessionParam, argsParam);
commands.Add(attribute.Command, lambda.Compile());
}
builder.Add(aliasAttribute.Alias, commands.ToImmutable());
}
return builder.ToImmutable();
}
} }

View file

@ -66,4 +66,15 @@ internal class GameserverCommandHandler : ServiceCommandHandler
await session.HandleGameCommand((ushort)cmd.CmdType, cmd.Payload.Memory); await session.HandleGameCommand((ushort)cmd.CmdType, cmd.Payload.Memory);
} }
} }
[ServiceCommand(ServiceCommandType.GmtalkByMuip)]
public async Task OnCmdGmtalkByMuip(ServiceCommand command)
{
CmdGmtalkByMuip cmd = CmdGmtalkByMuip.Parser.ParseFrom(command.Body.Span);
if (_sessionManager.TryGet(cmd.SessionId, out PlayerSession? session))
{
await session.HandleGmTalkCommand(cmd.Msg);
}
}
} }

View file

@ -16,6 +16,11 @@ internal class PlayerSession : RPGSession
_moduleManager = _scope.ServiceProvider.GetRequiredService<ModuleManager>(); _moduleManager = _scope.ServiceProvider.GetRequiredService<ModuleManager>();
} }
public async Task HandleGmTalkCommand(string cmd)
{
await _moduleManager.HandleGmCommandAsync(this, cmd.Split(' '));
}
public async Task HandleGameCommand(ushort cmdType, ReadOnlyMemory<byte> body) public async Task HandleGameCommand(ushort cmdType, ReadOnlyMemory<byte> body)
{ {
await _moduleManager.HandleAsync(this, (CmdType)cmdType, body); await _moduleManager.HandleAsync(this, (CmdType)cmdType, body);

View file

@ -3,6 +3,11 @@
"ServiceType": "Gameserver" "ServiceType": "Gameserver"
}, },
"Nodes": [ "Nodes": [
{
"Type": "Muipserver",
"Host": "127.0.0.1",
"Port": "21031"
},
{ {
"Type": "Gateserver", "Type": "Gateserver",
"Host": "127.0.0.1", "Host": "127.0.0.1",

View file

@ -17,4 +17,8 @@
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Properties\" />
</ItemGroup>
</Project> </Project>

View file

@ -3,6 +3,11 @@
"ServiceType": "Gateserver" "ServiceType": "Gateserver"
}, },
"Nodes": [ "Nodes": [
{
"Type": "Muipserver",
"Host": "127.0.0.1",
"Port": "21031"
},
{ {
"Type": "Gateserver", "Type": "Gateserver",
"Host": "127.0.0.1", "Host": "127.0.0.1",

View file

@ -0,0 +1,53 @@
using Google.Protobuf;
using RPG.Network.Proto;
using RPG.Services.Core.Network;
using RPG.Services.Core.Network.Command;
namespace RPG.Services.MUIP.Handlers;
internal static class ApiHandler
{
private const string LogCategoryName = "MuipHttpApi";
private const string GmTalkSessionIdQueryKey = "s_id";
private const string GmTalkCommandQueryKey = "cmd";
private static ILogger? _logger;
public static IResult OnGmTalk(HttpContext ctx, ServiceBox services, ILoggerFactory loggerFactory)
{
_logger ??= loggerFactory.CreateLogger(LogCategoryName);
IQueryCollection query = ctx.Request.Query;
if (!query.ContainsKey(GmTalkSessionIdQueryKey) || !query.ContainsKey(GmTalkCommandQueryKey))
return Results.BadRequest();
string? sessionIdTxt = query[GmTalkSessionIdQueryKey];
string? cmd = query[GmTalkCommandQueryKey];
if (cmd == null || sessionIdTxt == null)
return Results.BadRequest();
if (!ulong.TryParse(sessionIdTxt, out ulong sessionId))
return Results.BadRequest();
_logger.LogInformation("Received GM Talk request via HTTP, session id: {id}, cmd: {cmd}", sessionId, cmd);
SendServiceCommand(services, ServiceCommandType.GmtalkByMuip, new CmdGmtalkByMuip
{
SessionId = sessionId,
Msg = cmd
}, RPGServiceType.Gameserver);
return Results.Ok("Request forwarded to Gameserver");
}
private static void SendServiceCommand<TBody>(ServiceBox services, ServiceCommandType commandType, TBody body, RPGServiceType target) where TBody : IMessage<TBody>
{
ServiceCommand command = new(services.CurrentType, commandType, body.ToByteArray());
byte[] buffer = GC.AllocateUninitializedArray<byte>(command.Body.Length + 7);
ServiceCommandEncoder.EncodeCommand(command, buffer);
services.SendToService(target, buffer);
}
}

View file

@ -0,0 +1,11 @@
using RPG.Services.Core.Network;
using RPG.Services.Core.Network.Command;
namespace RPG.Services.MUIP.Network.Command;
internal class MuipserverCommandHandler : ServiceCommandHandler
{
public MuipserverCommandHandler(ILogger<ServiceCommandHandler> logger, ServiceBox services) : base(logger, services)
{
}
}

View file

@ -0,0 +1,23 @@
using RPG.Services.MUIP.Handlers;
using RPG.Services.Core.Extensions;
using RPG.Services.MUIP.Network.Command;
namespace RPG.Services.MUIP;
internal static class Program
{
private static async Task Main(string[] args)
{
Console.Title = "Snowflake | MUIP";
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseUrls("http://*:1337");
builder.SetupRPGService<RPGMuipserver, MuipserverCommandHandler>(true);
WebApplication app = builder.Build();
app.MapGet("/api/gmtalk", ApiHandler.OnGmTalk);
await app.RunAsync();
}
}

View file

@ -0,0 +1,29 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:13849",
"sslPort": 0
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5001",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View file

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\RPG.Services.Core\RPG.Services.Core.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,10 @@
using RPG.Services.Core;
namespace RPG.Services.MUIP;
internal class RPGMuipserver : RPGServiceBase
{
public RPGMuipserver(ServiceManager serviceManager) : base(serviceManager)
{
}
}

View file

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View file

@ -0,0 +1,29 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Service": {
"ServiceType": "Muipserver"
},
"Nodes": [
{
"Type": "Muipserver",
"Host": "127.0.0.1",
"Port": "21031"
},
{
"Type": "Gateserver",
"Host": "127.0.0.1",
"Port": "21051"
},
{
"Type": "Gameserver",
"Host": "127.0.0.1",
"Port": "21081"
}
]
}

View file

@ -1,5 +1,4 @@
using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Http.HttpResults;
using RPG.Services.SDK.Models;
using RPG.Services.SDK.Models.Region; using RPG.Services.SDK.Models.Region;
namespace RPG.Services.SDK.Handlers; namespace RPG.Services.SDK.Handlers;

View file

@ -3,27 +3,29 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59 VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RPG.Network.Proto", "RPG.Network.Proto\RPG.Network.Proto.csproj", "{7DA70126-3F73-407B-A024-5856F354FA97}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RPG.Network.Proto", "RPG.Network.Proto\RPG.Network.Proto.csproj", "{7DA70126-3F73-407B-A024-5856F354FA97}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RPG.GameCore", "RPG.GameCore\RPG.GameCore.csproj", "{74042D70-7EA0-4348-9BDB-D1E5D0FC868A}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RPG.GameCore", "RPG.GameCore\RPG.GameCore.csproj", "{74042D70-7EA0-4348-9BDB-D1E5D0FC868A}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Framework", "Framework", "{0FF9D567-C413-43F0-9EDF-09D4D36154B7}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Framework", "Framework", "{0FF9D567-C413-43F0-9EDF-09D4D36154B7}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Services", "Services", "{F199706E-4A92-4A2F-BDDD-25DA4691D43E}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Services", "Services", "{F199706E-4A92-4A2F-BDDD-25DA4691D43E}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RPG.Services.SDK", "RPG.Services.SDK\RPG.Services.SDK.csproj", "{855DA130-974F-4CE8-8DB5-2BD59DC2C3AA}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RPG.Services.SDK", "RPG.Services.SDK\RPG.Services.SDK.csproj", "{855DA130-974F-4CE8-8DB5-2BD59DC2C3AA}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RPG.Services.Gateserver", "RPG.Services.Gateserver\RPG.Services.Gateserver.csproj", "{EB7A2038-E2AF-4565-944C-D850D6AEAEED}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RPG.Services.Gateserver", "RPG.Services.Gateserver\RPG.Services.Gateserver.csproj", "{EB7A2038-E2AF-4565-944C-D850D6AEAEED}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RPG.Services.Gameserver", "RPG.Services.Gameserver\RPG.Services.Gameserver.csproj", "{565F9857-3E97-4363-9A5D-05CED8718116}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RPG.Services.Gameserver", "RPG.Services.Gameserver\RPG.Services.Gameserver.csproj", "{565F9857-3E97-4363-9A5D-05CED8718116}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RPG.Services.Core", "RPG.Services.Core\RPG.Services.Core.csproj", "{1B434662-DEC9-40C9-A709-CE87026191D9}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RPG.Services.Core", "RPG.Services.Core\RPG.Services.Core.csproj", "{1B434662-DEC9-40C9-A709-CE87026191D9}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D96091AB-B78F-4092-ADEF-7A4D9F1B90C6}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D96091AB-B78F-4092-ADEF-7A4D9F1B90C6}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig .editorconfig = .editorconfig
EndProjectSection EndProjectSection
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RPG.Services.MUIP", "RPG.Services.MUIP\RPG.Services.MUIP.csproj", "{2E029A8A-8878-4B71-B72B-2881CDE83B71}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -54,6 +56,10 @@ Global
{1B434662-DEC9-40C9-A709-CE87026191D9}.Debug|Any CPU.Build.0 = Debug|Any CPU {1B434662-DEC9-40C9-A709-CE87026191D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1B434662-DEC9-40C9-A709-CE87026191D9}.Release|Any CPU.ActiveCfg = Release|Any CPU {1B434662-DEC9-40C9-A709-CE87026191D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1B434662-DEC9-40C9-A709-CE87026191D9}.Release|Any CPU.Build.0 = Release|Any CPU {1B434662-DEC9-40C9-A709-CE87026191D9}.Release|Any CPU.Build.0 = Release|Any CPU
{2E029A8A-8878-4B71-B72B-2881CDE83B71}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2E029A8A-8878-4B71-B72B-2881CDE83B71}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2E029A8A-8878-4B71-B72B-2881CDE83B71}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2E029A8A-8878-4B71-B72B-2881CDE83B71}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -65,6 +71,7 @@ Global
{EB7A2038-E2AF-4565-944C-D850D6AEAEED} = {F199706E-4A92-4A2F-BDDD-25DA4691D43E} {EB7A2038-E2AF-4565-944C-D850D6AEAEED} = {F199706E-4A92-4A2F-BDDD-25DA4691D43E}
{565F9857-3E97-4363-9A5D-05CED8718116} = {F199706E-4A92-4A2F-BDDD-25DA4691D43E} {565F9857-3E97-4363-9A5D-05CED8718116} = {F199706E-4A92-4A2F-BDDD-25DA4691D43E}
{1B434662-DEC9-40C9-A709-CE87026191D9} = {0FF9D567-C413-43F0-9EDF-09D4D36154B7} {1B434662-DEC9-40C9-A709-CE87026191D9} = {0FF9D567-C413-43F0-9EDF-09D4D36154B7}
{2E029A8A-8878-4B71-B72B-2881CDE83B71} = {F199706E-4A92-4A2F-BDDD-25DA4691D43E}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {9B5CE103-882A-419D-8FA3-89C8642687F6} SolutionGuid = {9B5CE103-882A-419D-8FA3-89C8642687F6}