SDL: gdk: Add SDL_GDKGetDefaultUser, SDL_GetPrefPath implementation

From c0cd8c8142b70c70da4dddc32244da6593cb6bb0 Mon Sep 17 00:00:00 2001
From: Ethan Lee <[EMAIL REDACTED]>
Date: Fri, 25 Aug 2023 10:39:39 -0400
Subject: [PATCH] gdk: Add SDL_GDKGetDefaultUser, SDL_GetPrefPath
 implementation

---
 VisualC-GDK/SDL/SDL.vcxproj                |  2 +-
 VisualC-GDK/SDL/SDL.vcxproj.filters        |  6 +-
 docs/README-gdk.md                         |  6 ++
 include/SDL3/SDL_system.h                  | 16 +++-
 src/core/gdk/SDL_gdk.cpp                   | 20 +++++
 src/filesystem/gdk/SDL_sysfilesystem.cpp   | 96 ++++++++++++++++++++++
 src/filesystem/windows/SDL_sysfilesystem.c | 20 -----
 7 files changed, 141 insertions(+), 25 deletions(-)
 create mode 100644 src/filesystem/gdk/SDL_sysfilesystem.cpp

diff --git a/VisualC-GDK/SDL/SDL.vcxproj b/VisualC-GDK/SDL/SDL.vcxproj
index fd9d84557ae6..f4419c9ee20d 100644
--- a/VisualC-GDK/SDL/SDL.vcxproj
+++ b/VisualC-GDK/SDL/SDL.vcxproj
@@ -582,7 +582,7 @@
     <ClCompile Include="..\..\src\events\SDL_touch.c" />
     <ClCompile Include="..\..\src\events\SDL_windowevents.c" />
     <ClCompile Include="..\..\src\file\SDL_rwops.c" />
-    <ClCompile Include="..\..\src\filesystem\windows\SDL_sysfilesystem.c" />
+    <ClCompile Include="..\..\src\filesystem\gdk\SDL_sysfilesystem.c" />
     <ClCompile Include="..\..\src\haptic\dummy\SDL_syshaptic.c" />
     <ClCompile Include="..\..\src\haptic\SDL_haptic.c" />
     <ClCompile Include="..\..\src\haptic\windows\SDL_dinputhaptic.c" />
diff --git a/VisualC-GDK/SDL/SDL.vcxproj.filters b/VisualC-GDK/SDL/SDL.vcxproj.filters
index 81dee5cc3d56..f55ad0342a17 100644
--- a/VisualC-GDK/SDL/SDL.vcxproj.filters
+++ b/VisualC-GDK/SDL/SDL.vcxproj.filters
@@ -31,7 +31,7 @@
     <Filter Include="filesystem">
       <UniqueIdentifier>{377061e4-3856-4f05-b916-0d3b360df0f6}</UniqueIdentifier>
     </Filter>
-    <Filter Include="filesystem\windows">
+    <Filter Include="filesystem\gdk">
       <UniqueIdentifier>{226a6643-1c65-4c7f-92aa-861313d974bb}</UniqueIdentifier>
     </Filter>
     <Filter Include="haptic">
@@ -916,8 +916,8 @@
     <ClCompile Include="..\..\src\file\SDL_rwops.c">
       <Filter>file</Filter>
     </ClCompile>
-    <ClCompile Include="..\..\src\filesystem\windows\SDL_sysfilesystem.c">
-      <Filter>filesystem\windows</Filter>
+    <ClCompile Include="..\..\src\filesystem\gdk\SDL_sysfilesystem.c">
+      <Filter>filesystem\gdk</Filter>
     </ClCompile>
     <ClCompile Include="..\..\src\haptic\SDL_haptic.c">
       <Filter>haptic</Filter>
