diff --git a/Readme.md b/Readme.md
index da36cdf..2f73864 100644
--- a/Readme.md
+++ b/Readme.md
@@ -278,7 +278,7 @@ These version are not stable and this should be considered for advanced users on
-Allows users to temporarily move and rotate the center of the playspace. This allows to reach interaction elements that are just outside our real-world walls or otherwise inaccessible (e.g. when your playspace is smaller than the recommended one). Can also be used to discover the terrors that lie outside of the intended playspace (ever wondered what's behind the door in The Lab?).
+Allows users to temporarily move and rotate the center of the playspace. This allows reaching interaction elements that are just outside our real-world walls or otherwise inaccessible (e.g. when your playspace is smaller than the recommended one). Can also be used to discover the terrors that lie outside of the intended playspace (ever wondered what's behind the door in The Lab?).
- **Adjust Chaperone**: When enabled, the chaperone bounds stay accurate even when the playspace is moved or rotated (so noone gets hurt). Depending on chaperone mode this may or may not adjust with height.
- **Seated Recenter**: When clicked, applies the current location and angle of the headset to the offsets such that the user is brought to a centered position. Also applies the current headset height to the default seated height (not visible in offsets). This action will also be activated automatically when a seated application sends a reset center command. *Note: This button is only visible when running seated applications.*
@@ -347,7 +347,7 @@ Allows users to temporarily move and rotate the center of the playspace. This al
-- **Keyboard Utilities:** Some applications (mostly 2D-monitor applications with tackled-on/modded-in VR support) do not support the VR keyboard but require an actual physical keyboard for text input. This utilities allow to send emulated key strokes from VR keyboard input to such application to overcome this limitation. Does not work in desktop mode. The "Keyboard Input" button works as described [here](docs/specs/Keyboard_Manager-Parser_Spec.md).
+- **Keyboard Utilities:** Some applications (mostly 2D-monitor applications with tackled-on/modded-in VR support) do not support the VR keyboard but require an actual physical keyboard for text input. This utilities allow to send emulated key strokes from VR keyboard input to such application to overcome this limitation. Does not work in desktop mode. Adding vrcDebug=true to [utilitiesSettings] in the OpenVRAdvancedSettings.ini will display some buttons for VRChat debug overlay keybinds. The "Keyboard Input" button works as described [here](docs/specs/Keyboard_Manager-Parser_Spec.md).
- **Alarm Clock:** Just a simple alarm clock so you don't miss important appointments. VR can sure mess up perception of time. Does not work in desktop mode.
@@ -388,6 +388,9 @@ Allows users to temporarily move and rotate the center of the playspace. This al
- **Allow External App Chaperone Edits:** Incorporates changes to the chaperone and universe center made by other applications. These changes could come from anywhere, including apps with unpredictable interactions with Advanced Settings. Therefore, this option opens the potential for chaperone misalignment or corruption. However it also allows other chaperone tweaking tools to function in tandem with Advanced Settings. **This option should only be checked if required for compatibility with other apps.** *Note: Changes will only take effect when offsets and rotation are all zero.*
- **Old-Style Motion:** Uses the old system of writing the chaperone universe center and bounds to disk every frame. Use this option only if you experience issues with playspace motion such as snapping back to reset position after releasing a space-drag. This old mode is smooth on most systems but is in theory less performant than the normal method.
- **Universe-Centered Rotation:** Causes Rotation to be applied to Universe Center Rather than HMD (Disables offsets automatically compensating to pivot at the HMD).
+- **Enable Auto-set Seated Offsets During Seated Recenter:** Stops the Space Offsets tab offsets from moving you to the center during seated mode recentering. (In case the offset recentering interferes with position in some applications).
+- **Disable All Motion Features When in Seated Mode:** Use this if Advanced Settings causes strange positioning in a seated application. (Different apps handle seated mode in different ways, some are less compatible with Advanced Settings). Note: all Space Offsets and Motion tab features will be disabled.
+- **Disable App Vsync:** Allows setting a custom base update rate for Advanced Settings. (Might be useful on HMDs with very high or very low refresh rates).
# How to Compile
diff --git a/docs/screenshots/OffsetPage.png b/docs/screenshots/OffsetPage.png
index bdc3fa3..5c587ca 100644
Binary files a/docs/screenshots/OffsetPage.png and b/docs/screenshots/OffsetPage.png differ
diff --git a/docs/screenshots/SettingsPage.png b/docs/screenshots/SettingsPage.png
index 30314c7..badcdd7 100644
Binary files a/docs/screenshots/SettingsPage.png and b/docs/screenshots/SettingsPage.png differ
diff --git a/src/res/qml/PlayspacePage.qml b/src/res/qml/PlayspacePage.qml
index 749c9e4..a1e0b33 100644
--- a/src/res/qml/PlayspacePage.qml
+++ b/src/res/qml/PlayspacePage.qml
@@ -8,6 +8,69 @@ import "common"
MyStackViewPage {
headerText: "Space Offsets"
+ MyDialogOkPopup {
+ id: offsetMessageDialog
+ function showMessage(title, text) {
+ dialogTitle = title
+ dialogText = text
+ open()
+ }
+ }
+
+ MyDialogOkCancelPopup {
+ id: offsetDeleteProfileDialog
+ property int profileIndex: -1
+ dialogTitle: "Delete Profile"
+ dialogText: "Do you really want to delete this profile?"
+ onClosed: {
+ if (okClicked) {
+ MoveCenterTabController.deleteOffsetProfile(profileIndex)
+ }
+ }
+ }
+
+ MyDialogOkCancelPopup {
+ id: offsetNewProfileDialog
+ dialogTitle: "Create New Profile"
+ dialogWidth: 800
+ dialogHeight: 400
+ dialogContentItem: ColumnLayout {
+ RowLayout {
+ Layout.topMargin: 16
+ Layout.leftMargin: 16
+ Layout.rightMargin: 16
+ MyText {
+ text: "Name: "
+ }
+ MyTextField {
+ id: offsetNewProfileName
+ keyBoardUID: 105
+ color: "#cccccc"
+ text: ""
+ Layout.fillWidth: true
+ font.pointSize: 20
+ function onInputEvent(input) {
+ offsetNewProfileName.text = input
+ }
+ }
+ }
+ }
+ onClosed: {
+ if (okClicked) {
+ if (offsetNewProfileName.text == "") {
+ offsetMessageDialog.showMessage("Create New Profile", "ERROR: Empty profile name.")
+ } else {
+ MoveCenterTabController.addOffsetProfile(offsetNewProfileName.text)
+ }
+
+ }
+ }
+ function openPopup() {
+ offsetNewProfileName.text = ""
+ open()
+ }
+ }
+
content: ColumnLayout {
spacing: 18
@@ -29,6 +92,74 @@ MyStackViewPage {
text: "Settings Tab: 'Disable Motion in Seated Mode' checked"
}
+ ColumnLayout {
+ Layout.bottomMargin: 32
+ spacing: 18
+ RowLayout {
+ spacing: 18
+
+ MyText {
+ text: "Profile:"
+ }
+
+ MyComboBox {
+ id: offsetProfileComboBox
+ Layout.maximumWidth: 799
+ Layout.minimumWidth: 799
+ Layout.preferredWidth: 799
+ Layout.fillWidth: true
+ model: [""]
+ onCurrentIndexChanged: {
+ if (currentIndex > 0) {
+ offsetApplyProfileButton.enabled = true
+ offsetDeleteProfileButton.enabled = true
+ } else {
+ offsetApplyProfileButton.enabled = false
+ offsetDeleteProfileButton.enabled = false
+ }
+ }
+ }
+
+ MyPushButton {
+ id: offsetApplyProfileButton
+ enabled: false
+ Layout.preferredWidth: 200
+ text: "Apply"
+ onClicked: {
+ if (offsetProfileComboBox.currentIndex > 0) {
+ MoveCenterTabController.applyOffsetProfile(offsetProfileComboBox.currentIndex - 1)
+ offsetProfileComboBox.currentIndex = 0
+ }
+ }
+ }
+ }
+ RowLayout {
+ spacing: 18
+ Item {
+ Layout.fillWidth: true
+ }
+ MyPushButton {
+ id: offsetDeleteProfileButton
+ enabled: false
+ Layout.preferredWidth: 200
+ text: "Delete Profile"
+ onClicked: {
+ if (offsetProfileComboBox.currentIndex > 0) {
+ offsetDeleteProfileDialog.profileIndex = offsetProfileComboBox.currentIndex - 1
+ offsetDeleteProfileDialog.open()
+ }
+ }
+ }
+ MyPushButton {
+ Layout.preferredWidth: 200
+ text: "New Profile"
+ onClicked: {
+ offsetNewProfileDialog.openPopup()
+ }
+ }
+ }
+ }
+
GroupBox {
Layout.fillWidth: true
id: offsetsGroupBox
@@ -416,6 +547,7 @@ MyStackViewPage {
resetButtonRow.visible = false
seatedDisableWarningText.visible = false
}
+ reloadOffsetProfiles()
}
Connections {
@@ -481,8 +613,19 @@ MyStackViewPage {
seatedDisableWarningText.visible = false
}
}
+ onOffsetProfilesUpdated: {
+ reloadOffsetProfiles()
+ }
}
}
-
+ function reloadOffsetProfiles() {
+ var profiles = [""]
+ var profileCount = MoveCenterTabController.getOffsetProfileCount()
+ for (var i = 0; i < profileCount; i++) {
+ profiles.push(MoveCenterTabController.getOffsetProfileName(i))
+ }
+ offsetProfileComboBox.currentIndex = 0
+ offsetProfileComboBox.model = profiles
+ }
}
diff --git a/src/tabcontrollers/MoveCenterTabController.cpp b/src/tabcontrollers/MoveCenterTabController.cpp
index 1ff207c..62691e1 100644
--- a/src/tabcontrollers/MoveCenterTabController.cpp
+++ b/src/tabcontrollers/MoveCenterTabController.cpp
@@ -165,6 +165,7 @@ void MoveCenterTabController::initStage1()
m_disableSeatedMotion = value.toBool();
}
settings->endGroup();
+ reloadOffsetProfiles();
m_lastDragUpdateTimePoint = std::chrono::steady_clock::now();
m_lastGravityUpdateTimePoint = std::chrono::steady_clock::now();
}
@@ -176,6 +177,126 @@ void MoveCenterTabController::initStage2( OverlayController* var_parent )
outputLogSettings();
}
+void MoveCenterTabController::reloadOffsetProfiles()
+{
+ m_offsetProfiles.clear();
+ auto settings = OverlayController::appSettings();
+ settings->beginGroup( "playspaceSettings" );
+ auto profileCount = settings->beginReadArray( "offsetProfiles" );
+ for ( int i = 0; i < profileCount; i++ )
+ {
+ settings->setArrayIndex( i );
+ m_offsetProfiles.emplace_back();
+ auto& entry = m_offsetProfiles[static_cast( i )];
+ entry.profileName
+ = settings->value( "profileName" ).toString().toStdString();
+ entry.offsetX = settings->value( "offsetX", 0.0f ).toFloat();
+ entry.offsetY = settings->value( "offsetY", 0.0f ).toFloat();
+ entry.offsetZ = settings->value( "offsetZ", 0.0f ).toFloat();
+ entry.rotation = settings->value( "rotation", 0 ).toInt();
+ }
+ settings->endArray();
+ settings->endGroup();
+}
+
+void MoveCenterTabController::saveOffsetProfiles()
+{
+ auto settings = OverlayController::appSettings();
+ settings->beginGroup( "playspaceSettings" );
+ settings->beginWriteArray( "offsetProfiles" );
+ unsigned i = 0;
+ for ( auto& p : m_offsetProfiles )
+ {
+ settings->setArrayIndex( static_cast( i ) );
+ settings->setValue( "profileName",
+ QString::fromStdString( p.profileName ) );
+ settings->setValue( "offsetX", p.offsetX );
+ settings->setValue( "offsetY", p.offsetY );
+ settings->setValue( "offsetZ", p.offsetZ );
+ settings->setValue( "rotation", p.rotation );
+ i++;
+ }
+ settings->endArray();
+ settings->endGroup();
+}
+
+Q_INVOKABLE unsigned MoveCenterTabController::getOffsetProfileCount()
+{
+ return static_cast( m_offsetProfiles.size() );
+}
+
+Q_INVOKABLE QString
+ MoveCenterTabController::getOffsetProfileName( unsigned index )
+{
+ if ( index >= m_offsetProfiles.size() )
+ {
+ return QString();
+ }
+ else
+ {
+ return QString::fromStdString( m_offsetProfiles[index].profileName );
+ }
+}
+
+void MoveCenterTabController::addOffsetProfile( QString name )
+{
+ OffsetProfile* profile = nullptr;
+ for ( auto& p : m_offsetProfiles )
+ {
+ if ( p.profileName.compare( name.toStdString() ) == 0 )
+ {
+ profile = &p;
+ break;
+ }
+ }
+ if ( !profile )
+ {
+ auto i = m_offsetProfiles.size();
+ m_offsetProfiles.emplace_back();
+ profile = &m_offsetProfiles[i];
+ }
+ profile->profileName = name.toStdString();
+ profile->offsetX = m_offsetX;
+ profile->offsetY = m_offsetY;
+ profile->offsetZ = m_offsetZ;
+ profile->rotation = m_rotation;
+ saveOffsetProfiles();
+ OverlayController::appSettings()->sync();
+ emit offsetProfilesUpdated();
+}
+
+void MoveCenterTabController::applyOffsetProfile( unsigned index )
+{
+ if ( index < m_offsetProfiles.size() )
+ {
+ auto& profile = m_offsetProfiles[index];
+ m_rotation = profile.rotation;
+ m_offsetX = profile.offsetX;
+ m_offsetY = profile.offsetY;
+ m_offsetZ = profile.offsetZ;
+ emit rotationChanged( m_rotation );
+ emit offsetXChanged( m_offsetX );
+ emit offsetYChanged( m_offsetY );
+ emit offsetZChanged( m_offsetZ );
+ LOG( INFO ) << "Applying Offset Profile:" << profile.profileName
+ << " X:" << m_offsetX << " Y:" << m_offsetY
+ << " Z:" << m_offsetZ << " Rotation:"
+ << ( static_cast( m_rotation ) / 100 );
+ }
+}
+
+void MoveCenterTabController::deleteOffsetProfile( unsigned index )
+{
+ if ( index < m_offsetProfiles.size() )
+ {
+ auto pos = m_offsetProfiles.begin() + index;
+ m_offsetProfiles.erase( pos );
+ saveOffsetProfiles();
+ OverlayController::appSettings()->sync();
+ emit offsetProfilesUpdated();
+ }
+}
+
void MoveCenterTabController::outputLogSettings()
{
if ( !m_adjustChaperone )
diff --git a/src/tabcontrollers/MoveCenterTabController.h b/src/tabcontrollers/MoveCenterTabController.h
index c2db884..bfdfef2 100644
--- a/src/tabcontrollers/MoveCenterTabController.h
+++ b/src/tabcontrollers/MoveCenterTabController.h
@@ -24,6 +24,15 @@ constexpr double k_maxOvrasUniverseCenteredTurningOffset = 25000.0;
class OverlayController;
+struct OffsetProfile
+{
+ std::string profileName;
+ float offsetX = 0.0f;
+ float offsetY = 0.0f;
+ float offsetZ = 0.0f;
+ int rotation = 0;
+};
+
class MoveCenterTabController : public QObject
{
Q_OBJECT
@@ -210,6 +219,8 @@ private:
void outputLogHmdMatrix( vr::HmdMatrix34_t hmdMatrix );
void outputLogSettings();
+ std::vector m_offsetProfiles;
+
public:
void initStage1();
void initStage2( OverlayController* parent );
@@ -252,6 +263,11 @@ public:
void resetHmdYawTotal();
void incomingSeatedReset();
+ void reloadOffsetProfiles();
+ void saveOffsetProfiles();
+ Q_INVOKABLE unsigned getOffsetProfileCount();
+ Q_INVOKABLE QString getOffsetProfileName( unsigned index );
+
// actions:
void leftHandSpaceDrag( bool leftHandDragActive );
void rightHandSpaceDrag( bool rightHandDragActive );
@@ -324,6 +340,10 @@ public slots:
void outputLogPoses();
void zeroOffsets();
+ void addOffsetProfile( QString name );
+ void applyOffsetProfile( unsigned index );
+ void deleteOffsetProfile( unsigned index );
+
signals:
void trackingUniverseChanged( int value );
void offsetXChanged( float value );
@@ -357,6 +377,8 @@ signals:
void universeCenteredRotationChanged( bool value );
void enableSeatedOffsetsRecenterChanged( bool value );
void disableSeatedMotionChanged( bool value );
+
+ void offsetProfilesUpdated();
};
} // namespace advsettings