Skip to content

Commit 0f2f46e

Browse files
committed
feat(audience): auto-collect Epic account ID and detect Epic platform
1 parent dc64354 commit 0f2f46e

4 files changed

Lines changed: 134 additions & 0 deletions

File tree

examples/audience/Assets/link.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,8 @@ hooks fire under IL2CPP with stripping High.
3434
<assembly fullname="Facepunch.Steamworks.Posix" preserve="all" />
3535
<assembly fullname="Facepunch.Steamworks.Win64" preserve="all" />
3636
<assembly fullname="Facepunch.Steamworks.Win32" preserve="all" />
37+
38+
<!-- Epic auto-detection; mirrors SDK link.xml. Ignored if EOS plugin is not present. -->
39+
<assembly fullname="EOSSDK" preserve="all" />
40+
<assembly fullname="PlayEveryWare.EpicOnlineServices" preserve="all" />
3741
</linker>

src/Packages/Audience/Runtime/ImmutableAudience.cs

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ public static void Init(AudienceConfig config)
257257

258258
FireGameLaunch(config, consentAtInit, skanRegistered, attributionContext);
259259
TryIdentifySteamUser();
260+
TryIdentifyEpicUser();
260261

261262
CheckAndFireAttStatusChanged(config, consentAtInit);
262263

@@ -1214,6 +1215,114 @@ private static bool TryGetFacepunchId(out string? id)
12141215
return true;
12151216
}
12161217

