~ruther/NosSmooth

5ee7ece7c2ea9823bc6f28e8b6c847edb83cecb0 — Rutherther 3 years ago 56a9884
feat(pathfinding): add basic pathfinding using A*
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}