Files
tekton/scenes/ui/lobby_chat.gd
T
adtpdn decdb74ade chore: release version 2.3.5 and refactor lobby
Bump export_presets.cfg version to 2.3.5. Update CHANGELOG_DRAFT.md.
Refactor lobby.gd into LobbyChat, LobbyMainMenu, LobbyRoomList, LobbyRoom.
Move Nakama config to environment variables in nakama_manager.gd.
Derive auth_manager.gd encryption key from OS.get_unique_id().sha256_text().
Remove Steam email auth fallback. Require auth ticket.
Make GachaManager.pull() async in gacha_panel.gd.
Remove dummy wallet seeding. Add store_type to IAP payload.
Validate IAP receipts server-side in economy.lua.
Register gacha module in main.lua.
Clean backend_service.gd stubs.
Fix featured_banners type safety in gacha_manager.gd. Guards non-array responses.
Move tiles_armagedon_a1.res to assets/models/meshes/. Fix import fallback_path.
2026-05-22 12:08:11 +08:00

356 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 = {}
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)
_chat_messages.clear()
var history_result = await NakamaManager.client.list_channel_messages_async(NakamaManager.session, _chat_channel.id, 50, 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)
_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 NakamaManager.client.rpc_async(NakamaManager.session, "admin_clear_global_chat", payload)
if rpc_result.is_exception():
push_warning("[Chat] admin_clear_global_chat RPC failed: " + rpc_result.get_exception().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