15043b5655
- 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)
374 lines
12 KiB
GDScript
374 lines
12 KiB
GDScript
class_name LobbyChat
|
|
extends RefCounted
|
|
|
|
var lobby: Control
|
|
|
|
const GLOBAL_CHAT_ROOM := "social_global"
|
|
var _chat_channel = null
|
|
var _chat_messages: Array = []
|
|
var _active_chat_context: String = "global"
|
|
var _dm_tabs: Dictionary = {}
|
|
var _dm_messages: Dictionary = {}
|
|
var _chat_config: Dictionary = {"prefix": "", "max_messages": 50, "max_age_days": 0}
|
|
|
|
func _init(p_lobby: Control):
|
|
lobby = p_lobby
|
|
|
|
if lobby.chat_send_btn:
|
|
lobby.chat_send_btn.pressed.connect(_on_chat_send_pressed)
|
|
if lobby.chat_input:
|
|
lobby.chat_input.text_submitted.connect(func(_t): _on_chat_send_pressed())
|
|
lobby.chat_input.text_changed.connect(_on_chat_input_changed)
|
|
|
|
_setup_friend_suggest_ui()
|
|
|
|
func join_global_chat() -> void:
|
|
if _chat_channel != null:
|
|
return
|
|
|
|
var socket = NakamaManager.socket
|
|
if not socket or not socket.is_connected_to_host():
|
|
push_warning("[Chat] Socket not connected, skipping chat join.")
|
|
return
|
|
|
|
var result = await socket.join_chat_async(GLOBAL_CHAT_ROOM,
|
|
NakamaSocket.ChannelType.Room,
|
|
true, false)
|
|
|
|
if result.is_exception():
|
|
push_warning("[Chat] Failed to join global chat: " + result.get_exception().message)
|
|
return
|
|
|
|
_chat_channel = result
|
|
print("[Chat] Joined global channel: ", _chat_channel.id)
|
|
|
|
if not socket.received_channel_message.is_connected(_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()
|
|
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:
|
|
var msgs = history_result.messages.duplicate()
|
|
msgs.reverse()
|
|
for msg in msgs:
|
|
_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()
|
|
_refresh_chat_display()
|
|
|
|
func _on_chat_message_received(message) -> void:
|
|
if _chat_channel == null or message.channel_id != _chat_channel.id:
|
|
return
|
|
if NakamaManager.session and message.sender_id == NakamaManager.session.user_id:
|
|
return
|
|
_add_chat_message(message, true)
|
|
|
|
func _add_chat_message(message, refresh_display: bool) -> void:
|
|
var text: String = ""
|
|
var parsed = JSON.parse_string(message.content)
|
|
if typeof(parsed) == TYPE_DICTIONARY:
|
|
text = parsed.get("msg", message.content)
|
|
else:
|
|
text = message.content
|
|
|
|
var sender: String = message.username
|
|
if sender.is_empty() and message.sender_id:
|
|
sender = message.sender_id.substr(0, 8)
|
|
elif sender.is_empty():
|
|
sender = "Unknown"
|
|
|
|
var ts_str: String = _format_nakama_time(message.create_time)
|
|
|
|
_chat_messages.append({
|
|
"sender": sender,
|
|
"content": text,
|
|
"ts": ts_str,
|
|
"date": message.create_time.substr(0, 10) if message.create_time else Time.get_date_string_from_system()
|
|
})
|
|
|
|
if refresh_display:
|
|
_trim_old_messages()
|
|
_refresh_chat_display()
|
|
|
|
func _on_chat_send_pressed() -> void:
|
|
if lobby.chat_input == null or lobby.chat_input.text.strip_edges().is_empty():
|
|
return
|
|
|
|
var text = lobby.chat_input.text.strip_edges()
|
|
lobby.chat_input.text = ""
|
|
lobby.chat_input.grab_focus()
|
|
|
|
if lobby._friend_suggest_panel and lobby._friend_suggest_panel.visible:
|
|
lobby._friend_suggest_panel.hide()
|
|
|
|
if text.begins_with("@"):
|
|
var space_idx = text.find(" ")
|
|
var target_username = ""
|
|
var dm_message = ""
|
|
|
|
if space_idx > 0:
|
|
target_username = text.substr(1, space_idx - 1)
|
|
dm_message = text.substr(space_idx + 1).strip_edges()
|
|
else:
|
|
target_username = text.substr(1).strip_edges()
|
|
|
|
if not target_username.is_empty():
|
|
var target_friend = null
|
|
for f in FriendManager.friends:
|
|
if f.get("username", "") == target_username:
|
|
target_friend = f
|
|
break
|
|
|
|
if target_friend:
|
|
var user_id = target_friend.get("user_id", "")
|
|
var username = target_friend.get("username", "")
|
|
|
|
_open_dm_tab(user_id, username)
|
|
|
|
if not dm_message.is_empty():
|
|
_send_dm_message(user_id, dm_message)
|
|
return
|
|
elif text.begins_with("@"):
|
|
_inject_local_message("User %s not found in friends." % target_username)
|
|
return
|
|
|
|
if _active_chat_context != "global":
|
|
if text == "/clear":
|
|
_dm_messages[_active_chat_context] = []
|
|
_refresh_chat_display()
|
|
return
|
|
_send_dm_message(_active_chat_context, text)
|
|
return
|
|
|
|
if text == "/clear":
|
|
var is_admin = await AdminManager._check_admin_status()
|
|
if is_admin:
|
|
_chat_messages.clear()
|
|
_refresh_chat_display()
|
|
if _chat_channel and NakamaManager.session and NakamaManager.client:
|
|
var payload = JSON.stringify({"channel_id": _chat_channel.id})
|
|
var rpc_result = await BackendService.admin_clear_global_chat(payload)
|
|
if rpc_result.get("success", false) == false:
|
|
push_warning("[Chat] admin_clear_global_chat RPC failed: " + str(rpc_result.get("message", "")))
|
|
else:
|
|
_inject_local_message("[SYSTEM] : Global chat cleared by admin.")
|
|
else:
|
|
_inject_local_message("[SYSTEM] : Unknown cmd, /clear only usable on DM between user.")
|
|
return
|
|
|
|
_inject_local_message(text)
|
|
|
|
var socket = NakamaManager.socket
|
|
if not socket or _chat_channel == null:
|
|
return
|
|
|
|
var content := {"msg": text}
|
|
var result = await socket.write_chat_message_async(_chat_channel.id, content)
|
|
if result.is_exception():
|
|
push_warning("[Chat] Failed to send message: " + result.get_exception().message)
|
|
|
|
func _send_dm_message(user_id: String, text: String) -> void:
|
|
var sent = await FriendManager.send_dm(user_id, text)
|
|
if sent:
|
|
if not _dm_messages.has(user_id):
|
|
_dm_messages[user_id] = []
|
|
_dm_messages[user_id].append({"sender": "You", "content": text, "ts": _get_local_time()})
|
|
if _active_chat_context == user_id:
|
|
_refresh_chat_display()
|
|
|
|
func on_lobby_dm_received(from_user_id: String, from_name: String, message: String) -> void:
|
|
if not _dm_messages.has(from_user_id):
|
|
_dm_messages[from_user_id] = []
|
|
|
|
_dm_messages[from_user_id].append({"sender": from_name, "content": message, "ts": _get_local_time()})
|
|
|
|
if not _dm_tabs.has(from_user_id):
|
|
_create_dm_tab(from_user_id, from_name)
|
|
|
|
if _active_chat_context == from_user_id:
|
|
_refresh_chat_display()
|
|
|
|
func _open_dm_tab(user_id: String, username: String) -> void:
|
|
if not _dm_tabs.has(user_id):
|
|
_create_dm_tab(user_id, username)
|
|
_dm_messages[user_id] = []
|
|
switch_chat_tab(user_id)
|
|
|
|
var history = await FriendManager.get_dm_history(user_id)
|
|
var my_id = NakamaManager.session.user_id if NakamaManager.session else ""
|
|
for entry in history:
|
|
var is_self = entry.get("from") == my_id
|
|
var sender_name = "You" if is_self else username
|
|
var ts = _format_nakama_time(entry.get("create_time", ""))
|
|
_dm_messages[user_id].append({"sender": sender_name, "content": entry.get("msg", ""), "ts": ts})
|
|
|
|
if _active_chat_context == user_id:
|
|
_refresh_chat_display()
|
|
else:
|
|
switch_chat_tab(user_id)
|
|
|
|
func _create_dm_tab(user_id: String, username: String) -> void:
|
|
var tabs_container = lobby.get_node_or_null("%ChatTabsContainer")
|
|
var template = lobby.get_node_or_null("%DMTabTemplate")
|
|
if not tabs_container or not template: return
|
|
|
|
var hbox = template.duplicate()
|
|
hbox.visible = true
|
|
|
|
var btn = hbox.get_node("DMTabBtn")
|
|
if btn:
|
|
btn.text = username
|
|
btn.pressed.connect(func(): switch_chat_tab(user_id))
|
|
|
|
var close_btn = hbox.get_node("DMTabCloseBtn")
|
|
if close_btn:
|
|
close_btn.pressed.connect(func(): _close_dm_tab(user_id))
|
|
|
|
tabs_container.add_child(hbox)
|
|
_dm_tabs[user_id] = hbox
|
|
|
|
func _close_dm_tab(user_id: String) -> void:
|
|
if _dm_tabs.has(user_id):
|
|
var tab = _dm_tabs[user_id]
|
|
tab.queue_free()
|
|
_dm_tabs.erase(user_id)
|
|
|
|
if _active_chat_context == user_id:
|
|
switch_chat_tab("global")
|
|
|
|
func switch_chat_tab(context_id: String) -> void:
|
|
_active_chat_context = context_id
|
|
_refresh_chat_display()
|
|
|
|
var tabs_container = lobby.get_node_or_null("%ChatTabsContainer")
|
|
if tabs_container:
|
|
var global_btn = lobby.get_node_or_null("%GlobalChatTabBtn")
|
|
if global_btn:
|
|
global_btn.modulate = Color(1.0, 1.0, 1.0) if context_id == "global" else Color(0.6, 0.6, 0.6)
|
|
|
|
for u_id in _dm_tabs:
|
|
var tab_hbox = _dm_tabs[u_id]
|
|
tab_hbox.modulate = Color(1.0, 1.0, 1.0) if context_id == u_id else Color(0.6, 0.6, 0.6)
|
|
|
|
func _setup_friend_suggest_ui() -> void:
|
|
lobby._friend_suggest_panel = lobby.get_node_or_null("%FriendSuggestPanel")
|
|
lobby._friend_suggest_list = lobby.get_node_or_null("%FriendSuggestList")
|
|
if not lobby._friend_suggest_panel or not lobby._friend_suggest_list:
|
|
push_warning("[Lobby] FriendSuggestPanel or FriendSuggestList not found in scene")
|
|
return
|
|
lobby._friend_suggest_panel.visible = false
|
|
lobby._friend_suggest_list.item_activated.connect(_on_friend_suggest_activated)
|
|
lobby._friend_suggest_list.item_selected.connect(_on_friend_suggest_activated)
|
|
|
|
func _on_chat_input_changed(new_text: String) -> void:
|
|
if not lobby._friend_suggest_panel or not lobby._friend_suggest_list:
|
|
return
|
|
if new_text.begins_with("@") and not " " in new_text:
|
|
var search = new_text.substr(1).to_lower()
|
|
lobby._friend_suggest_list.clear()
|
|
for f in FriendManager.friends:
|
|
if f.get("state", -1) == FriendManager.STATE_FRIEND:
|
|
var uname = f.get("username", "")
|
|
if uname.to_lower().begins_with(search) or search.is_empty():
|
|
lobby._friend_suggest_list.add_item(uname)
|
|
|
|
lobby._friend_suggest_panel.visible = lobby._friend_suggest_list.item_count > 0
|
|
else:
|
|
lobby._friend_suggest_panel.visible = false
|
|
|
|
func _on_friend_suggest_activated(index: int) -> void:
|
|
if not lobby._friend_suggest_list:
|
|
return
|
|
var uname = lobby._friend_suggest_list.get_item_text(index)
|
|
lobby.chat_input.text = "@%s " % uname
|
|
lobby.chat_input.caret_column = lobby.chat_input.text.length()
|
|
lobby.chat_input.grab_focus()
|
|
lobby._friend_suggest_panel.visible = false
|
|
|
|
func _inject_local_message(text: String) -> void:
|
|
var display_name = UserProfileManager.get_display_name("You")
|
|
var ts_str = _get_local_time()
|
|
_chat_messages.append({
|
|
"sender": display_name,
|
|
"content": text,
|
|
"ts": ts_str,
|
|
"date": Time.get_date_string_from_system()
|
|
})
|
|
_refresh_chat_display()
|
|
|
|
func _trim_old_messages() -> void:
|
|
if _chat_messages.size() > 100:
|
|
_chat_messages = _chat_messages.slice(-100)
|
|
|
|
func _refresh_chat_display() -> void:
|
|
if not lobby.chat_display:
|
|
return
|
|
|
|
lobby.chat_display.clear()
|
|
|
|
var messages_to_show = _chat_messages
|
|
if _active_chat_context != "global":
|
|
if _dm_messages.has(_active_chat_context):
|
|
messages_to_show = _dm_messages[_active_chat_context]
|
|
else:
|
|
messages_to_show = []
|
|
|
|
for msg in messages_to_show:
|
|
var ts: String = msg.get("ts", "")
|
|
var sender: String = msg.get("sender", "?")
|
|
var text: String = msg.get("content", "")
|
|
|
|
lobby.chat_display.append_text("[color=#888888][%s][/color] [b]%s:[/b] %s\n" % [ts, sender, text])
|
|
|
|
if lobby.chat_display:
|
|
var scrollbar = lobby.chat_display.get_v_scroll_bar()
|
|
if scrollbar:
|
|
lobby.chat_display.scroll_to_line(lobby.chat_display.get_line_count())
|
|
|
|
func _format_nakama_time(time_str: String) -> String:
|
|
if time_str.is_valid_int():
|
|
var unix_time = time_str.to_int()
|
|
var d = Time.get_date_dict_from_unix_time(unix_time)
|
|
var t = Time.get_time_dict_from_unix_time(unix_time)
|
|
return "%02d-%02d-%02d - %02d:%02d" % [d.day, d.month, d.year % 100, t.hour, t.minute]
|
|
|
|
if time_str.length() >= 19:
|
|
var date_part = time_str.substr(0, 10).split("-")
|
|
var time_part = time_str.substr(11, 5) # HH:MM
|
|
if date_part.size() == 3:
|
|
var year = date_part[0].substr(2, 2)
|
|
var month = date_part[1]
|
|
var day = date_part[2]
|
|
return "%s-%s-%s - %s" % [day, month, year, time_part]
|
|
|
|
return _get_local_time()
|
|
|
|
func _get_local_time() -> String:
|
|
var d = Time.get_date_dict_from_system()
|
|
var t = Time.get_time_dict_from_system()
|
|
return "%02d-%02d-%02d - %02d:%02d" % [d.day, d.month, d.year % 100, t.hour, t.minute]
|
|
|
|
func leave_global_chat() -> void:
|
|
var socket = NakamaManager.socket
|
|
if socket and _chat_channel:
|
|
socket.received_channel_message.disconnect(_on_chat_message_received)
|
|
await socket.leave_chat_async(_chat_channel.id)
|
|
_chat_channel = null
|