Merge branch 'main' into fix/client-freeze-on-drag
This commit is contained in:
359
extern/include/utf8.h
vendored
359
extern/include/utf8.h
vendored
@@ -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 <cstdio>
|
||||
@@ -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<wchar_t> 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<wchar_t> visual;
|
||||
visual.reserve((size_t)n);
|
||||
|
||||
if (msgHasRTL)
|
||||
{
|
||||
// Arabic message: apply BiDi to message, then add " : name"
|
||||
std::vector<wchar_t> 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<wchar_t> 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<wchar_t> 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<wchar_t> 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<wchar_t> 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<wchar_t> 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<wchar_t> 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<wchar_t> msgVisual = BuildVisualBidiText_Tagless(msg, msgLen, false);
|
||||
visual.insert(visual.end(), msgVisual.begin(), msgVisual.end());
|
||||
}
|
||||
}
|
||||
|
||||
return visual;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TextTail formatting for RTL UI
|
||||
// ============================================================================
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -534,7 +534,7 @@ void CGraphicThingInstance::RegisterMotionThing(DWORD dwMotionKey, CGraphicThing
|
||||
{
|
||||
CGraphicThing::TRef * pMotionRef = new CGraphicThing::TRef;
|
||||
pMotionRef->SetPointer(pMotionThing);
|
||||
m_roMotionThingMap.insert(std::map<DWORD, CGraphicThing::TRef *>::value_type(dwMotionKey, pMotionRef));
|
||||
m_roMotionThingMap.insert(std::make_pair(dwMotionKey, pMotionRef));
|
||||
}
|
||||
|
||||
void CGraphicThingInstance::ResetLocalTime()
|
||||
|
||||
103
src/EterLib/BufferPool.cpp
Normal file
103
src/EterLib/BufferPool.cpp
Normal file
@@ -0,0 +1,103 @@
|
||||
#include "StdAfx.h"
|
||||
#include "BufferPool.h"
|
||||
#include <algorithm>
|
||||
|
||||
CBufferPool::CBufferPool()
|
||||
: m_totalAllocated(0)
|
||||
{
|
||||
}
|
||||
|
||||
CBufferPool::~CBufferPool()
|
||||
{
|
||||
Clear();
|
||||
}
|
||||
|
||||
std::vector<uint8_t> CBufferPool::Acquire(size_t minSize)
|
||||
{
|
||||
std::lock_guard<std::mutex> 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<uint8_t> result = std::move(m_pool[bestIndex].buffer);
|
||||
m_pool.erase(m_pool.begin() + bestIndex);
|
||||
result.clear();
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> newBuffer;
|
||||
newBuffer.reserve(minSize);
|
||||
m_totalAllocated++;
|
||||
return newBuffer;
|
||||
}
|
||||
|
||||
void CBufferPool::Release(std::vector<uint8_t>&& buffer)
|
||||
{
|
||||
size_t capacity = buffer.capacity();
|
||||
|
||||
if (capacity == 0 || capacity > MAX_BUFFER_SIZE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> 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<std::mutex> lock(m_mutex);
|
||||
return m_pool.size();
|
||||
}
|
||||
|
||||
size_t CBufferPool::GetTotalAllocated() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return m_totalAllocated;
|
||||
}
|
||||
|
||||
size_t CBufferPool::GetTotalMemoryPooled() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
size_t total = 0;
|
||||
for (const auto& buf : m_pool)
|
||||
{
|
||||
total += buf.capacity;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
void CBufferPool::Clear()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_pool.clear();
|
||||
}
|
||||
50
src/EterLib/BufferPool.h
Normal file
50
src/EterLib/BufferPool.h
Normal file
@@ -0,0 +1,50 @@
|
||||
#ifndef __INC_ETERLIB_BUFFERPOOL_H__
|
||||
#define __INC_ETERLIB_BUFFERPOOL_H__
|
||||
|
||||
#include <vector>
|
||||
#include <mutex>
|
||||
#include <cstdint>
|
||||
|
||||
// Buffer pool for file I/O operations
|
||||
class CBufferPool
|
||||
{
|
||||
public:
|
||||
CBufferPool();
|
||||
~CBufferPool();
|
||||
|
||||
// Get buffer with minimum size
|
||||
std::vector<uint8_t> Acquire(size_t minSize);
|
||||
|
||||
// Return buffer to pool
|
||||
void Release(std::vector<uint8_t>&& 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<uint8_t> buffer;
|
||||
size_t capacity;
|
||||
|
||||
TPooledBuffer(std::vector<uint8_t>&& buf)
|
||||
: buffer(std::move(buf))
|
||||
, capacity(buffer.capacity())
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<TPooledBuffer> 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__
|
||||
59
src/EterLib/DecodedImageData.h
Normal file
59
src/EterLib/DecodedImageData.h
Normal file
@@ -0,0 +1,59 @@
|
||||
#ifndef __INC_ETERLIB_DECODEDIMAGEDATA_H__
|
||||
#define __INC_ETERLIB_DECODEDIMAGEDATA_H__
|
||||
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <d3d9.h>
|
||||
|
||||
// Decoded image data for GPU upload
|
||||
struct TDecodedImageData
|
||||
{
|
||||
enum EFormat
|
||||
{
|
||||
FORMAT_UNKNOWN = 0,
|
||||
FORMAT_RGBA8,
|
||||
FORMAT_RGB8,
|
||||
FORMAT_DDS,
|
||||
};
|
||||
|
||||
std::vector<uint8_t> 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__
|
||||
270
src/EterLib/FileLoaderThreadPool.cpp
Normal file
270
src/EterLib/FileLoaderThreadPool.cpp
Normal file
@@ -0,0 +1,270 @@
|
||||
#include "StdAfx.h"
|
||||
#include "FileLoaderThreadPool.h"
|
||||
#include "BufferPool.h"
|
||||
#include "ImageDecoder.h"
|
||||
#include "PackLib/PackManager.h"
|
||||
#include <algorithm>
|
||||
|
||||
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<TLoadResult>(COMPLETED_QUEUE_SIZE);
|
||||
|
||||
m_workers.reserve(threadCount);
|
||||
for (unsigned int i = 0; i < threadCount; ++i)
|
||||
{
|
||||
TWorkerThread worker;
|
||||
worker.pRequestQueue = new SPSCQueue<TLoadRequest>(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<TLoadRequest>* 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);
|
||||
}
|
||||
90
src/EterLib/FileLoaderThreadPool.h
Normal file
90
src/EterLib/FileLoaderThreadPool.h
Normal file
@@ -0,0 +1,90 @@
|
||||
#ifndef __INC_ETERLIB_FILELOADERTHREADPOOL_H__
|
||||
#define __INC_ETERLIB_FILELOADERTHREADPOOL_H__
|
||||
|
||||
#include <vector>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#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<TLoadRequest>* pRequestQueue;
|
||||
std::atomic<bool> 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<TWorkerThread> m_workers;
|
||||
SPSCQueue<TLoadResult>* m_pCompletedQueue;
|
||||
|
||||
std::atomic<bool> m_bShutdown;
|
||||
std::atomic<uint32_t> m_nextRequestID;
|
||||
std::atomic<int> 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__
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -2,9 +2,15 @@
|
||||
#include "PackLib/PackManager.h"
|
||||
#include "GrpImageTexture.h"
|
||||
#include "EterImageLib/DDSTextureLoader9.h"
|
||||
#include "DecodedImageData.h"
|
||||
|
||||
#include <stb_image.h>
|
||||
|
||||
#if defined(_M_IX86) || defined(_M_X64)
|
||||
#include <emmintrin.h> // SSE2
|
||||
#include <tmmintrin.h> // 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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -211,7 +211,7 @@ void CGraphicObjectInstance::ReleaseAlwaysHidden() {
|
||||
|
||||
bool CGraphicObjectInstance::isShow()
|
||||
{
|
||||
return m_isVisible && !m_isAlwaysHidden;
|
||||
return m_isVisible;
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -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<wchar_t> 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<wchar_t> 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<wchar_t> 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<wchar_t> 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<TVisChar>& vis, bool forceRTL)
|
||||
{
|
||||
if (vis.empty())
|
||||
return;
|
||||
|
||||
// Extract only characters
|
||||
std::vector<wchar_t> 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<wchar_t> 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<TVisChar> 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<wchar_t> 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<std::wstring> linkTargets; // linkTargets[i] is target text for link i
|
||||
linkTargets.reserve(4); // Reserve space for typical number of links
|
||||
|
||||
std::vector<TVisChar> 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<wchar_t> 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<wchar_t> 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<LinkRange> 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<wchar_t> 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<wchar_t> 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<wchar_t> 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 "+<digits>" pattern and reverse to "<digits>+"
|
||||
if (pos < closeBracket && linkVisual[pos] == L'+')
|
||||
if (openBracket >= 0 && closeBracket > openBracket)
|
||||
{
|
||||
int digitStart = pos + 1;
|
||||
int digitEnd = digitStart;
|
||||
// Extract content between brackets
|
||||
std::vector<wchar_t> 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<wchar_t> 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<std::vector<TVisChar>> segments;
|
||||
segments.reserve(estimatedSegments); // Estimate: links + text between
|
||||
|
||||
std::vector<bool> 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<wchar_t> 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<TVisChar> 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<SVertex> 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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
92
src/EterLib/ImageDecoder.cpp
Normal file
92
src/EterLib/ImageDecoder.cpp
Normal file
@@ -0,0 +1,92 @@
|
||||
#include "StdAfx.h"
|
||||
#include "ImageDecoder.h"
|
||||
#include "EterImageLib/DDSTextureLoader9.h"
|
||||
#include <stb_image.h>
|
||||
|
||||
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;
|
||||
}
|
||||
18
src/EterLib/ImageDecoder.h
Normal file
18
src/EterLib/ImageDecoder.h
Normal file
@@ -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__
|
||||
@@ -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<CGraphicImage*>(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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
#include "Resource.h"
|
||||
#include "FileLoaderThread.h"
|
||||
#include "FileLoaderThreadPool.h"
|
||||
|
||||
#include <set>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
class CTextureCache;
|
||||
|
||||
class CResourceManager : public CSingleton<CResourceManager>
|
||||
{
|
||||
public:
|
||||
@@ -42,6 +45,9 @@ class CResourceManager : public CSingleton<CResourceManager>
|
||||
void ProcessBackgroundLoading();
|
||||
void PushBackgroundLoadingSet(std::set<std::string> & LoadingSet);
|
||||
|
||||
CTextureCache* GetTextureCache() { return m_pTextureCache; }
|
||||
CFileLoaderThreadPool* GetLoaderThreadPool() { return m_pLoaderThreadPool; }
|
||||
|
||||
protected:
|
||||
void __DestroyDeletingResourceMap();
|
||||
void __DestroyResourceMap();
|
||||
@@ -68,6 +74,8 @@ class CResourceManager : public CSingleton<CResourceManager>
|
||||
TResourceRefDecreaseWaitingMap m_pResRefDecreaseWaitingMap;
|
||||
|
||||
static CFileLoaderThread ms_loadingThread;
|
||||
CFileLoaderThreadPool* m_pLoaderThreadPool;
|
||||
CTextureCache* m_pTextureCache;
|
||||
};
|
||||
|
||||
extern int g_iLoadingDelayTime;
|
||||
79
src/EterLib/SPSCQueue.h
Normal file
79
src/EterLib/SPSCQueue.h
Normal file
@@ -0,0 +1,79 @@
|
||||
#ifndef __INC_ETERLIB_SPSCQUEUE_H__
|
||||
#define __INC_ETERLIB_SPSCQUEUE_H__
|
||||
|
||||
#include <atomic>
|
||||
#include <vector>
|
||||
#include <cassert>
|
||||
|
||||
// Lock-free queue for single producer/consumer pairs
|
||||
template<typename T>
|
||||
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<T> m_buffer;
|
||||
|
||||
alignas(64) std::atomic<size_t> m_head;
|
||||
alignas(64) std::atomic<size_t> m_tail;
|
||||
};
|
||||
|
||||
#endif // __INC_ETERLIB_SPSCQUEUE_H__
|
||||
109
src/EterLib/TextureCache.cpp
Normal file
109
src/EterLib/TextureCache.cpp
Normal file
@@ -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<std::mutex> 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<std::mutex> 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<std::mutex> 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();
|
||||
}
|
||||
55
src/EterLib/TextureCache.h
Normal file
55
src/EterLib/TextureCache.h
Normal file
@@ -0,0 +1,55 @@
|
||||
#ifndef __INC_ETERLIB_TEXTURECACHE_H__
|
||||
#define __INC_ETERLIB_TEXTURECACHE_H__
|
||||
|
||||
#include <unordered_map>
|
||||
#include <list>
|
||||
#include <string>
|
||||
#include <mutex>
|
||||
|
||||
// LRU cache for decoded textures
|
||||
class CTextureCache
|
||||
{
|
||||
public:
|
||||
struct TCachedTexture
|
||||
{
|
||||
std::vector<uint8_t> 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<std::string> m_lruList;
|
||||
std::unordered_map<std::string, std::pair<TCachedTexture, std::list<std::string>::iterator>> m_cache;
|
||||
|
||||
mutable std::mutex m_mutex;
|
||||
std::atomic<size_t> m_hits;
|
||||
std::atomic<size_t> m_misses;
|
||||
};
|
||||
|
||||
#endif // __INC_ETERLIB_TEXTURECACHE_H__
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ class CRaceManager : public CSingleton<CRaceManager>
|
||||
|
||||
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<CRaceManager>
|
||||
private:
|
||||
std::string m_strPathName;
|
||||
CRaceData * m_pSelectedRaceData;
|
||||
static bool s_bPreloaded;
|
||||
};
|
||||
@@ -56,6 +56,27 @@ bool CTextureSet::Load(const char * c_szTextureSetFileName, float fTerrainTexCoo
|
||||
|
||||
m_Textures.resize(lCount + 1);
|
||||
|
||||
std::vector<std::string> 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);
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
#include "Pack.h"
|
||||
#include "EterLib/BufferPool.h"
|
||||
#include <zstd.h>
|
||||
|
||||
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<uint8_t> compressed_data(entry.compressed_size);
|
||||
std::vector<uint8_t> 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;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
#include "config.h"
|
||||
|
||||
class CBufferPool;
|
||||
|
||||
class CPack : public std::enable_shared_from_this<CPack>
|
||||
{
|
||||
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;
|
||||
|
||||
@@ -1,14 +1,38 @@
|
||||
#include "PackManager.h"
|
||||
#include "EterLib/BufferPool.h"
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
|
||||
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<CPack> pack = std::make_shared<CPack>();
|
||||
|
||||
std::lock_guard<std::mutex> 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;
|
||||
}
|
||||
|
||||
@@ -1,26 +1,34 @@
|
||||
#pragma once
|
||||
#include <unordered_map>
|
||||
#include <mutex>
|
||||
|
||||
#include "EterBase/Singleton.h"
|
||||
#include "Pack.h"
|
||||
|
||||
class CBufferPool;
|
||||
|
||||
class CPackManager : public CSingleton<CPackManager>
|
||||
{
|
||||
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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -372,6 +372,7 @@ class CInstanceBase
|
||||
EFFECT_HAPPINESS_RING_EQUIP, // 행복의 반지 착용 순간에 발동하는 이펙트
|
||||
EFFECT_LOVE_PENDANT_EQUIP, // 행복의 반지 착용 순간에 발동하는 이펙트
|
||||
EFFECT_TEMP,
|
||||
EFFECT_AGGREGATE_MONSTER,
|
||||
EFFECT_NUM,
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "MarkManager.h"
|
||||
|
||||
#include <utf8.h>
|
||||
// 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);
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <utf8.h>
|
||||
@@ -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<size_t>(std::thread::hardware_concurrency(), packFiles.size());
|
||||
const size_t packsPerThread = (packFiles.size() + numThreads - 1) / numThreads;
|
||||
|
||||
std::vector<std::thread> threads;
|
||||
std::atomic<size_t> 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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user