mirror of
https://github.com/leozide/leocad.git
synced 2026-10-27 06:20:55 +00:00
External projects were loaded before their identities were registered, allowing mutually referencing files to recurse until stack exhaustion. Track canonical paths during nested loads and reject repeated paths with a failed-piece error. Primitive dependencies could wait indefinitely on themselves or form cycles across workers. Track active dependency edges, reject cycles, and replace polling with cancellation-aware condition waits. Signal cancellation during queue drains and restore its previous state afterward. Validation: full debug build and whitespace checks passed. A temporary regression harness covered external and primitive cycles, 100 concurrent cyclic-load attempts, cancellation, completion, cache hits, and queue shutdown.
2732 lines
69 KiB
C++
2732 lines
69 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 "pieceinf.h"
|
|
#include "lc_colors.h"
|
|
#include "lc_texture.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()
|
|
{
|
|
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;
|
|
mStudStyle = static_cast<lcStudStyle>(lcGetProfileInt(LC_PROFILE_STUD_STYLE));
|
|
mStudCylinderColorEnabled = lcGetProfileInt(LC_PROFILE_STUD_CYLINDER_COLOR_ENABLED);
|
|
}
|
|
|
|
lcPiecesLibrary::~lcPiecesLibrary()
|
|
{
|
|
mThumbnailManager.reset();
|
|
mCancelLoading.store(true);
|
|
mAssetLoader->CancelAndDrain();
|
|
mAssetLoader.reset();
|
|
Unload();
|
|
ReleaseBuffers();
|
|
}
|
|
|
|
void lcPiecesLibrary::Unload()
|
|
{
|
|
const bool HadLoader = static_cast<bool>(mAssetLoader);
|
|
|
|
if (HadLoader && mThumbnailManager)
|
|
mThumbnailManager->Clear();
|
|
|
|
if (mAssetLoader)
|
|
{
|
|
mAssetLoader->CancelAndDrain();
|
|
mAssetLoader.reset();
|
|
}
|
|
|
|
mPieceErrorMutex.lock();
|
|
mFailedPartErrors.clear();
|
|
mPieceErrorMutex.unlock();
|
|
|
|
for (const auto& PieceIt : mPieces)
|
|
delete PieceIt.second;
|
|
|
|
mPieces.clear();
|
|
|
|
while (!mProjectPieces.empty())
|
|
{
|
|
PieceInfo* Info = mProjectPieces.begin()->first;
|
|
Project* Owner = mProjectPieces.begin()->second;
|
|
|
|
if (Owner)
|
|
Owner->UnregisterPiece(Info);
|
|
|
|
mProjectPieces.erase(Info);
|
|
|
|
const bool DeleteAfterUnload = !Info->IsModel() && !Info->IsProject();
|
|
Info->Unload();
|
|
|
|
if (DeleteAfterUnload)
|
|
delete Info;
|
|
}
|
|
|
|
mSources.clear();
|
|
|
|
for (lcTexture* Texture : mTextures)
|
|
delete Texture;
|
|
|
|
mTextures.clear();
|
|
|
|
mNumOfficialPieces = 0;
|
|
|
|
for (std::unique_ptr<lcZipFile>& ZipFile : mZipFiles)
|
|
ZipFile.reset();
|
|
|
|
if (HadLoader)
|
|
mAssetLoader = std::unique_ptr<lcAssetLoader>(new lcAssetLoader(this));
|
|
}
|
|
|
|
void lcPiecesLibrary::RemoveTemporaryPieces()
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
|
|
for (auto PieceIt = mPieces.begin(); PieceIt != mPieces.end();)
|
|
{
|
|
PieceInfo* Info = PieceIt->second;
|
|
|
|
if (!Info->IsLibraryPiece() && Info->GetRefCount() == 0)
|
|
{
|
|
ClearPieceLoadError(Info);
|
|
PieceIt = mPieces.erase(PieceIt);
|
|
delete Info;
|
|
}
|
|
else
|
|
PieceIt++;
|
|
}
|
|
|
|
std::vector<PieceInfo*> Unused;
|
|
|
|
for (const std::unordered_map<PieceInfo*, Project*>::value_type& Entry : mProjectPieces)
|
|
{
|
|
if (Entry.first->GetRefCount() == 0)
|
|
Unused.push_back(Entry.first);
|
|
}
|
|
|
|
for (PieceInfo* Info : Unused)
|
|
UnloadPieceInfo(Info);
|
|
}
|
|
|
|
void lcPiecesLibrary::ReleaseProjectPieces(Project* OwnerProject)
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
std::vector<PieceInfo*> Unused;
|
|
|
|
OwnerProject->ClearPieceIndex();
|
|
|
|
for (std::unordered_map<PieceInfo*, Project*>::value_type& Entry : mProjectPieces)
|
|
{
|
|
if (Entry.second != OwnerProject)
|
|
continue;
|
|
|
|
Entry.second = nullptr;
|
|
|
|
if (Entry.first->GetRefCount() == 0)
|
|
Unused.push_back(Entry.first);
|
|
}
|
|
|
|
for (PieceInfo* Info : Unused)
|
|
UnloadPieceInfo(Info);
|
|
}
|
|
|
|
void lcPiecesLibrary::TransferProjectPieces(Project* Source, Project* Destination)
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
std::vector<PieceInfo*> Unused;
|
|
|
|
Source->TransferPieceIndexTo(Destination);
|
|
|
|
for (std::unordered_map<PieceInfo*, Project*>::value_type& Entry : mProjectPieces)
|
|
{
|
|
if (Entry.second == Source)
|
|
{
|
|
Entry.second = Destination;
|
|
|
|
if (Entry.first->GetRefCount() == 0)
|
|
Unused.push_back(Entry.first);
|
|
}
|
|
}
|
|
|
|
for (PieceInfo* Info : Unused)
|
|
UnloadPieceInfo(Info);
|
|
}
|
|
|
|
void lcPiecesLibrary::RegisterProjectPiece(Project* Project, const std::string& Name, PieceInfo* Info)
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
Project->RegisterPiece(Name, Info);
|
|
mProjectPieces[Info] = Project;
|
|
}
|
|
|
|
void lcPiecesLibrary::RemovePiece(PieceInfo* Info)
|
|
{
|
|
ClearPieceLoadError(Info);
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
|
|
const std::unordered_map<PieceInfo*, Project*>::iterator Local = mProjectPieces.find(Info);
|
|
const bool IsLocal = Local != mProjectPieces.end();
|
|
|
|
if (IsLocal)
|
|
{
|
|
if (Local->second)
|
|
Local->second->UnregisterPiece(Info);
|
|
|
|
mProjectPieces.erase(Local);
|
|
}
|
|
|
|
if (!IsLocal)
|
|
{
|
|
for (std::map<std::string, PieceInfo*>::iterator PieceIt = mPieces.begin(); PieceIt != mPieces.end(); PieceIt++)
|
|
{
|
|
if (PieceIt->second == Info)
|
|
{
|
|
mPieces.erase(PieceIt);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
delete Info;
|
|
}
|
|
|
|
void lcPiecesLibrary::UnloadPieceInfo(PieceInfo* Info)
|
|
{
|
|
const std::unordered_map<PieceInfo*, Project*>::iterator Local = mProjectPieces.find(Info);
|
|
const bool DeleteAfterUnload = !Info->IsLibraryPiece() && !Info->IsModel() && !Info->IsProject();
|
|
|
|
if (Local != mProjectPieces.end())
|
|
{
|
|
if (Local->second)
|
|
Local->second->UnregisterPiece(Info);
|
|
|
|
mProjectPieces.erase(Local);
|
|
}
|
|
|
|
ClearPieceLoadError(Info);
|
|
Info->Unload();
|
|
|
|
if (DeleteAfterUnload)
|
|
RemovePiece(Info);
|
|
}
|
|
|
|
void lcPiecesLibrary::SetModelPieceName(PieceInfo* Info, const char* Name)
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
|
|
strncpy(Info->mFileName, Name, sizeof(Info->mFileName) - 1);
|
|
Info->mFileName[sizeof(Info->mFileName) - 1] = 0;
|
|
strncpy(Info->m_strDescription, Name, sizeof(Info->m_strDescription) - 1);
|
|
Info->m_strDescription[sizeof(Info->m_strDescription) - 1] = 0;
|
|
}
|
|
|
|
bool lcPiecesLibrary::RenamePiece(PieceInfo* Info, const char* NewName)
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
|
|
const std::unordered_map<PieceInfo*, Project*>::iterator Local = mProjectPieces.find(Info);
|
|
const std::string NewPieceName = NormalizePieceName(NewName);
|
|
|
|
if (Local != mProjectPieces.end() && Local->second)
|
|
{
|
|
PieceInfo* Existing = Local->second->FindPiece(NewPieceName);
|
|
|
|
if (Existing && Existing != Info)
|
|
return false;
|
|
}
|
|
else if (Local == mProjectPieces.end())
|
|
{
|
|
const std::map<std::string, PieceInfo*>::const_iterator Existing = mPieces.find(NewPieceName);
|
|
|
|
if (Existing != mPieces.end() && Existing->second != Info)
|
|
return false;
|
|
}
|
|
|
|
if (Local != mProjectPieces.end() && Local->second)
|
|
Local->second->UnregisterPiece(Info);
|
|
|
|
if (Local == mProjectPieces.end())
|
|
{
|
|
for (std::map<std::string, PieceInfo*>::iterator PieceIt = mPieces.begin(); PieceIt != mPieces.end(); PieceIt++)
|
|
{
|
|
if (PieceIt->second == Info)
|
|
{
|
|
mPieces.erase(PieceIt);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
strncpy(Info->mFileName, NewName, sizeof(Info->mFileName));
|
|
Info->mFileName[sizeof(Info->mFileName) - 1] = 0;
|
|
strncpy(Info->m_strDescription, NewName, sizeof(Info->m_strDescription));
|
|
Info->m_strDescription[sizeof(Info->m_strDescription) - 1] = 0;
|
|
|
|
const std::string PieceName = NormalizePieceName(Info->mFileName);
|
|
|
|
if (Local != mProjectPieces.end())
|
|
{
|
|
if (Local->second)
|
|
Local->second->RegisterPiece(PieceName, Info);
|
|
}
|
|
else
|
|
mPieces[PieceName] = Info;
|
|
|
|
LoadLock.unlock();
|
|
if (Info->mState != lcPieceInfoState::Failed)
|
|
ClearPieceLoadError(Info);
|
|
|
|
return true;
|
|
}
|
|
|
|
std::string lcPiecesLibrary::NormalizePieceName(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;
|
|
}
|
|
|
|
PieceInfo* lcPiecesLibrary::CreateModelPiece(const char* PieceName, Project* Project, bool& Reused)
|
|
{
|
|
const std::string Name = NormalizePieceName(PieceName);
|
|
PieceInfo* Info;
|
|
Reused = false;
|
|
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
|
|
Info = Project->FindPiece(Name);
|
|
|
|
if (!Info || Info->IsModel())
|
|
{
|
|
Info = new PieceInfo(false);
|
|
Info->CreatePart(PieceName);
|
|
Project->RegisterPiece(Name, Info);
|
|
mProjectPieces[Info] = Project;
|
|
}
|
|
else
|
|
Reused = true;
|
|
|
|
// Keep a local piece alive while a pending load finishes and the model takes its reference.
|
|
Info->AddRef();
|
|
}
|
|
|
|
if (Reused)
|
|
mAssetLoader->InvalidatePiece(Info);
|
|
|
|
return Info;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
PieceInfo* lcPiecesLibrary::FindPiece(const char* PieceName, Project* CurrentProject, bool CreateMissing, bool SearchProjectFolder)
|
|
{
|
|
const std::string Name = NormalizePieceName(PieceName);
|
|
QString ProjectPath;
|
|
QString LoadError;
|
|
|
|
if (CurrentProject)
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
|
|
if (PieceInfo* Info = CurrentProject->FindPiece(Name))
|
|
return Info;
|
|
|
|
if (SearchProjectFolder && !CurrentProject->GetFileName().isEmpty())
|
|
ProjectPath = QFileInfo(CurrentProject->GetFileName()).absolutePath();
|
|
}
|
|
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
const std::map<std::string, PieceInfo*>::const_iterator PieceIt = mPieces.find(Name);
|
|
|
|
if (PieceIt != mPieces.end())
|
|
return PieceIt->second;
|
|
}
|
|
|
|
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)
|
|
{
|
|
PieceInfo* Info = new PieceInfo(false);
|
|
|
|
Info->CreateProject(ExternalProject.value(), PieceName);
|
|
RegisterProjectPiece(CurrentProject, Name, Info);
|
|
|
|
return Info;
|
|
}
|
|
else
|
|
LoadError = ExternalProject.error();
|
|
}
|
|
}
|
|
|
|
if (CreateMissing)
|
|
{
|
|
PieceInfo* Info = new PieceInfo(false);
|
|
|
|
Info->CreatePart(PieceName);
|
|
Info->SetFailed(LoadError.isEmpty() ? tr("Could not find piece '%1'.").arg(QString::fromLatin1(PieceName)) : LoadError);
|
|
|
|
if (CurrentProject)
|
|
RegisterProjectPiece(CurrentProject, Name, Info);
|
|
else
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
mPieces[Name] = Info;
|
|
}
|
|
|
|
return Info;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
bool lcPiecesLibrary::RemapProjectPiece(PieceInfo* Info, const QString& ProjectDirectory, bool IsPreview)
|
|
{
|
|
const QByteArray PieceName(Info->mFileName);
|
|
const QFileInfo Candidate(QDir(ProjectDirectory).absoluteFilePath(QString::fromLatin1(PieceName.constData())));
|
|
|
|
if (Info->IsProject() && Candidate.isFile() &&
|
|
QFileInfo(Info->GetProject()->GetFileName()).canonicalFilePath() == Candidate.canonicalFilePath())
|
|
return false;
|
|
|
|
if (!Info->IsProject() && !Candidate.isFile())
|
|
return false;
|
|
|
|
InvalidatePiece(Info);
|
|
|
|
if (Candidate.isFile())
|
|
{
|
|
const lcResult<Project*> ExternalProject = LoadExternalProject(Candidate, IsPreview, true);
|
|
|
|
if (ExternalProject)
|
|
Info->CreateProject(ExternalProject.value(), PieceName.constData());
|
|
else
|
|
{
|
|
Info->DetachContainer();
|
|
Info->SetFailed(ExternalProject.error());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Info->DetachContainer();
|
|
Info->SetFailed(tr("Could not find external project '%1'.").arg(Candidate.absoluteFilePath()));
|
|
}
|
|
|
|
NotifyConsumersChanged();
|
|
|
|
return true;
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
lcTexture* Texture = FindTextureDeferred(TextureName, std::vector<QString>{ ProjectPath });
|
|
|
|
if (!Texture)
|
|
return nullptr;
|
|
|
|
if (EnsureTextureReady(Texture))
|
|
return Texture;
|
|
|
|
ReleaseTexture(Texture);
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
lcTexture* lcPiecesLibrary::FindTextureDeferred(const char* TextureName, const std::vector<QString>& SearchDirectories)
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
|
|
QString FilePath;
|
|
QString ResolvedProjectPath;
|
|
|
|
for (const QString& Directory : SearchDirectories)
|
|
{
|
|
if (Directory.isEmpty())
|
|
continue;
|
|
|
|
FilePath = FindProjectTextureFile(Directory, QString::fromLatin1(TextureName));
|
|
|
|
if (!FilePath.isEmpty())
|
|
{
|
|
ResolvedProjectPath = Directory;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!FilePath.isEmpty())
|
|
{
|
|
const QString CanonicalPath = QFileInfo(FilePath).canonicalFilePath();
|
|
FilePath = CanonicalPath.isEmpty() ? QFileInfo(FilePath).absoluteFilePath() : CanonicalPath;
|
|
}
|
|
|
|
for (lcTexture* Texture : mTextures)
|
|
{
|
|
if (strcmp(TextureName, Texture->mName))
|
|
continue;
|
|
|
|
if (Texture->IsTemporary() != !FilePath.isEmpty())
|
|
continue;
|
|
|
|
if (Texture->IsTemporary() && Texture->mFilePath != FilePath)
|
|
continue;
|
|
|
|
Texture->AddRef();
|
|
|
|
return Texture;
|
|
}
|
|
|
|
if (FilePath.isEmpty())
|
|
return nullptr;
|
|
|
|
lcTexture* Texture = new lcTexture(LC_TEXTURE_MIPMAPS);
|
|
|
|
lcstrcpy(Texture->mName, TextureName);
|
|
Texture->mFilePath = FilePath;
|
|
Texture->mProjectPath = ResolvedProjectPath;
|
|
Texture->SetTemporary(true);
|
|
Texture->AddRef();
|
|
|
|
mTextures.push_back(Texture);
|
|
|
|
return Texture;
|
|
}
|
|
|
|
lcTextureSourceSnapshot lcPiecesLibrary::SnapshotTextureSource(const lcTexture* Texture) const
|
|
{
|
|
lcTextureSourceSnapshot Source;
|
|
|
|
Source.Name = QString::fromLatin1(Texture->mName);
|
|
Source.FilePath = Texture->mFilePath;
|
|
|
|
return Source;
|
|
}
|
|
|
|
lcTextureBuildResult lcPiecesLibrary::BuildTextureData(const lcTextureSourceSnapshot& Source)
|
|
{
|
|
lcTextureBuildResult Result;
|
|
|
|
Result.DecodedImage.reset(new Image);
|
|
|
|
bool Loaded = false;
|
|
|
|
if (!Source.FilePath.isEmpty())
|
|
Loaded = Result.DecodedImage->FileLoad(Source.FilePath);
|
|
else
|
|
{
|
|
lcMemFile TextureFile;
|
|
QByteArray Name = Source.Name.toLatin1();
|
|
char FileName[2 * LC_MAXPATH];
|
|
bool Extracted = false;
|
|
|
|
{
|
|
QMutexLocker TextureLock(&mTextureMutex);
|
|
|
|
if (mZipFiles[static_cast<int>(lcZipFileType::Official)])
|
|
{
|
|
snprintf(FileName, sizeof(FileName), "ldraw/parts/textures/%s.png", Name.constData());
|
|
|
|
Extracted = mZipFiles[static_cast<int>(lcZipFileType::Official)]->ExtractFile(FileName, TextureFile);
|
|
|
|
if (!Extracted && mZipFiles[static_cast<int>(lcZipFileType::Unofficial)])
|
|
{
|
|
snprintf(FileName, sizeof(FileName), "parts/textures/%s.png", Name.constData());
|
|
Extracted = mZipFiles[static_cast<int>(lcZipFileType::Unofficial)]->ExtractFile(FileName, TextureFile);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Extracted)
|
|
Loaded = Result.DecodedImage->FileLoad(TextureFile);
|
|
}
|
|
|
|
if (Loaded)
|
|
Result.DecodedImage->ResizePow2();
|
|
else
|
|
{
|
|
Result.DecodedImage.reset();
|
|
Result.Error = lcTextureLoadError::DecodeFailed;
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
|
|
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;
|
|
|
|
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 && !memcmp(Name, "LDRAW/PARTS/TEXTURES/", 21)) ||
|
|
(ZipFileType == lcZipFileType::Unofficial && !memcmp(Name, "PARTS/TEXTURES/", 15)))
|
|
{
|
|
lcTexture* Texture = new lcTexture(LC_TEXTURE_MIPMAPS);
|
|
mTextures.push_back(Texture);
|
|
|
|
*Dst = 0;
|
|
strncpy(Texture->mName, Name + (ZipFileType == lcZipFileType::Official ? 21 : 15), sizeof(Texture->mName)-1);
|
|
Texture->mName[sizeof(Texture->mName) - 1] = 0;
|
|
}
|
|
}
|
|
|
|
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))
|
|
{
|
|
PieceInfo* Info = FindPiece(Name, nullptr, false, false);
|
|
|
|
if (!Info)
|
|
{
|
|
Info = new PieceInfo(true);
|
|
|
|
strncpy(Info->mFileName, FileInfo.file_name + (Name - NameBuffer), sizeof(Info->mFileName)-1);
|
|
Info->mFileName[sizeof(Info->mFileName) - 1] = 0;
|
|
|
|
mPieces[Name] = Info;
|
|
}
|
|
|
|
Info->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)
|
|
{
|
|
PieceInfo* Info = PieceIt.second;
|
|
|
|
mZipFiles[static_cast<int>(Info->mZipFileType)]->ExtractFile(Info->mZipFileIndex, PieceFile, 256);
|
|
PieceFile.Seek(0, SEEK_END);
|
|
PieceFile.WriteU8(0);
|
|
|
|
char* Src = (char*)PieceFile.mBuffer + 2;
|
|
char* Dst = Info->m_strDescription;
|
|
|
|
for (;;)
|
|
{
|
|
if (*Src != '\r' && *Src != '\n' && *Src && Dst - Info->m_strDescription < (int)sizeof(Info->m_strDescription) - 1)
|
|
{
|
|
*Dst++ = *Src++;
|
|
continue;
|
|
}
|
|
|
|
*Dst = 0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
SaveArchiveCacheIndex(IndexFileName);
|
|
}
|
|
}
|
|
|
|
bool lcPiecesLibrary::OpenDirectory(const QDir& LibraryDir, bool ShowProgress)
|
|
{
|
|
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));
|
|
}
|
|
|
|
for (unsigned int BaseFolderIdx = 0; BaseFolderIdx < LC_ARRAY_COUNT(BaseFolders); BaseFolderIdx++)
|
|
{
|
|
QDir BaseDir(LibraryDir.absoluteFilePath(QLatin1String(BaseFolders[BaseFolderIdx])));
|
|
QDir Dir(BaseDir.absoluteFilePath(QLatin1String("parts/textures/")), QLatin1String("*.png"), QDir::SortFlags(QDir::Name | QDir::IgnoreCase), QDir::Files | QDir::Hidden | QDir::Readable);
|
|
QStringList FileList = Dir.entryList();
|
|
|
|
mTextures.reserve(mTextures.size() + FileList.size());
|
|
|
|
for (int FileIdx = 0; FileIdx < FileList.size(); FileIdx++)
|
|
{
|
|
char Name[LC_MAXPATH];
|
|
QByteArray FileString = FileList[FileIdx].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++;
|
|
}
|
|
|
|
if (Dst - Name <= 4)
|
|
continue;
|
|
|
|
Dst -= 4;
|
|
if (memcmp(Dst, ".PNG", 4))
|
|
continue;
|
|
*Dst = 0;
|
|
|
|
lcTexture* Texture = new lcTexture(LC_TEXTURE_MIPMAPS);
|
|
mTextures.push_back(Texture);
|
|
|
|
strncpy(Texture->mName, Name, sizeof(Texture->mName));
|
|
Texture->mName[sizeof(Texture->mName) - 1] = 0;
|
|
Texture->mFilePath = Dir.absoluteFilePath(FileList[FileIdx]);
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
PieceInfo* Info = new PieceInfo(true);
|
|
|
|
strncpy(Info->mFileName, FileString, sizeof(Info->mFileName));
|
|
Info->mFileName[sizeof(Info->mFileName) - 1] = 0;
|
|
Info->mFolderType = FolderIdx;
|
|
Info->mFolderIndex = FileIdx;
|
|
|
|
mPieces[Name] = Info;
|
|
}
|
|
}
|
|
|
|
QAtomicInt FilesLoaded;
|
|
bool Modified = false;
|
|
|
|
auto ReadDescriptions = [&FileLists, &CachedDescriptions, &FilesLoaded, &Modified](const std::pair<std::string, PieceInfo*>& Entry)
|
|
{
|
|
PieceInfo* Info = Entry.second;
|
|
FilesLoaded.ref();
|
|
|
|
lcDiskFile PieceFile(FileLists[Info->mFolderType][Info->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(Info->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[Info->mFolderType][Info->mFolderIndex].lastModified().toMSecsSinceEpoch();
|
|
|
|
if (FileTime == CachedFileTime)
|
|
{
|
|
lcstrcpy(Info->m_strDescription, Description);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!PieceFile.Open(QIODevice::ReadOnly) || !PieceFile.ReadLine(Line, sizeof(Line)))
|
|
{
|
|
lcstrcpy(Info->m_strDescription, "Unknown");
|
|
return;
|
|
}
|
|
|
|
const char* Src = Line + 2;
|
|
char* Dst = Info->m_strDescription;
|
|
|
|
for (;;)
|
|
{
|
|
if (*Src != '\r' && *Src != '\n' && *Src && Dst - Info->m_strDescription < (int)sizeof(Info->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<PieceInfo*> SortedPieces;
|
|
SortedPieces.reserve(mPieces.size());
|
|
for (const auto& PieceIt : mPieces)
|
|
SortedPieces.push_back(PieceIt.second);
|
|
|
|
auto PieceInfoCompare = [](PieceInfo* Info1, PieceInfo* Info2)
|
|
{
|
|
return strcmp(Info1->mFileName, Info2->mFileName) < 0;
|
|
};
|
|
|
|
std::sort(SortedPieces.begin(), SortedPieces.end(), PieceInfoCompare);
|
|
|
|
for (const PieceInfo* Info : SortedPieces)
|
|
{
|
|
if (NewIndexFile.WriteBuffer(Info->mFileName, strlen(Info->mFileName) + 1) == 0)
|
|
return;
|
|
|
|
if (NewIndexFile.WriteBuffer(Info->m_strDescription, strlen(Info->m_strDescription) + 1) == 0)
|
|
return;
|
|
|
|
NewIndexFile.WriteU8(static_cast<quint8>(Info->mFolderType));
|
|
|
|
quint64 FileTime = FileLists[Info->mFolderType][Info->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)
|
|
{
|
|
PieceInfo* Info = PieceIt.second;
|
|
quint8 Length;
|
|
|
|
static_assert(sizeof(Info->m_strDescription) == 256);
|
|
|
|
if (IndexFile.ReadBuffer((char*)&Length, sizeof(Length)) == 0)
|
|
return false;
|
|
|
|
if (IndexFile.ReadBuffer((char*)Info->m_strDescription, Length) == 0)
|
|
return false;
|
|
|
|
Info->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 PieceInfo* Info = PieceIt.second;
|
|
const quint8 Length = (quint8)strlen(Info->m_strDescription);
|
|
|
|
if (IndexFile.WriteBuffer((char*)&Length, sizeof(Length)) == 0)
|
|
return false;
|
|
|
|
if (IndexFile.WriteBuffer((char*)Info->m_strDescription, Length) == 0)
|
|
return false;
|
|
}
|
|
|
|
return WriteArchiveCacheFile(FileName, IndexFile);
|
|
}
|
|
|
|
bool lcPiecesLibrary::LoadPieceInfo(PieceInfo* Info, lcPieceLoadFlags Flags)
|
|
{
|
|
return mAssetLoader->LoadPieceInfo(Info, Flags);
|
|
}
|
|
|
|
void lcPiecesLibrary::NotifyConsumersChanged()
|
|
{
|
|
emit AssetRequestsChanged();
|
|
}
|
|
|
|
bool lcPiecesLibrary::EnsurePieceReady(PieceInfo* Info)
|
|
{
|
|
return mAssetLoader->EnsurePieceReady(Info);
|
|
}
|
|
|
|
bool lcPiecesLibrary::EnsurePieceAssetsReady(PieceInfo* Info)
|
|
{
|
|
const bool PieceReady = EnsurePieceReady(Info);
|
|
|
|
if (Info->IsModel())
|
|
return static_cast<bool>(Info->GetModel()->EnsureAssetsReady()) && PieceReady;
|
|
|
|
if (Info->IsProject())
|
|
{
|
|
const bool ProjectReady = static_cast<bool>(Info->GetProject()->EnsureAssetsReady());
|
|
|
|
Info->GetProject()->UpdatePieceInfo(Info);
|
|
return ProjectReady && PieceReady;
|
|
}
|
|
|
|
return PieceReady;
|
|
}
|
|
|
|
bool lcPiecesLibrary::EnsurePiecesReady(const std::vector<PieceInfo*>& Parts)
|
|
{
|
|
return mAssetLoader->EnsurePiecesReady(Parts);
|
|
}
|
|
|
|
void lcPiecesLibrary::QueueSynthMesh(lcPiece* Piece)
|
|
{
|
|
mAssetLoader->QueueSynthMesh(Piece);
|
|
}
|
|
|
|
void lcPiecesLibrary::CancelSynthMesh(lcPiece* Piece)
|
|
{
|
|
if (mAssetLoader)
|
|
mAssetLoader->CancelSynthMesh(Piece);
|
|
}
|
|
|
|
bool lcPiecesLibrary::EnsureSynthMeshesReady(const std::vector<lcPiece*>& Pieces)
|
|
{
|
|
return mAssetLoader->EnsureSynthMeshesReady(Pieces);
|
|
}
|
|
|
|
QString lcPiecesLibrary::GetSynthMeshError(const lcPiece* Piece) const
|
|
{
|
|
return mAssetLoader ? mAssetLoader->GetSynthMeshError(Piece) : QString();
|
|
}
|
|
|
|
void lcPiecesLibrary::QueueModelPiece(PieceInfo* Info)
|
|
{
|
|
mAssetLoader->QueueModelPiece(Info);
|
|
}
|
|
|
|
void lcPiecesLibrary::InvalidatePiece(PieceInfo* Info)
|
|
{
|
|
if (mAssetLoader)
|
|
mAssetLoader->InvalidatePiece(Info);
|
|
else
|
|
{
|
|
Info->mState = lcPieceInfoState::Unloaded;
|
|
ClearPieceLoadError(Info);
|
|
}
|
|
}
|
|
|
|
bool lcPiecesLibrary::EnsureTextureReady(lcTexture* Texture)
|
|
{
|
|
return mAssetLoader->EnsureTextureReady(Texture);
|
|
}
|
|
|
|
void lcPiecesLibrary::ReleasePieceInfo(PieceInfo* Info)
|
|
{
|
|
int Remaining;
|
|
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
|
|
if (Info->GetRefCount() == 0)
|
|
return;
|
|
|
|
Remaining = Info->Release();
|
|
|
|
if (Remaining == 0)
|
|
UnloadPieceInfo(Info);
|
|
}
|
|
|
|
if (Remaining != 0 && mAssetLoader)
|
|
mAssetLoader->OnConsumerReleased(Info);
|
|
}
|
|
|
|
void lcPiecesLibrary::AddPieceReference(PieceInfo* Info)
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
|
|
Info->AddRef();
|
|
}
|
|
|
|
bool lcPiecesLibrary::HasPieceConsumers(const PieceInfo* Info)
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
|
|
return Info->GetRefCount() > 1;
|
|
}
|
|
|
|
void lcPiecesLibrary::ReleasePieceLoadHold(PieceInfo* Info)
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
|
|
if (Info->Release() == 0)
|
|
UnloadPieceInfo(Info);
|
|
}
|
|
|
|
void lcPiecesLibrary::SetPieceLoadError(const PieceInfo* Info, QString Error)
|
|
{
|
|
QMutexLocker ErrorLock(&mPieceErrorMutex);
|
|
|
|
if (Error.isEmpty())
|
|
mFailedPartErrors.erase(Info);
|
|
else
|
|
mFailedPartErrors.insert_or_assign(Info, std::move(Error));
|
|
}
|
|
|
|
QString lcPiecesLibrary::GetPieceLoadError(const PieceInfo* Info) const
|
|
{
|
|
QMutexLocker ErrorLock(&mPieceErrorMutex);
|
|
|
|
const auto It = mFailedPartErrors.find(Info);
|
|
|
|
return It == mFailedPartErrors.end() ? QString() : It->second;
|
|
}
|
|
|
|
void lcPiecesLibrary::ClearPieceLoadError(const PieceInfo* Info)
|
|
{
|
|
QMutexLocker ErrorLock(&mPieceErrorMutex);
|
|
|
|
mFailedPartErrors.erase(Info);
|
|
}
|
|
|
|
void lcPiecesLibrary::WaitForLoadQueue()
|
|
{
|
|
mAssetLoader->WaitForLoadQueue();
|
|
}
|
|
|
|
lcPartSourceSnapshot lcPiecesLibrary::SnapshotPieceSource(const PieceInfo* Info) const
|
|
{
|
|
lcPartSourceSnapshot Source;
|
|
|
|
Source.FileName = QString::fromLatin1(Info->mFileName);
|
|
Source.LibraryDirectory = mLibraryDir.absolutePath();
|
|
Source.CachePath = QFileInfo(QDir(mCachePath), Source.FileName).absoluteFilePath();
|
|
Source.ZipFileType = Info->mZipFileType;
|
|
Source.ZipFileIndex = Info->mZipFileIndex;
|
|
Source.StudStyle = mStudStyle;
|
|
Source.StudCylinderColorEnabled = mStudCylinderColorEnabled;
|
|
|
|
if (Info->IsModel())
|
|
{
|
|
Source.InlineModel = true;
|
|
Source.InlineMeshLines = Info->GetModel()->GetFileLines();
|
|
|
|
const lcModel* Model = Info->GetModel();
|
|
Source.TextureSearchDirectories = Model->GetAssetSearchDirectories();
|
|
}
|
|
|
|
return Source;
|
|
}
|
|
|
|
lcPartBuildResult lcPiecesLibrary::BuildPieceData(const lcPartSourceSnapshot& Source)
|
|
{
|
|
lcPartBuildResult Result;
|
|
|
|
if (mCancelLoading.load())
|
|
{
|
|
Result.Error = tr("Library loading was cancelled.");
|
|
return Result;
|
|
}
|
|
|
|
if (Source.InlineModel)
|
|
{
|
|
Result.MeshData.reset(new lcLibraryMeshData);
|
|
|
|
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);
|
|
|
|
lcMeshLoader MeshLoader(*Result.MeshData, nullptr, lcMeshLoaderFlag::Optimize | lcMeshLoaderFlag::RequireAllIncludes);
|
|
|
|
if (!MeshLoader.LoadMesh(PieceFile, LC_MESHDATA_SHARED))
|
|
{
|
|
Result.MeshData.reset();
|
|
Result.Error = tr("Could not load model geometry %1.").arg(Source.FileName);
|
|
|
|
if (!MeshLoader.GetError().isEmpty())
|
|
Result.Error += QLatin1Char('\n') + MeshLoader.GetError();
|
|
}
|
|
else if (Result.MeshData->IsEmpty())
|
|
{
|
|
Result.MeshData.reset();
|
|
Result.EmptyGeometry = true;
|
|
}
|
|
|
|
if (Result.MeshData)
|
|
{
|
|
Result.TextureDependencies = Result.MeshData->GetTextureDependencies();
|
|
Result.MeshData->SetMeshLoader(nullptr);
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
|
|
if (!Source.SkipCache && Source.ZipFileType != lcZipFileType::Count && mZipFiles[static_cast<int>(Source.ZipFileType)])
|
|
{
|
|
std::unique_ptr<lcMemFile> CacheData(new lcMemFile);
|
|
|
|
if (ReadArchiveCacheFile(Source.CachePath, *CacheData))
|
|
{
|
|
qint32 Flags;
|
|
|
|
if (CacheData->ReadBuffer(&Flags, sizeof(Flags)) == sizeof(Flags) && Flags == MeshCacheSettingsKey(Source.StudStyle, Source.StudCylinderColorEnabled))
|
|
{
|
|
Result.CacheData = std::move(CacheData);
|
|
|
|
return Result;
|
|
}
|
|
}
|
|
}
|
|
|
|
Result.MeshData.reset(new lcLibraryMeshData);
|
|
|
|
lcMeshLoader MeshLoader(*Result.MeshData, nullptr, lcMeshLoaderFlag::Optimize | lcMeshLoaderFlag::RequireAllIncludes);
|
|
|
|
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)
|
|
{
|
|
Result.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())
|
|
Result.MeshData.reset();
|
|
|
|
if (Result.MeshData)
|
|
{
|
|
Result.TextureDependencies = Result.MeshData->GetTextureDependencies();
|
|
Result.MeshData->SetMeshLoader(nullptr);
|
|
}
|
|
|
|
return Result;
|
|
}
|
|
|
|
void lcPiecesLibrary::SaveBuiltPieceCache(const lcPartSourceSnapshot& 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::GetPieceFile(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 PieceInfo* Info = PieceIt->second;
|
|
|
|
HasPiece = true;
|
|
PieceZipFileType = Info->mZipFileType;
|
|
PieceZipFileIndex = Info->mZipFileIndex;
|
|
PieceFileName = Info->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](PieceInfo* Info)
|
|
{
|
|
lcMesh* Mesh = Info->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, PieceInfo*>::value_type& PieceIt : mPieces)
|
|
AddMesh(PieceIt.second);
|
|
|
|
for (const std::unordered_map<PieceInfo*, 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)
|
|
{
|
|
PieceInfo* Info = PieceIt.second;
|
|
if (Info->GetRefCount() == 0 && Info->mState != lcPieceInfoState::Unloaded)
|
|
ReleasePieceInfo(Info);
|
|
}
|
|
}
|
|
|
|
bool lcPiecesLibrary::LoadTexture(lcTexture* Texture)
|
|
{
|
|
QMutexLocker Lock(&mTextureMutex);
|
|
char FileName[2*LC_MAXPATH];
|
|
|
|
if (mZipFiles[static_cast<int>(lcZipFileType::Official)])
|
|
{
|
|
lcMemFile TextureFile;
|
|
|
|
snprintf(FileName, sizeof(FileName), "ldraw/parts/textures/%s.png", Texture->mName);
|
|
|
|
if (!mZipFiles[static_cast<int>(lcZipFileType::Official)]->ExtractFile(FileName, TextureFile))
|
|
{
|
|
snprintf(FileName, sizeof(FileName), "parts/textures/%s.png", Texture->mName);
|
|
|
|
if (!mZipFiles[static_cast<int>(lcZipFileType::Unofficial)] || !mZipFiles[static_cast<int>(lcZipFileType::Unofficial)]->ExtractFile(FileName, TextureFile))
|
|
return false;
|
|
}
|
|
|
|
return Texture->Load(TextureFile);
|
|
}
|
|
else
|
|
return Texture->Load(Texture->mFilePath);
|
|
}
|
|
|
|
void lcPiecesLibrary::ReleaseTexture(lcTexture* Texture)
|
|
{
|
|
QMutexLocker LoadLock(&mLoadMutex);
|
|
|
|
if (Texture->Release() == 0 && Texture->IsTemporary())
|
|
{
|
|
std::vector<lcTexture*>::iterator TextureIt = std::find(mTextures.begin(), mTextures.end(), Texture);
|
|
if (TextureIt != mTextures.end())
|
|
mTextures.erase(TextureIt);
|
|
delete Texture;
|
|
}
|
|
}
|
|
|
|
bool lcPiecesLibrary::SupportsStudStyle() const
|
|
{
|
|
return true;
|
|
}
|
|
|
|
void lcPiecesLibrary::SetStudStyle(lcStudStyle StudStyle, bool Reload, 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();
|
|
}
|
|
}
|
|
|
|
mLoadMutex.unlock();
|
|
|
|
if (Reload)
|
|
{
|
|
mLoadMutex.lock();
|
|
|
|
const auto ReloadPiece = [this](PieceInfo* Info)
|
|
{
|
|
if (Info->mState != lcPieceInfoState::Loaded || !Info->GetMesh() || !(Info->GetMesh()->mFlags & lcMeshFlag::HasStyleStud))
|
|
return;
|
|
|
|
if (Info->IsModel())
|
|
{
|
|
lcModel* Model = Info->GetModel();
|
|
Info->SetModel(Model);
|
|
Info->ReleaseMesh();
|
|
Info->mState = lcPieceInfoState::Unloaded;
|
|
|
|
mAssetLoader->QueuePiece(Info, false);
|
|
}
|
|
else if (!Info->IsProject())
|
|
{
|
|
Info->Unload();
|
|
mAssetLoader->QueuePiece(Info, false);
|
|
}
|
|
|
|
mBuffersDirty = true;
|
|
};
|
|
|
|
for (const std::map<std::string, PieceInfo*>::value_type& PieceIt : mPieces)
|
|
ReloadPiece(PieceIt.second);
|
|
|
|
for (const std::unordered_map<PieceInfo*, Project*>::value_type& Local : mProjectPieces)
|
|
ReloadPiece(Local.first);
|
|
|
|
mLoadMutex.unlock();
|
|
mAssetLoader->ReloadSynthMeshes();
|
|
}
|
|
|
|
mAssetLoader->ResumeQueuedWork();
|
|
|
|
if (Reload)
|
|
WaitForLoadQueue();
|
|
}
|
|
|
|
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::PieceInCategory(PieceInfo* Info, const char* CategoryKeywords) const
|
|
{
|
|
if (!Info->IsLibraryPiece())
|
|
return false;
|
|
|
|
const char* PieceName;
|
|
if (Info->m_strDescription[0] == '~' || Info->m_strDescription[0] == '_')
|
|
PieceName = Info->m_strDescription + 1;
|
|
else
|
|
PieceName = Info->m_strDescription;
|
|
|
|
return lcMatchCategory(PieceName, CategoryKeywords);
|
|
}
|
|
|
|
void lcPiecesLibrary::GetCategoryEntries(int CategoryIndex, bool GroupPieces, std::vector<PieceInfo*>& SinglePieces, std::vector<PieceInfo*>& 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<PieceInfo*>& SinglePieces, std::vector<PieceInfo*>& GroupedPieces)
|
|
{
|
|
SinglePieces.clear();
|
|
GroupedPieces.clear();
|
|
|
|
for (const auto& PieceIt : mPieces)
|
|
{
|
|
PieceInfo* Info = PieceIt.second;
|
|
|
|
if (!PieceInCategory(Info, CategoryKeywords))
|
|
continue;
|
|
|
|
if (!GroupPieces)
|
|
{
|
|
SinglePieces.emplace_back(Info);
|
|
continue;
|
|
}
|
|
|
|
// Check if it's a patterned piece.
|
|
if (Info->IsPatterned())
|
|
{
|
|
PieceInfo* Parent;
|
|
|
|
// Find the parent of this patterned piece.
|
|
char ParentName[LC_PIECE_NAME_LEN];
|
|
lcstrcpy(ParentName, Info->mFileName);
|
|
*strchr(ParentName, 'P') = '\0';
|
|
lcstrcat(ParentName, ".dat");
|
|
|
|
Parent = FindPiece(ParentName, nullptr, false, false);
|
|
|
|
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(Info);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Check if this piece has already been added to this category by one of its children.
|
|
if (std::find(GroupedPieces.begin(), GroupedPieces.end(), Info) == GroupedPieces.end())
|
|
SinglePieces.emplace_back(Info);
|
|
}
|
|
}
|
|
}
|
|
|
|
void lcPiecesLibrary::GetParts(std::vector<PieceInfo*>& Parts) const
|
|
{
|
|
Parts.clear();
|
|
Parts.reserve(mPieces.size());
|
|
|
|
for (const auto& PartIt : mPieces)
|
|
Parts.emplace_back(PartIt.second);
|
|
}
|
|
|
|
std::vector<PieceInfo*> lcPiecesLibrary::GetVisibleTrainTrackParts(const lcTrainTrackConnectionType& ConnectionType) const
|
|
{
|
|
std::vector<PieceInfo*> Parts;
|
|
|
|
for (const auto& [Name, Info] : mPieces)
|
|
{
|
|
lcTrainTrackInfo* TrainTrackInfo = Info->GetTrainTrackInfo();
|
|
|
|
if (TrainTrackInfo && TrainTrackInfo->IsVisible() && TrainTrackInfo->CanConnectTo(ConnectionType, true))
|
|
Parts.emplace_back(Info);
|
|
}
|
|
|
|
return Parts;
|
|
}
|
|
|
|
std::vector<PieceInfo*> lcPiecesLibrary::GetPartsFromSet(const std::vector<std::string>& PartIds) const
|
|
{
|
|
std::vector<PieceInfo*> Parts;
|
|
Parts.reserve(PartIds.size());
|
|
|
|
for (const std::string& PartId : PartIds)
|
|
{
|
|
std::map<std::string, PieceInfo*>::const_iterator PartIt = mPieces.find(PartId);
|
|
|
|
if (PartIt != mPieces.end())
|
|
Parts.push_back(PartIt->second);
|
|
}
|
|
|
|
return Parts;
|
|
}
|
|
|
|
std::string lcPiecesLibrary::GetPartId(const PieceInfo* Info) const
|
|
{
|
|
std::map<std::string, PieceInfo*>::const_iterator PartIt = std::find_if(mPieces.begin(), mPieces.end(), [Info](const std::pair<std::string, PieceInfo*>& PartIt)
|
|
{
|
|
return PartIt.second == Info;
|
|
});
|
|
|
|
if (PartIt != mPieces.end())
|
|
return PartIt->first;
|
|
else
|
|
return std::string();
|
|
}
|
|
|
|
bool lcPiecesLibrary::LoadBuiltinPieces()
|
|
{
|
|
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)
|
|
{
|
|
PieceInfo* Info = PieceIt.second;
|
|
|
|
mZipFiles[static_cast<int>(Info->mZipFileType)]->ExtractFile(Info->mZipFileIndex, PieceFile, 256);
|
|
PieceFile.Seek(0, SEEK_END);
|
|
PieceFile.WriteU8(0);
|
|
|
|
char* Src = (char*)PieceFile.mBuffer + 2;
|
|
char* Dst = Info->m_strDescription;
|
|
|
|
for (;;)
|
|
{
|
|
if (*Src != '\r' && *Src != '\n' && *Src && Dst - Info->m_strDescription < (int)sizeof(Info->m_strDescription) - 1)
|
|
{
|
|
*Dst++ = *Src++;
|
|
continue;
|
|
}
|
|
|
|
*Dst = 0;
|
|
break;
|
|
}
|
|
}
|
|
|
|
lcLoadDefaultColors(lcStudStyle::Plain);
|
|
UpdateLoadingMeshColors();
|
|
lcLoadDefaultCategories(true);
|
|
lcSynthInit();
|
|
lcTrainTrackInfo::Initialize(this);
|
|
|
|
return true;
|
|
}
|