0
0
Fork 0
mirror of https://github.com/Sanae6/SmoOnlineServer.git synced 2024-11-24 04:05:17 +00:00

Reformat solution

This commit is contained in:
Sanae 2022-02-09 19:44:50 -06:00
parent 351304f99f
commit 7dbe2ecd55
25 changed files with 89 additions and 73 deletions

View file

@ -1,28 +1,34 @@
using System.Buffers; using System.Net.Sockets;
using System.Net.Sockets;
using Shared.Packet.Packets; using Shared.Packet.Packets;
namespace Server; namespace Server;
public class Client : IDisposable { public class Client : IDisposable {
public Socket? Socket; public readonly Dictionary<string, object> Metadata = new Dictionary<string, object>(); // can be used to store any information about a player
public bool Connected = false; public bool Connected = false;
public Guid Id;
public CostumePacket CurrentCostume = new CostumePacket { public CostumePacket CurrentCostume = new CostumePacket {
BodyName = "", BodyName = "",
CapName = "" CapName = ""
}; };
public readonly Dictionary<string, object> Metadata = new Dictionary<string, object>(); // can be used to store any information about a player
public Guid Id;
public Socket? Socket;
public void Dispose() {
Socket?.Disconnect(false);
}
public async Task Send(Memory<byte> data) { public async Task Send(Memory<byte> data) {
if (!Connected) return; if (!Connected) return;
await Socket!.SendAsync(data, SocketFlags.None); await Socket!.SendAsync(data, SocketFlags.None);
} }
public void Dispose() { public static bool operator ==(Client? left, Client? right) {
Socket?.Disconnect(false); return left is { } leftClient && right is { } rightClient && leftClient.Id == rightClient.Id;
} }
public static bool operator ==(Client? left, Client? right) => left is { } leftClient && right is { } rightClient && leftClient.Id == rightClient.Id; public static bool operator !=(Client? left, Client? right) {
public static bool operator !=(Client? left, Client? right) => !(left == right); return !(left == right);
}
} }

View file

@ -1,6 +1,3 @@
using System.Buffers; Server.Server server = new Server.Server();
using System.Net.Sockets;
Server.Server server = new Server.Server();
await server.Listen(1027); await server.Listen(1027);

View file