diff --git a/docs/README-gdk.md b/docs/README-gdk.md
index a785e653047b..fefe16357941 100644
--- a/docs/README-gdk.md
+++ b/docs/README-gdk.md
@@ -29,6 +29,12 @@ The Windows GDK port supports the full set of Win32 APIs, renderers, controllers
   * Global task queue callbacks are dispatched during `SDL_PumpEvents` (which is also called internally if using `SDL_PollEvent`).
   * You can get the handle of the global task queue through `SDL_GDKGetTaskQueue`, if needed. When done with the queue, be sure to use `XTaskQueueCloseHandle` to decrement the reference count (otherwise it will cause a resource leak).
 
+* Single-player games have some additional features available:
+  * Call `SDL_GDKGetDefaultUser` to get the default XUserHandle pointer.
+  * `SDL_GetPrefPath` still works, but only for single-player titles.
+
+These functions mostly wrap around async APIs, and thus should be treated as synchronous alternatives. Also note that the single-player functions return on any OS errors, so be sure to validate the return values!
+
 * What doesn't work:
   * Compilation with anything other than through the included Visual C++ solution file
 
diff --git a/include/SDL3/SDL_system.h b/include/SDL3/SDL_system.h
index b934066f8339..98d67c96127a 100644
--- a/include/SDL3/SDL_system.h
+++ b/include/SDL3/SDL_system.h
@@ -623,7 +623,8 @@ extern DECLSPEC void SDLCALL SDL_OnApplicationDidChangeStatusBarOrientation(void
 
 /* Functions used only by GDK */
 #ifdef __GDK__
-typedef struct XTaskQueueObject * XTaskQueueHandle;
+typedef struct XTaskQueueObject *XTaskQueueHandle;
+typedef struct XUser *XUserHandle;
 
 /**
  * Gets a reference to the global async task queue handle for GDK,
@@ -641,6 +642,19 @@ typedef struct XTaskQueueObject * XTaskQueueHandle;
  */
 extern DECLSPEC int SDLCALL SDL_GDKGetTaskQueue(XTaskQueueHandle * outTaskQueue);
 
+/**
+ * Gets a reference to the default user handle for GDK.
+ *
+ * This is effectively a synchronous version of XUserAddAsync, which always
+ * prefers the default user and allows a sign-in UI.
+ *
+ * \param outUserHandle a pointer to be filled in with the default user handle.
+ * \returns 0 if success, -1 if any error occurs.
+ *
+ * \since This function is available since SDL 2.28.0.
+ */
+extern DECLSPEC int SDLCALL SDL_GDKGetDefaultUser(XUserHandle * outUserHandle);
+
 #endif
 
 /* Ends C function definitions when using C++ */
diff --git a/src/core/gdk/SDL_gdk.cpp b/src/core/gdk/SDL_gdk.cpp
index d883a3c7bcc9..c2dd14e244d2 100644
--- a/src/core/gdk/SDL_gdk.cpp
+++ b/src/core/gdk/SDL_gdk.cpp
@@ -214,3 +214,23 @@ SDL_GDKSuspendComplete()
         SetEvent(plmSuspendComplete);
     }
 }