1218+
// Resolves PlayEveryWare.EpicOnlineServices.EOSManager across install methods.
1219+
// Returns null when the EOS Unity plugin is not present.
1220+
private static System.Type? ResolveEosManagerType() =>
1221+
System.Type.GetType("PlayEveryWare.EpicOnlineServices.EOSManager, PlayEveryWare.EpicOnlineServices")
1222+
?? System.Type.GetType("PlayEveryWare.EpicOnlineServices.EOSManager, Assembly-CSharp");
1223+
1224+
// Gets the initialised PlatformInterface handle from EOSManager.Instance.
1225+
// Returns null when the EOS plugin is absent or EOS has not been initialised.
1226+
private static object? GetEosPlatformInterface()
1227+
{
1228+
var managerType = ResolveEosManagerType();
1229+
if (managerType == null) return null;
1230+
// Use the compiled getter name (get_Instance) for IL2CPP compatibility;
1231+
// property metadata can be stripped even when the method body survives.
1232+
var instance = managerType.GetMethod("get_Instance")?.Invoke(null, null)
1233+
?? managerType.GetProperty("Instance")?.GetValue(null);
1234+
if (instance == null) return null;
1235+
return instance.GetType().GetMethod("GetEOSPlatformInterface")?.Invoke(instance, null);
1236+
}
1237+
1238+
// Sets distribution_platform = "epic" when the game was launched from the Epic
1239+
// Games Store launcher. Uses command-line args injected by the EGS launcher
1240+
// (-epicenv=, -epicapp=) rather than EOS-being-initialised, which would
1241+
// misattribute Steam games that use EOS purely for cross-play.
1242+
// Config override wins afterward.
1243+
private static void TryDetectEpicPlatform(Dictionary<string, object> properties)
1244+
{
1245+
try
1246+
{
1247+
if (IsLaunchedFromEpicGamesStore())
1248+
properties["distribution_platform"] = DistributionPlatforms.Epic;
1249+
}
1250+
catch (Exception ex)
1251+
{
1252+
Log.Warn(AudienceLogs.EpicPlatformDetectionFailed(ex));
1253+
}
1254+
}
1255+
1256+
// EGS launcher injects these args into every game it launches, regardless of
1257+
// whether the game integrates EOS. Absent when EOS is used for cross-play only.
1258+
// -EpicPortal is the strongest signal (bare flag, no value); -epicapp= and
1259+
// -epicenv= are the environment/artifact identifiers also always present.
1260+
private static bool IsLaunchedFromEpicGamesStore()
1261+
{
1262+
var args = Environment.GetCommandLineArgs();
1263+
foreach (var arg in args)
1264+
{
1265+
if (arg.Equals("-EpicPortal", StringComparison.OrdinalIgnoreCase) ||
1266+
arg.StartsWith("-epicenv=", StringComparison.OrdinalIgnoreCase) ||
1267+
arg.StartsWith("-epicapp=", StringComparison.OrdinalIgnoreCase))
1268+
return true;
1269+
}
1270+
return false;
1271+
}
1272+
1273+
// Calls Identify with the logged-in EOS ProductUserId.
1274+
// No-op if EOS is not present, not initialised, no user is logged in,
1275+
// or consent is below Full.
1276+
private static void TryIdentifyEpicUser()
1277+
{
1278+
try
1279+
{
1280+
if (!TryGetEpicAccountId(out var id))
1281+
return;
1282+
Log.Debug(AudienceLogs.EpicAutoIdentified(id!));
1283+
Identify(id!, IdentityType.Epic);
1284+
}
1285+
catch (Exception ex)
1286+
{
1287+
Log.Warn(AudienceLogs.EpicIdentityCollectionFailed(ex));
1288+
}
1289+
}
1290+
1291+
// Reads the EOS EpicAccountId via AuthInterface.GetLoggedInAccountByIndex(0).
1292+
// EpicAccountId is the player's Epic Games Account — consistent across all products.
1293+
// Requires the game to have already initialised EOS via EOSManager.
1294+
private static bool TryGetEpicAccountId(out string? id)
1295+
{
1296+
id = null;
1297+
1298+
// Guard: EOSSDK types must be present.
1299+
if (System.Type.GetType("Epic.OnlineServices.Auth.AuthInterface, EOSSDK") == null)
1300+
return false;
1301+
1302+
var platformInterface = GetEosPlatformInterface();
1303+
if (platformInterface == null) return false;
1304+
1305+
var authInterface = platformInterface.GetType()
1306+
.GetMethod("GetAuthInterface")?.Invoke(platformInterface, null);
1307+
if (authInterface == null) return false;
1308+
1309+
// Skip if no accounts are logged in.
1310+
var countResult = authInterface.GetType()
1311+
.GetMethod("GetLoggedInAccountsCount")?.Invoke(authInterface, null);
1312+
if (!(countResult is int count && count > 0)) return false;
1313+
1314+
// C# binding takes a plain int index, not an options struct.
1315+
var epicAccountId = authInterface.GetType()
1316+
.GetMethod("GetLoggedInAccountByIndex")?.Invoke(authInterface, new object[] { 0 });
1317+
if (epicAccountId == null) return false;
1318+
1319+
if (epicAccountId.GetType().GetMethod("IsValid")?.Invoke(epicAccountId, null) as bool? != true)
1320+
return false;
1321+
1322+
id = epicAccountId.ToString();
1323+
return !string.IsNullOrEmpty(id);
1324+
}
1325+
12171326
// consentAtInit only gates the launch; Track still checks live _state via CanTrack.
12181327
private static void FireGameLaunch(
12191328
AudienceConfig config,
@@ -1246,6 +1355,7 @@ private static void FireGameLaunch(
12461355

12471356
// Auto-detect distribution platform via reflection. Config override wins below.
12481357
TryDetectSteamPlatform(properties);
1358+
TryDetectEpicPlatform(properties);
12491359

12501360
// Config-supplied distributionPlatform overrides the auto-detected value.
12511361
if (config.DistributionPlatform != null)

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,5 +171,18 @@ internal static string SteamAutoIdentified(string steamId) =>
171171
internal static string SteamIdentityCollectionFailed(Exception ex) =>
172172
$"Steam identity collection threw {ex.GetType().Name}: {ex.Message}. " +
173173
"Steam user ID will not be auto-collected.";
174+
175+
// ---- Epic auto-detection ----
176+
177+
internal static string EpicPlatformDetectionFailed(Exception ex) =>
178+
$"Epic platform detection threw {ex.GetType().Name}: {ex.Message}. " +
179+
"distribution_platform will not be auto-set.";
180+
181+
internal static string EpicAutoIdentified(string epicId) =>
182+
$"auto-identified epic user: {epicId}";
183+
184+
internal static string EpicIdentityCollectionFailed(Exception ex) =>
185+
$"Epic identity collection threw {ex.GetType().Name}: {ex.Message}. " +
186+
"Epic user ID will not be auto-collected.";
174187
}
175188
}

src/Packages/Audience/link.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,11 @@ framework dependency.
4141
<assembly fullname="Facepunch.Steamworks.Posix" preserve="all" />
4242
<assembly fullname="Facepunch.Steamworks.Win64" preserve="all" />
4343
<assembly fullname="Facepunch.Steamworks.Win32" preserve="all" />
44+
45+
<!--
46+
Epic platform auto-detection. The SDK resolves these types via reflection
47+
at runtime; entries for missing assemblies are silently ignored.
48+
-->
49+
<assembly fullname="EOSSDK" preserve="all" />
50+
<assembly fullname="PlayEveryWare.EpicOnlineServices" preserve="all" />
4451
</linker>

0 commit comments

Comments
 (0)