Skip to content

Commit 31cef5e

Browse files
nattb8claude
andcommitted
feat(audience): auto-collect Epic account ID and detect Epic platform
- Calls AuthInterface.GetLoggedInAccountByIndex(0) after EOS login to get the player's EpicAccountId and fires an identify event with identity_type='epic'. - Sets distribution_platform='epic' when the game was launched from EGS (detected via -EpicPortal / -epicapp= / -epicenv= CLI args). - ResolveEosManagerType: added com.playeveryware.eos.core fallback so UPM installs of the PlayEveryWare plugin are found without Assembly-CSharp. - TryGetEpicAccountId guard: checks com.Epic.OnlineServices first (UPM install), then falls back to EOSSDK (legacy DLL install); previously only checked EOSSDK so UPM installs were silently skipped. - link.xml: changed from assembly-level to type-level preserve="all" for Epic types; assembly-level only protects type declarations, not method metadata, so GetLoggedInAccountsCount was stripped under IL2CPP High stripping and returned null from reflection. Added entries for both com.Epic.OnlineServices (UPM) and EOSSDK/PlayEveryWare (legacy). - examples/audience/Assets/link.xml: mirrors the same Epic rules because Unity's linker skips package link.xml when the package is referenced via a file: path outside the project tree. - examples: added MacOSBuilder headless build script, StandaloneAutoTest EOS Account Portal login path (--eos-account-portal), and .gitignore for EOS credential files. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent dc64354 commit 31cef5e

14 files changed

Lines changed: 519 additions & 1 deletion

File tree

examples/audience/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.env
2+
Assets/StreamingAssets/EOS/EpicOnlineServicesConfig.json
3+
Assets/StreamingAssets/EOS/eos_macos_config.json
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#nullable enable
2+
3+
using System;
4+
using UnityEditor;
5+
using UnityEngine;
6+
7+
namespace Immutable.Audience.Samples.SampleApp.Editor
8+
{
9+
// Invoked by CI / local debugging via:
10+
// Unity -batchmode -buildTarget StandaloneOSX \
11+
// -executeMethod Immutable.Audience.Samples.SampleApp.Editor.MacOSBuilder.Build \
12+
// -quit
13+
//
14+
// Optional CLI arg:
15+
// --buildPath <path> Output path for the .app (default: Builds/macOS/SampleApp.app)
16+
internal static class MacOSBuilder
17+
{
18+
private const string DefaultBuildPath = "Builds/macOS/SampleApp.app";
19+
20+
public static void Build()
21+
{
22+
string buildPath = GetArgValue("--buildPath") ?? DefaultBuildPath;
23+
24+
var options = new BuildPlayerOptions
25+
{
26+
scenes = new[] { "Assets/SampleApp/Scenes/SampleApp.unity" },
27+
locationPathName = buildPath,
28+
target = BuildTarget.StandaloneOSX,
29+
targetGroup = BuildTargetGroup.Standalone,
30+
options = BuildOptions.None,
31+
};
32+
33+
Debug.Log($"[MacOSBuilder] Building → {buildPath}");
34+
35+
var report = BuildPipeline.BuildPlayer(options);
36+
var summary = report.summary;
37+
38+
if (summary.result == UnityEditor.Build.Reporting.BuildResult.Succeeded)
39+
Debug.Log($"[MacOSBuilder] Build succeeded ({summary.totalSize / 1024 / 1024} MB).");
40+
else
41+
throw new Exception($"[MacOSBuilder] Build failed: {summary.result}");
42+
}
43+
44+
private static string? GetArgValue(string key)
45+
{
46+
var args = Environment.GetCommandLineArgs();
47+
for (int i = 0; i < args.Length - 1; i++)
48+
if (args[i] == key) return args[i + 1];
49+
return null;
50+
}
51+
}
52+
}

