Merge branch 'develop' of https://git.pthub.vn/Unity/perfect-world-unity into feature/friend_ui

This commit is contained in:
VuNgocHaiC7
2026-03-23 18:00:14 +07:00
10 changed files with 653 additions and 130 deletions
@@ -230,7 +230,6 @@ namespace BrewMonster
base.Tick(dwDeltaTime);
// Spawn fly GFX when entering Flying state / 进入飞行状态时生成飞行特效
if (prevState == GfxSkillEventState.enumWait && m_enumState == GfxSkillEventState.enumFlying)
{
@@ -295,7 +294,7 @@ namespace BrewMonster
protected override void HitTarget(Vector3 vTarget)
{
base.HitTarget(vTarget);
// Only destroy fly GFX if NOT tracing target
// If tracing target, fly GFX will be cleaned up when buff expires
// 只有在不跟踪目标时才销毁飞行特效
@@ -311,10 +310,9 @@ namespace BrewMonster
if (m_flyGfxInstance != null)
{
SkillGfxMan.InstanceSub?.AddTraceTargetGfx(m_flyGfxInstance, 0); // Skill ID not available, use 0
BMLogger.Log($"[TRACE_TARGET_GFX] HitTarget: Added fly GFX to trace target list (m_bTraceTarget=true)");
}
}
SpawnHitGfx(vTarget);
// TODO Phase 2: Special hit effects (rune, critical, nullity)
@@ -356,7 +354,6 @@ namespace BrewMonster
if (m_bTraceTarget)
{
SkillGfxMan.InstanceSub?.AddTraceTargetGfx(m_flyGfxInstance, 0); // Skill ID not available, use 0
BMLogger.Log($"[TRACE_TARGET_GFX] SpawnFlyGfx: Added fly GFX to trace target list (m_bTraceTarget=true)");
}
}
@@ -489,7 +486,7 @@ namespace BrewMonster
DestroyFlyGfx();
}
}
if (m_hitGfxInstance != null)
{
if (SkillGfxMan.InstanceSub != null && !SkillGfxMan.InstanceSub.IsTraceTargetGfx(m_hitGfxInstance))
@@ -499,7 +496,7 @@ namespace BrewMonster
m_hitGfxInstance = null;
}
}
base.Resume();
}
@@ -515,7 +512,7 @@ namespace BrewMonster
// Program integration\\Hit\\Invalid attack hit.gfx
szPath = "程序联入\\击中\\无效攻击击中.gfx";
}
// TODO: return AfxGetGFXExMan()->LoadGfx(pDev, szPath);
return null;
}*/
@@ -526,7 +523,7 @@ namespace BrewMonster
/// </summary>
/*public void SetHitGfx(A3DGFXEx pHitGfx)
{
// if (m_dwModifier & CECAttackEvent::MOD_CRITICAL_STRIKE)
// if (m_dwModifier & CECAttackEvent::MOD_CRITICAL_STRIKE)
// pHitGfx->SetActualScale(pHitGfx->GetScale() * 2.0f);
m_pHitGfx = pHitGfx;
}*/
@@ -1052,8 +1049,6 @@ namespace BrewMonster
{
m_TraceTargetGfxList.Add(gfxInstance);
m_TraceTargetGfxSkillMap[gfxInstance] = skillId;
BMLogger.Log($"[TRACE_TARGET_GFX] Added GFX for skill {skillId}, total tracked: {m_TraceTargetGfxList.Count}");
}
}
@@ -1063,8 +1058,6 @@ namespace BrewMonster
/// </summary>
public void RemoveAllTraceTargetGfx()
{
BMLogger.Log($"[TRACE_TARGET_GFX] Removing {m_TraceTargetGfxList.Count} trace target GFX");
foreach (GameObject gfx in m_TraceTargetGfxList)
{
if (gfx != null)
@@ -1102,11 +1095,6 @@ namespace BrewMonster
m_TraceTargetGfxList.Remove(gfx);
m_TraceTargetGfxSkillMap.Remove(gfx);
}
if (toRemove.Count > 0)
{
BMLogger.Log($"[TRACE_TARGET_GFX] Removed {toRemove.Count} GFX for skill {skillId}");
}
}
/// <summary>
@@ -6,7 +6,7 @@
* CREATED BY: Duyuxin, 2005/1/5
* CONVERTED TO C#: 2025
*
* HISTORY:
* HISTORY:
*
* Copyright (c) 2005 Archosaur Studio, All Rights Reserved.
*/
@@ -271,7 +271,7 @@ namespace BrewMonster
{
m_aShortcuts[iSlot] = null;
}
BMLogger.LogError("SetShortcut iSlot= " + iSlot);
// BMLogger.LogError("SetShortcut iSlot= " + iSlot);
m_aShortcuts[iSlot] = pShortcut;
}
@@ -500,10 +500,10 @@ namespace BrewMonster
// Record shortcut's position and type
data.AddRange(BitConverter.GetBytes(i));
data.AddRange(BitConverter.GetBytes((int)pSC.GetType()));
BMLogger.Log($"[MH] Saving shortcut slot: {i} Type: {pSC.GetType()}");
// TODO: implement other shortcut types
// TODO: implement other shortcut types
switch ((CECShortcut.ShortcutType)pSC.GetType())
{
/* case CECShortcut.ShortcutType.SCT_COMMAND:
@@ -595,7 +595,7 @@ namespace BrewMonster
int iSCType = GPDataTypeHelper.FromBytes<int>(pDataBuf, offset);
offset += sizeof(int);
//BMLogger.Log("[MH] Loading shortcut slot: " + iSlot + " Type: " + iSCType);
switch ((CECShortcut.ShortcutType)iSCType)
@@ -744,7 +744,7 @@ namespace BrewMonster
}
/* default:
//TODO: uncomment
//TODO: uncomment
//BMLogger.LogError("CECShortcutSet::LoadConfigData - Unknown shortcut type");
return false;*/
}
@@ -770,7 +770,7 @@ namespace BrewMonster
}
#endregion
}
#region Placeholder Classes
@@ -1124,11 +1124,11 @@ namespace BrewMonster
public override ShortcutType GetType() => ShortcutType.SCT_ITEM;
public override CECShortcut Clone() => null;
public bool Init(int iIvtr, int iSlot, CECIvtrItem pItem)
{
m_iInventory = iIvtr;
m_iIvtrSlot = iSlot;
return true;
public bool Init(int iIvtr, int iSlot, CECIvtrItem pItem)
{
m_iInventory = iIvtr;
m_iIvtrSlot = iSlot;
return true;
}
public int GetInventory() => m_iInventory;
public int GetIvtrSlot() => m_iIvtrSlot;
@@ -1175,15 +1175,15 @@ namespace BrewMonster
// Placeholder classes for dependencies
public class CECIvtrItem { }
public class CECInventory
{
public CECIvtrItem GetItem(int slot) => null;
public class CECInventory
{
public CECIvtrItem GetItem(int slot) => null;
}
public class CECHostGoblin
{
public CECSkill GetSkillByID(int id) => null;
public class CECHostGoblin
{
public CECSkill GetSkillByID(int id) => null;
}*/
// class CECSCSysModule : public CECShortcut
public class CECSCSysModule : CECShortcut
{
@@ -7,6 +7,8 @@ namespace BrewMonster.PerfectWorld.Scripts.Utility.ChatFilter
{
public class ChatFilter
{
private const string BadWordReplacement = "**";
private HashSet<string> badWordSet = new HashSet<string>();
// =========================
@@ -26,14 +28,16 @@ namespace BrewMonster.PerfectWorld.Scripts.Utility.ChatFilter
// =========================
// NORMALIZE
// =========================
private string NormalizeRuntime(string input, out List<int> map)
private string NormalizeRuntime(string input, out List<int> charMap, out List<(int start, int end)> tokenRanges)
{
map = new List<int>();
charMap = new List<int>();
tokenRanges = new List<(int, int)>();
string formD = input.Normalize(NormalizationForm.FormD);
StringBuilder sb = new StringBuilder();
bool lastWasSpace = false;
bool inToken = false;
int tokenStart = -1;
for (int i = 0; i < formD.Length; i++)
{
@@ -47,22 +51,32 @@ namespace BrewMonster.PerfectWorld.Scripts.Utility.ChatFilter
if (n == '\0')
{
if (!lastWasSpace)
if (inToken)
{
sb.Append(' ');
map.Add(i);
lastWasSpace = true;
tokenRanges.Add((tokenStart, charMap.Count - 1));
inToken = false;
}
sb.Append(' ');
charMap.Add(i);
}
else
{
if (!inToken)
{
tokenStart = charMap.Count;
inToken = true;
}
sb.Append(n);
map.Add(i);
lastWasSpace = false;
charMap.Add(i);
}
}
return sb.ToString().Trim();
if (inToken)
tokenRanges.Add((tokenStart, charMap.Count - 1));
return sb.ToString();
}
private char NormalizeChar(char c)
@@ -138,21 +152,75 @@ namespace BrewMonster.PerfectWorld.Scripts.Utility.ChatFilter
return false;
}
private static List<(int start, int end)> MergeOverlappingSpans(List<(int start, int end)> spans)
{
if (spans == null || spans.Count == 0)
return spans;
spans.Sort((a, b) => a.start.CompareTo(b.start));
var merged = new List<(int start, int end)>(spans.Count);
foreach (var span in spans)
{
if (merged.Count == 0)
{
merged.Add(span);
continue;
}
var last = merged[merged.Count - 1];
if (span.start <= last.end)
merged[merged.Count - 1] = (last.start, Math.Max(last.end, span.end));
else
merged.Add(span);
}
return merged;
}
private static string BuildFilteredString(string input, List<(int start, int end)> mergedSpans, string replacement)
{
if (mergedSpans == null || mergedSpans.Count == 0)
return input;
var sb = new StringBuilder(input.Length);
int last = 0;
foreach (var (s, e) in mergedSpans)
{
if (s > last)
sb.Append(input, last, s - last);
sb.Append(replacement);
last = e + 1;
}
if (last < input.Length)
sb.Append(input, last, input.Length - last);
return sb.ToString();
}
// =========================
// FILTER
// =========================
public string Filter(string input, out bool isValidWord)
{
isValidWord = false;
if (string.IsNullOrEmpty(input))
return input;
isValidWord = true;
List<int> map;
string normalized = NormalizeRuntime(input, out map);
List<int> charMap;
List<(int start, int end)> tokenRanges;
string normalized = NormalizeRuntime(input, out charMap, out tokenRanges);
var tokens = normalized.Split(' ', StringSplitOptions.RemoveEmptyEntries);
char[] result = input.ToCharArray();
var matchSpans = new List<(int start, int end)>();
for (int i = 0; i < tokens.Length; i++)
{
@@ -161,23 +229,27 @@ namespace BrewMonster.PerfectWorld.Scripts.Utility.ChatFilter
int startToken = i;
int endToken = i + len - 1;
int startChar = FindCharIndex(normalized, startToken, map);
int endChar = FindCharIndex(normalized, endToken, map);
if (startChar >= 0 && endChar >= 0)
if (startToken < tokenRanges.Count && endToken < tokenRanges.Count)
{
for (int k = startChar; k <= endChar && k < result.Length; k++)
{
result[k] = '*';
isValidWord = false;
}
int normStart = tokenRanges[startToken].start;
int normEnd = tokenRanges[endToken].end;
int realStart = charMap[normStart];
int realEnd = charMap[normEnd];
matchSpans.Add((realStart, realEnd));
isValidWord = false;
}
i += len - 1;
}
}
return new string(result);
if (matchSpans.Count == 0)
return input;
var merged = MergeOverlappingSpans(matchSpans);
return BuildFilteredString(input, merged, BadWordReplacement);
}
}
}
@@ -47,6 +47,11 @@ namespace BrewMonster.PerfectWorld.Editor.ChatFilter
RunTest();
}
if (GUILayout.Button("Run plan examples", GUILayout.Height(28)))
{
RunPlanExamplesFromCleanWords();
}
if (GUILayout.Button("Clear", GUILayout.Width(72), GUILayout.Height(28)))
{
_input = "";
@@ -78,5 +83,38 @@ namespace BrewMonster.PerfectWorld.Editor.ChatFilter
_hasRun = true;
Repaint();
}
/// <summary>
/// Runs sample inputs that use entries from clean_words.txt (plan: ** replacement, spaces preserved).
/// </summary>
private static void RunPlanExamplesFromCleanWords()
{
ChatFilterService.Init();
var cases = new[]
{
("con chó", "con **", false),
("bitch cho", "** **", false),
("hello world", "hello world", true),
};
int failed = 0;
foreach (var (input, expectedFiltered, expectedValid) in cases)
{
string got = ChatFilterService.Filter(input, out bool isValid);
bool ok = got == expectedFiltered && isValid == expectedValid;
if (!ok)
{
failed++;
Debug.LogWarning(
$"[ChatFilter plan check] FAIL\n in: {input}\n expected: {expectedFiltered} (valid={expectedValid})\n got: {got} (valid={isValid})");
}
else
Debug.Log($"[ChatFilter plan check] OK: \"{input}\" -> \"{got}\"");
}
if (failed == 0)
Debug.Log("[ChatFilter plan check] All examples passed.");
}
}
}
+6 -28
View File
@@ -3,6 +3,7 @@ using BrewMonster.Network;
using BrewMonster.UI;
using System;
using System.Collections.Generic;
using BrewMonster.PerfectWorld.Scripts.Utility.ChatFilter;
using BrewMonster.Scripts.Managers;
using UnityEngine;
using TMPro;
@@ -299,7 +300,7 @@ public class CECUIManager : MonoSingleton<CECUIManager>
return null;
}
public CDlgMessageBox ShowMessageBoxYes(string title, string message, AUIDialog dlg, Action onClickedYes)
{
var msgBox = GetInGameUIMan().GetDialog("DlgMessageBox") as CDlgMessageBox;
@@ -336,7 +337,7 @@ public class CECUIManager : MonoSingleton<CECUIManager>
return null;
}
// public CDlgMessageBox ShowMessageBox(MessageBoxData messageBoxData)
// {
// var msgBox = GetInGameUIMan().GetDialog("DlgMessageBox") as CDlgMessageBox;
@@ -467,7 +468,7 @@ public class CECUIManager : MonoSingleton<CECUIManager>
// g_pGame->GetGameRun()->GetPendingLogOut().AppendForSaveConfig(new CECPendingLogoutCrossServer());
// else
// g_pGame->GetGameRun()->GetPendingLogOut().AppendForSaveConfig(new CECPendingLogoutHalf());
EC_Game.GetGameRun().GetPendingLogOut().AppendForSaveConfig(new CECPendingLogoutHalf());
}
else
@@ -714,7 +715,7 @@ public class CECUIManager : MonoSingleton<CECUIManager>
// {
// EC_Game.GetGameRun().StartGame(0, Vector3.zero);
// }
CECShortcut pSC = EC_Game.GetGameRun().GetPoseCmdShortcuts().GetShortcut(slot);
if (pSC != null) // && pObjSrc->GetDataPtr("ptr_CECShortcut") == pSC
{
@@ -775,29 +776,6 @@ public class CECUIManager : MonoSingleton<CECUIManager>
public void FilterBadWords(ref string str)
{
if (string.IsNullOrEmpty(str))
return;
string strLwr = str.ToLower();
char[] chars = str.ToCharArray();
foreach (string bad in gameUI.m_vecBadWords)
{
int pos = 0;
string badLwr = bad.ToLower();
while ((pos = strLwr.IndexOf(badLwr, pos, StringComparison.Ordinal)) >= 0)
{
for (int j = 0; j < badLwr.Length; j++)
{
chars[pos + j] = '*';
}
pos += badLwr.Length;
}
}
str = new string(chars);
str = ChatFilterService.Filter(str, out var isValidWord);
}
}
+40 -12
View File
@@ -23,6 +23,7 @@ namespace BrewMonster.Scripts.ChatUI
public List<ChannelButtonMapping> channelButtons = new();
private const int MAX_HISTORY = 10;
private ChatChannel m_currentChannel = ChatChannel.GP_CHAT_LOCAL;
private struct ChatMsg
{
@@ -57,6 +58,13 @@ namespace BrewMonster.Scripts.ChatUI
{
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;
}
}
// =====================================================
@@ -79,13 +87,34 @@ namespace BrewMonster.Scripts.ChatUI
}
}
m_currentChannel = channel;
var config = chatSystem.channelIcons.Find(c => c.channel == channel);
if (config.prefix != null)
{
inputField.text = config.prefix;
inputField.ActivateInputField();
inputField.MoveTextEnd(false);
string currentText = inputField.text;
currentText = RemoveKnownPrefix(currentText);
inputField.text = config.prefix + currentText;
}
inputField.ActivateInputField();
inputField.MoveTextEnd(false);
}
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);
}
// Không tự ý remove prefix của Whisper (bắt đầu bằng '/') vì nó đi kèm tên người chơi
return text;
}
private void OnSubmit(string text)
@@ -242,8 +271,8 @@ namespace BrewMonster.Scripts.ChatUI
private ChatChannel ParseAndSendMessage(string text, int nPack, int nSlot)
{
ChatChannel channel;
string pszMsg;
ChatChannel channel = m_currentChannel;
string pszMsg = text;
if (text.StartsWith("!!"))
{
@@ -275,11 +304,7 @@ namespace BrewMonster.Scripts.ChatUI
HandleWhisper(text, nPack, nSlot);
return ChatChannel.GP_CHAT_WHISPER;
}
else
{
channel = ChatChannel.GP_CHAT_LOCAL;
pszMsg = text;
}
// 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;
@@ -363,8 +388,11 @@ namespace BrewMonster.Scripts.ChatUI
{
string strModified = msg;
// 1. Filter bad words
CECUIManager.Instance.FilterBadWords(ref strModified);
if (channel is not ChatChannel.GP_CHAT_MISC)
{
// 1. Filter bad words
CECUIManager.Instance.FilterBadWords(ref strModified);
}
if (string.IsNullOrEmpty(strModified))
return;
+1 -1
View File
@@ -1,3 +1,3 @@
fileFormatVersion: 2
guid: 598459b2399743b5ba1eb55fd1d9611e
guid: 474cab0ba62d0eb45bdc28c5b381eb18
timeCreated: 1762861835
+96
View File
@@ -0,0 +1,96 @@
using System;
using System.Net;
using CSNetwork;
using CSNetwork.GPDataType;
using BrewMonster.Common;
using UnityEngine;
// Account login info struct
public struct AccountLoginInfo
{
public uint login_time;
public uint login_ip;
public uint current_ip;
public void Reset()
{
login_time = 0;
login_ip = 0;
current_ip = 0;
}
}
// Game runtime partial class
partial class CECGameRun
{
public AccountLoginInfo m_AccountLoginInfo = new AccountLoginInfo();
public bool m_bAccountLoginInfoShown = false;
public byte m_accountInfoFlag = 0;
public bool m_bAccountInfoShown = false;
public void ResetAccountLoginInfo()
{
m_AccountLoginInfo.Reset();
m_bAccountLoginInfoShown = true;
}
public void ShowAccountLoginInfo()
{
Debug.Log("[Cuong] ShowAccountLoginInfo");
if (!m_bAccountLoginInfoShown)
{
m_bAccountLoginInfoShown = true;
// Assuming CECUIConfig::Instance().GetGameUI().bEnableShowIP translates to true for now
bool bEnableShowIP = true;
if (bEnableShowIP)
{
// Last login time
DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
DateTime t = Epoch.AddSeconds(m_AccountLoginInfo.login_time).ToLocalTime();
string timeStr = string.Format("({0}-{1:00}-{2:00} {3:00}:{4:00})",
t.Year, t.Month, t.Day, t.Hour, t.Minute);
string textTime = string.Format("Lần đăng nhập trước: {0}", timeStr); // 9343
EventBus.Publish(new GameSession.ChatMessageEvent { context = textTime, channel = (byte)ChatChannel.GP_CHAT_SYSTEM });
Debug.Log($"[Cuong] ShowAccountLoginInfo {textTime}");
// Last login IP
string ipStr = new IPAddress((long)m_AccountLoginInfo.login_ip).ToString();
string textIp = string.Format("IP đăng nhập trước: {0}", ipStr); // 9344
EventBus.Publish(new GameSession.ChatMessageEvent { context = textIp, channel = (byte)ChatChannel.GP_CHAT_SYSTEM });
Debug.Log($"[Cuong] ShowAccountLoginInfo {textIp}");
// Current login IP
string curIpStr = new IPAddress((long)m_AccountLoginInfo.current_ip).ToString();
string textCurIp = string.Format("IP đăng nhập hiện tại: {0}", curIpStr); // 9345
EventBus.Publish(new GameSession.ChatMessageEvent { context = textCurIp, channel = (byte)ChatChannel.GP_CHAT_SYSTEM });
Debug.Log($"[Cuong] ShowAccountLoginInfo {textCurIp}");
}
}
}
public void SetAccountInfoFlag(byte accountinfo_flag)
{
m_accountInfoFlag = accountinfo_flag;
m_bAccountInfoShown = false;
}
public void ShowAccountInfo()
{
if (!m_bAccountInfoShown)
{
m_bAccountInfoShown = true;
bool bEnableCompleteAccount = true;
if (bEnableCompleteAccount && ((m_accountInfoFlag & 0x03) != 0))
{
string text = "Hoàn tất thông tin tài khoản..."; // 9347
EventBus.Publish(new GameSession.ChatMessageEvent { context = text, channel = (byte)ChatChannel.GP_CHAT_SYSTEM });
Debug.Log($"[Cuong] ShowAccountInfo {text}");
}
}
}
}
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 598459b2399743b5ba1eb55fd1d9611e
timeCreated: 1762861835
File diff suppressed because one or more lines are too long