mirror of
https://github.com/leozide/leocad.git
synced 2026-10-26 22:10:32 +00:00
Replace manual piece-data retain/release bookkeeping with shared copyable asset-reference handles. Pieces, models, selection, minifigs, thumbnails, and stored insertion previews retain their dependencies explicitly, while transient queries continue to borrow pointers. Separate registered lookup and acquisition from asynchronous load requests and explicit readiness waits. Keep loader-held references through work and notification delivery, resetting shared request handles on the UI thread and guarding callback-triggered library teardown. Validated with the Qt 6.10 debug build and 26 offscreen lifecycle checks, including cancellation, stale requests, copy/move operations, model/preview teardown, undo, project merge/export, and loader shutdown.
2862 lines
77 KiB
C++
2862 lines
77 KiB
C++
#include "lc_global.h"
|
|
#include "lc_library.h"
|
|
#include "lc_assetloader.h"
|
|
#include "lc_thumbnailmanager.h"
|
|
#include "lc_zipfile.h"
|
|
#include "lc_file.h"
|
|
#include "lc_piecedata.h"
|
|
#include "lc_colors.h"
|
|
#include "lc_texture.h"
|
|
#include "lc_texturemanager.h"
|
|
#include "lc_category.h"
|
|
#include "lc_application.h"
|
|
#include "lc_context.h"
|
|
#include "lc_glextensions.h"
|
|
#include "lc_synth.h"
|
|
#include "lc_traintrack.h"
|
|
#include "project.h"
|
|
#include "lc_profile.h"
|
|
#include "lc_meshloader.h"
|
|
#include "lc_model.h"
|
|
#include "lc_view.h"
|
|
#include "lc_string.h"
|
|
#include <zlib.h>
|
|
|
|
#if MAX_MEM_LEVEL >= 8
|
|
# define DEF_MEM_LEVEL 8
|
|
#else
|
|
# define DEF_MEM_LEVEL MAX_MEM_LEVEL
|
|
#endif
|
|
|
|
constexpr quint32 LC_LIBRARY_CACHE_VERSION = 0x0110;
|
|
constexpr qint32 LC_LIBRARY_MESH_CACHE_VERSION = 2;
|
|
|
|
enum class lcLibraryCacheFlag : quint32
|
|
{
|
|
Archive = 0x0001,
|
|
Directory = 0x0002
|
|
};
|
|
|
|
lcPiecesLibrary::lcPiecesLibrary(lcStudStyle StudStyle, bool StudCylinderColorEnabled)
|
|
: mStudStyle(StudStyle), mStudCylinderColorEnabled(StudCylinderColorEnabled)
|
|
{
|
|
mTextureManager = std::make_unique<lcTextureManager>();
|
|
mAssetLoader = std::unique_ptr<lcAssetLoader>(new lcAssetLoader(this));
|
|
mThumbnailManager = std::unique_ptr<lcThumbnailManager>(new lcThumbnailManager(this));
|
|
mLoadingMesh = std::unique_ptr<lcMesh>(new lcMesh);
|
|
mLoadingMesh->CreateBox();
|
|
QStringList cachePathList = QStandardPaths::standardLocations(QStandardPaths::CacheLocation);
|
|
mCachePath = cachePathList.first();
|
|
|
|
QDir Dir;
|
|
Dir.mkpath(mCachePath);
|
|
|
|
mNumOfficialPieces = 0;
|
|
mBuffersDirty = false;
|
|
mHasUnofficial = false;
|
|
}
|
|
|
|
lcPiecesLibrary::~lcPiecesLibrary()
|
|
{
|
|
mThumbnailManager.reset();
|
|
mCancelLoading.store(true);
|
|
mAssetLoader.reset();
|
|
Unload();
|
|
ReleaseBuffers();
|
|
}
|
|
|
|
void lcPiecesLibrary::Unload()
|
|
{
|
|
const bool HadLoader = static_cast<bool>(mAssetLoader);
|
|
|
|
if (HadLoader && mThumbnailManager)
|
|
mThumbnailManager->Clear();
|
|
|
|
mAssetLoader.reset();
|
|
mSynthPieces.clear();
|
|
|
|
mPieceErrorMutex.lock();
|
|
mFailedPartErrors.clear();
|
|
mPieceErrorMutex.unlock();
|
|
|
|
// Destroy external projects before their model identities. Their teardown
|
|
// releases child references, including references to the shared catalog.
|
|
std::unordered_set<Project*> ExternalProjects;
|
|
|
|
for (const auto& Entry : mProjectPieces)
|
|
if (Entry.first->IsProject())
|
|
ExternalProjects.insert(Entry.first->GetProject());
|
|
|
|
for (;;)
|
|
{
|
|
const auto External = std::find_if(mProjectPieces.begin(), mProjectPieces.end(), [&ExternalProjects](const auto& Entry)
|
|
{
|
|
return Entry.first->IsProject() && ExternalProjects.find(Entry.second) == ExternalProjects.end();
|
|
});
|
|
|
|
if (External == mProjectPieces.end())
|
|
break;
|
|
|
|
RemovePieceData(External->first);
|
|
}
|
|
|
|
while (!mProjectPieces.empty())
|
|
RemovePieceData(mProjectPieces.begin()->first);
|
|
|
|
for (const auto& PieceIt : mPieces)
|
|
delete PieceIt.second;
|
|
|
|
mPieces.clear();
|
|
|
|
mSources.clear();
|
|
|
|
mTextureManager->Clear();
|
|
mTextureDiskEntries.clear();
|
|
|
|
for (std::map<QString, int>& Entries : mTextureArchiveEntries)
|
|
Entries.clear();
|
|
|
|
mNumOfficialPieces = 0;
|
|
|
|
for (std::unique_ptr<lcZipFile>& ZipFile : mZipFiles)
|
|
ZipFile.reset();
|
|
|
|
if (HadLoader)
|
|
mAssetLoader = std::unique_ptr<lcAssetLoader>(new lcAssetLoader(this));
|
|
}
|
|
|
|
void lcPiecesLibrary::RemoveTemporaryPieceData()
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
|
|
for (auto PieceIt = mPieces.begin(); PieceIt != mPieces.end();)
|
|
{
|
|
lcPieceData* PieceData = PieceIt->second;
|
|
|
|
if (!PieceData->IsLibraryPiece() && PieceData->GetRefCount() == 0)
|
|
{
|
|
ClearPieceDataLoadError(PieceData);
|
|
PieceIt = mPieces.erase(PieceIt);
|
|
delete PieceData;
|
|
}
|
|
else
|
|
PieceIt++;
|
|
}
|
|
|
|
std::vector<lcPieceData*> Unused;
|
|
|
|
for (const std::unordered_map<lcPieceData*, Project*>::value_type& Entry : mProjectPieces)
|
|
{
|
|
if (Entry.first->GetRefCount() == 0)
|
|
Unused.push_back(Entry.first);
|
|
}
|
|
|
|
for (lcPieceData* PieceData : Unused)
|
|
UnloadPieceData(PieceData);
|
|
}
|
|
|
|
void lcPiecesLibrary::ReleaseProjectPieceData(Project* OwnerProject)
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
std::vector<lcPieceData*> Unused;
|
|
|
|
OwnerProject->ClearPieceDataIndex();
|
|
|
|
for (std::unordered_map<lcPieceData*, Project*>::value_type& Entry : mProjectPieces)
|
|
{
|
|
if (Entry.second != OwnerProject)
|
|
continue;
|
|
|
|
Entry.second = nullptr;
|
|
|
|
if (Entry.first->GetRefCount() == 0)
|
|
Unused.push_back(Entry.first);
|
|
}
|
|
|
|
for (lcPieceData* PieceData : Unused)
|
|
UnloadPieceData(PieceData);
|
|
}
|
|
|
|
void lcPiecesLibrary::TransferProjectPieceData(Project* Source, Project* Destination)
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
std::vector<lcPieceData*> Unused;
|
|
|
|
Source->TransferPieceDataIndexTo(Destination);
|
|
|
|
for (std::unordered_map<lcPieceData*, Project*>::value_type& Entry : mProjectPieces)
|
|
{
|
|
if (Entry.second == Source)
|
|
{
|
|
Entry.second = Destination;
|
|
|
|
if (Entry.first->GetRefCount() == 0)
|
|
Unused.push_back(Entry.first);
|
|
}
|
|
}
|
|
|
|
for (lcPieceData* PieceData : Unused)
|
|
UnloadPieceData(PieceData);
|
|
}
|
|
|
|
void lcPiecesLibrary::RegisterProjectPieceData(Project* Project, const std::string& Name, lcPieceData* PieceData)
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
Project->RegisterPieceData(Name, PieceData);
|
|
mProjectPieces[PieceData] = Project;
|
|
}
|
|
|
|
void lcPiecesLibrary::RemovePieceData(lcPieceData* PieceData)
|
|
{
|
|
ClearPieceDataLoadError(PieceData);
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
|
|
const std::unordered_map<lcPieceData*, Project*>::iterator Local = mProjectPieces.find(PieceData);
|
|
const bool IsLocal = Local != mProjectPieces.end();
|
|
|
|
if (IsLocal)
|
|
{
|
|
if (Local->second)
|
|
Local->second->UnregisterPieceData(PieceData);
|
|
|
|
mProjectPieces.erase(Local);
|
|
}
|
|
|
|
if (!IsLocal)
|
|
{
|
|
for (std::map<std::string, lcPieceData*>::iterator PieceIt = mPieces.begin(); PieceIt != mPieces.end(); PieceIt++)
|
|
{
|
|
if (PieceIt->second == PieceData)
|
|
{
|
|
mPieces.erase(PieceIt);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
delete PieceData;
|
|
}
|
|
|
|
void lcPiecesLibrary::UnloadPieceData(lcPieceData* PieceData)
|
|
{
|
|
if (!PieceData->IsLibraryPiece())
|
|
{
|
|
RemovePieceData(PieceData);
|
|
return;
|
|
}
|
|
|
|
ClearPieceDataLoadError(PieceData);
|
|
PieceData->Unload();
|
|
}
|
|
|
|
void lcPiecesLibrary::SetModelPieceDataName(lcPieceData* PieceData, const char* Name)
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
|
|
strncpy(PieceData->mFileName, Name, sizeof(PieceData->mFileName) - 1);
|
|
PieceData->mFileName[sizeof(PieceData->mFileName) - 1] = 0;
|
|
strncpy(PieceData->m_strDescription, Name, sizeof(PieceData->m_strDescription) - 1);
|
|
PieceData->m_strDescription[sizeof(PieceData->m_strDescription) - 1] = 0;
|
|
}
|
|
|
|
bool lcPiecesLibrary::RenamePieceData(lcPieceData* PieceData, const char* NewName)
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
|
|
const std::unordered_map<lcPieceData*, Project*>::iterator Local = mProjectPieces.find(PieceData);
|
|
const std::string NewPieceName = NormalizePieceDataName(NewName);
|
|
|
|
if (Local != mProjectPieces.end() && Local->second)
|
|
{
|
|
lcPieceData* Existing = Local->second->FindPieceData(NewPieceName);
|
|
|
|
if (Existing && Existing != PieceData)
|
|
return false;
|
|
}
|
|
else if (Local == mProjectPieces.end())
|
|
{
|
|
const std::map<std::string, lcPieceData*>::const_iterator Existing = mPieces.find(NewPieceName);
|
|
|
|
if (Existing != mPieces.end() && Existing->second != PieceData)
|
|
return false;
|
|
}
|
|
|
|
if (Local != mProjectPieces.end() && Local->second)
|
|
Local->second->UnregisterPieceData(PieceData);
|
|
|
|
if (Local == mProjectPieces.end())
|
|
{
|
|
for (std::map<std::string, lcPieceData*>::iterator PieceIt = mPieces.begin(); PieceIt != mPieces.end(); PieceIt++)
|
|
{
|
|
if (PieceIt->second == PieceData)
|
|
{
|
|
mPieces.erase(PieceIt);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
strncpy(PieceData->mFileName, NewName, sizeof(PieceData->mFileName));
|
|
PieceData->mFileName[sizeof(PieceData->mFileName) - 1] = 0;
|
|
strncpy(PieceData->m_strDescription, NewName, sizeof(PieceData->m_strDescription));
|
|
PieceData->m_strDescription[sizeof(PieceData->m_strDescription) - 1] = 0;
|
|
|
|
const std::string PieceName = NormalizePieceDataName(PieceData->mFileName);
|
|
|
|
if (Local != mProjectPieces.end())
|
|
{
|
|
if (Local->second)
|
|
Local->second->RegisterPieceData(PieceName, PieceData);
|
|
}
|
|
else
|
|
mPieces[PieceName] = PieceData;
|
|
|
|
LoadLock.unlock();
|
|
if (PieceData->mState != lcAssetState::Failed)
|
|
ClearPieceDataLoadError(PieceData);
|
|
|
|
return true;
|
|
}
|
|
|
|
std::string lcPiecesLibrary::NormalizePieceDataName(const char* PieceName)
|
|
{
|
|
std::string Name;
|
|
Name.reserve(LC_PIECE_NAME_LEN - 1);
|
|
|
|
for (const char* Src = PieceName; *Src && Name.size() < LC_PIECE_NAME_LEN - 1; Src++)
|
|
{
|
|
if (*Src == '\\')
|
|
Name.push_back('/');
|
|
else if (*Src >= 'a' && *Src <= 'z')
|
|
Name.push_back(*Src + 'A' - 'a');
|
|
else
|
|
Name.push_back(*Src);
|
|
}
|
|
|
|
return Name;
|
|
}
|
|
|
|
lcPieceDataRef lcPiecesLibrary::AcquireModelPieceData(const char* PieceName, Project* Project, bool& Reused)
|
|
{
|
|
const std::string Name = NormalizePieceDataName(PieceName);
|
|
lcPieceData* PieceData;
|
|
Reused = false;
|
|
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
|
|
PieceData = Project->FindPieceData(Name);
|
|
|
|
if (!PieceData || PieceData->IsModel())
|
|
{
|
|
PieceData = new lcPieceData(false);
|
|
PieceData->CreatePart(PieceName);
|
|
Project->RegisterPieceData(Name, PieceData);
|
|
mProjectPieces[PieceData] = Project;
|
|
}
|
|
else
|
|
Reused = true;
|
|
|
|
// Keep a local piece alive while a pending load finishes and the model takes its reference.
|
|
PieceData->AddRef();
|
|
}
|
|
|
|
lcPieceDataRef PieceDataRef = lcPieceDataRef::Adopt(PieceData, this);
|
|
|
|
if (Reused)
|
|
InvalidatePieceDataLoad(PieceData);
|
|
|
|
return PieceDataRef;
|
|
}
|
|
|
|
lcResult<Project*> lcPiecesLibrary::LoadExternalProject(const QFileInfo& FileInfo, bool Preview, bool DeferModelMeshRequests)
|
|
{
|
|
static thread_local std::vector<QString> LoadingPaths;
|
|
const QString Path = FileInfo.canonicalFilePath();
|
|
|
|
if (std::find(LoadingPaths.begin(), LoadingPaths.end(), Path) != LoadingPaths.end())
|
|
return lcUnexpected(tr("External project include cycle at '%1'.").arg(Path));
|
|
|
|
LoadingPaths.push_back(Path);
|
|
|
|
Project* ExternalProject = new Project(Preview);
|
|
ExternalProject->SetDeferModelMeshRequests(DeferModelMeshRequests);
|
|
const bool Loaded = ExternalProject->Load(FileInfo.absoluteFilePath(), false);
|
|
|
|
LoadingPaths.pop_back();
|
|
|
|
if (!Loaded)
|
|
{
|
|
delete ExternalProject;
|
|
return lcUnexpected(tr("Could not load external project '%1'.").arg(FileInfo.absoluteFilePath()));
|
|
}
|
|
|
|
return ExternalProject;
|
|
}
|
|
|
|
lcPieceData* lcPiecesLibrary::FindPieceData(const char* PieceName, Project* CurrentProject)
|
|
{
|
|
const std::string Name = NormalizePieceDataName(PieceName);
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
|
|
if (CurrentProject)
|
|
if (lcPieceData* PieceData = CurrentProject->FindPieceData(Name))
|
|
return PieceData;
|
|
|
|
const std::map<std::string, lcPieceData*>::const_iterator PieceIt = mPieces.find(Name);
|
|
|
|
return PieceIt != mPieces.end() ? PieceIt->second : nullptr;
|
|
}
|
|
|
|
lcPieceDataRef lcPiecesLibrary::AcquirePieceData(const char* PieceName, Project* CurrentProject, bool CreateMissing, bool SearchProjectFolder)
|
|
{
|
|
QString ProjectPath;
|
|
QString LoadError;
|
|
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
|
|
if (lcPieceData* PieceData = FindPieceData(PieceName, CurrentProject))
|
|
return lcPieceDataRef(PieceData, this);
|
|
|
|
if (CurrentProject && SearchProjectFolder && !CurrentProject->GetFileName().isEmpty())
|
|
ProjectPath = QFileInfo(CurrentProject->GetFileName()).absolutePath();
|
|
}
|
|
|
|
const std::string Name = NormalizePieceDataName(PieceName);
|
|
|
|
if (!ProjectPath.isEmpty())
|
|
{
|
|
QFileInfo ProjectFile = QFileInfo(ProjectPath + QDir::separator() + PieceName);
|
|
|
|
if (ProjectFile.isFile())
|
|
{
|
|
const lcResult<Project*> ExternalProject = LoadExternalProject(ProjectFile, CurrentProject && CurrentProject->IsPreview(), CurrentProject && CurrentProject->DefersModelMeshRequests());
|
|
|
|
if (ExternalProject)
|
|
{
|
|
lcPieceData* PieceData = new lcPieceData(false);
|
|
|
|
PieceData->CreateProject(ExternalProject.value(), PieceName);
|
|
RegisterProjectPieceData(CurrentProject, Name, PieceData);
|
|
|
|
return lcPieceDataRef(PieceData, this);
|
|
}
|
|
else
|
|
LoadError = ExternalProject.error();
|
|
}
|
|
}
|
|
|
|
if (CreateMissing)
|
|
{
|
|
lcPieceData* PieceData = new lcPieceData(false);
|
|
|
|
PieceData->CreatePart(PieceName);
|
|
PieceData->SetFailed(LoadError.isEmpty() ? tr("Could not find piece '%1'.").arg(QString::fromLatin1(PieceName)) : LoadError);
|
|
|
|
if (CurrentProject)
|
|
RegisterProjectPieceData(CurrentProject, Name, PieceData);
|
|
else
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
mPieces[Name] = PieceData;
|
|
}
|
|
|
|
return lcPieceDataRef(PieceData, this);
|
|
}
|
|
|
|
return lcPieceDataRef();
|
|
}
|
|
|
|
bool lcPiecesLibrary::RemapProjectPieceData(lcPieceData* PieceData, const QString& ProjectDirectory, bool IsPreview)
|
|
{
|
|
const QByteArray PieceName(PieceData->mFileName);
|
|
const QFileInfo Candidate(QDir(ProjectDirectory).absoluteFilePath(QString::fromLatin1(PieceName.constData())));
|
|
|
|
if (PieceData->IsProject() && Candidate.isFile() &&
|
|
QFileInfo(PieceData->GetProject()->GetFileName()).canonicalFilePath() == Candidate.canonicalFilePath())
|
|
return false;
|
|
|
|
if (!PieceData->IsProject() && !Candidate.isFile())
|
|
return false;
|
|
|
|
InvalidatePieceDataLoad(PieceData);
|
|
|
|
if (Candidate.isFile())
|
|
{
|
|
const lcResult<Project*> ExternalProject = LoadExternalProject(Candidate, IsPreview, true);
|
|
|
|
if (ExternalProject)
|
|
PieceData->CreateProject(ExternalProject.value(), PieceName.constData());
|
|
else
|
|
{
|
|
PieceData->DetachContainer();
|
|
PieceData->SetFailed(ExternalProject.error());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
PieceData->DetachContainer();
|
|
PieceData->SetFailed(tr("Could not find external project '%1'.").arg(Candidate.absoluteFilePath()));
|
|
}
|
|
|
|
NotifyAssetStatusChanged();
|
|
|
|
return true;
|
|
}
|
|
|
|
lcTexture* lcPiecesLibrary::FindTexture(const char* TextureName, Project* CurrentProject, bool SearchProjectFolder)
|
|
{
|
|
QString ProjectPath;
|
|
|
|
if (SearchProjectFolder && CurrentProject)
|
|
{
|
|
QString FileName = CurrentProject->GetFileName();
|
|
|
|
if (!FileName.isEmpty())
|
|
ProjectPath = QFileInfo(FileName).absolutePath();
|
|
}
|
|
|
|
std::map<QString, std::shared_ptr<const lcEmbeddedData>> EmbeddedData;
|
|
|
|
if (CurrentProject)
|
|
EmbeddedData = CurrentProject->GetEmbeddedDataLookup();
|
|
|
|
lcFileSource Source;
|
|
lcTexture* Texture = FindTextureDeferred(TextureName, std::vector<QString>{ ProjectPath }, EmbeddedData, Source);
|
|
|
|
if (!Texture)
|
|
return nullptr;
|
|
|
|
if (mAssetLoader->WaitForTexture(Texture, std::move(Source)))
|
|
return Texture;
|
|
|
|
ReleaseTexture(Texture);
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
QString lcPiecesLibrary::FindProjectTextureFile(const QString& ProjectPath, const QString& TextureName)
|
|
{
|
|
QDir Directory(ProjectPath);
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
|
|
const auto SkipEmptyParts = Qt::SkipEmptyParts;
|
|
#else
|
|
const auto SkipEmptyParts = QString::SplitBehavior::SkipEmptyParts;
|
|
#endif
|
|
const QStringList PathComponents = (TextureName + QLatin1String(".png")).split(QLatin1Char('/'), SkipEmptyParts);
|
|
|
|
for (int ComponentIdx = 0; ComponentIdx < PathComponents.size(); ComponentIdx++)
|
|
{
|
|
const QString& Component = PathComponents[ComponentIdx];
|
|
const bool LastComponent = ComponentIdx == PathComponents.size() - 1;
|
|
|
|
if (Component == QLatin1String(".") || Component == QLatin1String(".."))
|
|
{
|
|
if (LastComponent || !Directory.cd(Component))
|
|
return QString();
|
|
continue;
|
|
}
|
|
|
|
QFileInfo Entry(Directory, Component);
|
|
|
|
if (LastComponent ? !Entry.isFile() : !Entry.isDir())
|
|
{
|
|
Entry = QFileInfo();
|
|
|
|
for (const QFileInfo& Candidate : Directory.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System))
|
|
{
|
|
if (Candidate.fileName().compare(Component, Qt::CaseInsensitive) == 0 &&
|
|
(LastComponent ? Candidate.isFile() : Candidate.isDir()))
|
|
{
|
|
Entry = Candidate;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (LastComponent)
|
|
return Entry.isFile() ? Entry.absoluteFilePath() : QString();
|
|
|
|
if (!Entry.isDir())
|
|
return QString();
|
|
|
|
Directory.setPath(Entry.absoluteFilePath());
|
|
}
|
|
|
|
return QString();
|
|
}
|
|
|
|
lcFileSource lcPiecesLibrary::ResolveTextureSource(const char* TextureName, const std::vector<QString>& SearchDirectories, const std::map<QString, std::shared_ptr<const lcEmbeddedData>>& EmbeddedData, QString& ProjectPath)
|
|
{
|
|
ProjectPath.clear();
|
|
|
|
if (!TextureName[0] || strlen(TextureName) >= LC_TEXTURE_NAME_LEN)
|
|
return {};
|
|
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
|
|
const bool ArchiveLibrary = static_cast<bool>(mZipFiles[static_cast<int>(lcZipFileType::Official)]);
|
|
lcFileSource Source;
|
|
const QString Name = QString::fromLatin1(TextureName);
|
|
const QString Candidates[] = { QLatin1String("textures/") + Name, Name };
|
|
const QString Folders[] = { QStringLiteral("p/"), QStringLiteral("parts/"), QStringLiteral("models/"), QStringLiteral("unofficial/p/"), QStringLiteral("unofficial/parts/") };
|
|
|
|
// Complete the prefixed search before considering any unprefixed location.
|
|
for (const QString& Candidate : Candidates)
|
|
{
|
|
QString EmbeddedName = Candidate + QLatin1String(".png");
|
|
EmbeddedName.replace(QLatin1Char('\\'), QLatin1Char('/'));
|
|
const auto Embedded = EmbeddedData.find(QDir::cleanPath(EmbeddedName).toUpper());
|
|
|
|
if (Embedded != EmbeddedData.end())
|
|
{
|
|
Source.EmbeddedData = Embedded->second;
|
|
break;
|
|
}
|
|
|
|
for (const QString& Directory : SearchDirectories)
|
|
{
|
|
if (Directory.isEmpty())
|
|
continue;
|
|
|
|
Source.FilePath = FindProjectTextureFile(Directory, Candidate);
|
|
|
|
if (!Source.FilePath.isEmpty())
|
|
{
|
|
ProjectPath = Directory;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!Source.FilePath.isEmpty())
|
|
break;
|
|
|
|
for (const QString& Folder : Folders)
|
|
{
|
|
if (ArchiveLibrary)
|
|
{
|
|
const bool Unofficial = Folder.startsWith(QLatin1String("unofficial/"));
|
|
const int Type = static_cast<int>(Unofficial ? lcZipFileType::Unofficial : lcZipFileType::Official);
|
|
QString EntryName;
|
|
|
|
if (Unofficial)
|
|
EntryName = Folder.mid(11) + Candidate + QLatin1String(".png");
|
|
else
|
|
EntryName = QLatin1String("ldraw/") + Folder + Candidate + QLatin1String(".png");
|
|
|
|
const std::map<QString, int>& Entries = mTextureArchiveEntries[Type];
|
|
const auto Entry = Entries.find(QDir::cleanPath(EntryName).toUpper());
|
|
|
|
if (Entry != Entries.end())
|
|
{
|
|
Source.ArchiveType = Type;
|
|
Source.ArchiveIndex = Entry->second;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
const QString EntryName = QDir::cleanPath(Folder + Candidate + QLatin1String(".png")).toUpper();
|
|
const auto Entry = mTextureDiskEntries.find(EntryName);
|
|
|
|
if (Entry != mTextureDiskEntries.end())
|
|
{
|
|
Source.FilePath = Entry->second;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!Source.FilePath.isEmpty() || Source.ArchiveIndex != -1)
|
|
break;
|
|
}
|
|
|
|
if (!Source.FilePath.isEmpty())
|
|
{
|
|
const QString CanonicalPath = QFileInfo(Source.FilePath).canonicalFilePath();
|
|
Source.FilePath = CanonicalPath.isEmpty() ? QFileInfo(Source.FilePath).absoluteFilePath() : CanonicalPath;
|
|
}
|
|
|
|
return Source;
|
|
}
|
|
|
|
lcTexture* lcPiecesLibrary::FindTextureDeferred(const char* TextureName, const std::vector<QString>& SearchDirectories, const std::map<QString, std::shared_ptr<const lcEmbeddedData>>& EmbeddedData, lcFileSource& Source)
|
|
{
|
|
QString ProjectPath;
|
|
Source = ResolveTextureSource(TextureName, SearchDirectories, EmbeddedData, ProjectPath);
|
|
|
|
return mTextureManager->AcquireTexture(TextureName, Source, ProjectPath);
|
|
}
|
|
|
|
lcTextureBuildResult lcPiecesLibrary::BuildTextureData(const lcFileSource& Source)
|
|
{
|
|
const lcResult<std::unique_ptr<lcFile>> File = OpenFile(Source);
|
|
|
|
if (!File)
|
|
{
|
|
lcTextureBuildResult Result;
|
|
Result.Error = lcTextureLoadError::DecodeFailed;
|
|
Result.ErrorDetails = File.error();
|
|
return Result;
|
|
}
|
|
|
|
lcTextureBuildResult Result = mTextureManager->BuildTextureData(*File.value());
|
|
|
|
if (Result.DecodedImage.isNull() && Source.EmbeddedData)
|
|
Result.ErrorDetails = tr("Could not decode embedded texture '%1'.").arg(Source.EmbeddedData->FileName);
|
|
|
|
return Result;
|
|
}
|
|
|
|
lcResult<std::unique_ptr<lcFile>> lcPiecesLibrary::OpenFile(const lcFileSource& Source)
|
|
{
|
|
if (Source.EmbeddedData)
|
|
{
|
|
const lcEmbeddedData& Data = *Source.EmbeddedData;
|
|
|
|
if (!Data.Error.isEmpty())
|
|
return lcUnexpected(Data.Error);
|
|
|
|
if (!Data.HasValidEncoding())
|
|
return lcUnexpected(tr("Invalid Base64 data for embedded file '%1'.").arg(Data.FileName));
|
|
|
|
const QByteArray Bytes = QByteArray::fromBase64(Data.EncodedData);
|
|
std::unique_ptr<lcMemFile> File(new lcMemFile);
|
|
|
|
if (File->WriteBuffer(Bytes.constData(), Bytes.size()) != static_cast<size_t>(Bytes.size()))
|
|
return lcUnexpected(tr("Could not read embedded file '%1'.").arg(Data.FileName));
|
|
|
|
File->Seek(0, SEEK_SET);
|
|
return std::unique_ptr<lcFile>(std::move(File));
|
|
}
|
|
|
|
if (!Source.FilePath.isEmpty())
|
|
{
|
|
std::unique_ptr<lcDiskFile> File(new lcDiskFile(Source.FilePath));
|
|
|
|
if (!File->Open(QIODevice::ReadOnly))
|
|
return lcUnexpected(tr("Could not open file '%1'.").arg(Source.FilePath));
|
|
|
|
return std::unique_ptr<lcFile>(std::move(File));
|
|
}
|
|
|
|
if (Source.ArchiveType < 0 || Source.ArchiveType >= static_cast<int>(lcZipFileType::Count) || Source.ArchiveIndex < 0)
|
|
return lcUnexpected(tr("Invalid archive file source."));
|
|
|
|
std::unique_ptr<lcMemFile> File(new lcMemFile);
|
|
|
|
// Source archives are stable until workers are drained; extraction locks
|
|
// the selected archive internally without serializing unrelated archives.
|
|
const std::unique_ptr<lcZipFile>& Archive = mZipFiles[Source.ArchiveType];
|
|
|
|
if (!Archive || static_cast<size_t>(Source.ArchiveIndex) >= Archive->mFiles.size())
|
|
return lcUnexpected(tr("Could not read archive entry %1.").arg(Source.ArchiveIndex));
|
|
|
|
if (!Archive->ExtractFile(Source.ArchiveIndex, *File))
|
|
return lcUnexpected(tr("Could not read archive file '%1'.").arg(QString::fromLatin1(Archive->mFiles[Source.ArchiveIndex].file_name)));
|
|
|
|
File->Seek(0, SEEK_SET);
|
|
return std::unique_ptr<lcFile>(std::move(File));
|
|
}
|
|
|
|
bool lcPiecesLibrary::Load(const QString& LibraryPath, bool ShowProgress)
|
|
{
|
|
Unload();
|
|
|
|
if (OpenArchive(LibraryPath, lcZipFileType::Official))
|
|
{
|
|
LoadColors();
|
|
|
|
mLibraryDir = QFileInfo(LibraryPath).absoluteDir();
|
|
QString UnofficialFileName = mLibraryDir.absoluteFilePath(QLatin1String("ldrawunf.zip"));
|
|
|
|
if (!OpenArchive(UnofficialFileName, lcZipFileType::Unofficial))
|
|
UnofficialFileName.clear();
|
|
|
|
ReadArchiveDescriptions(LibraryPath, UnofficialFileName);
|
|
}
|
|
else
|
|
{
|
|
mLibraryDir.setPath(LibraryPath);
|
|
|
|
if (OpenDirectory(mLibraryDir, ShowProgress))
|
|
LoadColors();
|
|
else
|
|
return false;
|
|
}
|
|
|
|
UpdateStudStyleSource();
|
|
lcLoadDefaultCategories();
|
|
lcSynthInit();
|
|
lcTrainTrackInfo::Initialize(this);
|
|
|
|
return true;
|
|
}
|
|
|
|
void lcPiecesLibrary::LoadColors()
|
|
{
|
|
QString CustomColorsPath = lcGetProfileString(LC_PROFILE_COLOR_CONFIG);
|
|
|
|
if (!CustomColorsPath.isEmpty())
|
|
{
|
|
lcDiskFile ColorFile(CustomColorsPath);
|
|
|
|
if (ColorFile.Open(QIODevice::ReadOnly) && lcLoadColorFile(ColorFile, mStudStyle))
|
|
{
|
|
UpdateLoadingMeshColors();
|
|
emit ColorsLoaded();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (mZipFiles[static_cast<int>(lcZipFileType::Official)])
|
|
{
|
|
lcMemFile ColorFile;
|
|
|
|
if (!mZipFiles[static_cast<int>(lcZipFileType::Official)]->ExtractFile("ldraw/ldconfig.ldr", ColorFile) || !lcLoadColorFile(ColorFile, mStudStyle))
|
|
lcLoadDefaultColors(mStudStyle);
|
|
}
|
|
else
|
|
{
|
|
lcDiskFile ColorFile(mLibraryDir.absoluteFilePath(QLatin1String("ldconfig.ldr")));
|
|
|
|
if (!ColorFile.Open(QIODevice::ReadOnly) || !lcLoadColorFile(ColorFile, mStudStyle))
|
|
{
|
|
ColorFile.SetFileName(mLibraryDir.absoluteFilePath(QLatin1String("LDConfig.ldr")));
|
|
|
|
if (!ColorFile.Open(QIODevice::ReadOnly) || !lcLoadColorFile(ColorFile, mStudStyle))
|
|
lcLoadDefaultColors(mStudStyle);
|
|
}
|
|
}
|
|
|
|
UpdateLoadingMeshColors();
|
|
emit ColorsLoaded();
|
|
}
|
|
|
|
void lcPiecesLibrary::UpdateLoadingMeshColors()
|
|
{
|
|
lcMeshSection* Sections = mLoadingMesh->mLods[LC_MESH_LOD_HIGH].Sections;
|
|
|
|
Sections[0].ColorIndex = gDefaultColor;
|
|
Sections[1].ColorIndex = gEdgeColor;
|
|
}
|
|
|
|
bool lcPiecesLibrary::IsStudPrimitive(const char* FileName)
|
|
{
|
|
return memcmp(FileName, "STU", 3) == 0;
|
|
}
|
|
|
|
bool lcPiecesLibrary::IsStudStylePrimitive(const char* FileName)
|
|
{
|
|
constexpr std::array<const char*, 15> StudStylePrimitives =
|
|
{
|
|
"2-4STUD4.DAT", "STUD.DAT", "STUD2.DAT", "STUD2A.DAT", "STUD3.DAT", "STUD4.DAT", "STUD4A.DAT", "STUD4H.DAT",
|
|
"8/STUD.DAT", "8/STUD2.DAT", "8/STUD2A.DAT", "8/STUD3.DAT", "8/STUD4.DAT", "8/STUD4A.DAT", "8/STUD4H.DAT"
|
|
};
|
|
|
|
for (const char* StudStylePrimitive : StudStylePrimitives)
|
|
if (!strcmp(StudStylePrimitive, FileName))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
qint32 lcPiecesLibrary::MeshCacheSettingsKey(lcStudStyle StudStyle, bool StudCylinderColorEnabled)
|
|
{
|
|
return (LC_LIBRARY_MESH_CACHE_VERSION << 16) | (static_cast<qint32>(StudStyle) << 1) | static_cast<qint32>(StudCylinderColorEnabled);
|
|
}
|
|
|
|
void lcPiecesLibrary::UpdateStudStyleSource()
|
|
{
|
|
if (!mSources.empty() && mSources.front()->Type == lcLibrarySourceType::StudStyle)
|
|
mSources.erase(mSources.begin());
|
|
|
|
mZipFiles[static_cast<int>(lcZipFileType::StudStyle)].reset();
|
|
|
|
if (mStudStyle == lcStudStyle::Plain || (mStudStyle >= lcStudStyle::HighContrast && !mStudCylinderColorEnabled))
|
|
return;
|
|
|
|
const QLatin1String FileNames[] =
|
|
{
|
|
QLatin1String(""), // Plain
|
|
QLatin1String(":/resources/studlogo1.zip"), // ThinLinesLogo
|
|
QLatin1String(":/resources/studlogo2.zip"), // OutlineLogo
|
|
QLatin1String(":/resources/studlogo3.zip"), // SharpTopLogo
|
|
QLatin1String(":/resources/studlogo4.zip"), // RoundedTopLogo
|
|
QLatin1String(":/resources/studlogo5.zip"), // FlattenedLogo
|
|
QLatin1String(":/resources/studslegostyle1.zip"), // HighContrast
|
|
QLatin1String(":/resources/studslegostyle2.zip") // HighContrastLogo
|
|
};
|
|
|
|
LC_ARRAY_SIZE_CHECK(FileNames, lcStudStyle::Count);
|
|
|
|
std::unique_ptr<lcDiskFile> StudStyleFile(new lcDiskFile(FileNames[static_cast<int>(mStudStyle)]));
|
|
|
|
if (StudStyleFile->Open(QIODevice::ReadOnly))
|
|
OpenArchive(std::move(StudStyleFile), lcZipFileType::StudStyle);
|
|
}
|
|
|
|
bool lcPiecesLibrary::OpenArchive(const QString& FileName, lcZipFileType ZipFileType)
|
|
{
|
|
std::unique_ptr<lcDiskFile> File(new lcDiskFile(FileName));
|
|
|
|
if (!File->Open(QIODevice::ReadOnly))
|
|
return false;
|
|
|
|
return OpenArchive(std::move(File), ZipFileType);
|
|
}
|
|
|
|
bool lcPiecesLibrary::OpenArchive(std::unique_ptr<lcFile> File, lcZipFileType ZipFileType)
|
|
{
|
|
std::unique_ptr<lcZipFile> ZipFile(new lcZipFile());
|
|
|
|
if (!ZipFile->OpenRead(std::move(File)))
|
|
return false;
|
|
|
|
mTextureArchiveEntries[static_cast<int>(ZipFileType)].clear();
|
|
|
|
std::unique_ptr<lcLibrarySource> Source(new lcLibrarySource);
|
|
Source->Type = ZipFileType != lcZipFileType::StudStyle ? lcLibrarySourceType::Library : lcLibrarySourceType::StudStyle;
|
|
|
|
for (quint32 FileIdx = 0; FileIdx < ZipFile->mFiles.size(); FileIdx++)
|
|
{
|
|
lcZipFileInfo& FileInfo = ZipFile->mFiles[FileIdx];
|
|
char NameBuffer[LC_PIECE_NAME_LEN];
|
|
char* Name = NameBuffer;
|
|
|
|
const char* Src = FileInfo.file_name;
|
|
char* Dst = Name;
|
|
|
|
while (*Src && Dst - Name < LC_PIECE_NAME_LEN)
|
|
{
|
|
if (*Src >= 'a' && *Src <= 'z')
|
|
*Dst = *Src + 'A' - 'a';
|
|
else if (*Src == '\\')
|
|
*Dst = '/';
|
|
else
|
|
*Dst = *Src;
|
|
|
|
Src++;
|
|
Dst++;
|
|
}
|
|
|
|
if (Dst - Name <= 4)
|
|
continue;
|
|
|
|
*Dst = 0;
|
|
Dst -= 4;
|
|
if (memcmp(Dst, ".DAT", 4))
|
|
{
|
|
if (!memcmp(Dst, ".PNG", 4))
|
|
{
|
|
if (ZipFileType == lcZipFileType::Official || ZipFileType == lcZipFileType::Unofficial)
|
|
mTextureArchiveEntries[static_cast<int>(ZipFileType)].emplace(QDir::cleanPath(QString::fromLatin1(Name)).toUpper(), FileIdx);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (ZipFileType == lcZipFileType::Official)
|
|
{
|
|
if (memcmp(Name, "LDRAW/", 6))
|
|
continue;
|
|
|
|
Name += 6;
|
|
}
|
|
|
|
if (!memcmp(Name, "PARTS/", 6))
|
|
{
|
|
Name += 6;
|
|
|
|
if (memcmp(Name, "S/", 2))
|
|
{
|
|
lcPieceData* PieceData = FindPieceData(Name, nullptr);
|
|
|
|
if (!PieceData)
|
|
{
|
|
PieceData = new lcPieceData(true);
|
|
|
|
strncpy(PieceData->mFileName, FileInfo.file_name + (Name - NameBuffer), sizeof(PieceData->mFileName)-1);
|
|
PieceData->mFileName[sizeof(PieceData->mFileName) - 1] = 0;
|
|
|
|
mPieces[Name] = PieceData;
|
|
}
|
|
|
|
PieceData->SetZipFile(ZipFileType, FileIdx);
|
|
}
|
|
else
|
|
Source->Primitives[Name] = new lcLibraryPrimitive(QString(), FileInfo.file_name + (Name - NameBuffer), ZipFileType, FileIdx, false, false, true);
|
|
}
|
|
else if (!memcmp(Name, "P/", 2))
|
|
{
|
|
Name += 2;
|
|
|
|
Source->Primitives[Name] = new lcLibraryPrimitive(QString(), FileInfo.file_name + (Name - NameBuffer), ZipFileType, FileIdx, IsStudPrimitive(Name), IsStudStylePrimitive(Name), false);
|
|
}
|
|
}
|
|
|
|
mZipFiles[static_cast<int>(ZipFileType)] = std::move(ZipFile);
|
|
|
|
if (ZipFileType != lcZipFileType::StudStyle)
|
|
mSources.emplace_back(std::move(Source));
|
|
else
|
|
mSources.insert(mSources.begin(), std::move(Source));
|
|
|
|
return true;
|
|
}
|
|
|
|
void lcPiecesLibrary::ReadArchiveDescriptions(const QString& OfficialFileName, const QString& UnofficialFileName)
|
|
{
|
|
QFileInfo OfficialInfo(OfficialFileName);
|
|
QFileInfo UnofficialInfo(UnofficialFileName);
|
|
|
|
mArchiveCheckSum[0] = OfficialInfo.size();
|
|
mArchiveCheckSum[1] = OfficialInfo.lastModified().toMSecsSinceEpoch();
|
|
|
|
if (!UnofficialFileName.isEmpty())
|
|
{
|
|
mArchiveCheckSum[2] = UnofficialInfo.size();
|
|
mArchiveCheckSum[3] = UnofficialInfo.lastModified().toMSecsSinceEpoch();
|
|
}
|
|
else
|
|
{
|
|
mArchiveCheckSum[2] = 0;
|
|
mArchiveCheckSum[3] = 0;
|
|
}
|
|
|
|
QString IndexFileName = QFileInfo(QDir(mCachePath), QLatin1String("index")).absoluteFilePath();
|
|
|
|
if (!LoadCacheIndex(IndexFileName))
|
|
{
|
|
lcMemFile PieceFile;
|
|
|
|
for (const auto& PieceIt : mPieces)
|
|
{
|
|
lcPieceData* PieceData = PieceIt.second;
|
|
|
|
mZipFiles[static_cast<int>(PieceData->mZipFileType)]->ExtractFile(PieceData->mZipFileIndex, PieceFile, 256);
|
|
PieceFile.Seek(0, SEEK_END);
|
|
PieceFile.WriteU8(0);
|
|
|
|
char* Src = (char*)PieceFile.mBuffer + 2;
|
|
char* Dst = PieceData->m_strDescription;
|
|
|
|
for (;;)
|
|
{
|
|
if (*Src != '\r' && *Src != '\n' && *Src && Dst - PieceData->m_strDescription < (int)sizeof(PieceData->m_strDescription) - 1)
|
|
{
|
|
*Dst++ = *Src++;
|
|
continue;
|
|
}
|
|
|
|
*Dst = 0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
SaveArchiveCacheIndex(IndexFileName);
|
|
}
|
|
}
|
|
|
|
bool lcPiecesLibrary::OpenDirectory(const QDir& LibraryDir, bool ShowProgress)
|
|
{
|
|
mTextureDiskEntries.clear();
|
|
|
|
const QLatin1String BaseFolders[] = { QLatin1String(""), QLatin1String("unofficial/") };
|
|
constexpr int NumBaseFolders = LC_ARRAY_COUNT(BaseFolders);
|
|
|
|
QFileInfoList FileLists[NumBaseFolders];
|
|
|
|
for (unsigned int BaseFolderIdx = 0; BaseFolderIdx < NumBaseFolders; BaseFolderIdx++)
|
|
{
|
|
QString ParstPath = QDir(LibraryDir.absoluteFilePath(BaseFolders[BaseFolderIdx])).absoluteFilePath(QLatin1String("parts/"));
|
|
QDir Dir = QDir(ParstPath, QLatin1String("*.dat"), QDir::SortFlags(QDir::Name | QDir::IgnoreCase), QDir::Files | QDir::Hidden | QDir::Readable);
|
|
FileLists[BaseFolderIdx] = Dir.entryInfoList();
|
|
}
|
|
|
|
if (FileLists[static_cast<int>(lcLibraryFolderType::Official)].isEmpty())
|
|
return false;
|
|
|
|
mHasUnofficial = !FileLists[static_cast<int>(lcLibraryFolderType::Unofficial)].isEmpty();
|
|
ReadDirectoryDescriptions(FileLists, ShowProgress);
|
|
|
|
for (unsigned int BaseFolderIdx = 0; BaseFolderIdx < LC_ARRAY_COUNT(BaseFolders); BaseFolderIdx++)
|
|
{
|
|
std::unique_ptr<lcLibrarySource> Source(new lcLibrarySource);
|
|
Source->Type = lcLibrarySourceType::Library;
|
|
|
|
const char* PrimitiveDirectories[] = { "p/", "parts/s/" };
|
|
bool SubFileDirectories[] = { false, false, true };
|
|
QDir BaseDir(LibraryDir.absoluteFilePath(QLatin1String(BaseFolders[BaseFolderIdx])));
|
|
|
|
for (int DirectoryIdx = 0; DirectoryIdx < (int)(LC_ARRAY_COUNT(PrimitiveDirectories)); DirectoryIdx++)
|
|
{
|
|
QString ChildPath = BaseDir.absoluteFilePath(QLatin1String(PrimitiveDirectories[DirectoryIdx]));
|
|
QDirIterator DirIterator(ChildPath, QStringList() << QLatin1String("*.dat"), QDir::Files | QDir::Hidden | QDir::Readable, QDirIterator::Subdirectories);
|
|
|
|
while (DirIterator.hasNext())
|
|
{
|
|
char Name[LC_PIECE_NAME_LEN];
|
|
QString FileName = DirIterator.next();
|
|
QByteArray FileString = BaseDir.relativeFilePath(FileName).toLatin1();
|
|
const char* Src = strchr(FileString, '/') + 1;
|
|
char* Dst = Name;
|
|
|
|
while (*Src && Dst - Name < (int)sizeof(Name))
|
|
{
|
|
if (*Src >= 'a' && *Src <= 'z')
|
|
*Dst = *Src + 'A' - 'a';
|
|
else if (*Src == '\\')
|
|
*Dst = '/';
|
|
else
|
|
*Dst = *Src;
|
|
|
|
Src++;
|
|
Dst++;
|
|
}
|
|
*Dst = 0;
|
|
|
|
if (Dst - Name <= 4)
|
|
continue;
|
|
|
|
Dst -= 4;
|
|
if (memcmp(Dst, ".DAT", 4))
|
|
continue;
|
|
|
|
if (BaseFolderIdx > 0 && IsPrimitive(Name))
|
|
continue;
|
|
|
|
if (BaseFolderIdx == static_cast<int>(lcLibraryFolderType::Unofficial))
|
|
mHasUnofficial = true;
|
|
|
|
const bool SubFile = SubFileDirectories[DirectoryIdx];
|
|
Source->Primitives[Name] = new lcLibraryPrimitive(std::move(FileName), strchr(FileString, '/') + 1, lcZipFileType::Count, 0, !SubFile && IsStudPrimitive(Name), IsStudStylePrimitive(Name), SubFile);
|
|
}
|
|
}
|
|
|
|
mSources.emplace_back(std::move(Source));
|
|
|
|
// Index PNG paths once; UI-thread texture lookup must not enumerate library folders.
|
|
const char* TextureDirectories[] = { "p/", "parts/", "models/" };
|
|
|
|
for (const char* TextureDirectory : TextureDirectories)
|
|
{
|
|
const QString ChildPath = BaseDir.absoluteFilePath(QLatin1String(TextureDirectory));
|
|
QDirIterator DirIterator(ChildPath, QStringList() << QLatin1String("*.png"), QDir::Files | QDir::Hidden | QDir::Readable, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
|
|
|
|
while (DirIterator.hasNext())
|
|
{
|
|
const QString FilePath = DirIterator.next();
|
|
const QString Name = QDir::cleanPath(LibraryDir.relativeFilePath(FilePath)).toUpper();
|
|
|
|
mTextureDiskEntries.emplace(Name, FilePath);
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void lcPiecesLibrary::ReadDirectoryDescriptions(const QFileInfoList (&FileLists)[static_cast<int>(lcLibraryFolderType::Count)], bool ShowProgress)
|
|
{
|
|
QString IndexFileName = QFileInfo(QDir(mCachePath), QLatin1String("index")).absoluteFilePath();
|
|
lcMemFile IndexFile;
|
|
std::vector<const char*> CachedDescriptions;
|
|
|
|
if (ReadDirectoryCacheFile(IndexFileName, IndexFile))
|
|
{
|
|
QString LibraryPath = IndexFile.ReadQString();
|
|
|
|
if (LibraryPath == mLibraryDir.absolutePath())
|
|
{
|
|
int NumDescriptions = IndexFile.ReadU32();
|
|
CachedDescriptions.reserve(NumDescriptions);
|
|
|
|
while (NumDescriptions--)
|
|
{
|
|
const char* FileName = (const char*)IndexFile.mBuffer + IndexFile.GetPosition();
|
|
CachedDescriptions.push_back(FileName);
|
|
IndexFile.Seek(strlen(FileName) + 1, SEEK_CUR);
|
|
const char* Description = (const char*)IndexFile.mBuffer + IndexFile.GetPosition();
|
|
IndexFile.Seek(strlen(Description) + 1, SEEK_CUR);
|
|
IndexFile.Seek(4 + 1 + 8, SEEK_CUR);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int FolderIdx = 0; FolderIdx < static_cast<int>(lcLibraryFolderType::Count); FolderIdx++)
|
|
{
|
|
const QFileInfoList& FileList = FileLists[FolderIdx];
|
|
|
|
for (int FileIdx = 0; FileIdx < FileList.size(); FileIdx++)
|
|
{
|
|
char Name[LC_PIECE_NAME_LEN];
|
|
QByteArray FileString = FileList[FileIdx].fileName().toLatin1();
|
|
const char* Src = FileString;
|
|
char* Dst = Name;
|
|
|
|
while (*Src && Dst - Name < (int)sizeof(Name))
|
|
{
|
|
if (*Src >= 'a' && *Src <= 'z')
|
|
*Dst = *Src + 'A' - 'a';
|
|
else if (*Src == '\\')
|
|
*Dst = '/';
|
|
else
|
|
*Dst = *Src;
|
|
|
|
Src++;
|
|
Dst++;
|
|
}
|
|
*Dst = 0;
|
|
|
|
if (FolderIdx > 0 && mPieces.find(Name) != mPieces.end())
|
|
continue;
|
|
|
|
lcPieceData* PieceData = new lcPieceData(true);
|
|
|
|
strncpy(PieceData->mFileName, FileString, sizeof(PieceData->mFileName));
|
|
PieceData->mFileName[sizeof(PieceData->mFileName) - 1] = 0;
|
|
PieceData->mFolderType = FolderIdx;
|
|
PieceData->mFolderIndex = FileIdx;
|
|
|
|
mPieces[Name] = PieceData;
|
|
}
|
|
}
|
|
|
|
QAtomicInt FilesLoaded;
|
|
bool Modified = false;
|
|
|
|
auto ReadDescriptions = [&FileLists, &CachedDescriptions, &FilesLoaded, &Modified](const std::pair<std::string, lcPieceData*>& Entry)
|
|
{
|
|
lcPieceData* PieceData = Entry.second;
|
|
FilesLoaded.ref();
|
|
|
|
lcDiskFile PieceFile(FileLists[PieceData->mFolderType][PieceData->mFolderIndex].absoluteFilePath());
|
|
char Line[1024];
|
|
|
|
if (!CachedDescriptions.empty())
|
|
{
|
|
auto DescriptionCompare = [](const void* Key, const void* Element)
|
|
{
|
|
return strcmp((const char*)Key, *(const char**)Element);
|
|
};
|
|
|
|
void* CachedDescription = bsearch(PieceData->mFileName, &CachedDescriptions.front(), CachedDescriptions.size(), sizeof(char*), DescriptionCompare);
|
|
|
|
if (CachedDescription)
|
|
{
|
|
const char* FileName = *(const char**)CachedDescription;
|
|
const char* Description = FileName + strlen(FileName) + 1;
|
|
const uint64_t CachedFileTime = *(uint64_t*)(Description + strlen(Description) + 1 + 4 + 1);
|
|
|
|
quint64 FileTime = FileLists[PieceData->mFolderType][PieceData->mFolderIndex].lastModified().toMSecsSinceEpoch();
|
|
|
|
if (FileTime == CachedFileTime)
|
|
{
|
|
lcstrcpy(PieceData->m_strDescription, Description);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!PieceFile.Open(QIODevice::ReadOnly) || !PieceFile.ReadLine(Line, sizeof(Line)))
|
|
{
|
|
lcstrcpy(PieceData->m_strDescription, "Unknown");
|
|
return;
|
|
}
|
|
|
|
const char* Src = Line + 2;
|
|
char* Dst = PieceData->m_strDescription;
|
|
|
|
for (;;)
|
|
{
|
|
if (*Src != '\r' && *Src != '\n' && *Src && Dst - PieceData->m_strDescription < (int)sizeof(PieceData->m_strDescription) - 1)
|
|
{
|
|
*Dst++ = *Src++;
|
|
continue;
|
|
}
|
|
|
|
*Dst = 0;
|
|
break;
|
|
}
|
|
|
|
Modified = true;
|
|
};
|
|
|
|
QProgressDialog* ProgressDialog = new QProgressDialog(nullptr);
|
|
ProgressDialog->setWindowFlags(ProgressDialog->windowFlags() & ~Qt::WindowCloseButtonHint);
|
|
ProgressDialog->setWindowTitle(tr("Initializing"));
|
|
ProgressDialog->setLabelText(tr("Loading Parts Library"));
|
|
ProgressDialog->setMaximum((int)mPieces.size());
|
|
ProgressDialog->setMinimum(0);
|
|
ProgressDialog->setValue(0);
|
|
ProgressDialog->setCancelButton(nullptr);
|
|
ProgressDialog->setAutoReset(false);
|
|
if (ShowProgress)
|
|
ProgressDialog->show();
|
|
|
|
QFuture<void> LoadFuture = QtConcurrent::map(mPieces, ReadDescriptions);
|
|
|
|
while (!LoadFuture.isFinished())
|
|
{
|
|
ProgressDialog->setValue(FilesLoaded);
|
|
QApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
|
}
|
|
|
|
ProgressDialog->setValue(FilesLoaded);
|
|
QApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
|
|
|
|
ProgressDialog->deleteLater();
|
|
|
|
if (Modified)
|
|
{
|
|
lcMemFile NewIndexFile;
|
|
|
|
NewIndexFile.WriteQString(mLibraryDir.absolutePath());
|
|
|
|
NewIndexFile.WriteU32((quint32)mPieces.size());
|
|
|
|
std::vector<lcPieceData*> SortedPieces;
|
|
SortedPieces.reserve(mPieces.size());
|
|
for (const auto& PieceIt : mPieces)
|
|
SortedPieces.push_back(PieceIt.second);
|
|
|
|
auto PieceDataCompare = [](lcPieceData* PieceData1, lcPieceData* PieceData2)
|
|
{
|
|
return strcmp(PieceData1->mFileName, PieceData2->mFileName) < 0;
|
|
};
|
|
|
|
std::sort(SortedPieces.begin(), SortedPieces.end(), PieceDataCompare);
|
|
|
|
for (const lcPieceData* PieceData : SortedPieces)
|
|
{
|
|
if (NewIndexFile.WriteBuffer(PieceData->mFileName, strlen(PieceData->mFileName) + 1) == 0)
|
|
return;
|
|
|
|
if (NewIndexFile.WriteBuffer(PieceData->m_strDescription, strlen(PieceData->m_strDescription) + 1) == 0)
|
|
return;
|
|
|
|
NewIndexFile.WriteU8(static_cast<quint8>(PieceData->mFolderType));
|
|
|
|
quint64 FileTime = FileLists[PieceData->mFolderType][PieceData->mFolderIndex].lastModified().toMSecsSinceEpoch();
|
|
|
|
NewIndexFile.WriteU64(FileTime);
|
|
}
|
|
|
|
WriteDirectoryCacheFile(IndexFileName, NewIndexFile);
|
|
}
|
|
}
|
|
|
|
bool lcPiecesLibrary::ReadArchiveCacheFile(const QString& FileName, lcMemFile& CacheFile)
|
|
{
|
|
QFile File(FileName);
|
|
|
|
if (!File.open(QIODevice::ReadOnly))
|
|
return false;
|
|
|
|
quint32 CacheVersion, CacheFlags;
|
|
|
|
if (File.read((char*)&CacheVersion, sizeof(CacheVersion)) != sizeof(CacheVersion) || CacheVersion != LC_LIBRARY_CACHE_VERSION)
|
|
return false;
|
|
|
|
if (File.read((char*)&CacheFlags, sizeof(CacheFlags)) != sizeof(CacheFlags) ||
|
|
CacheFlags != static_cast<quint32>(lcLibraryCacheFlag::Archive))
|
|
return false;
|
|
|
|
qint64 CacheCheckSum[4];
|
|
|
|
if (File.read((char*)&CacheCheckSum, sizeof(CacheCheckSum)) != sizeof(CacheCheckSum) || memcmp(CacheCheckSum, mArchiveCheckSum, sizeof(CacheCheckSum)))
|
|
return false;
|
|
|
|
quint32 UncompressedSize;
|
|
|
|
if (File.read((char*)&UncompressedSize, sizeof(UncompressedSize)) != sizeof(UncompressedSize))
|
|
return false;
|
|
|
|
QByteArray CompressedData = File.readAll();
|
|
|
|
CacheFile.SetLength(UncompressedSize);
|
|
CacheFile.Seek(0, SEEK_SET);
|
|
|
|
if (UncompressedSize && !CacheFile.mBuffer)
|
|
return false;
|
|
|
|
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
|
constexpr qsizetype CHUNK = 16384;
|
|
#else
|
|
constexpr int CHUNK = 16384;
|
|
#endif
|
|
int ret;
|
|
unsigned have;
|
|
z_stream strm;
|
|
unsigned char in[CHUNK];
|
|
unsigned char out[CHUNK];
|
|
int pos;
|
|
|
|
strm.zalloc = Z_NULL;
|
|
strm.zfree = Z_NULL;
|
|
strm.opaque = Z_NULL;
|
|
strm.avail_in = 0;
|
|
strm.next_in = Z_NULL;
|
|
pos = 0;
|
|
|
|
ret = inflateInit2(&strm, -MAX_WBITS);
|
|
if (ret != Z_OK)
|
|
return false;
|
|
|
|
do
|
|
{
|
|
strm.avail_in = lcMin(CompressedData.size() - pos, CHUNK);
|
|
strm.next_in = in;
|
|
|
|
if (strm.avail_in == 0)
|
|
break;
|
|
|
|
memcpy(in, CompressedData.constData() + pos, strm.avail_in);
|
|
pos += strm.avail_in;
|
|
|
|
do
|
|
{
|
|
strm.avail_out = CHUNK;
|
|
strm.next_out = out;
|
|
ret = inflate(&strm, Z_NO_FLUSH);
|
|
|
|
switch (ret)
|
|
{
|
|
case Z_NEED_DICT:
|
|
Q_FALLTHROUGH();
|
|
case Z_DATA_ERROR:
|
|
Q_FALLTHROUGH();
|
|
case Z_MEM_ERROR:
|
|
Q_FALLTHROUGH();
|
|
case Z_STREAM_ERROR:
|
|
(void)inflateEnd(&strm);
|
|
return false;
|
|
}
|
|
|
|
have = CHUNK - strm.avail_out;
|
|
|
|
if (strm.total_out > UncompressedSize)
|
|
{
|
|
inflateEnd(&strm);
|
|
return false;
|
|
}
|
|
|
|
CacheFile.WriteBuffer(out, have);
|
|
} while (strm.avail_out == 0);
|
|
} while (ret != Z_STREAM_END);
|
|
|
|
(void)inflateEnd(&strm);
|
|
|
|
CacheFile.Seek(0, SEEK_SET);
|
|
|
|
return ret == Z_STREAM_END && strm.total_out == UncompressedSize;
|
|
}
|
|
|
|
bool lcPiecesLibrary::WriteArchiveCacheFile(const QString& FileName, lcMemFile& CacheFile)
|
|
{
|
|
QFile File(FileName);
|
|
|
|
if (!File.open(QIODevice::WriteOnly))
|
|
return false;
|
|
|
|
constexpr quint32 CacheVersion = LC_LIBRARY_CACHE_VERSION;
|
|
constexpr quint32 CacheFlags = static_cast<quint32>(lcLibraryCacheFlag::Archive);
|
|
|
|
if (File.write((char*)&CacheVersion, sizeof(CacheVersion)) != sizeof(CacheVersion))
|
|
return false;
|
|
|
|
if (File.write((char*)&CacheFlags, sizeof(CacheFlags)) != sizeof(CacheFlags))
|
|
return false;
|
|
|
|
if (File.write((char*)&mArchiveCheckSum, sizeof(mArchiveCheckSum)) != sizeof(mArchiveCheckSum))
|
|
return false;
|
|
|
|
const quint32 UncompressedSize = (quint32)CacheFile.GetLength();
|
|
|
|
if (File.write((char*)&UncompressedSize, sizeof(UncompressedSize)) != sizeof(UncompressedSize))
|
|
return false;
|
|
|
|
constexpr size_t BufferSize = 16384;
|
|
char WriteBuffer[BufferSize];
|
|
z_stream Stream;
|
|
quint32 Crc32 = 0;
|
|
|
|
CacheFile.Seek(0, SEEK_SET);
|
|
|
|
Stream.zalloc = nullptr;
|
|
Stream.zfree = nullptr;
|
|
Stream.opaque = nullptr;
|
|
|
|
if (deflateInit2(&Stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY) != Z_OK)
|
|
return false;
|
|
|
|
Bytef* BufferIn = CacheFile.mBuffer;
|
|
int FlushMode;
|
|
int DeflateResult = Z_OK;
|
|
|
|
do
|
|
{
|
|
uInt Read = (uInt)lcMin(CacheFile.GetLength() - (BufferIn - CacheFile.mBuffer), BufferSize);
|
|
Stream.avail_in = Read;
|
|
Stream.next_in = BufferIn;
|
|
Crc32 = crc32(Crc32, BufferIn, Read);
|
|
BufferIn += Read;
|
|
|
|
FlushMode = (BufferIn >= CacheFile.mBuffer + CacheFile.GetLength()) ? Z_FINISH : Z_NO_FLUSH;
|
|
|
|
do
|
|
{
|
|
Stream.avail_out = BufferSize;
|
|
Stream.next_out = (Bytef*)WriteBuffer;
|
|
DeflateResult = deflate(&Stream, FlushMode);
|
|
|
|
if (DeflateResult != Z_OK && DeflateResult != Z_STREAM_END && DeflateResult != Z_BUF_ERROR)
|
|
{
|
|
deflateEnd(&Stream);
|
|
return false;
|
|
}
|
|
|
|
const qint64 BytesToWrite = static_cast<qint64>(BufferSize - Stream.avail_out);
|
|
|
|
if (File.write(WriteBuffer, BytesToWrite) != BytesToWrite)
|
|
{
|
|
deflateEnd(&Stream);
|
|
return false;
|
|
}
|
|
} while (Stream.avail_out == 0);
|
|
} while (FlushMode != Z_FINISH);
|
|
|
|
deflateEnd(&Stream);
|
|
|
|
return DeflateResult == Z_STREAM_END;
|
|
}
|
|
|
|
bool lcPiecesLibrary::ReadDirectoryCacheFile(const QString& FileName, lcMemFile& CacheFile)
|
|
{
|
|
QFile File(FileName);
|
|
|
|
if (!File.open(QIODevice::ReadOnly))
|
|
return false;
|
|
|
|
quint32 CacheVersion, CacheFlags;
|
|
|
|
if (File.read((char*)&CacheVersion, sizeof(CacheVersion)) != sizeof(CacheVersion) || CacheVersion != LC_LIBRARY_CACHE_VERSION)
|
|
return false;
|
|
|
|
if (File.read((char*)&CacheFlags, sizeof(CacheFlags)) != sizeof(CacheFlags) ||
|
|
CacheFlags != static_cast<quint32>(lcLibraryCacheFlag::Directory))
|
|
return false;
|
|
|
|
quint32 UncompressedSize;
|
|
|
|
if (File.read((char*)&UncompressedSize, sizeof(UncompressedSize)) != sizeof(UncompressedSize))
|
|
return false;
|
|
|
|
QByteArray Data = qUncompress(File.readAll());
|
|
if (Data.isEmpty() || static_cast<quint64>(Data.size()) != UncompressedSize)
|
|
return false;
|
|
|
|
CacheFile.SetLength(Data.size());
|
|
CacheFile.Seek(0, SEEK_SET);
|
|
|
|
if (!CacheFile.mBuffer)
|
|
return false;
|
|
|
|
CacheFile.WriteBuffer(Data.constData(), Data.size());
|
|
CacheFile.Seek(0, SEEK_SET);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool lcPiecesLibrary::WriteDirectoryCacheFile(const QString& FileName, lcMemFile& CacheFile)
|
|
{
|
|
QFile File(FileName);
|
|
|
|
if (!File.open(QIODevice::WriteOnly))
|
|
return false;
|
|
|
|
constexpr quint32 CacheVersion = LC_LIBRARY_CACHE_VERSION;
|
|
if (File.write((char*)&CacheVersion, sizeof(CacheVersion)) != sizeof(CacheVersion))
|
|
return false;
|
|
|
|
constexpr quint32 CacheFlags = static_cast<quint32>(lcLibraryCacheFlag::Directory);
|
|
if (File.write((char*)&CacheFlags, sizeof(CacheFlags)) != sizeof(CacheFlags))
|
|
return false;
|
|
|
|
const quint32 UncompressedSize = (quint32)CacheFile.GetLength();
|
|
if (File.write((char*)&UncompressedSize, sizeof(UncompressedSize)) != sizeof(UncompressedSize))
|
|
return false;
|
|
|
|
const QByteArray CompressedData = qCompress(CacheFile.mBuffer, (int)CacheFile.GetLength());
|
|
|
|
if (File.write(CompressedData) != CompressedData.size())
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool lcPiecesLibrary::LoadCacheIndex(const QString& FileName)
|
|
{
|
|
lcMemFile IndexFile;
|
|
|
|
if (!ReadArchiveCacheFile(FileName, IndexFile))
|
|
return false;
|
|
|
|
quint32 NumFiles;
|
|
|
|
if (IndexFile.ReadBuffer((char*)&NumFiles, sizeof(NumFiles)) == 0 || NumFiles != mPieces.size())
|
|
return false;
|
|
|
|
for (const auto& PieceIt : mPieces)
|
|
{
|
|
lcPieceData* PieceData = PieceIt.second;
|
|
quint8 Length;
|
|
|
|
static_assert(sizeof(PieceData->m_strDescription) == 256);
|
|
|
|
if (IndexFile.ReadBuffer((char*)&Length, sizeof(Length)) == 0)
|
|
return false;
|
|
|
|
if (IndexFile.ReadBuffer((char*)PieceData->m_strDescription, Length) == 0)
|
|
return false;
|
|
|
|
PieceData->m_strDescription[Length] = 0;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool lcPiecesLibrary::SaveArchiveCacheIndex(const QString& FileName)
|
|
{
|
|
lcMemFile IndexFile;
|
|
|
|
const quint32 NumFiles = (quint32)mPieces.size();
|
|
|
|
if (IndexFile.WriteBuffer((char*)&NumFiles, sizeof(NumFiles)) == 0)
|
|
return false;
|
|
|
|
for (const auto& PieceIt : mPieces)
|
|
{
|
|
const lcPieceData* PieceData = PieceIt.second;
|
|
const quint8 Length = (quint8)strlen(PieceData->m_strDescription);
|
|
|
|
if (IndexFile.WriteBuffer((char*)&Length, sizeof(Length)) == 0)
|
|
return false;
|
|
|
|
if (IndexFile.WriteBuffer((char*)PieceData->m_strDescription, Length) == 0)
|
|
return false;
|
|
}
|
|
|
|
return WriteArchiveCacheFile(FileName, IndexFile);
|
|
}
|
|
|
|
void lcPiecesLibrary::RequestPieceDataLoad(lcPieceData* PieceData, lcLoadPriority LoadPriority)
|
|
{
|
|
Q_ASSERT(QThread::currentThread() == thread());
|
|
|
|
if (PieceData->IsProject() || (PieceData->IsModel() && !PieceData->GetModel()->HasDirectGeometry()))
|
|
{
|
|
PieceData->mState = lcAssetState::Loaded;
|
|
ClearPieceDataLoadError(PieceData);
|
|
return;
|
|
}
|
|
|
|
if (PieceData->mState == lcAssetState::Unloaded)
|
|
{
|
|
PieceData->mState = lcAssetState::Loading;
|
|
ClearPieceDataLoadError(PieceData);
|
|
|
|
const lcBoundingBox& Box = GetLoadingMesh()->mBoundingBox;
|
|
|
|
if (!PieceData->IsModel())
|
|
PieceData->SetBoundingBox(Box.Min, Box.Max);
|
|
else
|
|
{
|
|
const lcBoundingBox& ModelBox = PieceData->GetBoundingBox();
|
|
PieceData->SetBoundingBox(lcMin(ModelBox.Min, Box.Min), lcMax(ModelBox.Max, Box.Max));
|
|
}
|
|
}
|
|
|
|
if (PieceData->IsLoading())
|
|
mAssetLoader->RequestPieceLoad(PieceData, LoadPriority);
|
|
}
|
|
|
|
void lcPiecesLibrary::PieceDataLoadCancelled(lcPieceData* PieceData)
|
|
{
|
|
Q_ASSERT(QThread::currentThread() == thread());
|
|
|
|
PieceData->mState = lcAssetState::Unloaded;
|
|
ClearPieceDataLoadError(PieceData);
|
|
}
|
|
|
|
void lcPiecesLibrary::NotifyAssetStatusChanged()
|
|
{
|
|
emit AssetStatusChanged();
|
|
}
|
|
|
|
bool lcPiecesLibrary::EnsurePieceDataAssetsReady(const lcPieceDataRef& PieceData)
|
|
{
|
|
const bool PieceReady = WaitForPieceData(PieceData);
|
|
|
|
if (PieceData->IsModel())
|
|
return static_cast<bool>(PieceData->GetModel()->EnsureAssetsReady()) && PieceReady;
|
|
|
|
if (PieceData->IsProject())
|
|
{
|
|
const bool ProjectReady = static_cast<bool>(PieceData->GetProject()->EnsureAssetsReady());
|
|
|
|
PieceData->GetProject()->UpdatePieceData(PieceData.Get());
|
|
return ProjectReady && PieceReady;
|
|
}
|
|
|
|
return PieceReady;
|
|
}
|
|
|
|
bool lcPiecesLibrary::WaitForPieceData(const lcPieceDataRef& PieceDataRef)
|
|
{
|
|
Q_ASSERT(QThread::currentThread() == thread());
|
|
|
|
if (PieceDataRef->mState == lcAssetState::Unloaded)
|
|
RequestPieceDataLoad(PieceDataRef.Get(), lcLoadPriority::Blocking);
|
|
|
|
const bool RequestsSucceeded = mAssetLoader->WaitForPieces({ PieceDataRef.Get() });
|
|
|
|
return RequestsSucceeded && PieceDataRef->mState == lcAssetState::Loaded;
|
|
}
|
|
|
|
bool lcPiecesLibrary::WaitForPieceData(const std::vector<lcPieceDataRef>& PieceDataRefs)
|
|
{
|
|
Q_ASSERT(QThread::currentThread() == thread());
|
|
|
|
std::vector<lcPieceData*> Pieces;
|
|
Pieces.reserve(PieceDataRefs.size());
|
|
|
|
for (const lcPieceDataRef& PieceDataRef : PieceDataRefs)
|
|
{
|
|
if (PieceDataRef->mState == lcAssetState::Unloaded)
|
|
RequestPieceDataLoad(PieceDataRef.Get(), lcLoadPriority::Blocking);
|
|
|
|
Pieces.push_back(PieceDataRef.Get());
|
|
}
|
|
|
|
const bool RequestsSucceeded = mAssetLoader->WaitForPieces(Pieces);
|
|
|
|
for (const lcPieceDataRef& PieceDataRef : PieceDataRefs)
|
|
if (PieceDataRef->mState != lcAssetState::Loaded)
|
|
return false;
|
|
|
|
return RequestsSucceeded;
|
|
}
|
|
|
|
void lcPiecesLibrary::RequestSynthMesh(lcPiece* Piece)
|
|
{
|
|
Q_ASSERT(QThread::currentThread() == thread());
|
|
|
|
lcPieceData* PieceData = Piece->mPieceData.Get();
|
|
std::shared_ptr<const lcSynthInfo> SynthInfo = PieceData ? PieceData->GetSynthInfoShared() : nullptr;
|
|
|
|
if (!SynthInfo)
|
|
{
|
|
CancelSynthMesh(Piece);
|
|
return;
|
|
}
|
|
|
|
mSynthPieces.insert_or_assign(Piece, QString());
|
|
|
|
mAssetLoader->RequestSynthMesh(Piece, PieceData, std::move(SynthInfo), Piece->GetControlPoints());
|
|
}
|
|
|
|
void lcPiecesLibrary::CancelSynthMesh(lcPiece* Piece)
|
|
{
|
|
Q_ASSERT(QThread::currentThread() == thread());
|
|
|
|
mSynthPieces.erase(Piece);
|
|
|
|
if (mAssetLoader)
|
|
mAssetLoader->CancelSynthMesh(Piece);
|
|
}
|
|
|
|
bool lcPiecesLibrary::WaitForSynthMeshes(const std::vector<lcPiece*>& Pieces)
|
|
{
|
|
Q_ASSERT(QThread::currentThread() == thread());
|
|
|
|
const bool RequestsSucceeded = mAssetLoader->WaitForSynthMeshes(Pieces);
|
|
|
|
for (const lcPiece* Piece : Pieces)
|
|
if (Piece->mPieceData && Piece->mPieceData->GetSynthInfo() && !Piece->HasGeneratedMesh())
|
|
return false;
|
|
|
|
return RequestsSucceeded;
|
|
}
|
|
|
|
QString lcPiecesLibrary::GetSynthMeshError(const lcPiece* Piece) const
|
|
{
|
|
Q_ASSERT(QThread::currentThread() == thread());
|
|
|
|
const auto It = mSynthPieces.find(const_cast<lcPiece*>(Piece));
|
|
|
|
return It != mSynthPieces.end() ? It->second : QString();
|
|
}
|
|
|
|
bool lcPiecesLibrary::PublishPieceDataMesh(lcPieceData* PieceData, std::unique_ptr<lcMesh> Mesh, QString Error)
|
|
{
|
|
Q_ASSERT(QThread::currentThread() == thread());
|
|
|
|
const bool Loaded = Error.isEmpty();
|
|
|
|
if (Loaded)
|
|
{
|
|
if (Mesh)
|
|
PieceData->SetMesh(Mesh.release());
|
|
|
|
PieceData->mState = lcAssetState::Loaded;
|
|
ClearPieceDataLoadError(PieceData);
|
|
}
|
|
else
|
|
PieceData->SetFailed(std::move(Error));
|
|
|
|
ScheduleBufferRepack();
|
|
|
|
return Loaded;
|
|
}
|
|
|
|
bool lcPiecesLibrary::PublishSynthMesh(lcPiece* Piece, std::unique_ptr<lcMesh> Mesh, QString Error)
|
|
{
|
|
Q_ASSERT(QThread::currentThread() == thread());
|
|
|
|
const bool Loaded = Error.isEmpty() && Mesh;
|
|
|
|
if (!Loaded && Error.isEmpty())
|
|
Error = tr("Could not generate geometry for piece '%1'.").arg(QString::fromLatin1(Piece->mPieceData->mFileName));
|
|
|
|
Piece->SetGeneratedMesh(Loaded ? Mesh.release() : nullptr);
|
|
|
|
const auto It = mSynthPieces.find(Piece);
|
|
|
|
if (It != mSynthPieces.end())
|
|
It->second = Loaded ? QString() : std::move(Error);
|
|
|
|
return Loaded;
|
|
}
|
|
|
|
void lcPiecesLibrary::PublishTexture(lcTexture* Texture, lcTextureBuildResult Result)
|
|
{
|
|
Q_ASSERT(QThread::currentThread() == thread());
|
|
|
|
if (!Result.DecodedImage.isNull())
|
|
Texture->SetImage(std::move(Result.DecodedImage), LC_TEXTURE_MIPMAPS);
|
|
else
|
|
Texture->SetFailed(Result.Error, std::move(Result.ErrorDetails));
|
|
}
|
|
|
|
void lcPiecesLibrary::RebuildModelPieceData(lcPieceData* PieceData)
|
|
{
|
|
Q_ASSERT(QThread::currentThread() == thread());
|
|
Q_ASSERT(PieceData->IsModel());
|
|
|
|
InvalidatePieceDataLoad(PieceData);
|
|
PieceData->ReleaseMesh();
|
|
RequestPieceDataLoad(PieceData, lcLoadPriority::Normal);
|
|
}
|
|
|
|
void lcPiecesLibrary::InvalidatePieceDataLoad(lcPieceData* PieceData)
|
|
{
|
|
Q_ASSERT(QThread::currentThread() == thread());
|
|
|
|
PieceData->mState = lcAssetState::Unloaded;
|
|
ClearPieceDataLoadError(PieceData);
|
|
|
|
if (mAssetLoader)
|
|
mAssetLoader->InvalidatePieceLoad(PieceData);
|
|
}
|
|
|
|
void lcPiecesLibrary::ReleasePieceData(lcPieceData* PieceData)
|
|
{
|
|
int Remaining;
|
|
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
|
|
if (PieceData->GetRefCount() == 0)
|
|
return;
|
|
|
|
Remaining = PieceData->Release();
|
|
|
|
if (Remaining == 0)
|
|
UnloadPieceData(PieceData);
|
|
}
|
|
|
|
if (Remaining == 1 && mAssetLoader)
|
|
mAssetLoader->CancelPieceLoad(PieceData);
|
|
}
|
|
|
|
void lcPiecesLibrary::AddPieceDataReference(lcPieceData* PieceData)
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
|
|
PieceData->AddRef();
|
|
}
|
|
|
|
bool lcPiecesLibrary::HasPieceDataConsumers(const lcPieceData* PieceData)
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
|
|
return PieceData->GetRefCount() > 1;
|
|
}
|
|
|
|
void lcPiecesLibrary::SetPieceDataLoadError(const lcPieceData* PieceData, QString Error)
|
|
{
|
|
QMutexLocker ErrorLock(&mPieceErrorMutex);
|
|
|
|
if (Error.isEmpty())
|
|
mFailedPartErrors.erase(PieceData);
|
|
else
|
|
mFailedPartErrors.insert_or_assign(PieceData, std::move(Error));
|
|
}
|
|
|
|
QString lcPiecesLibrary::GetPieceDataLoadError(const lcPieceData* PieceData) const
|
|
{
|
|
QMutexLocker ErrorLock(&mPieceErrorMutex);
|
|
|
|
const auto It = mFailedPartErrors.find(PieceData);
|
|
|
|
return It == mFailedPartErrors.end() ? QString() : It->second;
|
|
}
|
|
|
|
void lcPiecesLibrary::ClearPieceDataLoadError(const lcPieceData* PieceData)
|
|
{
|
|
QMutexLocker ErrorLock(&mPieceErrorMutex);
|
|
|
|
mFailedPartErrors.erase(PieceData);
|
|
}
|
|
|
|
lcPieceSourceSnapshot lcPiecesLibrary::SnapshotPieceDataSource(const lcPieceData* PieceData) const
|
|
{
|
|
lcPieceSourceSnapshot Source;
|
|
|
|
Source.FileName = QString::fromLatin1(PieceData->mFileName);
|
|
Source.LibraryDirectory = mLibraryDir.absolutePath();
|
|
Source.CachePath = QFileInfo(QDir(mCachePath), Source.FileName).absoluteFilePath();
|
|
Source.ZipFileType = PieceData->mZipFileType;
|
|
Source.ZipFileIndex = PieceData->mZipFileIndex;
|
|
Source.StudStyle = mStudStyle;
|
|
Source.StudCylinderColorEnabled = mStudCylinderColorEnabled;
|
|
|
|
if (PieceData->IsModel())
|
|
{
|
|
Source.InlineModel = true;
|
|
Source.InlineMeshLines = PieceData->GetModel()->GetFileLines();
|
|
|
|
const lcModel* Model = PieceData->GetModel();
|
|
Source.TextureSearchDirectories = Model->GetAssetSearchDirectories();
|
|
|
|
if (Model->GetProject())
|
|
Source.EmbeddedData = Model->GetProject()->GetEmbeddedDataLookup();
|
|
}
|
|
|
|
return Source;
|
|
}
|
|
|
|
lcPieceBuildResult lcPiecesLibrary::BuildPieceData(const lcPieceSourceSnapshot& Source)
|
|
{
|
|
lcPieceBuildResult Result;
|
|
|
|
if (mCancelLoading.load())
|
|
{
|
|
Result.Error = tr("Library loading was cancelled.");
|
|
return Result;
|
|
}
|
|
|
|
if (!Source.InlineModel && Source.ZipFileType != lcZipFileType::Count && mZipFiles[static_cast<int>(Source.ZipFileType)])
|
|
{
|
|
lcMemFile CacheData;
|
|
|
|
if (ReadArchiveCacheFile(Source.CachePath, CacheData))
|
|
{
|
|
qint32 Flags;
|
|
|
|
if (CacheData.ReadBuffer(&Flags, sizeof(Flags)) == sizeof(Flags) && Flags == MeshCacheSettingsKey(Source.StudStyle, Source.StudCylinderColorEnabled))
|
|
{
|
|
Result.Mesh.reset(new lcMesh);
|
|
|
|
if (Result.Mesh->FileLoad(CacheData))
|
|
{
|
|
Result.FromCache = true;
|
|
return Result;
|
|
}
|
|
|
|
Result.Mesh.reset();
|
|
}
|
|
}
|
|
}
|
|
|
|
lcLibraryMeshData MeshData;
|
|
lcMeshLoader MeshLoader(MeshData, nullptr, lcMeshLoaderFlag::Optimize | lcMeshLoaderFlag::RequireAllIncludes);
|
|
|
|
if (Source.InlineModel)
|
|
{
|
|
lcMemFile PieceFile;
|
|
|
|
for (const QString& Line : Source.InlineMeshLines)
|
|
{
|
|
const QByteArray Buffer = Line.toLatin1();
|
|
|
|
PieceFile.WriteBuffer(Buffer.constData(), Buffer.size());
|
|
PieceFile.WriteBuffer("\r\n", 2);
|
|
}
|
|
|
|
PieceFile.Seek(0, SEEK_SET);
|
|
|
|
if (!MeshLoader.LoadMesh(PieceFile, LC_MESHDATA_SHARED))
|
|
{
|
|
Result.Error = tr("Could not load model geometry %1.").arg(Source.FileName);
|
|
|
|
if (!MeshLoader.GetError().isEmpty())
|
|
Result.Error += QLatin1Char('\n') + MeshLoader.GetError();
|
|
|
|
return Result;
|
|
}
|
|
|
|
if (MeshData.IsEmpty())
|
|
{
|
|
Result.EmptyGeometry = true;
|
|
return Result;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bool Loaded = false;
|
|
|
|
if (Source.ZipFileType != lcZipFileType::Count && mZipFiles[static_cast<int>(Source.ZipFileType)])
|
|
{
|
|
lcMemFile PieceFile;
|
|
|
|
if (mZipFiles[static_cast<int>(Source.ZipFileType)]->ExtractFile(Source.ZipFileIndex, PieceFile))
|
|
Loaded = MeshLoader.LoadMesh(PieceFile, LC_MESHDATA_SHARED);
|
|
}
|
|
else
|
|
{
|
|
lcDiskFile PieceFile;
|
|
|
|
PieceFile.SetFileName(QDir(Source.LibraryDirectory).absoluteFilePath(QStringLiteral("parts/") + Source.FileName));
|
|
|
|
if (PieceFile.Open(QIODevice::ReadOnly))
|
|
Loaded = MeshLoader.LoadMesh(PieceFile, LC_MESHDATA_SHARED);
|
|
|
|
if (mHasUnofficial && !Loaded)
|
|
{
|
|
MeshData.Clear();
|
|
|
|
PieceFile.SetFileName(QDir(Source.LibraryDirectory).absoluteFilePath(QStringLiteral("unofficial/parts/") + Source.FileName));
|
|
|
|
if (PieceFile.Open(QIODevice::ReadOnly))
|
|
Loaded = MeshLoader.LoadMesh(PieceFile, LC_MESHDATA_SHARED);
|
|
}
|
|
}
|
|
|
|
if (mCancelLoading.load())
|
|
Result.Error = tr("Library loading was cancelled.");
|
|
else if (!Loaded)
|
|
{
|
|
Result.Error = tr("Could not load part %1.").arg(Source.FileName);
|
|
|
|
if (!MeshLoader.GetError().isEmpty())
|
|
Result.Error += QLatin1Char('\n') + MeshLoader.GetError();
|
|
}
|
|
|
|
if (!Result.Error.isEmpty())
|
|
return Result;
|
|
}
|
|
|
|
Result.Mesh.reset(MeshData.CreateMesh(nullptr));
|
|
|
|
if (!Result.Mesh)
|
|
Result.Error = tr("Could not prepare piece %1 for rendering.").arg(Source.FileName);
|
|
|
|
return Result;
|
|
}
|
|
|
|
void lcPiecesLibrary::SaveBuiltPieceDataCache(const lcPieceSourceSnapshot& Source, lcMesh& Mesh)
|
|
{
|
|
if (Source.ZipFileType == lcZipFileType::Count)
|
|
return;
|
|
|
|
lcMemFile MeshData;
|
|
|
|
const qint32 Flags = MeshCacheSettingsKey(Source.StudStyle, Source.StudCylinderColorEnabled);
|
|
|
|
if (MeshData.WriteBuffer((char*)&Flags, sizeof(Flags)) != sizeof(Flags) || !Mesh.FileSave(MeshData))
|
|
return;
|
|
|
|
WriteArchiveCacheFile(Source.CachePath, MeshData);
|
|
}
|
|
|
|
void lcPiecesLibrary::GetPrimitiveFile(lcLibraryPrimitive* Primitive, std::function<void(lcFile& File)> Callback)
|
|
{
|
|
if (mZipFiles[static_cast<int>(lcZipFileType::Official)])
|
|
{
|
|
lcMemFile IncludeFile;
|
|
|
|
if (mZipFiles[static_cast<int>(Primitive->mZipFileType)]->ExtractFile(Primitive->mZipFileIndex, IncludeFile))
|
|
Callback(IncludeFile);
|
|
}
|
|
else
|
|
{
|
|
lcDiskFile IncludeFile(Primitive->mFileName);
|
|
|
|
if (IncludeFile.Open(QIODevice::ReadOnly))
|
|
Callback(IncludeFile);
|
|
}
|
|
}
|
|
|
|
void lcPiecesLibrary::GetPieceDataFile(const char* PieceName, std::function<void(lcFile& File)> Callback)
|
|
{
|
|
bool HasPiece = false;
|
|
lcZipFileType PieceZipFileType = lcZipFileType::Count;
|
|
int PieceZipFileIndex = -1;
|
|
std::string PieceFileName;
|
|
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
const auto PieceIt = mPieces.find(PieceName);
|
|
|
|
if (PieceIt != mPieces.end())
|
|
{
|
|
const lcPieceData* PieceData = PieceIt->second;
|
|
|
|
HasPiece = true;
|
|
PieceZipFileType = PieceData->mZipFileType;
|
|
PieceZipFileIndex = PieceData->mZipFileIndex;
|
|
PieceFileName = PieceData->mFileName;
|
|
}
|
|
}
|
|
|
|
if (HasPiece)
|
|
{
|
|
if (mZipFiles[static_cast<int>(lcZipFileType::Official)] && PieceZipFileType != lcZipFileType::Count)
|
|
{
|
|
lcMemFile IncludeFile;
|
|
|
|
if (mZipFiles[static_cast<int>(PieceZipFileType)]->ExtractFile(PieceZipFileIndex, IncludeFile))
|
|
Callback(IncludeFile);
|
|
}
|
|
else
|
|
{
|
|
lcDiskFile IncludeFile;
|
|
char FileName[LC_MAXPATH];
|
|
bool Found = false;
|
|
|
|
snprintf(FileName, sizeof(FileName), "parts/%s", PieceFileName.c_str());
|
|
IncludeFile.SetFileName(mLibraryDir.absoluteFilePath(QLatin1String(FileName)));
|
|
Found = IncludeFile.Open(QIODevice::ReadOnly);
|
|
|
|
if (mHasUnofficial && !Found)
|
|
{
|
|
snprintf(FileName, sizeof(FileName), "unofficial/parts/%s", PieceFileName.c_str());
|
|
IncludeFile.SetFileName(mLibraryDir.absoluteFilePath(QLatin1String(FileName)));
|
|
Found = IncludeFile.Open(QIODevice::ReadOnly);
|
|
}
|
|
|
|
if (Found)
|
|
Callback(IncludeFile);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bool Found = false;
|
|
|
|
if (mZipFiles[static_cast<int>(lcZipFileType::Official)])
|
|
{
|
|
lcMemFile IncludeFile;
|
|
|
|
auto LoadIncludeFile = [&IncludeFile, PieceName, this](const char* Folder, lcZipFileType ZipFileType)
|
|
{
|
|
char IncludeFileName[LC_MAXPATH];
|
|
snprintf(IncludeFileName, sizeof(IncludeFileName), Folder, PieceName);
|
|
return mZipFiles[static_cast<int>(ZipFileType)]->ExtractFile(IncludeFileName, IncludeFile);
|
|
};
|
|
|
|
Found = LoadIncludeFile("ldraw/parts/%s", lcZipFileType::Official);
|
|
|
|
if (!Found)
|
|
Found = LoadIncludeFile("ldraw/p/%s", lcZipFileType::Official);
|
|
|
|
if (mZipFiles[static_cast<int>(lcZipFileType::Unofficial)] && !Found)
|
|
{
|
|
Found = LoadIncludeFile("parts/%s", lcZipFileType::Unofficial);
|
|
|
|
if (!Found)
|
|
Found = LoadIncludeFile("p/%s", lcZipFileType::Unofficial);
|
|
}
|
|
|
|
if (Found)
|
|
Callback(IncludeFile);
|
|
}
|
|
else
|
|
{
|
|
lcDiskFile IncludeFile;
|
|
|
|
auto LoadIncludeFile = [&IncludeFile, PieceName, this](const QLatin1String& Folder)
|
|
{
|
|
const QString IncludeFileName = Folder + PieceName;
|
|
IncludeFile.SetFileName(mLibraryDir.absoluteFilePath(IncludeFileName));
|
|
if (IncludeFile.Open(QIODevice::ReadOnly))
|
|
return true;
|
|
|
|
#if defined(Q_OS_MACOS) || defined(Q_OS_LINUX)
|
|
// todo: search the parts/primitive lists and get the file name from there instead of using toLower
|
|
IncludeFile.SetFileName(mLibraryDir.absoluteFilePath(IncludeFileName.toLower()));
|
|
return IncludeFile.Open(QIODevice::ReadOnly);
|
|
#else
|
|
return false;
|
|
#endif
|
|
};
|
|
|
|
Found = LoadIncludeFile(QLatin1String("parts/"));
|
|
|
|
if (!Found)
|
|
Found = LoadIncludeFile(QLatin1String("p/"));
|
|
|
|
if (mHasUnofficial && !Found)
|
|
{
|
|
Found = LoadIncludeFile(QLatin1String("unofficial/parts/"));
|
|
|
|
if (!Found)
|
|
Found = LoadIncludeFile(QLatin1String("unofficial/p/"));
|
|
}
|
|
|
|
if (Found)
|
|
Callback(IncludeFile);
|
|
}
|
|
}
|
|
}
|
|
|
|
void lcPiecesLibrary::ReleaseBuffers()
|
|
{
|
|
mBuffersDirty = true;
|
|
|
|
// Library-only loading can finish without creating a renderer or shared buffers.
|
|
if (!mVertexBuffer.IsValid() && !mIndexBuffer.IsValid())
|
|
return;
|
|
|
|
lcContext* Context = lcContext::GetGlobalOffscreenContext();
|
|
|
|
Context->MakeCurrent();
|
|
Context->DestroyVertexBuffer(mVertexBuffer);
|
|
Context->DestroyIndexBuffer(mIndexBuffer);
|
|
}
|
|
|
|
void lcPiecesLibrary::UpdateBuffers(lcContext* Context)
|
|
{
|
|
if (!gSupportsVertexBufferObject || !mBuffersDirty)
|
|
return;
|
|
|
|
int VertexDataSize = 0;
|
|
int IndexDataSize = 0;
|
|
std::vector<lcMesh*> Meshes;
|
|
|
|
const auto AddMesh = [&VertexDataSize, &IndexDataSize, &Meshes](lcPieceData* PieceData)
|
|
{
|
|
lcMesh* Mesh = PieceData->GetMesh();
|
|
|
|
if (!Mesh)
|
|
return;
|
|
|
|
if (Mesh->mVertexDataSize < 0 || Mesh->mIndexDataSize < 0 ||
|
|
Mesh->mVertexDataSize > 16 * 1024 * 1024 || Mesh->mIndexDataSize > 16 * 1024 * 1024 ||
|
|
(Mesh->mVertexDataSize && !Mesh->mVertexData) || (Mesh->mIndexDataSize && !Mesh->mIndexData) ||
|
|
VertexDataSize > std::numeric_limits<int>::max() - Mesh->mVertexDataSize ||
|
|
IndexDataSize > std::numeric_limits<int>::max() - Mesh->mIndexDataSize)
|
|
{
|
|
Mesh->mVertexCacheOffset = -1;
|
|
Mesh->mIndexCacheOffset = -1;
|
|
return;
|
|
}
|
|
|
|
VertexDataSize += Mesh->mVertexDataSize;
|
|
IndexDataSize += Mesh->mIndexDataSize;
|
|
|
|
Meshes.push_back(Mesh);
|
|
};
|
|
|
|
for (const std::map<std::string, lcPieceData*>::value_type& PieceIt : mPieces)
|
|
AddMesh(PieceIt.second);
|
|
|
|
for (const std::unordered_map<lcPieceData*, Project*>::value_type& Local : mProjectPieces)
|
|
AddMesh(Local.first);
|
|
|
|
if (!VertexDataSize)
|
|
{
|
|
Context->DestroyVertexBuffer(mVertexBuffer);
|
|
Context->DestroyIndexBuffer(mIndexBuffer);
|
|
mBuffersDirty = false;
|
|
|
|
return;
|
|
}
|
|
|
|
void* VertexData = malloc(VertexDataSize);
|
|
void* IndexData = IndexDataSize ? malloc(IndexDataSize) : nullptr;
|
|
|
|
if (!VertexData || (IndexDataSize && !IndexData))
|
|
{
|
|
free(VertexData);
|
|
free(IndexData);
|
|
|
|
return;
|
|
}
|
|
|
|
VertexDataSize = 0;
|
|
IndexDataSize = 0;
|
|
|
|
for (lcMesh* Mesh : Meshes)
|
|
{
|
|
if (Mesh->mIndexDataSize && !IndexData)
|
|
{
|
|
for (lcMesh* CachedMesh : Meshes)
|
|
{
|
|
CachedMesh->mVertexCacheOffset = -1;
|
|
CachedMesh->mIndexCacheOffset = -1;
|
|
}
|
|
|
|
free(VertexData);
|
|
free(IndexData);
|
|
return;
|
|
}
|
|
|
|
Mesh->mVertexCacheOffset = VertexDataSize;
|
|
Mesh->mIndexCacheOffset = IndexDataSize;
|
|
|
|
if (Mesh->mVertexDataSize)
|
|
memcpy((char*)VertexData + VertexDataSize, Mesh->mVertexData, Mesh->mVertexDataSize);
|
|
|
|
if (Mesh->mIndexDataSize)
|
|
memcpy((char*)IndexData + IndexDataSize, Mesh->mIndexData, Mesh->mIndexDataSize);
|
|
|
|
VertexDataSize += Mesh->mVertexDataSize;
|
|
IndexDataSize += Mesh->mIndexDataSize;
|
|
}
|
|
|
|
Context->DestroyVertexBuffer(mVertexBuffer);
|
|
Context->DestroyIndexBuffer(mIndexBuffer);
|
|
|
|
mVertexBuffer = Context->CreateVertexBuffer(VertexDataSize, VertexData);
|
|
if (IndexDataSize)
|
|
mIndexBuffer = Context->CreateIndexBuffer(IndexDataSize, IndexData);
|
|
mBuffersDirty = false;
|
|
|
|
free(VertexData);
|
|
free(IndexData);
|
|
}
|
|
|
|
void lcPiecesLibrary::ScheduleBufferRepack()
|
|
{
|
|
mStreamingBuffersPending = true;
|
|
|
|
if (mBufferRepackScheduled)
|
|
return;
|
|
|
|
mBufferRepackScheduled = true;
|
|
|
|
QTimer::singleShot(120, this, [this]()
|
|
{
|
|
mBufferRepackScheduled = false;
|
|
|
|
if (!mStreamingBuffersPending)
|
|
return;
|
|
|
|
if (mAssetLoader && mAssetLoader->HasPendingWork())
|
|
{
|
|
ScheduleBufferRepack();
|
|
return;
|
|
}
|
|
|
|
mStreamingBuffersPending = false;
|
|
mBuffersDirty = true;
|
|
lcView::UpdateAllViews();
|
|
});
|
|
}
|
|
|
|
lcMesh* lcPiecesLibrary::GetLoadingMesh() const
|
|
{
|
|
return mLoadingMesh.get();
|
|
}
|
|
|
|
void lcPiecesLibrary::UnloadUnusedParts()
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
|
|
for (const auto& PieceIt : mPieces)
|
|
{
|
|
lcPieceData* PieceData = PieceIt.second;
|
|
if (PieceData->GetRefCount() == 0 && PieceData->mState != lcAssetState::Unloaded)
|
|
ReleasePieceData(PieceData);
|
|
}
|
|
}
|
|
|
|
void lcPiecesLibrary::AddTextureReference(lcTexture* Texture)
|
|
{
|
|
mTextureManager->AddTextureReference(Texture);
|
|
}
|
|
|
|
void lcPiecesLibrary::ReleaseTexture(lcTexture* Texture)
|
|
{
|
|
mTextureManager->ReleaseTexture(Texture);
|
|
}
|
|
|
|
bool lcPiecesLibrary::SupportsStudStyle() const
|
|
{
|
|
return true;
|
|
}
|
|
|
|
void lcPiecesLibrary::UpdateStudStyleAndColors(lcStudStyle StudStyle, bool StudCylinderColorEnabled)
|
|
{
|
|
if (mStudStyle == StudStyle && mStudCylinderColorEnabled == StudCylinderColorEnabled)
|
|
return;
|
|
|
|
// Finish running workers before changing shared primitive data and colors.
|
|
// Queued requests resume with the new settings afterward.
|
|
mAssetLoader->PauseQueuedWork();
|
|
|
|
mStudStyle = StudStyle;
|
|
mStudCylinderColorEnabled = StudCylinderColorEnabled;
|
|
|
|
LoadColors();
|
|
UpdateStudStyleSource();
|
|
|
|
mLoadMutex.lock();
|
|
|
|
for (const std::unique_ptr<lcLibrarySource>& Source : mSources)
|
|
{
|
|
for (const auto& PrimitiveIt : Source->Primitives)
|
|
{
|
|
lcLibraryPrimitive* Primitive = PrimitiveIt.second;
|
|
|
|
if (Primitive->mStudStyle || Primitive->mMeshData.mHasStyleStud)
|
|
Primitive->Unload();
|
|
}
|
|
}
|
|
|
|
const auto ReloadPiece = [this](lcPieceData* PieceData)
|
|
{
|
|
if (PieceData->mState != lcAssetState::Loaded || !PieceData->GetMesh() || !(PieceData->GetMesh()->mFlags & lcMeshFlag::HasStyleStud))
|
|
return;
|
|
|
|
if (PieceData->IsModel())
|
|
{
|
|
PieceData->ReleaseMesh();
|
|
PieceData->mState = lcAssetState::Unloaded;
|
|
|
|
RequestPieceDataLoad(PieceData, lcLoadPriority::Low);
|
|
}
|
|
else if (!PieceData->IsProject())
|
|
{
|
|
PieceData->Unload();
|
|
RequestPieceDataLoad(PieceData, lcLoadPriority::Low);
|
|
}
|
|
|
|
mBuffersDirty = true;
|
|
};
|
|
|
|
for (const std::map<std::string, lcPieceData*>::value_type& PieceIt : mPieces)
|
|
ReloadPiece(PieceIt.second);
|
|
|
|
for (const std::unordered_map<lcPieceData*, Project*>::value_type& Local : mProjectPieces)
|
|
ReloadPiece(Local.first);
|
|
|
|
mLoadMutex.unlock();
|
|
|
|
std::vector<lcPiece*> SynthPieces;
|
|
SynthPieces.reserve(mSynthPieces.size());
|
|
|
|
for (const auto& Entry : mSynthPieces)
|
|
SynthPieces.push_back(Entry.first);
|
|
|
|
for (lcPiece* Piece : SynthPieces)
|
|
Piece->RefreshGeneratedMesh();
|
|
|
|
mAssetLoader->ResumeQueuedWork();
|
|
}
|
|
|
|
bool lcPiecesLibrary::IsPrimitive(const char* Name) const
|
|
{
|
|
for (const std::unique_ptr<lcLibrarySource>& Source : mSources)
|
|
if (Source->Primitives.find(Name) != Source->Primitives.end())
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
lcLibraryPrimitive* lcPiecesLibrary::FindPrimitive(const char* Name) const
|
|
{
|
|
for (const std::unique_ptr<lcLibrarySource>& Source : mSources)
|
|
{
|
|
const auto PrimitiveIt = Source->Primitives.find(Name);
|
|
|
|
if (PrimitiveIt != Source->Primitives.end())
|
|
return PrimitiveIt->second;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
lcResult<void> lcPiecesLibrary::LoadPrimitive(lcLibraryPrimitive* Primitive)
|
|
{
|
|
QThread* Thread = QThread::currentThread();
|
|
lcLibraryPrimitive* Parent = nullptr;
|
|
|
|
mPrimitiveMutex.lock();
|
|
|
|
if (ShouldCancelLoading())
|
|
{
|
|
mPrimitiveMutex.unlock();
|
|
return lcUnexpected(tr("Library loading was cancelled."));
|
|
}
|
|
|
|
if (Primitive->mState == lcPrimitiveState::Loaded)
|
|
{
|
|
mPrimitiveMutex.unlock();
|
|
return lcResult<void>();
|
|
}
|
|
|
|
std::vector<lcLibraryPrimitive*>& Stack = mPrimitiveLoadStacks[Thread];
|
|
|
|
if (!Stack.empty())
|
|
{
|
|
Parent = Stack.back();
|
|
|
|
for (lcLibraryPrimitive* Dependency = Primitive; Dependency;)
|
|
{
|
|
if (Dependency == Parent)
|
|
{
|
|
mPrimitiveMutex.unlock();
|
|
return lcUnexpected(tr("Primitive include cycle at '%1'.").arg(QString::fromLatin1(Primitive->mName)));
|
|
}
|
|
|
|
const auto It = mPrimitiveDependencies.find(Dependency);
|
|
Dependency = It != mPrimitiveDependencies.end() ? It->second : nullptr;
|
|
}
|
|
|
|
mPrimitiveDependencies[Parent] = Primitive;
|
|
}
|
|
|
|
Stack.push_back(Primitive);
|
|
mPrimitiveMutex.unlock();
|
|
|
|
lcResult<void> Result = LoadPrimitiveData(Primitive);
|
|
|
|
mPrimitiveMutex.lock();
|
|
|
|
if (Parent)
|
|
mPrimitiveDependencies.erase(Parent);
|
|
|
|
std::vector<lcLibraryPrimitive*>& CompletedStack = mPrimitiveLoadStacks[Thread];
|
|
CompletedStack.pop_back();
|
|
|
|
if (CompletedStack.empty())
|
|
mPrimitiveLoadStacks.erase(Thread);
|
|
|
|
mPrimitiveMutex.unlock();
|
|
|
|
return Result;
|
|
}
|
|
|
|
lcResult<void> lcPiecesLibrary::LoadPrimitiveData(lcLibraryPrimitive* Primitive)
|
|
{
|
|
mPrimitiveMutex.lock();
|
|
|
|
if (ShouldCancelLoading())
|
|
{
|
|
mPrimitiveMutex.unlock();
|
|
return lcUnexpected(tr("Library loading was cancelled."));
|
|
}
|
|
|
|
if (Primitive->mState == lcPrimitiveState::NotLoaded)
|
|
Primitive->mState = lcPrimitiveState::Loading;
|
|
else
|
|
{
|
|
while (Primitive->mState == lcPrimitiveState::Loading)
|
|
{
|
|
if (ShouldCancelLoading())
|
|
{
|
|
mPrimitiveMutex.unlock();
|
|
return lcUnexpected(tr("Library loading was cancelled."));
|
|
}
|
|
|
|
mPrimitiveLoaded.wait(&mPrimitiveMutex, 10);
|
|
}
|
|
|
|
const bool Loaded = Primitive->mState == lcPrimitiveState::Loaded;
|
|
const QString Error = Primitive->mLoadError;
|
|
mPrimitiveMutex.unlock();
|
|
|
|
if (Loaded)
|
|
return lcResult<void>();
|
|
|
|
return lcUnexpected(Error);
|
|
}
|
|
|
|
mPrimitiveMutex.unlock();
|
|
|
|
lcMeshLoader MeshLoader(Primitive->mMeshData, nullptr, lcMeshLoaderFlag::Optimize | lcMeshLoaderFlag::RequireAllIncludes);
|
|
|
|
const auto LoadFailed = [this, Primitive, &MeshLoader]() -> lcResult<void>
|
|
{
|
|
Primitive->mMeshData.Clear();
|
|
QString Error = MeshLoader.GetError();
|
|
|
|
if (Error.isEmpty())
|
|
Error = tr("Could not load included file %1.").arg(QString::fromLatin1(Primitive->mName));
|
|
|
|
QMutexLocker LoadLock(&mPrimitiveMutex);
|
|
Primitive->mLoadError = Error;
|
|
Primitive->mState = lcPrimitiveState::NotLoaded;
|
|
mPrimitiveLoaded.wakeAll();
|
|
return lcUnexpected(std::move(Error));
|
|
};
|
|
|
|
if (mZipFiles[static_cast<int>(lcZipFileType::Official)])
|
|
{
|
|
lcLibraryPrimitive* LowPrimitive = nullptr;
|
|
|
|
lcMemFile PrimFile;
|
|
|
|
if (Primitive->mStud && !Primitive->mStudStyle)
|
|
{
|
|
if (strncmp(Primitive->mName, "8/", 2)) // todo: this is currently the only place that uses mName so use mFileName instead. this should also be done for the loose file libraries.
|
|
{
|
|
char Name[LC_PIECE_NAME_LEN];
|
|
lcstrcpy(Name, "8/");
|
|
lcstrcat(Name, Primitive->mName);
|
|
lcstrupr(Name);
|
|
|
|
LowPrimitive = FindPrimitive(Name); // todo: low primitives don't work with studlogo, because the low stud gets added as shared
|
|
}
|
|
}
|
|
|
|
if (!mZipFiles[static_cast<int>(Primitive->mZipFileType)]->ExtractFile(Primitive->mZipFileIndex, PrimFile))
|
|
return LoadFailed();
|
|
|
|
if (!LowPrimitive)
|
|
{
|
|
if (!MeshLoader.LoadMesh(PrimFile, LC_MESHDATA_SHARED))
|
|
return LoadFailed();
|
|
}
|
|
else
|
|
{
|
|
if (!MeshLoader.LoadMesh(PrimFile, LC_MESHDATA_HIGH))
|
|
return LoadFailed();
|
|
|
|
if (!mZipFiles[static_cast<int>(LowPrimitive->mZipFileType)]->ExtractFile(LowPrimitive->mZipFileIndex, PrimFile))
|
|
return LoadFailed();
|
|
|
|
if (!MeshLoader.LoadMesh(PrimFile, LC_MESHDATA_LOW))
|
|
return LoadFailed();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (Primitive->mZipFileType == lcZipFileType::Count)
|
|
{
|
|
lcDiskFile PrimFile(Primitive->mFileName);
|
|
|
|
if (!PrimFile.Open(QIODevice::ReadOnly) || !MeshLoader.LoadMesh(PrimFile, LC_MESHDATA_SHARED)) // todo: LOD like the zip files
|
|
return LoadFailed();
|
|
}
|
|
else
|
|
{
|
|
lcMemFile PrimFile;
|
|
|
|
if (!mZipFiles[static_cast<int>(Primitive->mZipFileType)]->ExtractFile(Primitive->mZipFileIndex, PrimFile))
|
|
return LoadFailed();
|
|
|
|
if (!MeshLoader.LoadMesh(PrimFile, LC_MESHDATA_SHARED))
|
|
return LoadFailed();
|
|
}
|
|
}
|
|
|
|
mPrimitiveMutex.lock();
|
|
Primitive->mLoadError.clear();
|
|
Primitive->mState = lcPrimitiveState::Loaded;
|
|
mPrimitiveLoaded.wakeAll();
|
|
mPrimitiveMutex.unlock();
|
|
|
|
return lcResult<void>();
|
|
}
|
|
|
|
bool lcPiecesLibrary::PieceDataInCategory(lcPieceData* PieceData, const char* CategoryKeywords) const
|
|
{
|
|
if (!PieceData->IsLibraryPiece())
|
|
return false;
|
|
|
|
const char* PieceName;
|
|
if (PieceData->m_strDescription[0] == '~' || PieceData->m_strDescription[0] == '_')
|
|
PieceName = PieceData->m_strDescription + 1;
|
|
else
|
|
PieceName = PieceData->m_strDescription;
|
|
|
|
return lcMatchCategory(PieceName, CategoryKeywords);
|
|
}
|
|
|
|
void lcPiecesLibrary::GetCategoryEntries(int CategoryIndex, bool GroupPieces, std::vector<lcPieceData*>& SinglePieces, std::vector<lcPieceData*>& GroupedPieces)
|
|
{
|
|
if (CategoryIndex >= 0 && CategoryIndex < static_cast<int>(gCategories.size()))
|
|
GetCategoryEntries(gCategories[CategoryIndex].Keywords.constData(), GroupPieces, SinglePieces, GroupedPieces);
|
|
}
|
|
|
|
void lcPiecesLibrary::GetCategoryEntries(const char* CategoryKeywords, bool GroupPieces, std::vector<lcPieceData*>& SinglePieces, std::vector<lcPieceData*>& GroupedPieces)
|
|
{
|
|
SinglePieces.clear();
|
|
GroupedPieces.clear();
|
|
|
|
for (const auto& PieceIt : mPieces)
|
|
{
|
|
lcPieceData* PieceData = PieceIt.second;
|
|
|
|
if (!PieceDataInCategory(PieceData, CategoryKeywords))
|
|
continue;
|
|
|
|
if (!GroupPieces)
|
|
{
|
|
SinglePieces.emplace_back(PieceData);
|
|
continue;
|
|
}
|
|
|
|
// Check if it's a patterned piece.
|
|
if (PieceData->IsPatterned())
|
|
{
|
|
lcPieceData* Parent;
|
|
|
|
// Find the parent of this patterned piece.
|
|
char ParentName[LC_PIECE_NAME_LEN];
|
|
lcstrcpy(ParentName, PieceData->mFileName);
|
|
*strchr(ParentName, 'P') = '\0';
|
|
lcstrcat(ParentName, ".dat");
|
|
|
|
Parent = FindPieceData(ParentName, nullptr);
|
|
|
|
if (Parent)
|
|
{
|
|
// Check if the parent was added as a single piece.
|
|
auto ParentIt = std::find(SinglePieces.begin(), SinglePieces.end(), Parent);
|
|
|
|
if (ParentIt != SinglePieces.end())
|
|
SinglePieces.erase(ParentIt);
|
|
|
|
if (std::find(GroupedPieces.begin(), GroupedPieces.end(), Parent) == GroupedPieces.end())
|
|
GroupedPieces.emplace_back(Parent);
|
|
}
|
|
else
|
|
{
|
|
// Patterned pieces should have a parent but in case they don't just add them anyway.
|
|
SinglePieces.emplace_back(PieceData);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Check if this piece has already been added to this category by one of its children.
|
|
if (std::find(GroupedPieces.begin(), GroupedPieces.end(), PieceData) == GroupedPieces.end())
|
|
SinglePieces.emplace_back(PieceData);
|
|
}
|
|
}
|
|
}
|
|
|
|
void lcPiecesLibrary::GetParts(std::vector<lcPieceData*>& Parts) const
|
|
{
|
|
Parts.clear();
|
|
Parts.reserve(mPieces.size());
|
|
|
|
for (const auto& PartIt : mPieces)
|
|
Parts.emplace_back(PartIt.second);
|
|
}
|
|
|
|
std::vector<lcPieceData*> lcPiecesLibrary::GetVisibleTrainTrackParts(const lcTrainTrackConnectionType& ConnectionType) const
|
|
{
|
|
std::vector<lcPieceData*> Parts;
|
|
|
|
for (const auto& [Name, PieceData] : mPieces)
|
|
{
|
|
lcTrainTrackInfo* TrainTrackInfo = PieceData->GetTrainTrackInfo();
|
|
|
|
if (TrainTrackInfo && TrainTrackInfo->IsVisible() && TrainTrackInfo->CanConnectTo(ConnectionType, true))
|
|
Parts.emplace_back(PieceData);
|
|
}
|
|
|
|
return Parts;
|
|
}
|
|
|
|
std::vector<lcPieceData*> lcPiecesLibrary::GetPartsFromSet(const std::vector<std::string>& PartIds) const
|
|
{
|
|
std::vector<lcPieceData*> Parts;
|
|
Parts.reserve(PartIds.size());
|
|
|
|
for (const std::string& PartId : PartIds)
|
|
{
|
|
std::map<std::string, lcPieceData*>::const_iterator PartIt = mPieces.find(PartId);
|
|
|
|
if (PartIt != mPieces.end())
|
|
Parts.push_back(PartIt->second);
|
|
}
|
|
|
|
return Parts;
|
|
}
|
|
|
|
std::string lcPiecesLibrary::GetPartId(const lcPieceData* PieceData) const
|
|
{
|
|
std::map<std::string, lcPieceData*>::const_iterator PartIt = std::find_if(mPieces.begin(), mPieces.end(), [PieceData](const std::pair<std::string, lcPieceData*>& PartIt)
|
|
{
|
|
return PartIt.second == PieceData;
|
|
});
|
|
|
|
if (PartIt != mPieces.end())
|
|
return PartIt->first;
|
|
else
|
|
return std::string();
|
|
}
|
|
|
|
bool lcPiecesLibrary::LoadBuiltinPieceLibrary()
|
|
{
|
|
std::unique_ptr<lcDiskFile> File(new lcDiskFile(":/resources/library.zip"));
|
|
|
|
if (!File->Open(QIODevice::ReadOnly) || !OpenArchive(std::move(File), lcZipFileType::Official))
|
|
return false;
|
|
|
|
lcMemFile PieceFile;
|
|
|
|
for (const auto& PieceIt : mPieces)
|
|
{
|
|
lcPieceData* PieceData = PieceIt.second;
|
|
|
|
mZipFiles[static_cast<int>(PieceData->mZipFileType)]->ExtractFile(PieceData->mZipFileIndex, PieceFile, 256);
|
|
PieceFile.Seek(0, SEEK_END);
|
|
PieceFile.WriteU8(0);
|
|
|
|
char* Src = (char*)PieceFile.mBuffer + 2;
|
|
char* Dst = PieceData->m_strDescription;
|
|
|
|
for (;;)
|
|
{
|
|
if (*Src != '\r' && *Src != '\n' && *Src && Dst - PieceData->m_strDescription < (int)sizeof(PieceData->m_strDescription) - 1)
|
|
{
|
|
*Dst++ = *Src++;
|
|
continue;
|
|
}
|
|
|
|
*Dst = 0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
lcLoadDefaultColors(mStudStyle);
|
|
UpdateLoadingMeshColors();
|
|
UpdateStudStyleSource();
|
|
lcLoadDefaultCategories(true);
|
|
lcSynthInit();
|
|
lcTrainTrackInfo::Initialize(this);
|
|
|
|
return true;
|
|
}
|