// SPDX-FileCopyrightText: 2023 Joshua Goins // SPDX-License-Identifier: GPL-3.0-or-later #include "gearview.h" #include #include #include #include #include #include "filecache.h" #include "magic_enum.hpp" GearView::GearView(GameData* data, FileCache& cache) : data(data), cache(cache) { mdlPart = new MDLPart(data, cache); reloadRaceDeforms(); auto layout = new QVBoxLayout(); layout->addWidget(mdlPart); setLayout(layout); mdlPart->requestUpdate = [this] { auto &io = ImGui::GetIO(); if (updating) { if (ImGui::Begin("Loading", nullptr, ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs)) { ImGui::SetWindowPos(ImVec2(0, 0)); ImGui::SetWindowSize(io.DisplaySize); const char *loadingLabel{"Loading gear..."}; ImGui::SetCursorPosX((io.DisplaySize.x - ImGui::CalcTextSize(loadingLabel).x) * 0.5f); ImGui::SetCursorPosY((io.DisplaySize.y - ImGui::CalcTextSize(loadingLabel).y) * 0.5f); ImGui::Text("%s", loadingLabel); } ImGui::End(); } if (updating) { return; } if (needsUpdate()) { updating = true; Q_EMIT loadingChanged(true); QtConcurrent::run(QThreadPool::globalInstance(), [this] { updatePart(); Q_EMIT loadingChanged(false); }); } }; } std::vector> GearView::supportedRaces() const { std::vector> races; for (const auto &gear : loadedGears) { for (const auto [race, race_name] : magic_enum::enum_entries()) { for (const auto subrace : physis_get_supported_subraces(race).subraces) { auto equip_path = physis_build_equipment_path(gear.info.modelInfo.primaryID, race, subrace, currentGender, gear.info.slot); if (cache.fileExists(QLatin1String(equip_path))) races.emplace_back(race, subrace); } } } return races; } std::vector GearView::supportedGenders() const { std::vector genders; for (const auto &gear : loadedGears) { for (auto [gender, gender_name] : magic_enum::enum_entries()) { auto equip_path = physis_build_equipment_path(gear.info.modelInfo.primaryID, currentRace, Subrace::Midlander, currentGender, gear.info.slot); if (cache.fileExists(QLatin1String(equip_path))) genders.push_back(gender); } } return genders; } int GearView::lodCount() const { return maxLod; } void GearView::exportModel(const QString& fileName) { mdlPart->exportModel(fileName); } void GearView::addGear(GearInfo &gear) { qDebug() << "Adding gear" << gear.name.c_str(); queuedGearAdditions.emplace_back(gear); gearDirty = true; Q_EMIT gearChanged(); } void GearView::removeGear(GearInfo &gear) { qDebug() << "Removing gear" << gear.name.c_str(); queuedGearRemovals.emplace_back(gear); gearDirty = true; Q_EMIT gearChanged(); } void GearView::setRace(Race race) { if (currentRace == race) { return; } currentRace = race; const auto supportedSubraces = physis_get_supported_subraces(race); if (supportedSubraces.subraces[0] != currentSubrace && supportedSubraces.subraces[1] != currentSubrace) { setSubrace(supportedSubraces.subraces[0]); } if (race == Race::AuRa || race == Race::Miqote) { setTail(1); } else { setTail(-1); } raceDirty = true; Q_EMIT raceChanged(); } void GearView::setSubrace(Subrace subrace) { if (currentSubrace == subrace) { return; } currentSubrace = subrace; // Hyur is the only race that has two different subraces if (currentRace == Race::Hyur) { raceDirty = true; } Q_EMIT subraceChanged(); } void GearView::setGender(Gender gender) { if (currentGender == gender) { return; } currentGender = gender; raceDirty = true; Q_EMIT genderChanged(); } void GearView::setLevelOfDetail(int lod) { if (currentLod == lod) { return; } currentLod = lod; Q_EMIT levelOfDetailChanged(); } void GearView::setFace(const int faceCode) { if (face == faceCode) { return; } if (faceCode == -1) { face = std::nullopt; } else { face = faceCode; } faceDirty = true; Q_EMIT faceChanged(); } void GearView::setHair(int hairCode) { if (hair == hairCode) { return; } if (hairCode == -1) { hair = std::nullopt; } else { hair = hairCode; } hairDirty = true; Q_EMIT hairChanged(); } void GearView::setEar(const int earCode) { if (ear == earCode) { return; } if (earCode == -1) { ear = std::nullopt; } else { ear = earCode; } earDirty = true; Q_EMIT earChanged(); } void GearView::setTail(const int tailCode) { if (tail == tailCode) { return; } if (tailCode == -1) { tail = std::nullopt; } else { tail = tailCode; } tailDirty = true; Q_EMIT tailChanged(); } void GearView::reloadRaceDeforms() { qDebug() << "Loading race deform matrices for " << magic_enum::enum_name(currentRace).data() << magic_enum::enum_name(currentSubrace).data() << magic_enum::enum_name(currentGender).data(); const int raceCode = physis_get_race_code(currentRace, currentSubrace, currentGender); qDebug() << "Race code: " << raceCode; QString skelName = QStringLiteral("c%1b0001.skel").arg(raceCode, 4, 10, QLatin1Char{'0'}); mdlPart->setSkeleton(physis_skeleton_from_skel(physis_read_file(skelName.toStdString().c_str()))); // racial deforms don't work on Hyur Midlander, not needed? TODO not sure if (currentSubrace != Subrace::Midlander) { QString deformName = QStringLiteral("c%1_deform.json").arg(raceCode, 4, 10, QLatin1Char{'0'}); mdlPart->loadRaceDeformMatrices(physis_read_file(deformName.toStdString().c_str())); } else { for (auto &data : mdlPart->boneData) { data.deformRaceMatrix = glm::mat4(1.0f); } } } MDLPart &GearView::part() const { return *mdlPart; } void GearView::updatePart() { qInfo() << raceDirty << gearDirty << updating; if (raceDirty) { // if race changes, all of the models need to be reloaded. // TODO: in the future, we can be a bit smarter about this, lots of races use the same model (hyur) for (auto &part : loadedGears) { mdlPart->removeModel(part.mdl); } queuedGearAdditions = loadedGears; loadedGears.clear(); gearDirty = true; } if (gearDirty) { for (auto &gearAddition : queuedGearAdditions) { qInfo() << "Looking up" << magic_enum::enum_name(currentRace) << magic_enum::enum_name(currentSubrace) << magic_enum::enum_name(currentGender); auto mdl_data = cache.lookupFile(QLatin1String( physis_build_equipment_path(gearAddition.info.modelInfo.primaryID, currentRace, currentSubrace, currentGender, gearAddition.info.slot))); // attempt to load the next best race // currently hardcoded to hyur midlander Race fallbackRace = currentRace; Subrace fallbackSubrace = currentSubrace; if (mdl_data.size == 0) { mdl_data = cache.lookupFile(QLatin1String( physis_build_equipment_path(gearAddition.info.modelInfo.primaryID, Race::Hyur, Subrace::Midlander, currentGender, gearAddition.info.slot))); fallbackRace = Race::Hyur; fallbackSubrace = Subrace::Midlander; } if (mdl_data.size > 0) { auto mdl = physis_mdl_parse(mdl_data.size, mdl_data.data); std::vector materials; for (int i = 0; i < mdl.num_material_names; i++) { const char *material_name = mdl.material_names[i]; const std::string mtrl_path = gearAddition.info.getMtrlPath(material_name); const std::string skinmtrl_path = physis_build_skin_material_path(physis_get_race_code(fallbackRace, fallbackSubrace, currentGender), 1, material_name); if (cache.fileExists(QLatin1String(mtrl_path.c_str()))) { auto mat = physis_material_parse(cache.lookupFile(QLatin1String(mtrl_path.c_str()))); materials.push_back(mat); } if (cache.fileExists(QLatin1String(skinmtrl_path.c_str()))) { auto mat = physis_material_parse(cache.lookupFile(QLatin1String(skinmtrl_path.c_str()))); materials.push_back(mat); } } maxLod = std::max(mdl.num_lod, maxLod); mdlPart->addModel(mdl, materials, currentLod); gearAddition.mdl = mdl; loadedGears.push_back(gearAddition); } } for (auto &queuedRemoval : queuedGearRemovals) { mdlPart->removeModel(queuedRemoval.mdl); loadedGears.erase(std::remove_if(loadedGears.begin(), loadedGears.end(), [queuedRemoval](const LoadedGear other) { return queuedRemoval.info == other.info; }), loadedGears.end()); } } if (face) { auto mdl_data = cache.lookupFile(QLatin1String(physis_build_character_path(CharacterCategory::Face, *face, currentRace, currentSubrace, currentGender))); if (mdl_data.size > 0) { auto mdl = physis_mdl_parse(mdl_data.size, mdl_data.data); std::vector materials; for (int i = 0; i < mdl.num_material_names; i++) { const char* material_name = mdl.material_names[i]; const std::string skinmtrl_path = physis_build_face_material_path(physis_get_race_code(currentRace, currentSubrace, currentGender), *face, material_name); if (cache.fileExists(QLatin1String(skinmtrl_path.c_str()))) { auto mat = physis_material_parse(cache.lookupFile(QLatin1String(skinmtrl_path.c_str()))); materials.push_back(mat); } } mdlPart->addModel(mdl, materials, currentLod); } } if (hair) { auto mdl_data = cache.lookupFile(QLatin1String(physis_build_character_path(CharacterCategory::Hair, *hair, currentRace, currentSubrace, currentGender))); if (mdl_data.size > 0) { auto mdl = physis_mdl_parse(mdl_data.size, mdl_data.data); std::vector materials; for (int i = 0; i < mdl.num_material_names; i++) { const char* material_name = mdl.material_names[i]; const std::string skinmtrl_path = physis_build_hair_material_path(physis_get_race_code(currentRace, currentSubrace, currentGender), *hair, material_name); if (cache.fileExists(QLatin1String(skinmtrl_path.c_str()))) { auto mat = physis_material_parse(cache.lookupFile(QLatin1String(skinmtrl_path.c_str()))); materials.push_back(mat); } } mdlPart->addModel(mdl, materials, currentLod); } } if (ear) { auto mdl_data = cache.lookupFile(QLatin1String(physis_build_character_path(CharacterCategory::Hair, *ear, currentRace, currentSubrace, currentGender))); if (mdl_data.size > 0) { auto mdl = physis_mdl_parse(mdl_data.size, mdl_data.data); std::vector materials; for (int i = 0; i < mdl.num_material_names; i++) { const char* material_name = mdl.material_names[i]; const std::string skinmtrl_path = physis_build_ear_material_path(physis_get_race_code(currentRace, currentSubrace, currentGender), *ear, material_name); if (cache.fileExists(QLatin1String(skinmtrl_path.c_str()))) { auto mat = physis_material_parse(cache.lookupFile(QLatin1String(skinmtrl_path.c_str()))); materials.push_back(mat); } } mdlPart->addModel(mdl, materials, currentLod); } } if (tail) { auto mdl_data = cache.lookupFile(QLatin1String(physis_build_character_path(CharacterCategory::Tail, *tail, currentRace, currentSubrace, currentGender))); if (mdl_data.size > 0) { auto mdl = physis_mdl_parse(mdl_data.size, mdl_data.data); const char* material_name = mdl.material_names[0]; const std::string skinmtrl_path = physis_build_tail_material_path(physis_get_race_code(currentRace, currentSubrace, currentGender), *tail, material_name); if (cache.fileExists(QLatin1String(skinmtrl_path.c_str()))) { auto mat = physis_material_parse(cache.lookupFile(QLatin1String(skinmtrl_path.c_str()))); mdlPart->addModel(mdl, {mat}, currentLod); } } } raceDirty = false; gearDirty = false; updating = false; faceDirty = false; hairDirty = false; earDirty = false; tailDirty = false; } bool GearView::needsUpdate() const { return gearDirty || raceDirty || faceDirty || hairDirty || earDirty || tailDirty; } #include "moc_gearview.cpp"