A Extensions/NosSmooth.Extensions.Pathfinding/Errors/StateNotInitializedError.cs => Extensions/NosSmooth.Extensions.Pathfinding/Errors/StateNotInitializedError.cs +15 -0
@@ 0,0 1,15 @@
+//
+// StateNotInitializedError.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Pathfinding.Errors;
+
+/// <summary>
+/// Pathfinder state not initialized.
+/// </summary>
+public record StateNotInitializedError() : ResultError
+ ("The pathfinder state is not yet initialized, the map is unknown. Must wait for c_map packet.");<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Pathfinding/Extensions/ServiceCollectionExtensions.cs => Extensions/NosSmooth.Extensions.Pathfinding/Extensions/ServiceCollectionExtensions.cs +37 -0
@@ 0,0 1,37 @@
+//
+// ServiceCollectionExtensions.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Extensions.DependencyInjection;
+using NosSmooth.Core.Extensions;
+using NosSmooth.Extensions.Pathfinding.Responders;
+using NosSmooth.Packets.Server.Maps;
+
+namespace NosSmooth.Extensions.Pathfinding.Extensions;
+
+/// <summary>
+/// Extension methods for <see cref="IServiceCollection"/>.
+/// </summary>
+public static class ServiceCollectionExtensions
+{
+ /// <summary>
+ /// Adds NosTale pathfinding using <see cref="Pathfinder"/> and <see cref="WalkManager"/>.
+ /// </summary>
+ /// <remarks>
+ /// Finds and walks a given path.
+ /// </remarks>
+ /// <param name="serviceCollection">The service collection.</param>
+ /// <returns>The collection.</returns>
+ public static IServiceCollection AddNostalePathfinding(this IServiceCollection serviceCollection)
+ {
+ return serviceCollection
+ .AddPacketResponder<AtResponder>()
+ .AddPacketResponder<CMapResponder>()
+ .AddPacketResponder<WalkResponder>()
+ .AddSingleton<WalkManager>()
+ .AddSingleton<Pathfinder>()
+ .AddSingleton<PathfinderState>();
+ }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Pathfinding/NosSmooth.Extensions.Pathfinding.csproj => Extensions/NosSmooth.Extensions.Pathfinding/NosSmooth.Extensions.Pathfinding.csproj +15 -0
@@ 0,0 1,15 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ <Description>NosSmooth extension allowing for finding paths on maps.</Description>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="NosSmooth.Core" Version="1.1.1" />
+ <PackageReference Include="NosSmooth.Data.Abstractions" Version="2.0.0" />
+ </ItemGroup>
+
+</Project>
A Extensions/NosSmooth.Extensions.Pathfinding/Path.cs => Extensions/NosSmooth.Extensions.Pathfinding/Path.cs +165 -0
@@ 0,0 1,165 @@
+//
+// Path.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace NosSmooth.Extensions.Pathfinding;
+
+/// <summary>
+/// Represents a found walkable path.
+/// </summary>
+public class Path
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Path"/> class.
+ /// </summary>
+ /// <param name="map">The map id.</param>
+ /// <param name="x">The current x.</param>
+ /// <param name="y">The current y.</param>
+ /// <param name="targetX">The target x.</param>
+ /// <param name="targetY">The target y.</param>
+ /// <param name="parts">The parts that represent the path from the current to the target.</param>
+ public Path
+ (
+ int map,
+ short x,
+ short y,
+ short targetX,
+ short targetY,
+ IReadOnlyList<(short X, short Y)> parts
+ )
+ {
+ MapId = map;
+ CurrentX = x;
+ CurrentY = y;
+ TargetX = targetX;
+ TargetY = targetY;
+ Parts = parts;
+ CurrentPartIndex = 0;
+ }
+
+ /// <summary>
+ /// Gets the map id this path is for.
+ /// </summary>
+ public int MapId { get; }
+
+ /// <summary>
+ /// Gets whether the path has reached an end.
+ /// </summary>
+ public bool ReachedEnd => CurrentPartIndex >= Parts.Count - 1;
+
+ /// <summary>
+ /// Gets the current walk path index.
+ /// </summary>
+ public int CurrentPartIndex { get; private set; }
+
+ /// <summary>
+ /// Gets the list of the parts that have to be taken.
+ /// </summary>
+ public IReadOnlyList<(short X, short Y)> Parts { get; }
+
+ /// <summary>
+ /// Gets the target x coordinate.
+ /// </summary>
+ public short TargetX { get; }
+
+ /// <summary>
+ /// Gets the target y coordinate.
+ /// </summary>
+ public short TargetY { get; }
+
+ /// <summary>
+ /// Gets the current x coordinate.
+ /// </summary>
+ public short CurrentX { get; private set; }
+
+ /// <summary>
+ /// gets the current y coordinate.
+ /// </summary>
+ public short CurrentY { get; private set; }
+
+ /// <summary>
+ /// Take a path only in the same direction.
+ /// </summary>
+ /// <returns>A position to walk to.</returns>
+ public (short X, short Y) TakeForwardPath()
+ {
+ if (ReachedEnd || CurrentPartIndex + 2 >= Parts.Count)
+ {
+ return (TargetX, TargetY);
+ }
+
+ var zeroTile = (CurrentX, CurrentY);
+ var firstTile = Parts[++CurrentPartIndex];
+ var currentTile = firstTile;
+ var nextTile = Parts[CurrentPartIndex + 1];
+
+ while (!ReachedEnd && IsInLine(zeroTile, firstTile, nextTile))
+ {
+ currentTile = nextTile;
+ CurrentPartIndex++;
+ if (!ReachedEnd)
+ {
+ nextTile = Parts[CurrentPartIndex + 1];
+ }
+ }
+
+ return currentTile;
+ }
+
+ private bool IsInLine((short X, short Y) start, (short X, short Y) first, (short X, short Y) current)
+ {
+ var xFirstDiff = first.X - start.X;
+ var yFirstDiff = first.Y - start.Y;
+
+ var xCurrentDiff = current.X - start.X;
+ var yCurrentDiff = current.Y - start.Y;
+
+ if (xFirstDiff == 0 && yFirstDiff == 0)
+ {
+ throw new ArgumentException("The path went back to the start.");
+ }
+
+ if (xCurrentDiff == 0 && yCurrentDiff == 0)
+ {
+ throw new ArgumentException("The path went back to the start.");
+ }
+
+ if (xFirstDiff != 0)
+ {
+ var xRatio = xCurrentDiff / (float)xFirstDiff;
+ return (yFirstDiff * xRatio) - yCurrentDiff < float.Epsilon * 10;
+ }
+
+ var yRatio = yCurrentDiff / (float)yFirstDiff;
+ return (xFirstDiff * yRatio) - xCurrentDiff < float.Epsilon * 10;
+ }
+
+ /// <summary>
+ /// Take the given number of tiles and return the position we ended up at.
+ /// </summary>
+ /// <remarks>
+ /// If the count is greater than what is remaining, the end will be taken.
+ /// </remarks>
+ /// <param name="partsCount">The count of parts to take.</param>
+ /// <returns>A position to walk to.</returns>
+ public (short X, short Y) TakePath(uint partsCount)
+ {
+ if (ReachedEnd)
+ {
+ return (TargetX, TargetY);
+ }
+
+ if (CurrentPartIndex + partsCount >= Parts.Count - 1)
+ {
+ CurrentPartIndex = Parts.Count - 1;
+ }
+ else
+ {
+ CurrentPartIndex += (int)partsCount;
+ }
+
+ return Parts[CurrentPartIndex];
+ }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Pathfinding/Pathfinder.cs => Extensions/NosSmooth.Extensions.Pathfinding/Pathfinder.cs +215 -0
@@ 0,0 1,215 @@
+//
+// Pathfinder.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Core.Client;
+using NosSmooth.Data.Abstractions.Infos;
+using NosSmooth.Extensions.Pathfinding.Errors;
+using NosSmooth.Packets.Enums;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Pathfinding;
+
+/// <summary>
+/// Find path between two given points.
+/// </summary>
+public class Pathfinder
+{
+ private readonly PathfinderState _state;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="Pathfinder"/> class.
+ /// </summary>
+ /// <param name="state">The state.</param>
+ public Pathfinder(PathfinderState state)
+ {
+ _state = state;
+ }
+
+ /// <summary>
+ /// Attempts to find a path between the current position and the target.
+ /// </summary>
+ /// <param name="targetX">The target x coordinate.</param>
+ /// <param name="targetY">The target y coordinate.</param>
+ /// <returns>A path or an error.</returns>
+ public Result<Path> FindPathFromCurrent
+ (
+ short targetX,
+ short targetY
+ )
+ => FindPathFrom(_state.X, _state.Y, targetX, targetY);
+
+ /// <summary>
+ /// Attempts to find a path between the given positions on the current map.
+ /// </summary>
+ /// <param name="x">The start x coordinate.</param>
+ /// <param name="y">The start y coordinate.</param>
+ /// <param name="targetX">The target x coordinate.</param>
+ /// <param name="targetY">The target y coordinate.</param>
+ /// <returns>A path or an error.</returns>
+ public Result<Path> FindPathFrom
+ (
+ short x,
+ short y,
+ short targetX,
+ short targetY
+ )
+ {
+ if (_state.MapInfo is null)
+ {
+ return new StateNotInitializedError();
+ }
+
+ return FindPathOnMap
+ (
+ _state.MapInfo,
+ x,
+ y,
+ targetX,
+ targetY
+ );
+ }
+
+ /// <summary>
+ /// Attempts to find path on the given map with the given coordinates.
+ /// </summary>
+ /// <param name="mapInfo">The map info.</param>
+ /// <param name="x">The start x coordinate.</param>
+ /// <param name="y">The start y coordinate.</param>
+ /// <param name="targetX">The target x coordinate.</param>
+ /// <param name="targetY">The target y coordinate.</param>
+ /// <returns>A path or an error.</returns>
+ public Result<Path> FindPathOnMap
+ (
+ IMapInfo mapInfo,
+ short x,
+ short y,
+ short targetX,
+ short targetY
+ )
+ {
+ if (!mapInfo.IsWalkable((short)targetX, (short)targetY))
+ {
+ return new NotFoundError("The requested target is not walkable, path cannot be found.");
+ }
+
+ var target = (targetX, targetY);
+ var visited = new HashSet<(short X, short Y)>();
+ var offsets = new (short X, short Y)[] { (0, 1), (1, 0), (1, 1), (0, -1), (-1, 0), (-1, -1), (1, -1), (-1, 1) };
+ var distances = new[] { 1, 1, 1.41421356237, 1, 1, 1.41421356237, 1.41421356237, 1.41421356237 };
+ var queue = new PriorityQueue<PathEntry, double>(); // estimated cost to path.
+
+ var start = new PathEntry(0, null, (x, y));
+ queue.Enqueue(start, 0);
+ visited.Add((x, y));
+
+ while (queue.TryDequeue(out var current, out _))
+ {
+ for (int i = 0; i < offsets.Length; i++)
+ {
+ var offset = offsets[i];
+ var distance = distances[i];
+
+ var currX = current.Position.X + offset.X;
+ var currY = current.Position.Y + offset.Y;
+
+ if (visited.Contains(((short)currX, (short)currY)))
+ {
+ // The estimated distance function should be consistent,
+ // the cost cannot be lower on this visit.
+ continue;
+ }
+ visited.Add(((short)currX, (short)currY));
+
+ if (currX == targetX && currY == targetY)
+ {
+ return ReconstructPath
+ (
+ mapInfo.Id,
+ x,
+ y,
+ targetX,
+ targetY,
+ current.CreateChild(distance, (short)currX, (short)currY)
+ );
+ }
+
+ if (currX < 0 || currY < 0 || currX >= mapInfo.Width || currY >= mapInfo.Height)
+ {
+ // Out of bounds
+ continue;
+ }
+
+ if (!mapInfo.IsWalkable((short)currX, (short)currY))
+ {
+ // Current tile not walkable
+ continue;
+ }
+
+ var path = current.CreateChild(distance, (short)currX, (short)currY);
+ var estimatedDistance = EstimateDistance(path.Position, target);
+ queue.Enqueue(path, path.Cost + estimatedDistance);
+ }
+ }
+
+ return new NotFoundError("Could not find path to the given position.");
+ }
+
+ private Path ReconstructPath
+ (
+ int mapId,
+ short x,
+ short y,
+ short targetX,
+ short targetY,
+ PathEntry entry
+ )
+ {
+ var entries = new List<(short X, short Y)>();
+ var current = entry;
+ while (current is not null)
+ {
+ entries.Add(current.Position);
+ current = current.Previous;
+ }
+
+ entries.Reverse();
+ return new Path
+ (
+ mapId,
+ x,
+ y,
+ targetX,
+ targetY,
+ entries
+ );
+ }
+
+ private double EstimateDistance((short X, short Y) current, (short X, short Y) next)
+ {
+ return Math.Sqrt(((current.X - next.X) * (current.X - next.X)) + ((current.Y - next.Y) * (current.Y - next.Y)));
+ }
+
+ private class PathEntry
+ {
+ public PathEntry(double cost, PathEntry? previous, (short X, short Y) position)
+ {
+ Cost = cost;
+ Previous = previous;
+ Position = position;
+ }
+
+ public double Cost { get; }
+
+ public PathEntry? Previous { get; }
+
+ public (short X, short Y) Position { get; }
+
+ public PathEntry CreateChild(double walkCost, short x, short y)
+ {
+ return new PathEntry(Cost + walkCost, this, (x, y));
+ }
+ }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Pathfinding/PathfinderState.cs => Extensions/NosSmooth.Extensions.Pathfinding/PathfinderState.cs +37 -0
@@ 0,0 1,37 @@
+//
+// PathfinderState.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Core.Client;
+using NosSmooth.Core.Stateful;
+using NosSmooth.Data.Abstractions.Infos;
+
+namespace NosSmooth.Extensions.Pathfinding;
+
+/// <summary>
+/// State of the <see cref="Pathfinder"/>.
+/// </summary>
+public class PathfinderState : IStatefulEntity
+{
+ /// <summary>
+ /// Gets or sets the current map id.
+ /// </summary>
+ internal int? MapId { get; set; }
+
+ /// <summary>
+ /// Gets or sets the current map information.
+ /// </summary>
+ internal IMapInfo? MapInfo { get; set; }
+
+ /// <summary>
+ /// Gets or sets the current x.
+ /// </summary>
+ internal short X { get; set; }
+
+ /// <summary>
+ /// Gets or sets the current y.
+ /// </summary>
+ internal short Y { get; set; }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Pathfinding/Responders/AtResponder.cs => Extensions/NosSmooth.Extensions.Pathfinding/Responders/AtResponder.cs +38 -0
@@ 0,0 1,38 @@
+//
+// AtResponder.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Core.Packets;
+using NosSmooth.Packets.Server.Maps;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Pathfinding.Responders;
+
+/// <inheritdoc />
+internal class AtResponder : IPacketResponder<AtPacket>
+{
+ private readonly PathfinderState _state;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AtResponder"/> class.
+ /// </summary>
+ /// <param name="state">The state.</param>
+ public AtResponder(PathfinderState state)
+ {
+ _state = state;
+
+ }
+
+ /// <inheritdoc />
+ public Task<Result> Respond(PacketEventArgs<AtPacket> packetArgs, CancellationToken ct = default)
+ {
+ var packet = packetArgs.Packet;
+
+ _state.X = packet.X;
+ _state.Y = packet.Y;
+
+ return Task.FromResult(Result.FromSuccess());
+ }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Pathfinding/Responders/CMapResponder.cs => Extensions/NosSmooth.Extensions.Pathfinding/Responders/CMapResponder.cs +47 -0
@@ 0,0 1,47 @@
+//
+// CMapResponder.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Core.Packets;
+using NosSmooth.Data.Abstractions;
+using NosSmooth.Packets.Server.Maps;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Pathfinding.Responders;
+
+/// <inheritdoc />
+internal class CMapResponder : IPacketResponder<CMapPacket>
+{
+ private readonly PathfinderState _state;
+ private readonly IInfoService _infoService;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CMapResponder"/> class.
+ /// </summary>
+ /// <param name="state">The state.</param>
+ /// <param name="infoService">The info service.</param>
+ public CMapResponder(PathfinderState state, IInfoService infoService)
+ {
+ _state = state;
+ _infoService = infoService;
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> Respond(PacketEventArgs<CMapPacket> packetArgs, CancellationToken ct = default)
+ {
+ var packet = packetArgs.Packet;
+
+ _state.MapId = packet.Id;
+ var mapInfoResult = await _infoService.GetMapInfoAsync(packet.Id, ct);
+
+ if (!mapInfoResult.IsSuccess)
+ {
+ return Result.FromError(mapInfoResult);
+ }
+
+ _state.MapInfo = mapInfoResult.Entity;
+ return Result.FromSuccess();
+ }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Pathfinding/Responders/WalkResponder.cs => Extensions/NosSmooth.Extensions.Pathfinding/Responders/WalkResponder.cs +37 -0
@@ 0,0 1,37 @@
+//
+// WalkResponder.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Core.Packets;
+using NosSmooth.Packets.Client.Movement;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Pathfinding.Responders;
+
+/// <inheritdoc />
+internal class WalkResponder : IPacketResponder<WalkPacket>
+{
+ private readonly PathfinderState _state;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="WalkResponder"/> class.
+ /// </summary>
+ /// <param name="state">The state.</param>
+ public WalkResponder(PathfinderState state)
+ {
+ _state = state;
+ }
+
+ /// <inheritdoc />
+ public Task<Result> Respond(PacketEventArgs<WalkPacket> packetArgs, CancellationToken ct = default)
+ {
+ var packet = packetArgs.Packet;
+
+ _state.X = packet.PositionX;
+ _state.Y = packet.PositionY;
+
+ return Task.FromResult(Result.FromSuccess());
+ }
+}<
\ No newline at end of file
A Extensions/NosSmooth.Extensions.Pathfinding/WalkManager.cs => Extensions/NosSmooth.Extensions.Pathfinding/WalkManager.cs +80 -0
@@ 0,0 1,80 @@
+//
+// WalkManager.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Core.Client;
+using NosSmooth.Core.Commands.Walking;
+using NosSmooth.Core.Errors;
+using Remora.Results;
+
+namespace NosSmooth.Extensions.Pathfinding;
+
+/// <summary>
+/// The walk manager using pathfinding to walk to given position.
+/// </summary>
+public class WalkManager
+{
+ private readonly INostaleClient _client;
+ private readonly Pathfinder _pathfinder;
+ private readonly PathfinderState _state;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="WalkManager"/> class.
+ /// </summary>
+ /// <param name="client">The client.</param>
+ /// <param name="pathfinder">The pathfinder.</param>
+ /// <param name="state">The state.</param>
+ public WalkManager(INostaleClient client, Pathfinder pathfinder, PathfinderState state)
+ {
+ _client = client;
+ _pathfinder = pathfinder;
+ _state = state;
+ }
+
+ /// <summary>
+ /// Go to the given position.
+ /// </summary>
+ /// <remarks>
+ /// Expect <see cref="WalkNotFinishedError"/> if the destination could not be reached.
+ /// Expect <see cref="NotFoundError"/> if the path could not be found.
+ /// </remarks>
+ /// <param name="x">The target x coordinate.</param>
+ /// <param name="y">The target y coordinate.</param>
+ /// <param name="ct">The cancellation token used for cancelling the operation.</param>
+ /// <param name="petSelectors">The pet selectors to go with.</param>
+ /// <returns>A result that may not succeed.</returns>
+ public async Task<Result> GoToAsync(short x, short y, CancellationToken ct = default, params int[] petSelectors)
+ {
+ var pathResult = _pathfinder.FindPathFromCurrent(x, y);
+ if (!pathResult.IsSuccess)
+ {
+ return Result.FromError(pathResult);
+ }
+
+ var path = pathResult.Entity;
+ while (!path.ReachedEnd)
+ {
+ if (path.MapId != _state.MapId)
+ {
+ return new WalkNotFinishedError(_state.X, _state.Y, WalkUnfinishedReason.MapChanged);
+ }
+
+ var next = path.TakeForwardPath();
+ var walkResult = await _client.SendCommandAsync(new WalkCommand(next.X, next.Y, petSelectors, 2), ct);
+ if (!walkResult.IsSuccess)
+ {
+ if (path.ReachedEnd && walkResult.Error is WalkNotFinishedError walkNotFinishedError
+ && walkNotFinishedError.Reason == WalkUnfinishedReason.MapChanged)
+ {
+ return Result.FromSuccess();
+ }
+
+ return walkResult;
+ }
+ }
+
+ return Result.FromSuccess();
+ }
+}<
\ No newline at end of file
M NosSmooth.sln => NosSmooth.sln +17 -0
@@ 46,6 46,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Game", "Core\NosS
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileClient", "Samples\FileClient\FileClient.csproj", "{D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{3A6D13E3-4BBA-4B3D-AE99-BAC8B375F7DF}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Extensions.Pathfinding", "Extensions\NosSmooth.Extensions.Pathfinding\NosSmooth.Extensions.Pathfinding.csproj", "{564CAD6F-09B1-450B-83ED-9BCDE106B646}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ 224,6 228,18 @@ Global
{D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}.Release|x64.Build.0 = Release|Any CPU
{D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}.Release|x86.ActiveCfg = Release|Any CPU
{D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B}.Release|x86.Build.0 = Release|Any CPU
+ {564CAD6F-09B1-450B-83ED-9BCDE106B646}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {564CAD6F-09B1-450B-83ED-9BCDE106B646}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {564CAD6F-09B1-450B-83ED-9BCDE106B646}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {564CAD6F-09B1-450B-83ED-9BCDE106B646}.Debug|x64.Build.0 = Debug|Any CPU
+ {564CAD6F-09B1-450B-83ED-9BCDE106B646}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {564CAD6F-09B1-450B-83ED-9BCDE106B646}.Debug|x86.Build.0 = Debug|Any CPU
+ {564CAD6F-09B1-450B-83ED-9BCDE106B646}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {564CAD6F-09B1-450B-83ED-9BCDE106B646}.Release|Any CPU.Build.0 = Release|Any CPU
+ {564CAD6F-09B1-450B-83ED-9BCDE106B646}.Release|x64.ActiveCfg = Release|Any CPU
+ {564CAD6F-09B1-450B-83ED-9BCDE106B646}.Release|x64.Build.0 = Release|Any CPU
+ {564CAD6F-09B1-450B-83ED-9BCDE106B646}.Release|x86.ActiveCfg = Release|Any CPU
+ {564CAD6F-09B1-450B-83ED-9BCDE106B646}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ 243,6 259,7 @@ Global
{7C9C7375-6FC0-4704-9332-1F74CDF41D11} = {01B5E872-271F-4D30-A1AA-AD48D81840C5}
{055C66A7-640C-49BB-81A7-28E630F51C37} = {99E72557-BCE9-496A-B49C-79537B0E6063}
{D33E1AC5-8946-4D6F-A6D3-D81F98E4F86B} = {99E72557-BCE9-496A-B49C-79537B0E6063}
+ {564CAD6F-09B1-450B-83ED-9BCDE106B646} = {3A6D13E3-4BBA-4B3D-AE99-BAC8B375F7DF}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C5F46653-4DEC-429B-8580-4ED18ED9B4CA}