Files
test/Assets/Scripts/ChatInputHandler.cs
T
2026-04-22 16:32:58 +07:00

1138 lines
41 KiB
C#

using System;
using UnityEngine;
using TMPro;
using CSNetwork.GPDataType;
using System.Collections.Generic;
using BrewMonster;
using BrewMonster.Network;
using BrewMonster.Scripts.Chat;
using BrewMonster.Scripts.Chat.EmotionData;
using BrewMonster.Scripts.Managers;
using BrewMonster.UI;
using CSNetwork;
using PerfectWorld.Scripts.Managers;
namespace BrewMonster.Scripts.ChatUI
{
public struct ChatChannelFilterChangedEvent
{
public ChatChannel channel;
public ChatChannelFilterChangedEvent(ChatChannel c) { channel = c; }
}
public struct WhisperPlayerEvent
{
public string playerName;
public WhisperPlayerEvent(string name) { playerName = name; }
}
public class ChatInputHandler : MonoBehaviour
{
public TMP_InputField inputField;
public ChatSystemSO chatSystem;
[Header("Mobile Keyboard Follow")]
[Tooltip("Rect chứa input/chat composer cần neo lên trên keyboard.")]
[SerializeField] private RectTransform inputBarRoot;
[Tooltip("Padding đáy thêm cho vùng safe-area/gesture.")]
[SerializeField] private float keyboardBottomPadding = 0f;
[Tooltip("Bật để input bar bám theo keyboard khi mobile keyboard mở.")]
[SerializeField] private bool followMobileKeyboard = true;
[Header("Typing Preview")]
[Tooltip("Root của box xem trước nội dung đang gõ (nằm trên input bar).")]
[SerializeField] private GameObject typingPreviewRoot;
[Tooltip("Text hiển thị nội dung đang gõ realtime.")]
[SerializeField] private TMP_Text typingPreviewText;
[Tooltip("Rect của preview box để neo theo input bar.")]
[SerializeField] private RectTransform typingPreviewRect;
[Tooltip("Khoảng cách dọc cộng thêm cho preview box.")]
[SerializeField] private float previewVerticalOffset = 8f;
[Header("Emoji")]
[Tooltip("SO ánh xạ emotion → TMP sprite tag. Gán EmotionLibrarySpriteMap đã build từ Emotion Atlas Converter.")]
[SerializeField] EmotionLibrarySpriteMap _spriteMap;
/// <summary>
/// Nội dung tin nhắn theo protocol wire (MarshalEditBoxText) — gửi server; không phải TMP.
/// Message body in wire protocol (MarshalEditBoxText) — sent to server; not TMP.
/// </summary>
private string _chatWireBody = "";
private bool _syncingWireToDisplay;
/// <summary>
/// Bản sao text input lần trước — dùng phát hiện xóa 1 ký tự trong thẻ &lt;sprite&gt;.
/// English: Last input text snapshot — used to detect single-char delete inside a sprite tag.
/// </summary>
private string _lastInputFieldText = "";
private bool _applyingSpriteAwareDelete;
[Serializable]
public struct ChannelButtonMapping
{
public ChatChannel channel;
public UnityEngine.UI.Button button;
}
public List<ChannelButtonMapping> channelButtons = new();
[Header("Channel / whisper dropdown")]
[Tooltip("TMP_Dropdown: 4 kênh (Local/Team/Faction/World) khi không whisper; khi whisper thì hiện MRU tên người. English: TMP_Dropdown — four channels when not whisper; MRU names in whisper.")]
[SerializeField] private TMP_Dropdown recentWhisperDropdown;
/// <summary>Thứ tự kênh trong dropdown (không gồm Whisper). English: Channel order in dropdown (whisper excluded).</summary>
private static readonly ChatChannel[] ChatDropdownChannelOrder =
{
ChatChannel.GP_CHAT_LOCAL,
ChatChannel.GP_CHAT_TEAM,
ChatChannel.GP_CHAT_FACTION,
ChatChannel.GP_CHAT_FARCRY,
};
[Tooltip("Số người whisper gần nhất tối đa trong dropdown. English: Max recent whisper targets in the dropdown.")]
[SerializeField] private int maxRecentWhisperTargets = 5;
/// <summary>Giới hạn MRU whisper cho UX dropdown (tối thiểu 1). English: MRU whisper cap for dropdown UX (minimum 1).</summary>
public int MaxRecentWhisperTargets => Mathf.Max(1, maxRecentWhisperTargets);
/// <summary>Dòng đầu dropdown: không chọn MRU (giữ target đang gõ). English: First row: no MRU pick.</summary>
private const string WhisperDropdownPlaceholder = "\u2014";
private readonly List<string> m_recentWhispers = new();
private bool m_whisperDropdownBulkUpdate;
private const int MAX_HISTORY = 10;
private ChatChannel m_currentChannel = ChatChannel.GP_CHAT_LOCAL;
private string m_whisperTarget = "";
public void SetWhisperTarget(string playerName)
{
if (string.IsNullOrEmpty(playerName)) return;
m_whisperTarget = playerName;
OnCommand_speakmode(ChatChannel.GP_CHAT_WHISPER);
}
private struct ChatMsg
{
public ChatChannel channel;
public float time;
public string msg;
public int pack;
public int slot;
}
private List<ChatMsg> m_vecHistory = new();
private int m_nCurHistory = 0;
private float m_dwTickFarCry = 0;
private float m_dwTickFarCry2 = 0;
private Vector2 _inputBarBaseAnchoredPos;
private Vector2 _typingPreviewBaseAnchoredPos;
private bool _cachedAnchors;
private void Awake()
{
_spriteMap?.EnsureCachedTmpSpriteAssetNames();
EventBus.Subscribe<WhisperPlayerEvent>(OnWhisperPlayerEvent);
}
private void OnDestroy()
{
EventBus.Unsubscribe<WhisperPlayerEvent>(OnWhisperPlayerEvent);
if (inputField != null)
{
inputField.onSubmit.RemoveListener(OnSubmit);
inputField.onValueChanged.RemoveListener(OnInputValueChanged);
}
if (recentWhisperDropdown != null)
recentWhisperDropdown.onValueChanged.RemoveListener(OnChatDropdownValueChanged);
}
private void OnWhisperPlayerEvent(WhisperPlayerEvent e)
{
SetWhisperTarget(e.playerName);
}
private void Start()
{
// PC / Editor: Enter submits. Android: only sendButton → SubmitFromSendButton (no onSubmit).
if (inputField != null && Application.platform != RuntimePlatform.Android)
inputField.onSubmit.AddListener(OnSubmit);
if (inputField != null)
inputField.onValueChanged.AddListener(OnInputValueChanged);
_lastInputFieldText = inputField != null ? (inputField.text ?? "") : "";
WireChatDropdown();
RebuildChatDropdownOptions();
foreach (var mapping in channelButtons)
{
if (mapping.button != null)
{
ChatChannel ch = mapping.channel; // Capture variable for closure
mapping.button.onClick.AddListener(() => OnCommand_speakmode(ch));
}
}
// Select the first button as default if available
if (channelButtons.Count > 0 && channelButtons[0].button != null)
{
OnCommand_speakmode(channelButtons[0].channel);
}
// [Mobile Fix] Giữ cho chữ được hiển thị trực tiếp trên UI của game thay vì bị
// đẩy vào một thanh ngang phụ (Native OS Input) bật lên cùng bàn phím.
if (inputField != null)
{
inputField.shouldHideMobileInput = true;
}
UpdateChatDropdownInteractable();
CacheInitialAnchors();
UpdateTypingPreviewFromInput();
}
private void Update()
{
UpdateKeyboardAnchoredLayout();
}
// =====================================================
// PORT C++: CDlgChat::OnCommand_speakmode
// Sets the chat input prefix based on the selected channel from SO.
// =====================================================
private void OnCommand_speakmode(ChatChannel channel)
{
if (chatSystem == null) return;
// Update button visual states using sprites from ChatSystemSO
foreach (var mapping in channelButtons)
{
if (mapping.button != null && mapping.button.image != null)
{
if (mapping.channel == channel)
mapping.button.image.sprite = chatSystem.selectedSprite;
else
mapping.button.image.sprite = chatSystem.unselectedSprite;
}
}
if (m_currentChannel != channel && channel != ChatChannel.GP_CHAT_WHISPER)
{
m_whisperTarget = "";
}
m_currentChannel = channel;
// Handle System channel input restriction
if (m_currentChannel == ChatChannel.GP_CHAT_SYSTEM)
{
inputField.interactable = false;
inputField.text = "";
}
else
{
inputField.interactable = true;
var config = chatSystem.channelIcons.Find(c => c.channel == channel);
string currentText = inputField.text;
currentText = RemoveKnownPrefix(currentText);
if (channel == ChatChannel.GP_CHAT_WHISPER && !string.IsNullOrEmpty(m_whisperTarget))
{
inputField.text = "/" + m_whisperTarget + " " + currentText;
}
else if (config.prefix != null)
{
inputField.text = config.prefix + currentText;
}
else
{
inputField.text = currentText;
}
inputField.ActivateInputField();
inputField.MoveTextEnd(false);
}
SyncChatWireBodyFromInput();
RebuildChatDropdownOptions();
UpdateChatDropdownInteractable();
EventBus.Publish(new ChatChannelFilterChangedEvent(m_currentChannel));
}
void WireChatDropdown()
{
if (recentWhisperDropdown == null) return;
recentWhisperDropdown.onValueChanged.RemoveListener(OnChatDropdownValueChanged);
recentWhisperDropdown.onValueChanged.AddListener(OnChatDropdownValueChanged);
}
void UpdateChatDropdownInteractable()
{
if (recentWhisperDropdown == null) return;
recentWhisperDropdown.interactable = m_currentChannel != ChatChannel.GP_CHAT_SYSTEM;
}
void OnChatDropdownValueChanged(int index)
{
if (m_whisperDropdownBulkUpdate) return;
if (recentWhisperDropdown == null) return;
if (m_currentChannel == ChatChannel.GP_CHAT_WHISPER)
{
if (index <= 0) return;
int mruIndex = index - 1;
if (mruIndex < 0 || mruIndex >= m_recentWhispers.Count) return;
SetWhisperTarget(m_recentWhispers[mruIndex]);
return;
}
if (index < 0 || index >= ChatDropdownChannelOrder.Length) return;
ChatChannel ch = ChatDropdownChannelOrder[index];
if (ch == m_currentChannel) return;
OnCommand_speakmode(ch);
}
void RebuildChatDropdownOptions()
{
if (recentWhisperDropdown == null) return;
m_whisperDropdownBulkUpdate = true;
recentWhisperDropdown.ClearOptions();
if (m_currentChannel == ChatChannel.GP_CHAT_WHISPER)
{
var whisperOpts = new List<TMP_Dropdown.OptionData>
{
new TMP_Dropdown.OptionData(WhisperDropdownPlaceholder)
};
foreach (var name in m_recentWhispers)
whisperOpts.Add(new TMP_Dropdown.OptionData(name));
recentWhisperDropdown.AddOptions(whisperOpts);
SyncWhisperMruDropdownSelection();
}
else
{
var channelOpts = new List<TMP_Dropdown.OptionData>();
foreach (var ch in ChatDropdownChannelOrder)
channelOpts.Add(new TMP_Dropdown.OptionData(GetChannelDropdownLabel(ch)));
recentWhisperDropdown.AddOptions(channelOpts);
SyncPublicChannelDropdownSelection();
}
m_whisperDropdownBulkUpdate = false;
}
void SyncPublicChannelDropdownSelection()
{
if (recentWhisperDropdown == null || recentWhisperDropdown.options.Count == 0) return;
int idx = 0;
for (int i = 0; i < ChatDropdownChannelOrder.Length; i++)
{
if (ChatDropdownChannelOrder[i] != m_currentChannel) continue;
idx = i;
break;
}
int max = recentWhisperDropdown.options.Count - 1;
recentWhisperDropdown.SetValueWithoutNotify(Mathf.Clamp(idx, 0, max));
}
void SyncWhisperMruDropdownSelection()
{
if (recentWhisperDropdown == null) return;
int idx = 0;
if (!string.IsNullOrEmpty(m_whisperTarget))
{
int inMru = m_recentWhispers.IndexOf(m_whisperTarget);
if (inMru >= 0)
idx = inMru + 1;
}
int max = recentWhisperDropdown.options.Count - 1;
if (max < 0) return;
recentWhisperDropdown.SetValueWithoutNotify(Mathf.Clamp(idx, 0, max));
}
void RecordRecentWhisper(string playerName)
{
if (string.IsNullOrEmpty(playerName)) return;
int cap = MaxRecentWhisperTargets;
m_recentWhispers.Remove(playerName);
m_recentWhispers.Insert(0, playerName);
while (m_recentWhispers.Count > cap)
m_recentWhispers.RemoveAt(m_recentWhispers.Count - 1);
if (m_currentChannel == ChatChannel.GP_CHAT_WHISPER)
RebuildChatDropdownOptions();
}
private string RemoveKnownPrefix(string text)
{
if (string.IsNullOrEmpty(text)) return text;
if (text.StartsWith("!!") || text.StartsWith("!~") ||
text.StartsWith("!@") || text.StartsWith("!#"))
{
return text.Substring(2);
}
else if (text.StartsWith("$"))
{
return text.Substring(1);
}
else if (text.StartsWith("/"))
{
int spaceIndex = text.IndexOf(' ');
if (spaceIndex > 0)
{
return text.Substring(spaceIndex + 1);
}
return "";
}
return text;
}
private void OnSubmit(string text)
{
if (string.IsNullOrWhiteSpace(text))
return;
OnCommand_speak(text);
}
/// <summary>
/// Gọi từ nút Send trên UI. Trên Android đây là cách gửi duy nhất (không dùng Enter).
/// </summary>
public void SubmitFromSendButton()
{
if (inputField == null) return;
OnSubmit(inputField.text);
}
// =====================================================
// PORT C++: CDlgChat::OnCommand_speak
// =====================================================
private void OnCommand_speak(string text)
{
SyncChatWireBodyFromInput();
string routingLine = (text ?? "").Trim();
string strText = _chatWireBody?.Trim() ?? "";
if (strText.Length <= 0)
{
ClearTextInInputField();
//ChangeFocus();
return;
}
int nPack = -1;
int nSlot = -1;
FilterBadWords(ref strText);
_chatWireBody = strText;
if (!CanUseRawCharacters(0))
return;
if (HandleDebugCommand(routingLine))
return;
float now = Time.time;
if (!CheckFarCryRequirement(routingLine, now))
return;
if (!CheckSuperFarCryRequirement(routingLine, now))
return;
if (!CheckSpamProtection(strText, now))
return;
ChatChannel resolvedChannel = ParseAndSendMessage(routingLine, nPack, nSlot);
if (resolvedChannel == ChatChannel.GP_CHAT_FARCRY && routingLine.StartsWith("!@"))
m_dwTickFarCry = now;
if (resolvedChannel == ChatChannel.GP_CHAT_SUPERFARCRY && routingLine.StartsWith("!#"))
m_dwTickFarCry2 = now;
SaveHistory(resolvedChannel, strText, nPack, nSlot, now);
ClearTextInInputField();
//ChangeFocus();
}
// =====================================================
// DEBUG COMMAND
// =====================================================
private bool HandleDebugCommand(string text)
{
if (text == "##debug")
{
Debug.Log("Toggle debug console");
return true;
}
return false;
}
// =====================================================
// FARCRY CHECK (!@)
// =====================================================
private bool CheckFarCryRequirement(string text, float now)
{
if (text.StartsWith("!@"))
{
int itemNum = GetPlayerItemCount(12979) + GetPlayerItemCount(36092);
if (itemNum < 1 || GetPlayerLevel() < 5)
{
AddChatMessage(CECUIManager.Instance.GameUI.GetStringFromTable(731), ChatChannel.GP_CHAT_MISC);
return false;
}
if (now - m_dwTickFarCry <= 1f)
{
AddChatMessage(CECUIManager.Instance.GameUI.GetStringFromTable(730), ChatChannel.GP_CHAT_MISC);
return false;
}
}
return true;
}
// =====================================================
// SUPER FARCRY (!#)
// =====================================================
private bool CheckSuperFarCryRequirement(string text, float now)
{
if (text.StartsWith("!#"))
{
int itemNum = GetPlayerItemCount(27728) + GetPlayerItemCount(27729);
if (itemNum < 1)
{
AddChatMessage(CECUIManager.Instance.GameUI.GetStringFromTable(8531), ChatChannel.GP_CHAT_MISC);
return false;
}
if (now - m_dwTickFarCry2 <= 1f)
{
AddChatMessage(CECUIManager.Instance.GameUI.GetStringFromTable(8530), ChatChannel.GP_CHAT_MISC);
return false;
}
}
return true;
}
// =====================================================
// SPAM PROTECTION
// =====================================================
private bool CheckSpamProtection(string text, float now)
{
if (m_vecHistory.Count == 0)
return true;
var last = m_vecHistory[m_vecHistory.Count - 1];
if (now - last.time <= 1f)
{
AddChatMessage(CECUIManager.Instance.GameUI.GetStringFromTable(272), ChatChannel.GP_CHAT_MISC);
return false;
}
foreach (var cm in m_vecHistory)
{
if (cm.channel != ChatChannel.GP_CHAT_WHISPER &&
cm.msg == text &&
now - cm.time <= 6f)
{
AddChatMessage(CECUIManager.Instance.GameUI.GetStringFromTable(273), ChatChannel.GP_CHAT_MISC);
return false;
}
}
return true;
}
// =====================================================
// PARSE MESSAGE PREFIX
// =====================================================
private ChatChannel ParseAndSendMessage(string text, int nPack, int nSlot)
{
ChatChannel channel = m_currentChannel;
string pszMsg = _chatWireBody;
if (text.StartsWith("!!"))
{
channel = ChatChannel.GP_CHAT_TEAM;
}
else if (text.StartsWith("!~"))
{
channel = ChatChannel.GP_CHAT_FACTION;
}
else if (text.StartsWith("!@"))
{
channel = ChatChannel.GP_CHAT_FARCRY;
}
else if (text.StartsWith("!#"))
{
channel = ChatChannel.GP_CHAT_SUPERFARCRY;
}
else if (text.StartsWith("$"))
{
channel = ChatChannel.GP_CHAT_TRADE;
}
else if (text.StartsWith("/"))
{
HandleWhisper(text, nPack, nSlot);
return ChatChannel.GP_CHAT_WHISPER;
}
else if (channel == ChatChannel.GP_CHAT_WHISPER)
{
// Cho phép chat kênh Whisper mượt mà mà không bắt buộc gõ ký tự "/" ở đầu
if (!string.IsNullOrEmpty(m_whisperTarget))
{
HandleWhisper("/" + m_whisperTarget + " " + text, nPack, nSlot);
}
else
{
HandleWhisper("/" + text, nPack, nSlot);
}
return ChatChannel.GP_CHAT_WHISPER;
}
// Không gõ prefix thủ công thì sẽ dùng m_currentChannel đã được gán ở đầu hàm
SendChat(channel, pszMsg, nPack, nSlot);
return channel;
}
// =====================================================
// WHISPER
// =====================================================
private void HandleWhisper(string text, int nPack, int nSlot)
{
string cmd = text.Substring(1);
int spaceIndex = cmd.IndexOf(' ');
if (spaceIndex <= 0)
{
AddChatMessage(CECUIManager.Instance.GameUI.GetStringFromTable(234), ChatChannel.GP_CHAT_MISC);
return;
}
string player = cmd.Substring(0, spaceIndex);
m_whisperTarget = player;
SendPrivateChat(player, _chatWireBody, nPack, nSlot);
}
// =====================================================
// SEND CHAT
// =====================================================
private void SendChat(ChatChannel channel, string msg, int pack, int slot)
{
UnityGameSession.SendChatData((byte)channel, msg, pack, slot);
}
private void SendPrivateChat(string target, string msg, int pack, int slot)
{
BMLogger.Log($"[Cuong] Whisper to {target}: {msg}");
// [Port] C++: CDlgChat::OnCommand_speak → privatechat branch
// Gửi tin nhắn mật (whisper) lên server qua GameSession.
// Tra cứu ID của người nhận từ danh sách bạn bè (giống C++: pFriend ? pFriend->id : -1)
int idTarget = GetCharacterIdByName(target);
UnityGameSession.Instance.GameSession.SendPrivateChatData(target, msg, 0, idTarget, pack, slot);
// [Port] C++: DlgChat.cpp dòng 1531-1532
// Server KHÔNG echo whisper lại cho người gửi → phải hiển thị local.
// C++ dùng: a_sprintf(szMsg, GetStringFromTable(233), szName, szText)
// → format "对&Name&说:msg" — dấu & được thêm bởi FORMAT STRING, không phải bởi caller.
// C# dùng FIXMSG_PRIVATECHAT2 tương ứng; fallback nếu format string lỗi.
// QUAN TRỌNG: chỉ truyền target (không có & bao quanh) vào string.Format.
// Nếu format string đã có &{0}& thì kết quả đúng là "&target&".
// Nếu format string không có & thì AddChatMessage cũng chỉ wrap màu bình thường.
// Tránh truyền "&target&" vì kết quả sẽ là "&&target&&" → regex chỉ match "target"
// nhưng để lại & dư ở ngoài → UI hiển thị "&target&" thay vì link sạch.
CECStringTab pStrTab = EC_Game.GetFixedMsgs();
string fmt = AUIDialog.FormatPrintf(pStrTab.GetWideString((int)FixedMsg.FIXMSG_PRIVATECHAT2));
string localMsg;
try
{
localMsg = string.Format(fmt, target, _chatWireBody);
}
catch
{
// Fallback: dùng &target& để AddChatMessage tạo link — format không có & thì mình thêm
localMsg = $"&{target}&: {_chatWireBody}";
}
CECGameUIMan pGameUI = EC_Game.GetGameRun()?.GetUIManager()?.GetInGameUIMan();
if (pGameUI != null)
{
// idTarget là ID người NHẬN (hoặc -1 nếu không tìm thấy trong friend list)
// AddChatMessage sẽ convert &target& thành TMP link tag có dạng idTarget|target
pGameUI.AddChatMessage(localMsg, ChatChannel.GP_CHAT_WHISPER, idTarget, null, 0, 0, null, _chatWireBody);
}
RecordRecentWhisper(target);
}
// [Port] C++: pFriendMan->GetFriendByName(name) → pFriend->id
// Tìm character ID của người chơi theo tên từ friend list.
// Trả về -1 nếu không tìm thấy (giống C++ trả về idFriend = -1).
private int GetCharacterIdByName(string name)
{
if (string.IsNullOrEmpty(name)) return -1;
var host = EC_Game.GetGameRun()?.GetHostPlayer();
if (host == null) return -1;
var friendMan = host.GetFriendMan();
if (friendMan == null) return -1;
var friend = friendMan.GetFriendByName(name);
return friend.HasValue ? friend.Value.Id : -1;
}
// =====================================================
// HISTORY
// =====================================================
private void SaveHistory(ChatChannel channel, string msg, int pack, int slot, float now)
{
if (m_vecHistory.Count >= MAX_HISTORY)
m_vecHistory.RemoveAt(0);
m_vecHistory.Add(new ChatMsg
{
channel = channel,
msg = msg,
pack = pack,
slot = slot,
time = now
});
m_nCurHistory = m_vecHistory.Count;
}
// =====================================================
// UTILITIES
// =====================================================
private void FilterBadWords(ref string text)
{
CECUIManager.Instance.FilterBadWords(ref text);
}
private void AddChatMessage(string msg, ChatChannel channel, int idPlayer = -1, string pszPlayer = "", byte byFlag = 0)
{
string strModified = msg;
if (channel is not ChatChannel.GP_CHAT_MISC)
{
// 1. Filter bad words
CECUIManager.Instance.FilterBadWords(ref strModified);
}
if (string.IsNullOrEmpty(strModified))
return;
// 2. Blacklist check (Placeholder for porting)
if (IsPlayerBlacklisted(idPlayer))
return;
// 3. Flag handling (Friend chat routing)
if (byFlag == 1) // CHANNEL_FRIEND equivalent
{
// AddFriendMessage(strModified, idPlayer, pszPlayer, ...);
// return;
}
// 4. Formatting with Rich Text
string colorHex = GetChannelColorHex(channel);
string prefix = GetChannelPrefix(channel);
string sender = string.IsNullOrEmpty(pszPlayer) ? "" : $"{pszPlayer}: ";
string finalMsg = $"<color=#{colorHex}>{prefix}{sender}{strModified}</color>";
// 5. Publish event
EventBus.Publish(new GameSession.ChatMessageEvent(finalMsg, (byte)channel));
}
private string GetChannelColorHex(ChatChannel channel)
{
// [Port] C++: CDlgChat::m_pszColor[] — DlgChat.cpp dòng 120-136
// Giữ đúng màu gốc cho các kênh dùng trong local error messages
return channel switch
{
ChatChannel.GP_CHAT_LOCAL => "FFFFFF",
ChatChannel.GP_CHAT_FARCRY => "FFE400",
ChatChannel.GP_CHAT_TEAM => "00FF00",
ChatChannel.GP_CHAT_FACTION => "00FFFC",
ChatChannel.GP_CHAT_WHISPER => "0065FE", // C++: "^0065FE" — KHÔNG phải FF00FF
ChatChannel.GP_CHAT_TRADE => "FF742E",
ChatChannel.GP_CHAT_SYSTEM => "BED293",
ChatChannel.GP_CHAT_BROADCAST => "FF3600",
ChatChannel.GP_CHAT_MISC => "9AA6FF",
ChatChannel.GP_CHAT_SUPERFARCRY => "ff9b3e",
ChatChannel.GP_CHAT_BATTLE => "FFFFFF",
ChatChannel.GP_CHAT_COUNTRY => "FFFFFF",
_ => "FFFFFF"
};
}
private string GetChannelPrefix(ChatChannel channel)
{
return channel switch
{
ChatChannel.GP_CHAT_TEAM => "[Team] ",
ChatChannel.GP_CHAT_FACTION => "[Faction] ",
ChatChannel.GP_CHAT_FARCRY => "[World] ",
ChatChannel.GP_CHAT_WHISPER => "[Whisper] ",
_ => ""
};
}
/// <summary>Nhãn hiển thị trên dropdown cho từng kênh công khai. English: Dropdown caption per public channel.</summary>
private string GetChannelDropdownLabel(ChatChannel channel)
{
return channel switch
{
ChatChannel.GP_CHAT_LOCAL => "Tự do",
ChatChannel.GP_CHAT_TEAM => "Đội",
ChatChannel.GP_CHAT_FACTION => "Bang",
ChatChannel.GP_CHAT_FARCRY => "Thế giới",
_ => channel.ToString()
};
}
private bool IsPlayerBlacklisted(int idPlayer) => false; // Placeholder
private void ClearTextInInputField()
{
_chatWireBody = "";
if (m_currentChannel == ChatChannel.GP_CHAT_WHISPER && !string.IsNullOrEmpty(m_whisperTarget))
{
inputField.text = "/" + m_whisperTarget + " ";
}
else
{
inputField.text = "";
}
UpdateTypingPreviewFromInput();
}
private int GetEmotionSetForCurrentChannel()
{
return m_currentChannel == ChatChannel.GP_CHAT_SUPERFARCRY ? GameSession.SUPER_FAR_CRY_EMOTION_SET : 0;
}
private string BuildVisualPrefixForCurrentChannel()
{
if (chatSystem == null)
return "";
if (m_currentChannel == ChatChannel.GP_CHAT_WHISPER && !string.IsNullOrEmpty(m_whisperTarget))
return "/" + m_whisperTarget + " ";
var cfg = chatSystem.channelIcons.Find(c => c.channel == m_currentChannel);
return cfg.prefix ?? "";
}
/// <summary>
/// Lấy phần thân (TMP) sau prefix kênh / whisper — English: TMP body after channel or whisper prefix.
/// </summary>
private string ExtractMessageBodyFromVisual(string full)
{
if (string.IsNullOrEmpty(full))
return "";
if (full.StartsWith("/"))
{
int sp = full.IndexOf(' ');
if (sp > 0 && sp + 1 < full.Length)
return full.Substring(sp + 1);
return "";
}
string t = RemoveKnownPrefix(full);
if (chatSystem != null)
{
var cfg = chatSystem.channelIcons.Find(c => c.channel == m_currentChannel);
if (cfg.prefix != null && cfg.prefix.Length > 0 && t.StartsWith(cfg.prefix))
t = t.Substring(cfg.prefix.Length);
}
return t;
}
private void SyncChatWireBodyFromInput()
{
if (inputField == null)
return;
string tmpBody = ExtractMessageBodyFromVisual(inputField.text ?? "");
if (_spriteMap == null)
{
_chatWireBody = tmpBody;
return;
}
_chatWireBody = ChatWireTmpCodec.TmpBodyToWire(tmpBody, _spriteMap);
}
private void OnInputValueChanged(string newText)
{
newText ??= "";
if (_syncingWireToDisplay)
{
_lastInputFieldText = newText;
UpdateTypingPreviewFromInput();
return;
}
if (_applyingSpriteAwareDelete)
{
_lastInputFieldText = newText;
SyncChatWireBodyFromInput();
UpdateTypingPreviewFromInput();
return;
}
string prev = _lastInputFieldText ?? "";
if (TryApplySpriteAwareSingleDelete(prev, newText, out string fixedText, out int caretRestore))
{
_applyingSpriteAwareDelete = true;
try
{
inputField.text = fixedText;
}
finally
{
_applyingSpriteAwareDelete = false;
}
int clampedCaret = Mathf.Clamp(caretRestore, 0, fixedText.Length);
inputField.caretPosition = clampedCaret;
inputField.ForceLabelUpdate();
_lastInputFieldText = fixedText;
SyncChatWireBodyFromInput();
UpdateTypingPreviewFromInput();
return;
}
_lastInputFieldText = newText;
SyncChatWireBodyFromInput();
UpdateTypingPreviewFromInput();
}
/// <summary>
/// Xóa Backspace/Delete từng ký tự trong thẻ TMP &lt;sprite&gt; → gỡ cả thẻ (một emoji).
/// English: Single-char delete inside a TMP sprite tag removes the whole tag (one in-game emoji).
/// </summary>
private static bool TryGetSingleCharDeleteRemovedIndex(string prev, string curr, out int removedIndex)
{
removedIndex = -1;
if (prev == null)
prev = "";
if (curr == null)
curr = "";
if (prev.Length != curr.Length + 1)
return false;
int i = 0;
for (; i < curr.Length; i++)
{
if (prev[i] != curr[i])
{
removedIndex = i;
return string.CompareOrdinal(prev, i + 1, curr, i, curr.Length - i) == 0;
}
}
removedIndex = prev.Length - 1;
return true;
}
private bool TryApplySpriteAwareSingleDelete(string prev, string curr, out string fixedText, out int caretRestore)
{
fixedText = null;
caretRestore = 0;
if (inputField == null || !TryGetSingleCharDeleteRemovedIndex(prev, curr, out int removedIndex))
return false;
string prefix = BuildVisualPrefixForCurrentChannel();
if (removedIndex < prefix.Length)
return false;
if (!ChatWireTmpCodec.TryGetSpriteTagRangeContainingCharacterIndex(prev, removedIndex, out int tagStart, out int tagEnd))
return false;
fixedText = prev.Substring(0, tagStart) + prev.Substring(tagEnd);
caretRestore = tagStart;
return true;
}
private void RefreshInputDisplayFromWire()
{
if (inputField == null)
return;
_syncingWireToDisplay = true;
try
{
string prefix = BuildVisualPrefixForCurrentChannel();
string tmpBody = ChatWireTmpCodec.WireBodyToTmpForDisplay(_chatWireBody, _spriteMap, GetEmotionSetForCurrentChannel());
inputField.text = prefix + tmpBody;
inputField.MoveTextEnd(false);
}
finally
{
_syncingWireToDisplay = false;
}
}
private void ChangeFocus()
{
inputField.ActivateInputField();
}
void CacheInitialAnchors()
{
if (_cachedAnchors) return;
if (inputBarRoot != null)
_inputBarBaseAnchoredPos = inputBarRoot.anchoredPosition;
if (typingPreviewRect != null)
_typingPreviewBaseAnchoredPos = typingPreviewRect.anchoredPosition;
_cachedAnchors = true;
}
void UpdateKeyboardAnchoredLayout()
{
if (!followMobileKeyboard)
return;
CacheInitialAnchors();
float keyboardHeight = GetVisibleKeyboardHeight();
float yOffset = keyboardHeight + keyboardBottomPadding;
if (inputBarRoot != null)
{
var inputPos = _inputBarBaseAnchoredPos;
inputPos.y += yOffset;
inputBarRoot.anchoredPosition = inputPos;
}
if (typingPreviewRect != null)
{
var previewPos = _typingPreviewBaseAnchoredPos;
previewPos.y += yOffset + previewVerticalOffset;
typingPreviewRect.anchoredPosition = previewPos;
}
}
float GetVisibleKeyboardHeight()
{
if (!TouchScreenKeyboard.visible)
return 0f;
Rect area = TouchScreenKeyboard.area;
return area.height > 0f ? area.height : 0f;
}
void UpdateTypingPreviewFromInput()
{
if (typingPreviewRoot == null || typingPreviewText == null || inputField == null)
return;
string body = ExtractMessageBodyFromVisual(inputField.text ?? "")?.Trim() ?? "";
bool shouldShow = inputField.isFocused && !string.IsNullOrEmpty(body);
typingPreviewRoot.SetActive(shouldShow);
if (shouldShow)
typingPreviewText.text = body;
}
/// <summary>
/// C++: GetHostPlayer()->GetPack()->GetItemTotalNum(id) — đếm túi chính.
/// </summary>
private int GetPlayerItemCount(int templateId)
{
var host = EC_Game.GetGameRun()?.GetHostPlayer();
EC_Inventory pack = host?.GetPack();
return pack?.GetItemTotalNum(templateId) ?? 0;
}
private int GetPlayerLevel()
{
return EC_Game.GetGameRun()?.GetHostPlayer()?.GetBasicProps().iLevel ?? 0;
}
// =====================================================
// EMOJI INSERT
// =====================================================
/// <summary>
/// Thêm emotion vào buffer wire rồi refresh TMP — gửi server đúng protocol.
/// Append emotion to wire buffer then refresh TMP — server protocol preserved.
/// </summary>
public void AppendEmotionWire(int emotionSet, int emotionIndex)
{
string segment = ChatWireTmpCodec.BuildMarshaledEmotionWire(emotionSet, emotionIndex);
if (string.IsNullOrEmpty(segment))
return;
int reserveChars = 0;
if (_spriteMap != null && EmotionTMPTagBuilder.TryBuildEmotionTag(_spriteMap, emotionSet, emotionIndex, out string tag))
reserveChars = tag.Length;
if (!CanUseRawCharacters(reserveChars))
return;
_chatWireBody += segment;
RefreshInputDisplayFromWire();
}
/// <summary>
/// Chèn TMP emotion tag vào inputField tại vị trí caret hiện tại.
/// Inserts the TMP emotion tag into the inputField at the current caret position.
/// Gọi từ EmojiPickerUI khi người chơi bấm chọn emoji.
/// Called from EmojiPickerUI when the player selects an emoji.
/// </summary>
public void InsertEmoji(int emotionSet, int emotionIndex)
{
AppendEmotionWire(emotionSet, emotionIndex);
}
private bool CanUseRawCharacters(int reserveChars)
{
if (chatSystem == null)
return true;
string rawBody = ExtractMessageBodyFromVisual(inputField != null ? inputField.text ?? "" : "");
if (chatSystem.CanUseRawCharacters(rawBody, reserveChars, out int usedChars, out int limitChars))
return true;
//AddChatMessage($"Tin nhan vuot gioi han: {usedChars}/{limitChars} ky tu.", ChatChannel.GP_CHAT_MISC);
return false;
}
}
}