diff --git a/extern/include/utf8.h b/extern/include/utf8.h index 1ed5eef..84f50d2 100644 --- a/extern/include/utf8.h +++ b/extern/include/utf8.h @@ -22,9 +22,11 @@ constexpr size_t ARABIC_SHAPING_EXPANSION_FACTOR_RETRY = 4; constexpr size_t ARABIC_SHAPING_SAFETY_MARGIN_RETRY = 64; // ============================================================================ -// DEBUG LOGGING (Uncomment to enable BiDi debugging) +// DEBUG LOGGING (Only enabled in Debug builds) // ============================================================================ -// #define DEBUG_BIDI +#ifdef _DEBUG + #define DEBUG_BIDI // Enabled in debug builds for diagnostics +#endif #ifdef DEBUG_BIDI #include @@ -242,8 +244,9 @@ static inline bool IsNameTokenPunct(wchar_t ch) case L'\\': case L'(': case L')': - case L'[': - case L']': + // Brackets are handled specially - see GetCharDirSmart + // case L'[': + // case L']': case L'{': case L'}': case L'<': @@ -264,16 +267,44 @@ static inline bool IsStrongLTR(wchar_t ch) static inline bool HasStrongLTRNeighbor(const wchar_t* s, int n, int i) { - // Remove null/size check (caller guarantees validity) - // Early exit after first strong neighbor found + // Skip neutral characters (spaces, punctuation) to find nearest strong character + // This fixes mixed-direction text like "english + arabic" - // Check previous character - if (i > 0 && IsStrongLTR(s[i - 1])) - return true; + // Search backwards for strong character (skip neutrals/whitespace) + for (int j = i - 1; j >= 0; --j) + { + wchar_t ch = s[j]; - // Check next character - if (i + 1 < n && IsStrongLTR(s[i + 1])) - return true; + // Skip spaces and common neutral punctuation + if (ch == L' ' || ch == L'\t' || ch == L'\n') + continue; + + // Found strong LTR + if (IsStrongLTR(ch)) + return true; + + // Found strong RTL or other strong character + if (IsRTLCodepoint(ch) || IsStrongAlpha(ch)) + break; + } + + // Search forwards for strong character (skip neutrals/whitespace) + for (int j = i + 1; j < n; ++j) + { + wchar_t ch = s[j]; + + // Skip spaces and common neutral punctuation + if (ch == L' ' || ch == L'\t' || ch == L'\n') + continue; + + // Found strong LTR + if (IsStrongLTR(ch)) + return true; + + // Found strong RTL or other strong character + if (IsRTLCodepoint(ch) || IsStrongAlpha(ch)) + break; + } return false; } @@ -302,6 +333,138 @@ static inline ECharDir GetCharDirSmart(const wchar_t* s, int n, int i) if (IsStrongLTR(ch)) return ECharDir::LTR; + // Parentheses: always LTR to keep them with their content + if (ch == L'(' || ch == L')') + return ECharDir::LTR; + + // Common punctuation: treat as strong LTR to prevent jumping around in mixed text + // This makes "Hello + اختبار" and "اختبار + Hello" both keep punctuation in place + if (ch == L'+' || ch == L'-' || ch == L'=' || ch == L'*' || ch == L'/' || + ch == L'<' || ch == L'>' || ch == L'&' || ch == L'|' || ch == L'@' || ch == L'#') + return ECharDir::LTR; + + // Percentage sign: attach to numbers (scan nearby for digits/minus/plus) + // Handles: "%20", "20%", "-6%", "%d%%", etc. + if (ch == L'%') + { + // Look backward for digit, %, -, or + + for (int j = i - 1; j >= 0 && (i - j) < 5; --j) + { + wchar_t prev = s[j]; + if (IsDigit(prev) || prev == L'%' || prev == L'-' || prev == L'+') + return ECharDir::LTR; + if (prev != L' ' && prev != L'\t') + break; // Stop if we hit non-numeric character + } + // Look forward for digit, %, -, or + + for (int j = i + 1; j < n && (j - i) < 5; ++j) + { + wchar_t next = s[j]; + if (IsDigit(next) || next == L'%' || next == L'-' || next == L'+') + return ECharDir::LTR; + if (next != L' ' && next != L'\t') + break; // Stop if we hit non-numeric character + } + return ECharDir::Neutral; + } + + // Minus/dash: attach to numbers (scan nearby for digits/%) + // Handles: "-6", "5-10", "-6%%", etc. + if (ch == L'-') + { + // Look backward for digit or % + for (int j = i - 1; j >= 0 && (i - j) < 3; --j) + { + wchar_t prev = s[j]; + if (IsDigit(prev) || prev == L'%') + return ECharDir::LTR; + if (prev != L' ' && prev != L'\t') + break; + } + // Look forward for digit or % + for (int j = i + 1; j < n && (j - i) < 3; ++j) + { + wchar_t next = s[j]; + if (IsDigit(next) || next == L'%') + return ECharDir::LTR; + if (next != L' ' && next != L'\t') + break; + } + return ECharDir::Neutral; + } + + // Colon: attach to preceding text direction + // Look backward to find strong character + if (ch == L':') + { + for (int j = i - 1; j >= 0; --j) + { + if (s[j] == L' ' || s[j] == L'\t') + continue; // Skip spaces + if (IsRTLCodepoint(s[j])) + return ECharDir::RTL; // Attach to RTL text + if (IsStrongLTR(s[j])) + return ECharDir::LTR; // Attach to LTR text + } + return ECharDir::Neutral; + } + + // Enhancement marker: '+' followed by digit(s) should attach to preceding text + // If preceded by RTL, treat as RTL to keep "+9" with the item name + // Otherwise treat as LTR + if (ch == L'+' && i + 1 < n && IsDigit(s[i + 1])) + { + // Look backward for the last strong character + for (int j = i - 1; j >= 0; --j) + { + if (IsRTLCodepoint(s[j])) + return ECharDir::RTL; // Attach to preceding RTL text + if (IsStrongLTR(s[j])) + return ECharDir::LTR; // Attach to preceding LTR text + // Skip neutral characters + } + return ECharDir::LTR; // Default to LTR if no strong character found + } + + // Brackets: always attach to the content inside them + // This fixes hyperlinks like "[درع فولاذي أسود+9]" + if (ch == L'[' || ch == L']') + { + // Opening bracket '[': look forward for strong character + if (ch == L'[') + { + for (int j = i + 1; j < n; ++j) + { + wchar_t next = s[j]; + if (next == L']') break; // End of bracket content + if (IsRTLCodepoint(next)) return ECharDir::RTL; + if (IsStrongLTR(next)) return ECharDir::LTR; + } + } + // Closing bracket ']': look backward for strong character + else if (ch == L']') + { + for (int j = i - 1; j >= 0; --j) + { + wchar_t prev = s[j]; + if (prev == L'[') break; // Start of bracket content + if (IsRTLCodepoint(prev)) return ECharDir::RTL; + if (IsStrongLTR(prev)) return ECharDir::LTR; + } + } + // If we can't determine, treat as neutral + return ECharDir::Neutral; + } + + // Spaces should attach to adjacent strong characters to avoid fragmentation + // This fixes "english + arabic" by keeping " + " with "english" + if (ch == L' ' || ch == L'\t') + { + if (HasStrongLTRNeighbor(s, n, i)) + return ECharDir::LTR; + // Note: We don't check for RTL neighbor because ResolveNeutralDir handles that + } + // Name-token punctuation: if adjacent to LTR, treat as LTR to keep token intact if (IsNameTokenPunct(ch) && HasStrongLTRNeighbor(s, n, i)) return ECharDir::LTR; @@ -318,10 +481,11 @@ struct TStrongDirCache TStrongDirCache(const wchar_t* s, int n, EBidiDir base) : nextStrong(n), baseDir(base) { // Build reverse lookup: scan from end to beginning + // Use GetCharDirSmart for context-aware character classification EBidiDir lastSeen = baseDir; for (int i = n - 1; i >= 0; --i) { - ECharDir cd = GetCharDir(s[i]); + ECharDir cd = GetCharDirSmart(s, n, i); if (cd == ECharDir::LTR) lastSeen = EBidiDir::LTR; else if (cd == ECharDir::RTL) @@ -397,66 +561,6 @@ static std::vector BuildVisualBidiText_Tagless(const wchar_t* s, int n, if (!s || n <= 0) return {}; - // Detect chat format "name : msg" and extract components - int chatSepPos = -1; - for (int i = 0; i < n - 2; ++i) - { - if (s[i] == L' ' && s[i + 1] == L':' && s[i + 2] == L' ') - { - chatSepPos = i; - break; - } - } - - // If chat format detected, process name and message separately - if (chatSepPos > 0 && forceRTL) - { - // Use pointers instead of copying (zero-copy optimization) - const wchar_t* name = s; - const int nameLen = chatSepPos; - - const int msgStart = chatSepPos + 3; - const wchar_t* msg = s + msgStart; - const int msgLen = n - msgStart; - - // Check if message contains RTL - bool msgHasRTL = false; - for (int i = 0; i < msgLen; ++i) - { - if (IsRTLCodepoint(msg[i])) - { - msgHasRTL = true; - break; - } - } - - // Build result based on message direction (pre-reserve exact size) - std::vector visual; - visual.reserve((size_t)n); - - if (msgHasRTL) - { - // Arabic message: apply BiDi to message, then add " : name" - std::vector msgVisual = BuildVisualBidiText_Tagless(msg, msgLen, false); - visual.insert(visual.end(), msgVisual.begin(), msgVisual.end()); - visual.push_back(L' '); - visual.push_back(L':'); - visual.push_back(L' '); - visual.insert(visual.end(), name, name + nameLen); // Direct pointer insert - } - else - { - // English message: "msg : name" - visual.insert(visual.end(), msg, msg + msgLen); // Direct pointer insert - visual.push_back(L' '); - visual.push_back(L':'); - visual.push_back(L' '); - visual.insert(visual.end(), name, name + nameLen); // Direct pointer insert - } - - return visual; - } - // 1) base direction EBidiDir base = forceRTL ? EBidiDir::RTL : DetectBaseDir_FirstStrong(s, n); @@ -503,6 +607,16 @@ static std::vector BuildVisualBidiText_Tagless(const wchar_t* s, int n, d = ResolveNeutralDir(s, n, i, base, lastStrong, &strongCache); } +#ifdef DEBUG_BIDI + if (i < 50) // Only log first 50 chars to avoid spam + { + BIDI_LOG("Char[%d] U+%04X '%lc' → CharDir=%s, RunDir=%s", + i, (unsigned int)ch, (ch >= 32 && ch < 127) ? ch : L'?', + cd == ECharDir::RTL ? "RTL" : (cd == ECharDir::LTR ? "LTR" : "Neutral"), + d == EBidiDir::RTL ? "RTL" : "LTR"); + } +#endif + push_run(d); runs.back().text.push_back(ch); } @@ -528,7 +642,13 @@ static std::vector BuildVisualBidiText_Tagless(const wchar_t* s, int n, int outLen = Arabic_MakeShape(r.text.data(), (int)r.text.size(), shaped.data(), (int)shaped.size()); if (outLen <= 0) { - BIDI_LOG("Arabic_MakeShape failed for run of %zu chars", r.text.size()); + BIDI_LOG("Arabic_MakeShape FAILED for RTL run of %zu characters", r.text.size()); + BIDI_LOG(" WARNING: This RTL text segment will NOT be displayed!"); + BIDI_LOG(" First few characters: U+%04X U+%04X U+%04X U+%04X", + r.text.size() > 0 ? (unsigned int)r.text[0] : 0, + r.text.size() > 1 ? (unsigned int)r.text[1] : 0, + r.text.size() > 2 ? (unsigned int)r.text[2] : 0, + r.text.size() > 3 ? (unsigned int)r.text[3] : 0); continue; } @@ -584,6 +704,97 @@ static std::vector BuildVisualBidiText_Tagless(const wchar_t* s, int n, return visual; } +// ============================================================================ +// Chat Message BiDi Processing (Separate name/message handling) +// ============================================================================ + +// Build visual BiDi text for chat messages with separate name and message +// This avoids fragile " : " detection and handles cases where username contains " : " +// +// RECOMMENDED USAGE: +// Instead of: SetValue("PlayerName : Message") +// Use this function with separated components: +// - name: "PlayerName" (without " : ") +// - msg: "Message" (without " : ") +// +// INTEGRATION NOTES: +// To use this properly, you need to: +// 1. Modify the server/network code to send chat name and message separately +// 2. Or parse the chat string in PythonNetworkStreamPhaseGame.cpp BEFORE passing to GrpTextInstance +// 3. Then call this function instead of BuildVisualBidiText_Tagless +// +static inline std::vector BuildVisualChatMessage( + const wchar_t* name, int nameLen, + const wchar_t* msg, int msgLen, + bool forceRTL) +{ + if (!name || !msg || nameLen <= 0 || msgLen <= 0) + return {}; + + // Check if message contains RTL or hyperlink tags + bool msgHasRTL = false; + bool msgHasTags = false; + for (int i = 0; i < msgLen; ++i) + { + if (IsRTLCodepoint(msg[i])) + msgHasRTL = true; + if (msg[i] == L'|') + msgHasTags = true; + if (msgHasRTL && msgHasTags) + break; + } + + // Build result based on UI direction (pre-reserve exact size) + std::vector visual; + visual.reserve((size_t)(nameLen + msgLen + 3)); // +3 for " : " + + // Decision: UI direction determines order (for visual consistency) + // RTL UI: "Message : Name" (message on right, consistent with RTL reading flow) + // LTR UI: "Name : Message" (name on left, consistent with LTR reading flow) + if (forceRTL) + { + // RTL UI: "Message : Name" + // Don't apply BiDi if message has tags (hyperlinks are pre-formatted) + if (msgHasTags) + { + visual.insert(visual.end(), msg, msg + msgLen); + } + else + { + // Apply BiDi to message with auto-detection (don't force RTL) + // Let the BiDi algorithm detect base direction from first strong character + std::vector msgVisual = BuildVisualBidiText_Tagless(msg, msgLen, false); + visual.insert(visual.end(), msgVisual.begin(), msgVisual.end()); + } + visual.push_back(L' '); + visual.push_back(L':'); + visual.push_back(L' '); + visual.insert(visual.end(), name, name + nameLen); // Name on left side + } + else + { + // LTR UI: "Name : Message" + visual.insert(visual.end(), name, name + nameLen); // Name on left side + visual.push_back(L' '); + visual.push_back(L':'); + visual.push_back(L' '); + // Don't apply BiDi if message has tags (hyperlinks are pre-formatted) + if (msgHasTags) + { + visual.insert(visual.end(), msg, msg + msgLen); + } + else + { + // Apply BiDi to message with auto-detection (don't force RTL) + // Let the BiDi algorithm detect base direction from first strong character + std::vector msgVisual = BuildVisualBidiText_Tagless(msg, msgLen, false); + visual.insert(visual.end(), msgVisual.begin(), msgVisual.end()); + } + } + + return visual; +} + // ============================================================================ // TextTail formatting for RTL UI // ============================================================================ diff --git a/src/EterGrnLib/Model.cpp b/src/EterGrnLib/Model.cpp index 968f838..6f9fa18 100644 --- a/src/EterGrnLib/Model.cpp +++ b/src/EterGrnLib/Model.cpp @@ -257,7 +257,7 @@ BOOL CGrannyModel::CheckMeshIndex(int iIndex) const { if (iIndex < 0) return FALSE; - if (iIndex >= m_meshNodeSize) + if (iIndex >= GetMeshCount()) return FALSE; return TRUE; diff --git a/src/EterGrnLib/ThingInstance.cpp b/src/EterGrnLib/ThingInstance.cpp index a418584..765c4bf 100644 --- a/src/EterGrnLib/ThingInstance.cpp +++ b/src/EterGrnLib/ThingInstance.cpp @@ -534,7 +534,7 @@ void CGraphicThingInstance::RegisterMotionThing(DWORD dwMotionKey, CGraphicThing { CGraphicThing::TRef * pMotionRef = new CGraphicThing::TRef; pMotionRef->SetPointer(pMotionThing); - m_roMotionThingMap.insert(std::map::value_type(dwMotionKey, pMotionRef)); + m_roMotionThingMap.insert(std::make_pair(dwMotionKey, pMotionRef)); } void CGraphicThingInstance::ResetLocalTime() diff --git a/src/EterLib/BufferPool.cpp b/src/EterLib/BufferPool.cpp new file mode 100644 index 0000000..5f6f30d --- /dev/null +++ b/src/EterLib/BufferPool.cpp @@ -0,0 +1,103 @@ +#include "StdAfx.h" +#include "BufferPool.h" +#include + +CBufferPool::CBufferPool() + : m_totalAllocated(0) +{ +} + +CBufferPool::~CBufferPool() +{ + Clear(); +} + +std::vector CBufferPool::Acquire(size_t minSize) +{ + std::lock_guard lock(m_mutex); + + size_t bestIndex = SIZE_MAX; + size_t bestCapacity = SIZE_MAX; + + for (size_t i = 0; i < m_pool.size(); ++i) + { + if (m_pool[i].capacity >= minSize && m_pool[i].capacity < bestCapacity) + { + bestIndex = i; + bestCapacity = m_pool[i].capacity; + + if (bestCapacity == minSize) + break; + } + } + + if (bestIndex != SIZE_MAX) + { + std::vector result = std::move(m_pool[bestIndex].buffer); + m_pool.erase(m_pool.begin() + bestIndex); + result.clear(); + return result; + } + + std::vector newBuffer; + newBuffer.reserve(minSize); + m_totalAllocated++; + return newBuffer; +} + +void CBufferPool::Release(std::vector&& buffer) +{ + size_t capacity = buffer.capacity(); + + if (capacity == 0 || capacity > MAX_BUFFER_SIZE) + { + return; + } + + std::lock_guard lock(m_mutex); + + if (m_pool.size() >= MAX_POOL_SIZE) + { + auto smallest = std::min_element(m_pool.begin(), m_pool.end(), + [](const TPooledBuffer& a, const TPooledBuffer& b) { + return a.capacity < b.capacity; + }); + + if (smallest != m_pool.end() && smallest->capacity < capacity) + { + *smallest = TPooledBuffer(std::move(buffer)); + } + return; + } + + m_pool.emplace_back(std::move(buffer)); +} + +size_t CBufferPool::GetPoolSize() const +{ + std::lock_guard lock(m_mutex); + return m_pool.size(); +} + +size_t CBufferPool::GetTotalAllocated() const +{ + std::lock_guard lock(m_mutex); + return m_totalAllocated; +} + +size_t CBufferPool::GetTotalMemoryPooled() const +{ + std::lock_guard lock(m_mutex); + size_t total = 0; + for (const auto& buf : m_pool) + { + total += buf.capacity; + } + return total; +} + +void CBufferPool::Clear() +{ + std::lock_guard lock(m_mutex); + m_pool.clear(); +} diff --git a/src/EterLib/BufferPool.h b/src/EterLib/BufferPool.h new file mode 100644 index 0000000..d74b3ea --- /dev/null +++ b/src/EterLib/BufferPool.h @@ -0,0 +1,50 @@ +#ifndef __INC_ETERLIB_BUFFERPOOL_H__ +#define __INC_ETERLIB_BUFFERPOOL_H__ + +#include +#include +#include + +// Buffer pool for file I/O operations +class CBufferPool +{ +public: + CBufferPool(); + ~CBufferPool(); + + // Get buffer with minimum size + std::vector Acquire(size_t minSize); + + // Return buffer to pool + void Release(std::vector&& buffer); + + // Get statistics + size_t GetPoolSize() const; + size_t GetTotalAllocated() const; + size_t GetTotalMemoryPooled() const; // Total bytes held in pool + + // Clear pool + void Clear(); + +private: + struct TPooledBuffer + { + std::vector buffer; + size_t capacity; + + TPooledBuffer(std::vector&& buf) + : buffer(std::move(buf)) + , capacity(buffer.capacity()) + { + } + }; + + std::vector m_pool; + mutable std::mutex m_mutex; + size_t m_totalAllocated; + + static const size_t MAX_POOL_SIZE = 64; + static const size_t MAX_BUFFER_SIZE = 64 * 1024 * 1024; +}; + +#endif // __INC_ETERLIB_BUFFERPOOL_H__ diff --git a/src/EterLib/DecodedImageData.h b/src/EterLib/DecodedImageData.h new file mode 100644 index 0000000..a5dcee7 --- /dev/null +++ b/src/EterLib/DecodedImageData.h @@ -0,0 +1,59 @@ +#ifndef __INC_ETERLIB_DECODEDIMAGEDATA_H__ +#define __INC_ETERLIB_DECODEDIMAGEDATA_H__ + +#include +#include +#include + +// Decoded image data for GPU upload +struct TDecodedImageData +{ + enum EFormat + { + FORMAT_UNKNOWN = 0, + FORMAT_RGBA8, + FORMAT_RGB8, + FORMAT_DDS, + }; + + std::vector pixels; + int width; + int height; + EFormat format; + D3DFORMAT d3dFormat; + bool isDDS; + int mipLevels; + + TDecodedImageData() + : width(0) + , height(0) + , format(FORMAT_UNKNOWN) + , d3dFormat(D3DFMT_UNKNOWN) + , isDDS(false) + , mipLevels(1) + { + } + + void Clear() + { + pixels.clear(); + width = 0; + height = 0; + format = FORMAT_UNKNOWN; + d3dFormat = D3DFMT_UNKNOWN; + isDDS = false; + mipLevels = 1; + } + + bool IsValid() const + { + return width > 0 && height > 0 && !pixels.empty(); + } + + size_t GetDataSize() const + { + return pixels.size(); + } +}; + +#endif // __INC_ETERLIB_DECODEDIMAGEDATA_H__ diff --git a/src/EterLib/FileLoaderThreadPool.cpp b/src/EterLib/FileLoaderThreadPool.cpp new file mode 100644 index 0000000..aa078c7 --- /dev/null +++ b/src/EterLib/FileLoaderThreadPool.cpp @@ -0,0 +1,270 @@ +#include "StdAfx.h" +#include "FileLoaderThreadPool.h" +#include "BufferPool.h" +#include "ImageDecoder.h" +#include "PackLib/PackManager.h" +#include + +static const bool USE_STAGED_TEXTURE_LOADING = true; + +CFileLoaderThreadPool::CFileLoaderThreadPool() + : m_pCompletedQueue(nullptr) + , m_bShutdown(false) + , m_nextRequestID(0) + , m_activeTasks(0) + , m_threadCount(0) +{ +} + +CFileLoaderThreadPool::~CFileLoaderThreadPool() +{ + Shutdown(); +} + +bool CFileLoaderThreadPool::Initialize(unsigned int threadCount) +{ + if (!m_workers.empty()) + { + TraceError("CFileLoaderThreadPool::Initialize: Already initialized"); + return false; + } + + if (threadCount == 0) + { + threadCount = std::thread::hardware_concurrency(); + if (threadCount == 0) + threadCount = 4; + else + threadCount = std::max(4u, threadCount / 2); + } + + threadCount = std::max(4u, std::min(16u, threadCount)); + m_threadCount = threadCount; + + Tracenf("CFileLoaderThreadPool: Initializing with %u worker threads", threadCount); + + m_pCompletedQueue = new SPSCQueue(COMPLETED_QUEUE_SIZE); + + m_workers.reserve(threadCount); + for (unsigned int i = 0; i < threadCount; ++i) + { + TWorkerThread worker; + worker.pRequestQueue = new SPSCQueue(REQUEST_QUEUE_SIZE); + worker.bBusy.store(false, std::memory_order_relaxed); + + try + { + worker.thread = std::thread(&CFileLoaderThreadPool::WorkerThreadFunction, this, i); + } + catch (const std::exception& e) + { + TraceError("CFileLoaderThreadPool::Initialize: Failed to create thread %u: %s", i, e.what()); + delete worker.pRequestQueue; + worker.pRequestQueue = nullptr; + Shutdown(); + return false; + } + + m_workers.push_back(std::move(worker)); + } + + return true; +} + +void CFileLoaderThreadPool::Shutdown() +{ + if (m_workers.empty()) + return; + + // Signal shutdown + m_bShutdown.store(true, std::memory_order_release); + + // Wait for all workers to finish + for (auto& worker : m_workers) + { + if (worker.thread.joinable()) + worker.thread.join(); + + // Cleanup request queue + if (worker.pRequestQueue) + { + delete worker.pRequestQueue; + worker.pRequestQueue = nullptr; + } + } + + m_workers.clear(); + + // Cleanup completed queue + if (m_pCompletedQueue) + { + delete m_pCompletedQueue; + m_pCompletedQueue = nullptr; + } + + m_threadCount = 0; +} + +bool CFileLoaderThreadPool::Request(const std::string& fileName) +{ + if (m_workers.empty()) + { + TraceError("CFileLoaderThreadPool::Request: Thread pool not initialized"); + return false; + } + + TLoadRequest request; + request.stFileName = fileName; + request.requestID = m_nextRequestID.fetch_add(1, std::memory_order_relaxed); + + request.decodeImage = false; + if (USE_STAGED_TEXTURE_LOADING) + { + size_t dotPos = fileName.find_last_of('.'); + if (dotPos != std::string::npos && dotPos + 1 < fileName.size()) + { + const char* ext = fileName.c_str() + dotPos; + size_t extLen = fileName.size() - dotPos; + + if ((extLen == 4 && (_stricmp(ext, ".dds") == 0 || _stricmp(ext, ".png") == 0 || + _stricmp(ext, ".jpg") == 0 || _stricmp(ext, ".tga") == 0 || _stricmp(ext, ".bmp") == 0)) || + (extLen == 5 && _stricmp(ext, ".jpeg") == 0)) + { + request.decodeImage = true; + } + } + } + + unsigned int targetWorker = SelectLeastBusyWorker(); + + if (!m_workers[targetWorker].pRequestQueue->Push(request)) + { + for (unsigned int i = 0; i < m_threadCount; ++i) + { + unsigned int workerIdx = (targetWorker + i) % m_threadCount; + if (m_workers[workerIdx].pRequestQueue->Push(request)) + { + m_activeTasks.fetch_add(1, std::memory_order_relaxed); + return true; + } + } + + TraceError("CFileLoaderThreadPool::Request: All worker queues full for file: %s", fileName.c_str()); + return false; + } + + m_activeTasks.fetch_add(1, std::memory_order_relaxed); + return true; +} + +bool CFileLoaderThreadPool::Fetch(TLoadResult& result) +{ + if (!m_pCompletedQueue) + return false; + + if (m_pCompletedQueue->Pop(result)) + { + m_activeTasks.fetch_sub(1, std::memory_order_relaxed); + return true; + } + return false; +} + +size_t CFileLoaderThreadPool::GetPendingCount() const +{ + size_t total = 0; + for (const auto& worker : m_workers) + { + if (worker.pRequestQueue) + total += worker.pRequestQueue->Size(); + } + return total; +} + +bool CFileLoaderThreadPool::IsIdle() const +{ + return m_activeTasks.load(std::memory_order_acquire) == 0; +} + +unsigned int CFileLoaderThreadPool::SelectLeastBusyWorker() const +{ + unsigned int leastBusyIdx = 0; + size_t minSize = m_workers[0].pRequestQueue->Size(); + + for (unsigned int i = 1; i < m_threadCount; ++i) + { + size_t queueSize = m_workers[i].pRequestQueue->Size(); + if (queueSize < minSize) + { + minSize = queueSize; + leastBusyIdx = i; + } + } + + return leastBusyIdx; +} + +void CFileLoaderThreadPool::WorkerThreadFunction(unsigned int workerIndex) +{ + TWorkerThread& worker = m_workers[workerIndex]; + SPSCQueue* pRequestQueue = worker.pRequestQueue; + + CBufferPool* pBufferPool = CPackManager::instance().GetBufferPool(); + + Tracenf("CFileLoaderThreadPool: Worker thread %u started", workerIndex); + + int idleCount = 0; + + while (!m_bShutdown.load(std::memory_order_acquire)) + { + TLoadRequest request; + + if (pRequestQueue->Pop(request)) + { + idleCount = 0; + worker.bBusy.store(true, std::memory_order_release); + + TLoadResult result; + result.stFileName = request.stFileName; + result.requestID = request.requestID; + result.File.clear(); + result.hasDecodedImage = false; + + CPackManager::instance().GetFileWithPool(request.stFileName, result.File, pBufferPool); + + if (request.decodeImage && !result.File.empty()) + { + if (CImageDecoder::DecodeImage(result.File.data(), result.File.size(), result.decodedImage)) + { + result.hasDecodedImage = true; + result.File.clear(); + } + } + + while (!m_pCompletedQueue->Push(result)) + { + std::this_thread::yield(); + + if (m_bShutdown.load(std::memory_order_acquire)) + break; + } + + worker.bBusy.store(false, std::memory_order_release); + } + else + { + idleCount++; + if (idleCount > 1000) + { + Sleep(1); + idleCount = 0; + } + else if (idleCount > 10) + { + std::this_thread::yield(); + } + } + } + + Tracenf("CFileLoaderThreadPool: Worker thread %u stopped", workerIndex); +} diff --git a/src/EterLib/FileLoaderThreadPool.h b/src/EterLib/FileLoaderThreadPool.h new file mode 100644 index 0000000..de7c52a --- /dev/null +++ b/src/EterLib/FileLoaderThreadPool.h @@ -0,0 +1,90 @@ +#ifndef __INC_ETERLIB_FILELOADERTHREADPOOL_H__ +#define __INC_ETERLIB_FILELOADERTHREADPOOL_H__ + +#include +#include +#include +#include "SPSCQueue.h" +#include "PackLib/PackManager.h" +#include "DecodedImageData.h" + +class CFileLoaderThreadPool +{ +public: + struct TLoadRequest + { + std::string stFileName; + uint32_t requestID; + bool decodeImage; + }; + + struct TLoadResult + { + std::string stFileName; + TPackFile File; + uint32_t requestID; + TDecodedImageData decodedImage; + bool hasDecodedImage; + }; + +public: + CFileLoaderThreadPool(); + ~CFileLoaderThreadPool(); + + bool Initialize(unsigned int threadCount = 0); + void Shutdown(); + bool Request(const std::string& fileName); + bool Fetch(TLoadResult& result); + size_t GetPendingCount() const; + bool IsIdle() const; + +private: + struct TWorkerThread + { + std::thread thread; + SPSCQueue* pRequestQueue; + std::atomic bBusy; + + TWorkerThread() : pRequestQueue(nullptr), bBusy(false) {} + + TWorkerThread(TWorkerThread&& other) noexcept + : thread(std::move(other.thread)) + , pRequestQueue(other.pRequestQueue) + , bBusy(other.bBusy.load()) + { + other.pRequestQueue = nullptr; + } + + TWorkerThread& operator=(TWorkerThread&& other) noexcept + { + if (this != &other) + { + thread = std::move(other.thread); + pRequestQueue = other.pRequestQueue; + bBusy.store(other.bBusy.load()); + other.pRequestQueue = nullptr; + } + return *this; + } + + TWorkerThread(const TWorkerThread&) = delete; + TWorkerThread& operator=(const TWorkerThread&) = delete; + }; + + void WorkerThreadFunction(unsigned int workerIndex); + unsigned int SelectLeastBusyWorker() const; + +private: + std::vector m_workers; + SPSCQueue* m_pCompletedQueue; + + std::atomic m_bShutdown; + std::atomic m_nextRequestID; + std::atomic m_activeTasks; // Fast IsIdle check + unsigned int m_threadCount; + + static const size_t REQUEST_QUEUE_SIZE = 16384; // Doubled from 8192 + static const size_t COMPLETED_QUEUE_SIZE = 32768; // Doubled from 16384 +}; + +#endif // __INC_ETERLIB_FILELOADERTHREADPOOL_H__ diff --git a/src/EterLib/GrpImage.cpp b/src/EterLib/GrpImage.cpp index cfd3b4b..91a7f01 100644 --- a/src/EterLib/GrpImage.cpp +++ b/src/EterLib/GrpImage.cpp @@ -1,5 +1,6 @@ #include "StdAfx.h" #include "GrpImage.h" +#include "DecodedImageData.h" CGraphicImage::CGraphicImage(const char * c_szFileName, DWORD dwFilter) : CResource(c_szFileName), @@ -79,6 +80,23 @@ bool CGraphicImage::OnLoad(int iSize, const void * c_pvBuf) return true; } +bool CGraphicImage::OnLoadFromDecodedData(const TDecodedImageData& decodedImage) +{ + if (!decodedImage.IsValid()) + return false; + + m_imageTexture.SetFileName(CResource::GetFileName()); + + if (!m_imageTexture.CreateFromDecodedData(decodedImage, D3DFMT_UNKNOWN, m_dwFilter)) + return false; + + m_rect.left = 0; + m_rect.top = 0; + m_rect.right = m_imageTexture.GetWidth(); + m_rect.bottom = m_imageTexture.GetHeight(); + return true; +} + void CGraphicImage::OnClear() { // Tracef("Image Destroy : %s\n", m_pszFileName); diff --git a/src/EterLib/GrpImage.h b/src/EterLib/GrpImage.h index 65d94f9..2766c3e 100644 --- a/src/EterLib/GrpImage.h +++ b/src/EterLib/GrpImage.h @@ -5,6 +5,8 @@ #include "Resource.h" #include "GrpImageTexture.h" +struct TDecodedImageData; + class CGraphicImage : public CResource { public: @@ -28,6 +30,8 @@ class CGraphicImage : public CResource const CGraphicTexture & GetTextureReference() const; CGraphicTexture * GetTexturePointer(); + bool OnLoadFromDecodedData(const TDecodedImageData& decodedImage); + protected: bool OnLoad(int iSize, const void * c_pvBuf); diff --git a/src/EterLib/GrpImageTexture.cpp b/src/EterLib/GrpImageTexture.cpp index 05582eb..e2d58cd 100644 --- a/src/EterLib/GrpImageTexture.cpp +++ b/src/EterLib/GrpImageTexture.cpp @@ -2,9 +2,15 @@ #include "PackLib/PackManager.h" #include "GrpImageTexture.h" #include "EterImageLib/DDSTextureLoader9.h" +#include "DecodedImageData.h" #include +#if defined(_M_IX86) || defined(_M_X64) +#include // SSE2 +#include // SSSE3 (for _mm_shuffle_epi8) +#endif + bool CGraphicImageTexture::Lock(int* pRetPitch, void** ppRetPixels, int level) { D3DLOCKED_RECT lockedRect; @@ -110,17 +116,41 @@ bool CGraphicImageTexture::CreateFromSTB(UINT bufSize, const void* c_pvBuf) unsigned char* data = stbi_load_from_memory((stbi_uc*)c_pvBuf, bufSize, &width, &height, &channels, 4); // force RGBA if (data) { LPDIRECT3DTEXTURE9 texture; - if (SUCCEEDED(ms_lpd3dDevice->CreateTexture(width, height, 1, 0, channels == 4 ? D3DFMT_A8R8G8B8 : D3DFMT_X8R8G8B8, D3DPOOL_DEFAULT, &texture, nullptr))) { + if (SUCCEEDED(ms_lpd3dDevice->CreateTexture(width, height, 1, 0, channels == 4 ? D3DFMT_A8R8G8B8 : D3DFMT_X8R8G8B8, D3DPOOL_MANAGED, &texture, nullptr))) { D3DLOCKED_RECT rect; if (SUCCEEDED(texture->LockRect(0, &rect, nullptr, 0))) { uint8_t* dstData = (uint8_t*)rect.pBits; uint8_t* srcData = (uint8_t*)data; - for (size_t i = 0; i < width * height; ++i, dstData += 4, srcData += 4) { - dstData[0] = srcData[2]; - dstData[1] = srcData[1]; - dstData[2] = srcData[0]; - dstData[3] = srcData[3]; + size_t pixelCount = width * height; + + #if defined(_M_IX86) || defined(_M_X64) + { + size_t simdPixels = pixelCount & ~3; + __m128i shuffle_mask = _mm_setr_epi8(2, 1, 0, 3, 6, 5, 4, 7, 10, 9, 8, 11, 14, 13, 12, 15); + + for (size_t i = 0; i < simdPixels; i += 4) { + __m128i pixels = _mm_loadu_si128((__m128i*)(srcData + i * 4)); + pixels = _mm_shuffle_epi8(pixels, shuffle_mask); + _mm_storeu_si128((__m128i*)(dstData + i * 4), pixels); + } + + for (size_t i = simdPixels; i < pixelCount; ++i) { + size_t idx = i * 4; + dstData[idx + 0] = srcData[idx + 2]; + dstData[idx + 1] = srcData[idx + 1]; + dstData[idx + 2] = srcData[idx + 0]; + dstData[idx + 3] = srcData[idx + 3]; + } } + #else + for (size_t i = 0; i < pixelCount; ++i) { + size_t idx = i * 4; + dstData[idx + 0] = srcData[idx + 2]; + dstData[idx + 1] = srcData[idx + 1]; + dstData[idx + 2] = srcData[idx + 0]; + dstData[idx + 3] = srcData[idx + 3]; + } + #endif texture->UnlockRect(0); m_width = width; @@ -228,6 +258,98 @@ bool CGraphicImageTexture::CreateFromDiskFile(const char * c_szFileName, D3DFORM return CreateDeviceObjects(); } +bool CGraphicImageTexture::CreateFromDecodedData(const TDecodedImageData& decodedImage, D3DFORMAT d3dFmt, DWORD dwFilter) +{ + assert(ms_lpd3dDevice != NULL); + assert(m_lpd3dTexture == NULL); + + if (!decodedImage.IsValid()) + return false; + + m_bEmpty = true; + + if (decodedImage.isDDS) + { + // DDS format - use DirectX loader + if (!CreateFromDDSTexture(decodedImage.pixels.size(), decodedImage.pixels.data())) + return false; + } + else if (decodedImage.format == TDecodedImageData::FORMAT_RGBA8) + { + LPDIRECT3DTEXTURE9 texture; + D3DFORMAT format = D3DFMT_A8R8G8B8; + + if (FAILED(ms_lpd3dDevice->CreateTexture( + decodedImage.width, + decodedImage.height, + 1, + 0, + format, + D3DPOOL_MANAGED, + &texture, + nullptr))) + { + return false; + } + + D3DLOCKED_RECT rect; + if (SUCCEEDED(texture->LockRect(0, &rect, nullptr, 0))) + { + uint8_t* dstData = (uint8_t*)rect.pBits; + const uint8_t* srcData = decodedImage.pixels.data(); + size_t pixelCount = decodedImage.width * decodedImage.height; + + #if defined(_M_IX86) || defined(_M_X64) + { + size_t simdPixels = pixelCount & ~3; + __m128i shuffle_mask = _mm_setr_epi8(2, 1, 0, 3, 6, 5, 4, 7, 10, 9, 8, 11, 14, 13, 12, 15); + + for (size_t i = 0; i < simdPixels; i += 4) { + __m128i pixels = _mm_loadu_si128((__m128i*)(srcData + i * 4)); + pixels = _mm_shuffle_epi8(pixels, shuffle_mask); + _mm_storeu_si128((__m128i*)(dstData + i * 4), pixels); + } + + for (size_t i = simdPixels; i < pixelCount; ++i) { + size_t idx = i * 4; + dstData[idx + 0] = srcData[idx + 2]; + dstData[idx + 1] = srcData[idx + 1]; + dstData[idx + 2] = srcData[idx + 0]; + dstData[idx + 3] = srcData[idx + 3]; + } + } + #else + for (size_t i = 0; i < pixelCount; ++i) { + size_t idx = i * 4; + dstData[idx + 0] = srcData[idx + 2]; + dstData[idx + 1] = srcData[idx + 1]; + dstData[idx + 2] = srcData[idx + 0]; + dstData[idx + 3] = srcData[idx + 3]; + } + #endif + + texture->UnlockRect(0); + + m_width = decodedImage.width; + m_height = decodedImage.height; + m_lpd3dTexture = texture; + m_bEmpty = false; + } + else + { + texture->Release(); + return false; + } + } + else + { + TraceError("CreateFromDecodedData: Unsupported decoded image format"); + return false; + } + + return !m_bEmpty; +} + CGraphicImageTexture::CGraphicImageTexture() { Initialize(); diff --git a/src/EterLib/GrpImageTexture.h b/src/EterLib/GrpImageTexture.h index 34b6f69..2ffeec7 100644 --- a/src/EterLib/GrpImageTexture.h +++ b/src/EterLib/GrpImageTexture.h @@ -2,6 +2,8 @@ #include "GrpTexture.h" +struct TDecodedImageData; + class CGraphicImageTexture : public CGraphicTexture { public: @@ -18,6 +20,7 @@ class CGraphicImageTexture : public CGraphicTexture bool CreateFromMemoryFile(UINT bufSize, const void* c_pvBuf, D3DFORMAT d3dFmt, DWORD dwFilter = D3DX_FILTER_LINEAR); bool CreateFromDDSTexture(UINT bufSize, const void* c_pvBuf); bool CreateFromSTB(UINT bufSize, const void* c_pvBuf); + bool CreateFromDecodedData(const TDecodedImageData& decodedImage, D3DFORMAT d3dFmt, DWORD dwFilter); void SetFileName(const char * c_szFileName); diff --git a/src/EterLib/GrpObjectInstance.cpp b/src/EterLib/GrpObjectInstance.cpp index 2cce95e..3f4e8fb 100644 --- a/src/EterLib/GrpObjectInstance.cpp +++ b/src/EterLib/GrpObjectInstance.cpp @@ -211,7 +211,7 @@ void CGraphicObjectInstance::ReleaseAlwaysHidden() { bool CGraphicObjectInstance::isShow() { - return m_isVisible && !m_isAlwaysHidden; + return m_isVisible; } // diff --git a/src/EterLib/GrpTextInstance.cpp b/src/EterLib/GrpTextInstance.cpp index 22ada7b..a33e8e8 100644 --- a/src/EterLib/GrpTextInstance.cpp +++ b/src/EterLib/GrpTextInstance.cpp @@ -101,11 +101,9 @@ void CGraphicTextInstance::Update() m_pCharInfoVector.clear(); m_dwColorInfoVector.clear(); m_hyperlinkVector.clear(); - m_logicalToVisualPos.clear(); - m_visualToLogicalPos.clear(); m_textWidth = 0; m_textHeight = spaceHeight; // Use space height instead of 0 for cursor rendering - m_computedRTL = (m_direction == ETextDirection::RTL); + m_computedRTL = IsRTL(); // Use global RTL setting m_isUpdate = true; }; @@ -131,610 +129,317 @@ void CGraphicTextInstance::Update() const char* utf8 = m_stText.c_str(); const int utf8Len = (int)m_stText.size(); - const DWORD defaultColor = m_dwTextColor; + DWORD dwColor = m_dwTextColor; - // UTF-8 -> UTF-16 conversion - reserve enough space to avoid reallocation + // UTF-8 -> UTF-16 conversion std::vector wTextBuf((size_t)utf8Len + 1u, 0); - const int wTextLen = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8, utf8Len, wTextBuf.data(), (int)wTextBuf.size()); + int wTextLen = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8, utf8Len, wTextBuf.data(), (int)wTextBuf.size()); + + // If strict UTF-8 conversion fails, try lenient mode (replaces invalid sequences) if (wTextLen <= 0) { - ResetState(); - return; - } + // Try lenient conversion (no MB_ERR_INVALID_CHARS flag) + wTextLen = MultiByteToWideChar(CP_UTF8, 0, utf8, utf8Len, wTextBuf.data(), (int)wTextBuf.size()); - - // Detect user-typed text direction (skip hyperlink and color tags) - // Used to determine segment order - bool userTextIsRTL = false; - bool foundUserText = false; - { - int hyperlinkStep = 0; // 0 = normal, 1 = in metadata (hidden), 2 = in visible text - const int wTextLenMinusOne = wTextLen - 1; - - for (int i = 0; i < wTextLen; ++i) + if (wTextLen <= 0) { - // Check for tags (cache bounds check) - if (i < wTextLenMinusOne && wTextBuf[i] == L'|') - { - if (wTextBuf[i + 1] == L'H') - { - hyperlinkStep = 1; // Start metadata - ++i; - continue; - } - else if (wTextBuf[i + 1] == L'h') - { - if (hyperlinkStep == 1) - hyperlinkStep = 2; // End metadata, start visible - else if (hyperlinkStep == 2) - hyperlinkStep = 0; // End visible - ++i; - continue; - } - else if (wTextBuf[i + 1] == L'c' && i + 10 <= wTextLen) - { - // Color tag |cFFFFFFFF - skip 10 characters - i += 9; // +1 from loop increment = 10 total - continue; - } - else if (wTextBuf[i + 1] == L'r') - { - // Color end tag |r - skip - ++i; - continue; - } - } - - // Only check user-typed text (step 0 = normal text) - // SKIP hyperlink visible text (step 2) to prevent hyperlink language from affecting direction - if (hyperlinkStep == 0) - { - if (IsRTLCodepoint(wTextBuf[i])) - { - userTextIsRTL = true; - foundUserText = true; - break; - } - if (IsStrongAlpha(wTextBuf[i])) - { - userTextIsRTL = false; - foundUserText = true; - break; - } - } + ResetState(); + return; } } - // Base direction for BiDi algorithm (for non-hyperlink text reordering) - const bool baseRTL = - (m_direction == ETextDirection::RTL) ? true : - (m_direction == ETextDirection::LTR) ? false : - userTextIsRTL; + // Set computed RTL based on global setting + m_computedRTL = IsRTL(); - // Computed direction for rendering and alignment - // Always use baseRTL to respect the UI direction setting - // In RTL UI, all text (input and display) should use RTL alignment - m_computedRTL = baseRTL; - - // Secret: draw '*' but keep direction + // Secret mode: draw '*' instead of actual characters if (m_isSecret) { for (int i = 0; i < wTextLen; ++i) - __DrawCharacter(pFontTexture, L'*', defaultColor); + __DrawCharacter(pFontTexture, L'*', dwColor); pFontTexture->UpdateTexture(); m_isUpdate = true; return; } - const bool hasTags = (std::find(wTextBuf.begin(), wTextBuf.begin() + wTextLen, L'|') != (wTextBuf.begin() + wTextLen)); + // === RENDERING APPROACH === + // Use BuildVisualBidiText_Tagless() and BuildVisualChatMessage() from utf8.h + // These functions handle Arabic shaping, BiDi reordering, and chat formatting properly - // ======================================================================== - // Case 1: No tags - Simple BiDi reordering - // ======================================================================== - if (!hasTags) + // Special handling for chat messages + if (m_isChatMessage && !m_chatName.empty() && !m_chatMessage.empty()) { - // Build identity mapping (logical == visual for tagless text) - const size_t mappingSize = (size_t)wTextLen + 1; - m_logicalToVisualPos.resize(mappingSize); - m_visualToLogicalPos.resize(mappingSize); - for (int i = 0; i <= wTextLen; ++i) + std::wstring wName = Utf8ToWide(m_chatName); + std::wstring wMsg = Utf8ToWide(m_chatMessage); + + // Check if message has tags (hyperlinks) + bool msgHasTags = (std::find(wMsg.begin(), wMsg.end(), L'|') != wMsg.end()); + + if (!msgHasTags) { - m_logicalToVisualPos[i] = i; - m_visualToLogicalPos[i] = i; - } + // No tags: Use BuildVisualChatMessage() for simple BiDi + std::vector visual = BuildVisualChatMessage( + wName.data(), (int)wName.size(), + wMsg.data(), (int)wMsg.size(), + m_computedRTL); - // Check for RTL characters and chat message format in single pass - bool hasRTL = false; - bool isChatMessage = false; - const wchar_t* wTextPtr = wTextBuf.data(); - - for (int i = 0; i < wTextLen; ++i) - { - if (!hasRTL && IsRTLCodepoint(wTextPtr[i])) - hasRTL = true; - - if (!isChatMessage && i < wTextLen - 2 && - wTextPtr[i] == L' ' && wTextPtr[i + 1] == L':' && wTextPtr[i + 2] == L' ') - isChatMessage = true; - - // Early exit if both found - if (hasRTL && isChatMessage) - break; - } - - // Apply BiDi if text contains RTL OR is a chat message in RTL UI - // Skip BiDi for regular input text like :)) in RTL UI - if (hasRTL || (baseRTL && isChatMessage)) - { - std::vector visual = BuildVisualBidiText_Tagless(wTextBuf.data(), wTextLen, baseRTL); for (size_t i = 0; i < visual.size(); ++i) - __DrawCharacter(pFontTexture, visual[i], defaultColor); + __DrawCharacter(pFontTexture, visual[i], dwColor); + + pFontTexture->UpdateTexture(); + m_isUpdate = true; + return; } else { - // Pure LTR text or non-chat input - no BiDi processing - for (int i = 0; i < wTextLen; ++i) - __DrawCharacter(pFontTexture, wTextBuf[i], defaultColor); + // Has tags (hyperlinks): Rebuild as "Message : Name" or "Name : Message" + // then use tag-aware rendering below + if (m_computedRTL) + { + // RTL: "Message : Name" + m_stText = m_chatMessage + " : " + m_chatName; + } + else + { + // LTR: "Name : Message" (original format) + m_stText = m_chatName + " : " + m_chatMessage; + } + + // Re-convert to wide chars for tag-aware processing below + const char* utf8 = m_stText.c_str(); + const int utf8Len = (int)m_stText.size(); + wTextBuf.clear(); + wTextBuf.resize((size_t)utf8Len + 1u, 0); + wTextLen = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8, utf8Len, wTextBuf.data(), (int)wTextBuf.size()); + + if (wTextLen <= 0) + { + wTextLen = MultiByteToWideChar(CP_UTF8, 0, utf8, utf8Len, wTextBuf.data(), (int)wTextBuf.size()); + if (wTextLen <= 0) + { + ResetState(); + return; + } + } + // Fall through to tag-aware rendering below } } - // ======================================================================== - // Case 2: Has tags - Parse tags and apply BiDi to segments - // ======================================================================== - else + + // Check if text contains tags or RTL + const bool hasTags = (std::find(wTextBuf.begin(), wTextBuf.begin() + wTextLen, L'|') != (wTextBuf.begin() + wTextLen)); + bool hasRTL = false; + for (int i = 0; i < wTextLen; ++i) { - // Check if text contains RTL characters (cache pointer for performance) - bool hasRTL = false; - const wchar_t* wTextPtr = wTextBuf.data(); - for (int i = 0; i < wTextLen; ++i) + if (IsRTLCodepoint(wTextBuf[i])) { - if (IsRTLCodepoint(wTextPtr[i])) - { - hasRTL = true; - break; - } + hasRTL = true; + break; } - struct TVisChar + } + + // Tag-aware BiDi rendering: Parse tags, apply BiDi per segment, track colors/hyperlinks + if (hasRTL || hasTags) + { + DWORD currentColor = dwColor; + int hyperlinkStep = 0; // 0=normal, 1=collecting metadata, 2=visible hyperlink + std::wstring hyperlinkMetadata; + std::vector currentSegment; + + SHyperlink currentHyperlink; + currentHyperlink.sx = currentHyperlink.ex = 0; + + // Parse text with tags + for (int i = 0; i < wTextLen;) { - wchar_t ch; - DWORD color; - int linkIndex; // -1 = none, otherwise index into linkTargets - int logicalPos; // logical index in original wTextBuf (includes tags) - }; + int tagLen = 0; + std::wstring tagExtra; + int tagType = GetTextTag(&wTextBuf[i], wTextLen - i, tagLen, tagExtra); - auto ReorderTaggedWithBidi = [&](std::vector& vis, bool forceRTL) - { - if (vis.empty()) - return; - - // Extract only characters - std::vector buf; - buf.reserve(vis.size()); - for (const auto& vc : vis) - buf.push_back(vc.ch); - - // Use the exact same BiDi engine as tagless text - std::vector visual = BuildVisualBidiText_Tagless(buf.data(), (int)buf.size(), forceRTL); - - // If size differs (rare, but can happen with Arabic shaping expansion), - // do a safe best-effort resize while preserving style. - if ((int)visual.size() != (int)vis.size()) + if (tagType == TEXT_TAG_COLOR) { - // Keep style from nearest original character - std::vector resized; - resized.reserve(visual.size()); - - if (vis.empty()) - return; - - for (size_t i = 0; i < visual.size(); ++i) + // Flush current segment with BiDi before changing color + if (!currentSegment.empty()) { - size_t src = (i < vis.size()) ? i : (vis.size() - 1); - TVisChar tmp = vis[src]; - tmp.ch = visual[i]; - resized.push_back(tmp); + // Use auto-detection for BiDi (don't force RTL) + std::vector visual = BuildVisualBidiText_Tagless( + currentSegment.data(), (int)currentSegment.size(), false); + for (size_t j = 0; j < visual.size(); ++j) + { + int w = __DrawCharacter(pFontTexture, visual[j], currentColor); + currentHyperlink.ex += w; + } + currentSegment.clear(); } - vis.swap(resized); - return; + currentColor = htoi(tagExtra.c_str(), 8); + i += tagLen; } - - // Same size: just write back characters, keep color + linkIndex intact - for (size_t i = 0; i < vis.size(); ++i) - vis[i].ch = visual[i]; - }; - - DWORD curColor = defaultColor; - - // hyperlinkStep: 0=none, 1=collecting target after |H, 2=visible section between |h and |h - int hyperlinkStep = 0; - std::wstring hyperlinkTarget; - hyperlinkTarget.reserve(64); // Reserve typical hyperlink target size - int activeLinkIndex = -1; - - std::vector linkTargets; // linkTargets[i] is target text for link i - linkTargets.reserve(4); // Reserve space for typical number of links - - std::vector logicalVis; - logicalVis.reserve((size_t)wTextLen); // Reserve max possible size - - // Build logical->visual position mapping (reserve to avoid reallocation) - const size_t mappingSize = (size_t)wTextLen + 1; - m_logicalToVisualPos.resize(mappingSize, 0); - - // ==================================================================== - // PHASE 1: Parse tags and collect visible characters - // ==================================================================== - int tagLen = 1; - std::wstring tagExtra; - - for (int i = 0; i < wTextLen; ) - { - m_logicalToVisualPos[i] = (int)logicalVis.size(); - - tagExtra.clear(); - int ret = GetTextTag(&wTextBuf[i], wTextLen - i, tagLen, tagExtra); - if (tagLen <= 0) tagLen = 1; - - if (ret == TEXT_TAG_PLAIN) + else if (tagType == TEXT_TAG_RESTORE_COLOR) { - wchar_t ch = wTextBuf[i]; - - if (hyperlinkStep == 1) + // Flush segment before restoring color + if (!currentSegment.empty()) { - // Collect hyperlink target text between |H and first |h - hyperlinkTarget.push_back(ch); + // Use auto-detection for BiDi (don't force RTL) + std::vector visual = BuildVisualBidiText_Tagless( + currentSegment.data(), (int)currentSegment.size(), false); + for (size_t j = 0; j < visual.size(); ++j) + { + int w = __DrawCharacter(pFontTexture, visual[j], currentColor); + currentHyperlink.ex += w; + } + currentSegment.clear(); } - else - { - // Regular visible character - logicalVis.push_back(TVisChar{ ch, curColor, activeLinkIndex, i }); - } - - i += 1; - continue; + currentColor = dwColor; + i += tagLen; } - - // Tag handling - if (ret == TEXT_TAG_COLOR) - { - curColor = htoi(tagExtra.c_str(), 8); - } - else if (ret == TEXT_TAG_RESTORE_COLOR) - { - curColor = defaultColor; - } - else if (ret == TEXT_TAG_HYPERLINK_START) + else if (tagType == TEXT_TAG_HYPERLINK_START) { hyperlinkStep = 1; - hyperlinkTarget.clear(); - activeLinkIndex = -1; + hyperlinkMetadata.clear(); + i += tagLen; } - else if (ret == TEXT_TAG_HYPERLINK_END) + else if (tagType == TEXT_TAG_HYPERLINK_END) { if (hyperlinkStep == 1) { - // End metadata => start visible section - hyperlinkStep = 2; + // End of metadata, start visible section + // Flush any pending non-hyperlink segment first + if (!currentSegment.empty()) + { + // Use auto-detection for BiDi (don't force RTL) + std::vector visual = BuildVisualBidiText_Tagless( + currentSegment.data(), (int)currentSegment.size(), false); + for (size_t j = 0; j < visual.size(); ++j) + { + int w = __DrawCharacter(pFontTexture, visual[j], currentColor); + currentHyperlink.ex += w; + } + currentSegment.clear(); + } - linkTargets.push_back(hyperlinkTarget); - activeLinkIndex = (int)linkTargets.size() - 1; + hyperlinkStep = 2; + currentHyperlink.text = hyperlinkMetadata; + currentHyperlink.sx = currentHyperlink.ex; // Start hyperlink at current cursor position } else if (hyperlinkStep == 2) { - // End visible section - hyperlinkStep = 0; - activeLinkIndex = -1; - hyperlinkTarget.clear(); - } - } - - i += tagLen; - } - - // ==================================================================== - // PHASE 2: Apply BiDi to hyperlinks (if RTL text or RTL UI) - // ==================================================================== - if (hasRTL || baseRTL) - { - // Collect all hyperlink ranges (reserve typical count) - struct LinkRange { int start; int end; int linkIdx; }; - std::vector linkRanges; - linkRanges.reserve(linkTargets.size()); - - int currentLink = -1; - int linkStart = -1; - const int logicalVisCount = (int)logicalVis.size(); - - for (int i = 0; i <= logicalVisCount; ++i) - { - const int linkIdx = (i < logicalVisCount) ? logicalVis[i].linkIndex : -1; - - if (linkIdx != currentLink) - { - if (currentLink >= 0 && linkStart >= 0) + // End of visible section - render hyperlink text with proper Arabic handling + // Format: [Arabic Text] or [English Text] + // Keep brackets in position, reverse Arabic content between them + if (!currentSegment.empty()) { - linkRanges.push_back({linkStart, i, currentLink}); - } - - currentLink = linkIdx; - linkStart = (currentLink >= 0) ? i : -1; - } - } - - // Process hyperlinks in reverse order to avoid index shifting - const int numRanges = (int)linkRanges.size(); - for (int rangeIdx = numRanges - 1; rangeIdx >= 0; --rangeIdx) - { - const LinkRange& range = linkRanges[rangeIdx]; - const int linkStart = range.start; - const int linkEnd = range.end; - const int linkLength = linkEnd - linkStart; - - // Extract hyperlink text (pre-reserve exact size) - std::vector linkBuf; - linkBuf.reserve(linkLength); - for (int j = linkStart; j < linkEnd; ++j) - linkBuf.push_back(logicalVis[j].ch); - - // Apply BiDi with LTR base direction (hyperlinks use LTR structure like [+9 item]) - std::vector linkVisual = BuildVisualBidiText_Tagless(linkBuf.data(), (int)linkBuf.size(), false); - - // Normalize brackets and enhancement markers - const int linkVisualSize = (int)linkVisual.size(); - if (linkVisualSize > 0) - { - // Find first '[' and first ']' (cache size) - int openBracket = -1, closeBracket = -1; - for (int j = 0; j < linkVisualSize; ++j) - { - if (linkVisual[j] == L'[' && openBracket < 0) openBracket = j; - if (linkVisual[j] == L']' && closeBracket < 0) closeBracket = j; - } - - // Case 1: Brackets are reversed "]text[" => "[text]" - if (closeBracket >= 0 && openBracket > closeBracket) - { - std::vector normalized; - normalized.reserve(linkVisual.size()); - - // Rebuild: [ + (before ]) + (between ] and [) + (after [) + ] - normalized.push_back(L'['); - - for (int j = 0; j < closeBracket; ++j) - normalized.push_back(linkVisual[j]); - - for (int j = closeBracket + 1; j < openBracket; ++j) - normalized.push_back(linkVisual[j]); - - for (int j = openBracket + 1; j < (int)linkVisual.size(); ++j) - normalized.push_back(linkVisual[j]); - - normalized.push_back(L']'); - - linkVisual = normalized; - openBracket = 0; - closeBracket = (int)linkVisual.size() - 1; - } - - // Case 2: Normal brackets "[...]" - check for normalization - if (openBracket >= 0 && closeBracket > openBracket) - { - int pos = openBracket + 1; - - // Skip leading spaces inside brackets - while (pos < closeBracket && linkVisual[pos] == L' ') + // Find bracket positions + int openBracket = -1, closeBracket = -1; + for (size_t idx = 0; idx < currentSegment.size(); ++idx) { - linkVisual.erase(linkVisual.begin() + pos); - closeBracket--; + if (currentSegment[idx] == L'[' && openBracket == -1) + openBracket = (int)idx; + else if (currentSegment[idx] == L']' && closeBracket == -1) + closeBracket = (int)idx; } - // Check for "+" pattern and reverse to "+" - if (pos < closeBracket && linkVisual[pos] == L'+') + if (openBracket >= 0 && closeBracket > openBracket) { - int digitStart = pos + 1; - int digitEnd = digitStart; + // Extract content between brackets + std::vector content( + currentSegment.begin() + openBracket + 1, + currentSegment.begin() + closeBracket); - while (digitEnd < closeBracket && (linkVisual[digitEnd] >= L'0' && linkVisual[digitEnd] <= L'9')) - digitEnd++; + // Apply Arabic shaping to content + std::vector shaped(content.size() * 2 + 16, 0); + int shapedLen = Arabic_MakeShape(content.data(), (int)content.size(), + shaped.data(), (int)shaped.size()); - if (digitEnd > digitStart) + // Render: "[" + reversed_arabic + "]" + // 1. Opening bracket + int w = __DrawCharacter(pFontTexture, L'[', currentColor); + currentHyperlink.ex += w; + + // 2. Arabic content (shaped and REVERSED for RTL display) + if (shapedLen > 0) { - wchar_t plus = L'+'; - for (int k = pos; k < digitEnd - 1; ++k) - linkVisual[k] = linkVisual[k + 1]; - linkVisual[digitEnd - 1] = plus; + for (int j = shapedLen - 1; j >= 0; --j) + { + w = __DrawCharacter(pFontTexture, shaped[j], currentColor); + currentHyperlink.ex += w; + } + } + else + { + // Fallback: reverse original content + for (int j = (int)content.size() - 1; j >= 0; --j) + { + w = __DrawCharacter(pFontTexture, content[j], currentColor); + currentHyperlink.ex += w; + } + } + + // 3. Closing bracket + w = __DrawCharacter(pFontTexture, L']', currentColor); + currentHyperlink.ex += w; + + // 4. Render any text after closing bracket (if any) + for (size_t idx = closeBracket + 1; idx < currentSegment.size(); ++idx) + { + w = __DrawCharacter(pFontTexture, currentSegment[idx], currentColor); + currentHyperlink.ex += w; } } + else + { + // No brackets found - render as-is (shouldn't happen for hyperlinks) + for (size_t j = 0; j < currentSegment.size(); ++j) + { + int w = __DrawCharacter(pFontTexture, currentSegment[j], currentColor); + currentHyperlink.ex += w; + } + } + currentSegment.clear(); } + m_hyperlinkVector.push_back(currentHyperlink); + hyperlinkStep = 0; } - - // Write back - handle size changes by erasing/inserting - const int originalSize = linkLength; - const int newSize = (int)linkVisual.size(); - const int sizeDiff = newSize - originalSize; - - // Replace existing characters (cache min for performance) - const int copyCount = (std::min)(originalSize, newSize); - for (int j = 0; j < copyCount; ++j) - logicalVis[linkStart + j].ch = linkVisual[j]; - - if (sizeDiff < 0) + i += tagLen; + } + else // TEXT_TAG_PLAIN or TEXT_TAG_TAG + { + if (hyperlinkStep == 1) { - // Shrunk - remove extra characters - logicalVis.erase(logicalVis.begin() + linkStart + newSize, - logicalVis.begin() + linkStart + originalSize); + // Collecting hyperlink metadata (hidden) + hyperlinkMetadata.push_back(wTextBuf[i]); } - else if (sizeDiff > 0) + else { - // Grew - insert new characters - TVisChar templateChar = logicalVis[linkStart]; - templateChar.logicalPos = logicalVis[linkStart].logicalPos; - for (int j = originalSize; j < newSize; ++j) - { - templateChar.ch = linkVisual[j]; - logicalVis.insert(logicalVis.begin() + linkStart + j, templateChar); - } + // Add to current segment + // Will be BiDi-processed for normal text, or rendered directly for hyperlinks + currentSegment.push_back(wTextBuf[i]); } + i += tagLen; } } - // Apply BiDi to non-hyperlink segments and reorder segments for RTL UI - if (hasRTL || baseRTL) + // Flush any remaining segment + if (!currentSegment.empty()) { - // Split text into hyperlink and non-hyperlink segments (reserve typical count) - const size_t estimatedSegments = linkTargets.size() * 2 + 1; - std::vector> segments; - segments.reserve(estimatedSegments); // Estimate: links + text between - - std::vector isHyperlink; // true if segment is a hyperlink - isHyperlink.reserve(estimatedSegments); - - int segStart = 0; - int currentLinkIdx = (logicalVis.empty() ? -1 : logicalVis[0].linkIndex); - const int logicalVisSize2 = (int)logicalVis.size(); - - for (int i = 1; i <= logicalVisSize2; ++i) + // Use auto-detection for BiDi (don't force RTL) + std::vector visual = BuildVisualBidiText_Tagless( + currentSegment.data(), (int)currentSegment.size(), false); + for (size_t j = 0; j < visual.size(); ++j) { - const int linkIdx = (i < logicalVisSize2) ? logicalVis[i].linkIndex : -1; - - if (linkIdx != currentLinkIdx) - { - // Segment boundary - std::vector seg(logicalVis.begin() + segStart, logicalVis.begin() + i); - segments.push_back(seg); - isHyperlink.push_back(currentLinkIdx >= 0); - - segStart = i; - currentLinkIdx = linkIdx; - } - } - - // Apply BiDi to non-hyperlink segments only (cache segment count) - const size_t numSegments = segments.size(); - for (size_t s = 0; s < numSegments; ++s) - { - if (!isHyperlink[s]) - ReorderTaggedWithBidi(segments[s], baseRTL); - } - - // Rebuild text from segments (reverse order for RTL UI) - logicalVis.clear(); - logicalVis.reserve(logicalVisSize2); // Reserve original size - - if (baseRTL) - { - // RTL UI - reverse segments for right-to-left reading - for (int s = (int)numSegments - 1; s >= 0; --s) - { - logicalVis.insert(logicalVis.end(), segments[s].begin(), segments[s].end()); - } - } - else - { - // LTR UI - keep original segment order - for (size_t s = 0; s < numSegments; ++s) - { - logicalVis.insert(logicalVis.end(), segments[s].begin(), segments[s].end()); - } + int w = __DrawCharacter(pFontTexture, visual[j], currentColor); + currentHyperlink.ex += w; } } - // ==================================================================== - // FINAL: Rebuild visual<->logical mapping AFTER all BiDi/tag reordering - // ==================================================================== - - m_visualToLogicalPos.clear(); - m_logicalToVisualPos.clear(); - - // logical positions refer to indices in wTextBuf (tagged string) - m_logicalToVisualPos.resize((size_t)wTextLen + 1, -1); - m_visualToLogicalPos.resize((size_t)logicalVis.size() + 1, wTextLen); - - // Fill visual->logical from stored glyph origin - for (size_t v = 0; v < logicalVis.size(); ++v) - { - int lp = logicalVis[v].logicalPos; - if (lp < 0) lp = 0; - if (lp > wTextLen) lp = wTextLen; - - m_visualToLogicalPos[v] = lp; - - // For logical->visual, keep the first visual position that maps to lp - if (m_logicalToVisualPos[(size_t)lp] < 0) - m_logicalToVisualPos[(size_t)lp] = (int)v; - } - - // End positions - m_visualToLogicalPos[logicalVis.size()] = wTextLen; - m_logicalToVisualPos[(size_t)wTextLen] = (int)logicalVis.size(); - - // Fill gaps in logical->visual so cursor movement doesn't break on tag-only regions - int last = 0; - for (int i = 0; i <= wTextLen; ++i) - { - if (m_logicalToVisualPos[(size_t)i] < 0) - m_logicalToVisualPos[(size_t)i] = last; - else - last = m_logicalToVisualPos[(size_t)i]; - } - - // ==================================================================== - // PHASE 3: Render and build hyperlink ranges - // ==================================================================== - m_hyperlinkVector.clear(); - m_hyperlinkVector.reserve(linkTargets.size()); // Reserve for known hyperlinks - - int x = 0; - int currentLink = -1; - SHyperlink curLinkRange{}; - curLinkRange.sx = 0; - curLinkRange.ex = 0; - - // Cache size for loop (avoid repeated size() calls) - const size_t logicalVisRenderSize = logicalVis.size(); - for (size_t idx = 0; idx < logicalVisRenderSize; ++idx) - { - const TVisChar& vc = logicalVis[idx]; - const int charWidth = __DrawCharacter(pFontTexture, vc.ch, vc.color); - - // Hyperlink range tracking - const int linkIdx = vc.linkIndex; - - if (linkIdx != currentLink) - { - // Close previous hyperlink - if (currentLink >= 0) - { - curLinkRange.text = linkTargets[(size_t)currentLink]; - m_hyperlinkVector.push_back(curLinkRange); - } - - // Open new hyperlink - currentLink = linkIdx; - if (currentLink >= 0) - { - curLinkRange = SHyperlink{}; - curLinkRange.sx = (short)x; - curLinkRange.ex = (short)x; - } - } - - if (currentLink >= 0) - { - curLinkRange.ex = (short)(curLinkRange.ex + charWidth); - } - - x += charWidth; - } - - // Close last hyperlink - if (currentLink >= 0) - { - curLinkRange.text = linkTargets[(size_t)currentLink]; - m_hyperlinkVector.push_back(curLinkRange); - } + pFontTexture->UpdateTexture(); + m_isUpdate = true; + return; } + // Simple LTR rendering for plain text (no tags, no RTL) + // Just draw characters in logical order + for (int i = 0; i < wTextLen; ++i) + __DrawCharacter(pFontTexture, wTextBuf[i], dwColor); + pFontTexture->UpdateTexture(); m_isUpdate = true; } @@ -1090,38 +795,7 @@ void CGraphicTextInstance::Render(RECT * pClipRect) continue; STATEMANAGER.SetTexture(0, pTexture); - - // Each character is 4 vertices forming a quad (0=TL, 1=BL, 2=TR, 3=BR) - // We need to convert quads to triangle list format - // Triangle list needs 6 vertices per quad: v0,v1,v2, v2,v1,v3 - - size_t numQuads = vtxBatch.size() / 4; - std::vector triangleVerts; - triangleVerts.reserve(numQuads * 6); - - for (size_t i = 0; i < numQuads; ++i) - { - size_t baseIdx = i * 4; - const SVertex& v0 = vtxBatch[baseIdx + 0]; // TL - const SVertex& v1 = vtxBatch[baseIdx + 1]; // BL - const SVertex& v2 = vtxBatch[baseIdx + 2]; // TR - const SVertex& v3 = vtxBatch[baseIdx + 3]; // BR - - // First triangle: TL, BL, TR - triangleVerts.push_back(v0); - triangleVerts.push_back(v1); - triangleVerts.push_back(v2); - - // Second triangle: TR, BL, BR - triangleVerts.push_back(v2); - triangleVerts.push_back(v1); - triangleVerts.push_back(v3); - } - - if (!triangleVerts.empty()) - { - STATEMANAGER.DrawPrimitiveUP(D3DPT_TRIANGLELIST, triangleVerts.size() / 3, triangleVerts.data(), sizeof(SVertex)); - } + STATEMANAGER.DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, vtxBatch.size() - 2, vtxBatch.data(), sizeof(SVertex)); } if (m_isCursor) @@ -1392,6 +1066,23 @@ void CGraphicTextInstance::SetValue(const char* c_szText, size_t len) return; m_stText = c_szText; + m_isChatMessage = false; // Reset chat mode + m_isUpdate = false; +} + +void CGraphicTextInstance::SetChatValue(const char* c_szName, const char* c_szMessage) +{ + if (!c_szName || !c_szMessage) + return; + + // Store separated components + m_chatName = c_szName; + m_chatMessage = c_szMessage; + m_isChatMessage = true; + + // Build combined text for rendering (will be processed by Update()) + // Use BuildVisualChatMessage in Update() instead of BuildVisualBidiText_Tagless + m_stText = std::string(c_szName) + " : " + std::string(c_szMessage); m_isUpdate = false; } @@ -1525,6 +1216,9 @@ void CGraphicTextInstance::__Initialize() // Only chat messages should be explicitly set to RTL m_direction = ETextDirection::Auto; m_computedRTL = false; + m_isChatMessage = false; + m_chatName = ""; + m_chatMessage = ""; m_textWidth = 0; m_textHeight = 0; diff --git a/src/EterLib/GrpTextInstance.h b/src/EterLib/GrpTextInstance.h index 72462c1..ec7d1aa 100644 --- a/src/EterLib/GrpTextInstance.h +++ b/src/EterLib/GrpTextInstance.h @@ -62,6 +62,7 @@ class CGraphicTextInstance void SetTextPointer(CGraphicText* pText); void SetValueString(const std::string& c_stValue); void SetValue(const char* c_szValue, size_t len = -1); + void SetChatValue(const char* c_szName, const char* c_szMessage); // Chat-specific setter with name/message separation void SetPosition(float fx, float fy, float fz = 0.0f); void SetSecret(bool Value); void SetOutline(bool Value); @@ -131,6 +132,9 @@ class CGraphicTextInstance bool m_isUpdate; bool m_isUpdateFontTexture; bool m_computedRTL; // Result of BiDi analysis (used when m_direction == Auto) + bool m_isChatMessage; // True if this text was set via SetChatValue (has separated name/message) + std::string m_chatName; // Chat sender name (only used when m_isChatMessage is true) + std::string m_chatMessage; // Chat message text (only used when m_isChatMessage is true) CGraphicText::TRef m_roText; CGraphicFontTexture::TPCharacterInfomationVector m_pCharInfoVector; diff --git a/src/EterLib/ImageDecoder.cpp b/src/EterLib/ImageDecoder.cpp new file mode 100644 index 0000000..ebcc242 --- /dev/null +++ b/src/EterLib/ImageDecoder.cpp @@ -0,0 +1,92 @@ +#include "StdAfx.h" +#include "ImageDecoder.h" +#include "EterImageLib/DDSTextureLoader9.h" +#include + +bool CImageDecoder::DecodeImage(const void* pData, size_t dataSize, TDecodedImageData& outImage) +{ + if (!pData || dataSize == 0) + return false; + + outImage.Clear(); + + if (DecodeDDS(pData, dataSize, outImage)) + return true; + + if (DecodeSTB(pData, dataSize, outImage)) + return true; + + return false; +} + +bool CImageDecoder::DecodeDDS(const void* pData, size_t dataSize, TDecodedImageData& outImage) +{ + if (dataSize < 4) + return false; + + const uint32_t DDS_MAGIC = 0x20534444; + uint32_t magic = *(const uint32_t*)pData; + + if (magic != DDS_MAGIC) + return false; + + if (dataSize < 128) + return false; + + struct DDSHeader + { + uint32_t magic; + uint32_t size; + uint32_t flags; + uint32_t height; + uint32_t width; + uint32_t pitchOrLinearSize; + uint32_t depth; + uint32_t mipMapCount; + uint32_t reserved1[11]; + }; + + const DDSHeader* header = (const DDSHeader*)pData; + + outImage.width = header->width; + outImage.height = header->height; + outImage.mipLevels = (header->mipMapCount > 0) ? header->mipMapCount : 1; + outImage.isDDS = true; + outImage.format = TDecodedImageData::FORMAT_DDS; + + outImage.pixels.resize(dataSize); + memcpy(outImage.pixels.data(), pData, dataSize); + + return true; +} + +bool CImageDecoder::DecodeSTB(const void* pData, size_t dataSize, TDecodedImageData& outImage) +{ + int width, height, channels; + + unsigned char* imageData = stbi_load_from_memory( + (const stbi_uc*)pData, + (int)dataSize, + &width, + &height, + &channels, + 4 + ); + + if (!imageData) + return false; + + outImage.width = width; + outImage.height = height; + outImage.format = TDecodedImageData::FORMAT_RGBA8; + outImage.isDDS = false; + outImage.mipLevels = 1; + + size_t pixelDataSize = width * height * 4; + outImage.pixels.resize(pixelDataSize); + memcpy(outImage.pixels.data(), imageData, pixelDataSize); + + stbi_image_free(imageData); + + return true; +} diff --git a/src/EterLib/ImageDecoder.h b/src/EterLib/ImageDecoder.h new file mode 100644 index 0000000..289d11b --- /dev/null +++ b/src/EterLib/ImageDecoder.h @@ -0,0 +1,18 @@ +#ifndef __INC_ETERLIB_IMAGEDECODER_H__ +#define __INC_ETERLIB_IMAGEDECODER_H__ + +#include "DecodedImageData.h" + +// Image decoder for worker threads +class CImageDecoder +{ +public: + // Decode image from memory (DDS, PNG, JPG, TGA, BMP) + static bool DecodeImage(const void* pData, size_t dataSize, TDecodedImageData& outImage); + +private: + static bool DecodeDDS(const void* pData, size_t dataSize, TDecodedImageData& outImage); + static bool DecodeSTB(const void* pData, size_t dataSize, TDecodedImageData& outImage); +}; + +#endif // __INC_ETERLIB_IMAGEDECODER_H__ diff --git a/src/EterLib/ResourceManager.cpp b/src/EterLib/ResourceManager.cpp index 4c572f2..8c6a714 100644 --- a/src/EterLib/ResourceManager.cpp +++ b/src/EterLib/ResourceManager.cpp @@ -7,6 +7,8 @@ #include "ResourceManager.h" #include "GrpImage.h" +#include "TextureCache.h" +#include "DecodedImageData.h" int g_iLoadingDelayTime = 1; // Reduced from 20ms to 1ms for faster async loading @@ -68,7 +70,16 @@ void CResourceManager::ProcessBackgroundLoading() } //printf("REQ %s\n", stFileName.c_str()); - ms_loadingThread.Request(stFileName); + + if (m_pLoaderThreadPool) + { + m_pLoaderThreadPool->Request(stFileName); + } + else + { + ms_loadingThread.Request(stFileName); + } + m_WaitingMap.insert(TResourceRequestMap::value_type(dwFileCRC, stFileName)); itor = m_RequestMap.erase(itor); //break; // NOTE: 여기서 break 하면 천천히 로딩 된다. @@ -76,6 +87,44 @@ void CResourceManager::ProcessBackgroundLoading() DWORD dwCurrentTime = ELTimer_GetMSec(); + if (m_pLoaderThreadPool) + { + CFileLoaderThreadPool::TLoadResult result; + while (m_pLoaderThreadPool->Fetch(result)) + { + CResource * pResource = GetResourcePointer(result.stFileName.c_str()); + + if (pResource) + { + if (pResource->IsEmpty()) + { + if (result.hasDecodedImage) + { + CGraphicImage* pImage = dynamic_cast(pResource); + if (pImage) + { + pImage->OnLoadFromDecodedData(result.decodedImage); + } + else + { + pResource->OnLoad(result.File.size(), result.File.data()); + } + } + else + { + pResource->OnLoad(result.File.size(), result.File.data()); + } + + pResource->AddReferenceOnly(); + m_pResRefDecreaseWaitingMap.insert(TResourceRefDecreaseWaitingMap::value_type(dwCurrentTime, pResource)); + } + } + + m_WaitingMap.erase(GetCRC32(result.stFileName.c_str(), result.stFileName.size())); + } + } + + // Process old thread results CFileLoaderThread::TData * pData; while (ms_loadingThread.Fetch(&pData)) { @@ -528,12 +577,36 @@ void CResourceManager::ReserveDeletingResource(CResource * pResource) } CResourceManager::CResourceManager() + : m_pLoaderThreadPool(nullptr) + , m_pTextureCache(nullptr) { ms_loadingThread.Create(0); + + m_pLoaderThreadPool = new CFileLoaderThreadPool(); + if (!m_pLoaderThreadPool->Initialize()) + { + TraceError("CResourceManager: Failed to initialize FileLoaderThreadPool"); + delete m_pLoaderThreadPool; + m_pLoaderThreadPool = nullptr; + } + + m_pTextureCache = new CTextureCache(512); } CResourceManager::~CResourceManager() { Destroy(); ms_loadingThread.Shutdown(); + + if (m_pLoaderThreadPool) + { + delete m_pLoaderThreadPool; + m_pLoaderThreadPool = nullptr; + } + + if (m_pTextureCache) + { + delete m_pTextureCache; + m_pTextureCache = nullptr; + } } diff --git a/src/EterLib/ResourceManager.h b/src/EterLib/ResourceManager.h index 22308a5..71ee2b4 100644 --- a/src/EterLib/ResourceManager.h +++ b/src/EterLib/ResourceManager.h @@ -2,11 +2,14 @@ #include "Resource.h" #include "FileLoaderThread.h" +#include "FileLoaderThreadPool.h" #include #include #include +class CTextureCache; + class CResourceManager : public CSingleton { public: @@ -42,6 +45,9 @@ class CResourceManager : public CSingleton void ProcessBackgroundLoading(); void PushBackgroundLoadingSet(std::set & LoadingSet); + CTextureCache* GetTextureCache() { return m_pTextureCache; } + CFileLoaderThreadPool* GetLoaderThreadPool() { return m_pLoaderThreadPool; } + protected: void __DestroyDeletingResourceMap(); void __DestroyResourceMap(); @@ -68,6 +74,8 @@ class CResourceManager : public CSingleton TResourceRefDecreaseWaitingMap m_pResRefDecreaseWaitingMap; static CFileLoaderThread ms_loadingThread; + CFileLoaderThreadPool* m_pLoaderThreadPool; + CTextureCache* m_pTextureCache; }; extern int g_iLoadingDelayTime; \ No newline at end of file diff --git a/src/EterLib/SPSCQueue.h b/src/EterLib/SPSCQueue.h new file mode 100644 index 0000000..10ae55b --- /dev/null +++ b/src/EterLib/SPSCQueue.h @@ -0,0 +1,79 @@ +#ifndef __INC_ETERLIB_SPSCQUEUE_H__ +#define __INC_ETERLIB_SPSCQUEUE_H__ + +#include +#include +#include + +// Lock-free queue for single producer/consumer pairs +template +class SPSCQueue +{ +public: + explicit SPSCQueue(size_t capacity) + : m_capacity(capacity + 1) // +1 to distinguish full from empty + , m_buffer(m_capacity) + , m_head(0) + , m_tail(0) + { + assert(capacity > 0); + } + + ~SPSCQueue() + { + } + + // Push item (returns false if full) + bool Push(const T& item) + { + const size_t head = m_head.load(std::memory_order_relaxed); + const size_t next_head = (head + 1) % m_capacity; + + if (next_head == m_tail.load(std::memory_order_acquire)) + return false; // Queue is full + + m_buffer[head] = item; + m_head.store(next_head, std::memory_order_release); + return true; + } + + // Pop item (returns false if empty) + bool Pop(T& item) + { + const size_t tail = m_tail.load(std::memory_order_relaxed); + + if (tail == m_head.load(std::memory_order_acquire)) + return false; // Queue is empty + + item = m_buffer[tail]; + m_tail.store((tail + 1) % m_capacity, std::memory_order_release); + return true; + } + + // Check if empty + bool IsEmpty() const + { + return m_tail.load(std::memory_order_acquire) == m_head.load(std::memory_order_acquire); + } + + // Get queue size + size_t Size() const + { + const size_t head = m_head.load(std::memory_order_acquire); + const size_t tail = m_tail.load(std::memory_order_acquire); + + if (head >= tail) + return head - tail; + else + return m_capacity - tail + head; + } + +private: + const size_t m_capacity; + std::vector m_buffer; + + alignas(64) std::atomic m_head; + alignas(64) std::atomic m_tail; +}; + +#endif // __INC_ETERLIB_SPSCQUEUE_H__ diff --git a/src/EterLib/TextureCache.cpp b/src/EterLib/TextureCache.cpp new file mode 100644 index 0000000..030b200 --- /dev/null +++ b/src/EterLib/TextureCache.cpp @@ -0,0 +1,109 @@ +#include "StdAfx.h" +#include "TextureCache.h" + +CTextureCache::CTextureCache(size_t maxMemoryMB) + : m_maxMemory(maxMemoryMB * 1024 * 1024) + , m_currentMemory(0) + , m_hits(0) + , m_misses(0) +{ +} + +CTextureCache::~CTextureCache() +{ + Clear(); +} + +bool CTextureCache::Get(const std::string& filename, TCachedTexture& outTexture) +{ + std::lock_guard lock(m_mutex); + + auto it = m_cache.find(filename); + if (it == m_cache.end()) + { + m_misses.fetch_add(1); + return false; + } + + // Move to back of LRU (most recently used) + m_lruList.erase(it->second.second); + m_lruList.push_back(filename); + it->second.second = std::prev(m_lruList.end()); + + // Copy texture data + outTexture = it->second.first; + + m_hits.fetch_add(1); + return true; +} + +void CTextureCache::Put(const std::string& filename, const TCachedTexture& texture) +{ + std::lock_guard lock(m_mutex); + + // Check if already cached + auto it = m_cache.find(filename); + if (it != m_cache.end()) + { + // Update existing entry + m_currentMemory -= it->second.first.memorySize; + m_lruList.erase(it->second.second); + m_cache.erase(it); + } + + // Evict if needed + while (m_currentMemory + texture.memorySize > m_maxMemory && !m_cache.empty()) + { + Evict(); + } + + // Don't cache if too large + if (texture.memorySize > m_maxMemory / 4) + { + return; // Skip caching huge textures + } + + // Add to cache + m_lruList.push_back(filename); + auto lruIt = std::prev(m_lruList.end()); + m_cache[filename] = {texture, lruIt}; + m_currentMemory += texture.memorySize; +} + +void CTextureCache::Clear() +{ + std::lock_guard lock(m_mutex); + m_cache.clear(); + m_lruList.clear(); + m_currentMemory = 0; +} + +float CTextureCache::GetHitRate() const +{ + size_t hits = m_hits.load(); + size_t misses = m_misses.load(); + size_t total = hits + misses; + + if (total == 0) + return 0.0f; + + return (float)hits / (float)total; +} + +void CTextureCache::Evict() +{ + // Remove least recently used (front of list) + if (m_lruList.empty()) + return; + + const std::string& filename = m_lruList.front(); + auto it = m_cache.find(filename); + + if (it != m_cache.end()) + { + m_currentMemory -= it->second.first.memorySize; + m_cache.erase(it); + } + + m_lruList.pop_front(); +} diff --git a/src/EterLib/TextureCache.h b/src/EterLib/TextureCache.h new file mode 100644 index 0000000..5416a13 --- /dev/null +++ b/src/EterLib/TextureCache.h @@ -0,0 +1,55 @@ +#ifndef __INC_ETERLIB_TEXTURECACHE_H__ +#define __INC_ETERLIB_TEXTURECACHE_H__ + +#include +#include +#include +#include + +// LRU cache for decoded textures +class CTextureCache +{ +public: + struct TCachedTexture + { + std::vector pixels; + int width; + int height; + size_t memorySize; + std::string filename; + }; + + CTextureCache(size_t maxMemoryMB = 256); + ~CTextureCache(); + + // Get cached texture + bool Get(const std::string& filename, TCachedTexture& outTexture); + + // Add texture to cache + void Put(const std::string& filename, const TCachedTexture& texture); + + // Clear cache + void Clear(); + + // Get statistics + size_t GetMemoryUsage() const { return m_currentMemory; } + size_t GetMaxMemory() const { return m_maxMemory; } + size_t GetCachedCount() const { return m_cache.size(); } + float GetHitRate() const; + +private: + void Evict(); + +private: + size_t m_maxMemory; + size_t m_currentMemory; + + std::list m_lruList; + std::unordered_map::iterator>> m_cache; + + mutable std::mutex m_mutex; + std::atomic m_hits; + std::atomic m_misses; +}; + +#endif // __INC_ETERLIB_TEXTURECACHE_H__ diff --git a/src/GameLib/RaceManager.cpp b/src/GameLib/RaceManager.cpp index 11d43ee..073e623 100644 --- a/src/GameLib/RaceManager.cpp +++ b/src/GameLib/RaceManager.cpp @@ -3,6 +3,8 @@ #include "RaceMotionData.h" #include "PackLib/PackManager.h" +bool CRaceManager::s_bPreloaded = false; + bool __IsGuildRace(unsigned race) { if (race >= 14000 && race < 15000) @@ -448,3 +450,46 @@ CRaceManager::~CRaceManager() { Destroy(); } + +void CRaceManager::PreloadPlayerRaceMotions() +{ + if (s_bPreloaded) + return; + + // Preload all player races (0-7) + for (DWORD dwRace = 0; dwRace <= 7; ++dwRace) + { + CRaceData* pRaceData = NULL; + if (!Instance().GetRaceDataPointer(dwRace, &pRaceData)) + continue; + + CRaceData::TMotionModeDataIterator itor; + + if (pRaceData->CreateMotionModeIterator(itor)) + { + do + { + CRaceData::TMotionModeData* pMotionModeData = itor->second; + + CRaceData::TMotionVectorMap::iterator itorMotion = pMotionModeData->MotionVectorMap.begin(); + for (; itorMotion != pMotionModeData->MotionVectorMap.end(); ++itorMotion) + { + const CRaceData::TMotionVector& c_rMotionVector = itorMotion->second; + CRaceData::TMotionVector::const_iterator it; + + for (it = c_rMotionVector.begin(); it != c_rMotionVector.end(); ++it) + { + CGraphicThing* pMotion = it->pMotion; + if (pMotion) + { + pMotion->AddReference(); + } + } + } + } + while (pRaceData->NextMotionModeIterator(itor)); + } + } + + s_bPreloaded = true; +} diff --git a/src/GameLib/RaceManager.h b/src/GameLib/RaceManager.h index bdb0fbe..88fb01d 100644 --- a/src/GameLib/RaceManager.h +++ b/src/GameLib/RaceManager.h @@ -29,6 +29,9 @@ class CRaceManager : public CSingleton BOOL GetRaceDataPointer(DWORD dwRaceIndex, CRaceData ** ppRaceData); + // Race motion preloading + static void PreloadPlayerRaceMotions(); + static bool IsPreloaded() { return s_bPreloaded; } protected: CRaceData* __LoadRaceData(DWORD dwRaceIndex); @@ -46,4 +49,5 @@ class CRaceManager : public CSingleton private: std::string m_strPathName; CRaceData * m_pSelectedRaceData; + static bool s_bPreloaded; }; \ No newline at end of file diff --git a/src/PRTerrainLib/TextureSet.cpp b/src/PRTerrainLib/TextureSet.cpp index 0016cc7..7f9d6f3 100644 --- a/src/PRTerrainLib/TextureSet.cpp +++ b/src/PRTerrainLib/TextureSet.cpp @@ -56,6 +56,27 @@ bool CTextureSet::Load(const char * c_szTextureSetFileName, float fTerrainTexCoo m_Textures.resize(lCount + 1); + std::vector textureFiles; + textureFiles.reserve(lCount); + + for (long i = 0; i < lCount; ++i) + { + _snprintf(szTextureName, sizeof(szTextureName), "texture%03d", i + 1); + + if (stTokenVectorMap.end() == stTokenVectorMap.find(szTextureName)) + continue; + + const CTokenVector & rVector = stTokenVectorMap[szTextureName]; + const std::string & c_rstrFileName = rVector[0].c_str(); + + textureFiles.push_back(c_rstrFileName); + } + + for (const auto& filename : textureFiles) + { + CResourceManager::Instance().GetResourcePointer(filename.c_str()); + } + for (long i = 0; i < lCount; ++i) { _snprintf(szTextureName, sizeof(szTextureName), "texture%03d", i + 1); diff --git a/src/PackLib/Pack.cpp b/src/PackLib/Pack.cpp index c87d1d5..93e0b42 100644 --- a/src/PackLib/Pack.cpp +++ b/src/PackLib/Pack.cpp @@ -1,6 +1,18 @@ #include "Pack.h" +#include "EterLib/BufferPool.h" #include +static thread_local ZSTD_DCtx* g_zstdDCtx = nullptr; + +static ZSTD_DCtx* GetThreadLocalZSTDContext() +{ + if (!g_zstdDCtx) + { + g_zstdDCtx = ZSTD_createDCtx(); + } + return g_zstdDCtx; +} + bool CPack::Open(const std::string& path, TPackFileMap& entries) { std::error_code ec; @@ -38,27 +50,44 @@ bool CPack::Open(const std::string& path, TPackFileMap& entries) } bool CPack::GetFile(const TPackFileEntry& entry, TPackFile& result) +{ + return GetFileWithPool(entry, result, nullptr); +} + +bool CPack::GetFileWithPool(const TPackFileEntry& entry, TPackFile& result, CBufferPool* pPool) { result.resize(entry.file_size); size_t offset = m_header.data_begin + entry.offset; + ZSTD_DCtx* dctx = GetThreadLocalZSTDContext(); + switch (entry.encryption) { case 0: { - size_t decompressed_size = ZSTD_decompress(result.data(), result.size(), m_file.data() + offset, entry.compressed_size); + size_t decompressed_size = ZSTD_decompressDCtx(dctx, result.data(), result.size(), m_file.data() + offset, entry.compressed_size); if (decompressed_size != entry.file_size) { return false; } } break; case 1: { - std::vector compressed_data(entry.compressed_size); + std::vector compressed_data; + if (pPool) { + compressed_data = pPool->Acquire(entry.compressed_size); + } + compressed_data.resize(entry.compressed_size); + memcpy(compressed_data.data(), m_file.data() + offset, entry.compressed_size); m_decryption.Resynchronize(entry.iv, sizeof(entry.iv)); m_decryption.ProcessData(compressed_data.data(), compressed_data.data(), entry.compressed_size); - size_t decompressed_size = ZSTD_decompress(result.data(), result.size(), compressed_data.data(), compressed_data.size()); + size_t decompressed_size = ZSTD_decompressDCtx(dctx, result.data(), result.size(), compressed_data.data(), compressed_data.size()); + + if (pPool) { + pPool->Release(std::move(compressed_data)); + } + if (decompressed_size != entry.file_size) { return false; } diff --git a/src/PackLib/Pack.h b/src/PackLib/Pack.h index 740807d..24638fa 100644 --- a/src/PackLib/Pack.h +++ b/src/PackLib/Pack.h @@ -4,6 +4,8 @@ #include "config.h" +class CBufferPool; + class CPack : public std::enable_shared_from_this { public: @@ -12,6 +14,7 @@ public: bool Open(const std::string& path, TPackFileMap& entries); bool GetFile(const TPackFileEntry& entry, TPackFile& result); + bool GetFileWithPool(const TPackFileEntry& entry, TPackFile& result, CBufferPool* pPool); private: TPackFileHeader m_header; diff --git a/src/PackLib/PackManager.cpp b/src/PackLib/PackManager.cpp index 9258c78..8ad49af 100644 --- a/src/PackLib/PackManager.cpp +++ b/src/PackLib/PackManager.cpp @@ -1,14 +1,38 @@ #include "PackManager.h" +#include "EterLib/BufferPool.h" #include #include +CPackManager::CPackManager() + : m_load_from_pack(true) + , m_pBufferPool(nullptr) +{ + m_pBufferPool = new CBufferPool(); +} + +CPackManager::~CPackManager() +{ + if (m_pBufferPool) + { + delete m_pBufferPool; + m_pBufferPool = nullptr; + } +} + bool CPackManager::AddPack(const std::string& path) { std::shared_ptr pack = std::make_shared(); + + std::lock_guard lock(m_mutex); return pack->Open(path, m_entries); } bool CPackManager::GetFile(std::string_view path, TPackFile& result) +{ + return GetFileWithPool(path, result, m_pBufferPool); +} + +bool CPackManager::GetFileWithPool(std::string_view path, TPackFile& result, CBufferPool* pPool) { thread_local std::string buf; NormalizePath(path, buf); @@ -16,7 +40,7 @@ bool CPackManager::GetFile(std::string_view path, TPackFile& result) if (m_load_from_pack) { auto it = m_entries.find(buf); if (it != m_entries.end()) { - return it->second.first->GetFile(it->second.second, result); + return it->second.first->GetFileWithPool(it->second.second, result, pPool); } } else { @@ -25,7 +49,14 @@ bool CPackManager::GetFile(std::string_view path, TPackFile& result) ifs.seekg(0, std::ios::end); size_t size = ifs.tellg(); ifs.seekg(0, std::ios::beg); - result.resize(size); + + if (pPool) { + result = pPool->Acquire(size); + result.resize(size); + } else { + result.resize(size); + } + if (ifs.read((char*)result.data(), size)) { return true; } diff --git a/src/PackLib/PackManager.h b/src/PackLib/PackManager.h index ecf939d..f6b712e 100644 --- a/src/PackLib/PackManager.h +++ b/src/PackLib/PackManager.h @@ -1,26 +1,34 @@ #pragma once #include +#include #include "EterBase/Singleton.h" #include "Pack.h" +class CBufferPool; + class CPackManager : public CSingleton { public: - CPackManager() = default; - virtual ~CPackManager() = default; + CPackManager(); + virtual ~CPackManager(); bool AddPack(const std::string& path); bool GetFile(std::string_view path, TPackFile& result); + bool GetFileWithPool(std::string_view path, TPackFile& result, CBufferPool* pPool); bool IsExist(std::string_view path) const; void SetPackLoadMode() { m_load_from_pack = true; } void SetFileLoadMode() { m_load_from_pack = false; } + CBufferPool* GetBufferPool() { return m_pBufferPool; } + private: void NormalizePath(std::string_view in, std::string& out) const; private: bool m_load_from_pack = true; TPackFileMap m_entries; + CBufferPool* m_pBufferPool; + mutable std::mutex m_mutex; // Thread safety for parallel pack loading }; diff --git a/src/UserInterface/InstanceBase.cpp b/src/UserInterface/InstanceBase.cpp index 8d53629..7f1294e 100644 --- a/src/UserInterface/InstanceBase.cpp +++ b/src/UserInterface/InstanceBase.cpp @@ -2116,7 +2116,6 @@ void CInstanceBase::SetStateFlags(DWORD dwStateFlags) // MR-4: Fix PK Mode Bug // Prevent killer mode for same-guild attacks in GUILD PK mode bool skipKiller = false; - if ((dwStateFlags & ADD_CHARACTER_STATE_KILLER) && PK_MODE_GUILD == GetPKMode()) { CPythonPlayer& rkPlayer = CPythonPlayer::Instance(); @@ -2225,6 +2224,7 @@ bool CInstanceBase::IsAttackableInstance(CInstanceBase& rkInstVictim) return false; } } + if (PK_MODE_GUILD == GetPKMode()) if (GetGuildID() == rkInstVictim.GetGuildID()) return false; @@ -2255,6 +2255,9 @@ bool CInstanceBase::IsAttackableInstance(CInstanceBase& rkInstVictim) if (IsPVPInstance(rkInstVictim)) return true; + if (rkInstVictim.GetPKMode() == PK_MODE_PROTECT) + return false; + // MR-4: Fix PK Mode Bug if (PK_MODE_REVENGE == GetPKMode()) { @@ -2262,12 +2265,13 @@ bool CInstanceBase::IsAttackableInstance(CInstanceBase& rkInstVictim) { if ( (GetGuildID() == 0 || GetGuildID() != rkInstVictim.GetGuildID()) && - IsConflictAlignmentInstance(rkInstVictim) && rkInstVictim.GetAlignment() < 0 ) return true; } } + + return false; // MR-4: -- END OF -- Fix PK Mode Bug } else diff --git a/src/UserInterface/InstanceBase.h b/src/UserInterface/InstanceBase.h index 02ab491..9b819a1 100644 --- a/src/UserInterface/InstanceBase.h +++ b/src/UserInterface/InstanceBase.h @@ -372,6 +372,7 @@ class CInstanceBase EFFECT_HAPPINESS_RING_EQUIP, // 행복의 반지 착용 순간에 발동하는 이펙트 EFFECT_LOVE_PENDANT_EQUIP, // 행복의 반지 착용 순간에 발동하는 이펙트 EFFECT_TEMP, + EFFECT_AGGREGATE_MONSTER, EFFECT_NUM, }; diff --git a/src/UserInterface/InstanceBaseBattle.cpp b/src/UserInterface/InstanceBaseBattle.cpp index eb8cfcc..6e92ddf 100644 --- a/src/UserInterface/InstanceBaseBattle.cpp +++ b/src/UserInterface/InstanceBaseBattle.cpp @@ -349,7 +349,12 @@ bool CInstanceBase::NEW_UseSkill(UINT uSkill, UINT uMot, UINT uMotLoopCount, boo float fCurRot=m_GraphicThingInstance.GetTargetRotation(); SetAdvancingRotation(fCurRot); - m_GraphicThingInstance.InterceptOnceMotion(CRaceMotionData::NAME_SKILL + uMot, 0.1f, uSkill, 1.0f); + // MR-7: Don't show skill motion if character is invisible + if (!IsAffect(AFFECT_INVISIBILITY)) + { + m_GraphicThingInstance.InterceptOnceMotion(CRaceMotionData::NAME_SKILL + uMot, 0.1f, uSkill, 1.0f); + } + // MR-7: -- END OF -- Don't show skill motion if character is invisible m_GraphicThingInstance.__OnUseSkill(uMot, uMotLoopCount, isMovingSkill); diff --git a/src/UserInterface/InstanceBaseEffect.cpp b/src/UserInterface/InstanceBaseEffect.cpp index 4f50084..0c97120 100644 --- a/src/UserInterface/InstanceBaseEffect.cpp +++ b/src/UserInterface/InstanceBaseEffect.cpp @@ -1056,40 +1056,81 @@ void CInstanceBase::__DetachEffect(DWORD dwEID) DWORD CInstanceBase::__AttachEffect(UINT eEftType) { - // 2004.07.17.levites.isShow를 ViewFrustumCheck로 변경 - if (IsAffect(AFFECT_INVISIBILITY)) - return 0; - - if (eEftType>=EFFECT_NUM) + if (eEftType >= EFFECT_NUM) return 0; if (ms_astAffectEffectAttachBone[eEftType].empty()) { - return m_GraphicThingInstance.AttachEffectByID(0, NULL, ms_adwCRCAffectEffect[eEftType]); + DWORD dwEftID = m_GraphicThingInstance.AttachEffectByID(0, NULL, ms_adwCRCAffectEffect[eEftType]); + + // MR-7: Recover affect visual effects when coming out of invisibility + if (dwEftID && IsAffect(AFFECT_INVISIBILITY)) + { + CEffectManager::Instance().SelectEffectInstance(dwEftID); + CEffectManager::Instance().HideEffect(); + CEffectManager::Instance().ApplyAlwaysHidden(); + } + + return dwEftID; + // MR-7: -- END OF -- Recover affect visual effects when coming out of invisibility } else { std::string & rstrBoneName = ms_astAffectEffectAttachBone[eEftType]; const char * c_szBoneName; + // 양손에 붙일 때 사용한다. // 이런 식의 예외 처리를 해놓은 것은 캐릭터 마다 Equip 의 Bone Name 이 다르기 때문. if (0 == rstrBoneName.compare("PART_WEAPON")) { if (m_GraphicThingInstance.GetAttachingBoneName(CRaceData::PART_WEAPON, &c_szBoneName)) { - return m_GraphicThingInstance.AttachEffectByID(0, c_szBoneName, ms_adwCRCAffectEffect[eEftType]); + // MR-7: Recover affect visual effects when coming out of invisibility + DWORD dwEftID = m_GraphicThingInstance.AttachEffectByID(0, c_szBoneName, ms_adwCRCAffectEffect[eEftType]); + + if (dwEftID && IsAffect(AFFECT_INVISIBILITY)) + { + CEffectManager::Instance().SelectEffectInstance(dwEftID); + CEffectManager::Instance().HideEffect(); + CEffectManager::Instance().ApplyAlwaysHidden(); + } + + return dwEftID; + // MR-7: -- END OF -- Recover affect visual effects when coming out of invisibility } } else if (0 == rstrBoneName.compare("PART_WEAPON_LEFT")) { if (m_GraphicThingInstance.GetAttachingBoneName(CRaceData::PART_WEAPON_LEFT, &c_szBoneName)) { - return m_GraphicThingInstance.AttachEffectByID(0, c_szBoneName, ms_adwCRCAffectEffect[eEftType]); + // MR-7: Recover affect visual effects when coming out of invisibility + DWORD dwEftID = m_GraphicThingInstance.AttachEffectByID(0, c_szBoneName, ms_adwCRCAffectEffect[eEftType]); + + if (dwEftID && IsAffect(AFFECT_INVISIBILITY)) + { + CEffectManager::Instance().SelectEffectInstance(dwEftID); + CEffectManager::Instance().HideEffect(); + CEffectManager::Instance().ApplyAlwaysHidden(); + } + + return dwEftID; + // MR-7: -- END OF -- Recover affect visual effects when coming out of invisibility } } else { - return m_GraphicThingInstance.AttachEffectByID(0, rstrBoneName.c_str(), ms_adwCRCAffectEffect[eEftType]); + // MR-7: Recover affect visual effects when coming out of invisibility + DWORD dwEftID = m_GraphicThingInstance.AttachEffectByID(0, rstrBoneName.c_str(), ms_adwCRCAffectEffect[eEftType]); + + if (dwEftID && IsAffect(AFFECT_INVISIBILITY)) + { + CEffectManager::Instance().SelectEffectInstance(dwEftID); + CEffectManager::Instance().HideEffect(); + CEffectManager::Instance().ApplyAlwaysHidden(); + } + + return dwEftID; + // MR-7: -- END OF -- Recover affect visual effects when coming out of invisibility } } diff --git a/src/UserInterface/Packet.h b/src/UserInterface/Packet.h index f05c83c..7a5f4e8 100644 --- a/src/UserInterface/Packet.h +++ b/src/UserInterface/Packet.h @@ -2260,7 +2260,8 @@ enum SPECIAL_EFFECT SE_EQUIP_RAMADAN_RING, // 초승달의 반지를 착용하는 순간에 발동하는 이펙트 SE_EQUIP_HALLOWEEN_CANDY, // 할로윈 사탕을 착용(-_-;)한 순간에 발동하는 이펙트 SE_EQUIP_HAPPINESS_RING, // 크리스마스 행복의 반지를 착용하는 순간에 발동하는 이펙트 - SE_EQUIP_LOVE_PENDANT, // 발렌타인 사랑의 팬던트(71145) 착용할 때 이펙트 (발동이펙트임, 지속이펙트 아님) + SE_EQUIP_LOVE_PENDANT, // 발렌타인 사랑의 팬던트(71145) 착용할 때 이펙트 (발동이펙트임, 지속이펙트 아님), + SE_AGGREGATE_MONSTER, }; typedef struct SPacketGCSpecialEffect diff --git a/src/UserInterface/PythonCharacterManagerModule.cpp b/src/UserInterface/PythonCharacterManagerModule.cpp index 4f8e1f6..e1d6690 100644 --- a/src/UserInterface/PythonCharacterManagerModule.cpp +++ b/src/UserInterface/PythonCharacterManagerModule.cpp @@ -713,6 +713,12 @@ PyObject * chrmgrIsPossibleEmoticon(PyObject* poSelf, PyObject* poArgs) return Py_BuildValue("i", result); } +PyObject * chrmgrPreloadRaceMotions(PyObject* poSelf, PyObject* poArgs) +{ + CRaceManager::PreloadPlayerRaceMotions(); + return Py_BuildNone(); +} + void initchrmgr() { static PyMethodDef s_methods[] = @@ -746,6 +752,7 @@ void initchrmgr() { "SetAffect", chrmgrSetAffect, METH_VARARGS }, { "SetEmoticon", chrmgrSetEmoticon, METH_VARARGS }, { "IsPossibleEmoticon", chrmgrIsPossibleEmoticon, METH_VARARGS }, + { "PreloadRaceMotions", chrmgrPreloadRaceMotions, METH_VARARGS }, { "RegisterEffect", chrmgrRegisterEffect, METH_VARARGS }, { "RegisterCacheEffect", chrmgrRegisterCacheEffect, METH_VARARGS }, { "RegisterPointEffect", chrmgrRegisterPointEffect, METH_VARARGS }, @@ -841,4 +848,5 @@ void initchrmgr() PyModule_AddIntConstant(poModule, "EFFECT_HAPPINESS_RING_EQUIP", CInstanceBase::EFFECT_HAPPINESS_RING_EQUIP); PyModule_AddIntConstant(poModule, "EFFECT_LOVE_PENDANT_EQUIP", CInstanceBase::EFFECT_LOVE_PENDANT_EQUIP); + PyModule_AddIntConstant(poModule, "EFFECT_AGGREGATE_MONSTER", CInstanceBase::EFFECT_AGGREGATE_MONSTER); } diff --git a/src/UserInterface/PythonChat.cpp b/src/UserInterface/PythonChat.cpp index 1fb2048..ad4fe0e 100644 --- a/src/UserInterface/PythonChat.cpp +++ b/src/UserInterface/PythonChat.cpp @@ -499,9 +499,27 @@ void CPythonChat::AppendChat(int iType, const char * c_szChat) SChatLine * pChatLine = SChatLine::New(); pChatLine->iType = iType; - // Pass chat text as-is to BiDi algorithm - // BuildVisualBidiText_Tagless will detect chat format and handle reordering - pChatLine->Instance.SetValue(c_szChat); + // Parse chat format "name : message" for proper BiDi handling + // This avoids issues with usernames containing " : " + const char* colonPos = strstr(c_szChat, " : "); + if (colonPos && colonPos != c_szChat) // Make sure " : " is not at the start + { + // Extract name and message + size_t nameLen = colonPos - c_szChat; + const char* msgStart = colonPos + 3; // Skip " : " + + // Create temporary buffers + std::string name(c_szChat, nameLen); + std::string message(msgStart); + + // Use new SetChatValue API for proper BiDi handling + pChatLine->Instance.SetChatValue(name.c_str(), message.c_str()); + } + else + { + // Fallback: Not in chat format (INFO, NOTICE, etc.) + pChatLine->Instance.SetValue(c_szChat); + } if (IsRTL()) { @@ -768,9 +786,23 @@ void CWhisper::AppendChat(int iType, const char * c_szChat) SChatLine * pChatLine = SChatLine::New(); - // Pass chat text as-is to BiDi algorithm - // BuildVisualBidiText_Tagless will detect chat format and handle reordering - pChatLine->Instance.SetValue(c_szChat); + // Parse chat format "name : message" for proper BiDi handling + const char* colonPos = strstr(c_szChat, " : "); + if (colonPos && colonPos != c_szChat) + { + // Extract name and message + size_t nameLen = colonPos - c_szChat; + const char* msgStart = colonPos + 3; + + std::string name(c_szChat, nameLen); + std::string message(msgStart); + + pChatLine->Instance.SetChatValue(name.c_str(), message.c_str()); + } + else + { + pChatLine->Instance.SetValue(c_szChat); + } if (IsRTL()) { diff --git a/src/UserInterface/PythonMiniMap.cpp b/src/UserInterface/PythonMiniMap.cpp index 7767832..f199d07 100644 --- a/src/UserInterface/PythonMiniMap.cpp +++ b/src/UserInterface/PythonMiniMap.cpp @@ -222,7 +222,9 @@ void CPythonMiniMap::Update(float fCenterX, float fCenterY) } } - const float c_fMiniMapWindowRadius = 55.0f; + // Calculate dynamic radius based on actual minimap window size + // Subtract border width (approx 9 pixels) to keep markers inside visible area + const float c_fMiniMapWindowRadius = (m_fWidth < m_fHeight ? m_fWidth : m_fHeight) / 2.0f - 9.0f; float fDistanceFromCenterX = (rAtlasMarkInfo.m_fX - m_fCenterX) * fooCellScale * m_fScale; float fDistanceFromCenterY = (rAtlasMarkInfo.m_fY - m_fCenterY) * fooCellScale * m_fScale; @@ -230,12 +232,11 @@ void CPythonMiniMap::Update(float fCenterX, float fCenterY) if (fDistanceFromCenter >= c_fMiniMapWindowRadius) { - float fRadianX = acosf(fDistanceFromCenterX / fDistanceFromCenter); - float fRadianY = asinf(fDistanceFromCenterY / fDistanceFromCenter); - fDistanceFromCenterX = 55.0f * cosf(fRadianX); - fDistanceFromCenterY = 55.0f * sinf(fRadianY); - rAtlasMarkInfo.m_fMiniMapX = ( m_fWidth - (float)m_WhiteMark.GetWidth() ) / 2.0f + fDistanceFromCenterX + m_fScreenX + 2.0f; - rAtlasMarkInfo.m_fMiniMapY = ( m_fHeight - (float)m_WhiteMark.GetHeight() ) / 2.0f + fDistanceFromCenterY + m_fScreenY + 2.0f; + float fRadian = atan2f(fDistanceFromCenterY, fDistanceFromCenterX); + fDistanceFromCenterX = c_fMiniMapWindowRadius * cosf(fRadian); + fDistanceFromCenterY = c_fMiniMapWindowRadius * sinf(fRadian); + rAtlasMarkInfo.m_fMiniMapX = ( m_fWidth - (float)m_WhiteMark.GetWidth() ) / 2.0f + fDistanceFromCenterX + m_fScreenX; + rAtlasMarkInfo.m_fMiniMapY = ( m_fHeight - (float)m_WhiteMark.GetHeight() ) / 2.0f + fDistanceFromCenterY + m_fScreenY; } else { @@ -465,7 +466,10 @@ void CPythonMiniMap::Render(float fScreenX, float fScreenY) if (rAtlasMarkInfo.m_fMiniMapY <= 0.0f) continue; - __RenderTargetMark(rAtlasMarkInfo.m_fMiniMapX, rAtlasMarkInfo.m_fMiniMapY); + __RenderTargetMark( + rAtlasMarkInfo.m_fMiniMapX + m_WhiteMark.GetWidth() / 2, + rAtlasMarkInfo.m_fMiniMapY + m_WhiteMark.GetHeight() / 2 + ); } } @@ -665,7 +669,10 @@ bool CPythonMiniMap::Create() void CPythonMiniMap::__SetPosition() { - m_fMiniMapRadius = fMIN(6400.0f / ((float) CTerrainImpl::CELLSCALE) * m_fScale, 64.0f); + // Calculate dynamic radius - use smaller dimension to ensure circular clipping + // Subtract border width (approx 9 pixels) to keep markers inside visible area + float fWindowRadius = (m_fWidth < m_fHeight ? m_fWidth : m_fHeight) / 2.0f - 9.0f; + m_fMiniMapRadius = fMIN(6400.0f / ((float) CTerrainImpl::CELLSCALE) * m_fScale, fWindowRadius); m_matWorld._11 = m_fWidth * m_fScale; m_matWorld._22 = m_fHeight * m_fScale; @@ -1022,11 +1029,19 @@ void CPythonMiniMap::RenderAtlas(float fScreenX, float fScreenY) if (TYPE_TARGET == rAtlasMarkInfo.m_byType) { - __RenderMiniWayPointMark(rAtlasMarkInfo.m_fScreenX, rAtlasMarkInfo.m_fScreenY); + // Convert from WhiteMark-centered to actual center for rendering + __RenderMiniWayPointMark( + rAtlasMarkInfo.m_fScreenX + m_WhiteMark.GetWidth() / 2, + rAtlasMarkInfo.m_fScreenY + m_WhiteMark.GetHeight() / 2 + ); } else { - __RenderWayPointMark(rAtlasMarkInfo.m_fScreenX, rAtlasMarkInfo.m_fScreenY); + // Convert from WhiteMark-centered to actual center for rendering + __RenderWayPointMark( + rAtlasMarkInfo.m_fScreenX + m_WhiteMark.GetWidth() / 2, + rAtlasMarkInfo.m_fScreenY + m_WhiteMark.GetHeight() / 2 + ); } } diff --git a/src/UserInterface/PythonNetworkStreamPhaseGameItem.cpp b/src/UserInterface/PythonNetworkStreamPhaseGameItem.cpp index 5ddce82..ee32244 100644 --- a/src/UserInterface/PythonNetworkStreamPhaseGameItem.cpp +++ b/src/UserInterface/PythonNetworkStreamPhaseGameItem.cpp @@ -849,7 +849,9 @@ bool CPythonNetworkStream::RecvSpecialEffect() case SE_EQUIP_LOVE_PENDANT: effect = CInstanceBase::EFFECT_LOVE_PENDANT_EQUIP; break; - + case SE_AGGREGATE_MONSTER: + effect = CInstanceBase::EFFECT_AGGREGATE_MONSTER; + break; default: TraceError("%d 는 없는 스페셜 이펙트 번호입니다.TPacketGCSpecialEffect",kSpecialEffect.type); diff --git a/src/UserInterface/PythonTextTail.cpp b/src/UserInterface/PythonTextTail.cpp index 618cd54..8c1db1d 100644 --- a/src/UserInterface/PythonTextTail.cpp +++ b/src/UserInterface/PythonTextTail.cpp @@ -11,6 +11,7 @@ #include "MarkManager.h" #include +// EPlaceDir and TextTailBiDi() template are defined in utf8.h const D3DXCOLOR c_TextTail_Player_Color = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f); const D3DXCOLOR c_TextTail_Monster_Color = D3DXCOLOR(1.0f, 0.0f, 0.0f, 1.0f); diff --git a/src/UserInterface/UserInterface.cpp b/src/UserInterface/UserInterface.cpp index f7e18f8..cafbdfe 100644 --- a/src/UserInterface/UserInterface.cpp +++ b/src/UserInterface/UserInterface.cpp @@ -16,6 +16,8 @@ #include #include +#include +#include #include #include @@ -166,11 +168,45 @@ bool PackInitialize(const char * c_pszFolder) "uiloading", }; - CPackManager::instance().AddPack(std::format("{}/root.pck", c_pszFolder)); - for (const std::string& packFileName : packFiles) { - CPackManager::instance().AddPack(std::format("{}/{}.pck", c_pszFolder, packFileName)); + Tracef("PackInitialize: Loading root.pck..."); + if (!CPackManager::instance().AddPack(std::format("{}/root.pck", c_pszFolder))) + { + TraceError("Failed to load root.pck"); + return false; } + Tracef("PackInitialize: Loading %d pack files in parallel...", packFiles.size()); + const size_t numThreads = std::min(std::thread::hardware_concurrency(), packFiles.size()); + const size_t packsPerThread = (packFiles.size() + numThreads - 1) / numThreads; + + std::vector threads; + std::atomic failedCount(0); + + for (size_t t = 0; t < numThreads; ++t) + { + threads.emplace_back([&, t]() { + size_t start = t * packsPerThread; + size_t end = std::min(start + packsPerThread, packFiles.size()); + + for (size_t i = start; i < end; ++i) + { + std::string packPath = std::format("{}/{}.pck", c_pszFolder, packFiles[i]); + if (!CPackManager::instance().AddPack(packPath)) + { + TraceError("Failed to load %s", packPath.c_str()); + failedCount++; + } + } + }); + } + + // Wait for all threads to complete + for (auto& thread : threads) + { + thread.join(); + } + + Tracef("PackInitialize: Completed! Failed: %d / %d", failedCount.load(), packFiles.size()); return true; }