diff --git a/examples/audience/Assets/link.xml b/examples/audience/Assets/link.xml index 05aa338c..c90feed3 100644 --- a/examples/audience/Assets/link.xml +++ b/examples/audience/Assets/link.xml @@ -27,4 +27,11 @@ hooks fire under IL2CPP with stripping High. + + + + + + + diff --git a/src/Packages/Audience/Runtime/ImmutableAudience.cs b/src/Packages/Audience/Runtime/ImmutableAudience.cs index 0df90fc3..14c1ba4c 100644 --- a/src/Packages/Audience/Runtime/ImmutableAudience.cs +++ b/src/Packages/Audience/Runtime/ImmutableAudience.cs @@ -256,6 +256,7 @@ public static void Init(AudienceConfig config) } FireGameLaunch(config, consentAtInit, skanRegistered, attributionContext); + TryIdentifySteamUser(); CheckAndFireAttStatusChanged(config, consentAtInit); @@ -1089,6 +1090,130 @@ private static void RescheduleSendTimer(HttpTransport transport) timer.Change(nextMs, sendIntervalMs); } + // Resolves Steamworks.SteamAPI (Steamworks.NET) across all common install methods: + // UPM / OpenUPM package → com.rlabrecque.steamworks.net + // .dll plugin in Assets → Steamworks.NET + // source files in Assets → Assembly-CSharp + // Returns null when Steamworks.NET is not present. + private static System.Type? ResolveSteamApiType() => + System.Type.GetType("Steamworks.SteamAPI, com.rlabrecque.steamworks.net") + ?? System.Type.GetType("Steamworks.SteamAPI, Steamworks.NET") + ?? System.Type.GetType("Steamworks.SteamAPI, Assembly-CSharp"); + + // Resolves Steamworks.SteamClient (Facepunch.Steamworks) across common install methods. + // Facepunch ships platform-specific DLLs; the assembly name encodes the platform: + // macOS/Linux → Facepunch.Steamworks.Posix + // Windows 64 → Facepunch.Steamworks.Win64 + // Windows 32 → Facepunch.Steamworks.Win32 + // Source in Assets → Assembly-CSharp + // Returns null when Facepunch.Steamworks is not present. + private static System.Type? ResolveFacepunchSteamClientType() => + System.Type.GetType("Steamworks.SteamClient, Facepunch.Steamworks.Posix") + ?? System.Type.GetType("Steamworks.SteamClient, Facepunch.Steamworks.Win64") + ?? System.Type.GetType("Steamworks.SteamClient, Facepunch.Steamworks.Win32") + ?? System.Type.GetType("Steamworks.SteamClient, Assembly-CSharp"); + + // Sets distribution_platform = "steam" when any supported Steam wrapper is + // detected as active. Config override wins afterward (line ~1195). + private static void TryDetectSteamPlatform(Dictionary properties) + { + try + { + if (IsSteamworksNetRunning() || IsFacepunchSteamValid()) + properties["distribution_platform"] = DistributionPlatforms.Steam; + } + catch (Exception ex) + { + Log.Warn(AudienceLogs.SteamPlatformDetectionFailed(ex)); + } + } + + // Calls Identify with the logged-in SteamID64. Tries Steamworks.NET first, + // then Facepunch.Steamworks. No-ops if neither is present, Steam is not + // running, the ID is invalid, or consent is below Full. + private static void TryIdentifySteamUser() + { + try + { + if (!TryGetSteamworksNetId(out var id) && !TryGetFacepunchId(out id)) + return; + Log.Debug(AudienceLogs.SteamAutoIdentified(id!)); + Identify(id!, IdentityType.Steam); + } + catch (Exception ex) + { + Log.Warn(AudienceLogs.SteamIdentityCollectionFailed(ex)); + } + } + + // Returns true if Steamworks.NET's SteamAPI.IsSteamRunning() is true. + private static bool IsSteamworksNetRunning() + { + var steamApi = ResolveSteamApiType(); + if (steamApi == null) return false; + return steamApi.GetMethod("IsSteamRunning")?.Invoke(null, null) as bool? == true; + } + + // Returns true if Facepunch.Steamworks's SteamClient.IsValid is true. + private static bool IsFacepunchSteamValid() + { + var steamClient = ResolveFacepunchSteamClientType(); + if (steamClient == null) return false; + return steamClient.GetProperty("IsValid")?.GetValue(null) as bool? == true; + } + + // Reads the SteamID64 via Steamworks.NET (SteamUser.GetSteamID → m_SteamID ulong). + // Attempts SteamAPI.Init() first so the sample app works without manual Steam init; + // in shipping games Init() is already called before our SDK runs. + private static bool TryGetSteamworksNetId(out string? id) + { + id = null; + var steamApi = ResolveSteamApiType(); + if (steamApi == null) return false; + if (steamApi.GetMethod("IsSteamRunning")?.Invoke(null, null) as bool? != true) return false; + + // Safe to call even if already initialised; returns false but is otherwise a no-op. + steamApi.GetMethod("Init")?.Invoke(null, null); + + var steamUserType = + System.Type.GetType("Steamworks.SteamUser, com.rlabrecque.steamworks.net") + ?? System.Type.GetType("Steamworks.SteamUser, Steamworks.NET") + ?? System.Type.GetType("Steamworks.SteamUser, Assembly-CSharp"); + if (steamUserType == null) return false; + + var steamId = steamUserType.GetMethod("GetSteamID")?.Invoke(null, null); + if (steamId == null) return false; + + // CSteamID.m_SteamID == 0 means not logged in / not initialised. + var raw = steamId.GetType().GetField("m_SteamID")?.GetValue(steamId); + if (raw == null || (ulong)raw == 0) return false; + + id = ((ulong)raw).ToString(); + return true; + } + + // Reads the SteamID64 via Facepunch.Steamworks (SteamClient.SteamId.Value ulong). + // Requires SteamClient.Init(appId) to have been called by the game already; + // the SDK cannot call it as the appId is unknown. + private static bool TryGetFacepunchId(out string? id) + { + id = null; + var steamClient = ResolveFacepunchSteamClientType(); + if (steamClient == null) return false; + if (steamClient.GetProperty("IsValid")?.GetValue(null) as bool? != true) return false; + + var steamIdProp = steamClient.GetProperty("SteamId"); + var steamId = steamIdProp?.GetValue(null); + if (steamId == null) return false; + + // Facepunch SteamId exposes Value as a public ulong field (not a property). + var raw = steamId.GetType().GetField("Value")?.GetValue(steamId); + if (raw == null || (ulong)raw == 0) return false; + + id = ((ulong)raw).ToString(); + return true; + } + // consentAtInit only gates the launch; Track still checks live _state via CanTrack. private static void FireGameLaunch( AudienceConfig config, @@ -1119,7 +1244,10 @@ private static void FireGameLaunch( } } - // Config-supplied distributionPlatform overrides the provider value. + // Auto-detect distribution platform via reflection. Config override wins below. + TryDetectSteamPlatform(properties); + + // Config-supplied distributionPlatform overrides the auto-detected value. if (config.DistributionPlatform != null) properties["distribution_platform"] = config.DistributionPlatform; diff --git a/src/Packages/Audience/Runtime/Utility/Log.cs b/src/Packages/Audience/Runtime/Utility/Log.cs index 772f3aee..8fa5eb43 100644 --- a/src/Packages/Audience/Runtime/Utility/Log.cs +++ b/src/Packages/Audience/Runtime/Utility/Log.cs @@ -158,5 +158,18 @@ internal static string ATTIDFAProviderThrew(Exception ex) => internal static string GAIDFetchThrew(Exception ex) => $"GAID fetch threw {ex.GetType().Name}: {ex.Message}. " + "gaid will not ship on game_launch this session; next launch retries."; + + // ---- Steam auto-detection ---- + + internal static string SteamPlatformDetectionFailed(Exception ex) => + $"Steam platform detection threw {ex.GetType().Name}: {ex.Message}. " + + "distribution_platform will not be auto-set."; + + internal static string SteamAutoIdentified(string steamId) => + $"auto-identified steam user: {steamId}"; + + internal static string SteamIdentityCollectionFailed(Exception ex) => + $"Steam identity collection threw {ex.GetType().Name}: {ex.Message}. " + + "Steam user ID will not be auto-collected."; } } diff --git a/src/Packages/Audience/link.xml b/src/Packages/Audience/link.xml index 5f9428bb..38cd8398 100644 --- a/src/Packages/Audience/link.xml +++ b/src/Packages/Audience/link.xml @@ -29,4 +29,16 @@ framework dependency. + + + + + + +