Skip to content

Commit dc64354

Browse files
authored
Merge pull request #798 from immutable/feat/sdk-532-steam-user-id
feat(audience): auto-collect Steam user ID and detect Steam platform
2 parents 42e8f0b + 71ae613 commit dc64354

4 files changed

Lines changed: 161 additions & 1 deletion

File tree

examples/audience/Assets/link.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,11 @@ hooks fire under IL2CPP with stripping High.
2727
<type fullname="System.IO.Compression.GZipStream" preserve="all" />
2828
<type fullname="System.IO.Compression.CompressionLevel" preserve="all" />
2929
</assembly>
30+
31+
<!-- Steam auto-detection; mirrors SDK link.xml. Ignored if Steamworks is not present. -->
32+
<assembly fullname="com.rlabrecque.steamworks.net" preserve="all" />
33+
<assembly fullname="Steamworks.NET" preserve="all" />
34+
<assembly fullname="Facepunch.Steamworks.Posix" preserve="all" />
35+
<assembly fullname="Facepunch.Steamworks.Win64" preserve="all" />
36+
<assembly fullname="Facepunch.Steamworks.Win32" preserve="all" />
3037
</linker>

src/Packages/Audience/Runtime/ImmutableAudience.cs

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ public static void Init(AudienceConfig config)
256256
}
257257

258258
FireGameLaunch(config, consentAtInit, skanRegistered, attributionContext);
259+
TryIdentifySteamUser();
259260

260261
CheckAndFireAttStatusChanged(config, consentAtInit);
261262

@@ -1089,6 +1090,130 @@ private static void RescheduleSendTimer(HttpTransport transport)
10891090
timer.Change(nextMs, sendIntervalMs);
10901091
}
10911092

1093+
// Resolves Steamworks.SteamAPI (Steamworks.NET) across all common install methods:
1094+
// UPM / OpenUPM package → com.rlabrecque.steamworks.net
1095+
// .dll plugin in Assets → Steamworks.NET
1096+
// source files in Assets → Assembly-CSharp
1097+
// Returns null when Steamworks.NET is not present.
1098+
private static System.Type? ResolveSteamApiType() =>
1099+
System.Type.GetType("Steamworks.SteamAPI, com.rlabrecque.steamworks.net")
1100+
?? System.Type.GetType("Steamworks.SteamAPI, Steamworks.NET")
1101+
?? System.Type.GetType("Steamworks.SteamAPI, Assembly-CSharp");
1102+
1103+
// Resolves Steamworks.SteamClient (Facepunch.Steamworks) across common install methods.
1104+
// Facepunch ships platform-specific DLLs; the assembly name encodes the platform:
1105+
// macOS/Linux → Facepunch.Steamworks.Posix
1106+
// Windows 64 → Facepunch.Steamworks.Win64
1107+
// Windows 32 → Facepunch.Steamworks.Win32
1108+
// Source in Assets → Assembly-CSharp
1109+
// Returns null when Facepunch.Steamworks is not present.
1110+
private static System.Type? ResolveFacepunchSteamClientType() =>
1111+
System.Type.GetType("Steamworks.SteamClient, Facepunch.Steamworks.Posix")
1112+
?? System.Type.GetType("Steamworks.SteamClient, Facepunch.Steamworks.Win64")
1113+
?? System.Type.GetType("Steamworks.SteamClient, Facepunch.Steamworks.Win32")
1114+
?? System.Type.GetType("Steamworks.SteamClient, Assembly-CSharp");
1115+
1116+
// Sets distribution_platform = "steam" when any supported Steam wrapper is
1117+
// detected as active. Config override wins afterward (line ~1195).
1118+
private static void TryDetectSteamPlatform(Dictionary<string, object> properties)
1119+
{
1120+
try
1121+
{
1122+
if (IsSteamworksNetRunning() || IsFacepunchSteamValid())
1123+
properties["distribution_platform"] = DistributionPlatforms.Steam;
1124+
}
1125+
catch (Exception ex)
1126+
{
1127+
Log.Warn(AudienceLogs.SteamPlatformDetectionFailed(ex));
1128+
}
1129+
}
1130+
1131+
// Calls Identify with the logged-in SteamID64. Tries Steamworks.NET first,
1132+
// then Facepunch.Steamworks. No-ops if neither is present, Steam is not
1133+
// running, the ID is invalid, or consent is below Full.
1134+
private static void TryIdentifySteamUser()
1135+
{
1136+
try
1137+
{
1138+
if (!TryGetSteamworksNetId(out var id) && !TryGetFacepunchId(out id))
1139+
return;
1140+
Log.Debug(AudienceLogs.SteamAutoIdentified(id!));
1141+
Identify(id!, IdentityType.Steam);
1142+
}
1143+
catch (Exception ex)
1144+
{
1145+
Log.Warn(AudienceLogs.SteamIdentityCollectionFailed(ex));
1146+
}
1147+
}
1148+
1149+
// Returns true if Steamworks.NET's SteamAPI.IsSteamRunning() is true.
1150+
private static bool IsSteamworksNetRunning()
1151+
{
1152+
var steamApi = ResolveSteamApiType();
1153+
if (steamApi == null) return false;
1154+
return steamApi.GetMethod("IsSteamRunning")?.Invoke(null, null) as bool? == true;
1155+
}
1156+
1157+
// Returns true if Facepunch.Steamworks's SteamClient.IsValid is true.
1158+
private static bool IsFacepunchSteamValid()
1159+
{
1160+
var steamClient = ResolveFacepunchSteamClientType();
1161+
if (steamClient == null) return false;
1162+
return steamClient.GetProperty("IsValid")?.GetValue(null) as bool? == true;
1163+
}
1164+
1165+
// Reads the SteamID64 via Steamworks.NET (SteamUser.GetSteamID → m_SteamID ulong).
1166+
// Attempts SteamAPI.Init() first so the sample app works without manual Steam init;
1167+
// in shipping games Init() is already called before our SDK runs.
1168+
private static bool TryGetSteamworksNetId(out string? id)
1169+
{
1170+
id = null;
1171+
var steamApi = ResolveSteamApiType();
1172+
if (steamApi == null) return false;
1173+
if (steamApi.GetMethod("IsSteamRunning")?.Invoke(null, null) as bool? != true) return false;
1174+
1175+
// Safe to call even if already initialised; returns false but is otherwise a no-op.
1176+
steamApi.GetMethod("Init")?.Invoke(null, null);
1177+
1178+
var steamUserType =
1179+
System.Type.GetType("Steamworks.SteamUser, com.rlabrecque.steamworks.net")
1180+
?? System.Type.GetType("Steamworks.SteamUser, Steamworks.NET")
1181+
?? System.Type.GetType("Steamworks.SteamUser, Assembly-CSharp");
1182+
if (steamUserType == null) return false;
1183+
1184+
var steamId = steamUserType.GetMethod("GetSteamID")?.Invoke(null, null);
1185+
if (steamId == null) return false;
1186+
1187+
// CSteamID.m_SteamID == 0 means not logged in / not initialised.
1188+
var raw = steamId.GetType().GetField("m_SteamID")?.GetValue(steamId);
1189+
if (raw == null || (ulong)raw == 0) return false;
1190+
1191+
id = ((ulong)raw).ToString();
1192+
return true;
1193+
}
1194+
1195+
// Reads the SteamID64 via Facepunch.Steamworks (SteamClient.SteamId.Value ulong).
1196+
// Requires SteamClient.Init(appId) to have been called by the game already;
1197+
// the SDK cannot call it as the appId is unknown.
1198+
private static bool TryGetFacepunchId(out string? id)
1199+
{
1200+
id = null;
1201+
var steamClient = ResolveFacepunchSteamClientType();
1202+
if (steamClient == null) return false;
1203+
if (steamClient.GetProperty("IsValid")?.GetValue(null) as bool? != true) return false;
1204+
1205+
var steamIdProp = steamClient.GetProperty("SteamId");
1206+
var steamId = steamIdProp?.GetValue(null);
1207+
if (steamId == null) return false;
1208+
1209+
// Facepunch SteamId exposes Value as a public ulong field (not a property).
1210+
var raw = steamId.GetType().GetField("Value")?.GetValue(steamId);
1211+
if (raw == null || (ulong)raw == 0) return false;
1212+
1213+
id = ((ulong)raw).ToString();
1214+
return true;
1215+
}
1216+
10921217
// consentAtInit only gates the launch; Track still checks live _state via CanTrack.
10931218
private static void FireGameLaunch(
10941219
AudienceConfig config,
@@ -1119,7 +1244,10 @@ private static void FireGameLaunch(
11191244
}
11201245
}
11211246

