mirror of
https://github.com/Facepunch/Facepunch.Steamworks.git
synced 2024-12-24 13:45:37 +03:00
Friends, Refactoring, Cleanup, Placeholder
This commit is contained in:
parent
c4e4bdb749
commit
cb926f6436
30
Facepunch.Steamworks.Test/Client.Stats.cs
Normal file
30
Facepunch.Steamworks.Test/Client.Stats.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Facepunch.Steamworks.Test
|
||||
{
|
||||
public partial class Client
|
||||
{
|
||||
[TestMethod]
|
||||
public void UpdateStats()
|
||||
{
|
||||
using ( var client = new Facepunch.Steamworks.Client( 252490 ) )
|
||||
{
|
||||
client.Stats.UpdateStats();
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UpdateSUpdateGlobalStatstats()
|
||||
{
|
||||
using ( var client = new Facepunch.Steamworks.Client( 252490 ) )
|
||||
{
|
||||
client.Stats.UpdateGlobalStats( 1 );
|
||||
client.Stats.UpdateGlobalStats( 3 );
|
||||
client.Stats.UpdateGlobalStats( 7 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -52,7 +52,7 @@ public void AuthSessionTicket()
|
||||
Assert.IsTrue( ticket.Handle != 0 );
|
||||
Assert.IsTrue( ticket.Data.Length > 0 );
|
||||
|
||||
client.Auth.CancelAuthTicket( ticket );
|
||||
ticket.Cancel();
|
||||
|
||||
Assert.IsTrue( ticket.Handle == 0 );
|
||||
}
|
||||
|
@ -96,6 +96,9 @@
|
||||
<Name>Facepunch.Steamworks</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Client.Stats.cs" />
|
||||
</ItemGroup>
|
||||
<Choose>
|
||||
<When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'">
|
||||
<ItemGroup>
|
||||
|
@ -25,10 +25,31 @@ public class Auth
|
||||
{
|
||||
internal Client client;
|
||||
|
||||
public class Ticket
|
||||
public class Ticket : IDisposable
|
||||
{
|
||||
internal Client client;
|
||||
|
||||
public byte[] Data;
|
||||
public uint Handle;
|
||||
|
||||
/// <summary>
|
||||
/// Cancels a ticket.
|
||||
/// You should cancel your ticket when you close the game or leave a server.
|
||||
/// </summary>
|
||||
public void Cancel()
|
||||
{
|
||||
if ( client.Valid && Handle != 0 )
|
||||
{
|
||||
client.native.user.CancelAuthTicket( Handle );
|
||||
Handle = 0;
|
||||
Data = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -49,21 +70,13 @@ public unsafe Ticket GetAuthSessionTicket()
|
||||
|
||||
return new Ticket()
|
||||
{
|
||||
client = client,
|
||||
Data = data.Take( (int)ticketLength ).ToArray(),
|
||||
Handle = ticket
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels a ticket.
|
||||
/// You should cancel your ticket when you close the game or leave a server.
|
||||
/// </summary>
|
||||
public void CancelAuthTicket( Ticket ticket )
|
||||
{
|
||||
client.native.user.CancelAuthTicket( ticket.Handle );
|
||||
ticket.Handle = 0;
|
||||
ticket.Data = null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -35,10 +35,36 @@ public struct Server
|
||||
public string[] Tags { get; set; }
|
||||
public ulong SteamId { get; set; }
|
||||
|
||||
public uint Address { get; set; }
|
||||
|
||||
public int ConnectionPort { get; set; }
|
||||
|
||||
public int QueryPort { get; set; }
|
||||
|
||||
public string AddressString
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Format( "{0}.{1}.{2}.{3}", ( Address >> 24 ) & 0xFFul, ( Address >> 16 ) & 0xFFul, ( Address >> 8 ) & 0xFFul, Address & 0xFFul );
|
||||
}
|
||||
}
|
||||
public string ConnectionAddress
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Format( "{0}.{1}.{2}.{3}:{4}", ( Address >> 24 ) & 0xFFul, ( Address >> 16 ) & 0xFFul, ( Address >> 8 ) & 0xFFul, Address & 0xFFul, ConnectionPort );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
internal static Server FromSteam( gameserveritem_t item )
|
||||
{
|
||||
return new Server()
|
||||
{
|
||||
Address = item.m_NetAdr.m_unIP,
|
||||
ConnectionPort = item.m_NetAdr.m_usConnectionPort,
|
||||
QueryPort = item.m_NetAdr.m_usQueryPort,
|
||||
Name = item.m_szServerName,
|
||||
Ping = item.m_nPing,
|
||||
GameDir = item.m_szGameDir,
|
||||
@ -58,35 +84,6 @@ internal static Server FromSteam( gameserveritem_t item )
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public servernetadr_t m_NetAdr;
|
||||
public int m_nPing;
|
||||
[MarshalAs(UnmanagedType.I1)]
|
||||
public bool m_bHadSuccessfulResponse;
|
||||
[MarshalAs(UnmanagedType.I1)]
|
||||
public bool m_bDoNotRefresh;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
|
||||
public string m_szGameDir; //char[32]
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
|
||||
public string m_szMap; //char[32]
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
|
||||
public string m_szGameDescription; //char[64]
|
||||
public uint m_nAppID;
|
||||
public int m_nPlayers;
|
||||
public int m_nMaxPlayers;
|
||||
public int m_nBotPlayers;
|
||||
[MarshalAs(UnmanagedType.I1)]
|
||||
public bool m_bPassword;
|
||||
[MarshalAs(UnmanagedType.I1)]
|
||||
public bool m_bSecure;
|
||||
public uint m_ulTimeLastPlayed;
|
||||
public int m_nServerVersion;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
|
||||
public string m_szServerName; //char[64]
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
|
||||
public string m_szGameTags; //char[128]
|
||||
public ulong m_steamID;
|
||||
|
||||
/// <summary>
|
||||
/// A list of servers that responded. If you're only interested in servers that responded since you
|
||||
/// last updated, then simply clear this list.
|
||||
|
@ -58,5 +58,42 @@ public unsafe Request Test()
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe Request History( Dictionary< string, string > filter )
|
||||
{
|
||||
var request = new Request()
|
||||
{
|
||||
client = client
|
||||
};
|
||||
|
||||
request.Id = client.native.servers.RequestHistoryServerList( client.AppId, new IntPtr[] { }, request.GetVTablePointer() );
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
public void AddToHistory( Request.Server server )
|
||||
{
|
||||
// client.native.matchmaking
|
||||
}
|
||||
|
||||
public void RemoveFromHistory( Request.Server server )
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public void AddToFavourite( Request.Server server )
|
||||
{
|
||||
// client.native.matchmaking
|
||||
}
|
||||
|
||||
public void RemoveFromFavourite( Request.Server server )
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
public bool IsFavourite( Request.Server server )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ internal class Internal : IDisposable
|
||||
internal Valve.Steamworks.ISteamMatchmakingServers servers;
|
||||
internal Valve.Steamworks.ISteamInventory inventory;
|
||||
internal Valve.Steamworks.ISteamNetworking networking;
|
||||
internal Valve.Steamworks.ISteamUserStats userstats;
|
||||
|
||||
internal bool Init()
|
||||
{
|
||||
@ -41,6 +42,7 @@ internal bool Init()
|
||||
inventory = client.GetISteamInventory( _huser, _hpipe, "STEAMINVENTORY_INTERFACE_V001" );
|
||||
networking = client.GetISteamNetworking( _huser, _hpipe, "SteamNetworking005" );
|
||||
apps = client.GetISteamApps( _huser, _hpipe, "STEAMAPPS_INTERFACE_VERSION008" );
|
||||
userstats = client.GetISteamUserStats( _huser, _hpipe, "STEAMUSERSTATS_INTERFACE_VERSION011" );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -35,6 +35,18 @@ public void MarkContentCorrupt( bool missingFilesOnly = false )
|
||||
client.native.apps.MarkContentCorrupt( missingFilesOnly );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current BuildId of the game.
|
||||
/// This is pretty useless, as it isn't guarenteed to return
|
||||
/// the build id you're playing, or the latest build id.
|
||||
/// </summary>
|
||||
public int BuildId
|
||||
{
|
||||
get
|
||||
{
|
||||
return client.native.apps.GetAppBuildId();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
40
Facepunch.Steamworks/Client/Friends.cs
Normal file
40
Facepunch.Steamworks/Client/Friends.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Facepunch.Steamworks
|
||||
{
|
||||
public partial class Client : IDisposable
|
||||
{
|
||||
Friends _friends;
|
||||
|
||||
public Friends Friends
|
||||
{
|
||||
get
|
||||
{
|
||||
if ( _friends == null )
|
||||
_friends = new Friends( this );
|
||||
|
||||
return _friends;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class Friends
|
||||
{
|
||||
internal Client client;
|
||||
|
||||
internal Friends( Client c )
|
||||
{
|
||||
client = c;
|
||||
}
|
||||
|
||||
public string GetName( ulong steamid )
|
||||
{
|
||||
client.native.friends.RequestUserInformation( steamid, true );
|
||||
|
||||
return client.native.friends.GetFriendPersonaName( steamid );
|
||||
}
|
||||
}
|
||||
}
|
@ -44,25 +44,11 @@ internal Inventory( Client c )
|
||||
/// IE - your player is alive, and playing.
|
||||
/// Don't stress on it too much tho cuz it's super hijackable anyway.
|
||||
/// </summary>
|
||||
public void DropHeartbeat()
|
||||
public void PlaytimeHeartbeat()
|
||||
{
|
||||
client.native.inventory.SendItemDropHeartbeat();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trigger an item drop. Call this when it's a good time to award
|
||||
/// an item drop to a player. This won't automatically result in giving
|
||||
/// an item to a player. Just call it every minute or so, or on launch.
|
||||
/// ItemDefinition is usually a generator
|
||||
/// </summary>
|
||||
public void TriggerItemDrop( Definition definition )
|
||||
{
|
||||
int result = 0;
|
||||
client.native.inventory.TriggerItemDrop( ref result, definition.Id );
|
||||
client.native.inventory.DestroyResult( result );
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Call this to retrieve the items.
|
||||
/// Note that if this has already been called it won't
|
||||
@ -256,14 +242,42 @@ public T GetProperty<T>( string name )
|
||||
}
|
||||
}
|
||||
|
||||
internal void SetupCommonProperties()
|
||||
{
|
||||
Name = GetProperty<string>( "name" );
|
||||
Description = GetProperty<string>( "description" );
|
||||
Created = GetProperty<DateTime>( "timestamp" );
|
||||
Modified = GetProperty<DateTime>( "modified" );
|
||||
internal void SetupCommonProperties()
|
||||
{
|
||||
Name = GetProperty<string>( "name" );
|
||||
Description = GetProperty<string>( "description" );
|
||||
Created = GetProperty<DateTime>( "timestamp" );
|
||||
Modified = GetProperty<DateTime>( "modified" );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trigger an item drop. Call this when it's a good time to award
|
||||
/// an item drop to a player. This won't automatically result in giving
|
||||
/// an item to a player. Just call it every minute or so, or on launch.
|
||||
/// ItemDefinition is usually a generator
|
||||
/// </summary>
|
||||
public void TriggerItemDrop()
|
||||
{
|
||||
int result = 0;
|
||||
client.native.inventory.TriggerItemDrop( ref result, Id );
|
||||
client.native.inventory.DestroyResult( result );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Utility, given a "1;VLV250" string, convert it to a 2.5
|
||||
/// </summary>
|
||||
public static float PriceCategoryToFloat( string price )
|
||||
{
|
||||
price = price.Replace( "1;VLV", "" );
|
||||
|
||||
int iPrice = 0;
|
||||
if ( !int.TryParse( price, out iPrice ) )
|
||||
return 0.0f;
|
||||
|
||||
return int.Parse( price ) / 100.0f;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -29,5 +29,38 @@ internal Stats( Client c )
|
||||
{
|
||||
client = c;
|
||||
}
|
||||
|
||||
public void UpdateStats()
|
||||
{
|
||||
client.native.userstats.RequestCurrentStats();
|
||||
}
|
||||
|
||||
public void UpdateGlobalStats( int days = 1 )
|
||||
{
|
||||
client.native.userstats.GetNumberOfCurrentPlayers();
|
||||
client.native.userstats.RequestGlobalAchievementPercentages();
|
||||
client.native.userstats.RequestGlobalStats( days );
|
||||
}
|
||||
|
||||
public int GetInt( string name )
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int GetGlobalInt( string name )
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int GetFloat( string name )
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public int GetGlobalFloat( string name )
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -121,6 +121,7 @@
|
||||
<Compile Include="Client.Auth.cs" />
|
||||
<Compile Include="Client.cs" />
|
||||
<Compile Include="Client\App.cs" />
|
||||
<Compile Include="Client\Friends.cs" />
|
||||
<Compile Include="Client\Inventory.cs" />
|
||||
<Compile Include="Client\Stats.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
|
@ -1817,11 +1817,11 @@ internal abstract class ISteamMatchmakingServers
|
||||
{
|
||||
internal abstract IntPtr GetIntPtr();
|
||||
internal abstract IntPtr RequestInternetServerList( uint iApp, IntPtr[] ppchFilters, IntPtr pRequestServersResponse );
|
||||
internal abstract IntPtr RequestLANServerList( uint iApp, ISteamMatchmakingServerListResponse pRequestServersResponse );
|
||||
internal abstract IntPtr RequestFriendsServerList( uint iApp, IntPtr[] ppchFilters, ISteamMatchmakingServerListResponse pRequestServersResponse );
|
||||
internal abstract IntPtr RequestFavoritesServerList( uint iApp, IntPtr[] ppchFilters, ISteamMatchmakingServerListResponse pRequestServersResponse );
|
||||
internal abstract IntPtr RequestHistoryServerList( uint iApp, IntPtr[] ppchFilters, ISteamMatchmakingServerListResponse pRequestServersResponse );
|
||||
internal abstract IntPtr RequestSpectatorServerList( uint iApp, IntPtr[] ppchFilters, ISteamMatchmakingServerListResponse pRequestServersResponse );
|
||||
internal abstract IntPtr RequestLANServerList( uint iApp, IntPtr pRequestServersResponse );
|
||||
internal abstract IntPtr RequestFriendsServerList( uint iApp, IntPtr[] ppchFilters, IntPtr pRequestServersResponse );
|
||||
internal abstract IntPtr RequestFavoritesServerList( uint iApp, IntPtr[] ppchFilters, IntPtr pRequestServersResponse );
|
||||
internal abstract IntPtr RequestHistoryServerList( uint iApp, IntPtr[] ppchFilters, IntPtr pRequestServersResponse );
|
||||
internal abstract IntPtr RequestSpectatorServerList( uint iApp, IntPtr[] ppchFilters, IntPtr pRequestServersResponse );
|
||||
internal abstract void ReleaseRequest( IntPtr hServerListRequest );
|
||||
internal abstract gameserveritem_t GetServerDetails( IntPtr hRequest, int iServer );
|
||||
internal abstract void CancelQuery( IntPtr hRequest );
|
||||
@ -2457,7 +2457,7 @@ internal override ISteamUserStats GetISteamUserStats( uint hSteamUser, uint hSte
|
||||
{
|
||||
CheckIfUsable();
|
||||
IntPtr result = NativeEntrypoints.SteamAPI_ISteamClient_GetISteamUserStats(m_pSteamClient,hSteamUser,hSteamPipe,pchVersion);
|
||||
return (ISteamUserStats)Marshal.PtrToStructure( result, typeof( ISteamUserStats ) );
|
||||
return new CSteamUserStats( result );
|
||||
}
|
||||
internal override ISteamGameServerStats GetISteamGameServerStats( uint hSteamuser, uint hSteamPipe, string pchVersion )
|
||||
{
|
||||
@ -3781,34 +3781,34 @@ internal override IntPtr RequestInternetServerList( uint iApp, IntPtr[] ppchFilt
|
||||
IntPtr result = NativeEntrypoints.SteamAPI_ISteamMatchmakingServers_RequestInternetServerList(m_pSteamMatchmakingServers,iApp,ppchFilters,(uint) ppchFilters.Length,pRequestServersResponse);
|
||||
return result;
|
||||
}
|
||||
internal override IntPtr RequestLANServerList( uint iApp, ISteamMatchmakingServerListResponse pRequestServersResponse )
|
||||
internal override IntPtr RequestLANServerList( uint iApp, IntPtr pRequestServersResponse )
|
||||
{
|
||||
CheckIfUsable();
|
||||
IntPtr result = NativeEntrypoints.SteamAPI_ISteamMatchmakingServers_RequestLANServerList(m_pSteamMatchmakingServers,iApp,pRequestServersResponse.GetIntPtr());
|
||||
IntPtr result = NativeEntrypoints.SteamAPI_ISteamMatchmakingServers_RequestLANServerList(m_pSteamMatchmakingServers,iApp,pRequestServersResponse);
|
||||
return result;
|
||||
}
|
||||
internal override IntPtr RequestFriendsServerList( uint iApp, IntPtr[] ppchFilters, ISteamMatchmakingServerListResponse pRequestServersResponse )
|
||||
internal override IntPtr RequestFriendsServerList( uint iApp, IntPtr[] ppchFilters, IntPtr pRequestServersResponse )
|
||||
{
|
||||
CheckIfUsable();
|
||||
IntPtr result = NativeEntrypoints.SteamAPI_ISteamMatchmakingServers_RequestFriendsServerList(m_pSteamMatchmakingServers,iApp,ppchFilters,(uint) ppchFilters.Length,pRequestServersResponse.GetIntPtr());
|
||||
IntPtr result = NativeEntrypoints.SteamAPI_ISteamMatchmakingServers_RequestFriendsServerList(m_pSteamMatchmakingServers,iApp,ppchFilters,(uint) ppchFilters.Length,pRequestServersResponse);
|
||||
return result;
|
||||
}
|
||||
internal override IntPtr RequestFavoritesServerList( uint iApp, IntPtr[] ppchFilters, ISteamMatchmakingServerListResponse pRequestServersResponse )
|
||||
internal override IntPtr RequestFavoritesServerList( uint iApp, IntPtr[] ppchFilters, IntPtr pRequestServersResponse )
|
||||
{
|
||||
CheckIfUsable();
|
||||
IntPtr result = NativeEntrypoints.SteamAPI_ISteamMatchmakingServers_RequestFavoritesServerList(m_pSteamMatchmakingServers,iApp,ppchFilters,(uint) ppchFilters.Length,pRequestServersResponse.GetIntPtr());
|
||||
IntPtr result = NativeEntrypoints.SteamAPI_ISteamMatchmakingServers_RequestFavoritesServerList(m_pSteamMatchmakingServers,iApp,ppchFilters,(uint) ppchFilters.Length,pRequestServersResponse);
|
||||
return result;
|
||||
}
|
||||
internal override IntPtr RequestHistoryServerList( uint iApp, IntPtr[] ppchFilters, ISteamMatchmakingServerListResponse pRequestServersResponse )
|
||||
internal override IntPtr RequestHistoryServerList( uint iApp, IntPtr[] ppchFilters, IntPtr pRequestServersResponse )
|
||||
{
|
||||
CheckIfUsable();
|
||||
IntPtr result = NativeEntrypoints.SteamAPI_ISteamMatchmakingServers_RequestHistoryServerList(m_pSteamMatchmakingServers,iApp,ppchFilters,(uint) ppchFilters.Length,pRequestServersResponse.GetIntPtr());
|
||||
IntPtr result = NativeEntrypoints.SteamAPI_ISteamMatchmakingServers_RequestHistoryServerList(m_pSteamMatchmakingServers,iApp,ppchFilters,(uint) ppchFilters.Length,pRequestServersResponse);
|
||||
return result;
|
||||
}
|
||||
internal override IntPtr RequestSpectatorServerList( uint iApp, IntPtr[] ppchFilters, ISteamMatchmakingServerListResponse pRequestServersResponse )
|
||||
internal override IntPtr RequestSpectatorServerList( uint iApp, IntPtr[] ppchFilters, IntPtr pRequestServersResponse )
|
||||
{
|
||||
CheckIfUsable();
|
||||
IntPtr result = NativeEntrypoints.SteamAPI_ISteamMatchmakingServers_RequestSpectatorServerList(m_pSteamMatchmakingServers,iApp,ppchFilters,(uint) ppchFilters.Length,pRequestServersResponse.GetIntPtr());
|
||||
IntPtr result = NativeEntrypoints.SteamAPI_ISteamMatchmakingServers_RequestSpectatorServerList(m_pSteamMatchmakingServers,iApp,ppchFilters,(uint) ppchFilters.Length,pRequestServersResponse);
|
||||
return result;
|
||||
}
|
||||
internal override void ReleaseRequest( IntPtr hServerListRequest )
|
||||
@ -9037,7 +9037,7 @@ public struct MatchMakingKeyValuePair_t
|
||||
public struct servernetadr_t
|
||||
{
|
||||
public ushort m_usConnectionPort;
|
||||
public char m_usQueryPort;
|
||||
public ushort m_usQueryPort;
|
||||
public uint m_unIP;
|
||||
}
|
||||
[StructLayout( LayoutKind.Sequential, Size = 372, Pack = 4, CharSet = CharSet.Ansi )]
|
||||
|
Loading…
Reference in New Issue
Block a user