diff --git a/Samples/System/DeepLinking/Assets/LargeLogo.png b/Samples/System/DeepLinking/Assets/LargeLogo.png new file mode 100644 index 00000000..416dacd8 Binary files /dev/null and b/Samples/System/DeepLinking/Assets/LargeLogo.png differ diff --git a/Samples/System/DeepLinking/Assets/Logo.png b/Samples/System/DeepLinking/Assets/Logo.png new file mode 100644 index 00000000..4d646675 Binary files /dev/null and b/Samples/System/DeepLinking/Assets/Logo.png differ diff --git a/Samples/System/DeepLinking/Assets/SmallLogo.png b/Samples/System/DeepLinking/Assets/SmallLogo.png new file mode 100644 index 00000000..df5aa71e Binary files /dev/null and b/Samples/System/DeepLinking/Assets/SmallLogo.png differ diff --git a/Samples/System/DeepLinking/Assets/SplashScreen.png b/Samples/System/DeepLinking/Assets/SplashScreen.png new file mode 100644 index 00000000..f094c150 Binary files /dev/null and b/Samples/System/DeepLinking/Assets/SplashScreen.png differ diff --git a/Samples/System/DeepLinking/Assets/StoreLogo.png b/Samples/System/DeepLinking/Assets/StoreLogo.png new file mode 100644 index 00000000..f4407ee6 Binary files /dev/null and b/Samples/System/DeepLinking/Assets/StoreLogo.png differ diff --git a/Samples/System/DeepLinking/DeepLinking.cpp b/Samples/System/DeepLinking/DeepLinking.cpp new file mode 100644 index 00000000..e9424243 --- /dev/null +++ b/Samples/System/DeepLinking/DeepLinking.cpp @@ -0,0 +1,285 @@ +//-------------------------------------------------------------------------------------- +// DeepLinking.cpp +// +// Sample implementation +// +// Advanced Technology Group (ATG) +// Copyright (C) Microsoft Corporation. All rights reserved. +//-------------------------------------------------------------------------------------- + +#include "pch.h" +#include "imgui.h" +#include "imgui/imgui_atg.h" +#include "imgui/imgui_atg_device_context.h" +#include "DeepLinking.h" +#include + +namespace +{ + void DrawHeader(ImVec4 color, const char* text) + { + ImGui::PushFont(nullptr, 30.0f); + + // Set cursor to the middle (minus half the text width) and draw the text + ImGui::SetCursorPosX((ImGui::GetWindowSize().x - ImGui::CalcTextSize(text).x) * 0.5f); + ImGui::TextColored(color, text); + + ImGui::PopFont(); + } +} + +void Sample::Initialize(HWND /*hWnd*/) +{ + HRESULT hr = XGameActivationRegisterForEvent(nullptr, this, [](void* context, const XGameActivationInfo* activationInfo) + { + // only handle Protocol Activations in this sample + if (activationInfo->type != XGameActivationType::Protocol) + { + LOG("Received activation that was not a protocol activation, ignoring.\n"); + return; + } + + Sample* sample = static_cast(context); + + // Deep Link activations are started as: + // ms-windows-store://launch?productid=PRODID&path=PATH + // ...but arrive to the title as: + // ms-xbl-TITLEID://PATH/ + + // Additional querystring params must be urlencoded on the source side, but arrive decoded on the title side: + // Ensure that the querystring is appended to the path as "%2F%3Fkey1%3Dvalue1%26key2%3Dvalue2" on the source side + // This will arrive as "/?key1=value1&key2=value2" on the title side. + // ms-windows-store://launch?productId=PRODID&path=store%2F%3Foffer%3D1234%26key%3Dvalue" + // ...but arrives to the title as: + // ms-xbl-TITLEID://store/?offer=1234&key=value + // %26 & + // %2F / + // %3F ? + // %3D = + + std::string uri = activationInfo->protocolUri; + + // find the start of the path/verb portion of the uri + auto pos = uri.find("://"); + if (pos == std::string::npos) + { + LOG("Could not parse protocol URI: %s\n", uri.c_str()); + return; + } + pos += 3; + + // split the remainder into "path" and "querystring" on the first '?' + std::string rest = uri.substr(pos); + std::string path; + std::string querystring; + + auto queryPos = rest.find('?'); + if (queryPos == std::string::npos) + { + path = rest; + } + else + { + path = rest.substr(0, queryPos); + querystring = rest.substr(queryPos + 1); + } + + // strip a trailing '/' from the path (the platform appends one when there is no querystring) + if (!path.empty() && path.back() == '/') + { + path.pop_back(); + } + + // walk the querystring, splitting "key1=value1&key2=value2" into a map + std::map queryStrings; + for (size_t start = 0; start < querystring.size(); ) + { + size_t amp = querystring.find('&', start); + if (amp == std::string::npos) + { + amp = querystring.size(); + } + + std::string pair = querystring.substr(start, amp - start); + auto eq = pair.find('='); + if (eq != std::string::npos) + { + queryStrings[pair.substr(0, eq)] = pair.substr(eq + 1); + } + else if (!pair.empty()) + { + queryStrings[pair] = ""; + } + + start = amp + 1; + } + + // tell the sample to navigate to that path + sample->NavigateTo(uri, path, queryStrings); + + }, &m_activationToken); + + LOG("XGameActivationRegisterForEvent: 0x%08X\n", hr); +} + +void Sample::NavigateTo(const std::string& uri, const std::string& path, const std::map& queryStrings) +{ + // cache the uri/path/querystrings for display later + m_uri = uri; + m_path = path; + m_queryStrings = queryStrings; + + // look up the path in our map to determine which mode to enter + auto it = s_pathToGameMode.find(path); + m_mode = (it != s_pathToGameMode.end()) ? it->second : GameMode::None; + LOG("Navigating to path: '%s' (mode %d), %zu querystring param(s)\n", path.c_str(), static_cast(m_mode), queryStrings.size()); +} + +void Sample::Update() +{ +} + +void Sample::Draw() +{ + ImGuiAtg::BeginFullscreenLayout(); + + ImGui::CollapsingHeader("ATG DeepLinking Sample", ImGuiTreeNodeFlags_Leaf); + + // Reserve space for the standard sample footer below the split + float footerH = ImGuiAtg::GetFooterHeight(); + ImGui::BeginChild("##SplitArea", ImVec2(0, ImGui::GetContentRegionAvail().y - footerH)); + + // Content on left, log on right, with draggable splitter + ImGuiAtg::BeginSplitH("##LogSplit", 300.0f); + ImGui::BeginChild("##Content", ImVec2(0, ImGui::GetContentRegionAvail().y)); + + switch(m_mode) + { + case GameMode::None: + DrawHeader(ImVec4(0, 1, 0, 1), "TITLE SCREEN"); + ImGui::Separator(); + ImGui::Text("This is the title screen."); + break; + case GameMode::Lobby: + DrawHeader(ImVec4(0, 1, 1, 1), "LOBBY"); + ImGui::Separator(); + ImGui::Text("This is the lobby screen."); + break; + case GameMode::Campaign: + DrawHeader(ImVec4(1, 0, 0, 1), "CAMPAIGN"); + ImGui::Separator(); + ImGui::Text("This is campaign/single player mode screen."); + break; + case GameMode::Shop: + DrawHeader(ImVec4(1, 0, 1, 1), "SHOP"); + ImGui::Separator(); + ImGui::Text("This is the in-game shop screen."); + + // example of pulling specific values out of the parsed querystring + auto idIt = m_queryStrings.find("id"); + if (idIt != m_queryStrings.end()) + { + ImGui::Text("Showing item id: %s", idIt->second.c_str()); + } + else + { + ImGui::TextDisabled("(no 'id' querystring parameter was provided)"); + } + + auto qtyIt = m_queryStrings.find("qty"); + if (qtyIt != m_queryStrings.end()) + { + ImGui::Text("Quantity: %s", qtyIt->second.c_str()); + } + else + { + ImGui::TextDisabled("(no 'qty' querystring parameter was provided)"); + } + break; + } + + ImGui::Dummy(ImVec2(0, 100)); + + ImGui::CollapsingHeader("Debug Info", ImGuiTreeNodeFlags_Leaf); + + ImGui::Text("Full URI:"); + ImGui::SameLine(); + if (m_uri.empty()) + { + ImGui::TextDisabled("(none)"); + } + else + { + ImGui::TextWrapped("%s", m_uri.c_str()); + } + + ImGui::Text("Parsed Path:"); + ImGui::SameLine(); + if (m_path.empty()) + { + ImGui::TextDisabled("(none)"); + } + else + { + ImGui::TextWrapped("%s", m_path.c_str()); + } + + ImGui::Text("Query String:"); + if (m_queryStrings.empty()) + { + ImGui::SameLine(); + ImGui::TextDisabled("(none)"); + } + else + { + ImGui::Indent(); + for (const auto& kv : m_queryStrings) + { + ImGui::Text("%s = %s", kv.first.c_str(), kv.second.c_str()); + } + ImGui::Unindent(); + } + + ImGui::EndChild(); + + ImGuiAtg::SplitNext(); + ImGuiAtg::DrawLogPanel(); + ImGuiAtg::EndSplit(); + + ImGui::EndChild(); + + ImGuiAtg::DrawFooter(); + + ImGuiAtg::EndFullscreenLayout(); +} + +void Sample::Shutdown() +{ + // Wait (true) to ensure no in-flight activation callbacks can still reference `this` during/after shutdown. + XGameActivationUnregisterForEvent(m_activationToken, true); +} + +void Sample::Activated() +{ +} + +void Sample::Deactivated() +{ +} + +LRESULT Sample::WndProcHandler(HWND /*hWnd*/, UINT /*msg*/, WPARAM /*wParam*/, LPARAM /*lParam*/) +{ + return 0; +} + +#ifdef _GAMING_XBOX +void Sample::Suspend(ImGuiAtg::DeviceContext* dc) +{ + dc->Suspend(); +} + +void Sample::Resume(ImGuiAtg::DeviceContext* dc) +{ + dc->Resume(); +} +#endif diff --git a/Samples/System/DeepLinking/DeepLinking.h b/Samples/System/DeepLinking/DeepLinking.h new file mode 100644 index 00000000..714494cd --- /dev/null +++ b/Samples/System/DeepLinking/DeepLinking.h @@ -0,0 +1,66 @@ +//-------------------------------------------------------------------------------------- +// DeepLinking.h +// +// Header for sample +// +// Advanced Technology Group (ATG) +// Copyright (C) Microsoft Corporation. All rights reserved. +//-------------------------------------------------------------------------------------- + +#pragma once + +#include +#include +#include +#include + +namespace ImGuiAtg { class DeviceContext; } + +class Sample +{ +public: + enum class GameMode + { + None, + Lobby, + Campaign, + Shop, + }; + + Sample() = default; + ~Sample() = default; + + Sample(Sample const&) = delete; + Sample& operator= (Sample const&) = delete; + + void Initialize(HWND hWnd); + void Update(); + void Draw(); + void Shutdown(); + void Activated(); + void Deactivated(); + LRESULT WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); + +#ifdef _GAMING_XBOX + void Suspend(ImGuiAtg::DeviceContext* dc); + void Resume(ImGuiAtg::DeviceContext* dc); +#endif + +private: + void NavigateTo(const std::string& uri, const std::string& path, const std::map& queryStrings); + + // The lookup table is shared across instances and never mutated after construction. + static inline const std::unordered_map s_pathToGameMode + { + { "none", GameMode::None }, + { "lobby", GameMode::Lobby }, + { "campaign", GameMode::Campaign }, + { "shop", GameMode::Shop }, + }; + + XTaskQueueRegistrationToken m_activationToken{}; + std::string m_uri; + std::string m_path; + std::map m_queryStrings; + GameMode m_mode = GameMode::None; +}; diff --git a/Samples/System/DeepLinking/DeepLinking.sln b/Samples/System/DeepLinking/DeepLinking.sln new file mode 100644 index 00000000..374338ec --- /dev/null +++ b/Samples/System/DeepLinking/DeepLinking.sln @@ -0,0 +1,42 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36203.30 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DeepLinking", "DeepLinking.vcxproj", "{F3F4F56D-A581-4CF5-829D-6A4062CE05D7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Gaming.Xbox.Scarlett.x64 = Debug|Gaming.Xbox.Scarlett.x64 + Debug|Gaming.Xbox.XboxOne.x64 = Debug|Gaming.Xbox.XboxOne.x64 + Profile|Gaming.Xbox.Scarlett.x64 = Profile|Gaming.Xbox.Scarlett.x64 + Profile|Gaming.Xbox.XboxOne.x64 = Profile|Gaming.Xbox.XboxOne.x64 + Release|Gaming.Xbox.Scarlett.x64 = Release|Gaming.Xbox.Scarlett.x64 + Release|Gaming.Xbox.XboxOne.x64 = Release|Gaming.Xbox.XboxOne.x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F3F4F56D-A581-4CF5-829D-6A4062CE05D7}.Debug|Gaming.Xbox.Scarlett.x64.ActiveCfg = Debug|Gaming.Xbox.Scarlett.x64 + {F3F4F56D-A581-4CF5-829D-6A4062CE05D7}.Debug|Gaming.Xbox.Scarlett.x64.Build.0 = Debug|Gaming.Xbox.Scarlett.x64 + {F3F4F56D-A581-4CF5-829D-6A4062CE05D7}.Debug|Gaming.Xbox.Scarlett.x64.Deploy.0 = Debug|Gaming.Xbox.Scarlett.x64 + {F3F4F56D-A581-4CF5-829D-6A4062CE05D7}.Debug|Gaming.Xbox.XboxOne.x64.ActiveCfg = Debug|Gaming.Xbox.XboxOne.x64 + {F3F4F56D-A581-4CF5-829D-6A4062CE05D7}.Debug|Gaming.Xbox.XboxOne.x64.Build.0 = Debug|Gaming.Xbox.XboxOne.x64 + {F3F4F56D-A581-4CF5-829D-6A4062CE05D7}.Debug|Gaming.Xbox.XboxOne.x64.Deploy.0 = Debug|Gaming.Xbox.XboxOne.x64 + {F3F4F56D-A581-4CF5-829D-6A4062CE05D7}.Profile|Gaming.Xbox.Scarlett.x64.ActiveCfg = Profile|Gaming.Xbox.Scarlett.x64 + {F3F4F56D-A581-4CF5-829D-6A4062CE05D7}.Profile|Gaming.Xbox.Scarlett.x64.Build.0 = Profile|Gaming.Xbox.Scarlett.x64 + {F3F4F56D-A581-4CF5-829D-6A4062CE05D7}.Profile|Gaming.Xbox.Scarlett.x64.Deploy.0 = Profile|Gaming.Xbox.Scarlett.x64 + {F3F4F56D-A581-4CF5-829D-6A4062CE05D7}.Profile|Gaming.Xbox.XboxOne.x64.ActiveCfg = Profile|Gaming.Xbox.XboxOne.x64 + {F3F4F56D-A581-4CF5-829D-6A4062CE05D7}.Profile|Gaming.Xbox.XboxOne.x64.Build.0 = Profile|Gaming.Xbox.XboxOne.x64 + {F3F4F56D-A581-4CF5-829D-6A4062CE05D7}.Profile|Gaming.Xbox.XboxOne.x64.Deploy.0 = Profile|Gaming.Xbox.XboxOne.x64 + {F3F4F56D-A581-4CF5-829D-6A4062CE05D7}.Release|Gaming.Xbox.Scarlett.x64.ActiveCfg = Release|Gaming.Xbox.Scarlett.x64 + {F3F4F56D-A581-4CF5-829D-6A4062CE05D7}.Release|Gaming.Xbox.Scarlett.x64.Build.0 = Release|Gaming.Xbox.Scarlett.x64 + {F3F4F56D-A581-4CF5-829D-6A4062CE05D7}.Release|Gaming.Xbox.Scarlett.x64.Deploy.0 = Release|Gaming.Xbox.Scarlett.x64 + {F3F4F56D-A581-4CF5-829D-6A4062CE05D7}.Release|Gaming.Xbox.XboxOne.x64.ActiveCfg = Release|Gaming.Xbox.XboxOne.x64 + {F3F4F56D-A581-4CF5-829D-6A4062CE05D7}.Release|Gaming.Xbox.XboxOne.x64.Build.0 = Release|Gaming.Xbox.XboxOne.x64 + {F3F4F56D-A581-4CF5-829D-6A4062CE05D7}.Release|Gaming.Xbox.XboxOne.x64.Deploy.0 = Release|Gaming.Xbox.XboxOne.x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {7BE5A19B-3A37-4F4B-9B1E-7F8B5C1D2E3F} + EndGlobalSection +EndGlobal diff --git a/Samples/System/DeepLinking/DeepLinking.vcxproj b/Samples/System/DeepLinking/DeepLinking.vcxproj new file mode 100644 index 00000000..5fb8346e --- /dev/null +++ b/Samples/System/DeepLinking/DeepLinking.vcxproj @@ -0,0 +1,468 @@ + + + + + Debug + Gaming.Xbox.Scarlett.x64 + + + Profile + Gaming.Xbox.Scarlett.x64 + + + Release + Gaming.Xbox.Scarlett.x64 + + + Debug + Gaming.Xbox.XboxOne.x64 + + + Profile + Gaming.Xbox.XboxOne.x64 + + + Release + Gaming.Xbox.XboxOne.x64 + + + + DeepLinking + {f3f4f56d-a581-4cf5-829d-6a4062ce05d7} + en-US + Win32Proj + true + 17.0 + Native + x64 + true + + + + + $(GameDKCoreLatest) + $(GameDKXboxLatest) + + + Application + v143 + false + true + Unicode + false + false + + + Application + v143 + false + true + Unicode + false + false + + + Application + v143 + false + true + Unicode + false + false + + + Application + v143 + false + true + Unicode + false + false + + + Application + v143 + true + Unicode + false + false + + + Application + v143 + true + Unicode + false + false + + + + + + + + + + + + + + + + + + + + + + + + + $(Console_SdkLibPath);$(Console_SdkWindowsMetadataPath) + $(Console_SdkLibPath) + $(Console_SdkLibPath);$(Console_SdkWindowsMetadataPath) + $(Console_SdkIncludeRoot) + $(Console_SdkRoot)bin;$(Console_SdkToolPath);$(ExecutablePath) + false + + + $(Console_SdkLibPath);$(Console_SdkWindowsMetadataPath) + $(Console_SdkLibPath) + $(Console_SdkLibPath);$(Console_SdkWindowsMetadataPath) + $(Console_SdkIncludeRoot) + $(Console_SdkRoot)bin;$(Console_SdkToolPath);$(ExecutablePath) + false + + + $(Console_SdkLibPath);$(Console_SdkWindowsMetadataPath) + $(Console_SdkLibPath) + $(Console_SdkLibPath);$(Console_SdkWindowsMetadataPath) + $(Console_SdkIncludeRoot) + $(Console_SdkRoot)bin;$(Console_SdkToolPath);$(ExecutablePath) + false + + + $(Console_SdkLibPath);$(Console_SdkWindowsMetadataPath) + $(Console_SdkLibPath) + $(Console_SdkLibPath);$(Console_SdkWindowsMetadataPath) + $(Console_SdkIncludeRoot) + $(Console_SdkRoot)bin;$(Console_SdkToolPath);$(ExecutablePath) + false + + + $(Console_SdkLibPath);$(Console_SdkWindowsMetadataPath) + $(Console_SdkLibPath) + $(Console_SdkLibPath);$(Console_SdkWindowsMetadataPath) + $(Console_SdkIncludeRoot) + $(Console_SdkRoot)bin;$(Console_SdkToolPath);$(ExecutablePath) + true + + + $(Console_SdkLibPath);$(Console_SdkWindowsMetadataPath) + $(Console_SdkLibPath) + $(Console_SdkLibPath);$(Console_SdkWindowsMetadataPath) + $(Console_SdkIncludeRoot) + $(Console_SdkRoot)bin;$(Console_SdkToolPath);$(ExecutablePath) + true + + + false + + + + appnotify.lib;$(Console_Libs);%(XboxExtensionsDependencies);%(AdditionalDependencies) + true + Windows + true + true + + + Use + pch.h + $(ProjectDir);..\..\..\Kits\OpenSource\imgui;..\..\..\Kits\ATGTK\;%(AdditionalIncludeDirectories) + MaxSpeed + NDEBUG;%(PreprocessorDefinitions) + Level4 + true + true + stdcpp17 + true + /Zc:__cplusplus /ZH:SHA_256 %(AdditionalOptions) + 5204;%(DisableSpecificWarnings) + + + -HV 2021 %(AdditionalOptions) + + + + + appnotify.lib;$(Console_Libs);%(XboxExtensionsDependencies);%(AdditionalDependencies) + true + Windows + true + true + + + Use + pch.h + $(ProjectDir);..\..\..\Kits\OpenSource\imgui;..\..\..\Kits\ATGTK\;%(AdditionalIncludeDirectories) + MaxSpeed + NDEBUG;%(PreprocessorDefinitions) + Level4 + true + true + stdcpp17 + true + /Zc:__cplusplus /ZH:SHA_256 %(AdditionalOptions) + 5204;%(DisableSpecificWarnings) + + + -HV 2021 %(AdditionalOptions) + + + + + appnotify.lib;$(Console_Libs);%(XboxExtensionsDependencies);%(AdditionalDependencies) + true + Windows + true + true + + + Use + pch.h + $(ProjectDir);..\..\..\Kits\OpenSource\imgui;..\..\..\Kits\ATGTK\;%(AdditionalIncludeDirectories) + MaxSpeed + NDEBUG;PROFILE;%(PreprocessorDefinitions) + Level4 + true + true + stdcpp17 + true + /Zc:__cplusplus /ZH:SHA_256 %(AdditionalOptions) + 5204;%(DisableSpecificWarnings) + + + -HV 2021 %(AdditionalOptions) + + + + + appnotify.lib;$(Console_Libs);%(XboxExtensionsDependencies);%(AdditionalDependencies) + true + Windows + true + true + + + Use + pch.h + $(ProjectDir);..\..\..\Kits\OpenSource\imgui;..\..\..\Kits\ATGTK\;%(AdditionalIncludeDirectories) + MaxSpeed + NDEBUG;PROFILE;%(PreprocessorDefinitions) + Level4 + true + true + stdcpp17 + true + /Zc:__cplusplus /ZH:SHA_256 %(AdditionalOptions) + 5204;%(DisableSpecificWarnings) + + + -HV 2021 %(AdditionalOptions) + + + + + appnotify.lib;$(Console_Libs);%(XboxExtensionsDependencies);%(AdditionalDependencies) + Windows + true + + + pch.h + Use + false + $(ProjectDir);..\..\..\Kits\OpenSource\imgui;..\..\..\Kits\ATGTK\;%(AdditionalIncludeDirectories) + Level4 + Disabled + _DEBUG;%(PreprocessorDefinitions) + stdcpp17 + true + /Zc:__cplusplus /ZH:SHA_256 %(AdditionalOptions) + 5204;%(DisableSpecificWarnings) + + + -HV 2021 %(AdditionalOptions) + + + + + appnotify.lib;$(Console_Libs);%(XboxExtensionsDependencies);%(AdditionalDependencies) + Windows + true + + + pch.h + Use + false + $(ProjectDir);..\..\..\Kits\OpenSource\imgui;..\..\..\Kits\ATGTK\;%(AdditionalIncludeDirectories) + Level4 + Disabled + _DEBUG;%(PreprocessorDefinitions) + stdcpp17 + true + /Zc:__cplusplus /ZH:SHA_256 %(AdditionalOptions) + 5204;%(DisableSpecificWarnings) + + + -HV 2021 %(AdditionalOptions) + + + + + $(OutDir)Assets + + + 5204;%(DisableSpecificWarnings) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + VCR001 + + + 4365 + + + + + + + + + NotUsing + 6011;6387;26819;28182;%(DisableSpecificWarnings) + + + NotUsing + 6011;6387;26819;28182;%(DisableSpecificWarnings) + + + + + NotUsing + + + NotUsing + + + NotUsing + 4365;6011;6387;26819;28182;%(DisableSpecificWarnings) + + + NotUsing + 4365;6011;6387;26819;28182;%(DisableSpecificWarnings) + + + NotUsing + 4365;6011;6387;26819;28182;%(DisableSpecificWarnings) + + + NotUsing + 4365;6011;6387;26819;28182;%(DisableSpecificWarnings) + + + Create + Create + Create + Create + Create + Create + + + + + true + true + true + + + true + true + true + + + + + true + + + + + true + + + + + true + + + + + true + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Samples/System/DeepLinking/DeepLinking.vcxproj.filters b/Samples/System/DeepLinking/DeepLinking.vcxproj.filters new file mode 100644 index 00000000..abdbc409 --- /dev/null +++ b/Samples/System/DeepLinking/DeepLinking.vcxproj.filters @@ -0,0 +1,170 @@ + + + + + {125b73c9-834a-47a7-917e-173f8e31a1dc} + ico;cur;bmp;dds;dlg;fbx;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tga;tiff;tif;png;wav;mfcribbon-ms + + + {d83d2aab-0aeb-48bb-9970-9b3de91d28fe} + + + {5539f2d3-8b11-4cc9-bde1-83d577be9448} + + + {a74d5981-df07-4232-b6c3-518e027f55f5} + + + {c715808d-3324-4b99-aecd-ab120743da99} + + + {83199e07-dff2-4569-83c5-43f08286201e} + + + + + + + ATG Tool Kit + + + ImGui + + + ImGui + + + ImGui + + + ImGui + + + ImGui + + + ImGui + + + ImGui\Backends + + + ImGui\Backends + + + ImGui\ATG + + + ImGui\ATG + + + ImGui\Backends\Shaders + + + ImGui\Backends\Shaders + + + ImGui\Backends\Shaders + + + ImGui\Backends\Shaders + + + ImGui\Backends\Shaders + + + ImGui\Backends\Shaders + + + ImGui\Backends\Shaders + + + ImGui\Backends\Shaders + + + ImGui\Backends\Shaders + + + ImGui\Backends\Shaders + + + ImGui\Backends\Shaders + + + ImGui\Backends\Shaders + + + ImGui\Backends\Shaders + + + ImGui\Backends\Shaders + + + ImGui\Backends\Shaders + + + ImGui\Backends\Shaders + + + ImGui\Backends\Shaders + + + ImGui\Backends\Shaders + + + + + + + + ImGui\ATG + + + ImGui\ATG + + + ImGui + + + ImGui + + + ImGui + + + ImGui + + + ImGui\Backends + + + ImGui\Backends + + + + + + + + + Assets + + + Assets + + + Assets + + + Assets + + + Assets + + + + + + + + \ No newline at end of file diff --git a/Samples/System/DeepLinking/Main.cpp b/Samples/System/DeepLinking/Main.cpp new file mode 100644 index 00000000..f4fe914f --- /dev/null +++ b/Samples/System/DeepLinking/Main.cpp @@ -0,0 +1,330 @@ +//-------------------------------------------------------------------------------------- +// Main.cpp +// +// Window setup and message loop +// +// Advanced Technology Group (ATG) +// Copyright (C) Microsoft Corporation. All rights reserved. +//-------------------------------------------------------------------------------------- + +#include "pch.h" +#include "imgui.h" +#include "backends/imgui_impl_win32.h" +#include "backends/imgui_impl_dx12.h" +#include "imgui/imgui_atg.h" +#include "DeepLinking.h" + +#include +#include +#include +#include + +namespace +{ + static constexpr int c_WindowWidth = 1920; + static constexpr int c_WindowHeight = 1080; + + static std::unique_ptr g_d3dDeviceContext; + static std::unique_ptr g_sample; + +#ifdef _GAMING_XBOX + HANDLE g_plmSuspendComplete = nullptr; + HANDLE g_plmSignalResume = nullptr; +#endif +} + +static LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); + +#ifndef _GAMING_XBOX +static void ToggleFullscreen(HWND hWnd) +{ + static bool isFullscreen = false; + static RECT windowRect = {}; + + if (isFullscreen) + { + // Restore windowed mode + SetWindowLongPtr(hWnd, GWL_STYLE, WS_OVERLAPPEDWINDOW | WS_VISIBLE); + SetWindowPos(hWnd, HWND_TOP, + windowRect.left, windowRect.top, + windowRect.right - windowRect.left, windowRect.bottom - windowRect.top, + SWP_FRAMECHANGED); + } + else + { + // Switch to borderless fullscreen + GetWindowRect(hWnd, &windowRect); + SetWindowLongPtr(hWnd, GWL_STYLE, WS_POPUP | WS_VISIBLE); + SetWindowPos(hWnd, HWND_TOP, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), SWP_FRAMECHANGED); + } + isFullscreen = !isFullscreen; +} +#endif + +// Main code +int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE /*hPrevInstance*/, _In_ LPWSTR /*lpCmdLine*/, _In_ int /*nCmdShow*/) +{ +#ifdef _GAMING_DESKTOP + // Set working directory to exe location + char dir[_MAX_PATH] = {}; + if (GetModuleFileNameA(nullptr, dir, _MAX_PATH) > 0) + { + std::string exe = dir; + exe = exe.substr(0, exe.find_last_of("\\")); + std::ignore = SetCurrentDirectoryA(exe.c_str()); + } +#endif + + HRESULT hr = XGameRuntimeInitialize(); + if (FAILED(hr)) + { +#ifdef _GAMING_DESKTOP + if (hr == E_GAMERUNTIME_DLL_NOT_FOUND || hr == E_GAMERUNTIME_VERSION_MISMATCH) + { + std::ignore = MessageBoxW(nullptr, L"Game Runtime is not installed on this system or needs updating.", L"DeepLinking", MB_ICONERROR | MB_OK); + } +#endif + return 1; + } + +#ifdef _GAMING_XBOX + SetThreadAffinityMask(GetCurrentThread(), 0x1); +#endif + + // Create application window +#ifndef _GAMING_XBOX + ImGui_ImplWin32_EnableDpiAwareness(); +#endif + + WNDCLASSEXW wc = { sizeof(wc), CS_CLASSDC, WndProc, 0L, 0L, hInstance, nullptr, nullptr, nullptr, nullptr, L"DeepLinking", nullptr }; + RegisterClassExW(&wc); + +#ifdef _GAMING_XBOX + int width = 1920; + int height = 1080; +#else + int width = c_WindowWidth; + int height = c_WindowHeight; +#endif + + HWND hWnd = CreateWindowW(wc.lpszClassName, L"DeepLinking", WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, CW_USEDEFAULT, width, height, nullptr, nullptr, wc.hInstance, nullptr); + + // Initialize Direct3D + g_d3dDeviceContext = std::make_unique(); + if (!g_d3dDeviceContext->CreateDevice(hWnd, width, height)) + { + UnregisterClassW(wc.lpszClassName, wc.hInstance); + return 1; + } + + // Show the window + ShowWindow(hWnd, SW_SHOWNORMAL); +#ifndef _GAMING_XBOX + UpdateWindow(hWnd); + SetForegroundWindow(hWnd); +#endif + + // Setup Dear ImGui context + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); (void)io; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls + io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls + io.IniFilename = nullptr; // Disable INI file creation + + // Setup Dear ImGui style + ImGuiAtg::SetAtgStyle(); +#ifndef _GAMING_XBOX + ImGuiAtg::SetDpiScale(hWnd); +#endif + + // Setup Platform/Renderer backends + ImGui_ImplWin32_Init(hWnd); + g_d3dDeviceContext->DX12_Init(); + + g_sample = std::make_unique(); + g_sample->Initialize(hWnd); + +#ifdef _GAMING_XBOX + // Setup PLM suspend/resume + PAPPSTATE_REGISTRATION hPLM = {}; + g_plmSuspendComplete = CreateEventEx(nullptr, nullptr, 0, EVENT_MODIFY_STATE | SYNCHRONIZE); + g_plmSignalResume = CreateEventEx(nullptr, nullptr, 0, EVENT_MODIFY_STATE | SYNCHRONIZE); + + RegisterAppStateChangeNotification([](BOOLEAN quiesced, PVOID context) + { + if (quiesced) + { + ResetEvent(g_plmSuspendComplete); + ResetEvent(g_plmSignalResume); + PostMessage(reinterpret_cast(context), WM_USER, 0, 0); + std::ignore = WaitForSingleObject(g_plmSuspendComplete, INFINITE); + } + else + { + SetEvent(g_plmSignalResume); + } + }, hWnd, &hPLM); +#endif + + // Main loop + MSG msg = {}; + while (msg.message != WM_QUIT) + { + if (PeekMessage(&msg, nullptr, 0U, 0U, PM_REMOVE)) + { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + else + { + g_sample->Update(); + + // Start the Dear ImGui frame + g_d3dDeviceContext->DX12_PreRender(); + ImGui_ImplDX12_NewFrame(); + ImGui_ImplWin32_NewFrame(); + ImGui::NewFrame(); + + // Standard ATG sample shortcuts + ImGuiAtg::HandleStandardInput(); + + // Draw the ImGui controls + g_sample->Draw(); + + // Rendering + ImGui::Render(); + g_d3dDeviceContext->DX12_PostRender(); + } + } + + // Shutdown and cleanup + g_d3dDeviceContext->DX12_Shutdown(); + g_sample->Shutdown(); + g_sample.reset(); + ImGui_ImplDX12_Shutdown(); + ImGui_ImplWin32_Shutdown(); + ImGui::DestroyContext(); + +#ifdef _GAMING_XBOX + UnregisterAppStateChangeNotification(hPLM); + if (g_plmSuspendComplete) + { + CloseHandle(g_plmSuspendComplete); + } + if (g_plmSignalResume) + { + CloseHandle(g_plmSignalResume); + } +#endif + + XGameRuntimeUninitialize(); + + g_d3dDeviceContext.reset(); + DestroyWindow(hWnd); + UnregisterClassW(wc.lpszClassName, wc.hInstance); + + return 0; +} + +extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); + +LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) +{ + // Give the sample first crack at messages before passing them to ImGui or the default proc. + if (g_sample && g_sample->WndProcHandler(hWnd, msg, wParam, lParam)) + { + return 1; + } + + if (ImGui_ImplWin32_WndProcHandler(hWnd, msg, wParam, lParam)) + { + return 1; + } + + switch (msg) + { +#ifdef _GAMING_XBOX + case WM_USER: + if (g_sample) + { + g_sample->Suspend(g_d3dDeviceContext.get()); + SetEvent(g_plmSuspendComplete); + std::ignore = WaitForSingleObject(g_plmSignalResume, INFINITE); + g_sample->Resume(g_d3dDeviceContext.get()); + } + break; +#endif + +#ifndef _GAMING_XBOX + case WM_GETMINMAXINFO: + { + MINMAXINFO* mmi = (MINMAXINFO*)lParam; + mmi->ptMinTrackSize.x = 1280; + mmi->ptMinTrackSize.y = 720; + } + return 0; + + case WM_KEYDOWN: + if (wParam == VK_F11) + { + ToggleFullscreen(hWnd); + return 0; + } + break; + + case WM_SYSKEYDOWN: + if (wParam == VK_RETURN && (lParam & 0x60000000) == 0x20000000) + { + ToggleFullscreen(hWnd); + return 0; + } + break; + + case WM_DPICHANGED: + { + RECT* rect = (RECT*)lParam; + SetWindowPos(hWnd, NULL, rect->left, rect->top, rect->right - rect->left, rect->bottom - rect->top, SWP_NOZORDER); + ImGuiAtg::SetDpiScale(hWnd); + } + return 0; +#endif + + case WM_ACTIVATEAPP: + if (g_sample) + { + if (wParam) + { + g_sample->Activated(); + } + else + { + g_sample->Deactivated(); + } + } + break; + + case WM_SIZE: + if (g_d3dDeviceContext) + g_d3dDeviceContext->DX12_Resize(lParam, wParam); + return 0; + + case WM_SYSCOMMAND: + if ((wParam & 0xfff0) == SC_KEYMENU) // Disable ALT application menu + { + return 0; + } + break; + + case WM_DESTROY: + PostQuitMessage(0); + return 0; + } + return DefWindowProcW(hWnd, msg, wParam, lParam); +} + +void ExitSample() noexcept +{ + PostQuitMessage(0); +} diff --git a/Samples/System/DeepLinking/MicrosoftGameConfig_Scarlett.mgc b/Samples/System/DeepLinking/MicrosoftGameConfig_Scarlett.mgc new file mode 100644 index 00000000..da2523ed --- /dev/null +++ b/Samples/System/DeepLinking/MicrosoftGameConfig_Scarlett.mgc @@ -0,0 +1,32 @@ + + + + + + + + + + 00000000498D143E + 6477b884 + + + + + + + + + diff --git a/Samples/System/DeepLinking/MicrosoftGameConfig_XboxOne.mgc b/Samples/System/DeepLinking/MicrosoftGameConfig_XboxOne.mgc new file mode 100644 index 00000000..8bd5529c --- /dev/null +++ b/Samples/System/DeepLinking/MicrosoftGameConfig_XboxOne.mgc @@ -0,0 +1,32 @@ + + + + + + + + + + 00000000498D143E + 6477b884 + + + + + + + + + diff --git a/Samples/System/DeepLinking/media/image1.png b/Samples/System/DeepLinking/media/image1.png new file mode 100644 index 00000000..a9fe0986 Binary files /dev/null and b/Samples/System/DeepLinking/media/image1.png differ diff --git a/Samples/System/DeepLinking/pch.cpp b/Samples/System/DeepLinking/pch.cpp new file mode 100644 index 00000000..fb58e703 --- /dev/null +++ b/Samples/System/DeepLinking/pch.cpp @@ -0,0 +1,10 @@ +//-------------------------------------------------------------------------------------- +// pch.cpp +// +// Include the standard header and generate the precompiled header. +// +// Advanced Technology Group (ATG) +// Copyright (C) Microsoft Corporation. All rights reserved. +//-------------------------------------------------------------------------------------- + +#include "pch.h" diff --git a/Samples/System/DeepLinking/pch.h b/Samples/System/DeepLinking/pch.h new file mode 100644 index 00000000..3887a145 --- /dev/null +++ b/Samples/System/DeepLinking/pch.h @@ -0,0 +1,149 @@ +//-------------------------------------------------------------------------------------- +// pch.h +// +// Header for standard system include files. +// +// Advanced Technology Group (ATG) +// Copyright (C) Microsoft Corporation. All rights reserved. +//-------------------------------------------------------------------------------------- + +#pragma once + +#include +#ifndef _WIN32_WINNT +#define _WIN32_WINNT 0x0A00 +#endif +#include + +// Use the C++ standard templated min/max +#define NOMINMAX + +// ImGui Desktop needs GDI on PC, but the Xbox build can skip these headers. +#ifdef _GAMING_XBOX +#define NODRAWTEXT +#define NOGDI +#define NOBITMAP +#endif + +// Include if you need this +#define NOMCX + +// Include if you need this +#define NOSERVICE + +// WinHelp is deprecated +#define NOHELP + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif + +#include + +#include +#include + +#include + +#if _GRDK_VER < 0x65F41800 /* GDK Edition 251000 */ +#error This sample requires the October 2025 GDK or later +#endif + +#ifdef _GAMING_XBOX_SCARLETT +#include +#include +#elif defined(_GAMING_XBOX) +#include +#include +#else +#include +#include + +#ifdef _DEBUG +#include +#endif + +#include "d3dx12.h" +#endif + +#define _XM_NO_XMVECTOR_OVERLOADS_ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _GAMING_XBOX +#include +#else +// To use graphics markup events with the latest version of PIX, change this to include +// then add the NuGet package WinPixEventRuntime to the project. +#include +#endif +#include +#include + +#include "imgui.h" +#include "backends/imgui_impl_dx12.h" +#include "backends/imgui_impl_win32.h" +#include "imgui/imgui_atg.h" + +// To opt-out of telemetry uncomment the following line +//#define ATG_DISABLE_TELEMETRY + +namespace DX +{ + // Helper class for COM exceptions + class com_exception : public std::exception + { + public: + com_exception(HRESULT hr) noexcept : result(hr) {} + + const char* what() const noexcept override + { + static char s_str[64] = {}; + sprintf_s(s_str, "Failure with HRESULT of %08X", static_cast(result)); + return s_str; + } + + private: + HRESULT result; + }; + + // Helper utility converts D3D API failures into exceptions. + inline void ThrowIfFailed(HRESULT hr) + { + if (FAILED(hr)) + { +#ifdef _DEBUG + char str[64] = {}; + sprintf_s(str, "**ERROR** Fatal Error with HRESULT of %08X\n", static_cast(hr)); + OutputDebugStringA(str); + __debugbreak(); +#endif + throw com_exception(hr); + } + } +} + +// Enable off by default warnings to improve code conformance +#pragma warning(default : 4061 4062 4191 4263 4264 4265 4266 4289 4365 4746 4826 4841 4986 4987 5029 5038 5042) diff --git a/Samples/System/DeepLinking/readme.md b/Samples/System/DeepLinking/readme.md new file mode 100644 index 00000000..bd96466a --- /dev/null +++ b/Samples/System/DeepLinking/readme.md @@ -0,0 +1,313 @@ + + +# DeepLinking Sample + +*This sample is compatible with the Microsoft Game Development Kit (October 2025) and later.* + +# Description + +This sample shows how a GDK title can receive a protocol (URI) activation and use the data in the +URI to "deep link" the player directly into a specific area of the game (for example, the +multiplayer lobby or the campaign) instead of always starting at the title screen. Deep linking lets +Xbox editorial placements, marketing campaigns, and event hubs drop players straight into the right +destination with a single click. + +Deep linking is supported on Xbox console only today. + +![](./media/image1.png) + +## Protocol activation + +Deep linking is built on the GDK's protocol activation system. Any process on the system can launch the title +via protocol activiation and pass it a payload by activating a URI with that required parameters. +The title receives the full URI through its +`XGameActivationRegisterForEvent` callback. If the title is already running, the same callback fires +on the existing process rather than starting a second instance, so command-line parsing is not +sufficient: titles that want to handle activations after launch must register the callback. Two +flavors of protocol are available: + +- **The implicit `ms-xbl-://` protocol.** Every title with a Title ID has an auto-generated + `ms-xbl-XXXXXXXX://` protocol, where `XXXXXXXX` is the hexadecimal Title ID without the leading + `0x`. No declaration is required, and any caller (the Microsoft Store, another title via + `XLaunchUri`, the system shell, etc.) can activate the title through it. Store-driven deep links + authored as `ms-windows-store://launch?productId=...&path=...` are rewritten by the platform into + `ms-xbl-://` and delivered as activation type `Protocol`. This is what the sample + exercises. + +- **Custom protocols declared in `MicrosoftGameConfig.mgc`.** Titles can register one or more + additional protocols by adding a `` block to the MGC. Example: + ```xml + + ... + + + + + ``` + Once declared, the title can be activated with a URI such as + `atg-sample://Parameter1=xyz&Parameter2=abc`, and the URI is delivered to the same + activation callback with type `Protocol`. Custom protocols are useful for cross-title launches, + internal/debug entry points, and any case where the publisher wants to route URIs that do not flow + through the storefront. The `Executable` attribute lets different protocols target different + executables in a multi-exe package (for example, a debug build for QA-only activations). + +Both the implicit protocol and any custom protocols share the same activation callback; the title +inspects the incoming URI (scheme, path, querystring) to decide what to do. For the full reference, +see the [Protocol activation][protocolactivation] page in the GDK documentation. + +[protocolactivation]: https://learn.microsoft.com/en-us/gaming/gdk/docs/gdk-dev/overviews/protocolactivation + +## Deep-link URL structure + +Deep links generated by Xbox or by the publisher are authored against the Microsoft Store protocol: + +``` +ms-windows-store://launch?productId=&path= +``` + +If the title is installed, the system launches it and delivers the activation. If the title is not +installed, the same URL opens the title's PDP in the Store. + +`` is the title's **Microsoft Store product ID**: the 12-character alphanumeric ID +assigned in Partner Center (for this sample, `9NV0ZMV5GB11`). It is *not* the GDK Title ID, Service +Configuration ID, or any other identifier. `` is one of the routing paths the title has +defined and shared with Xbox. + +The platform decodes `path` before delivering the activation, rewriting the URI into the title's own +`ms-xbl-` protocol: + +``` +ms-xbl-:/// +``` + +The deep link protocol only exposes `productId` and `path`, so **anything richer (sub-paths, querystrings, +GUIDs) must be packed into `path` as a single url-encoded string** on the source side. For example, to also pass +`offer=1234` and `qty=3`, do not append as a standard query string. Instead, append it to the path as +`%2F%3Foffer%3D1234%26qty%3D3`. + +For example, the publisher generates: + +``` +ms-windows-store://launch?productId=9NV0ZMV5GB11&path=store%2F%3Foffer%3D1234%26qty%3D3 +``` + +...which arrives at the title's activation callback as: + +``` +ms-xbl-://store/?offer=1234&key=value +``` + +The callback does **not** need to URL-decode the value, it will arrive decoded to the title. + +Here is a table of the most common characters that will be requried for encoding your additional params: + +| Character | Encoded | +|-----------|---------| +| `/` | `%2F` | +| `?` | `%3F` | +| `=` | `%3D` | +| `&` | `%26` | + + +## Common use cases + +| Goal | Typical destination | +|---------------------------------|-------------------------------------------| +| Reactivate lapsed players | Lobby or landing screen | +| Season launch / new mode | Hub or new-mode screen | +| Limited-time event | Event hub | +| In-game purchase or promotion | In-game store, item shop, specific item, VC bundle | +| Battle pass | Battle pass page | +| Ownership-based engagement | Deep link if installed, PDP fallback if not | + +# Building the sample + +If using an Xbox Series X|S devkit, set the active solution platform to `Gaming.Xbox.Scarlett.x64`. + +If using an Xbox One devkit, set the active solution platform to `Gaming.Xbox.XboxOne.x64`. + +*For more information, see* __Running samples__, *in the GDK documentation.* + +# Using the sample + +This sample is published to the **XDS.1** sandbox with Microsoft Store product ID +**`9NV0ZMV5GB11`**, so it can be exercised end-to-end on any devkit switched to that sandbox without +re-publishing. Because deep linking relies on the title being addressable via its retail +`ms-xbl-://` protocol, the sample cannot be exercised from a plain "F5 deploy" of a loose +build. To test it: + +1. Switch the devkit to the `XDS.1` sandbox (or publish your own build to a sandbox you control). +2. Search the store and install the "ATG DeepLinking Sample" package on the devkit so the system + records the license and registers the protocol handler. +3. From this state, you can deploy the sample build from Visual Studio on top of the installed + package if you wish to experiment. The installed license and protocol registration carry over to + the freshly deployed bits. + +The paths the sample understands are arbitrary names chosen for the demo. A real title defines +whatever vocabulary it needs: + +| Path | Destination | +|------------|------------------------------| +| `lobby` | Lobby screen | +| `campaign` | Campaign / single-player | +| `shop` | In-game shop (reads `id` and `qty` querystrings) | +| `none` | Title screen (explicit) | +| *other* | Title screen (default) | + +Once the title is installed, trigger a deep link from a separate process. From an Xbox Gaming +command prompt, the following commands cover every screen and parser path the sample supports. The +double quotes are required; without them the shell will eat the `&` and the `path` parameter will be +lost. + +``` +:: Title screen (explicit), lobby, campaign, shop +xbapp launch "ms-windows-store://launch?productId=9NV0ZMV5GB11&path=none" +xbapp launch "ms-windows-store://launch?productId=9NV0ZMV5GB11&path=lobby" +xbapp launch "ms-windows-store://launch?productId=9NV0ZMV5GB11&path=campaign" +xbapp launch "ms-windows-store://launch?productId=9NV0ZMV5GB11&path=shop" + +:: Shop with querystring: id=1234, then id=1234&qty=3 +xbapp launch "ms-windows-store://launch?productId=9NV0ZMV5GB11&path=shop%2F%3Fid%3D1234" +xbapp launch "ms-windows-store://launch?productId=9NV0ZMV5GB11&path=shop%2F%3Fid%3D1234%26qty%3D3" + +:: Unknown path falls back to the title screen +xbapp launch "ms-windows-store://launch?productId=9NV0ZMV5GB11&path=doesnotexist" + +:: Custom protocol (atg-sample, declared in MicrosoftGameConfig.mgc). NOT required for deep +:: linking; included only to demonstrate that a title can define its own protocols. The URI is passed +:: through to the activation callback as-is. +xbapp launch "atg-sample://shop?id=1234&qty=3" +xbapp launch "atg-sample://campaign" +``` + +The shop-with-querystring URLs are delivered to the title as `ms-xbl-://shop/?id=1234` and +`ms-xbl-://shop/?id=1234&qty=3`. The custom-protocol URLs (which, again, are not part of +deep linking; they are an independent demonstration) arrive as `atg-sample://shop?id=1234&qty=3` +and `atg-sample://campaign`. The sample displays each parsed key/value pair under "Query String", +and the SHOP screen pulls `id` and `qty` out of the map. + +The default ATG ImGui shortcuts (`F2` / `LB+RB+Y` to toggle light/dark, `Alt+F4` / `LB+RB+View+Menu` +to exit) are also active. + +## Publisher responsibilities for deep linking + +Xbox owns the storefront placement, the routing of the protocol activation, and the Product Detail +Page (PDP) fallback when the title is not installed. The publisher owns everything from the moment +the activation arrives at the title: + +- **Define stable, unique routing paths** for every in-game destination that should be deep-linkable + (lobby, playlist, event hub, item shop, battle pass page, specific item, etc.). Once a path is + shared with Xbox it may be live in storefront placements; changing or removing it without + coordination will break those links. +- **Handle the routing payload on launch** for both cold-start (game not running) and warm-start + (game already running, raised via activation) paths. +- **Implement deterministic navigation.** The same link must always land on the same destination. If + the player is interrupted by sign-in, entitlement checks, or a title update, resume navigation to + the deep-link target once the interruption clears. +- **Handle suspend/resume and quick resume.** An activation can arrive while the title is suspended + or constrained; the title must route the player correctly when it resumes. +- **Use the correct Microsoft Store product ID** in every deep link the publisher generates. See + *Deep-link URL structure* below for the definition. The platform uses it for the PDP fallback when + the title is not installed; no additional game logic is required for that case. + +## Validation scenarios + +Before submitting deep links to Xbox, publishers are expected to validate end-to-end behavior across +at least these scenarios: + +1. **Title not installed.** The link should land on the title's Store PDP (the title must have been + published to the target sandbox). +2. **Title installed but not running.** Cold-launch should bring the player to the deep-link + destination, not the title screen. +3. **Title already running on the deep-link destination.** Re-invoking the same link should leave + the player on the same destination (deterministic navigation). +4. **Title already running, player navigated away.** Re-invoking the link should return the player + to the deep-link destination. +5. **Title constrained / suspended.** After opening another app or game, invoking the link should + resume the title and route to the deep-link destination. + +## Testing on retail consoles via Microsoft Edge + +For validation on retail consoles (where `xbapp launch` is not available), deep links can be invoked +from Microsoft Edge bookmarks: + +1. On PC, sign in to Microsoft Edge with a personal account. +2. Open the favorites bar and add a new favorite. Set the URL to the deep link + (`ms-windows-store://launch?productId=...&path=...`) and give it a descriptive name. A folder + hierarchy helps when managing many links. +3. On the Xbox console, open Microsoft Edge and sign in with the same account so the favorites sync. +4. Open the bookmark to invoke the deep link. The title must already be published and live to the + console's sandbox. + +# Implementation notes + +The interesting code lives in `DeepLinking.cpp`: + +- **Registration.** `Sample::Initialize` calls `XGameActivationRegisterForEvent`, passing `this` as + the callback context. The registration token is held in `m_activationToken` and released in + `Sample::Shutdown` via `XGameActivationUnregisterForEvent` with `wait=true`, so any in-flight + callback finishes before `this` is destroyed. Registering with a `nullptr` task queue runs the + callback on the process task queue, the right default for UI-driven samples. + +- **Older GDKs (April 2025 / 2504 and earlier).** Use `XGameProtocolRegisterForActivation` / + `XGameProtocolUnregisterForActivation` instead of `XGameActivation`. Registration semantics are + identical; only the callback signature differs (`const char* uri` on the older API vs. an + `XGameActivationInfo*` struct on the new one, which also carries non-protocol activation types). + URI parsing is unchanged. + +- **Filtering activation types.** The callback fires for every activation, so the sample early-outs + on anything that is not `XGameActivationType::Protocol`. + +- **URI parsing.** The callback skips past `://`, splits the remainder on the first `?` to separate + path from querystring, and strips the trailing `/` the platform appends when there is no + querystring. Because the platform has already URL-decoded the payload, the callback does **not** + decode it again. + +- **Querystring parsing.** When a `?` is present, the sample walks the querystring once, splitting + on `&` and then `=` into a `std::map` displayed under "Query String". + The SHOP screen pulls `id` and `qty` out of the map with `find` + `end()` checks (the pattern a + real title would use to load a specific item, quantity, event, or battle pass page). + +- **Path -> mode lookup.** `Sample::NavigateTo` looks the path up in `s_pathToGameMode` (a `static + const unordered_map` on `Sample`) using `find` + an explicit `GameMode::None` + fallback. `operator[]` would silently insert a default-constructed entry on every unknown path, so + `find` is the safer pattern. Unknown paths fall through to `GameMode::None` (the title screen). + +- **Example custom protocol declared in the MGC.** `MicrosoftGameConfig.mgc` declares one custom protocol, + `atg-sample`, pointing at `DeepLinking.exe`: + ```xml + + + + ``` + This is purely an example and NOT required for deep linking. Custom protocols are useful for cross-title + launches, internal/debug entry points, and any case where the publisher wants to route URIs that do not + flow through the storefront. + +- **Deterministic navigation in production titles.** The sample switches a single `m_mode` value on + activation. A shipping title is expected to route consistently to the same destination for the + same link, defer routing through sign-in or entitlement checks if needed, and resume routing after + the interruption clears. The callback fires for cold-launch and warm activations (including + activations that arrive while the title is suspended or constrained), so the routing code must be + safe to invoke from any state. + +# Update history + +06/2026 - Initial release diff --git a/gdk-samples-list.md b/gdk-samples-list.md index 8be91900..3e6d21b5 100644 --- a/gdk-samples-list.md +++ b/gdk-samples-list.md @@ -144,6 +144,7 @@ Following is a list of categories for the samples. | __Collision__ | This sample demonstrates DirectXMath's collision types for simple bounding volume tests. | ✓ | ✓ | | __CustomEventProvider__ | This sample demonstrates how to use custom ETW event providers on Xbox. | ✓ | | | __DataBreakPoints__ | This sample shows how to create hardware data breakpoints that are useful for detecting different types of memory access on Xbox. | ✓ | ✓ | +| __DeepLinking__ | This sample shows how a GDK title can receive a protocol (URI) activation to 'deep link' the player directly into a specific area of the game. | ✓ | | | __FrontPanelDemo__ | FrontPanelDemo combines several samples into one executable and then ties together the functionality with a menu system all hosted entirely on the Xbox DevKit front panel. | ✓ | | | __FrontPanelDolphin__ | FrontPanelDolphin demonstrates how to use the GPU to render to the Xbox DevKit FrontPanel. | ✓ | | | __FrontPanelGame__ | FrontPanelGame is the classic 'snake game' implemented completely on the Xbox DevKit Front Panel. | ✓ | |