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.
This commit is contained in:
@@ -0,0 +1,355 @@
|
||||
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
|
||||
Reference in New Issue
Block a user