+
+extern "C" DECLSPEC int
+SDL_GDKGetDefaultUser(XUserHandle *outUserHandle)
+{
+    XAsyncBlock block = { 0 };
+    HRESULT result;
+
+    if (FAILED(result = XUserAddAsync(XUserAddOptions::AddDefaultUserAllowingUI, &block))) {
+        return WIN_SetErrorFromHRESULT("XUserAddAsync", result);
+    }
+
+    do {
+        result = XUserAddResult(&block, outUserHandle);
+    } while (result == E_PENDING);
+    if (FAILED(result)) {
+        return WIN_SetErrorFromHRESULT("XUserAddResult", result);
+    }
+
+    return 0;
+}
diff --git a/src/filesystem/gdk/SDL_sysfilesystem.cpp b/src/filesystem/gdk/SDL_sysfilesystem.cpp
new file mode 100644
index 000000000000..30a2b3489e55
--- /dev/null
+++ b/src/filesystem/gdk/SDL_sysfilesystem.cpp
@@ -0,0 +1,96 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2023 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "../../SDL_internal.h"
+
+#ifdef SDL_FILESYSTEM_XBOX
+
+/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+/* System dependent filesystem routines                                */
+
+#include "../../core/windows/SDL_windows.h"
+#include "SDL_hints.h"
+#include "SDL_system.h"
+#include "SDL_filesystem.h"
+#include <XGameSaveFiles.h>
+
+char *
+SDL_GetBasePath(void)
+{
+    return SDL_strdup("G:\\");
+}
+
+char *
+SDL_GetPrefPath(const char *org, const char *app)
+{
+    XUserHandle user = NULL;
+    XAsyncBlock block = { 0 };
+    char *folderPath;
+    HRESULT result;
+    const char *csid = SDL_GetHint("SDL_GDK_SERVICE_CONFIGURATION_ID");
+    
+    if (app == NULL) {
+        SDL_InvalidParamError("app");
+        return NULL;
+    }
+
+    /* This should be set before calling SDL_GetPrefPath! */
+    if (csid == NULL) {
+        SDL_LogWarn(SDL_LOG_CATEGORY_SYSTEM, "Set SDL_GDK_SERVICE_CONFIGURATION_ID before calling SDL_GetPrefPath!");
+        return SDL_strdup("T:\\");
+    }
+
+    if (SDL_GDKGetDefaultUser(&user) < 0) {
+        /* Error already set, just return */
+        return NULL;
+    }
+
+    if (FAILED(result = XGameSaveFilesGetFolderWithUiAsync(user, csid, &block))) {
+        WIN_SetErrorFromHRESULT("XGameSaveFilesGetFolderWithUiAsync", result);
+        return NULL;
+    }
+
+    folderPath = (char*) SDL_malloc(MAX_PATH);
+    do {
+        result = XGameSaveFilesGetFolderWithUiResult(&block, MAX_PATH, folderPath);
+    } while (result == E_PENDING);
+    if (FAILED(result)) {
+        WIN_SetErrorFromHRESULT("XGameSaveFilesGetFolderWithUiResult", result);
+        SDL_free(folderPath);
+        return NULL;
+    }
+
+    /* We aren't using 'app' here because the container rules are a lot more
+     * strict than the NTFS rules, so it will most likely be invalid :(
+     */
+    SDL_strlcat(folderPath, "\\SDLPrefPath\\", MAX_PATH);
+    if (CreateDirectoryA(folderPath, NULL) == FALSE) {
+        if (GetLastError() != ERROR_ALREADY_EXISTS) {
+            WIN_SetError("CreateDirectoryA");
+            SDL_free(folderPath);
+            return NULL;
+        }
+    }
+    return folderPath;
+}
+
+#endif /* SDL_FILESYSTEM_XBOX */
+
+/* vi: set ts=4 sw=4 expandtab: */
diff --git a/src/filesystem/windows/SDL_sysfilesystem.c b/src/filesystem/windows/SDL_sysfilesystem.c
index 2dffe4c2a2a1..3027fb00e2b0 100644
--- a/src/filesystem/windows/SDL_sysfilesystem.c
+++ b/src/filesystem/windows/SDL_sysfilesystem.c
@@ -332,23 +332,3 @@ char *SDL_GetUserFolder(SDL_Folder folder)
     return retval;
 }
 #endif /* SDL_FILESYSTEM_WINDOWS */
-
-#ifdef SDL_FILESYSTEM_XBOX
-char *SDL_GetBasePath(void)
-{
-    SDL_Unsupported();
-    return NULL;
-}
-
-char *SDL_GetPrefPath(const char *org, const char *app)
-{
-    SDL_Unsupported();
-    return NULL;
-}
-
-char *SDL_GetUserFolder(SDL_Folder folder)
-{
-    SDL_Unsupported();
-    return NULL;
-}
-#endif /* SDL_FILESYSTEM_XBOX */