New network manager test
This commit is contained in:
parent
707165373b
commit
6650820429
@ -1,6 +1,6 @@
|
||||
extends Control
|
||||
|
||||
@export var network_manager: NetworkManager
|
||||
@export var network_manager: OldnetworkManager
|
||||
|
||||
@export var host_button: Button
|
||||
@export var join_button: Button
|
||||
|
@ -1,28 +1,18 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://dfa8n5rw2qpfd"]
|
||||
[gd_scene load_steps=2 format=3 uid="uid://dfa8n5rw2qpfd"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://bpmn7t87tfk7f" path="res://assets/core/networking/old/old_network_manager.tscn" id="1_i1w5w"]
|
||||
[ext_resource type="Script" path="res://assets/core/networking/old/old_network_test_ui.gd" id="2_rbs4r"]
|
||||
[ext_resource type="PackedScene" uid="uid://biryul3n6thlw" path="res://assets/core/networking/old/old_user_box_prefab.tscn" id="3_if51w"]
|
||||
|
||||
[node name="Test-lobby" type="Node"]
|
||||
|
||||
[node name="NetworkManager" parent="." instance=ExtResource("1_i1w5w")]
|
||||
|
||||
[node name="UiRoot" type="Control" parent="." node_paths=PackedStringArray("network_manager", "host_button", "join_button", "lobby_id", "hosted_lobby_id", "users_list")]
|
||||
[node name="UiRoot" type="Control" parent="."]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("2_rbs4r")
|
||||
network_manager = NodePath("../NetworkManager")
|
||||
host_button = NodePath("Panel/HBoxContainer/VBoxContainer/Host")
|
||||
join_button = NodePath("Panel/HBoxContainer/VBoxContainer/Join")
|
||||
lobby_id = NodePath("Panel/HBoxContainer/VBoxContainer/LobbyID")
|
||||
hosted_lobby_id = NodePath("Panel/HBoxContainer/VBoxContainer/HostedLobbyID")
|
||||
users_list = NodePath("Panel/HBoxContainer/UsersList")
|
||||
user_box_prefab = ExtResource("3_if51w")
|
||||
|
||||
[node name="Panel" type="Panel" parent="UiRoot"]
|
||||
layout_mode = 1
|
||||
|
@ -1,4 +1,4 @@
|
||||
class_name NetworkManager
|
||||
class_name OldnetworkManager
|
||||
extends Node
|
||||
|
||||
const PACKET_READ_LIMIT: int = 32
|
@ -1,4 +1,6 @@
|
||||
[gd_scene format=3 uid="uid://7t1x82gvrw8a"]
|
||||
[gd_scene load_steps=2 format=3 uid="uid://7t1x82gvrw8a"]
|
||||
|
||||
[ext_resource type="Script" path="res://assets/core/networking/scripts/lobby.gd" id="1_o4fbq"]
|
||||
|
||||
[node name="Lobby" type="Control"]
|
||||
layout_mode = 3
|
||||
@ -7,3 +9,52 @@ anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_o4fbq")
|
||||
|
||||
[node name="Panel" type="Panel" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="Panel"]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="Buttons" type="VBoxContainer" parent="Panel/HBoxContainer"]
|
||||
custom_minimum_size = Vector2(500, 0)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 6
|
||||
size_flags_vertical = 4
|
||||
|
||||
[node name="Host" type="Button" parent="Panel/HBoxContainer/Buttons"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 4
|
||||
text = "Host Lobby"
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="Panel/HBoxContainer/Buttons"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="LineEdit" type="LineEdit" parent="Panel/HBoxContainer/Buttons/HBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="Join" type="Button" parent="Panel/HBoxContainer/Buttons/HBoxContainer"]
|
||||
custom_minimum_size = Vector2(90, 0)
|
||||
layout_mode = 2
|
||||
text = "Join"
|
||||
|
||||
[node name="Leave" type="Button" parent="Panel/HBoxContainer/Buttons"]
|
||||
layout_mode = 2
|
||||
disabled = true
|
||||
text = "Leave Lobby"
|
||||
|
||||
[node name="PlayerList" type="VBoxContainer" parent="Panel/HBoxContainer"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
|
@ -3,6 +3,8 @@
|
||||
[ext_resource type="Texture2D" uid="uid://fwub8fvl2u4i" path="res://addons/ingameconsole/ps1hagrid.png" id="1_gtutw"]
|
||||
|
||||
[node name="UserBox" type="HBoxContainer"]
|
||||
offset_right = 610.0
|
||||
offset_bottom = 128.0
|
||||
|
||||
[node name="PFP" type="TextureRect" parent="."]
|
||||
custom_minimum_size = Vector2(128, 128)
|
||||
@ -13,5 +15,6 @@ expand_mode = 1
|
||||
|
||||
[node name="Username" type="Label" parent="."]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 8
|
||||
size_flags_horizontal = 3
|
||||
text = "Hagrid 1"
|
||||
horizontal_alignment = 1
|
@ -1,11 +1,312 @@
|
||||
extends Node
|
||||
|
||||
const STEAM_APP_ID: int = 480
|
||||
|
||||
const PACKET_READ_LIMIT: int = 32
|
||||
|
||||
var lobby_data
|
||||
var lobby_id: int = 0
|
||||
var lobby_members: Array = []
|
||||
var lobby_members_max: int = 10
|
||||
var lobby_vote_kick: bool = false
|
||||
var lobby_type: int = Steam.LOBBY_TYPE_PUBLIC
|
||||
var steam_id: int = 0
|
||||
var steam_username: String = ""
|
||||
var avatar_texture_cache: Dictionary = {}
|
||||
|
||||
signal lobby_created(lobby_id: int)
|
||||
signal lobby_joined(lobby_id: int)
|
||||
signal user_joined_lobby(user_id: int)
|
||||
signal user_left_lobby(user_id: int)
|
||||
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready() -> void:
|
||||
pass # Replace with function body.
|
||||
_init_steam()
|
||||
|
||||
|
||||
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
||||
func _process(delta: float) -> void:
|
||||
func _process(_delta: float) -> void:
|
||||
Steam.run_callbacks()
|
||||
|
||||
if lobby_id > 0:
|
||||
read_all_p2p_packets()
|
||||
|
||||
|
||||
func _init_steam() -> void:
|
||||
var init_result = Steam.steamInitEx(true, STEAM_APP_ID)
|
||||
print("Steam Init Result: " + str(init_result))
|
||||
|
||||
if init_result['status'] > 0:
|
||||
printerr("Steam failed to initialize, shutting down: " + str(init_result))
|
||||
#get_tree().quit()
|
||||
|
||||
steam_id = Steam.getSteamID()
|
||||
steam_username = Steam.getFriendPersonaName(steam_id)
|
||||
|
||||
Steam.p2p_session_request.connect(_on_p2p_session_request)
|
||||
Steam.p2p_session_connect_fail.connect(_on_p2p_session_connect_fail)
|
||||
|
||||
Steam.join_requested.connect(_on_lobby_join_requested)
|
||||
Steam.lobby_chat_update.connect(_on_lobby_chat_update)
|
||||
Steam.lobby_created.connect(_on_lobby_created)
|
||||
Steam.lobby_data_update.connect(_on_lobby_data_update)
|
||||
Steam.lobby_invite.connect(_on_lobby_invite)
|
||||
Steam.lobby_joined.connect(_on_lobby_joined)
|
||||
Steam.lobby_match_list.connect(_on_lobby_match_list)
|
||||
Steam.lobby_message.connect(_on_lobby_message)
|
||||
Steam.persona_state_change.connect(_on_persona_change)
|
||||
|
||||
|
||||
func _on_lobby_join_requested(this_lobby_id: int, friend_id: int):
|
||||
var owner_name: String = Steam.getFriendPersonaName(friend_id)
|
||||
print("Trying to join " + owner_name + "'s lobby...")
|
||||
join_lobby(this_lobby_id)
|
||||
|
||||
|
||||
func _on_lobby_chat_update(this_lobby_id: int, change_id: int, making_change_id: int, chat_state: int) -> void:
|
||||
var changer_name: String = Steam.getFriendPersonaName(change_id)
|
||||
|
||||
# If a player has joined the lobby
|
||||
if chat_state == Steam.CHAT_MEMBER_STATE_CHANGE_ENTERED:
|
||||
print("%s has joined the lobby." % changer_name)
|
||||
|
||||
elif chat_state == Steam.CHAT_MEMBER_STATE_CHANGE_LEFT:
|
||||
print("%s has left the lobby." % changer_name)
|
||||
|
||||
elif chat_state == Steam.CHAT_MEMBER_STATE_CHANGE_KICKED:
|
||||
print("%s has been kicked from the lobby." % changer_name)
|
||||
|
||||
elif chat_state == Steam.CHAT_MEMBER_STATE_CHANGE_BANNED:
|
||||
print("%s has been banned from the lobby." % changer_name)
|
||||
|
||||
else:
|
||||
print("%s did... something." % changer_name)
|
||||
|
||||
get_lobby_members()
|
||||
|
||||
|
||||
func _on_lobby_created(connect: int, this_lobby_id: int) -> void:
|
||||
if connect == 1:
|
||||
lobby_id = this_lobby_id
|
||||
lobby_created.emit(lobby_id)
|
||||
print("Lobby created with ID: " + str(lobby_id))
|
||||
|
||||
Steam.setLobbyJoinable(lobby_id, true)
|
||||
Steam.setLobbyData(lobby_id, "name", str(steam_username) + "'s Lobby")
|
||||
|
||||
var set_relay: bool = Steam.allowP2PPacketRelay(true)
|
||||
|
||||
|
||||
func _on_lobby_data_update():
|
||||
pass
|
||||
|
||||
|
||||
func _on_lobby_invite():
|
||||
pass
|
||||
|
||||
|
||||
func _on_lobby_joined(this_lobby_id: int, _permissions: int, response: int) -> void:
|
||||
if response == Steam.CHAT_ROOM_ENTER_RESPONSE_SUCCESS:
|
||||
lobby_id = this_lobby_id
|
||||
get_lobby_members()
|
||||
make_p2p_handshake()
|
||||
lobby_joined.emit(lobby_id)
|
||||
else:
|
||||
var fail_reason: String
|
||||
|
||||
match response:
|
||||
Steam.CHAT_ROOM_ENTER_RESPONSE_DOESNT_EXIST: fail_reason = "This lobby no longer exists."
|
||||
Steam.CHAT_ROOM_ENTER_RESPONSE_NOT_ALLOWED: fail_reason = "You don't have permission to join this lobby."
|
||||
Steam.CHAT_ROOM_ENTER_RESPONSE_FULL: fail_reason = "The lobby is now full."
|
||||
Steam.CHAT_ROOM_ENTER_RESPONSE_ERROR: fail_reason = "Uh... something unexpected happened!"
|
||||
Steam.CHAT_ROOM_ENTER_RESPONSE_BANNED: fail_reason = "You are banned from this lobby."
|
||||
Steam.CHAT_ROOM_ENTER_RESPONSE_LIMITED: fail_reason = "You cannot join due to having a limited account."
|
||||
Steam.CHAT_ROOM_ENTER_RESPONSE_CLAN_DISABLED: fail_reason = "This lobby is locked or disabled."
|
||||
Steam.CHAT_ROOM_ENTER_RESPONSE_COMMUNITY_BAN: fail_reason = "This lobby is community locked."
|
||||
Steam.CHAT_ROOM_ENTER_RESPONSE_MEMBER_BLOCKED_YOU: fail_reason = "A user in the lobby has blocked you from joining."
|
||||
Steam.CHAT_ROOM_ENTER_RESPONSE_YOU_BLOCKED_MEMBER: fail_reason = "A user you have blocked is in the lobby."
|
||||
|
||||
print("Failed to join this chat room: %s" % fail_reason)
|
||||
|
||||
|
||||
func _on_lobby_match_list():
|
||||
pass
|
||||
|
||||
|
||||
func _on_lobby_message():
|
||||
pass
|
||||
|
||||
|
||||
func _on_persona_change(this_steam_id: int, _flags: int) -> void:
|
||||
if lobby_id > 0:
|
||||
print("User (%s) had information change, updating lobby list." % str(this_steam_id))
|
||||
get_lobby_members()
|
||||
|
||||
|
||||
func _on_p2p_session_request(remote_id: int):
|
||||
var this_requester: String = Steam.getFriendPersonaName(remote_id)
|
||||
print("P2P session request from: " + this_requester)
|
||||
Steam.acceptP2PSessionWithUser(remote_id)
|
||||
make_p2p_handshake()
|
||||
|
||||
|
||||
func _on_p2p_session_connect_fail(steam_id: int, session_error: int) -> void:
|
||||
# If no error was given
|
||||
if session_error == 0:
|
||||
print("WARNING: Session failure with %s: no error given" % steam_id)
|
||||
|
||||
# Else if target user was not running the same game
|
||||
elif session_error == 1:
|
||||
print("WARNING: Session failure with %s: target user not running the same game" % steam_id)
|
||||
|
||||
# Else if local user doesn't own app / game
|
||||
elif session_error == 2:
|
||||
print("WARNING: Session failure with %s: local user doesn't own app / game" % steam_id)
|
||||
|
||||
# Else if target user isn't connected to Steam
|
||||
elif session_error == 3:
|
||||
print("WARNING: Session failure with %s: target user isn't connected to Steam" % steam_id)
|
||||
|
||||
# Else if connection timed out
|
||||
elif session_error == 4:
|
||||
print("WARNING: Session failure with %s: connection timed out" % steam_id)
|
||||
|
||||
# Else if unused
|
||||
elif session_error == 5:
|
||||
print("WARNING: Session failure with %s: unused" % steam_id)
|
||||
|
||||
# Else no known error
|
||||
else:
|
||||
print("WARNING: Session failure with %s: unknown error %s" % [steam_id, session_error])
|
||||
|
||||
|
||||
func host_lobby():
|
||||
if lobby_id == 0:
|
||||
Steam.createLobby(lobby_type, lobby_members_max)
|
||||
else:
|
||||
printerr("Cannot host lobby, already in a lobby")
|
||||
|
||||
|
||||
func join_lobby(this_lobby_id: int):
|
||||
print("Attempting to join lobby: " + str(this_lobby_id))
|
||||
lobby_members.clear()
|
||||
Steam.joinLobby(this_lobby_id)
|
||||
|
||||
|
||||
func leave_lobby():
|
||||
if lobby_id != 0:
|
||||
Steam.leaveLobby(lobby_id)
|
||||
lobby_id = 0
|
||||
|
||||
for this_member in lobby_members:
|
||||
if this_member["steam_id"] != steam_id:
|
||||
Steam.closeP2PSessionWithUser(this_member["steam_id"])
|
||||
|
||||
lobby_members.clear()
|
||||
|
||||
|
||||
func get_lobby_members():
|
||||
lobby_members.clear()
|
||||
var member_count: int = Steam.getNumLobbyMembers(lobby_id)
|
||||
|
||||
for this_member in range(0, member_count):
|
||||
var member_steam_id: int = Steam.getLobbyMemberByIndex(lobby_id, this_member)
|
||||
var member_steam_name: String = Steam.getFriendPersonaName(member_steam_id)
|
||||
lobby_members.append({"steam_id":member_steam_id, "steam_name":member_steam_name})
|
||||
user_joined_lobby.emit(member_steam_id)
|
||||
await request_player_avatar(member_steam_id)
|
||||
|
||||
|
||||
func make_p2p_handshake():
|
||||
print("Sending p2p handshake to the lobby...")
|
||||
send_p2p_packet(0, {"message":"handshake", "from":steam_id})
|
||||
|
||||
|
||||
func send_p2p_packet(target: int, packet_data: Dictionary) -> void:
|
||||
# Set the send_type and channel
|
||||
var send_type: int = Steam.P2P_SEND_RELIABLE
|
||||
var channel: int = 0
|
||||
|
||||
# Create a data array to send the data through
|
||||
var this_data: PackedByteArray
|
||||
|
||||
# Compress the PackedByteArray we create from our dictionary using the GZIP compression method
|
||||
var compressed_data: PackedByteArray = var_to_bytes(packet_data).compress(FileAccess.COMPRESSION_GZIP)
|
||||
this_data.append_array(compressed_data)
|
||||
|
||||
# If sending a packet to everyone
|
||||
if target == 0:
|
||||
# If there is more than one user, send packets
|
||||
if lobby_members.size() > 1:
|
||||
# Loop through all members that aren't you
|
||||
for this_member in lobby_members:
|
||||
if this_member['steam_id'] != steam_id:
|
||||
Steam.sendP2PPacket(this_member['steam_id'], this_data, send_type, channel)
|
||||
|
||||
# Else send it to someone specific
|
||||
else:
|
||||
Steam.sendP2PPacket(target, this_data, send_type, channel)
|
||||
|
||||
|
||||
func read_all_p2p_packets(read_count: int = 0):
|
||||
if read_count >= PACKET_READ_LIMIT:
|
||||
return
|
||||
|
||||
if Steam.getAvailableP2PPacketSize(0) > 0:
|
||||
read_p2p_packet()
|
||||
read_all_p2p_packets(read_count + 1)
|
||||
|
||||
|
||||
func read_p2p_packet() -> void:
|
||||
var packet_size: int = Steam.getAvailableP2PPacketSize(0)
|
||||
|
||||
# There is a packet
|
||||
if packet_size > 0:
|
||||
var this_packet: Dictionary = Steam.readP2PPacket(packet_size, 0)
|
||||
|
||||
if this_packet.is_empty() or this_packet == null:
|
||||
print("WARNING: read an empty packet with non-zero size!")
|
||||
|
||||
# Get the remote user's ID
|
||||
var packet_sender: int = this_packet['remote_steam_id']
|
||||
|
||||
# Make the packet data readable
|
||||
var packet_code: PackedByteArray = this_packet['data']
|
||||
|
||||
# Decompress the array before turning it into a useable dictionary
|
||||
var readable_data: Dictionary = bytes_to_var(packet_code.decompress_dynamic(-1, FileAccess.COMPRESSION_GZIP))
|
||||
|
||||
# Print the packet to output
|
||||
print("Packet: %s" % readable_data)
|
||||
|
||||
|
||||
func request_player_avatar(user_id: int, size: int = 128) -> void:
|
||||
if avatar_texture_cache.has(user_id):
|
||||
return
|
||||
|
||||
var avatar_texture: ImageTexture = null
|
||||
Steam.getPlayerAvatar(Steam.AVATAR_LARGE, user_id)
|
||||
print("Attempting to get avatar for " + Steam.getFriendPersonaName(user_id) + ".....")
|
||||
|
||||
Steam.avatar_loaded.connect(func(loaded_user_id: int, avatar_size: int, avatar_buffer: PackedByteArray):
|
||||
if loaded_user_id == user_id:
|
||||
var avatar_image: Image = Image.create_from_data(avatar_size, avatar_size, false, Image.FORMAT_RGBA8, avatar_buffer)
|
||||
|
||||
if avatar_size != size:
|
||||
avatar_image.resize(size, size, Image.INTERPOLATE_LANCZOS)
|
||||
|
||||
avatar_texture = ImageTexture.create_from_image(avatar_image)
|
||||
avatar_texture_cache[user_id] = avatar_texture
|
||||
)
|
||||
|
||||
# Wait for the avatar to load asynchronously
|
||||
await get_tree().process_frames(2)
|
||||
|
||||
if avatar_texture == null:
|
||||
printerr("Could not load image for user: " + Steam.getFriendPersonaName(user_id))
|
||||
|
||||
|
||||
func get_player_avatar(user_id: int) -> ImageTexture:
|
||||
if avatar_texture_cache.has(user_id):
|
||||
return avatar_texture_cache[user_id]
|
||||
else:
|
||||
return await request_player_avatar(user_id)
|
@ -1,11 +1,62 @@
|
||||
extends Control
|
||||
|
||||
@export var host_button: Button
|
||||
@export var join_button: Button
|
||||
@export var leave_button: Button
|
||||
@export var lobby_id: LineEdit
|
||||
@export var user_list_box: VBoxContainer
|
||||
@export var user_box_prefab: PackedScene
|
||||
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready() -> void:
|
||||
pass # Replace with function body.
|
||||
host_button.pressed.connect(_on_host)
|
||||
join_button.pressed.connect(_on_join)
|
||||
leave_button.pressed.connect(_on_leave)
|
||||
|
||||
NetworkManager.lobby_created.connect(_on_lobby_created)
|
||||
NetworkManager.lobby_joined.connect(_on_lobby_joined)
|
||||
NetworkManager.user_joined_lobby.connect(_on_user_joined_lobby)
|
||||
NetworkManager.user_left_lobby.connect(_on_user_left_lobby)
|
||||
|
||||
|
||||
func _on_host():
|
||||
NetworkManager.host_lobby()
|
||||
host_button.disabled = true
|
||||
join_button.disabled = true
|
||||
leave_button.disabled = false
|
||||
lobby_id.disabled = true
|
||||
lobby_id.text = str(NetworkManager.lobby_id)
|
||||
|
||||
|
||||
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
||||
func _process(delta: float) -> void:
|
||||
pass
|
||||
func _on_join():
|
||||
NetworkManager.join_lobby(int(lobby_id.text))
|
||||
host_button.disabled = true
|
||||
join_button.disabled = true
|
||||
leave_button.disabled = false
|
||||
lobby_id.disabled = true
|
||||
|
||||
|
||||
func _on_leave():
|
||||
NetworkManager.leave_lobby()
|
||||
|
||||
|
||||
func _on_lobby_created():
|
||||
print("Lobby created")
|
||||
|
||||
|
||||
func _on_lobby_joined():
|
||||
print("Lobby joined")
|
||||
|
||||
|
||||
func _on_user_joined_lobby(user_id:int):
|
||||
var user_box = user_box_prefab.instance()
|
||||
user_box.get_node("Username").text = str(user_id)
|
||||
user_list_box.add_child(user_box)
|
||||
user_box.get_node("PFP").texture = await NetworkManager.get_player_avatar(user_id)
|
||||
|
||||
|
||||
func _on_user_left_lobby(user_id:int):
|
||||
for child in user_list_box.get_children():
|
||||
if child.get_node("Username").text == str(user_id):
|
||||
user_list_box.remove_child(child)
|
||||
break
|
@ -1,6 +1,6 @@
|
||||
class_name Player extends CharacterBody3D
|
||||
|
||||
var network_manager: NetworkManager
|
||||
var network_manager: OldnetworkManager
|
||||
var player_tag: Label3D
|
||||
var username: String = ""
|
||||
|
||||
|
@ -18,6 +18,7 @@ config/icon="res://assets/icon.png"
|
||||
[autoload]
|
||||
|
||||
GameConsole="*res://addons/ingameconsole/GameConsole.tscn"
|
||||
NetworkManager="*res://assets/core/networking/scripts/NetworkManager.gd"
|
||||
|
||||
[debug]
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user