mirror of
https://github.com/Impostor/Impostor.git
synced 2026-10-20 01:46:11 +00:00
Re-add Hazel client
This commit is contained in:
1 parent
503cd221d5
commit
f906b010db
11 files changed
+413
-27
No files matched your search
@@ -7,12 +7,16 @@ namespace Impostor.Api.Net.Messages.C2S
|
||||
{
|
||||
public static void Serialize(IMessageWriter writer, GameOptionsData gameOptionsData)
|
||||
{
|
||||
writer.StartMessage(MessageFlags.HostGame);
|
||||
|
||||
using (var memory = new MemoryStream())
|
||||
using (var writerBin = new BinaryWriter(memory))
|
||||
{
|
||||
gameOptionsData.Serialize(writerBin, GameOptionsData.LatestVersion);
|
||||
writer.WriteBytesAndSize(memory.ToArray());
|
||||
}
|
||||
|
||||
writer.EndMessage();
|
||||
}
|
||||
|
||||
public static GameOptionsData Deserialize(IMessageReader reader)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Impostor.Client\Impostor.Client.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,70 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Hazel;
|
||||
using Hazel.Udp;
|
||||
using Impostor.Api.Innersloth;
|
||||
using Impostor.Api.Net.Messages;
|
||||
using Impostor.Api.Net.Messages.C2S;
|
||||
using Serilog;
|
||||
|
||||
namespace Impostor.Client.App
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
private static readonly ManualResetEvent QuitEvent = new ManualResetEvent(false);
|
||||
|
||||
private static async Task Main(string[] args)
|
||||
{
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.WriteTo.Console()
|
||||
.CreateLogger();
|
||||
|
||||
var writeHandshake = MessageWriter.Get(MessageType.Reliable);
|
||||
|
||||
writeHandshake.Write(50516550);
|
||||
writeHandshake.Write("AeonLucid");
|
||||
|
||||
var writeGameCreate = MessageWriter.Get(MessageType.Reliable);
|
||||
|
||||
Message00HostGameC2S.Serialize(writeGameCreate, new GameOptionsData
|
||||
{
|
||||
MaxPlayers = 4,
|
||||
NumImpostors = 2
|
||||
});
|
||||
|
||||
using (var connection = new UdpClientConnection(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 22023)))
|
||||
{
|
||||
var e = new ManualResetEvent(false);
|
||||
|
||||
// Register events.
|
||||
connection.DataReceived = DataReceived;
|
||||
connection.Disconnected = Disconnected;
|
||||
|
||||
// Connect and send handshake.
|
||||
await connection.ConnectAsync(writeHandshake.ToByteArray(false));
|
||||
Log.Information("Connected.");
|
||||
|
||||
// Create a game.
|
||||
await connection.Send(writeGameCreate);
|
||||
Log.Information("Requested game creation.");
|
||||
|
||||
e.WaitOne();
|
||||
}
|
||||
}
|
||||
|
||||
private static ValueTask DataReceived(DataReceivedEventArgs e)
|
||||
{
|
||||
Log.Information("Received data.");
|
||||
return default;
|
||||
}
|
||||
|
||||
private static ValueTask Disconnected(DisconnectedEventArgs e)
|
||||
{
|
||||
Log.Information("Disconnected: " + e.Reason);
|
||||
QuitEvent.Set();
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Impostor.Api\Impostor.Api.csproj" />
|
||||
<ProjectReference Include="..\Impostor.Hazel\Impostor.Hazel.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -155,21 +155,13 @@ namespace Hazel
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public abstract ValueTask SendBytes(byte[] bytes, MessageType messageType = MessageType.Unreliable);
|
||||
|
||||
/// <summary>
|
||||
/// Connects the connection to a server and begins listening.
|
||||
/// This method blocks and may thrown if there is a problem connecting.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The bytes of data to send in the handshake.</param>
|
||||
/// <param name="timeout">The number of milliseconds to wait before giving up on the connect attempt.</param>
|
||||
public abstract void Connect(byte[] bytes = null, int timeout = 5000);
|
||||
|
||||
/// <summary>
|
||||
/// Connects the connection to a server and begins listening.
|
||||
/// This method does not block.
|
||||
/// </summary>
|
||||
/// <param name="bytes">The bytes of data to send in the handshake.</param>
|
||||
public abstract void ConnectAsync(byte[] bytes = null);
|
||||
public abstract ValueTask ConnectAsync(byte[] bytes = null);
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the DataReceived event.
|
||||
|
||||
@@ -52,14 +52,14 @@ namespace Hazel
|
||||
/// <summary>
|
||||
/// Sends a disconnect message to the end point.
|
||||
/// </summary>
|
||||
protected abstract bool SendDisconnect(MessageWriter writer);
|
||||
protected abstract ValueTask<bool> SendDisconnect(MessageWriter writer);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the socket has been disconnected at the remote host.
|
||||
/// </summary>
|
||||
protected async ValueTask DisconnectRemote(string reason, IMessageReader reader)
|
||||
{
|
||||
if (this.SendDisconnect(null))
|
||||
if (await SendDisconnect(null))
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -107,7 +107,7 @@ namespace Hazel
|
||||
/// </summary>
|
||||
public override async ValueTask Disconnect(string reason, MessageWriter writer = null)
|
||||
{
|
||||
if (this.SendDisconnect(writer))
|
||||
if (await SendDisconnect(writer))
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using Impostor.Api.Net.Messages;
|
||||
using Serilog;
|
||||
|
||||
namespace Hazel.Udp
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a client's connection to a server that uses the UDP protocol.
|
||||
/// </summary>
|
||||
/// <inheritdoc/>
|
||||
public sealed class UdpClientConnection : UdpConnection
|
||||
{
|
||||
private static readonly ILogger Logger = Log.ForContext<UdpClientConnection>();
|
||||
|
||||
/// <summary>
|
||||
/// The socket we're connected via.
|
||||
/// </summary>
|
||||
private readonly UdpClient _socket;
|
||||
|
||||
private readonly Timer _reliablePacketTimer;
|
||||
private readonly SemaphoreSlim _connectWaitLock;
|
||||
private readonly MemoryPool<byte> _pool;
|
||||
private readonly Channel<MessageData> _channel;
|
||||
private Task _listenTask;
|
||||
private Task _handleTask;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new UdpClientConnection.
|
||||
/// </summary>
|
||||
/// <param name="remoteEndPoint">A <see cref="NetworkEndPoint"/> to connect to.</param>
|
||||
public UdpClientConnection(IPEndPoint remoteEndPoint, IPMode ipMode = IPMode.IPv4) : base(null)
|
||||
{
|
||||
EndPoint = remoteEndPoint;
|
||||
RemoteEndPoint = remoteEndPoint;
|
||||
IPMode = ipMode;
|
||||
|
||||
_socket = new UdpClient
|
||||
{
|
||||
DontFragment = false
|
||||
};
|
||||
|
||||
_reliablePacketTimer = new Timer(ManageReliablePacketsInternal, null, 100, Timeout.Infinite);
|
||||
_connectWaitLock = new SemaphoreSlim(1, 1);
|
||||
_pool = MemoryPool<byte>.Shared;
|
||||
_channel = Channel.CreateUnbounded<MessageData>(new UnboundedChannelOptions
|
||||
{
|
||||
SingleReader = true,
|
||||
SingleWriter = true
|
||||
});
|
||||
}
|
||||
|
||||
~UdpClientConnection()
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
private async void ManageReliablePacketsInternal(object state)
|
||||
{
|
||||
await ManageReliablePackets();
|
||||
|
||||
try
|
||||
{
|
||||
_reliablePacketTimer.Change(100, Timeout.Infinite);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override ValueTask WriteBytesToConnection(byte[] bytes, int length)
|
||||
{
|
||||
return WriteBytesToConnectionReal(bytes, length);
|
||||
}
|
||||
|
||||
private async ValueTask WriteBytesToConnectionReal(byte[] bytes, int length)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _socket.SendAsync(bytes, length);
|
||||
}
|
||||
catch (NullReferenceException) { }
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Already disposed and disconnected...
|
||||
}
|
||||
catch (SocketException ex)
|
||||
{
|
||||
await DisconnectInternal(HazelInternalErrors.SocketExceptionSend, "Could not send data as a SocketException occurred: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async ValueTask ConnectAsync(byte[] bytes = null)
|
||||
{
|
||||
State = ConnectionState.Connecting;
|
||||
|
||||
try
|
||||
{
|
||||
_socket.Connect(RemoteEndPoint);
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
State = ConnectionState.NotConnected;
|
||||
throw new HazelException("A SocketException occurred while binding to the port.", e);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_listenTask = ListenAsync();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// If the socket's been disposed then we can just end there but make sure we're in NotConnected state.
|
||||
// If we end up here I'm really lost...
|
||||
State = ConnectionState.NotConnected;
|
||||
return;
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
Dispose();
|
||||
throw new HazelException("A SocketException occurred while initiating a receive operation.", e);
|
||||
}
|
||||
|
||||
// Write bytes to the server to tell it hi (and to punch a hole in our NAT, if present)
|
||||
// When acknowledged set the state to connected
|
||||
await SendHello(bytes, () =>
|
||||
{
|
||||
State = ConnectionState.Connected;
|
||||
InitializeKeepAliveTimer();
|
||||
});
|
||||
|
||||
await _connectWaitLock.WaitAsync(TimeSpan.FromSeconds(10));
|
||||
}
|
||||
|
||||
private async Task ListenAsync()
|
||||
{
|
||||
// Start packet handler.
|
||||
await StartAsync();
|
||||
|
||||
// Listen.
|
||||
while (State != ConnectionState.NotConnected)
|
||||
{
|
||||
UdpReceiveResult data;
|
||||
|
||||
try
|
||||
{
|
||||
data = await _socket.ReceiveAsync();
|
||||
}
|
||||
catch (SocketException e)
|
||||
{
|
||||
await DisconnectInternal(HazelInternalErrors.SocketExceptionReceive, "Socket exception while reading data: " + e.Message);
|
||||
return;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.Buffer.Length == 0)
|
||||
{
|
||||
await DisconnectInternal(HazelInternalErrors.ReceivedZeroBytes, "Received 0 bytes");
|
||||
return;
|
||||
}
|
||||
|
||||
await HandleAsync(data.Buffer);
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask HandleAsync(ReadOnlyMemory<byte> memory)
|
||||
{
|
||||
// Rent memory.
|
||||
var dest = _pool.Rent(memory.Length);
|
||||
|
||||
// Copy data.
|
||||
memory.CopyTo(dest.Memory);
|
||||
|
||||
try
|
||||
{
|
||||
// Write to client.
|
||||
await Pipeline.Writer.WriteAsync(new MessageData(dest, memory.Length));
|
||||
}
|
||||
catch (ChannelClosedException)
|
||||
{
|
||||
// Clean up.
|
||||
dest.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void SetState(ConnectionState state)
|
||||
{
|
||||
if (state == ConnectionState.Connected)
|
||||
{
|
||||
_connectWaitLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a disconnect message to the end point.
|
||||
/// You may include optional disconnect data. The SendOption must be unreliable.
|
||||
/// </summary>
|
||||
protected override async ValueTask<bool> SendDisconnect(MessageWriter data = null)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
if (_state == ConnectionState.NotConnected) return false;
|
||||
_state = ConnectionState.NotConnected;
|
||||
}
|
||||
|
||||
var bytes = EmptyDisconnectBytes;
|
||||
if (data != null && data.Length > 0)
|
||||
{
|
||||
if (data.SendOption != MessageType.Unreliable)
|
||||
{
|
||||
throw new ArgumentException("Disconnect messages can only be unreliable.");
|
||||
}
|
||||
|
||||
bytes = data.ToByteArray(true);
|
||||
bytes[0] = (byte)UdpSendOption.Disconnect;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _socket.SendAsync(bytes, bytes.Length, RemoteEndPoint);
|
||||
}
|
||||
catch { }
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
State = ConnectionState.NotConnected;
|
||||
|
||||
try { _socket.Close(); } catch { }
|
||||
try { _socket.Dispose(); } catch { }
|
||||
|
||||
_reliablePacketTimer.Dispose();
|
||||
_connectWaitLock.Dispose();
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,7 +187,10 @@ namespace Hazel.Udp
|
||||
_isFirst = false;
|
||||
|
||||
// Slice 4 bytes to get handshake data.
|
||||
await _listener.InvokeNewConnection(message.Slice(4), this);
|
||||
if (_listener != null)
|
||||
{
|
||||
await _listener.InvokeNewConnection(message.Slice(4), this);
|
||||
}
|
||||
}
|
||||
|
||||
switch (message.Buffer.Span[0])
|
||||
@@ -256,6 +259,28 @@ namespace Hazel.Udp
|
||||
|
||||
Statistics.LogUnreliableSend(length, bytes.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a hello packet to the remote endpoint.
|
||||
/// </summary>
|
||||
/// <param name="bytes"></param>
|
||||
/// <param name="acknowledgeCallback">The callback to invoke when the hello packet is acknowledged.</param>
|
||||
protected ValueTask SendHello(byte[] bytes, Action acknowledgeCallback)
|
||||
{
|
||||
//First byte of handshake is version indicator so add data after
|
||||
byte[] actualBytes;
|
||||
if (bytes == null)
|
||||
{
|
||||
actualBytes = new byte[1];
|
||||
}
|
||||
else
|
||||
{
|
||||
actualBytes = new byte[bytes.Length + 1];
|
||||
Buffer.BlockCopy(bytes, 0, actualBytes, 1, bytes.Length);
|
||||
}
|
||||
|
||||
return HandleSend(actualBytes, (byte)UdpSendOption.Hello, acknowledgeCallback);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Dispose(bool disposing)
|
||||
|
||||
@@ -47,16 +47,7 @@ namespace Hazel.Udp
|
||||
/// <remarks>
|
||||
/// This will always throw a HazelException.
|
||||
/// </remarks>
|
||||
public override void Connect(byte[] bytes = null, int timeout = 5000)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot manually connect a UdpServerConnection, did you mean to use UdpClientConnection?");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// This will always throw a HazelException.
|
||||
/// </remarks>
|
||||
public override void ConnectAsync(byte[] bytes = null)
|
||||
public override ValueTask ConnectAsync(byte[] bytes = null)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot manually connect a UdpServerConnection, did you mean to use UdpClientConnection?");
|
||||
}
|
||||
@@ -64,11 +55,11 @@ namespace Hazel.Udp
|
||||
/// <summary>
|
||||
/// Sends a disconnect message to the end point.
|
||||
/// </summary>
|
||||
protected override bool SendDisconnect(MessageWriter data = null)
|
||||
protected override ValueTask<bool> SendDisconnect(MessageWriter data = null)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
if (this._state != ConnectionState.Connected) return false;
|
||||
if (this._state != ConnectionState.Connected) return ValueTask.FromResult(false);
|
||||
this._state = ConnectionState.NotConnected;
|
||||
}
|
||||
|
||||
@@ -87,7 +78,7 @@ namespace Hazel.Udp
|
||||
}
|
||||
catch { }
|
||||
|
||||
return true;
|
||||
return ValueTask.FromResult(true);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace Impostor.Server.Net.Manager
|
||||
}
|
||||
|
||||
_nodeLocator.Save(gameCodeStr, _publicIp);
|
||||
_logger.LogDebug("Created game with code {0} ({1}).", game.Code, gameCode);
|
||||
_logger.LogDebug("Created game with code {0}.", game.Code);
|
||||
|
||||
await _eventManager.CallAsync(new GameCreatedEvent(game));
|
||||
|
||||
|
||||
@@ -27,6 +27,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Plugins.Debugger",
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Hazel", "Impostor.Hazel\Impostor.Hazel.csproj", "{671B753B-31AE-4C36-AD71-09CF00FA17CA}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "client", "client", "{9F1919B0-915B-4749-9944-697DF7E7F67F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Client", "Impostor.Client\Impostor.Client.csproj", "{BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Client.App", "Impostor.Client.App\Impostor.Client.App.csproj", "{3DF86F12-7099-44F6-B98B-A148213D60B1}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -103,6 +109,22 @@ Global
|
||||
{671B753B-31AE-4C36-AD71-09CF00FA17CA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{671B753B-31AE-4C36-AD71-09CF00FA17CA}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{671B753B-31AE-4C36-AD71-09CF00FA17CA}.Release|x86.Build.0 = Release|Any CPU
|
||||
{BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Release|x86.Build.0 = Release|Any CPU
|
||||
{3DF86F12-7099-44F6-B98B-A148213D60B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3DF86F12-7099-44F6-B98B-A148213D60B1}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3DF86F12-7099-44F6-B98B-A148213D60B1}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{3DF86F12-7099-44F6-B98B-A148213D60B1}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{3DF86F12-7099-44F6-B98B-A148213D60B1}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3DF86F12-7099-44F6-B98B-A148213D60B1}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3DF86F12-7099-44F6-B98B-A148213D60B1}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{3DF86F12-7099-44F6-B98B-A148213D60B1}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -116,5 +138,7 @@ Global
|
||||
{7C3EB599-2292-4532-B280-D5BED1094DD4} = {94FAED42-15BB-40CF-BE64-5D5C3B4ABC8F}
|
||||
{82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69} = {94FAED42-15BB-40CF-BE64-5D5C3B4ABC8F}
|
||||
{ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7} = {36AA9913-E6EA-4A6C-90E6-2FD3CC2E3124}
|
||||
{BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB} = {9F1919B0-915B-4749-9944-697DF7E7F67F}
|
||||
{3DF86F12-7099-44F6-B98B-A148213D60B1} = {9F1919B0-915B-4749-9944-697DF7E7F67F}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
Reference in new issue
Block a user