From ac4973a98dfb4ba758733f85d17862c0e16aa3fc Mon Sep 17 00:00:00 2001 From: gerwang Date: Sat, 23 May 2026 04:55:03 -0500 Subject: [PATCH 1/2] Support "axes" array format for SFMA multi-axis funscripts XTPlayer's internal format uses a "channels" JSON key for multi-axis funscripts, but the wider ecosystem uses an "axes" array of {id, actions} objects. This adds parsing for both formats in funscript loading, library metadata detection, and SFMA status checks. Also fixes a copy-paste bug where isSFMA change detection compared against isMFS, and resets isSFMA before discovery to handle removal. --- src/lib/handler/funscripthandler.cpp | 115 +++++++++++++++++++++--- src/lib/handler/funscripthandler.h | 2 + src/lib/handler/medialibraryhandler.cpp | 3 +- 3 files changed, 108 insertions(+), 12 deletions(-) diff --git a/src/lib/handler/funscripthandler.cpp b/src/lib/handler/funscripthandler.cpp index 9e81128..57c310d 100644 --- a/src/lib/handler/funscripthandler.cpp +++ b/src/lib/handler/funscripthandler.cpp @@ -169,6 +169,33 @@ void FunscriptHandler::jsonToFunscript(QJsonObject json) } } } + + // Parse "axes" format (array of {id, actions} objects) + if(json.contains(m_axesJSONObjectName) && json[m_axesJSONObjectName].isArray()) + { + auto jsonAxes = json[m_axesJSONObjectName].toArray(); + for(auto val : jsonAxes) + { + QJsonObject axisObj = val.toObject(); + if(axisObj.contains("id") && axisObj.contains("actions")) + { + Track track = trackFromTCodeChannel(axisObj["id"].toString()); + if(track != Track::None && track != Track::Stroke) + { + ChannelModel33* channel = TCodeChannelLookup::getChannel(TCodeChannelLookup::ToString(track)); + if(channel && channel->Type != ChannelType::HalfOscillate) + { + Funscript funscript; + jsonToFunscript(axisObj, funscript); + setFunscriptSettings(track, funscript); + m_funscripts.insert(track, funscript); + SettingsHandler::setFunscriptLoaded(TCodeChannelLookup::ToString(track), true); + } + } + } + } + } + if(!m_funscripts.contains(Track::Stroke)) { Funscript funscript; @@ -630,9 +657,11 @@ QList FunscriptHandler::getSFMATracks(QString libraryItemMediaPath) return scriptInfos; // scriptInfos.append({"Default", libraryItemMediaPathNoExt, scriptPath, TCodeChannelLookup::ToString(Track::Stroke), ScriptType::MAIN, ScriptContainerType::BASE, "" }); - if(!json[m_sfmaJSONObjectName].isNull()) + QString libraryItemMediaNameNoExt = XFileUtil::getNameNoExtension(libraryItemMediaPath); + + // Check "channels" format (object with track name keys) + if(!json[m_sfmaJSONObjectName].isNull() && json[m_sfmaJSONObjectName].isObject()) { - QString libraryItemMediaNameNoExt = XFileUtil::getNameNoExtension(libraryItemMediaPath); auto jsonTracks = json[m_sfmaJSONObjectName].toObject(); auto channels = TCodeChannelLookup::getChannels(); foreach(QString channelName, channels) @@ -645,9 +674,52 @@ QList FunscriptHandler::getSFMATracks(QString libraryItemMediaPath) } } } + + // Check "axes" format (array of {id, actions} objects) + if(json.contains(m_axesJSONObjectName) && json[m_axesJSONObjectName].isArray()) + { + auto jsonAxes = json[m_axesJSONObjectName].toArray(); + for(auto val : jsonAxes) + { + QJsonObject axisObj = val.toObject(); + if(axisObj.contains("id") && axisObj.contains("actions")) + { + Track track = trackFromTCodeChannel(axisObj["id"].toString()); + if(track != Track::None && track != Track::Stroke) + { + ChannelModel33* channel = TCodeChannelLookup::getChannel(TCodeChannelLookup::ToString(track)); + if(channel && channel->Type != ChannelType::HalfOscillate) + scriptInfos.append({libraryItemMediaNameNoExt, libraryItemMediaNameNoExt, scriptPath, channel->trackName.isEmpty() ? axisObj["id"].toString() : channel->trackName, ScriptType::MAIN, ScriptContainerType::SFMA, "" }); + } + } + } + } + return scriptInfos; } +Track FunscriptHandler::trackFromTCodeChannel(const QString& tcodeChannel) +{ + // Strip modifier suffix (+/-) to get base channel name + QString base = tcodeChannel; + if(base.endsWith('+') || base.endsWith('-')) + base.chop(1); + + // Search the TCode version map for the matching Track + auto values = TCodeChannelLookup::GetSelectedVersionMap().values(); + auto keys = TCodeChannelLookup::GetSelectedVersionMap().keys(); + for(int i = 0; i < keys.length(); i++) + { + QString val = values[i]; + // Strip modifiers from map value too for comparison + if(val.endsWith('+') || val.endsWith('-')) + val.chop(1); + if(val == base) + return keys[i]; + } + return Track::None; +} + bool FunscriptHandler::isSFMA(QString libraryItemMediaPath) { QString scriptPath = XFileUtil::getPathNoExtension(libraryItemMediaPath) + ".funscript"; @@ -655,19 +727,40 @@ bool FunscriptHandler::isSFMA(QString libraryItemMediaPath) if(bytes.isEmpty()) return false; QJsonObject json = readJson(bytes); - if(json.isEmpty() || json[m_sfmaJSONObjectName].isNull()) + if(json.isEmpty()) return false; - auto jsonTracks = json[m_sfmaJSONObjectName].toObject(); - auto channels = TCodeChannelLookup::getChannels(); - foreach(QString channelName, channels) + // Check "channels" format + if(!json[m_sfmaJSONObjectName].isNull() && json[m_sfmaJSONObjectName].isObject()) { - ChannelModel33* channel = TCodeChannelLookup::getChannel(channelName); - if(channel->Type == ChannelType::HalfOscillate || channel->track == Track::Stroke) - continue; - if(jsonTracks.contains(channel->trackName)) - return true; + auto jsonTracks = json[m_sfmaJSONObjectName].toObject(); + auto channels = TCodeChannelLookup::getChannels(); + foreach(QString channelName, channels) + { + ChannelModel33* channel = TCodeChannelLookup::getChannel(channelName); + if(channel->Type == ChannelType::HalfOscillate || channel->track == Track::Stroke) + continue; + if(jsonTracks.contains(channel->trackName)) + return true; + } + } + + // Check "axes" format + if(json.contains(m_axesJSONObjectName) && json[m_axesJSONObjectName].isArray()) + { + auto jsonAxes = json[m_axesJSONObjectName].toArray(); + for(auto val : jsonAxes) + { + QJsonObject axisObj = val.toObject(); + if(axisObj.contains("id") && axisObj.contains("actions")) + { + Track track = trackFromTCodeChannel(axisObj["id"].toString()); + if(track != Track::None && track != Track::Stroke) + return true; + } + } } + return false; } diff --git a/src/lib/handler/funscripthandler.h b/src/lib/handler/funscripthandler.h index 3a1b49f..1fea1d7 100644 --- a/src/lib/handler/funscripthandler.h +++ b/src/lib/handler/funscripthandler.h @@ -64,12 +64,14 @@ public slots: static QList getSFMATracks(QString libraryItemMediaPath); static bool isMFS(QString libraryItemMediaPath); static QList getMFSTracks(QString libraryItemMediaPath); + static Track trackFromTCodeChannel(const QString& tcodeChannel); private: static inline QMutex mutex; static inline QHash m_funscripts; static inline const QString m_sfmaJSONObjectName = "channels"; + static inline const QString m_axesJSONObjectName = "axes"; bool m_loaded = false; bool _firstActionExecuted; static inline int m_offset; diff --git a/src/lib/handler/medialibraryhandler.cpp b/src/lib/handler/medialibraryhandler.cpp index 31dc57d..cd0eff5 100644 --- a/src/lib/handler/medialibraryhandler.cpp +++ b/src/lib/handler/medialibraryhandler.cpp @@ -677,7 +677,7 @@ void MediaLibraryHandler::processMetadata(LibraryListItem27 &item, bool &metadat if(item.type != LibraryListItemType::PlaylistInternal) { auto isMFS = item.metadata.isMFS; - auto isSFMA = item.metadata.isMFS; + auto isSFMA = item.metadata.isSFMA; if(discoverMultiAxis(item)) { if((isMFS != item.metadata.isMFS) || (isSFMA != item.metadata.isSFMA)) @@ -1690,6 +1690,7 @@ bool MediaLibraryHandler::discoverMultiAxis(LibraryListItem27 &item) { LogHandler::Debug("Discover MFS: "+item.ID); QStringList funscripts = TCodeChannelLookup::getValidMFSExtensions(); item.metadata.isMFS = false; + item.metadata.isSFMA = false; item.metadata.toolTip.clear(); item.metadata.MFSScripts.clear(); item.metadata.MFSTracks.clear(); From 5172021fc1a2ccf20296daed94aca921148ae769 Mon Sep 17 00:00:00 2001 From: gerwang Date: Sat, 23 May 2026 05:09:06 -0500 Subject: [PATCH 2/2] Fix ODR violation: change mediaLibrarySettings to out-of-class definition GCC 14 with -mno-direct-extern-access can cause static inline variables to have different instances across shared library boundaries. This made mediaLibrarySettings appear null in the XTPlayer executable even after being set by libxtengine.so, causing crashes at startup and when closing the welcome dialog. Changed from static inline (header-only, C++17) to a traditional static declaration in the header with a single out-of-class definition in settingshandler.cpp, so the symbol lives in exactly one place in libxtengine.so. --- src/lib/handler/settingshandler.cpp | 2 ++ src/lib/handler/settingshandler.h | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/handler/settingshandler.cpp b/src/lib/handler/settingshandler.cpp index ad70916..3a3fc35 100644 --- a/src/lib/handler/settingshandler.cpp +++ b/src/lib/handler/settingshandler.cpp @@ -4,6 +4,8 @@ #include "../tool/migration.h" +MediaLibrarySettings* SettingsHandler::mediaLibrarySettings = nullptr; + const QString SettingsHandler::XTEVersion = "0.6b"; const float SettingsHandler::XTEVersionNum = 0.6f; const QString SettingsHandler::XTEVersionTimeStamp = QString(XTEVersion +" %1T%2").arg(__DATE__).arg(__TIME__); diff --git a/src/lib/handler/settingshandler.h b/src/lib/handler/settingshandler.h index e3bb9c8..2ad3b59 100644 --- a/src/lib/handler/settingshandler.h +++ b/src/lib/handler/settingshandler.h @@ -97,7 +97,7 @@ public slots: static const float XTEVersionNum; static bool getSettingsChanged(); - static inline MediaLibrarySettings* mediaLibrarySettings = 0; + static MediaLibrarySettings* mediaLibrarySettings; static bool getHideWelcomeScreen(); static void setHideWelcomeScreen(bool value);