404 lines
12 KiB
GDScript
404 lines
12 KiB
GDScript
extends PanelContainer
|
|
## Admin panel for in-game server management using Nakama RPC
|
|
## Requires server-side tekton_admin.ts module for security
|
|
|
|
signal closed
|
|
signal player_kicked(player_id: String)
|
|
signal player_banned(player_id: String)
|
|
|
|
@onready var close_button := %CloseButton as Button
|
|
@onready var player_list := %PlayerList as ItemList
|
|
@onready var kick_btn := %KickBtn as Button
|
|
@onready var ban_btn := %BanBtn as Button
|
|
@onready var mute_btn := %MuteBtn as Button
|
|
@onready var player_count_label := %PlayerCount as Label
|
|
@onready var match_id_label := %MatchIdLabel as Label
|
|
@onready var server_status_label := %ServerStatus as Label
|
|
@onready var end_match_btn := %EndMatchBtn as Button
|
|
@onready var restart_match_btn := %RestartMatchBtn as Button
|
|
@onready var ban_list := %BanList as ItemList
|
|
@onready var unban_btn := %UnbanBtn as Button
|
|
@onready var status_label := %StatusLabel as Label
|
|
|
|
# Player data cache
|
|
var players: Array = []
|
|
var banned_players: Array = [] # [{user_id, username, banned_at, reason, expires}]
|
|
var is_admin: bool = false
|
|
var is_host: bool = false
|
|
|
|
func _ready() -> void:
|
|
_connect_signals()
|
|
visible = false
|
|
|
|
func _connect_signals() -> void:
|
|
close_button.pressed.connect(_on_close_pressed)
|
|
kick_btn.pressed.connect(_on_kick_pressed)
|
|
ban_btn.pressed.connect(_on_ban_pressed)
|
|
mute_btn.pressed.connect(_on_mute_pressed)
|
|
end_match_btn.pressed.connect(_on_end_match_pressed)
|
|
restart_match_btn.pressed.connect(_on_restart_match_pressed)
|
|
unban_btn.pressed.connect(_on_unban_pressed)
|
|
player_list.item_selected.connect(_on_player_selected)
|
|
|
|
func show_panel() -> void:
|
|
# Check permissions
|
|
is_host = LobbyManager.is_host
|
|
is_admin = await _check_admin_status()
|
|
|
|
if not is_host and not is_admin:
|
|
status_label.text = "Admin access required"
|
|
status_label.add_theme_color_override("font_color", Color.RED)
|
|
return
|
|
|
|
# Show/hide admin-only features
|
|
ban_btn.visible = is_admin
|
|
unban_btn.visible = is_admin
|
|
|
|
await _refresh_all_data()
|
|
visible = true
|
|
|
|
func _check_admin_status() -> bool:
|
|
# Check with server if user has admin role
|
|
var result := await _rpc_call("get_user_profile", {})
|
|
if result.has("role"):
|
|
return result.role in ["admin", "moderator", "owner"]
|
|
return false
|
|
|
|
func _on_close_pressed() -> void:
|
|
visible = false
|
|
emit_signal("closed")
|
|
|
|
# =============================================================================
|
|
# RPC Helpers
|
|
# =============================================================================
|
|
|
|
func _rpc_call(rpc_name: String, payload: Dictionary) -> Dictionary:
|
|
if not NakamaManager.client or not NakamaManager.session:
|
|
push_error("[AdminPanel] Not connected to Nakama")
|
|
return {"error": "Not connected"}
|
|
|
|
var result = await NakamaManager.client.rpc_async(
|
|
NakamaManager.session,
|
|
rpc_name,
|
|
JSON.stringify(payload)
|
|
)
|
|
|
|
if result.is_exception():
|
|
var error: String = result.get_exception().message
|
|
push_error("[AdminPanel] RPC Error: " + error)
|
|
status_label.text = "Error: " + error
|
|
status_label.add_theme_color_override("font_color", Color.RED)
|
|
return {"error": error}
|
|
|
|
if result.payload:
|
|
return JSON.parse_string(result.payload)
|
|
return {}
|
|
|
|
# =============================================================================
|
|
# Data Refresh
|
|
# =============================================================================
|
|
|
|
func _refresh_all_data() -> void:
|
|
await _refresh_player_list()
|
|
await _refresh_server_stats()
|
|
if is_admin:
|
|
await _refresh_ban_list()
|
|
status_label.text = ""
|
|
|
|
func _refresh_player_list() -> void:
|
|
player_list.clear()
|
|
players = LobbyManager.get_players()
|
|
|
|
for player in players:
|
|
var name: String = player.get("name", "Unknown")
|
|
var id: int = player.get("id", 0)
|
|
var user_id: String = player.get("user_id", "")
|
|
var is_player_host := id == 1
|
|
|
|
var display := name
|
|
if is_player_host:
|
|
display += " (Host)"
|
|
|
|
player_list.add_item(display)
|
|
# Store user_id as metadata
|
|
player_list.set_item_metadata(player_list.item_count - 1, {
|
|
"user_id": user_id,
|
|
"peer_id": id,
|
|
"name": name
|
|
})
|
|
|
|
_update_action_buttons()
|
|
|
|
func _refresh_server_stats() -> void:
|
|
var stats := await _rpc_call("admin_get_server_stats", {
|
|
"match_id": NakamaManager.current_match_id
|
|
})
|
|
|
|
if stats.has("error"):
|
|
return
|
|
|
|
player_count_label.text = str(stats.get("total_players", LobbyManager.get_players().size()))
|
|
|
|
var match_id := NakamaManager.current_match_id
|
|
if match_id.length() > 16:
|
|
match_id = match_id.substr(0, 16) + "..."
|
|
match_id_label.text = match_id if match_id else "N/A"
|
|
|
|
server_status_label.text = "Running"
|
|
server_status_label.add_theme_color_override("font_color", Color.GREEN)
|
|
|
|
func _refresh_ban_list() -> void:
|
|
ban_list.clear()
|
|
|
|
var result := await _rpc_call("admin_get_ban_list", {})
|
|
|
|
if result.has("error"):
|
|
return
|
|
|
|
banned_players = result.get("bans", [])
|
|
|
|
for ban in banned_players:
|
|
var display := "%s - %s" % [
|
|
ban.get("username", "Unknown"),
|
|
ban.get("reason", "No reason")
|
|
]
|
|
if ban.get("expires"):
|
|
display += " (until " + ban.expires + ")"
|
|
|
|
ban_list.add_item(display)
|
|
ban_list.set_item_metadata(ban_list.item_count - 1, ban)
|
|
|
|
# =============================================================================
|
|
# Player Actions
|
|
# =============================================================================
|
|
|
|
func _on_player_selected(_index: int) -> void:
|
|
_update_action_buttons()
|
|
|
|
func _update_action_buttons() -> void:
|
|
var selected := player_list.get_selected_items()
|
|
if selected.is_empty():
|
|
kick_btn.disabled = true
|
|
ban_btn.disabled = true
|
|
mute_btn.disabled = true
|
|
return
|
|
|
|
var idx: int = selected[0]
|
|
var meta: Dictionary = player_list.get_item_metadata(idx)
|
|
var is_player_host: bool = meta.get("peer_id", 0) == 1
|
|
|
|
# Can't kick/ban the host or yourself
|
|
var is_self: bool = meta.get("user_id", "") == AuthManager.current_user.get("user_id", "")
|
|
|
|
kick_btn.disabled = is_player_host or is_self
|
|
ban_btn.disabled = is_player_host or is_self or not is_admin
|
|
mute_btn.disabled = is_self
|
|
|
|
func _on_kick_pressed() -> void:
|
|
var selected := player_list.get_selected_items()
|
|
if selected.is_empty():
|
|
return
|
|
|
|
var meta: Dictionary = player_list.get_item_metadata(selected[0])
|
|
var user_id: String = meta.get("user_id", "")
|
|
var player_name: String = meta.get("name", "Unknown")
|
|
|
|
if user_id.is_empty():
|
|
status_label.text = "Cannot kick: Invalid player"
|
|
return
|
|
|
|
status_label.text = "Kicking player..."
|
|
|
|
var result := await _rpc_call("admin_kick_player", {
|
|
"match_id": NakamaManager.current_match_id,
|
|
"user_id": user_id,
|
|
"reason": "Kicked by " + ("admin" if is_admin else "host")
|
|
})
|
|
|
|
if result.get("success", false):
|
|
status_label.text = "Kicked: " + player_name
|
|
status_label.add_theme_color_override("font_color", Color.GREEN)
|
|
emit_signal("player_kicked", user_id)
|
|
await get_tree().create_timer(0.5).timeout
|
|
_refresh_player_list()
|
|
else:
|
|
status_label.text = "Failed to kick player"
|
|
status_label.add_theme_color_override("font_color", Color.RED)
|
|
|
|
func _on_ban_pressed() -> void:
|
|
var selected := player_list.get_selected_items()
|
|
if selected.is_empty():
|
|
return
|
|
|
|
var meta: Dictionary = player_list.get_item_metadata(selected[0])
|
|
var user_id: String = meta.get("user_id", "")
|
|
var player_name: String = meta.get("name", "Unknown")
|
|
|
|
if user_id.is_empty():
|
|
status_label.text = "Cannot ban: Invalid player"
|
|
return
|
|
|
|
# Show ban dialog
|
|
var dialog := _create_ban_dialog(user_id, player_name)
|
|
add_child(dialog)
|
|
dialog.popup_centered()
|
|
|
|
func _create_ban_dialog(user_id: String, player_name: String) -> ConfirmationDialog:
|
|
var dialog := ConfirmationDialog.new()
|
|
dialog.title = "Ban Player"
|
|
|
|
var vbox := VBoxContainer.new()
|
|
|
|
var info_label := Label.new()
|
|
info_label.text = "Ban player: " + player_name
|
|
vbox.add_child(info_label)
|
|
|
|
var reason_label := Label.new()
|
|
reason_label.text = "Reason:"
|
|
vbox.add_child(reason_label)
|
|
|
|
var reason_input := LineEdit.new()
|
|
reason_input.placeholder_text = "Enter ban reason"
|
|
vbox.add_child(reason_input)
|
|
|
|
var duration_label := Label.new()
|
|
duration_label.text = "Duration (hours, 0 = permanent):"
|
|
vbox.add_child(duration_label)
|
|
|
|
var duration_input := SpinBox.new()
|
|
duration_input.min_value = 0
|
|
duration_input.max_value = 8760 # 1 year
|
|
duration_input.value = 24 # Default 24 hours
|
|
vbox.add_child(duration_input)
|
|
|
|
dialog.add_child(vbox)
|
|
|
|
dialog.confirmed.connect(func():
|
|
_execute_ban(user_id, player_name, reason_input.text, int(duration_input.value))
|
|
dialog.queue_free()
|
|
)
|
|
|
|
dialog.canceled.connect(func():
|
|
dialog.queue_free()
|
|
)
|
|
|
|
return dialog
|
|
|
|
func _execute_ban(user_id: String, player_name: String, reason: String, duration_hours: int) -> void:
|
|
status_label.text = "Banning player..."
|
|
|
|
var result := await _rpc_call("admin_ban_player", {
|
|
"user_id": user_id,
|
|
"reason": reason if reason else "No reason provided",
|
|
"duration_hours": duration_hours,
|
|
"match_id": NakamaManager.current_match_id
|
|
})
|
|
|
|
if result.get("success", false):
|
|
status_label.text = "Banned: " + player_name
|
|
status_label.add_theme_color_override("font_color", Color.GREEN)
|
|
emit_signal("player_banned", user_id)
|
|
await get_tree().create_timer(0.5).timeout
|
|
await _refresh_all_data()
|
|
else:
|
|
status_label.text = "Failed to ban: " + result.get("error", "Unknown error")
|
|
status_label.add_theme_color_override("font_color", Color.RED)
|
|
|
|
func _on_mute_pressed() -> void:
|
|
var selected := player_list.get_selected_items()
|
|
if selected.is_empty():
|
|
return
|
|
|
|
var meta: Dictionary = player_list.get_item_metadata(selected[0])
|
|
var player_name: String = meta.get("name", "Unknown")
|
|
|
|
# TODO: Implement mute via chat system RPC
|
|
status_label.text = "Muted: " + player_name
|
|
|
|
# =============================================================================
|
|
# Server Controls
|
|
# =============================================================================
|
|
|
|
func _on_end_match_pressed() -> void:
|
|
var dialog := ConfirmationDialog.new()
|
|
dialog.dialog_text = "Are you sure you want to end this match?\nAll players will be returned to the lobby."
|
|
add_child(dialog)
|
|
dialog.popup_centered()
|
|
|
|
dialog.confirmed.connect(func():
|
|
status_label.text = "Ending match..."
|
|
|
|
var result := await _rpc_call("admin_end_match", {
|
|
"match_id": NakamaManager.current_match_id,
|
|
"reason": "Ended by " + ("admin" if is_admin else "host")
|
|
})
|
|
|
|
if result.get("success", false):
|
|
status_label.text = "Match ended"
|
|
else:
|
|
status_label.text = "Failed to end match"
|
|
|
|
dialog.queue_free()
|
|
)
|
|
|
|
dialog.canceled.connect(func(): dialog.queue_free())
|
|
|
|
func _on_restart_match_pressed() -> void:
|
|
var dialog := ConfirmationDialog.new()
|
|
dialog.dialog_text = "Restart the current match?"
|
|
add_child(dialog)
|
|
dialog.popup_centered()
|
|
|
|
dialog.confirmed.connect(func():
|
|
# For restart, we'll use a different approach - reload scene locally
|
|
# The actual match restart logic would depend on your game
|
|
get_tree().reload_current_scene()
|
|
dialog.queue_free()
|
|
)
|
|
|
|
dialog.canceled.connect(func(): dialog.queue_free())
|
|
|
|
# =============================================================================
|
|
# Ban List Management
|
|
# =============================================================================
|
|
|
|
func _on_unban_pressed() -> void:
|
|
var selected := ban_list.get_selected_items()
|
|
if selected.is_empty():
|
|
status_label.text = "Select a player to unban"
|
|
return
|
|
|
|
var meta: Dictionary = ban_list.get_item_metadata(selected[0])
|
|
var user_id: String = meta.get("user_id", "")
|
|
var username: String = meta.get("username", "Unknown")
|
|
|
|
if user_id.is_empty():
|
|
status_label.text = "Invalid ban entry"
|
|
return
|
|
|
|
status_label.text = "Unbanning..."
|
|
|
|
var result := await _rpc_call("admin_unban_player", {
|
|
"user_id": user_id
|
|
})
|
|
|
|
if result.get("success", false):
|
|
status_label.text = "Unbanned: " + username
|
|
status_label.add_theme_color_override("font_color", Color.GREEN)
|
|
await _refresh_ban_list()
|
|
else:
|
|
status_label.text = "Failed to unban"
|
|
status_label.add_theme_color_override("font_color", Color.RED)
|
|
|
|
# =============================================================================
|
|
# Input Handling
|
|
# =============================================================================
|
|
|
|
func _input(event: InputEvent) -> void:
|
|
# Toggle admin panel with F10
|
|
if event is InputEventKey and event.pressed and event.keycode == KEY_F10:
|
|
if visible:
|
|
_on_close_pressed()
|
|
else:
|
|
show_panel()
|