examples/audience/Assets/Editor/MacOSBuilder.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/audience/Assets/SampleApp/Scripts/Immutable.Audience.Samples.SampleApp.asmdef

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "Immutable.Audience.Samples.SampleApp",
33
"rootNamespace": "Immutable.Audience.Samples.SampleApp",
4-
"references": ["Immutable.Audience.Runtime"],
4+
"references": ["Immutable.Audience.Runtime", "com.playeveryware.eos.core", "com.Epic.OnlineServices"],
55
"includePlatforms": [],
66
"excludePlatforms": [],
77
"allowUnsafeCode": false,
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
#nullable enable
2+
3+
#if UNITY_STANDALONE_OSX || UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX
4+
5+
using System;
6+
using System.Threading.Tasks;
7+
using UnityEngine;
8+
using PlayEveryWare.EpicOnlineServices;
9+
using Epic.OnlineServices.Auth;
10+
using Epic.OnlineServices;
11+
12+
namespace Immutable.Audience.Samples.SampleApp
13+
{
14+
// Headless auto-test for standalone builds only. Activated by passing:
15+
// --auto-test <publishableKey>
16+
// on the command line. Injects itself at runtime; no scene changes needed.
17+
// Runs Init (Full consent) → Track → Flush → Quit.
18+
public class StandaloneAutoTest : MonoBehaviour
19+
{
20+
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
21+
private static void Bootstrap()
22+
{
23+
if (string.IsNullOrEmpty(GetArgValue("--auto-test"))) return;
24+
var go = new GameObject("[AutoTest]");
25+
DontDestroyOnLoad(go);
26+
go.AddComponent<StandaloneAutoTest>();
27+
}
28+
29+
private async void Start()
30+
{
31+
var key = GetArgValue("--auto-test")!;
32+
Debug.Log("[AutoTest] Starting headless test run");
33+
34+
// If Facepunch.Steamworks is present, call SteamClient.Init before the SDK runs.
35+
TryInitFacepunch();
36+
37+
// EOS login: --eos-account-portal opens Epic portal in browser (no DevAuthTool needed).
38+
// --eos-dev-auth <host:port> uses DevAuthTool with --eos-credential <name>.
39+
if (HasFlag("--eos-account-portal"))
40+
{
41+
var ok = await InitEosWithAccountPortal();
42+
if (!ok)
43+
{
44+
Debug.LogError("[AutoTest] EOS init/login failed — aborting");
45+
Application.Quit(1);
46+
return;
47+
}
48+
}
49+
else
50+
{
51+
var devAuthArg = GetArgValue("--eos-dev-auth");
52+
if (devAuthArg != null)
53+
{
54+
var parts = devAuthArg.Split(':');
55+
var host = parts[0];
56+
var port = parts.Length > 1 ? parts[1] : "7878";
57+
var credential = GetArgValue("--eos-credential") ?? "test";
58+
var ok = await InitEosWithDevAuthTool($"{host}:{port}", credential);
59+
if (!ok)
60+
{
61+
Debug.LogError("[AutoTest] EOS init/login failed — aborting");
62+
Application.Quit(1);
63+
return;
64+
}
65+
}
66+
}
67+
68+
var config = new AudienceConfig
69+
{
70+
PublishableKey = key,
71+
Consent = ConsentLevel.Full,
72+
Debug = true,
73+
TestMode = true,
74+
};
75+
76+
ImmutableAudience.Init(config);
77+
Debug.Log("[AutoTest] Init done, consent=Full");
78+
79+
ImmutableAudience.Track("auto_test_steam_detection");
80+
Debug.Log("[AutoTest] Track sent");
81+
82+
await Task.Delay(2000);
83+
84+
try
85+
{
86+
await ImmutableAudience.FlushAsync();
87+
Debug.Log("[AutoTest] Flush done");
88+
}
89+
catch (Exception ex)
90+
{
91+
Debug.LogError("[AutoTest] Flush failed: " + ex.Message);
92+
}
93+
94+
await Task.Delay(1000);
95+
Debug.Log("[AutoTest] Done. Check BQ for auto_test_steam_detection + identify.");
96+
Application.Quit();
97+
}
98+
99+
// Initialises EOSManager and logs in via Epic Account Portal (browser). Returns true on success.
100+
private static async Task<bool> InitEosWithAccountPortal()
101+
{
102+
Debug.Log("[AutoTest] Initialising EOS, login via Account Portal (browser will open)");
103+
var go = new GameObject("[EOSManager]");
104+
UnityEngine.Object.DontDestroyOnLoad(go);
105+
go.AddComponent<EOSManager>();
106+
107+
var deadline = DateTime.UtcNow.AddSeconds(10);
108+
while (EOSManager.Instance.GetEOSPlatformInterface() == null)
109+
{
110+
if (DateTime.UtcNow > deadline)
111+
{
112+
Debug.LogError("[AutoTest] Timed out waiting for EOS platform interface");
113+
return false;
114+
}
115+
await Task.Delay(200);
116+
}
117+
Debug.Log("[AutoTest] EOS platform interface ready — complete login in browser");
118+
119+
var tcs = new TaskCompletionSource<bool>();
120+
EOSManager.Instance.StartLoginWithLoginTypeAndToken(
121+
LoginCredentialType.AccountPortal,
122+
null,
123+
null,
124+
(LoginCallbackInfo info) =>
125+
{
126+
if (info.ResultCode == Result.Success)
127+
{
128+
Debug.Log("[AutoTest] EOS Account Portal login success");
129+
tcs.SetResult(true);
130+
}
131+
else
132+
{
133+
Debug.LogError($"[AutoTest] EOS login failed: {info.ResultCode}");
134+
tcs.SetResult(false);
135+
}
136+
});
137+
138+
// Wait up to 5 minutes for the user to complete the browser login.
139+
if (await Task.WhenAny(tcs.Task, Task.Delay(TimeSpan.FromMinutes(5))) != tcs.Task)
140+
{
141+
Debug.LogError("[AutoTest] Timed out waiting for Account Portal login");
142+
return false;
143+
}
144+
return await tcs.Task;
145+
}
146+
147+
// Initialises EOSManager and logs in via the DevAuthTool at host:port with the
148+
// given credential name. Returns true on success.
149+
private static async Task<bool> InitEosWithDevAuthTool(string hostPort, string credential)
150+
{
151+
Debug.Log($"[AutoTest] Initialising EOS, DevAuthTool={hostPort} credential={credential}");
152+
var go = new GameObject("[EOSManager]");
153+
UnityEngine.Object.DontDestroyOnLoad(go);
154+
go.AddComponent<EOSManager>();
155+
156+
// Wait for platform interface (EOSManager.Awake calls Init synchronously).
157+
var deadline = DateTime.UtcNow.AddSeconds(10);
158+
while (EOSManager.Instance.GetEOSPlatformInterface() == null)
159+
{
160+
if (DateTime.UtcNow > deadline)
161+
{
162+
Debug.LogError("[AutoTest] Timed out waiting for EOS platform interface");
163+
return false;
164+
}
165+
await Task.Delay(200);
166+
}
167+
Debug.Log("[AutoTest] EOS platform interface ready");
168+
169+
var tcs = new TaskCompletionSource<bool>();
170+
EOSManager.Instance.StartLoginWithLoginTypeAndToken(
171+
LoginCredentialType.Developer,
172+
hostPort,
173+
credential,
174+
(LoginCallbackInfo info) =>
175+
{
176+
if (info.ResultCode == Result.Success)
177+
{
178+
Debug.Log("[AutoTest] EOS DevAuthTool login success");
179+
tcs.SetResult(true);
180+
}
181+
else
182+
{
183+
Debug.LogError($"[AutoTest] EOS login failed: {info.ResultCode}");
184+
tcs.SetResult(false);
185+
}
186+
});
187+
188+
return await tcs.Task;
189+
}
190+
191+
// Initialises Facepunch.Steamworks via reflection so the test harness works
192+
// without a direct compile-time dependency. No-ops if Facepunch is not present.
193+
private static void TryInitFacepunch()
194+
{
195+
var steamClientType =
196+
System.Type.GetType("Steamworks.SteamClient, Facepunch.Steamworks.Posix")
197+
?? System.Type.GetType("Steamworks.SteamClient, Facepunch.Steamworks.Win64")
198+
?? System.Type.GetType("Steamworks.SteamClient, Facepunch.Steamworks.Win32");
199+
200+
if (steamClientType == null) return;
201+
202+
try
203+
{
204+
var initMethod = steamClientType.GetMethod("Init", new Type[] { typeof(uint), typeof(bool) });
205+
initMethod?.Invoke(null, new object[] { (uint)480, false });
206+
Debug.Log("[AutoTest] Facepunch SteamClient.Init(480) called");
207+
}
208+
catch (Exception ex)
209+
{
210+
Debug.LogWarning("[AutoTest] Facepunch SteamClient.Init failed: " + ex.Message);
211+
}
212+
}
213+
214+
private static string? GetArgValue(string key)
215+
{
216+
var args = Environment.GetCommandLineArgs();
217+
for (int i = 0; i < args.Length - 1; i++)
218+
if (args[i] == key) return args[i + 1];
219+
return null;
220+
}
221+
222+
private static bool HasFlag(string key)
223+
{
224+
foreach (var arg in Environment.GetCommandLineArgs())
225+
if (arg == key) return true;
226+
return false;
227+
}
228+
229+
}
230+
}
231+
232+
#endif

examples/audience/Assets/SampleApp/Scripts/StandaloneAutoTest.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/audience/Assets/StreamingAssets.meta

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/audience/Assets/StreamingAssets/EOS.meta

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/audience/Assets/StreamingAssets/EOS/EpicOnlineServicesConfig.json.meta

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/audience/Assets/StreamingAssets/EOS/eos_macos_config.json.meta

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)