1122-
// Config-supplied distributionPlatform overrides the provider value.
1247+
// Auto-detect distribution platform via reflection. Config override wins below.
1248+
TryDetectSteamPlatform(properties);
1249+
1250+
// Config-supplied distributionPlatform overrides the auto-detected value.
11231251
if (config.DistributionPlatform != null)
11241252
properties["distribution_platform"] = config.DistributionPlatform;
11251253

src/Packages/Audience/Runtime/Utility/Log.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,5 +158,18 @@ internal static string ATTIDFAProviderThrew(Exception ex) =>
158158
internal static string GAIDFetchThrew(Exception ex) =>
159159
$"GAID fetch threw {ex.GetType().Name}: {ex.Message}. " +
160160
"gaid will not ship on game_launch this session; next launch retries.";
161+
162+
// ---- Steam auto-detection ----
163+
164+
internal static string SteamPlatformDetectionFailed(Exception ex) =>
165+
$"Steam platform detection threw {ex.GetType().Name}: {ex.Message}. " +
166+
"distribution_platform will not be auto-set.";
167+
168+
internal static string SteamAutoIdentified(string steamId) =>
169+
$"auto-identified steam user: {steamId}";
170+
171+
internal static string SteamIdentityCollectionFailed(Exception ex) =>
172+
$"Steam identity collection threw {ex.GetType().Name}: {ex.Message}. " +
173+
"Steam user ID will not be auto-collected.";
161174
}
162175
}

src/Packages/Audience/link.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,16 @@ framework dependency.
2929
<type fullname="System.IO.Compression.GZipStream" preserve="all" />
3030
<type fullname="System.IO.Compression.CompressionLevel" preserve="all" />
3131
</assembly>
32+
33+
<!--
34+
Steam platform auto-detection. The SDK resolves these types via reflection
35+
at runtime so the linker cannot see the dependency and strips them under
36+
IL2CPP High stripping. Entries for missing assemblies are silently ignored,
37+
so games without Steamworks are unaffected.
38+
-->
39+
<assembly fullname="com.rlabrecque.steamworks.net" preserve="all" />
40+
<assembly fullname="Steamworks.NET" preserve="all" />
41+
<assembly fullname="Facepunch.Steamworks.Posix" preserve="all" />
42+
<assembly fullname="Facepunch.Steamworks.Win64" preserve="all" />
43+
<assembly fullname="Facepunch.Steamworks.Win32" preserve="all" />
3244
</linker>

0 commit comments

Comments
 (0)