feat: take_powerup VFX, rank fix, admin chat management
- Wire take_powerup AnimatedSprite3D on powerup pickup via add_powerup_from_item() - Make take_powerup animation one-shot (loop: false) - Fix rank Position label hidden at game start (visible = false, only shows when score > 0) - Competition ranking for tied scores in main.gd - Lobby Chat admin tab: system prefix, max messages, wipe, purge old, save config - Chat Storage admin tab: list/browse/delete individual channel messages - Backend RPCs: admin_get_chat_config, admin_set_chat_config, admin_purge_old_messages, admin_list_channel_messages, admin_delete_channel_message - Chat config applied on lobby join (max_messages, prefix injection)
This commit is contained in:
@@ -0,0 +1,221 @@
|
|||||||
|
[gd_resource type="SpriteFrames" format=3 uid="uid://bq8ifua64lag2"]
|
||||||
|
|
||||||
|
[ext_resource type="Texture2D" uid="uid://cwmqu5wsing63" path="res://assets/graphics/vfx/take_powerup.png" id="1_j5q3j"]
|
||||||
|
|
||||||
|
[sub_resource type="AtlasTexture" id="AtlasTexture_fs20j"]
|
||||||
|
atlas = ExtResource("1_j5q3j")
|
||||||
|
region = Rect2(0, 0, 386, 386)
|
||||||
|
|
||||||
|
[sub_resource type="AtlasTexture" id="AtlasTexture_mgj3q"]
|
||||||
|
atlas = ExtResource("1_j5q3j")
|
||||||
|
region = Rect2(386, 0, 386, 386)
|
||||||
|
|
||||||
|
[sub_resource type="AtlasTexture" id="AtlasTexture_0cqc8"]
|
||||||
|
atlas = ExtResource("1_j5q3j")
|
||||||
|
region = Rect2(772, 0, 386, 386)
|
||||||
|
|
||||||
|
[sub_resource type="AtlasTexture" id="AtlasTexture_6lpcc"]
|
||||||
|
atlas = ExtResource("1_j5q3j")
|
||||||
|
region = Rect2(1158, 0, 386, 386)
|
||||||
|
|
||||||
|
[sub_resource type="AtlasTexture" id="AtlasTexture_sufku"]
|
||||||
|
atlas = ExtResource("1_j5q3j")
|
||||||
|
region = Rect2(1544, 0, 386, 386)
|
||||||
|
|
||||||
|
[sub_resource type="AtlasTexture" id="AtlasTexture_m7myb"]
|
||||||
|
atlas = ExtResource("1_j5q3j")
|
||||||
|
region = Rect2(1930, 0, 386, 386)
|
||||||
|
|
||||||
|
[sub_resource type="AtlasTexture" id="AtlasTexture_85pl8"]
|
||||||
|
atlas = ExtResource("1_j5q3j")
|
||||||
|
region = Rect2(0, 386, 386, 386)
|
||||||
|
|
||||||
|
[sub_resource type="AtlasTexture" id="AtlasTexture_tum65"]
|
||||||
|
atlas = ExtResource("1_j5q3j")
|
||||||
|
region = Rect2(386, 386, 386, 386)
|
||||||
|
|
||||||
|
[sub_resource type="AtlasTexture" id="AtlasTexture_v7us7"]
|
||||||
|
atlas = ExtResource("1_j5q3j")
|
||||||
|
region = Rect2(772, 386, 386, 386)
|
||||||
|
|
||||||
|
[sub_resource type="AtlasTexture" id="AtlasTexture_y04gx"]
|
||||||
|
atlas = ExtResource("1_j5q3j")
|
||||||
|
region = Rect2(1158, 386, 386, 386)
|
||||||
|
|
||||||
|
[sub_resource type="AtlasTexture" id="AtlasTexture_uwml5"]
|
||||||
|
atlas = ExtResource("1_j5q3j")
|
||||||
|
region = Rect2(1544, 386, 386, 386)
|
||||||
|
|
||||||
|
[sub_resource type="AtlasTexture" id="AtlasTexture_ltsqf"]
|
||||||
|
atlas = ExtResource("1_j5q3j")
|
||||||
|
region = Rect2(1930, 386, 386, 386)
|
||||||
|
|
||||||
|
[sub_resource type="AtlasTexture" id="AtlasTexture_4st5b"]
|
||||||
|
atlas = ExtResource("1_j5q3j")
|
||||||
|
region = Rect2(0, 772, 386, 386)
|
||||||
|
|
||||||
|
[sub_resource type="AtlasTexture" id="AtlasTexture_vnofb"]
|
||||||
|
atlas = ExtResource("1_j5q3j")
|
||||||
|
region = Rect2(386, 772, 386, 386)
|
||||||
|
|
||||||
|
[sub_resource type="AtlasTexture" id="AtlasTexture_fculp"]
|
||||||
|
atlas = ExtResource("1_j5q3j")
|
||||||
|
region = Rect2(772, 772, 386, 386)
|
||||||
|
|
||||||
|
[sub_resource type="AtlasTexture" id="AtlasTexture_n4swe"]
|
||||||
|
atlas = ExtResource("1_j5q3j")
|
||||||
|
region = Rect2(1158, 772, 386, 386)
|
||||||
|
|
||||||
|
[sub_resource type="AtlasTexture" id="AtlasTexture_7kcav"]
|
||||||
|
atlas = ExtResource("1_j5q3j")
|
||||||
|
region = Rect2(1544, 772, 386, 386)
|
||||||
|
|
||||||
|
[sub_resource type="AtlasTexture" id="AtlasTexture_6b6x4"]
|
||||||
|
atlas = ExtResource("1_j5q3j")
|
||||||
|
region = Rect2(1930, 772, 386, 386)
|
||||||
|
|
||||||
|
[sub_resource type="AtlasTexture" id="AtlasTexture_sx3tb"]
|
||||||
|
atlas = ExtResource("1_j5q3j")
|
||||||
|
region = Rect2(0, 1158, 386, 386)
|
||||||
|
|
||||||
|
[sub_resource type="AtlasTexture" id="AtlasTexture_vcvbu"]
|
||||||
|
atlas = ExtResource("1_j5q3j")
|
||||||
|
region = Rect2(386, 1158, 386, 386)
|
||||||
|
|
||||||
|
[sub_resource type="AtlasTexture" id="AtlasTexture_h7b8t"]
|
||||||
|
atlas = ExtResource("1_j5q3j")
|
||||||
|
region = Rect2(772, 1158, 386, 386)
|
||||||
|
|
||||||
|
[sub_resource type="AtlasTexture" id="AtlasTexture_g0810"]
|
||||||
|
atlas = ExtResource("1_j5q3j")
|
||||||
|
region = Rect2(1158, 1158, 386, 386)
|
||||||
|
|
||||||
|
[sub_resource type="AtlasTexture" id="AtlasTexture_3t50e"]
|
||||||
|
atlas = ExtResource("1_j5q3j")
|
||||||
|
region = Rect2(1544, 1158, 386, 386)
|
||||||
|
|
||||||
|
[sub_resource type="AtlasTexture" id="AtlasTexture_yk8ef"]
|
||||||
|
atlas = ExtResource("1_j5q3j")
|
||||||
|
region = Rect2(1930, 1158, 386, 386)
|
||||||
|
|
||||||
|
[sub_resource type="AtlasTexture" id="AtlasTexture_5m1o2"]
|
||||||
|
atlas = ExtResource("1_j5q3j")
|
||||||
|
region = Rect2(0, 1544, 386, 386)
|
||||||
|
|
||||||
|
[sub_resource type="AtlasTexture" id="AtlasTexture_xp42j"]
|
||||||
|
atlas = ExtResource("1_j5q3j")
|
||||||
|
region = Rect2(386, 1544, 386, 386)
|
||||||
|
|
||||||
|
[sub_resource type="AtlasTexture" id="AtlasTexture_3ai78"]
|
||||||
|
atlas = ExtResource("1_j5q3j")
|
||||||
|
region = Rect2(772, 1544, 386, 386)
|
||||||
|
|
||||||
|
[sub_resource type="AtlasTexture" id="AtlasTexture_xsn24"]
|
||||||
|
atlas = ExtResource("1_j5q3j")
|
||||||
|
region = Rect2(1158, 1544, 386, 386)
|
||||||
|
|
||||||
|
[sub_resource type="AtlasTexture" id="AtlasTexture_eprgv"]
|
||||||
|
atlas = ExtResource("1_j5q3j")
|
||||||
|
region = Rect2(1544, 1544, 386, 386)
|
||||||
|
|
||||||
|
[sub_resource type="AtlasTexture" id="AtlasTexture_uc241"]
|
||||||
|
atlas = ExtResource("1_j5q3j")
|
||||||
|
region = Rect2(1930, 1544, 386, 386)
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
animations = [{
|
||||||
|
"frames": [{
|
||||||
|
"duration": 1.0,
|
||||||
|
"texture": SubResource("AtlasTexture_fs20j")
|
||||||
|
}, {
|
||||||
|
"duration": 1.0,
|
||||||
|
"texture": SubResource("AtlasTexture_mgj3q")
|
||||||
|
}, {
|
||||||
|
"duration": 1.0,
|
||||||
|
"texture": SubResource("AtlasTexture_0cqc8")
|
||||||
|
}, {
|
||||||
|
"duration": 1.0,
|
||||||
|
"texture": SubResource("AtlasTexture_6lpcc")
|
||||||
|
}, {
|
||||||
|
"duration": 1.0,
|
||||||
|
"texture": SubResource("AtlasTexture_sufku")
|
||||||
|
}, {
|
||||||
|
"duration": 1.0,
|
||||||
|
"texture": SubResource("AtlasTexture_m7myb")
|
||||||
|
}, {
|
||||||
|
"duration": 1.0,
|
||||||
|
"texture": SubResource("AtlasTexture_85pl8")
|
||||||
|
}, {
|
||||||
|
"duration": 1.0,
|
||||||
|
"texture": SubResource("AtlasTexture_tum65")
|
||||||
|
}, {
|
||||||
|
"duration": 1.0,
|
||||||
|
"texture": SubResource("AtlasTexture_v7us7")
|
||||||
|
}, {
|
||||||
|
"duration": 1.0,
|
||||||
|
"texture": SubResource("AtlasTexture_y04gx")
|
||||||
|
}, {
|
||||||
|
"duration": 1.0,
|
||||||
|
"texture": SubResource("AtlasTexture_uwml5")
|
||||||
|
}, {
|
||||||
|
"duration": 1.0,
|
||||||
|
"texture": SubResource("AtlasTexture_ltsqf")
|
||||||
|
}, {
|
||||||
|
"duration": 1.0,
|
||||||
|
"texture": SubResource("AtlasTexture_4st5b")
|
||||||
|
}, {
|
||||||
|
"duration": 1.0,
|
||||||
|
"texture": SubResource("AtlasTexture_vnofb")
|
||||||
|
}, {
|
||||||
|
"duration": 1.0,
|
||||||
|
"texture": SubResource("AtlasTexture_fculp")
|
||||||
|
}, {
|
||||||
|
"duration": 1.0,
|
||||||
|
"texture": SubResource("AtlasTexture_n4swe")
|
||||||
|
}, {
|
||||||
|
"duration": 1.0,
|
||||||
|
"texture": SubResource("AtlasTexture_7kcav")
|
||||||
|
}, {
|
||||||
|
"duration": 1.0,
|
||||||
|
"texture": SubResource("AtlasTexture_6b6x4")
|
||||||
|
}, {
|
||||||
|
"duration": 1.0,
|
||||||
|
"texture": SubResource("AtlasTexture_sx3tb")
|
||||||
|
}, {
|
||||||
|
"duration": 1.0,
|
||||||
|
"texture": SubResource("AtlasTexture_vcvbu")
|
||||||
|
}, {
|
||||||
|
"duration": 1.0,
|
||||||
|
"texture": SubResource("AtlasTexture_h7b8t")
|
||||||
|
}, {
|
||||||
|
"duration": 1.0,
|
||||||
|
"texture": SubResource("AtlasTexture_g0810")
|
||||||
|
}, {
|
||||||
|
"duration": 1.0,
|
||||||
|
"texture": SubResource("AtlasTexture_3t50e")
|
||||||
|
}, {
|
||||||
|
"duration": 1.0,
|
||||||
|
"texture": SubResource("AtlasTexture_yk8ef")
|
||||||
|
}, {
|
||||||
|
"duration": 1.0,
|
||||||
|
"texture": SubResource("AtlasTexture_5m1o2")
|
||||||
|
}, {
|
||||||
|
"duration": 1.0,
|
||||||
|
"texture": SubResource("AtlasTexture_xp42j")
|
||||||
|
}, {
|
||||||
|
"duration": 1.0,
|
||||||
|
"texture": SubResource("AtlasTexture_3ai78")
|
||||||
|
}, {
|
||||||
|
"duration": 1.0,
|
||||||
|
"texture": SubResource("AtlasTexture_xsn24")
|
||||||
|
}, {
|
||||||
|
"duration": 1.0,
|
||||||
|
"texture": SubResource("AtlasTexture_eprgv")
|
||||||
|
}, {
|
||||||
|
"duration": 1.0,
|
||||||
|
"texture": SubResource("AtlasTexture_uc241")
|
||||||
|
}],
|
||||||
|
"loop": false,
|
||||||
|
"name": &"take_powerup",
|
||||||
|
"speed": 15.0
|
||||||
|
}]
|
||||||
@@ -556,3 +556,28 @@ func _apply_loadout_character() -> void:
|
|||||||
if idx != -1:
|
if idx != -1:
|
||||||
LobbyManager.local_character_index = idx
|
LobbyManager.local_character_index = idx
|
||||||
print("[Lobby] Loadout character applied: ", saved_char)
|
print("[Lobby] Loadout character applied: ", saved_char)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Admin Chat Actions (called from Admin Panel)
|
||||||
|
# =============================================================================
|
||||||
|
func admin_wipe_chat() -> void:
|
||||||
|
"""Wipe the entire global lobby chat. Called by admin panel."""
|
||||||
|
if not chat or not chat._chat_channel:
|
||||||
|
push_warning("[Lobby] admin_wipe_chat: chat not connected.")
|
||||||
|
return
|
||||||
|
var payload = JSON.stringify({"channel_id": chat._chat_channel.id})
|
||||||
|
var result = await BackendService.admin_clear_global_chat(payload)
|
||||||
|
if result.get("success", false):
|
||||||
|
chat._chat_messages.clear()
|
||||||
|
chat._refresh_chat_display()
|
||||||
|
chat._inject_local_message("[SYSTEM] : Global chat cleared by admin.")
|
||||||
|
else:
|
||||||
|
push_warning("[Lobby] admin_wipe_chat failed: " + str(result.get("message", "")))
|
||||||
|
|
||||||
|
func admin_purge_chat(max_age_days: int) -> int:
|
||||||
|
"""Purge messages older than max_age_days. Returns count deleted. Called by admin panel."""
|
||||||
|
if not chat or not chat._chat_channel:
|
||||||
|
push_warning("[Lobby] admin_purge_chat: chat not connected.")
|
||||||
|
return 0
|
||||||
|
var result = await BackendService.admin_purge_old_messages(chat._chat_channel.id, max_age_days)
|
||||||
|
return result.get("deleted", 0)
|
||||||
|
|||||||
+12
-2
@@ -2262,12 +2262,22 @@ func _on_leaderboard_updated(sorted_scores: Array):
|
|||||||
else:
|
else:
|
||||||
sorted_players.sort_custom(func(a, b): return a.score > b.score)
|
sorted_players.sort_custom(func(a, b): return a.score > b.score)
|
||||||
|
|
||||||
# Assign rank
|
# Assign rank. Players sharing a score share a rank (standard competition
|
||||||
|
# ranking), and zero-score players get no rank at all — this prevents the
|
||||||
|
# match from starting with everyone displaying a position.
|
||||||
|
var prev_score = null
|
||||||
|
var prev_rank = 0
|
||||||
for i in range(sorted_players.size()):
|
for i in range(sorted_players.size()):
|
||||||
var p_node = sorted_players[i].node
|
var p_node = sorted_players[i].node
|
||||||
|
var p_score = sorted_players[i].score
|
||||||
var rank = i + 1
|
var rank = i + 1
|
||||||
|
# Tie: reuse the rank of the player above with the same score.
|
||||||
|
if prev_score != null and p_score == prev_score:
|
||||||
|
rank = prev_rank
|
||||||
|
prev_score = p_score
|
||||||
|
prev_rank = rank
|
||||||
if p_node.has_method("update_rank_visuals"):
|
if p_node.has_method("update_rank_visuals"):
|
||||||
p_node.update_rank_visuals(rank)
|
p_node.update_rank_visuals(rank, p_score)
|
||||||
|
|
||||||
func _on_global_timer_updated(time_remaining: float):
|
func _on_global_timer_updated(time_remaining: float):
|
||||||
"""Update the global match timer display."""
|
"""Update the global match timer display."""
|
||||||
|
|||||||
+7
-1
@@ -928,11 +928,17 @@ func _refresh_player_visuals():
|
|||||||
if active_character:
|
if active_character:
|
||||||
apply_loadout(active_character)
|
apply_loadout(active_character)
|
||||||
|
|
||||||
func update_rank_visuals(rank: int):
|
func update_rank_visuals(rank: int, score: int = -1):
|
||||||
var pos_label = get_node_or_null("Position")
|
var pos_label = get_node_or_null("Position")
|
||||||
if not pos_label:
|
if not pos_label:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Hide rank until the player has actually scored, so the match doesn't
|
||||||
|
# start with everyone showing a position (e.g. all "1st").
|
||||||
|
if score == 0:
|
||||||
|
pos_label.visible = false
|
||||||
|
return
|
||||||
|
|
||||||
if rank <= 4:
|
if rank <= 4:
|
||||||
pos_label.visible = true
|
pos_label.visible = true
|
||||||
if race_manager:
|
if race_manager:
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
[ext_resource type="AnimationLibrary" path="res://assets/characters/animations/animation-pack.res" id="6_5oq5w"]
|
[ext_resource type="AnimationLibrary" path="res://assets/characters/animations/animation-pack.res" id="6_5oq5w"]
|
||||||
[ext_resource type="Script" uid="uid://cwwwixc07jc86" path="res://scripts/bot_controller.gd" id="7_botctrl"]
|
[ext_resource type="Script" uid="uid://cwwwixc07jc86" path="res://scripts/bot_controller.gd" id="7_botctrl"]
|
||||||
[ext_resource type="FontFile" uid="uid://xnjx058n4tsw" path="res://assets/fonts/Nougat-ExtraBlack.ttf" id="8_y4r1p"]
|
[ext_resource type="FontFile" uid="uid://xnjx058n4tsw" path="res://assets/fonts/Nougat-ExtraBlack.ttf" id="8_y4r1p"]
|
||||||
|
[ext_resource type="SpriteFrames" uid="uid://bq8ifua64lag2" path="res://assets/graphics/vfx/effects/powerup.tres" id="10_d2wvv"]
|
||||||
[ext_resource type="SpriteFrames" uid="uid://7r0qbbm88vfy" path="res://assets/graphics/vfx/effects/animation-head.tres" id="10_y4r1p"]
|
[ext_resource type="SpriteFrames" uid="uid://7r0qbbm88vfy" path="res://assets/graphics/vfx/effects/animation-head.tres" id="10_y4r1p"]
|
||||||
|
|
||||||
[sub_resource type="TorusMesh" id="TorusMesh_ur7pv"]
|
[sub_resource type="TorusMesh" id="TorusMesh_ur7pv"]
|
||||||
@@ -81,6 +82,7 @@ autowrap_mode = 2
|
|||||||
|
|
||||||
[node name="Position" type="Label3D" parent="." unique_id=482425681]
|
[node name="Position" type="Label3D" parent="." unique_id=482425681]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.537, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.537, 0)
|
||||||
|
visible = false
|
||||||
billboard = 1
|
billboard = 1
|
||||||
no_depth_test = true
|
no_depth_test = true
|
||||||
render_priority = 2
|
render_priority = 2
|
||||||
@@ -116,6 +118,17 @@ width = 700.0
|
|||||||
[node name="BotController" type="Node" parent="." unique_id=723259755]
|
[node name="BotController" type="Node" parent="." unique_id=723259755]
|
||||||
script = ExtResource("7_botctrl")
|
script = ExtResource("7_botctrl")
|
||||||
|
|
||||||
|
[node name="take_powerup" type="AnimatedSprite3D" parent="." unique_id=1497442994]
|
||||||
|
transform = Transform3D(0.54, 0, 0, 0, 0.54, 0, 0, 0, 0.54, 0, 0.21994019, 0)
|
||||||
|
visible = false
|
||||||
|
modulate = Color(1, 1, 1, 0.8)
|
||||||
|
billboard = 2
|
||||||
|
no_depth_test = true
|
||||||
|
render_priority = 3
|
||||||
|
sprite_frames = ExtResource("10_d2wvv")
|
||||||
|
animation = &"take_powerup"
|
||||||
|
frame_progress = 0.5033338
|
||||||
|
|
||||||
[node name="skill_freeze" type="AnimatedSprite3D" parent="." unique_id=674916570]
|
[node name="skill_freeze" type="AnimatedSprite3D" parent="." unique_id=674916570]
|
||||||
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, -1, 0, 1, -4.371139e-08, 0, 1.5653763, 0)
|
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, -1, 0, 1, -4.371139e-08, 0, 1.5653763, 0)
|
||||||
visible = false
|
visible = false
|
||||||
|
|||||||
@@ -544,6 +544,144 @@ text = "Load Current"
|
|||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
custom_minimum_size = Vector2(160, 36)
|
custom_minimum_size = Vector2(160, 36)
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Lobby Chat" type="VBoxContainer" parent="Margin/VBox/Tabs"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 10
|
||||||
|
|
||||||
|
[node name="PrefixRow" type="HBoxContainer" parent="Margin/VBox/Tabs/Lobby Chat"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="Margin/VBox/Tabs/Lobby Chat/PrefixRow"]
|
||||||
|
layout_mode = 2
|
||||||
|
custom_minimum_size = Vector2(220, 0)
|
||||||
|
text = "System Prefix:"
|
||||||
|
|
||||||
|
[node name="PrefixEdit" type="LineEdit" parent="Margin/VBox/Tabs/Lobby Chat/PrefixRow"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
placeholder_text = "[SERVER]"
|
||||||
|
|
||||||
|
[node name="MaxMsgRow" type="HBoxContainer" parent="Margin/VBox/Tabs/Lobby Chat"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="Margin/VBox/Tabs/Lobby Chat/MaxMsgRow"]
|
||||||
|
layout_mode = 2
|
||||||
|
custom_minimum_size = Vector2(220, 0)
|
||||||
|
text = "Max messages loaded:"
|
||||||
|
|
||||||
|
[node name="MaxMsgSpin" type="SpinBox" parent="Margin/VBox/Tabs/Lobby Chat/MaxMsgRow"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
custom_minimum_size = Vector2(120, 0)
|
||||||
|
min_value = 10.0
|
||||||
|
max_value = 200.0
|
||||||
|
step = 10.0
|
||||||
|
value = 50.0
|
||||||
|
|
||||||
|
[node name="MaxAgeRow" type="HBoxContainer" parent="Margin/VBox/Tabs/Lobby Chat"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="Margin/VBox/Tabs/Lobby Chat/MaxAgeRow"]
|
||||||
|
layout_mode = 2
|
||||||
|
custom_minimum_size = Vector2(220, 0)
|
||||||
|
text = "Delete messages older than (days):"
|
||||||
|
|
||||||
|
[node name="MaxAgeSpin" type="SpinBox" parent="Margin/VBox/Tabs/Lobby Chat/MaxAgeRow"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
custom_minimum_size = Vector2(120, 0)
|
||||||
|
min_value = 0.0
|
||||||
|
max_value = 365.0
|
||||||
|
step = 1.0
|
||||||
|
value = 0.0
|
||||||
|
tooltip_text = "0 = don't auto-delete, use manual purge only"
|
||||||
|
|
||||||
|
[node name="ChatActions" type="HBoxContainer" parent="Margin/VBox/Tabs/Lobby Chat"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
|
[node name="WipeChatBtn" type="Button" parent="Margin/VBox/Tabs/Lobby Chat/ChatActions"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(140, 36)
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Wipe Chat"
|
||||||
|
|
||||||
|
[node name="PurgeOldBtn" type="Button" parent="Margin/VBox/Tabs/Lobby Chat/ChatActions"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(140, 36)
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Purge Old"
|
||||||
|
|
||||||
|
[node name="SaveConfigBtn" type="Button" parent="Margin/VBox/Tabs/Lobby Chat/ChatActions"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(140, 36)
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Save Config"
|
||||||
|
|
||||||
|
[node name="ChatStatusLabel" type="Label" parent="Margin/VBox/Tabs/Lobby Chat"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = ""
|
||||||
|
|
||||||
|
[node name="Chat Storage" type="VBoxContainer" parent="Margin/VBox/Tabs"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
|
[node name="ChannelIdRow" type="HBoxContainer" parent="Margin/VBox/Tabs/Chat Storage"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="Margin/VBox/Tabs/Chat Storage/ChannelIdRow"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Channel ID:"
|
||||||
|
|
||||||
|
[node name="ChannelIdEdit" type="LineEdit" parent="Margin/VBox/Tabs/Chat Storage/ChannelIdRow"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
placeholder_text = "Enter channel ID..."
|
||||||
|
|
||||||
|
[node name="LoadMessagesBtn" type="Button" parent="Margin/VBox/Tabs/Chat Storage/ChannelIdRow"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(100, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Load"
|
||||||
|
|
||||||
|
[node name="ChatTree" type="Tree" parent="Margin/VBox/Tabs/Chat Storage"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
columns = 4
|
||||||
|
column_titles_visible = true
|
||||||
|
allow_reselect = true
|
||||||
|
hide_root = true
|
||||||
|
select_mode = 1
|
||||||
|
|
||||||
|
[node name="ChatStorageActionBar" type="HBoxContainer" parent="Margin/VBox/Tabs/Chat Storage"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 8
|
||||||
|
|
||||||
|
[node name="RefreshChatBtn" type="Button" parent="Margin/VBox/Tabs/Chat Storage/ChatStorageActionBar"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(100, 36)
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Refresh"
|
||||||
|
|
||||||
|
[node name="Spacer" type="Control" parent="Margin/VBox/Tabs/Chat Storage/ChatStorageActionBar"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="DeleteSelectedBtn" type="Button" parent="Margin/VBox/Tabs/Chat Storage/ChatStorageActionBar"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(120, 36)
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Delete Selected"
|
||||||
|
|
||||||
[node name="HistoryDialog" type="AcceptDialog" parent="."]
|
[node name="HistoryDialog" type="AcceptDialog" parent="."]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
title = "User History"
|
title = "User History"
|
||||||
|
|||||||
+19
-1
@@ -9,6 +9,7 @@ var _chat_messages: Array = []
|
|||||||
var _active_chat_context: String = "global"
|
var _active_chat_context: String = "global"
|
||||||
var _dm_tabs: Dictionary = {}
|
var _dm_tabs: Dictionary = {}
|
||||||
var _dm_messages: Dictionary = {}
|
var _dm_messages: Dictionary = {}
|
||||||
|
var _chat_config: Dictionary = {"prefix": "", "max_messages": 50, "max_age_days": 0}
|
||||||
|
|
||||||
func _init(p_lobby: Control):
|
func _init(p_lobby: Control):
|
||||||
lobby = p_lobby
|
lobby = p_lobby
|
||||||
@@ -44,14 +45,31 @@ func join_global_chat() -> void:
|
|||||||
if not socket.received_channel_message.is_connected(_on_chat_message_received):
|
if not socket.received_channel_message.is_connected(_on_chat_message_received):
|
||||||
socket.received_channel_message.connect(_on_chat_message_received)
|
socket.received_channel_message.connect(_on_chat_message_received)
|
||||||
|
|
||||||
|
# Fetch admin chat config (prefix, max_messages, etc.)
|
||||||
|
if BackendService.has_method("admin_get_chat_config"):
|
||||||
|
var cfg_res = await BackendService.admin_get_chat_config()
|
||||||
|
if cfg_res.has("config"):
|
||||||
|
_chat_config = cfg_res["config"]
|
||||||
|
|
||||||
_chat_messages.clear()
|
_chat_messages.clear()
|
||||||
var history_result = await NakamaManager.client.list_channel_messages_async(NakamaManager.session, _chat_channel.id, 50, false)
|
var limit: int = _chat_config.get("max_messages", 50)
|
||||||
|
var history_result = await NakamaManager.client.list_channel_messages_async(NakamaManager.session, _chat_channel.id, limit, false)
|
||||||
if not history_result.is_exception() and history_result.messages:
|
if not history_result.is_exception() and history_result.messages:
|
||||||
var msgs = history_result.messages.duplicate()
|
var msgs = history_result.messages.duplicate()
|
||||||
msgs.reverse()
|
msgs.reverse()
|
||||||
for msg in msgs:
|
for msg in msgs:
|
||||||
_add_chat_message(msg, false)
|
_add_chat_message(msg, false)
|
||||||
|
|
||||||
|
# Inject admin system prefix if configured
|
||||||
|
var prefix: String = _chat_config.get("prefix", "")
|
||||||
|
if not prefix.is_empty():
|
||||||
|
_chat_messages.insert(0, {
|
||||||
|
"sender": "SYSTEM",
|
||||||
|
"content": prefix,
|
||||||
|
"ts": _get_local_time(),
|
||||||
|
"date": Time.get_date_string_from_system()
|
||||||
|
})
|
||||||
|
|
||||||
_trim_old_messages()
|
_trim_old_messages()
|
||||||
_refresh_chat_display()
|
_refresh_chat_display()
|
||||||
|
|
||||||
|
|||||||
@@ -153,6 +153,12 @@ func add_powerup_from_item(item_id: int):
|
|||||||
var effect = get_effect_from_item(item_id)
|
var effect = get_effect_from_item(item_id)
|
||||||
if effect == -1: return
|
if effect == -1: return
|
||||||
|
|
||||||
|
# VFX: show pickup burst on all peers (mirrors skill VFX pattern)
|
||||||
|
if player.is_multiplayer_authority() and player.has_method("can_rpc") and player.can_rpc():
|
||||||
|
player.rpc("play_skill_vfx", "take_powerup")
|
||||||
|
elif player.has_method("play_skill_vfx"):
|
||||||
|
player.play_skill_vfx("take_powerup")
|
||||||
|
|
||||||
# 1-PowerUp Rule: If this is a DIFFERENT power-up, clear the old one
|
# 1-PowerUp Rule: If this is a DIFFERENT power-up, clear the old one
|
||||||
var is_different = not inventory.get(effect, false)
|
var is_different = not inventory.get(effect, false)
|
||||||
var already_has_any = false
|
var already_has_any = false
|
||||||
|
|||||||
@@ -149,6 +149,21 @@ func _parse_error_msg(msg: String) -> String:
|
|||||||
func admin_clear_global_chat(payload: String) -> Dictionary:
|
func admin_clear_global_chat(payload: String) -> Dictionary:
|
||||||
return await api_rpc_async("admin_clear_global_chat", payload)
|
return await api_rpc_async("admin_clear_global_chat", payload)
|
||||||
|
|
||||||
|
func admin_get_chat_config() -> Dictionary:
|
||||||
|
return await api_rpc_async("admin_get_chat_config", "{}")
|
||||||
|
|
||||||
|
func admin_set_chat_config(config: Dictionary) -> Dictionary:
|
||||||
|
return await api_rpc_async("admin_set_chat_config", JSON.stringify(config))
|
||||||
|
|
||||||
|
func admin_purge_old_messages(channel_id: String, max_age_days: int) -> Dictionary:
|
||||||
|
return await api_rpc_async("admin_purge_old_messages", JSON.stringify({"channel_id": channel_id, "max_age_days": max_age_days}))
|
||||||
|
|
||||||
|
func admin_list_channel_messages(channel_id: String, limit: int = 50, cursor: String = "", forward: bool = true) -> Dictionary:
|
||||||
|
return await api_rpc_async("admin_list_channel_messages", JSON.stringify({"channel_id": channel_id, "limit": limit, "cursor": cursor, "forward": forward}))
|
||||||
|
|
||||||
|
func admin_delete_channel_message(channel_id: String, message_id: String) -> Dictionary:
|
||||||
|
return await api_rpc_async("admin_delete_channel_message", JSON.stringify({"channel_id": channel_id, "message_id": message_id}))
|
||||||
|
|
||||||
func send_friend_request(target_id: String) -> Dictionary:
|
func send_friend_request(target_id: String) -> Dictionary:
|
||||||
var payload = JSON.stringify({"target_user_id": target_id})
|
var payload = JSON.stringify({"target_user_id": target_id})
|
||||||
return await api_rpc_async("send_friend_request", payload)
|
return await api_rpc_async("send_friend_request", payload)
|
||||||
|
|||||||
@@ -69,6 +69,27 @@ var _all_server_mails: Array = []
|
|||||||
@onready var load_banners_btn := %LoadBannersBtn as Button
|
@onready var load_banners_btn := %LoadBannersBtn as Button
|
||||||
@onready var save_banners_btn := %SaveBannersBtn as Button
|
@onready var save_banners_btn := %SaveBannersBtn as Button
|
||||||
|
|
||||||
|
# Tab: Lobby Chat
|
||||||
|
@onready var chat_prefix_edit := %PrefixEdit as LineEdit
|
||||||
|
@onready var chat_max_msg_spin := %MaxMsgSpin as SpinBox
|
||||||
|
@onready var chat_max_age_spin := %MaxAgeSpin as SpinBox
|
||||||
|
@onready var chat_wipe_btn := %WipeChatBtn as Button
|
||||||
|
@onready var chat_purge_btn := %PurgeOldBtn as Button
|
||||||
|
@onready var chat_save_btn := %SaveConfigBtn as Button
|
||||||
|
@onready var chat_status_label := %ChatStatusLabel as Label
|
||||||
|
|
||||||
|
# Tab: Chat Storage
|
||||||
|
@onready var chat_channel_id_edit := %ChannelIdEdit as LineEdit
|
||||||
|
@onready var load_messages_btn := %LoadMessagesBtn as Button
|
||||||
|
@onready var chat_tree := %ChatTree as Tree
|
||||||
|
@onready var refresh_chat_btn := %RefreshChatBtn as Button
|
||||||
|
@onready var delete_selected_btn := %DeleteSelectedBtn as Button
|
||||||
|
|
||||||
|
var _chat_tree_root: TreeItem
|
||||||
|
var _chat_channel_id: String = ""
|
||||||
|
var _chat_cursor: String = ""
|
||||||
|
var _chat_messages_data: Array = []
|
||||||
|
|
||||||
const MONTH_NAMES = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
|
const MONTH_NAMES = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
|
||||||
|
|
||||||
# -- Data --
|
# -- Data --
|
||||||
@@ -175,6 +196,17 @@ func _setup_columns() -> void:
|
|||||||
mail_tree.set_column_expand(5, false)
|
mail_tree.set_column_expand(5, false)
|
||||||
_mail_root = mail_tree.create_item()
|
_mail_root = mail_tree.create_item()
|
||||||
|
|
||||||
|
# Chat Storage
|
||||||
|
chat_tree.set_column_title(0, "Sender")
|
||||||
|
chat_tree.set_column_title(1, "Content")
|
||||||
|
chat_tree.set_column_title(2, "Date")
|
||||||
|
chat_tree.set_column_title(3, "ID")
|
||||||
|
chat_tree.set_column_custom_minimum_width(0, 100)
|
||||||
|
chat_tree.set_column_expand(1, true)
|
||||||
|
chat_tree.set_column_custom_minimum_width(2, 120)
|
||||||
|
chat_tree.set_column_custom_minimum_width(3, 100)
|
||||||
|
_chat_tree_root = chat_tree.create_item()
|
||||||
|
|
||||||
func _connect_signals() -> void:
|
func _connect_signals() -> void:
|
||||||
close_btn.pressed.connect(_on_close)
|
close_btn.pressed.connect(_on_close)
|
||||||
refresh_btn.pressed.connect(_on_refresh)
|
refresh_btn.pressed.connect(_on_refresh)
|
||||||
@@ -217,6 +249,16 @@ func _connect_signals() -> void:
|
|||||||
load_banners_btn.pressed.connect(func(): await _load_featured_banners())
|
load_banners_btn.pressed.connect(func(): await _load_featured_banners())
|
||||||
save_banners_btn.pressed.connect(func(): await _save_featured_banners())
|
save_banners_btn.pressed.connect(func(): await _save_featured_banners())
|
||||||
|
|
||||||
|
# Chat actions
|
||||||
|
chat_wipe_btn.pressed.connect(_on_wipe_chat)
|
||||||
|
chat_purge_btn.pressed.connect(_on_purge_old_chat)
|
||||||
|
chat_save_btn.pressed.connect(_on_save_chat_config)
|
||||||
|
|
||||||
|
# Chat Storage actions
|
||||||
|
load_messages_btn.pressed.connect(_on_load_chat_messages)
|
||||||
|
refresh_chat_btn.pressed.connect(_on_load_chat_messages)
|
||||||
|
delete_selected_btn.pressed.connect(_on_delete_chat_message)
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Core Panel Logic
|
# Core Panel Logic
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -243,6 +285,10 @@ func _on_tab_changed(tab_index: int) -> void:
|
|||||||
await _load_mail()
|
await _load_mail()
|
||||||
elif tab_index == 5:
|
elif tab_index == 5:
|
||||||
await _load_featured_banners()
|
await _load_featured_banners()
|
||||||
|
elif tab_index == 6:
|
||||||
|
await _load_chat_config()
|
||||||
|
elif tab_index == 7:
|
||||||
|
await _on_load_chat_messages()
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# RPC Helper
|
# RPC Helper
|
||||||
@@ -1193,3 +1239,159 @@ func _save_featured_banners() -> void:
|
|||||||
_set_status("Save failed: " + str(res.error), CLR_STATUS_ERR)
|
_set_status("Save failed: " + str(res.error), CLR_STATUS_ERR)
|
||||||
elif res.has("success"):
|
elif res.has("success"):
|
||||||
_set_status("Banners saved! (%d slots)" % banners.size(), CLR_STATUS_OK)
|
_set_status("Banners saved! (%d slots)" % banners.size(), CLR_STATUS_OK)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# TAB 7: LOBBY CHAT
|
||||||
|
# =============================================================================
|
||||||
|
func _load_chat_config() -> void:
|
||||||
|
chat_status_label.text = "Loading config..."
|
||||||
|
var res := await _rpc("admin_get_chat_config", {})
|
||||||
|
if res.has("error"):
|
||||||
|
chat_status_label.text = "Failed: " + str(res.error)
|
||||||
|
return
|
||||||
|
|
||||||
|
var config: Dictionary = res.get("config", {})
|
||||||
|
chat_prefix_edit.text = config.get("prefix", "")
|
||||||
|
chat_max_msg_spin.value = config.get("max_messages", 50)
|
||||||
|
chat_max_age_spin.value = config.get("max_age_days", 0)
|
||||||
|
chat_status_label.text = ""
|
||||||
|
|
||||||
|
func _on_wipe_chat() -> void:
|
||||||
|
var confirm := ConfirmationDialog.new()
|
||||||
|
confirm.title = "Wipe Entire Lobby Chat?"
|
||||||
|
confirm.dialog_text = "This will delete ALL messages in the global lobby chat for everyone. Continue?"
|
||||||
|
add_child(confirm)
|
||||||
|
confirm.popup_centered()
|
||||||
|
confirm.confirmed.connect(func():
|
||||||
|
chat_status_label.text = "Wiping chat..."
|
||||||
|
var lobby = get_tree().get_first_node_in_group("Lobby")
|
||||||
|
if lobby and lobby.has_method("admin_wipe_chat"):
|
||||||
|
lobby.admin_wipe_chat()
|
||||||
|
chat_status_label.text = "Chat wiped!"
|
||||||
|
else:
|
||||||
|
chat_status_label.text = "Lobby not found — cannot wipe."
|
||||||
|
confirm.queue_free()
|
||||||
|
)
|
||||||
|
|
||||||
|
func _on_purge_old_chat() -> void:
|
||||||
|
var max_age: int = int(chat_max_age_spin.value)
|
||||||
|
if max_age <= 0:
|
||||||
|
chat_status_label.text = "Set 'Delete older than' to > 0 days first."
|
||||||
|
return
|
||||||
|
|
||||||
|
var confirm := ConfirmationDialog.new()
|
||||||
|
confirm.title = "Purge Old Messages?"
|
||||||
|
confirm.dialog_text = "Delete all messages older than %d days?" % max_age
|
||||||
|
add_child(confirm)
|
||||||
|
confirm.popup_centered()
|
||||||
|
confirm.confirmed.connect(func():
|
||||||
|
chat_status_label.text = "Purging old messages..."
|
||||||
|
var lobby = get_tree().get_first_node_in_group("Lobby")
|
||||||
|
if lobby and lobby.has_method("admin_purge_chat"):
|
||||||
|
var deleted: int = await lobby.admin_purge_chat(max_age)
|
||||||
|
chat_status_label.text = "Purged %d old messages." % deleted
|
||||||
|
else:
|
||||||
|
chat_status_label.text = "Lobby not found — cannot purge."
|
||||||
|
confirm.queue_free()
|
||||||
|
)
|
||||||
|
|
||||||
|
func _on_save_chat_config() -> void:
|
||||||
|
chat_status_label.text = "Saving..."
|
||||||
|
var config := {
|
||||||
|
"prefix": chat_prefix_edit.text.strip_edges(),
|
||||||
|
"max_messages": int(chat_max_msg_spin.value),
|
||||||
|
"max_age_days": int(chat_max_age_spin.value)
|
||||||
|
}
|
||||||
|
var res := await _rpc("admin_set_chat_config", config)
|
||||||
|
if res.has("error"):
|
||||||
|
chat_status_label.text = "Failed: " + str(res.error)
|
||||||
|
else:
|
||||||
|
chat_status_label.text = "Chat config saved!"
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# TAB 8: CHAT STORAGE
|
||||||
|
# =============================================================================
|
||||||
|
func _on_load_chat_messages() -> void:
|
||||||
|
var channel_id := chat_channel_id_edit.text.strip_edges()
|
||||||
|
if channel_id.is_empty():
|
||||||
|
_set_status("Enter a Channel ID first.", CLR_STATUS_ERR)
|
||||||
|
return
|
||||||
|
|
||||||
|
_chat_channel_id = channel_id
|
||||||
|
_chat_cursor = ""
|
||||||
|
_chat_messages_data.clear()
|
||||||
|
_clear_tree(chat_tree, _chat_tree_root)
|
||||||
|
|
||||||
|
await _fetch_chat_messages_batch()
|
||||||
|
|
||||||
|
func _fetch_chat_messages_batch() -> void:
|
||||||
|
_set_status("Loading messages...")
|
||||||
|
var payload := {
|
||||||
|
"channel_id": _chat_channel_id,
|
||||||
|
"limit": 50,
|
||||||
|
"cursor": _chat_cursor,
|
||||||
|
"forward": false
|
||||||
|
}
|
||||||
|
var res := await _rpc("admin_list_channel_messages", payload)
|
||||||
|
|
||||||
|
if res.has("error"):
|
||||||
|
_set_status("Failed: " + str(res.error), CLR_STATUS_ERR)
|
||||||
|
return
|
||||||
|
|
||||||
|
var msgs = res.get("messages", [])
|
||||||
|
var next_cursor = res.get("next_cursor", "")
|
||||||
|
|
||||||
|
for msg in msgs:
|
||||||
|
_chat_messages_data.append(msg)
|
||||||
|
var item := _chat_tree_root.create_child()
|
||||||
|
item.set_text(0, msg.get("username", msg.get("sender_id", "?").substr(0, 8)))
|
||||||
|
item.set_text(1, msg.get("content", ""))
|
||||||
|
item.set_text(2, msg.get("create_time", "").substr(0, 19).replace("T", " "))
|
||||||
|
var mid = msg.get("message_id", "")
|
||||||
|
item.set_text(3, mid)
|
||||||
|
item.set_tooltip_text(3, mid)
|
||||||
|
item.set_metadata(0, msg)
|
||||||
|
|
||||||
|
count_label.text = "%d messages loaded" % _chat_messages_data.size()
|
||||||
|
|
||||||
|
if not next_cursor.is_empty():
|
||||||
|
_chat_cursor = next_cursor
|
||||||
|
_set_status("Loaded page. Click Refresh to load more.", CLR_STATUS_OK)
|
||||||
|
else:
|
||||||
|
_chat_cursor = ""
|
||||||
|
_set_status("All messages loaded.", CLR_STATUS_OK)
|
||||||
|
|
||||||
|
func _on_delete_chat_message() -> void:
|
||||||
|
var item = chat_tree.get_selected()
|
||||||
|
if not item:
|
||||||
|
_set_status("Select a message to delete.", CLR_STATUS_ERR)
|
||||||
|
return
|
||||||
|
|
||||||
|
var msg = item.get_metadata(0)
|
||||||
|
if not msg:
|
||||||
|
return
|
||||||
|
|
||||||
|
var msg_id = msg.get("message_id", "")
|
||||||
|
if msg_id.is_empty():
|
||||||
|
return
|
||||||
|
|
||||||
|
var confirm := ConfirmationDialog.new()
|
||||||
|
confirm.title = "Delete Message?"
|
||||||
|
confirm.dialog_text = "Permanently delete message from " + msg.get("username", "?") + "?"
|
||||||
|
add_child(confirm)
|
||||||
|
confirm.popup_centered()
|
||||||
|
confirm.confirmed.connect(func():
|
||||||
|
_set_status("Deleting message...")
|
||||||
|
var res = await _rpc("admin_delete_channel_message", {
|
||||||
|
"channel_id": _chat_channel_id,
|
||||||
|
"message_id": msg_id
|
||||||
|
})
|
||||||
|
if res.get("success", false):
|
||||||
|
_set_status("Message deleted!", CLR_STATUS_OK)
|
||||||
|
chat_tree.get_root().remove_child(item)
|
||||||
|
item.free()
|
||||||
|
count_label.text = "%d messages loaded" % _chat_messages_data.size()
|
||||||
|
else:
|
||||||
|
_set_status("Failed: " + str(res.get("error", "")), CLR_STATUS_ERR)
|
||||||
|
confirm.queue_free()
|
||||||
|
)
|
||||||
|
|||||||
@@ -336,6 +336,157 @@ function admin.rpc_admin_get_player_list(context, payload)
|
|||||||
return nk.json_encode({ players = players })
|
return nk.json_encode({ players = players })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- Lobby Chat Management
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
function admin.rpc_admin_get_chat_config(context, payload)
|
||||||
|
utils.require_admin(context)
|
||||||
|
local configObjs = nk.storage_read({{
|
||||||
|
collection = "config",
|
||||||
|
key = "lobby_chat",
|
||||||
|
user_id = utils.SYSTEM_USER_ID
|
||||||
|
}})
|
||||||
|
local config = { prefix = "", max_messages = 50, max_age_days = 0 }
|
||||||
|
if configObjs and #configObjs > 0 and configObjs[1].value then
|
||||||
|
local val = configObjs[1].value
|
||||||
|
config.prefix = val.prefix or ""
|
||||||
|
config.max_messages = val.max_messages or 50
|
||||||
|
config.max_age_days = val.max_age_days or 0
|
||||||
|
end
|
||||||
|
return nk.json_encode({ config = config })
|
||||||
|
end
|
||||||
|
|
||||||
|
function admin.rpc_admin_set_chat_config(context, payload)
|
||||||
|
utils.require_admin(context)
|
||||||
|
local request = nk.json_decode(payload or "{}")
|
||||||
|
local config = {
|
||||||
|
prefix = request.prefix or "",
|
||||||
|
max_messages = request.max_messages or 50,
|
||||||
|
max_age_days = request.max_age_days or 0
|
||||||
|
}
|
||||||
|
nk.storage_write({{
|
||||||
|
collection = "config",
|
||||||
|
key = "lobby_chat",
|
||||||
|
user_id = utils.SYSTEM_USER_ID,
|
||||||
|
value = config,
|
||||||
|
permission_read = 2,
|
||||||
|
permission_write = 0
|
||||||
|
}})
|
||||||
|
nk.logger_info("[AdminChat] Chat config updated by " .. context.user_id)
|
||||||
|
return nk.json_encode({ success = true })
|
||||||
|
end
|
||||||
|
|
||||||
|
function admin.rpc_admin_purge_old_messages(context, payload)
|
||||||
|
utils.require_admin(context)
|
||||||
|
local request = nk.json_decode(payload or "{}")
|
||||||
|
local channelId = request.channel_id or ""
|
||||||
|
local maxAgeDays = request.max_age_days or 0
|
||||||
|
|
||||||
|
if channelId == "" then
|
||||||
|
error("channel_id is required")
|
||||||
|
end
|
||||||
|
if maxAgeDays <= 0 then
|
||||||
|
error("max_age_days must be > 0")
|
||||||
|
end
|
||||||
|
|
||||||
|
local cutoff = os.time() - (maxAgeDays * 86400)
|
||||||
|
local deleted = 0
|
||||||
|
local cursor = ""
|
||||||
|
|
||||||
|
repeat
|
||||||
|
local status, result = pcall(nk.channel_messages_list, channelId, 100, false, cursor)
|
||||||
|
if not status then break end
|
||||||
|
|
||||||
|
local messages = result.messages or {}
|
||||||
|
for _, msg in ipairs(messages) do
|
||||||
|
-- Parse create_time to compare against cutoff
|
||||||
|
local msgTime = 0
|
||||||
|
if msg.create_time then
|
||||||
|
-- Try ISO format: "2024-01-15T10:30:00Z"
|
||||||
|
local y, m, d, hh, mm, ss = msg.create_time:match("(%d+)%-(%d+)%-(%d+)T(%d+):(%d+):(%d+)")
|
||||||
|
if y then
|
||||||
|
msgTime = os.time({
|
||||||
|
year = tonumber(y), month = tonumber(m), day = tonumber(d),
|
||||||
|
hour = tonumber(hh), min = tonumber(mm), sec = tonumber(ss)
|
||||||
|
})
|
||||||
|
else
|
||||||
|
-- Try unix timestamp string
|
||||||
|
msgTime = tonumber(msg.create_time) or 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if msgTime > 0 and msgTime < cutoff then
|
||||||
|
pcall(nk.channel_message_remove, channelId, msg.message_id)
|
||||||
|
deleted = deleted + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
cursor = result.next_cursor or ""
|
||||||
|
until cursor == ""
|
||||||
|
|
||||||
|
nk.logger_info("[AdminChat] Purged " .. deleted .. " messages older than " .. maxAgeDays .. " days by " .. context.user_id)
|
||||||
|
return nk.json_encode({ success = true, deleted = deleted })
|
||||||
|
end
|
||||||
|
|
||||||
|
function admin.rpc_admin_list_channel_messages(context, payload)
|
||||||
|
utils.require_admin(context)
|
||||||
|
local request = nk.json_decode(payload or "{}")
|
||||||
|
local channelId = request.channel_id or ""
|
||||||
|
local limit = request.limit or 50
|
||||||
|
local cursor = request.cursor or ""
|
||||||
|
local forward = request.forward == nil and true or request.forward
|
||||||
|
|
||||||
|
if channelId == "" then
|
||||||
|
error("channel_id is required")
|
||||||
|
end
|
||||||
|
|
||||||
|
local status, result = pcall(nk.channel_messages_list, channelId, limit, forward, cursor)
|
||||||
|
if not status then
|
||||||
|
error("Failed to list messages: " .. tostring(result))
|
||||||
|
end
|
||||||
|
|
||||||
|
local msgs = {}
|
||||||
|
if result and result.messages then
|
||||||
|
for _, msg in ipairs(result.messages) do
|
||||||
|
table.insert(msgs, {
|
||||||
|
message_id = msg.message_id,
|
||||||
|
sender_id = msg.sender_id,
|
||||||
|
username = msg.username,
|
||||||
|
content = msg.content,
|
||||||
|
create_time = msg.create_time,
|
||||||
|
update_time = msg.update_time,
|
||||||
|
channel_id = msg.channel_id
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return nk.json_encode({
|
||||||
|
messages = msgs,
|
||||||
|
next_cursor = result.next_cursor or "",
|
||||||
|
cache_cursor = result.cache_cursor or ""
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
function admin.rpc_admin_delete_channel_message(context, payload)
|
||||||
|
utils.require_admin(context)
|
||||||
|
local request = nk.json_decode(payload or "{}")
|
||||||
|
local channelId = request.channel_id or ""
|
||||||
|
local messageId = request.message_id or ""
|
||||||
|
|
||||||
|
if channelId == "" or messageId == "" then
|
||||||
|
error("channel_id and message_id are required")
|
||||||
|
end
|
||||||
|
|
||||||
|
local status, err = pcall(nk.channel_message_remove, channelId, messageId)
|
||||||
|
if not status then
|
||||||
|
error("Failed to delete message: " .. tostring(err))
|
||||||
|
end
|
||||||
|
|
||||||
|
nk.logger_info("[AdminChat] Deleted message " .. messageId .. " from channel " .. channelId .. " by " .. context.user_id)
|
||||||
|
return nk.json_encode({ success = true })
|
||||||
|
end
|
||||||
|
|
||||||
-- Register RPCs
|
-- Register RPCs
|
||||||
nk.register_rpc(admin.rpc_admin_kick_player, "admin_kick_player")
|
nk.register_rpc(admin.rpc_admin_kick_player, "admin_kick_player")
|
||||||
nk.register_rpc(admin.rpc_admin_ban_player, "admin_ban_player")
|
nk.register_rpc(admin.rpc_admin_ban_player, "admin_ban_player")
|
||||||
@@ -349,6 +500,11 @@ nk.register_rpc(admin.rpc_admin_topup_gold, "admin_topup_gold")
|
|||||||
nk.register_rpc(admin.rpc_admin_clear_global_chat, "admin_clear_global_chat")
|
nk.register_rpc(admin.rpc_admin_clear_global_chat, "admin_clear_global_chat")
|
||||||
nk.register_rpc(admin.rpc_admin_list_users, "admin_list_users")
|
nk.register_rpc(admin.rpc_admin_list_users, "admin_list_users")
|
||||||
nk.register_rpc(admin.rpc_admin_delete_users, "admin_delete_users")
|
nk.register_rpc(admin.rpc_admin_delete_users, "admin_delete_users")
|
||||||
|
nk.register_rpc(admin.rpc_admin_get_chat_config, "admin_get_chat_config")
|
||||||
|
nk.register_rpc(admin.rpc_admin_set_chat_config, "admin_set_chat_config")
|
||||||
|
nk.register_rpc(admin.rpc_admin_purge_old_messages, "admin_purge_old_messages")
|
||||||
|
nk.register_rpc(admin.rpc_admin_list_channel_messages, "admin_list_channel_messages")
|
||||||
|
nk.register_rpc(admin.rpc_admin_delete_channel_message, "admin_delete_channel_message")
|
||||||
|
|
||||||
nk.logger_info("LUA TEST: admin module loaded successfully")
|
nk.logger_info("LUA TEST: admin module loaded successfully")
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ local nk = require("nakama")
|
|||||||
local utils = {}
|
local utils = {}
|
||||||
|
|
||||||
utils.ADMIN_ROLES = { ["admin"] = true, ["moderator"] = true, ["owner"] = true }
|
utils.ADMIN_ROLES = { ["admin"] = true, ["moderator"] = true, ["owner"] = true }
|
||||||
|
utils.SYSTEM_USER_ID = "00000000-0000-0000-0000-000000000000"
|
||||||
|
|
||||||
function utils.is_admin(context)
|
function utils.is_admin(context)
|
||||||
if not context.user_id then return false end
|
if not context.user_id then return false end
|
||||||
|
|||||||
Reference in New Issue
Block a user