mirror of
https://github.com/leozide/leocad.git
synced 2026-10-27 06:20:55 +00:00
Move sparse piece load errors and registry locking into lcPieceManager so registry access, reference bookkeeping and copied worker metadata share one owner. Release registry and error locks before resource teardown, recursive project deletion or loader cancellation; remove library lock scopes around UI source resolution and worker-drained settings changes. Add HasOtherReferences to the shared asset reference for piece and texture consumer checks, removing the library/manager consumer-query wrappers. Keep the post-release cancellation count check and existing lifetime policies. Rename the sparse error map to mPieceDataLoadErrors and document the completed manager boundary and thread/teardown contracts. Validation: warning-free macOS Qt 6.10 Debug build and all 43 offscreen lifecycle checks pass, including concurrent metadata/error reads, settings changes with pending requests and shared reference queries. Qt 5, ASan and successful GPU rendering were not tested.
2535 lines
69 KiB
C++
2535 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 "lc_piecedata.h"
|
|
#include "lc_piecemanager.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)
|
|
{
|
|
mPieceManager = std::make_unique<lcPieceManager>();
|
|
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()
|
|
{
|
|
Q_ASSERT(QThread::currentThread() == thread());
|
|
|
|
const bool HadLoader = static_cast<bool>(mAssetLoader);
|
|
|
|
if (HadLoader && mThumbnailManager)
|
|
mThumbnailManager->Clear();
|
|
|
|
mAssetLoader.reset();
|
|
mSynthPieces.clear();
|
|
|
|
mPieceManager->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()
|
|
{
|
|
mPieceManager->RemoveTemporaryPieceData();
|
|
}
|
|
|
|
void lcPiecesLibrary::ReleaseProjectPieceData(Project* OwnerProject)
|
|
{
|
|
mPieceManager->ReleaseProjectPieceData(OwnerProject);
|
|
}
|
|
|
|
void lcPiecesLibrary::TransferProjectPieceData(Project* Source, Project* Destination)
|
|
{
|
|
mPieceManager->TransferProjectPieceData(Source, Destination);
|
|
}
|
|
|
|
void lcPiecesLibrary::SetModelPieceDataName(lcPieceData* PieceData, const char* Name)
|
|
{
|
|
mPieceManager->SetModelPieceDataName(PieceData, Name);
|
|
}
|
|
|
|
bool lcPiecesLibrary::RenameModelPieceData(lcPieceData* PieceData, const char* NewName)
|
|
{
|
|
if (!mPieceManager->RenameModelPieceData(PieceData, NewName))
|
|
return false;
|
|
|
|
if (PieceData->mState != lcAssetState::Failed)
|
|
ClearPieceDataLoadError(PieceData);
|
|
|
|
return true;
|
|
}
|
|
|
|
std::string lcPiecesLibrary::NormalizePieceDataName(const char* PieceName)
|
|
{
|
|
return lcPieceManager::NormalizePieceDataName(PieceName);
|
|
}
|
|
|
|
lcPieceDataRef lcPiecesLibrary::AcquireModelPieceData(const char* PieceName, Project* Project, bool& Reused)
|
|
{
|
|
lcPieceData* PieceData = mPieceManager->AcquireModelPieceData(PieceName, Project, Reused);
|
|
|
|
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);
|
|
|
|
return mPieceManager->FindPieceData(Name, CurrentProject);
|
|
}
|
|
|
|
lcPieceDataRef lcPiecesLibrary::AcquirePieceData(const char* PieceName, Project* CurrentProject, bool CreateMissing, bool SearchProjectFolder)
|
|
{
|
|
Q_ASSERT(QThread::currentThread() == thread());
|
|
|
|
QString ProjectPath;
|
|
QString LoadError;
|
|
|
|
if (lcPieceData* PieceData = FindPieceData(PieceName, CurrentProject))
|
|
return lcPieceDataRef(PieceData, this);
|
|
|
|
if (CurrentProject && SearchProjectFolder && !CurrentProject->GetFileName().isEmpty())
|
|
ProjectPath = QFileInfo(CurrentProject->GetFileName()).absolutePath();
|
|
|
|
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 = mPieceManager->CreatePieceData(PieceName, CurrentProject);
|
|
|
|
PieceData->CreateProject(ExternalProject.value(), PieceName);
|
|
|
|
return lcPieceDataRef(PieceData, this);
|
|
}
|
|
else
|
|
LoadError = ExternalProject.error();
|
|
}
|
|
}
|
|
|
|
if (CreateMissing)
|
|
{
|
|
lcPieceData* PieceData = mPieceManager->CreatePieceData(PieceName, CurrentProject);
|
|
|
|
PieceData->SetFailed(LoadError.isEmpty() ? tr("Could not find piece '%1'.").arg(QString::fromLatin1(PieceName)) : LoadError);
|
|
|
|
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;
|
|
}
|
|
|
|
lcTextureRef lcPiecesLibrary::AcquireTexture(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;
|
|
return AcquireTexture(TextureName, std::vector<QString>{ ProjectPath }, EmbeddedData, Source);
|
|
}
|
|
|
|
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)
|
|
{
|
|
Q_ASSERT(QThread::currentThread() == thread());
|
|
|
|
ProjectPath.clear();
|
|
|
|
if (!TextureName[0] || strlen(TextureName) >= LC_TEXTURE_NAME_LEN)
|
|
return {};
|
|
|
|
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::FindTexture(const lcFileSource& Source)
|
|
{
|
|
return mTextureManager->FindTexture(Source);
|
|
}
|
|
|
|
lcTextureRef lcPiecesLibrary::AcquireTexture(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 lcTextureRef::Adopt(mTextureManager->AcquireTexture(TextureName, Source, ProjectPath), this);
|
|
}
|
|
|
|
void lcPiecesLibrary::RequestTextureLoad(lcTexture* Texture, lcLoadPriority LoadPriority)
|
|
{
|
|
Q_ASSERT(QThread::currentThread() == thread());
|
|
|
|
mAssetLoader->RequestTextureLoad(Texture, Texture->mSource, LoadPriority);
|
|
}
|
|
|
|
bool lcPiecesLibrary::WaitForTexture(const lcTextureRef& TextureRef)
|
|
{
|
|
Q_ASSERT(QThread::currentThread() == thread());
|
|
|
|
return mAssetLoader->WaitForTexture(TextureRef.Get(), TextureRef->mSource);
|
|
}
|
|
|
|
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 = mPieceManager->CreateLibraryPieceData(Name, FileInfo.file_name + (Name - NameBuffer));
|
|
}
|
|
|
|
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 (lcPieceData* PieceData : mPieceManager->GetLibraryPieceData())
|
|
{
|
|
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 && mPieceManager->FindPieceData(Name, nullptr))
|
|
continue;
|
|
|
|
lcPieceData* PieceData = mPieceManager->CreateLibraryPieceData(Name, FileString.constData());
|
|
PieceData->mFolderType = FolderIdx;
|
|
PieceData->mFolderIndex = FileIdx;
|
|
}
|
|
}
|
|
|
|
QAtomicInt FilesLoaded;
|
|
bool Modified = false;
|
|
|
|
auto ReadDescriptions = [&FileLists, &CachedDescriptions, &FilesLoaded, &Modified](lcPieceData* PieceData)
|
|
{
|
|
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)mPieceManager->GetLibraryPieceDataCount());
|
|
ProgressDialog->setMinimum(0);
|
|
ProgressDialog->setValue(0);
|
|
ProgressDialog->setCancelButton(nullptr);
|
|
ProgressDialog->setAutoReset(false);
|
|
if (ShowProgress)
|
|
ProgressDialog->show();
|
|
|
|
std::vector<lcPieceData*> LibraryPieceData = mPieceManager->GetLibraryPieceData();
|
|
QFuture<void> LoadFuture = QtConcurrent::map(LibraryPieceData, 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)mPieceManager->GetLibraryPieceDataCount());
|
|
|
|
std::vector<lcPieceData*> SortedPieces = mPieceManager->GetLibraryPieceData();
|
|
|
|
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 != mPieceManager->GetLibraryPieceDataCount())
|
|
return false;
|
|
|
|
for (lcPieceData* PieceData : mPieceManager->GetLibraryPieceData())
|
|
{
|
|
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)mPieceManager->GetLibraryPieceDataCount();
|
|
|
|
if (IndexFile.WriteBuffer((char*)&NumFiles, sizeof(NumFiles)) == 0)
|
|
return false;
|
|
|
|
for (const lcPieceData* PieceData : mPieceManager->GetLibraryPieceData())
|
|
{
|
|
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)
|
|
{
|
|
const int Remaining = mPieceManager->ReleasePieceData(PieceData);
|
|
|
|
if (Remaining == 1 && mAssetLoader)
|
|
mAssetLoader->CancelPieceLoad(PieceData);
|
|
}
|
|
|
|
void lcPiecesLibrary::AddPieceDataReference(lcPieceData* PieceData)
|
|
{
|
|
mPieceManager->AddPieceDataReference(PieceData);
|
|
}
|
|
|
|
void lcPiecesLibrary::SetPieceDataLoadError(const lcPieceData* PieceData, QString Error)
|
|
{
|
|
mPieceManager->SetPieceDataLoadError(PieceData, std::move(Error));
|
|
}
|
|
|
|
QString lcPiecesLibrary::GetPieceDataLoadError(const lcPieceData* PieceData) const
|
|
{
|
|
return mPieceManager->GetPieceDataLoadError(PieceData);
|
|
}
|
|
|
|
void lcPiecesLibrary::ClearPieceDataLoadError(const lcPieceData* PieceData)
|
|
{
|
|
mPieceManager->ClearPieceDataLoadError(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)
|
|
{
|
|
const std::optional<lcPieceManager::FileInfo> FileInfo = mPieceManager->GetPieceDataFileInfo(PieceName);
|
|
|
|
if (FileInfo)
|
|
{
|
|
if (mZipFiles[static_cast<int>(lcZipFileType::Official)] && FileInfo->ZipFileType != lcZipFileType::Count)
|
|
{
|
|
lcMemFile IncludeFile;
|
|
|
|
if (mZipFiles[static_cast<int>(FileInfo->ZipFileType)]->ExtractFile(FileInfo->ZipFileIndex, IncludeFile))
|
|
Callback(IncludeFile);
|
|
}
|
|
else
|
|
{
|
|
lcDiskFile IncludeFile;
|
|
char FileName[LC_MAXPATH];
|
|
bool Found = false;
|
|
|
|
snprintf(FileName, sizeof(FileName), "parts/%s", FileInfo->FileName.c_str());
|
|
IncludeFile.SetFileName(mLibraryDir.absoluteFilePath(QLatin1String(FileName)));
|
|
Found = IncludeFile.Open(QIODevice::ReadOnly);
|
|
|
|
if (mHasUnofficial && !Found)
|
|
{
|
|
snprintf(FileName, sizeof(FileName), "unofficial/parts/%s", FileInfo->FileName.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 (lcPieceData* PieceData : mPieceManager->GetAllPieceData())
|
|
AddMesh(PieceData);
|
|
|
|
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()
|
|
{
|
|
for (lcPieceData* PieceData : mPieceManager->GetLibraryPieceData())
|
|
{
|
|
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);
|
|
}
|
|
|
|
void lcPiecesLibrary::SetOfficialPieceLibrary()
|
|
{
|
|
if (mZipFiles[static_cast<int>(lcZipFileType::Official)])
|
|
mNumOfficialPieces = static_cast<int>(mPieceManager->GetLibraryPieceDataCount());
|
|
}
|
|
|
|
bool lcPiecesLibrary::SupportsStudStyle() const
|
|
{
|
|
return true;
|
|
}
|
|
|
|
void lcPiecesLibrary::UpdateStudStyleAndColors(lcStudStyle StudStyle, bool StudCylinderColorEnabled)
|
|
{
|
|
Q_ASSERT(QThread::currentThread() == thread());
|
|
|
|
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();
|
|
|
|
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 (lcPieceData* PieceData : mPieceManager->GetAllPieceData())
|
|
ReloadPiece(PieceData);
|
|
|
|
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 (lcPieceData* PieceData : mPieceManager->GetLibraryPieceData())
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<lcPieceData*> lcPiecesLibrary::GetParts() const
|
|
{
|
|
return mPieceManager->GetLibraryPieceData();
|
|
}
|
|
|
|
std::vector<lcPieceData*> lcPiecesLibrary::GetVisibleTrainTrackParts(const lcTrainTrackConnectionType& ConnectionType) const
|
|
{
|
|
std::vector<lcPieceData*> Parts;
|
|
|
|
for (lcPieceData* PieceData : mPieceManager->GetLibraryPieceData())
|
|
{
|
|
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)
|
|
{
|
|
if (lcPieceData* PieceData = mPieceManager->FindPieceData(PartId, nullptr))
|
|
Parts.push_back(PieceData);
|
|
}
|
|
|
|
return Parts;
|
|
}
|
|
|
|
std::string lcPiecesLibrary::GetPartId(const lcPieceData* PieceData) const
|
|
{
|
|
return mPieceManager->GetLibraryPieceDataName(PieceData);
|
|
}
|
|
|
|
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 (lcPieceData* PieceData : mPieceManager->GetLibraryPieceData())
|
|
{
|
|
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;
|
|
}
|