feat: adding basic multiplayer
This commit is contained in:
@@ -0,0 +1,69 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
signal player_connected(id: int)
|
||||||
|
signal player_disconnected(id: int)
|
||||||
|
signal connection_failed
|
||||||
|
signal connection_succeeded
|
||||||
|
|
||||||
|
const DEFAULT_PORT = 31415
|
||||||
|
|
||||||
|
var connected_players: Array[int] = []
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
multiplayer.peer_connected.connect(_on_peer_connected)
|
||||||
|
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
|
||||||
|
multiplayer.connected_to_server.connect(_on_connected_to_server)
|
||||||
|
multiplayer.connection_failed.connect(_on_connection_failed)
|
||||||
|
multiplayer.server_disconnected.connect(_on_server_disconnected)
|
||||||
|
|
||||||
|
func host_game(port: int = DEFAULT_PORT) -> Error:
|
||||||
|
var peer = ENetMultiplayerPeer.new()
|
||||||
|
var err = peer.create_server(port)
|
||||||
|
if err == OK:
|
||||||
|
multiplayer.multiplayer_peer = peer
|
||||||
|
connected_players.clear()
|
||||||
|
connected_players.append(1) # Host is 1
|
||||||
|
print("Hosting on port %s" % port)
|
||||||
|
return err
|
||||||
|
|
||||||
|
func join_game(ip: String, port: int = DEFAULT_PORT) -> Error:
|
||||||
|
var peer = ENetMultiplayerPeer.new()
|
||||||
|
var err = peer.create_client(ip, port)
|
||||||
|
if err == OK:
|
||||||
|
multiplayer.multiplayer_peer = peer
|
||||||
|
print("Connecting to %s:%s" % [ip, port])
|
||||||
|
return err
|
||||||
|
|
||||||
|
func disconnect_game() -> void:
|
||||||
|
multiplayer.multiplayer_peer = null
|
||||||
|
connected_players.clear()
|
||||||
|
|
||||||
|
func _on_peer_connected(id: int) -> void:
|
||||||
|
print("Player connected: ", id)
|
||||||
|
if not connected_players.has(id):
|
||||||
|
connected_players.append(id)
|
||||||
|
player_connected.emit(id)
|
||||||
|
|
||||||
|
func _on_peer_disconnected(id: int) -> void:
|
||||||
|
print("Player disconnected: ", id)
|
||||||
|
connected_players.erase(id)
|
||||||
|
player_disconnected.emit(id)
|
||||||
|
|
||||||
|
func _on_connected_to_server() -> void:
|
||||||
|
print("Successfully connected to server")
|
||||||
|
connection_succeeded.emit()
|
||||||
|
|
||||||
|
func _on_connection_failed() -> void:
|
||||||
|
print("Failed to connect to server")
|
||||||
|
multiplayer.multiplayer_peer = null
|
||||||
|
connection_failed.emit()
|
||||||
|
|
||||||
|
func _on_server_disconnected() -> void:
|
||||||
|
print("Server disconnected")
|
||||||
|
multiplayer.multiplayer_peer = null
|
||||||
|
connected_players.clear()
|
||||||
|
|
||||||
|
@rpc("authority", "call_local", "reliable")
|
||||||
|
func rpc_load_level(scene_path: String) -> void:
|
||||||
|
print("Loading level: ", scene_path)
|
||||||
|
get_tree().change_scene_to_file(scene_path)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bcq67g48h8hka
|
||||||
@@ -26,6 +26,7 @@ var default_bindings = {
|
|||||||
|
|
||||||
var current_bindings = {}
|
var current_bindings = {}
|
||||||
var show_debug_ui: bool = false
|
var show_debug_ui: bool = false
|
||||||
|
var username: String = "Player"
|
||||||
var mouse_sensitivity: float = 0.002
|
var mouse_sensitivity: float = 0.002
|
||||||
var ads_sensitivity: float = 0.001
|
var ads_sensitivity: float = 0.001
|
||||||
|
|
||||||
@@ -131,6 +132,7 @@ func _save_settings() -> void:
|
|||||||
var save_data = {
|
var save_data = {
|
||||||
"bindings": current_bindings,
|
"bindings": current_bindings,
|
||||||
"show_debug_ui": show_debug_ui,
|
"show_debug_ui": show_debug_ui,
|
||||||
|
"username": username,
|
||||||
"mouse_sensitivity": mouse_sensitivity,
|
"mouse_sensitivity": mouse_sensitivity,
|
||||||
"ads_sensitivity": ads_sensitivity,
|
"ads_sensitivity": ads_sensitivity,
|
||||||
"display_mode": display_mode,
|
"display_mode": display_mode,
|
||||||
@@ -165,12 +167,10 @@ func _load_settings() -> void:
|
|||||||
for action in loaded_bindings:
|
for action in loaded_bindings:
|
||||||
if current_bindings.has(action):
|
if current_bindings.has(action):
|
||||||
current_bindings[action] = loaded_bindings[action]
|
current_bindings[action] = loaded_bindings[action]
|
||||||
if data.has("show_debug_ui"):
|
if data.has("username"): username = data["username"]
|
||||||
show_debug_ui = data["show_debug_ui"]
|
if data.has("show_debug_ui"): show_debug_ui = data["show_debug_ui"]
|
||||||
if data.has("mouse_sensitivity"):
|
if data.has("mouse_sensitivity"): mouse_sensitivity = data["mouse_sensitivity"]
|
||||||
mouse_sensitivity = data["mouse_sensitivity"]
|
if data.has("ads_sensitivity"): ads_sensitivity = data["ads_sensitivity"]
|
||||||
if data.has("ads_sensitivity"):
|
|
||||||
ads_sensitivity = data["ads_sensitivity"]
|
|
||||||
if data.has("display_mode"): display_mode = data["display_mode"]
|
if data.has("display_mode"): display_mode = data["display_mode"]
|
||||||
if data.has("resolution_index"): resolution_index = data["resolution_index"]
|
if data.has("resolution_index"): resolution_index = data["resolution_index"]
|
||||||
if data.has("fps_cap"): fps_cap = data["fps_cap"]
|
if data.has("fps_cap"): fps_cap = data["fps_cap"]
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ config/icon="res://icon.svg"
|
|||||||
SettingsManager="*res://globals/settings_manager.gd"
|
SettingsManager="*res://globals/settings_manager.gd"
|
||||||
LoadoutManager="*res://globals/loadout_manager.gd"
|
LoadoutManager="*res://globals/loadout_manager.gd"
|
||||||
PauseMenu="*res://ui/pause_menu.gd"
|
PauseMenu="*res://ui/pause_menu.gd"
|
||||||
|
NetworkManager="*res://globals/network_manager.gd"
|
||||||
|
|
||||||
[input]
|
[input]
|
||||||
|
|
||||||
|
|||||||
+214
-2
@@ -2,6 +2,10 @@ extends Control
|
|||||||
|
|
||||||
var _ui_vbox: VBoxContainer
|
var _ui_vbox: VBoxContainer
|
||||||
var _level_selector_panel: VBoxContainer
|
var _level_selector_panel: VBoxContainer
|
||||||
|
var _multiplayer_panel: VBoxContainer
|
||||||
|
var _direct_connect_panel: VBoxContainer
|
||||||
|
var _host_lobby_panel: Control
|
||||||
|
var _host_lobby_players_list: ItemList
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
||||||
@@ -97,7 +101,157 @@ func _ready() -> void:
|
|||||||
|
|
||||||
_add_button(_level_selector_panel, "Back", _on_back_pressed)
|
_add_button(_level_selector_panel, "Back", _on_back_pressed)
|
||||||
|
|
||||||
func _build_level_card(parent: Container, title: String, scene_path: String, gradient: Gradient) -> void:
|
# ==========================================
|
||||||
|
# MULTIPLAYER PANEL
|
||||||
|
# ==========================================
|
||||||
|
_multiplayer_panel = VBoxContainer.new()
|
||||||
|
_multiplayer_panel.add_theme_constant_override("separation", 20)
|
||||||
|
_multiplayer_panel.set_anchor_and_offset(SIDE_LEFT, 0.0, 50)
|
||||||
|
_multiplayer_panel.set_anchor_and_offset(SIDE_TOP, 0.0, 50)
|
||||||
|
_multiplayer_panel.set_anchor_and_offset(SIDE_RIGHT, 0.0, 600)
|
||||||
|
_multiplayer_panel.set_anchor_and_offset(SIDE_BOTTOM, 1.0, -50)
|
||||||
|
_multiplayer_panel.hide()
|
||||||
|
add_child(_multiplayer_panel)
|
||||||
|
|
||||||
|
var mp_title = Label.new()
|
||||||
|
mp_title.text = "Multiplayer"
|
||||||
|
mp_title.add_theme_font_size_override("font_size", 64)
|
||||||
|
mp_title.add_theme_color_override("font_color", Color(1, 1, 1))
|
||||||
|
mp_title.add_theme_color_override("font_outline_color", Color(0, 0, 0))
|
||||||
|
mp_title.add_theme_constant_override("outline_size", 8)
|
||||||
|
_multiplayer_panel.add_child(mp_title)
|
||||||
|
|
||||||
|
var mp_spacer = Control.new()
|
||||||
|
mp_spacer.custom_minimum_size = Vector2(0, 20)
|
||||||
|
_multiplayer_panel.add_child(mp_spacer)
|
||||||
|
|
||||||
|
_add_button(_multiplayer_panel, "Host Game", _on_host_pressed)
|
||||||
|
_add_button(_multiplayer_panel, "Direct Connect", _on_direct_connect_pressed)
|
||||||
|
_add_button(_multiplayer_panel, "Back", _on_multiplayer_back_pressed)
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# DIRECT CONNECT PANEL
|
||||||
|
# ==========================================
|
||||||
|
_direct_connect_panel = VBoxContainer.new()
|
||||||
|
_direct_connect_panel.add_theme_constant_override("separation", 20)
|
||||||
|
_direct_connect_panel.set_anchor_and_offset(SIDE_LEFT, 0.0, 50)
|
||||||
|
_direct_connect_panel.set_anchor_and_offset(SIDE_TOP, 0.0, 50)
|
||||||
|
_direct_connect_panel.set_anchor_and_offset(SIDE_RIGHT, 0.0, 600)
|
||||||
|
_direct_connect_panel.set_anchor_and_offset(SIDE_BOTTOM, 1.0, -50)
|
||||||
|
_direct_connect_panel.hide()
|
||||||
|
add_child(_direct_connect_panel)
|
||||||
|
|
||||||
|
var dc_title = Label.new()
|
||||||
|
dc_title.text = "Direct Connect"
|
||||||
|
dc_title.add_theme_font_size_override("font_size", 64)
|
||||||
|
dc_title.add_theme_color_override("font_color", Color(1, 1, 1))
|
||||||
|
dc_title.add_theme_color_override("font_outline_color", Color(0, 0, 0))
|
||||||
|
dc_title.add_theme_constant_override("outline_size", 8)
|
||||||
|
_direct_connect_panel.add_child(dc_title)
|
||||||
|
|
||||||
|
var dc_ip_label = Label.new()
|
||||||
|
dc_ip_label.text = "IP Address:"
|
||||||
|
dc_ip_label.add_theme_font_size_override("font_size", 24)
|
||||||
|
_direct_connect_panel.add_child(dc_ip_label)
|
||||||
|
|
||||||
|
var dc_ip_input = LineEdit.new()
|
||||||
|
dc_ip_input.text = "127.0.0.1"
|
||||||
|
dc_ip_input.name = "IPInput"
|
||||||
|
dc_ip_input.add_theme_font_size_override("font_size", 24)
|
||||||
|
dc_ip_input.custom_minimum_size = Vector2(300, 50)
|
||||||
|
_direct_connect_panel.add_child(dc_ip_input)
|
||||||
|
|
||||||
|
var dc_port_label = Label.new()
|
||||||
|
dc_port_label.text = "Port:"
|
||||||
|
dc_port_label.add_theme_font_size_override("font_size", 24)
|
||||||
|
_direct_connect_panel.add_child(dc_port_label)
|
||||||
|
|
||||||
|
var dc_port_input = LineEdit.new()
|
||||||
|
dc_port_input.text = "31415" # Default Port
|
||||||
|
dc_port_input.name = "PortInput"
|
||||||
|
dc_port_input.add_theme_font_size_override("font_size", 24)
|
||||||
|
dc_port_input.custom_minimum_size = Vector2(300, 50)
|
||||||
|
_direct_connect_panel.add_child(dc_port_input)
|
||||||
|
|
||||||
|
var dc_spacer = Control.new()
|
||||||
|
dc_spacer.custom_minimum_size = Vector2(0, 20)
|
||||||
|
_direct_connect_panel.add_child(dc_spacer)
|
||||||
|
|
||||||
|
_add_button(_direct_connect_panel, "Connect", _on_connect_confirm_pressed)
|
||||||
|
_add_button(_direct_connect_panel, "Back", _on_dc_back_pressed)
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# HOST LOBBY PANEL
|
||||||
|
# ==========================================
|
||||||
|
_host_lobby_panel = Control.new()
|
||||||
|
_host_lobby_panel.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
_host_lobby_panel.hide()
|
||||||
|
add_child(_host_lobby_panel)
|
||||||
|
|
||||||
|
var hl_hbox = HBoxContainer.new()
|
||||||
|
hl_hbox.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
hl_hbox.add_theme_constant_override("separation", 50)
|
||||||
|
_host_lobby_panel.add_child(hl_hbox)
|
||||||
|
|
||||||
|
# Left Side (Level Selector)
|
||||||
|
var hl_left = VBoxContainer.new()
|
||||||
|
hl_left.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
hl_left.set_anchor_and_offset(SIDE_LEFT, 0.0, 50)
|
||||||
|
hl_left.set_anchor_and_offset(SIDE_TOP, 0.0, 50)
|
||||||
|
hl_left.add_theme_constant_override("separation", 20)
|
||||||
|
|
||||||
|
var hl_margin = MarginContainer.new()
|
||||||
|
hl_margin.add_theme_constant_override("margin_left", 50)
|
||||||
|
hl_margin.add_theme_constant_override("margin_top", 50)
|
||||||
|
hl_margin.add_theme_constant_override("margin_bottom", 50)
|
||||||
|
hl_margin.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
hl_hbox.add_child(hl_margin)
|
||||||
|
hl_margin.add_child(hl_left)
|
||||||
|
|
||||||
|
var hl_title = Label.new()
|
||||||
|
hl_title.text = "Host Lobby - Select Level"
|
||||||
|
hl_title.add_theme_font_size_override("font_size", 64)
|
||||||
|
hl_title.add_theme_color_override("font_color", Color(1, 1, 1))
|
||||||
|
hl_title.add_theme_color_override("font_outline_color", Color(0, 0, 0))
|
||||||
|
hl_title.add_theme_constant_override("outline_size", 8)
|
||||||
|
hl_left.add_child(hl_title)
|
||||||
|
|
||||||
|
var hl_cards = HBoxContainer.new()
|
||||||
|
hl_cards.add_theme_constant_override("separation", 30)
|
||||||
|
hl_left.add_child(hl_cards)
|
||||||
|
|
||||||
|
_build_level_card(hl_cards, "Dust 2", "res://scenes/dust2_level/dust2_level.tscn", d2_gradient, true)
|
||||||
|
_build_level_card(hl_cards, "Test Level", "res://scenes/test_level/test_level.tscn", tl_gradient, true)
|
||||||
|
|
||||||
|
var hl_spacer = Control.new()
|
||||||
|
hl_spacer.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
hl_left.add_child(hl_spacer)
|
||||||
|
|
||||||
|
_add_button(hl_left, "Disband & Back", _on_lobby_back_pressed)
|
||||||
|
|
||||||
|
# Right Side (Player List)
|
||||||
|
var hl_right = VBoxContainer.new()
|
||||||
|
hl_right.custom_minimum_size = Vector2(400, 0)
|
||||||
|
var hl_right_margin = MarginContainer.new()
|
||||||
|
hl_right_margin.add_theme_constant_override("margin_top", 50)
|
||||||
|
hl_right_margin.add_theme_constant_override("margin_right", 50)
|
||||||
|
hl_right_margin.add_theme_constant_override("margin_bottom", 50)
|
||||||
|
hl_hbox.add_child(hl_right_margin)
|
||||||
|
hl_right_margin.add_child(hl_right)
|
||||||
|
|
||||||
|
var pl_title = Label.new()
|
||||||
|
pl_title.text = "Players Connected"
|
||||||
|
pl_title.add_theme_font_size_override("font_size", 48)
|
||||||
|
pl_title.add_theme_color_override("font_outline_color", Color(0, 0, 0))
|
||||||
|
pl_title.add_theme_constant_override("outline_size", 8)
|
||||||
|
hl_right.add_child(pl_title)
|
||||||
|
|
||||||
|
_host_lobby_players_list = ItemList.new()
|
||||||
|
_host_lobby_players_list.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
_host_lobby_players_list.add_theme_font_size_override("font_size", 24)
|
||||||
|
hl_right.add_child(_host_lobby_players_list)
|
||||||
|
|
||||||
|
func _build_level_card(parent: Container, title: String, scene_path: String, gradient: Gradient, is_multiplayer: bool = false) -> void:
|
||||||
var btn = TextureButton.new()
|
var btn = TextureButton.new()
|
||||||
btn.custom_minimum_size = Vector2(300, 400)
|
btn.custom_minimum_size = Vector2(300, 400)
|
||||||
|
|
||||||
@@ -137,6 +291,9 @@ func _build_level_card(parent: Container, title: String, scene_path: String, gra
|
|||||||
lbl.set_anchors_preset(Control.PRESET_FULL_RECT)
|
lbl.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
overlay.add_child(lbl)
|
overlay.add_child(lbl)
|
||||||
|
|
||||||
|
if is_multiplayer:
|
||||||
|
btn.pressed.connect(func(): get_node("/root/NetworkManager").rpc_load_level.rpc(scene_path))
|
||||||
|
else:
|
||||||
btn.pressed.connect(func(): get_tree().change_scene_to_file(scene_path))
|
btn.pressed.connect(func(): get_tree().change_scene_to_file(scene_path))
|
||||||
|
|
||||||
parent.add_child(btn)
|
parent.add_child(btn)
|
||||||
@@ -159,7 +316,62 @@ func _on_singleplayer_pressed() -> void:
|
|||||||
_level_selector_panel.show()
|
_level_selector_panel.show()
|
||||||
|
|
||||||
func _on_multiplayer_pressed() -> void:
|
func _on_multiplayer_pressed() -> void:
|
||||||
print("Multiplayer not implemented yet.")
|
_ui_vbox.hide()
|
||||||
|
_multiplayer_panel.show()
|
||||||
|
|
||||||
|
func _on_multiplayer_back_pressed() -> void:
|
||||||
|
_multiplayer_panel.hide()
|
||||||
|
_ui_vbox.show()
|
||||||
|
|
||||||
|
func _on_host_pressed() -> void:
|
||||||
|
_multiplayer_panel.hide()
|
||||||
|
_host_lobby_panel.show()
|
||||||
|
var nm = get_node("/root/NetworkManager")
|
||||||
|
nm.host_game()
|
||||||
|
_update_lobby_players()
|
||||||
|
if not nm.player_connected.is_connected(_on_network_player_connected):
|
||||||
|
nm.player_connected.connect(_on_network_player_connected)
|
||||||
|
nm.player_disconnected.connect(_on_network_player_disconnected)
|
||||||
|
|
||||||
|
func _on_direct_connect_pressed() -> void:
|
||||||
|
_multiplayer_panel.hide()
|
||||||
|
_direct_connect_panel.show()
|
||||||
|
|
||||||
|
func _on_dc_back_pressed() -> void:
|
||||||
|
_direct_connect_panel.hide()
|
||||||
|
_multiplayer_panel.show()
|
||||||
|
|
||||||
|
func _on_connect_confirm_pressed() -> void:
|
||||||
|
var ip = _direct_connect_panel.get_node("IPInput").text
|
||||||
|
var port = _direct_connect_panel.get_node("PortInput").text.to_int()
|
||||||
|
get_node("/root/NetworkManager").join_game(ip, port)
|
||||||
|
# For simplicity, we can show a connecting label or just wait for the game to start via RPC
|
||||||
|
|
||||||
|
func _on_lobby_back_pressed() -> void:
|
||||||
|
_host_lobby_panel.hide()
|
||||||
|
_multiplayer_panel.show()
|
||||||
|
var nm = get_node("/root/NetworkManager")
|
||||||
|
nm.disconnect_game()
|
||||||
|
if nm.player_connected.is_connected(_on_network_player_connected):
|
||||||
|
nm.player_connected.disconnect(_on_network_player_connected)
|
||||||
|
nm.player_disconnected.disconnect(_on_network_player_disconnected)
|
||||||
|
|
||||||
|
func _on_network_player_connected(_id: int) -> void:
|
||||||
|
_update_lobby_players()
|
||||||
|
|
||||||
|
func _on_network_player_disconnected(_id: int) -> void:
|
||||||
|
_update_lobby_players()
|
||||||
|
|
||||||
|
func _update_lobby_players() -> void:
|
||||||
|
if not _host_lobby_players_list: return
|
||||||
|
_host_lobby_players_list.clear()
|
||||||
|
var nm = get_node("/root/NetworkManager")
|
||||||
|
var host_name = SettingsManager.username
|
||||||
|
for pid in nm.connected_players:
|
||||||
|
if pid == 1:
|
||||||
|
_host_lobby_players_list.add_item(host_name + " (Host)")
|
||||||
|
else:
|
||||||
|
_host_lobby_players_list.add_item("Player " + str(pid))
|
||||||
|
|
||||||
func _on_loadouts_pressed() -> void:
|
func _on_loadouts_pressed() -> void:
|
||||||
if has_node("/root/PauseMenu"):
|
if has_node("/root/PauseMenu"):
|
||||||
|
|||||||
+20
-3
@@ -239,9 +239,26 @@ func _build_ui() -> void:
|
|||||||
var general_tab = VBoxContainer.new()
|
var general_tab = VBoxContainer.new()
|
||||||
general_tab.name = "General"
|
general_tab.name = "General"
|
||||||
tab_container.add_child(general_tab)
|
tab_container.add_child(general_tab)
|
||||||
var gen_label = Label.new()
|
var user_hbox = HBoxContainer.new()
|
||||||
gen_label.text = "Settings coming soon..."
|
general_tab.add_child(user_hbox)
|
||||||
general_tab.add_child(gen_label)
|
|
||||||
|
var user_lbl = Label.new()
|
||||||
|
user_lbl.text = "Username:"
|
||||||
|
user_lbl.custom_minimum_size = Vector2(150, 0)
|
||||||
|
user_hbox.add_child(user_lbl)
|
||||||
|
|
||||||
|
var user_input = LineEdit.new()
|
||||||
|
user_input.text = SettingsManager.username
|
||||||
|
user_input.custom_minimum_size = Vector2(200, 0)
|
||||||
|
user_hbox.add_child(user_input)
|
||||||
|
|
||||||
|
var user_save = Button.new()
|
||||||
|
user_save.text = "Save"
|
||||||
|
user_save.pressed.connect(func():
|
||||||
|
SettingsManager.username = user_input.text
|
||||||
|
SettingsManager._save_settings()
|
||||||
|
)
|
||||||
|
user_hbox.add_child(user_save)
|
||||||
|
|
||||||
var spacer_gen = Control.new()
|
var spacer_gen = Control.new()
|
||||||
spacer_gen.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
spacer_gen.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
|||||||
Reference in New Issue
Block a user