|
|
|
@@ -1,403 +1,510 @@
|
|
|
|
|
extends PanelContainer
|
|
|
|
|
## Admin panel for in-game server management using Nakama RPC
|
|
|
|
|
## Requires server-side tekton_admin.ts module for security
|
|
|
|
|
extends Panel
|
|
|
|
|
## Server Admin Panel — Management for Users and Leaderboards.
|
|
|
|
|
## Features: User BAN/DELETE/ROLE management, Leaderboard score modification.
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
# -- UI refs --
|
|
|
|
|
@onready var tabs := %Tabs as TabContainer
|
|
|
|
|
@onready var title_label := %Title as Label
|
|
|
|
|
@onready var count_label := %CountLabel as Label
|
|
|
|
|
@onready var refresh_btn := %RefreshBtn as Button
|
|
|
|
|
@onready var close_btn := %CloseBtn 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
|
|
|
|
|
# Tab: Users
|
|
|
|
|
@onready var user_tree := %UserTree as Tree
|
|
|
|
|
@onready var select_all_btn := %SelectAllBtn as Button
|
|
|
|
|
@onready var deselect_btn := %DeselectBtn as Button
|
|
|
|
|
@onready var selected_label := %SelectedLabel as Label
|
|
|
|
|
@onready var ban_btn := %BanBtn as Button
|
|
|
|
|
@onready var unban_btn := %UnbanBtn as Button
|
|
|
|
|
@onready var delete_btn := %DeleteBtn as Button
|
|
|
|
|
|
|
|
|
|
# Tab: Leaderboards
|
|
|
|
|
@onready var lb_tree := %LeaderboardTree as Tree
|
|
|
|
|
@onready var sync_lb_btn := %SyncLeaderboardBtn as Button
|
|
|
|
|
@onready var reset_lb_btn := %ResetLBBtn as Button
|
|
|
|
|
|
|
|
|
|
# -- Data --
|
|
|
|
|
var all_users: Array = []
|
|
|
|
|
var lb_data: Array = []
|
|
|
|
|
var _user_root: TreeItem
|
|
|
|
|
var _lb_root: TreeItem
|
|
|
|
|
|
|
|
|
|
# -- Colors --
|
|
|
|
|
const CLR_BG := Color(0.1, 0.1, 0.12)
|
|
|
|
|
const CLR_HEADER := Color(0.85, 0.55, 0.2)
|
|
|
|
|
const CLR_TITLE := Color(0.95, 0.35, 0.35)
|
|
|
|
|
const CLR_TEXT := Color(0.82, 0.82, 0.82)
|
|
|
|
|
const CLR_DIM := Color(0.5, 0.5, 0.5)
|
|
|
|
|
const CLR_BANNED := Color(1.0, 0.3, 0.3)
|
|
|
|
|
const CLR_ADMIN := Color(0.4, 0.75, 1.0)
|
|
|
|
|
const CLR_MOD := Color(0.5, 0.9, 0.4)
|
|
|
|
|
const CLR_BTN_BAN := Color(1.0, 0.6, 0.2)
|
|
|
|
|
const CLR_BTN_UNBAN := Color(0.4, 0.8, 0.4)
|
|
|
|
|
const CLR_BTN_DEL := Color(1.0, 0.3, 0.3)
|
|
|
|
|
const CLR_STATUS_OK := Color(0.4, 0.9, 0.4)
|
|
|
|
|
const CLR_STATUS_ERR := Color(1.0, 0.3, 0.3)
|
|
|
|
|
|
|
|
|
|
func _ready() -> void:
|
|
|
|
|
_connect_signals()
|
|
|
|
|
visible = false
|
|
|
|
|
_apply_plain_style()
|
|
|
|
|
_setup_columns()
|
|
|
|
|
_connect_signals()
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# Plain style (overrides any inherited theme)
|
|
|
|
|
# =============================================================================
|
|
|
|
|
func _apply_plain_style() -> void:
|
|
|
|
|
title_label.add_theme_color_override("font_color", CLR_TITLE)
|
|
|
|
|
title_label.add_theme_font_size_override("font_size", 22)
|
|
|
|
|
count_label.add_theme_color_override("font_color", CLR_HEADER)
|
|
|
|
|
status_label.add_theme_color_override("font_color", CLR_DIM)
|
|
|
|
|
|
|
|
|
|
for btn: Button in [refresh_btn, close_btn, select_all_btn, deselect_btn]:
|
|
|
|
|
_style_button(btn, Color(0.2, 0.2, 0.24), CLR_TEXT)
|
|
|
|
|
_style_button(ban_btn, Color(0.3, 0.2, 0.1), CLR_BTN_BAN)
|
|
|
|
|
_style_button(unban_btn, Color(0.1, 0.22, 0.1), CLR_BTN_UNBAN)
|
|
|
|
|
_style_button(delete_btn, Color(0.28, 0.1, 0.1), CLR_BTN_DEL)
|
|
|
|
|
_style_button(sync_lb_btn, Color(0.2, 0.2, 0.3), CLR_ADMIN)
|
|
|
|
|
|
|
|
|
|
var tree_bg := StyleBoxFlat.new()
|
|
|
|
|
tree_bg.bg_color = Color(0.12, 0.12, 0.14)
|
|
|
|
|
tree_bg.set_corner_radius_all(4)
|
|
|
|
|
|
|
|
|
|
for t: Tree in [user_tree, lb_tree]:
|
|
|
|
|
t.add_theme_stylebox_override("panel", tree_bg)
|
|
|
|
|
t.add_theme_color_override("font_color", CLR_TEXT)
|
|
|
|
|
t.add_theme_color_override("title_button_color", CLR_HEADER)
|
|
|
|
|
|
|
|
|
|
func _style_button(btn: Button, bg_color: Color, text_color: Color) -> void:
|
|
|
|
|
var sb := StyleBoxFlat.new()
|
|
|
|
|
sb.bg_color = bg_color
|
|
|
|
|
sb.set_corner_radius_all(4)
|
|
|
|
|
sb.set_content_margin_all(6)
|
|
|
|
|
btn.add_theme_stylebox_override("normal", sb)
|
|
|
|
|
var sb_hover := sb.duplicate()
|
|
|
|
|
sb_hover.bg_color = bg_color.lightened(0.15)
|
|
|
|
|
btn.add_theme_stylebox_override("hover", sb_hover)
|
|
|
|
|
btn.add_theme_color_override("font_color", text_color)
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# Columns Setup
|
|
|
|
|
# =============================================================================
|
|
|
|
|
func _setup_columns() -> void:
|
|
|
|
|
# Users
|
|
|
|
|
user_tree.set_column_title(0, "☑")
|
|
|
|
|
user_tree.set_column_title(1, "User ID")
|
|
|
|
|
user_tree.set_column_title(2, "Username")
|
|
|
|
|
user_tree.set_column_title(3, "Display Name")
|
|
|
|
|
user_tree.set_column_title(4, "Created")
|
|
|
|
|
user_tree.set_column_title(5, "Edit")
|
|
|
|
|
user_tree.set_column_custom_minimum_width(0, 40)
|
|
|
|
|
user_tree.set_column_expand(0, false)
|
|
|
|
|
_user_root = user_tree.create_item()
|
|
|
|
|
|
|
|
|
|
# Leaderboards
|
|
|
|
|
lb_tree.set_column_title(0, "Rank")
|
|
|
|
|
lb_tree.set_column_title(1, "Player")
|
|
|
|
|
lb_tree.set_column_title(2, "High Score")
|
|
|
|
|
lb_tree.set_column_title(3, "Wins")
|
|
|
|
|
lb_tree.set_column_title(4, "Games")
|
|
|
|
|
lb_tree.set_column_title(5, "Edit")
|
|
|
|
|
lb_tree.set_column_expand(0, false)
|
|
|
|
|
lb_tree.set_column_custom_minimum_width(0, 60)
|
|
|
|
|
lb_tree.set_column_expand(5, false)
|
|
|
|
|
lb_tree.set_column_custom_minimum_width(5, 60)
|
|
|
|
|
_lb_root = lb_tree.create_item()
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
close_btn.pressed.connect(_on_close)
|
|
|
|
|
refresh_btn.pressed.connect(_on_refresh)
|
|
|
|
|
tabs.tab_changed.connect(_on_tab_changed)
|
|
|
|
|
|
|
|
|
|
# User actions
|
|
|
|
|
select_all_btn.pressed.connect(_select_all)
|
|
|
|
|
deselect_btn.pressed.connect(_deselect_all)
|
|
|
|
|
ban_btn.pressed.connect(_on_ban)
|
|
|
|
|
unban_btn.pressed.connect(_on_unban)
|
|
|
|
|
delete_btn.pressed.connect(_on_delete)
|
|
|
|
|
user_tree.item_edited.connect(_on_user_tree_item_edited)
|
|
|
|
|
user_tree.button_clicked.connect(_on_user_tree_button_clicked)
|
|
|
|
|
|
|
|
|
|
# LB actions
|
|
|
|
|
lb_tree.button_clicked.connect(_on_lb_tree_button_clicked)
|
|
|
|
|
sync_lb_btn.pressed.connect(_on_sync_leaderboard)
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# Core Panel Logic
|
|
|
|
|
# =============================================================================
|
|
|
|
|
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
|
|
|
|
|
_on_tab_changed(tabs.current_tab)
|
|
|
|
|
|
|
|
|
|
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:
|
|
|
|
|
func _on_close() -> void:
|
|
|
|
|
visible = false
|
|
|
|
|
emit_signal("closed")
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# RPC Helpers
|
|
|
|
|
# =============================================================================
|
|
|
|
|
func _on_refresh() -> void:
|
|
|
|
|
_on_tab_changed(tabs.current_tab)
|
|
|
|
|
|
|
|
|
|
func _rpc_call(rpc_name: String, payload: Dictionary) -> Dictionary:
|
|
|
|
|
func _on_tab_changed(tab_index: int) -> void:
|
|
|
|
|
_set_status("")
|
|
|
|
|
if tab_index == 0:
|
|
|
|
|
await _load_users()
|
|
|
|
|
else:
|
|
|
|
|
await _load_leaderboard()
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# RPC Helper
|
|
|
|
|
# =============================================================================
|
|
|
|
|
func _rpc(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)
|
|
|
|
|
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}
|
|
|
|
|
|
|
|
|
|
var err: String = result.get_exception().message
|
|
|
|
|
_set_status(err, CLR_STATUS_ERR)
|
|
|
|
|
return {"error": err}
|
|
|
|
|
if result.payload:
|
|
|
|
|
return JSON.parse_string(result.payload)
|
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# Data Refresh
|
|
|
|
|
# =============================================================================
|
|
|
|
|
func _set_status(msg: String, color: Color = CLR_DIM) -> void:
|
|
|
|
|
status_label.text = msg
|
|
|
|
|
status_label.add_theme_color_override("font_color", color)
|
|
|
|
|
|
|
|
|
|
func _refresh_all_data() -> void:
|
|
|
|
|
await _refresh_player_list()
|
|
|
|
|
await _refresh_server_stats()
|
|
|
|
|
if is_admin:
|
|
|
|
|
await _refresh_ban_list()
|
|
|
|
|
status_label.text = ""
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# TAB 1: USER MANAGEMENT
|
|
|
|
|
# =============================================================================
|
|
|
|
|
func _load_users() -> void:
|
|
|
|
|
_clear_tree(user_tree, _user_root)
|
|
|
|
|
_set_status("Loading users...")
|
|
|
|
|
|
|
|
|
|
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 res := await _rpc("admin_list_users", {})
|
|
|
|
|
if res.has("error"):
|
|
|
|
|
_set_status("Failed: " + str(res.error), CLR_STATUS_ERR)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
all_users = res.get("users", [])
|
|
|
|
|
count_label.text = "%d users" % all_users.size()
|
|
|
|
|
|
|
|
|
|
for user in all_users:
|
|
|
|
|
var uid: String = user.get("user_id", "")
|
|
|
|
|
var uname: String = user.get("username", "")
|
|
|
|
|
var dname: String = user.get("display_name", uname)
|
|
|
|
|
var role: String = user.get("role", "player")
|
|
|
|
|
var banned: bool = user.get("banned", false)
|
|
|
|
|
var created: String = str(user.get("create_time", ""))
|
|
|
|
|
|
|
|
|
|
var item := _user_root.create_child()
|
|
|
|
|
item.set_cell_mode(0, TreeItem.CELL_MODE_CHECK)
|
|
|
|
|
item.set_editable(0, true)
|
|
|
|
|
|
|
|
|
|
var display := name
|
|
|
|
|
if is_player_host:
|
|
|
|
|
display += " (Host)"
|
|
|
|
|
var short_id := uid.substr(0, 8) + "..."
|
|
|
|
|
item.set_text(1, short_id)
|
|
|
|
|
item.set_tooltip_text(1, uid)
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
var uname_display := uname
|
|
|
|
|
if role != "player": uname_display += " [%s]" % role.to_upper()
|
|
|
|
|
if banned: uname_display += " (BANNED)"
|
|
|
|
|
item.set_text(2, uname_display)
|
|
|
|
|
item.set_text(3, dname)
|
|
|
|
|
item.set_text(4, created.substr(0, 10) if created.length() > 10 else created)
|
|
|
|
|
item.add_button(5, _get_edit_icon(), 0, false, "Edit User")
|
|
|
|
|
|
|
|
|
|
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 + ")"
|
|
|
|
|
if banned: item.set_custom_color(2, CLR_BANNED)
|
|
|
|
|
elif role in ["admin", "owner"]: item.set_custom_color(2, CLR_ADMIN)
|
|
|
|
|
|
|
|
|
|
ban_list.add_item(display)
|
|
|
|
|
ban_list.set_item_metadata(ban_list.item_count - 1, ban)
|
|
|
|
|
item.set_metadata(0, user)
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# Player Actions
|
|
|
|
|
# =============================================================================
|
|
|
|
|
_update_selection_count()
|
|
|
|
|
_set_status("")
|
|
|
|
|
|
|
|
|
|
func _on_player_selected(_index: int) -> void:
|
|
|
|
|
_update_action_buttons()
|
|
|
|
|
func _on_user_tree_item_edited() -> void:
|
|
|
|
|
_update_selection_count()
|
|
|
|
|
|
|
|
|
|
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 _update_selection_count() -> void:
|
|
|
|
|
var count := _get_checked_items().size()
|
|
|
|
|
selected_label.text = "%d selected" % count
|
|
|
|
|
ban_btn.disabled = count == 0
|
|
|
|
|
unban_btn.disabled = count == 0
|
|
|
|
|
delete_btn.disabled = count == 0
|
|
|
|
|
|
|
|
|
|
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_user_tree_button_clicked(item: TreeItem, _col: int, _id: int, _mouse: int) -> void:
|
|
|
|
|
_show_edit_user_dialog(item.get_metadata(0))
|
|
|
|
|
|
|
|
|
|
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 _show_edit_user_dialog(user: Dictionary) -> void:
|
|
|
|
|
var uid: String = user.get("user_id", "")
|
|
|
|
|
var uname: String = user.get("username", "")
|
|
|
|
|
var role: String = user.get("role", "player")
|
|
|
|
|
var banned: bool = user.get("banned", false)
|
|
|
|
|
|
|
|
|
|
func _create_ban_dialog(user_id: String, player_name: String) -> ConfirmationDialog:
|
|
|
|
|
var dialog := ConfirmationDialog.new()
|
|
|
|
|
dialog.title = "Ban Player"
|
|
|
|
|
|
|
|
|
|
var dialog := AcceptDialog.new()
|
|
|
|
|
dialog.title = "Edit User: " + uname
|
|
|
|
|
dialog.min_size = Vector2i(380, 260)
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
vbox.add_theme_constant_override("separation", 8)
|
|
|
|
|
|
|
|
|
|
var id_lbl := Label.new()
|
|
|
|
|
id_lbl.text = "User ID: " + uid
|
|
|
|
|
id_lbl.add_theme_color_override("font_color", CLR_DIM)
|
|
|
|
|
vbox.add_child(id_lbl)
|
|
|
|
|
|
|
|
|
|
var role_hbox := HBoxContainer.new()
|
|
|
|
|
var role_lbl := Label.new()
|
|
|
|
|
role_lbl.text = "Role: "
|
|
|
|
|
role_hbox.add_child(role_lbl)
|
|
|
|
|
var role_option := OptionButton.new()
|
|
|
|
|
var roles := ["player", "moderator", "admin"]
|
|
|
|
|
for r in roles: role_option.add_item(r)
|
|
|
|
|
role_option.selected = roles.find(role)
|
|
|
|
|
role_hbox.add_child(role_option)
|
|
|
|
|
vbox.add_child(role_hbox)
|
|
|
|
|
|
|
|
|
|
var ban_check := CheckButton.new()
|
|
|
|
|
ban_check.text = "Banned"
|
|
|
|
|
ban_check.button_pressed = banned
|
|
|
|
|
vbox.add_child(ban_check)
|
|
|
|
|
|
|
|
|
|
var reason_input := LineEdit.new()
|
|
|
|
|
reason_input.placeholder_text = "Enter ban reason"
|
|
|
|
|
reason_input.placeholder_text = "Ban reason..."
|
|
|
|
|
reason_input.text = user.get("ban_reason", "")
|
|
|
|
|
reason_input.visible = banned
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
ban_check.toggled.connect(func(on): reason_input.visible = on)
|
|
|
|
|
|
|
|
|
|
var save_btn := Button.new()
|
|
|
|
|
save_btn.text = "Save Changes"
|
|
|
|
|
save_btn.custom_minimum_size.y = 36
|
|
|
|
|
vbox.add_child(save_btn)
|
|
|
|
|
|
|
|
|
|
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"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
save_btn.pressed.connect(func():
|
|
|
|
|
var new_role: String = roles[role_option.selected]
|
|
|
|
|
await _save_user_edit(uid, uname, new_role, ban_check.button_pressed, reason_input.text)
|
|
|
|
|
dialog.queue_free()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
dialog.canceled.connect(func(): dialog.queue_free())
|
|
|
|
|
|
|
|
|
|
func _on_restart_match_pressed() -> void:
|
|
|
|
|
func _save_user_edit(uid: String, uname: String, new_role: String, new_banned: bool, reason: String) -> void:
|
|
|
|
|
_set_status("Saving...")
|
|
|
|
|
await _rpc("admin_set_user_role", {"user_id": uid, "role": new_role})
|
|
|
|
|
if new_banned:
|
|
|
|
|
await _rpc("admin_ban_player", {"user_id": uid, "reason": reason, "duration_hours": 0})
|
|
|
|
|
else:
|
|
|
|
|
await _rpc("admin_unban_player", {"user_id": uid})
|
|
|
|
|
_set_status("Saved: " + uname, CLR_STATUS_OK)
|
|
|
|
|
await _load_users()
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# TAB 1: USER SELECTION BATCH ACTIONS
|
|
|
|
|
# =============================================================================
|
|
|
|
|
func _select_all() -> void:
|
|
|
|
|
if not _user_root: return
|
|
|
|
|
var item := _user_root.get_first_child()
|
|
|
|
|
while item:
|
|
|
|
|
item.set_checked(0, true)
|
|
|
|
|
item = item.get_next()
|
|
|
|
|
_update_selection_count()
|
|
|
|
|
|
|
|
|
|
func _deselect_all() -> void:
|
|
|
|
|
if not _user_root: return
|
|
|
|
|
var item := _user_root.get_first_child()
|
|
|
|
|
while item:
|
|
|
|
|
item.set_checked(0, false)
|
|
|
|
|
item = item.get_next()
|
|
|
|
|
_update_selection_count()
|
|
|
|
|
|
|
|
|
|
func _get_checked_items() -> Array[TreeItem]:
|
|
|
|
|
var items: Array[TreeItem] = []
|
|
|
|
|
if not _user_root: return items
|
|
|
|
|
var item := _user_root.get_first_child()
|
|
|
|
|
while item:
|
|
|
|
|
if item.is_checked(0): items.append(item)
|
|
|
|
|
item = item.get_next()
|
|
|
|
|
return items
|
|
|
|
|
|
|
|
|
|
func _get_checked_user_data() -> Array:
|
|
|
|
|
var data: Array = []
|
|
|
|
|
for item in _get_checked_items():
|
|
|
|
|
data.append(item.get_metadata(0))
|
|
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
func _on_ban() -> void:
|
|
|
|
|
var users := _get_checked_user_data()
|
|
|
|
|
if users.is_empty(): return
|
|
|
|
|
var to_ban: Array = []
|
|
|
|
|
for u in users:
|
|
|
|
|
if u.get("banned", false): continue
|
|
|
|
|
if u.get("role", "") in ["admin", "owner"]: continue
|
|
|
|
|
to_ban.append(u)
|
|
|
|
|
if to_ban.is_empty():
|
|
|
|
|
_set_status("No eligible users to ban", CLR_STATUS_ERR)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
var dialog := ConfirmationDialog.new()
|
|
|
|
|
dialog.dialog_text = "Restart the current match?"
|
|
|
|
|
dialog.title = "Ban %d user(s)" % to_ban.size()
|
|
|
|
|
var vbox := VBoxContainer.new()
|
|
|
|
|
vbox.add_theme_constant_override("separation", 8)
|
|
|
|
|
var list_names: Array = []
|
|
|
|
|
for i in range(min(to_ban.size(), 10)): list_names.append(to_ban[i].get("username", ""))
|
|
|
|
|
var name_str := ", ".join(list_names)
|
|
|
|
|
if to_ban.size() > 10: name_str += "... and %d others" % (to_ban.size() - 10)
|
|
|
|
|
var lbl := Label.new()
|
|
|
|
|
lbl.text = "Ban following users:\n" + name_str
|
|
|
|
|
lbl.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
|
|
|
lbl.custom_minimum_size = Vector2(350, 0)
|
|
|
|
|
vbox.add_child(lbl)
|
|
|
|
|
var reason_input := LineEdit.new()
|
|
|
|
|
reason_input.placeholder_text = "Ban reason"
|
|
|
|
|
vbox.add_child(reason_input)
|
|
|
|
|
var dur := SpinBox.new()
|
|
|
|
|
dur.min_value = 0; dur.max_value = 8760; dur.value = 24; dur.prefix = "Hours: "
|
|
|
|
|
vbox.add_child(dur)
|
|
|
|
|
dialog.add_child(vbox)
|
|
|
|
|
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()
|
|
|
|
|
var ok := 0
|
|
|
|
|
for u in to_ban:
|
|
|
|
|
var r := await _rpc("admin_ban_player", {"user_id": u.user_id, "reason": reason_input.text, "duration_hours": int(dur.value)})
|
|
|
|
|
if r.get("success", false): ok += 1
|
|
|
|
|
_set_status("Banned %d/%d" % [ok, to_ban.size()], CLR_STATUS_OK)
|
|
|
|
|
await _load_users()
|
|
|
|
|
dialog.queue_free()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func _on_unban() -> void:
|
|
|
|
|
var users := _get_checked_user_data()
|
|
|
|
|
var to_unban := []; for u in users: if u.get("banned", false): to_unban.append(u)
|
|
|
|
|
if to_unban.is_empty(): return
|
|
|
|
|
var ok := 0
|
|
|
|
|
for u in to_unban:
|
|
|
|
|
var r := await _rpc("admin_unban_player", {"user_id": u.user_id})
|
|
|
|
|
if r.get("success", false): ok += 1
|
|
|
|
|
_set_status("Unbanned %d/%d" % [ok, to_unban.size()], CLR_STATUS_OK)
|
|
|
|
|
await _load_users()
|
|
|
|
|
|
|
|
|
|
func _on_delete() -> void:
|
|
|
|
|
var users := _get_checked_user_data()
|
|
|
|
|
if users.is_empty(): return
|
|
|
|
|
var ids := []; var names := []
|
|
|
|
|
for u in users:
|
|
|
|
|
if u.get("role", "") in ["admin", "owner"]: continue
|
|
|
|
|
ids.append(u.user_id); names.append(u.username)
|
|
|
|
|
if ids.is_empty(): return
|
|
|
|
|
var dialog := ConfirmationDialog.new()
|
|
|
|
|
dialog.title = "DELETE %d account(s)" % ids.size()
|
|
|
|
|
var vbox := VBoxContainer.new(); vbox.add_theme_constant_override("separation", 12)
|
|
|
|
|
var list_names = []; for i in range(min(names.size(), 10)): list_names.append(names[i])
|
|
|
|
|
var name_str = ", ".join(list_names); if names.size() > 10: name_str += "... and %d others" % (names.size() - 10)
|
|
|
|
|
var text_lbl = Label.new(); text_lbl.text = "PERMANENTLY DELETE:\n" + name_str + "\n\nThis CANNOT be undone!"; text_lbl.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART; text_lbl.custom_minimum_size = Vector2(400, 0)
|
|
|
|
|
vbox.add_child(text_lbl); dialog.add_child(vbox); add_child(dialog); dialog.popup_centered()
|
|
|
|
|
dialog.confirmed.connect(func():
|
|
|
|
|
var r := await _rpc("admin_delete_users", {"user_ids": ids})
|
|
|
|
|
_set_status("Deleted %d accounts" % r.get("deleted", []).size(), CLR_STATUS_OK)
|
|
|
|
|
await _load_users()
|
|
|
|
|
dialog.queue_free()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
dialog.canceled.connect(func(): dialog.queue_free())
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# Ban List Management
|
|
|
|
|
# TAB 2: LEADERBOARD MANAGEMENT
|
|
|
|
|
# =============================================================================
|
|
|
|
|
func _load_leaderboard() -> void:
|
|
|
|
|
_clear_tree(lb_tree, _lb_root)
|
|
|
|
|
_set_status("Loading leaderboards...")
|
|
|
|
|
|
|
|
|
|
var res := await _rpc("get_leaderboard_stats", {})
|
|
|
|
|
if res.has("error"):
|
|
|
|
|
_set_status("Failed to load scores", CLR_STATUS_ERR)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
lb_data = res.get("leaderboard", [])
|
|
|
|
|
count_label.text = "%d records" % lb_data.size()
|
|
|
|
|
lb_data.sort_custom(func(a, b): return a.get("high_score", 0) > b.get("high_score", 0))
|
|
|
|
|
|
|
|
|
|
var rank = 1
|
|
|
|
|
for entry in lb_data:
|
|
|
|
|
var item := _lb_root.create_child()
|
|
|
|
|
item.set_text(0, "#%d" % rank)
|
|
|
|
|
item.set_text(1, entry.get("display_name", "Unknown"))
|
|
|
|
|
item.set_text(2, str(entry.get("high_score", 0)))
|
|
|
|
|
item.set_text(3, str(entry.get("games_won", 0)))
|
|
|
|
|
item.set_text(4, str(entry.get("games_played", 0)))
|
|
|
|
|
item.add_button(5, _get_edit_icon(), 0, false, "Edit Score")
|
|
|
|
|
item.set_metadata(0, entry)
|
|
|
|
|
rank += 1
|
|
|
|
|
_set_status("")
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
func _on_lb_tree_button_clicked(item: TreeItem, _col: int, _id: int, _mouse: int) -> void:
|
|
|
|
|
_show_edit_score_dialog(item.get_metadata(0))
|
|
|
|
|
|
|
|
|
|
func _show_edit_score_dialog(entry: Dictionary) -> void:
|
|
|
|
|
var uid: String = entry.get("user_id", "")
|
|
|
|
|
var name: String = entry.get("display_name", "Unknown")
|
|
|
|
|
var dialog := AcceptDialog.new(); dialog.title = "Edit Score: " + name; dialog.min_size = Vector2i(350, 280)
|
|
|
|
|
var vbox := VBoxContainer.new(); vbox.add_theme_constant_override("separation", 10)
|
|
|
|
|
var info_lbl := Label.new(); info_lbl.text = "Manage player stats for: " + name; info_lbl.add_theme_color_override("font_color", CLR_HEADER); vbox.add_child(info_lbl)
|
|
|
|
|
var grid := GridContainer.new(); grid.columns = 2
|
|
|
|
|
var fields = {"high_score": SpinBox.new(), "games_won": SpinBox.new(), "games_played": SpinBox.new()}
|
|
|
|
|
for key in fields:
|
|
|
|
|
var lbl = Label.new()
|
|
|
|
|
lbl.text = key.capitalize() + ":"
|
|
|
|
|
grid.add_child(lbl)
|
|
|
|
|
var spin = fields[key] as SpinBox; spin.max_value = 1000000; spin.value = entry.get(key, 0); spin.custom_minimum_size.x = 120; grid.add_child(spin)
|
|
|
|
|
vbox.add_child(grid); var btn_vbox = VBoxContainer.new(); btn_vbox.add_theme_constant_override("separation", 5)
|
|
|
|
|
var save_btn = Button.new(); save_btn.text = "UPDATE SCORE"; save_btn.custom_minimum_size.y = 40; btn_vbox.add_child(save_btn)
|
|
|
|
|
var reset_btn = Button.new(); reset_btn.text = "RESET / DELETE RECORD"; reset_btn.add_theme_color_override("font_color", CLR_BTN_DEL); btn_vbox.add_child(reset_btn)
|
|
|
|
|
vbox.add_child(btn_vbox); dialog.add_child(vbox); add_child(dialog); dialog.popup_centered()
|
|
|
|
|
save_btn.pressed.connect(func():
|
|
|
|
|
var stats = {}; for k in fields: stats[k] = int(fields[k].value)
|
|
|
|
|
await _save_score_edit(uid, stats); dialog.queue_free()
|
|
|
|
|
)
|
|
|
|
|
reset_btn.pressed.connect(func(): _confirm_reset_score(uid, name); dialog.queue_free())
|
|
|
|
|
|
|
|
|
|
func _confirm_reset_score(uid: String, name: String) -> void:
|
|
|
|
|
var confirm := ConfirmationDialog.new(); confirm.title = "Reset Score?"; confirm.dialog_text = "Are you sure?"
|
|
|
|
|
add_child(confirm); confirm.popup_centered(); confirm.confirmed.connect(func():
|
|
|
|
|
var res := await _rpc("admin_delete_stats", {"user_id": uid})
|
|
|
|
|
if res.has("success"): await _load_leaderboard()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func _save_score_edit(uid: String, stats: Dictionary) -> void:
|
|
|
|
|
_set_status("Updating score...")
|
|
|
|
|
var res := await _rpc("admin_update_stats", {"user_id": uid, "stats": stats})
|
|
|
|
|
if res.has("success"): _set_status("Updated", CLR_STATUS_OK); await _load_leaderboard()
|
|
|
|
|
|
|
|
|
|
func _on_sync_leaderboard() -> void:
|
|
|
|
|
_set_status("Syncing all records to native leaderboard...", CLR_HEADER)
|
|
|
|
|
var res := await _rpc("admin_sync_leaderboard", {})
|
|
|
|
|
if res.has("success"):
|
|
|
|
|
var count = res.get("synced", 0)
|
|
|
|
|
_set_status("Successfully synced %d records!" % count, CLR_STATUS_OK)
|
|
|
|
|
await _load_leaderboard()
|
|
|
|
|
else:
|
|
|
|
|
status_label.text = "Failed to unban"
|
|
|
|
|
status_label.add_theme_color_override("font_color", Color.RED)
|
|
|
|
|
_set_status("Sync failed: " + str(res.get("error", "Unknown")), CLR_STATUS_ERR)
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# Input Handling
|
|
|
|
|
# Common Helpers
|
|
|
|
|
# =============================================================================
|
|
|
|
|
func _clear_tree(tree: Tree, root: TreeItem) -> void:
|
|
|
|
|
if root:
|
|
|
|
|
var child := root.get_first_child()
|
|
|
|
|
while child:
|
|
|
|
|
var next := child.get_next()
|
|
|
|
|
root.remove_child(child)
|
|
|
|
|
child = next
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
func _get_edit_icon() -> Texture2D:
|
|
|
|
|
var img := Image.create(16, 16, false, Image.FORMAT_RGBA8)
|
|
|
|
|
img.fill(Color(0, 0, 0, 0))
|
|
|
|
|
for i in range(4, 12):
|
|
|
|
|
img.set_pixel(i, 11 - i + 4, CLR_TEXT)
|
|
|
|
|
img.set_pixel(i, 12 - i + 4, CLR_TEXT)
|
|
|
|
|
return ImageTexture.create_from_image(img)
|
|
|
|
|