summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJeremy Rimpo <jeremy.rimpo@servermonkey.com>2026-04-16 10:20:50 -0500
committerGitHub <noreply@github.com>2026-04-16 10:20:50 -0500
commit3a5140bb8f933f498e05193b7359198d495defd8 (patch)
tree1fa17aaf33023f1521eb890d04a47cc264c92082 /src
parent05593c0347c1aa2c73144b23b91ccbceae77e70b (diff)
Starfield: Updated blueprint / blueprint prefix support (#2368)
* Add blueprint handling with blueprintships * Blueprint changes - Add tooltips - Add warnings - Handle blueprint prefixes properly * Make sure we're assigning the property
Diffstat (limited to 'src')
-rw-r--r--src/modinfodialogconflicts.cpp2
-rw-r--r--src/modinfodialogfiletree.cpp2
-rw-r--r--src/modinfodialogimages.cpp3
-rw-r--r--src/pluginlist.cpp159
-rw-r--r--src/pluginlist.h5
5 files changed, 156 insertions, 15 deletions
diff --git a/src/modinfodialogconflicts.cpp b/src/modinfodialogconflicts.cpp
index f405f0e1..7c47ff01 100644
--- a/src/modinfodialogconflicts.cpp
+++ b/src/modinfodialogconflicts.cpp
@@ -249,7 +249,7 @@ void ConflictsTab::previewItems(QTreeView* tree)
void ConflictsTab::previewItem(const ConflictItem* item)
{
- core().previewFileWithAlternatives(parentWidget(), item->fileName());
+ core().previewFileWithAlternatives(m_parent->parentWidget(), item->fileName());
}
void ConflictsTab::exploreItems(QTreeView* tree)
diff --git a/src/modinfodialogfiletree.cpp b/src/modinfodialogfiletree.cpp
index 18220854..cb111d12 100644
--- a/src/modinfodialogfiletree.cpp
+++ b/src/modinfodialogfiletree.cpp
@@ -224,7 +224,7 @@ void FileTreeTab::onPreview()
return;
}
- core().previewFile(parentWidget(), mod().name(), m_fs->filePath(selection));
+ core().previewFile(m_parent->parentWidget(), mod().name(), m_fs->filePath(selection));
}
void FileTreeTab::onExplore()
diff --git a/src/modinfodialogimages.cpp b/src/modinfodialogimages.cpp
index 7b250e1e..706124f5 100644
--- a/src/modinfodialogimages.cpp
+++ b/src/modinfodialogimages.cpp
@@ -592,7 +592,8 @@ void ImagesTab::onShowDDS()
void ImagesTab::onPreviewButton()
{
- core().previewFileWithAlternatives(parentWidget(), m_files.selectedFile()->path());
+ core().previewFileWithAlternatives(m_parent->parentWidget(),
+ m_files.selectedFile()->path());
}
void ImagesTab::onFilterChanged()
diff --git a/src/pluginlist.cpp b/src/pluginlist.cpp
index 90785222..6b4c9281 100644
--- a/src/pluginlist.cpp
+++ b/src/pluginlist.cpp
@@ -226,8 +226,10 @@ void PluginList::refresh(const QString& profileName,
gamePlugins ? gamePlugins->blueprintPluginsAreSupported() : false;
const bool loadOrderMechanismNone =
m_GamePlugin->loadOrderMechanism() == IPluginGame::LoadOrderMechanism::None;
+ const QString& blueprintPrefix = m_GamePlugin->blueprintPrefix();
- m_CurrentProfile = profileName;
+ m_CurrentProfile = profileName;
+ m_BlueprintPlugins = blueprintPluginsAreSupported;
std::unordered_map<QString, FileEntryPtr> availablePlugins;
QStringList archiveCandidates;
@@ -287,7 +289,8 @@ void PluginList::refresh(const QString& profileName,
m_ESPs.emplace_back(filename, forceLoaded, forceEnabled, forceDisabled,
originName, ToQString(current->getFullPath()), hasIni,
loadedArchives, lightPluginsAreSupported,
- mediumPluginsAreSupported, blueprintPluginsAreSupported);
+ mediumPluginsAreSupported, blueprintPluginsAreSupported,
+ blueprintPrefix);
m_ESPs.rbegin()->priority = -1;
} catch (const std::exception& e) {
reportError(tr("failed to update esp info for file %1 (source id: %2), error: %3")
@@ -484,6 +487,24 @@ void PluginList::enableESP(const QString& name, bool enable)
m_ESPs[iter->second].forceLoaded ||
m_ESPs[iter->second].forceEnabled;
+ if (m_BlueprintPlugins &&
+ !name.startsWith(m_GamePlugin->blueprintPrefix(), Qt::CaseInsensitive)) {
+ QString blueprint = m_GamePlugin->blueprintPrefix();
+ blueprint += name.left(name.size() - 4) + ".esm";
+ auto blueprintPlugin = m_ESPsByName.find(blueprint);
+ if (blueprintPlugin != m_ESPsByName.end()) {
+ if (m_ESPs[iter->second].enabled) {
+ m_ESPs[blueprintPlugin->second].forceDisabled = false;
+ m_ESPs[blueprintPlugin->second].forceEnabled = true;
+ m_ESPs[blueprintPlugin->second].enabled = true;
+ } else {
+ m_ESPs[blueprintPlugin->second].forceDisabled = true;
+ m_ESPs[blueprintPlugin->second].forceEnabled = false;
+ m_ESPs[blueprintPlugin->second].enabled = false;
+ }
+ }
+ }
+
emit writePluginsList();
if (enabled != m_ESPs[iter->second].enabled) {
pluginStatesChanged({name}, state(name));
@@ -514,6 +535,20 @@ void PluginList::setEnabled(const QModelIndexList& indices, bool enabled)
if (m_ESPs[idx.row()].enabled != enabled) {
m_ESPs[idx.row()].enabled = enabled;
dirty.append(m_ESPs[idx.row()].name);
+ if (m_BlueprintPlugins &&
+ !m_ESPs[idx.row()].name.startsWith(m_GamePlugin->blueprintPrefix(),
+ Qt::CaseInsensitive)) {
+ QString blueprint = m_GamePlugin->blueprintPrefix();
+ blueprint +=
+ m_ESPs[idx.row()].name.left(m_ESPs[idx.row()].name.size() - 4) + ".esm";
+ auto blueprintPlugin = m_ESPsByName.find(blueprint);
+ if (blueprintPlugin != m_ESPsByName.end()) {
+ m_ESPs[blueprintPlugin->second].forceDisabled = false;
+ m_ESPs[blueprintPlugin->second].forceEnabled = true;
+ m_ESPs[blueprintPlugin->second].enabled = true;
+ dirty.append(m_ESPs[blueprintPlugin->second].name);
+ }
+ }
}
}
if (!dirty.isEmpty()) {
@@ -532,6 +567,18 @@ void PluginList::setEnabledAll(bool enabled)
if (info.enabled != enabled) {
info.enabled = enabled;
dirty.append(info.name);
+ if (m_BlueprintPlugins &&
+ !info.name.startsWith(m_GamePlugin->blueprintPrefix(), Qt::CaseInsensitive)) {
+ QString blueprint = m_GamePlugin->blueprintPrefix();
+ blueprint += info.name.left(info.name.size() - 4) + ".esm";
+ auto blueprintPlugin = m_ESPsByName.find(blueprint);
+ if (blueprintPlugin != m_ESPsByName.end()) {
+ m_ESPs[blueprintPlugin->second].forceDisabled = false;
+ m_ESPs[blueprintPlugin->second].forceEnabled = true;
+ m_ESPs[blueprintPlugin->second].enabled = true;
+ dirty.append(m_ESPs[blueprintPlugin->second].name);
+ }
+ }
}
}
if (!dirty.isEmpty()) {
@@ -955,6 +1002,26 @@ void PluginList::setState(const QString& name, PluginStates state)
m_ESPs[iter->second].enabled =
(state == IPluginList::STATE_ACTIVE && !m_ESPs[iter->second].forceDisabled) ||
m_ESPs[iter->second].forceLoaded || m_ESPs[iter->second].forceEnabled;
+
+ if (m_BlueprintPlugins &&
+ !m_ESPs[iter->second].name.startsWith(m_GamePlugin->blueprintPrefix(),
+ Qt::CaseInsensitive)) {
+ QString blueprint = m_GamePlugin->blueprintPrefix();
+ blueprint +=
+ m_ESPs[iter->second].name.left(m_ESPs[iter->second].name.size() - 4) + ".esm";
+ auto blueprintPlugin = m_ESPsByName.find(blueprint);
+ if (blueprintPlugin != m_ESPsByName.end()) {
+ if (m_ESPs[iter->second].enabled) {
+ m_ESPs[blueprintPlugin->second].forceDisabled = false;
+ m_ESPs[blueprintPlugin->second].forceEnabled = true;
+ m_ESPs[blueprintPlugin->second].enabled = true;
+ } else {
+ m_ESPs[blueprintPlugin->second].forceDisabled = true;
+ m_ESPs[blueprintPlugin->second].forceEnabled = false;
+ m_ESPs[blueprintPlugin->second].enabled = false;
+ }
+ }
+ }
} else {
log::warn("Plugin not found: {}", name);
}
@@ -1404,7 +1471,8 @@ QVariant PluginList::fontData(const QModelIndex& modelIndex) const
result.setItalic(true);
else if (m_ESPs[index].isMediumFlagged)
result.setUnderline(true);
- if (m_ESPs[index].isBlueprintFlagged)
+ if (m_BlueprintPlugins &&
+ (m_ESPs[index].isBlueprintFlagged || m_ESPs[index].isBlueprintPrefixed))
result.setLetterSpacing(QFont::SpacingType::AbsoluteSpacing, 2);
return result;
@@ -1515,11 +1583,29 @@ QVariant PluginList::tooltipData(const QModelIndex& modelIndex) const
"medium plugins in addition to other plugin types.");
}
- if (esp.isBlueprintFlagged) {
- toolTip += "<br><br>" +
- tr("This plugin has the blueprint flag. This forces it to load after "
- "every other non-blueprint plugin. Blueprint plugins will adhere to "
- "standard load order rules with other blueprint plugins.");
+ if (m_BlueprintPlugins) {
+ if (esp.isBlueprintFlagged) {
+ toolTip += "<br><br>" +
+ tr("This plugin has the blueprint flag. This forces it to load after "
+ "every other non-blueprint plugin.");
+ if (!esp.enabled) {
+ toolTip += " " +
+ tr("Blueprint plugins are removed "
+ "from plugins.txt by the game and are effectively disabled. They "
+ "are intended to be loaded by using the blueprint prefix:") +
+ " " + m_GamePlugin->blueprintPrefix() + "[main plugin].esm";
+ } else if (esp.isBlueprintPrefixed) {
+ toolTip +=
+ " " +
+ tr("This plugin is prefixed and being loaded by a paired main plugin: ") +
+ " " + m_GamePlugin->blueprintPrefix() + "[main plugin].esm";
+ }
+ } else if (esp.isBlueprintPrefixed && esp.enabled) {
+ toolTip += "<br><br>" +
+ tr("WARNING: This is a blueprint-prefixed but unflagged plugin being "
+ "autoloaded by another main plugin file. This is unintended usage "
+ "and may result in an ambiguous load order.");
+ }
}
if (esp.isLightFlagged && esp.isMediumFlagged) {
@@ -1539,6 +1625,16 @@ QVariant PluginList::tooltipData(const QModelIndex& modelIndex) const
auto feature = m_Organizer.gameFeatures().gameFeature<GamePlugins>();
if (feature && esp.hasLightExtension && feature->lightPluginsAreSupported()) {
toolTip += "<br><br>" + tr("Light plugins (ESL) are not supported by this game.");
+ } else if (m_BlueprintPlugins && esp.isBlueprintFlagged &&
+ esp.isBlueprintPrefixed) {
+ toolTip += "<br><br>" +
+ tr("This blueprint plugin must be enabled by a paired main plugin.");
+ } else if (m_BlueprintPlugins && esp.isBlueprintFlagged) {
+ toolTip += "<br><br>" + tr("This blueprint plugin is not using the blueprint "
+ "prefix and can't be loaded.");
+ } else if (m_BlueprintPlugins && esp.isBlueprintPrefixed) {
+ toolTip +=
+ "<br><br>" + tr("This is an invalid blueprint file with no blueprint flag.");
} else {
toolTip += "<br><br>" + tr("This game does not currently permit custom plugin "
"loading. There may be manual workarounds.");
@@ -1667,7 +1763,7 @@ QVariant PluginList::iconData(const QModelIndex& modelIndex) const
}
}
- if (esp.isBlueprintFlagged) {
+ if (m_BlueprintPlugins && esp.isBlueprintFlagged) {
result.append(":/MO/gui/resources/go-down.png");
}
@@ -1688,6 +1784,16 @@ bool PluginList::isProblematic(const ESPInfo& esp, const AdditionalInfo* info) c
return true;
}
+ if (m_BlueprintPlugins) {
+ if (esp.isBlueprintPrefixed && esp.enabled && !esp.isBlueprintFlagged) {
+ return true;
+ }
+
+ if (esp.isBlueprintFlagged && !esp.isBlueprintPrefixed) {
+ return true;
+ }
+ }
+
if (info) {
if (!info->loot.incompatibilities.empty()) {
return true;
@@ -1730,6 +1836,26 @@ bool PluginList::setData(const QModelIndex& modIndex, const QVariant& value, int
m_LastCheck.restart();
emit dataChanged(modIndex, modIndex);
+ if (m_BlueprintPlugins &&
+ !m_ESPs[modIndex.row()].name.startsWith(m_GamePlugin->blueprintPrefix(),
+ Qt::CaseInsensitive)) {
+ QString blueprint = m_GamePlugin->blueprintPrefix();
+ blueprint +=
+ m_ESPs[modIndex.row()].name.left(m_ESPs[modIndex.row()].name.size() - 4) +
+ ".esm";
+ auto blueprintPlugin = m_ESPsByName.find(blueprint);
+ if (blueprintPlugin != m_ESPsByName.end()) {
+ if (m_ESPs[modIndex.row()].enabled) {
+ m_ESPs[blueprintPlugin->second].forceDisabled = false;
+ m_ESPs[blueprintPlugin->second].forceEnabled = true;
+ m_ESPs[blueprintPlugin->second].enabled = true;
+ } else {
+ m_ESPs[blueprintPlugin->second].forceDisabled = true;
+ m_ESPs[blueprintPlugin->second].forceEnabled = false;
+ m_ESPs[blueprintPlugin->second].enabled = false;
+ }
+ }
+ }
refreshLoadOrder();
emit writePluginsList();
@@ -2002,7 +2128,8 @@ PluginList::ESPInfo::ESPInfo(const QString& name, bool forceLoaded, bool forceEn
bool forceDisabled, const QString& originName,
const QString& fullPath, bool hasIni,
std::set<QString> archives, bool lightSupported,
- bool mediumSupported, bool blueprintSupported)
+ bool mediumSupported, bool blueprintSupported,
+ const QString& blueprintPrefix)
: name(name), fullPath(fullPath), enabled(forceLoaded), forceLoaded(forceLoaded),
forceEnabled(forceEnabled), forceDisabled(forceDisabled), priority(0),
loadOrder(-1), originName(originName), hasIni(hasIni),
@@ -2020,7 +2147,17 @@ PluginList::ESPInfo::ESPInfo(const QString& name, bool forceLoaded, bool forceEn
isBlueprintFlagged = blueprintSupported &&
(isMasterFlagged || hasMasterExtension || hasLightExtension) &&
file.isBlueprint();
- hasNoRecords = file.isDummy();
+ isBlueprintPrefixed =
+ blueprintSupported && name.startsWith(blueprintPrefix, Qt::CaseInsensitive);
+ hasNoRecords = file.isDummy();
+
+ if (blueprintSupported) {
+ if ((isBlueprintFlagged || isBlueprintPrefixed) && !this->forceEnabled) {
+ this->forceDisabled = true;
+ }
+ }
+
+ hasNoRecords = file.isDummy();
formVersion = file.formVersion();
headerVersion = file.headerVersion();
diff --git a/src/pluginlist.h b/src/pluginlist.h
index 224ae09e..478fa85e 100644
--- a/src/pluginlist.h
+++ b/src/pluginlist.h
@@ -331,7 +331,8 @@ private:
ESPInfo(const QString& name, bool forceLoaded, bool forceEnabled,
bool forceDisabled, const QString& originName, const QString& fullPath,
bool hasIni, std::set<QString> archives, bool lightSupported,
- bool mediumSupported, bool blueprintSupported);
+ bool mediumSupported, bool blueprintSupported,
+ const QString& blueprintPrefix);
QString name;
QString fullPath;
@@ -350,6 +351,7 @@ private:
bool isMediumFlagged;
bool isLightFlagged;
bool isBlueprintFlagged;
+ bool isBlueprintPrefixed;
bool hasNoRecords;
bool modSelected;
bool isMasterOfSelectedPlugin;
@@ -425,6 +427,7 @@ private:
QElapsedTimer m_LastCheck;
const MOBase::IPluginGame* m_GamePlugin;
+ bool m_BlueprintPlugins = false;
QVariant displayData(const QModelIndex& modelIndex) const;
QVariant checkstateData(const QModelIndex& modelIndex) const;