@ -9,19 +9,20 @@ using Shared.Packet.Packets;
namespace Server; namespace Server;
public class Server { public class Server {
private readonly MemoryPool<byte> memoryPool = MemoryPool<byte>.Shared;
public readonly List<Client> Clients = new List<Client>(); public readonly List<Client> Clients = new List<Client>();
public readonly Logger Logger = new Logger("Server"); public readonly Logger Logger = new Logger("Server");
private readonly MemoryPool<byte> memoryPool = MemoryPool<byte>.Shared;
public async Task Listen(ushort port) { public async Task Listen(ushort port) {
Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
serverSocket.Bind(new IPEndPoint(IPAddress.Any, port)); serverSocket.Bind(new IPEndPoint(IPAddress.Any, port));
serverSocket.Listen(); serverSocket.Listen();
Logger.Info($"Listening on port {port}"); Logger.Info($"Listening on port {port}");
while (true) { while (true) {
Socket socket = await serverSocket.AcceptAsync(); Socket socket = await serverSocket.AcceptAsync();
Logger.Warn("ok"); Logger.Warn("ok");
if (Clients.Count > Constants.MaxClients) { if (Clients.Count > Constants.MaxClients) {
@ -47,14 +48,14 @@ public class Server {
PacketHeader header = new PacketHeader { PacketHeader header = new PacketHeader {
Id = sender?.Id ?? Guid.Empty, Id = sender?.Id ?? Guid.Empty,
Type = Constants.Packets[typeof(T)].Type, Type = Constants.Packets[typeof(T)].Type
}; };
FillPacket(header, packet, memory.Memory); FillPacket(header, packet, memory.Memory);
await Broadcast(memory, sender); await Broadcast(memory, sender);
} }
/// <summary> /// <summary>
/// Takes ownership of data and disposes once done. /// Takes ownership of data and disposes once done.
/// </summary> /// </summary>
/// <param name="data">Memory owner to dispose once done</param> /// <param name="data">Memory owner to dispose once done</param>
/// <param name="sender">Optional sender to not broadcast data to</param> /// <param name="sender">Optional sender to not broadcast data to</param>
@ -67,7 +68,7 @@ public class Server {
} }
/// <summary> /// <summary>
/// Broadcasts memory whose memory shouldn't be disposed, should only be fired by server code. /// Broadcasts memory whose memory shouldn't be disposed, should only be fired by server code.
/// </summary> /// </summary>
/// <param name="data">Memory to send to the clients</param> /// <param name="data">Memory to send to the clients</param>
/// <param name="sender">Optional sender to not broadcast data to</param> /// <param name="sender">Optional sender to not broadcast data to</param>
@ -81,14 +82,15 @@ public class Server {
private async void HandleSocket(Socket socket) { private async void HandleSocket(Socket socket) {
Client client = new Client {Socket = socket}; Client client = new Client { Socket = socket };
IMemoryOwner<byte> memory = null!; IMemoryOwner<byte> memory = null!;
bool first = true; bool first = true;
try { try {
while (true) { while (true) {
memory = memoryPool.Rent(Constants.MaxPacketSize); memory = memoryPool.Rent(Constants.MaxPacketSize);
int size = await socket.ReceiveAsync(memory.Memory, SocketFlags.None); int size = await socket.ReceiveAsync(memory.Memory, SocketFlags.None);
if (size == 0) { // treat it as a disconnect and exit if (size == 0) {
// treat it as a disconnect and exit
Logger.Info($"Socket {socket.RemoteEndPoint} disconnected."); Logger.Info($"Socket {socket.RemoteEndPoint} disconnected.");
await socket.DisconnectAsync(false); await socket.DisconnectAsync(false);
break; break;
@ -99,9 +101,7 @@ public class Server {
// connection initialization // connection initialization
if (first) { if (first) {
first = false; first = false;
if (header.Type != PacketType.Connect) { if (header.Type != PacketType.Connect) throw new Exception($"First packet was not init, instead it was {header.Type}");
throw new Exception($"First packet was not init, instead it was {header.Type}");
}
ConnectPacket connect = MemoryMarshal.Read<ConnectPacket>(memory.Memory.Span[Constants.HeaderSize..size]); ConnectPacket connect = MemoryMarshal.Read<ConnectPacket>(memory.Memory.Span[Constants.HeaderSize..size]);
lock (Clients) { lock (Clients) {
@ -119,6 +119,7 @@ public class Server {
} else { } else {
firstConn = true; firstConn = true;
} }
break; break;
} }
default: default:
@ -130,7 +131,7 @@ public class Server {
// do any cleanup required when it comes to new clients // do any cleanup required when it comes to new clients
List<Client> toDisconnect = Clients.FindAll(c => c.Id == header.Id && c.Connected && c.Socket != null); List<Client> toDisconnect = Clients.FindAll(c => c.Id == header.Id && c.Connected && c.Socket != null);
Clients.RemoveAll(c => c.Id == header.Id); Clients.RemoveAll(c => c.Id == header.Id);
client.Id = header.Id; client.Id = header.Id;
Clients.Add(client); Clients.Add(client);
@ -138,33 +139,32 @@ public class Server {
// done disconnecting and removing stale clients with the same id // done disconnecting and removing stale clients with the same id
} }
} }
List<Client> otherConnectedPlayers = Clients.FindAll(c => c.Id != header.Id && c.Connected && c.Socket != null); List<Client> otherConnectedPlayers = Clients.FindAll(c => c.Id != header.Id && c.Connected && c.Socket != null);
await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => { await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => {
IMemoryOwner<byte> connectBuffer = MemoryPool<byte>.Shared.Rent(256); IMemoryOwner<byte> connectBuffer = MemoryPool<byte>.Shared.Rent(256);
PacketHeader connectHeader = new PacketHeader() { PacketHeader connectHeader = new PacketHeader {
Id = other.Id, Id = other.Id,
Type = PacketType.Connect Type = PacketType.Connect
}; };
MemoryMarshal.Write(connectBuffer.Memory.Span, ref connectHeader); MemoryMarshal.Write(connectBuffer.Memory.Span, ref connectHeader);
ConnectPacket connectPacket = new ConnectPacket() { ConnectPacket connectPacket = new ConnectPacket {
ConnectionType = ConnectionTypes.FirstConnection // doesn't matter what it is :) ConnectionType = ConnectionTypes.FirstConnection // doesn't matter what it is :)
}; };
MemoryMarshal.Write(connectBuffer.Memory.Span, ref connectPacket); MemoryMarshal.Write(connectBuffer.Memory.Span, ref connectPacket);
await client.Send(connectBuffer.Memory); await client.Send(connectBuffer.Memory);
connectBuffer.Dispose(); connectBuffer.Dispose();
}); });
Logger.Info($"Client {socket.RemoteEndPoint} ({client.Id}) connected."); Logger.Info($"Client {socket.RemoteEndPoint} ({client.Id}) connected.");
} }
// todo support variable length packets if they show up // todo support variable length packets if they show up
Logger.Warn($"broadcasting {header.Type} from {client.Id}"); Logger.Warn($"broadcasting {header.Type} from {client.Id}");
await Broadcast(memory, client); await Broadcast(memory, client);
} }
} } catch (Exception e) {
catch (Exception e) { if (e is SocketException { SocketErrorCode: SocketError.ConnectionReset }) {
if (e is SocketException {SocketErrorCode: SocketError.ConnectionReset}) {
Logger.Info($"Client {socket.RemoteEndPoint} ({client.Id}) disconnected from the server"); Logger.Info($"Client {socket.RemoteEndPoint} ({client.Id}) disconnected from the server");
} else { } else {
Logger.Error($"Exception on socket {socket.RemoteEndPoint} ({client.Id}) and disconnecting for: {e}"); Logger.Error($"Exception on socket {socket.RemoteEndPoint} ({client.Id}) and disconnecting for: {e}");

View file

@ -8,7 +8,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Shared\Shared.csproj" /> <ProjectReference Include="..\Shared\Shared.csproj"/>
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -3,13 +3,11 @@ using System.Runtime.InteropServices;
using Shared.Packet; using Shared.Packet;
using Shared.Packet.Packets; using Shared.Packet.Packets;
namespace Shared; namespace Shared;
public static class Constants { public static class Constants {
public const int MaxPacketSize = 256; public const int MaxPacketSize = 256;
public const int MaxClients = 4; public const int MaxClients = 4;
public static int HeaderSize { get; } = Marshal.SizeOf<PacketHeader>();
public static int PacketDataSize { get; } = MaxPacketSize - HeaderSize;
public const int CostumeNameSize = 0x20; public const int CostumeNameSize = 0x20;
// dictionary of packet types to packet // dictionary of packet types to packet
@ -18,4 +16,7 @@ public static class Constants {
.GetTypes() .GetTypes()
.Where(type => type.IsAssignableTo(typeof(IPacket))) .Where(type => type.IsAssignableTo(typeof(IPacket)))
.ToDictionary(type => type, type => type.GetCustomAttribute<PacketAttribute>()!); .ToDictionary(type => type, type => type.GetCustomAttribute<PacketAttribute>()!);
public static int HeaderSize { get; } = Marshal.SizeOf<PacketHeader>();
public static int PacketDataSize { get; } = MaxPacketSize - HeaderSize;
} }

View file

@ -1,16 +1,18 @@
using System.Text; namespace Shared;
namespace Shared;
public static class Extensions { public static class Extensions {
public static string Hex(this Span<byte> span) { public static string Hex(this Span<byte> span) {
return span.ToArray().Hex(); return span.ToArray().Hex();
} }
public static string Hex(this IEnumerable<byte> array) => string.Join(' ', array.ToArray().Select(x => x.ToString("X2"))); public static string Hex(this IEnumerable<byte> array) {
return string.Join(' ', array.ToArray().Select(x => x.ToString("X2")));
}
public static unsafe byte* Ptr(this Span<byte> span) { public static unsafe byte* Ptr(this Span<byte> span) {
fixed (byte* data = span) return data; fixed (byte* data = span) {
return data;
}
} }
public static string TrimNullTerm(this string text) { public static string TrimNullTerm(this string text) {

View file

@ -1,11 +1,12 @@
namespace Shared; namespace Shared;
public class Logger { public class Logger {
public string Name { get; }
public Logger(string name) { public Logger(string name) {
Name = name; Name = name;
} }
public string Name { get; }
public void Info(string text) { public void Info(string text) {
Console.ResetColor(); Console.ResetColor();
Console.WriteLine($"Info [{Name}] {text}"); Console.WriteLine($"Info [{Name}] {text}");

View file

@ -1,9 +1,10 @@
namespace Shared.Packet; namespace Shared.Packet;
[AttributeUsage(AttributeTargets.Struct, AllowMultiple = true)] [AttributeUsage(AttributeTargets.Struct, AllowMultiple = true)]
public class PacketAttribute : Attribute { public class PacketAttribute : Attribute {
public PacketType Type { get; }
public PacketAttribute(PacketType type) { public PacketAttribute(PacketType type) {
Type = type; Type = type;
} }
public PacketType Type { get; }
} }

View file

@ -1,13 +1,14 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Shared.Packet.Packets; using Shared.Packet.Packets;
namespace Shared.Packet; namespace Shared.Packet;
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]
public struct PacketHeader : IPacket { public struct PacketHeader : IPacket {
// public int Length; // public int Length;
public Guid Id; public Guid Id;
public PacketType Type; public PacketType Type;
public void Serialize(Span<byte> data) { public void Serialize(Span<byte> data) {
// MemoryMarshal.Write(data, ref Length); // MemoryMarshal.Write(data, ref Length);
MemoryMarshal.Write(data, ref Id); MemoryMarshal.Write(data, ref Id);

View file

@ -1,4 +1,4 @@
namespace Shared.Packet; namespace Shared.Packet;
public enum PacketType { public enum PacketType {
Unknown, Unknown,

View file

@ -1,13 +1,14 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Shared.Packet.Packets; using Shared.Packet.Packets;
namespace Shared.Packet; namespace Shared.Packet;
public static class PacketUtils { public static class PacketUtils {
public static void SerializeHeaded<T>(Span<byte> data, PacketHeader header, T t) where T : struct, IPacket { public static void SerializeHeaded<T>(Span<byte> data, PacketHeader header, T t) where T : struct, IPacket {
header.Serialize(data); header.Serialize(data);
t.Serialize(data[Constants.HeaderSize..]); t.Serialize(data[Constants.HeaderSize..]);
} }
public static T Deserialize<T>(Span<byte> data) where T : IPacket, new() { public static T Deserialize<T>(Span<byte> data) where T : IPacket, new() {
T packet = new T(); T packet = new T();
packet.Deserialize(data); packet.Deserialize(data);

View file

@ -2,7 +2,7 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
namespace Shared.Packet.Packets; namespace Shared.Packet.Packets;
[Packet(PacketType.Cap)] [Packet(PacketType.Cap)]
public struct CapPacket : IPacket { public struct CapPacket : IPacket {
@ -10,6 +10,7 @@ public struct CapPacket : IPacket {
public Vector3 Position; public Vector3 Position;
public Quaternion Rotation; public Quaternion Rotation;
public string CapAnim; public string CapAnim;
public void Serialize(Span<byte> data) { public void Serialize(Span<byte> data) {
MemoryMarshal.Write(data, ref Position); MemoryMarshal.Write(data, ref Position);
MemoryMarshal.Write(data[12..], ref Position); MemoryMarshal.Write(data[12..], ref Position);

View file

@ -1,13 +1,15 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
namespace Shared.Packet.Packets; namespace Shared.Packet.Packets;
[Packet(PacketType.Capture)] [Packet(PacketType.Capture)]
public struct CapturePacket : IPacket { public struct CapturePacket : IPacket {
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = Constants.CostumeNameSize)] [MarshalAs(UnmanagedType.ByValTStr, SizeConst = Constants.CostumeNameSize)]
public string ModelName; public string ModelName;
public bool IsCaptured; public bool IsCaptured;
public void Serialize(Span<byte> data) { public void Serialize(Span<byte> data) {
Encoding.UTF8.GetBytes(ModelName).CopyTo(data[..Constants.CostumeNameSize]); Encoding.UTF8.GetBytes(ModelName).CopyTo(data[..Constants.CostumeNameSize]);
MemoryMarshal.Write(data[Constants.CostumeNameSize..], ref IsCaptured); MemoryMarshal.Write(data[Constants.CostumeNameSize..], ref IsCaptured);

View file

@ -1,10 +1,11 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Shared.Packet.Packets; namespace Shared.Packet.Packets;
[Packet(PacketType.Connect)] [Packet(PacketType.Connect)]
public struct ConnectPacket : IPacket { public struct ConnectPacket : IPacket {
public ConnectionTypes ConnectionType; public ConnectionTypes ConnectionType;
public void Serialize(Span<byte> data) { public void Serialize(Span<byte> data) {
MemoryMarshal.Write(data, ref ConnectionType); MemoryMarshal.Write(data, ref ConnectionType);
} }

View file

@ -1,4 +1,4 @@
namespace Shared.Packet.Packets; namespace Shared.Packet.Packets;
public enum ConnectionTypes { public enum ConnectionTypes {
FirstConnection, FirstConnection,

View file

@ -1,14 +1,16 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
namespace Shared.Packet.Packets; namespace Shared.Packet.Packets;
[Packet(PacketType.Costume)] [Packet(PacketType.Costume)]
public struct CostumePacket : IPacket { public struct CostumePacket : IPacket {
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = Constants.CostumeNameSize)] [MarshalAs(UnmanagedType.ByValTStr, SizeConst = Constants.CostumeNameSize)]
public string BodyName; public string BodyName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = Constants.CostumeNameSize)] [MarshalAs(UnmanagedType.ByValTStr, SizeConst = Constants.CostumeNameSize)]
public string CapName; public string CapName;
public void Serialize(Span<byte> data) { public void Serialize(Span<byte> data) {
Encoding.UTF8.GetBytes(BodyName).CopyTo(data[..Constants.CostumeNameSize]); Encoding.UTF8.GetBytes(BodyName).CopyTo(data[..Constants.CostumeNameSize]);
Encoding.UTF8.GetBytes(CapName).CopyTo(data[Constants.CostumeNameSize..]); Encoding.UTF8.GetBytes(CapName).CopyTo(data[Constants.CostumeNameSize..]);

View file

@ -1,13 +1,9 @@
namespace Shared.Packet.Packets; namespace Shared.Packet.Packets;
[Packet(PacketType.Disconnect)] [Packet(PacketType.Disconnect)]
public struct DisconnectPacket : IPacket { public struct DisconnectPacket : IPacket {
//empty packet //empty packet
public void Serialize(Span<byte> data) { public void Serialize(Span<byte> data) { }
}
public void Deserialize(Span<byte> data) { public void Deserialize(Span<byte> data) { }
}
} }

View file

@ -1,6 +1,4 @@
using System.Runtime.InteropServices; namespace Shared.Packet.Packets;
namespace Shared.Packet.Packets;
// Packet interface for type safety // Packet interface for type safety
public interface IPacket { public interface IPacket {

View file

@ -1,5 +1,4 @@
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
@ -11,17 +10,22 @@ public struct PlayerPacket : IPacket {
public Vector3 Position; public Vector3 Position;
public Quaternion Rotation; public Quaternion Rotation;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)] [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
public float[] AnimationBlendWeights; public float[] AnimationBlendWeights;
public float AnimationRate; public float AnimationRate;
public bool Is2d; public bool Is2d;
public bool ThrowingCap; public bool ThrowingCap;
public bool IsIt; public bool IsIt;
public int ScenarioNum; public int ScenarioNum;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = NameSize)] [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NameSize)]
public string Stage; public string Stage;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = NameSize)] [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NameSize)]
public string Act; public string Act;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = NameSize)] [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NameSize)]
public string SubAct; public string SubAct;

View file

@ -1,11 +1,12 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Shared.Packet.Packets; namespace Shared.Packet.Packets;
[Packet(PacketType.Shine)] [Packet(PacketType.Shine)]
public struct ShinePacket : IPacket { public struct ShinePacket : IPacket {
public int ShineId; public int ShineId;
public bool IsGrand; public bool IsGrand;
public void Serialize(Span<byte> data) { public void Serialize(Span<byte> data) {
MemoryMarshal.Write(data, ref ShineId); MemoryMarshal.Write(data, ref ShineId);
MemoryMarshal.Write(data, ref IsGrand); MemoryMarshal.Write(data, ref IsGrand);

View file

@ -1,10 +1,11 @@
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
namespace Shared.Packet.Packets; namespace Shared.Packet.Packets;
[Packet(PacketType.Tag)] [Packet(PacketType.Tag)]
public struct TagPacket : IPacket { public struct TagPacket : IPacket {
public bool IsIt = false; public bool IsIt;
public void Serialize(Span<byte> data) { public void Serialize(Span<byte> data) {
MemoryMarshal.Write(data, ref IsIt); MemoryMarshal.Write(data, ref IsIt);
} }

View file

@ -1,9 +1,10 @@
namespace Shared.Packet.Packets; namespace Shared.Packet.Packets;
[Packet(PacketType.Unknown)] // empty like boss [Packet(PacketType.Unknown)] // empty like boss
// [Packet(PacketType.Command)] // [Packet(PacketType.Command)]
public struct UnhandledPacket : IPacket { public struct UnhandledPacket : IPacket {
public byte[] Data = new byte[Constants.PacketDataSize]; public byte[] Data = new byte[Constants.PacketDataSize];
public void Serialize(Span<byte> data) { public void Serialize(Span<byte> data) {
Data.CopyTo(data); Data.CopyTo(data);
} }

View file

@ -8,7 +8,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="System.Numerics.Vectors" Version="4.5.0" /> <PackageReference Include="System.Numerics.Vectors" Version="4.5.0"/>
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -1,7 +1,6 @@
using System.Buffers; using System.Buffers;
using System.Net.Sockets; using System.Net.Sockets;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Shared; using Shared;
using Shared.Packet; using Shared.Packet;

View file

@ -9,7 +9,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Shared\Shared.csproj" /> <ProjectReference Include="..\Shared\Shared.csproj"/>
</ItemGroup> </ItemGroup>
</Project> </Project>