From 5ee7ece7c2ea9823bc6f28e8b6c847edb83cecb0 Mon Sep 17 00:00:00 2001 From: Rutherther Date: Sun, 13 Feb 2022 19:27:48 +0100 Subject: [PATCH] feat(pathfinding): add basic pathfinding using A* --- .../Errors/StateNotInitializedError.cs | 15 ++ .../Extensions/ServiceCollectionExtensions.cs | 37 +++ .../NosSmooth.Extensions.Pathfinding.csproj | 15 ++ .../NosSmooth.Extensions.Pathfinding/Path.cs | 165 ++++++++++++++ .../Pathfinder.cs | 215 ++++++++++++++++++ .../PathfinderState.cs | 37 +++ .../Responders/AtResponder.cs | 38 ++++ .../Responders/CMapResponder.cs | 47 ++++ .../Responders/WalkResponder.cs | 37 +++ .../WalkManager.cs | 80 +++++++ NosSmooth.sln | 17 ++ 11 files changed, 703 insertions(+) create mode 100644 Extensions/NosSmooth.Extensions.Pathfinding/Errors/StateNotInitializedError.cs create mode 100644 Extensions/NosSmooth.Extensions.Pathfinding/Extensions/ServiceCollectionExtensions.cs create mode 100644 Extensions/NosSmooth.Extensions.Pathfinding/NosSmooth.Extensions.Pathfinding.csproj create mode 100644 Extensions/NosSmooth.Extensions.Pathfinding/Path.cs create mode 100644 Extensions/NosSmooth.Extensions.Pathfinding/Pathfinder.cs create mode 100644 Extensions/NosSmooth.Extensions.Pathfinding/PathfinderState.cs create mode 100644 Extensions/NosSmooth.Extensions.Pathfinding/Responders/AtResponder.cs create mode 100644 Extensions/NosSmooth.Extensions.Pathfinding/Responders/CMapResponder.cs create mode 100644 Extensions/NosSmooth.Extensions.Pathfinding/Responders/WalkResponder.cs create mode 100644 Extensions/NosSmooth.Extensions.Pathfinding/WalkManager.cs diff --git a/Extensions/NosSmooth.Extensions.Pathfinding/Errors/StateNotInitializedError.cs b/Extensions/NosSmooth.Extensions.Pathfinding/Errors/StateNotInitializedError.cs new file mode 100644 index 0000000000000000000000000000000000000000..90844bde97b4d58a7ea99ed0e90b073e95e24e38 --- /dev/null +++ b/Extensions/NosSmooth.Extensions.Pathfinding/Errors/StateNotInitializedError.cs @@ -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; + +/// +/// Pathfinder state not initialized. +/// +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 diff --git a/Extensions/NosSmooth.Extensions.Pathfinding/Extensions/ServiceCollectionExtensions.cs b/Extensions/NosSmooth.Extensions.Pathfinding/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000000000000000000000000000000000000..61abeff8f50dc4c1e6b431c40c8f8b7daf0122a1 --- /dev/null +++ b/Extensions/NosSmooth.Extensions.Pathfinding/Extensions/ServiceCollectionExtensions.cs @@ -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; + +/// +/// Extension methods for . +/// +public static class ServiceCollectionExtensions +{ + /// + /// Adds NosTale pathfinding using and . + /// + /// + /// Finds and walks a given path. + /// + /// The service collection. + /// The collection. + public static IServiceCollection AddNostalePathfinding(this IServiceCollection serviceCollection) + { + return serviceCollection + .AddPacketResponder() + .AddPacketResponder() + .AddPacketResponder() + .AddSingleton() + .AddSingleton() + .AddSingleton(); + } +} \ No newline at end of file diff --git a/Extensions/NosSmooth.Extensions.Pathfinding/NosSmooth.Extensions.Pathfinding.csproj b/Extensions/NosSmooth.Extensions.Pathfinding/NosSmooth.Extensions.Pathfinding.csproj new file mode 100644 index 0000000000000000000000000000000000000000..e4c161fe191a5408cf41a089c9104c9bce746a80 --- /dev/null +++ b/Extensions/NosSmooth.Extensions.Pathfinding/NosSmooth.Extensions.Pathfinding.csproj @@ -0,0 +1,15 @@ + + + + net6.0 + enable + enable + NosSmooth extension allowing for finding paths on maps. + + + + + + + + diff --git a/Extensions/NosSmooth.Extensions.Pathfinding/Path.cs b/Extensions/NosSmooth.Extensions.Pathfinding/Path.cs new file mode 100644 index 0000000000000000000000000000000000000000..0918f8ad5546113bbf48875e7a13d565828aaddf --- /dev/null +++ b/Extensions/NosSmooth.Extensions.Pathfinding/Path.cs @@ -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; + +/// +/// Represents a found walkable path. +/// +public class Path +{ + /// + /// Initializes a new instance of the class. + /// + /// The map id. + /// The current x. + /// The current y. + /// The target x. + /// The target y. + /// The parts that represent the path from the current to the target. + 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; + } + + /// + /// Gets the map id this path is for. + /// + public int MapId { get; } + + /// + /// Gets whether the path has reached an end. + /// + public bool ReachedEnd => CurrentPartIndex >= Parts.Count - 1; + + /// + /// Gets the current walk path index. + /// + public int CurrentPartIndex { get; private set; } + + /// + /// Gets the list of the parts that have to be taken. + /// + public IReadOnlyList<(short X, short Y)> Parts { get; } + + /// + /// Gets the target x coordinate. + /// + public short TargetX { get; } + + /// + /// Gets the target y coordinate. + /// + public short TargetY { get; } + + /// + /// Gets the current x coordinate. + /// + public short CurrentX { get; private set; } + + /// + /// gets the current y coordinate. + /// + public short CurrentY { get; private set; } + + /// + /// Take a path only in the same direction. + /// + /// A position to walk to. + 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; + } + + /// + /// Take the given number of tiles and return the position we ended up at. + /// + /// + /// If the count is greater than what is remaining, the end will be taken. + /// + /// The count of parts to take. + /// A position to walk to. + 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 diff --git a/Extensions/NosSmooth.Extensions.Pathfinding/Pathfinder.cs b/Extensions/NosSmooth.Extensions.Pathfinding/Pathfinder.cs new file mode 100644 index 0000000000000000000000000000000000000000..1235a0580bb24c37a7a3546cb994609fc9945d14 --- /dev/null +++ b/Extensions/NosSmooth.Extensions.Pathfinding/Pathfinder.cs @@ -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; + +/// +/// Find path between two given points. +/// +public class Pathfinder +{ + private readonly PathfinderState _state; + + /// + /// Initializes a new instance of the class. + /// + /// The state. + public Pathfinder(PathfinderState state) + { + _state = state; + } + + /// + /// Attempts to find a path between the current position and the target. + /// + /// The target x coordinate. + /// The target y coordinate. + /// A path or an error. + public Result FindPathFromCurrent + ( + short targetX, + short targetY + ) + => FindPathFrom(_state.X, _state.Y, targetX, targetY); + + /// + /// Attempts to find a path between the given positions on the current map. + /// + /// The start x coordinate. + /// The start y coordinate. + /// The target x coordinate. + /// The target y coordinate. + /// A path or an error. + public Result 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 + ); + } + + /// + /// Attempts to find path on the given map with the given coordinates. + /// + /// The map info. + /// The start x coordinate. + /// The start y coordinate. + /// The target x coordinate. + /// The target y coordinate. + /// A path or an error. + public Result 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(); // 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 diff --git a/Extensions/NosSmooth.Extensions.Pathfinding/PathfinderState.cs b/Extensions/NosSmooth.Extensions.Pathfinding/PathfinderState.cs new file mode 100644 index 0000000000000000000000000000000000000000..7dac6d9c8d01e59f83e472d01f6cd267a7526726 --- /dev/null +++ b/Extensions/NosSmooth.Extensions.Pathfinding/PathfinderState.cs @@ -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; + +/// +/// State of the . +/// +public class PathfinderState : IStatefulEntity +{ + /// + /// Gets or sets the current map id. + /// + internal int? MapId { get; set; } + + /// + /// Gets or sets the current map information. + /// + internal IMapInfo? MapInfo { get; set; } + + /// + /// Gets or sets the current x. + /// + internal short X { get; set; } + + /// + /// Gets or sets the current y. + /// + internal short Y { get; set; } +} \ No newline at end of file diff --git a/Extensions/NosSmooth.Extensions.Pathfinding/Responders/AtResponder.cs b/Extensions/NosSmooth.Extensions.Pathfinding/Responders/AtResponder.cs new file mode 100644 index 0000000000000000000000000000000000000000..0554bc5ccae140a1e2f3fdc76e51a0f841b9aa1d --- /dev/null +++ b/Extensions/NosSmooth.Extensions.Pathfinding/Responders/AtResponder.cs @@ -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; + +/// +internal class AtResponder : IPacketResponder +{ + private readonly PathfinderState _state; + + /// + /// Initializes a new instance of the class. + /// + /// The state. + public AtResponder(PathfinderState state) + { + _state = state; + + } + + /// + public Task Respond(PacketEventArgs 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 diff --git a/Extensions/NosSmooth.Extensions.Pathfinding/Responders/CMapResponder.cs b/Extensions/NosSmooth.Extensions.Pathfinding/Responders/CMapResponder.cs new file mode 100644 index 0000000000000000000000000000000000000000..7c2796b47329d437a9085e50c0e5534cc4762a2e --- /dev/null +++ b/Extensions/NosSmooth.Extensions.Pathfinding/Responders/CMapResponder.cs @@ -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; + +/// +internal class CMapResponder : IPacketResponder +{ + private readonly PathfinderState _state; + private readonly IInfoService _infoService; + + /// + /// Initializes a new instance of the class. + /// + /// The state. + /// The info service. + public CMapResponder(PathfinderState state, IInfoService infoService) + { + _state = state; + _infoService = infoService; + } + + /// + public async Task Respond(PacketEventArgs 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 diff --git a/Extensions/NosSmooth.Extensions.Pathfinding/Responders/WalkResponder.cs b/Extensions/NosSmooth.Extensions.Pathfinding/Responders/WalkResponder.cs new file mode 100644 index 0000000000000000000000000000000000000000..e2b35f0994fdf0f1cb8f49afcf4263ee23e4a469 --- /dev/null +++ b/Extensions/NosSmooth.Extensions.Pathfinding/Responders/WalkResponder.cs @@ -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; + +/// +internal class WalkResponder : IPacketResponder +{ + private readonly PathfinderState _state; + + /// + /// Initializes a new instance of the class. + /// + /// The state. + public WalkResponder(PathfinderState state) + { + _state = state; + } + + /// + public Task Respond(PacketEventArgs 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 diff --git a/Extensions/NosSmooth.Extensions.Pathfinding/WalkManager.cs b/Extensions/NosSmooth.Extensions.Pathfinding/WalkManager.cs new file mode 100644 index 0000000000000000000000000000000000000000..64e3e201d8e62f316618ac5376a00e7631b764ff --- /dev/null +++ b/Extensions/NosSmooth.Extensions.Pathfinding/WalkManager.cs @@ -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; + +/// +/// The walk manager using pathfinding to walk to given position. +/// +public class WalkManager +{ + private readonly INostaleClient _client; + private readonly Pathfinder _pathfinder; + private readonly PathfinderState _state; + + /// + /// Initializes a new instance of the class. + /// + /// The client. + /// The pathfinder. + /// The state. + public WalkManager(INostaleClient client, Pathfinder pathfinder, PathfinderState state) + { + _client = client; + _pathfinder = pathfinder; + _state = state; + } + + /// + /// Go to the given position. + /// + /// + /// Expect if the destination could not be reached. + /// Expect if the path could not be found. + /// + /// The target x coordinate. + /// The target y coordinate. + /// The cancellation token used for cancelling the operation. + /// The pet selectors to go with. + /// A result that may not succeed. + public async Task 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 diff --git a/NosSmooth.sln b/NosSmooth.sln index 65c262d40928ea5513c87847bc4da6c2056b0d40..bd39cece42453f78715dc918c09a7bb4a72129ea 100644 --- a/NosSmooth.sln +++ b/NosSmooth.sln @@ -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}