Skip to content

Commit 9e825b9

Browse files
feat(audience-sample-app): implement sample across three partials
Three partials of the AudienceSample MonoBehaviour, each with one concern: * AudienceSample.cs SDK calls, On* handlers, mirror state, SDK callbacks (OnError, Log.Writer adapter), config builders. Reads UXML state ONLY via UI's Capture*Form accessors — never touches a UXML field directly. * AudienceSample.UI.cs UXML fields, binding, rendering, log pane mechanics, drag-resize, Refresh* methods, Capture*Form accessors. No SDK calls. * AudienceSample.Events.cs Catalogue (typed + string events), typed- event factory, props builder. Pure factory: no UXML, no SDK. Form-snapshot DTOs (InitForm, IdentifyForm, AliasForm, CustomEventForm) hand button-click state from UI to main. Refresh* fan-out collapses to OnSdkStateChanged. Threading: SDK OnError fires from background flush threads; AppendLog captures SynchronizationContext + main thread id at startup and re-dispatches via SyncContext.Post on off-main entry. Required because _logView.schedule.Execute silently drops off-main callers in 2021.3 runtime panels. The log pane caps at 200 rows (oldest evicted on overflow) with stateless per-row "was at bottom?" auto-scroll. Each row stashes its LogEntry on userData so the visual tree IS the log data — no parallel buffer to maintain.
1 parent 58d1546 commit 9e825b9

7 files changed

Lines changed: 1403 additions & 2 deletions

File tree

examples/audience/Assets/SampleApp/Resources/AudienceSample.uxml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,11 +235,11 @@
235235
<ui:VisualElement name="identity-state-list" class="identity-state-list">
236236
<ui:VisualElement class="identity-state-row">
237237
<ui:Label class="identity-state-key" text="USERID" />
238-
<ui:Label name="identity-userId" class="identity-state-value" text="—" />
238+
<ui:Label name="identity-user-id" class="identity-state-value" text="—" />
239239
</ui:VisualElement>
240240
<ui:VisualElement class="identity-state-row">
241241
<ui:Label class="identity-state-key" text="IDENTITYTYPE" />
242-
<ui:Label name="identity-identityType" class="identity-state-value" text="—" />
242+
<ui:Label name="identity-identity-type" class="identity-state-value" text="—" />
243243
</ui:VisualElement>
244244
<ui:VisualElement class="identity-state-row">
245245
<ui:Label class="identity-state-key" text="TRAITS" />
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
#nullable enable
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Globalization;
6+
using UnityEngine.UIElements;
7+
8+
namespace Immutable.Audience.Samples.SampleApp
9+
{
10+
// Events partial of AudienceSample — see AudienceSample.cs for the partial layout.
11+
public sealed partial class AudienceSample
12+
{
13+
// ---- Event DSL types ----
14+
15+
internal enum FieldKind { String, Number, Enum }
16+
17+
internal readonly struct EventField
18+
{
19+
public readonly string Key;
20+
public readonly FieldKind Kind;
21+
public readonly bool Optional;
22+
public readonly string[]? EnumValues;
23+
24+
private EventField(string key, FieldKind kind, bool optional, string[]? enumValues)
25+
{ Key = key; Kind = kind; Optional = optional; EnumValues = enumValues; }
26+
27+
public static EventField Text(string key, bool optional = false) => new EventField(key, FieldKind.String, optional, null);
28+
public static EventField Number(string key, bool optional = false) => new EventField(key, FieldKind.Number, optional, null);
29+
public static EventField Enum(string key, string[] values, bool optional = false) => new EventField(key, FieldKind.Enum, optional, values);
30+
}
31+
32+
internal readonly struct EventSpec
33+
{
34+
public readonly string Name;
35+
public readonly EventField[] Fields;
36+
public EventSpec(string name, EventField[] fields) { Name = name; Fields = fields; }
37+
}
38+
39+
private const string OptionalEnumSentinel = "(not set)";
40+
41+
// ---- Event catalogue ----
42+
43+
internal static readonly EventSpec[] Catalogue =
44+
{
45+
new EventSpec("sign_up", new[] { EventField.Text("method", optional: true) }),
46+
new EventSpec("sign_in", new[] { EventField.Text("method", optional: true) }),
47+
new EventSpec("email_acquired", new[] { EventField.Text("source", optional: true) }),
48+
new EventSpec("wishlist_add", new[] {
49+
EventField.Text("gameId"),
50+
EventField.Text("source", optional: true),
51+
EventField.Text("platform", optional: true),
52+
}),
53+
new EventSpec("wishlist_remove", new[] { EventField.Text("gameId") }),
54+
new EventSpec("purchase", new[] {
55+
EventField.Text("currency"),
56+
EventField.Number("value"),
57+
EventField.Text("itemId", optional: true),
58+
EventField.Text("itemName", optional: true),
59+
EventField.Number("quantity", optional: true),
60+
EventField.Text("transactionId", optional: true),
61+
}),
62+
// game_launch is deliberately absent. The Event Reference v1 defines
63+
// it as auto-tracked on Init with no public typed class; firing it
64+
// from the Send button would double-emit.
65+
new EventSpec("progression", new[] {
66+
EventField.Enum("status", new[] { "start", "complete", "fail" }),
67+
EventField.Text("world", optional: true),
68+
EventField.Text("level", optional: true),
69+
EventField.Text("stage", optional: true),
70+
EventField.Number("score", optional: true),
71+
EventField.Number("durationSec", optional: true),
72+
}),
73+
new EventSpec("resource", new[] {
74+
EventField.Enum("flow", new[] { "sink", "source" }),
75+
EventField.Text("currency"),
76+
EventField.Number("amount"),
77+
EventField.Text("itemType", optional: true),
78+
EventField.Text("itemId", optional: true),
79+
}),
80+
new EventSpec("milestone_reached", new[] { EventField.Text("name") }),
81+
new EventSpec("game_page_viewed", new[] {
82+
EventField.Text("gameId"),
83+
EventField.Text("gameName", optional: true),
84+
EventField.Text("slug", optional: true),
85+
}),
86+
new EventSpec("link_clicked", new[] {
87+
EventField.Text("url"),
88+
EventField.Text("label", optional: true),
89+
EventField.Text("source", optional: true),
90+
EventField.Text("gameId", optional: true),
91+
}),
92+
};
93+
94+
// ---- Typed event construction ----
95+
96+
// Returns null for events not covered by the typed surface; callers
97+
// fall back to the string overload.
98+
private static IEvent? BuildTypedEvent(string name, Dictionary<string, object> props)
99+
{
100+
switch (name)
101+
{
102+
case "progression":
103+
return new Progression
104+
{
105+
Status = ParseProgressionStatus(props),
106+
World = OptionalString(props, "world"),
107+
Level = OptionalString(props, "level"),
108+
Stage = OptionalString(props, "stage"),
109+
Score = OptionalInt(props, "score"),
110+
DurationSec = OptionalFloat(props, "durationSec"),
111+
};
112+
case "resource":
113+
return new Resource
114+
{
115+
Flow = ParseResourceFlow(props),
116+
Currency = OptionalString(props, "currency") ?? "",
117+
Amount = OptionalFloat(props, "amount") ?? 0f,
118+
ItemType = OptionalString(props, "itemType"),
119+
ItemId = OptionalString(props, "itemId"),
120+
};
121+
case "purchase":
122+
return new Purchase
123+
{
124+
Currency = OptionalString(props, "currency") ?? "",
125+
Value = OptionalDecimal(props, "value") ?? 0m,
126+
ItemId = OptionalString(props, "itemId"),
127+
ItemName = OptionalString(props, "itemName"),
128+
Quantity = OptionalInt(props, "quantity"),
129+
TransactionId = OptionalString(props, "transactionId"),
130+
};
131+
case "milestone_reached":
132+
return new MilestoneReached { Name = OptionalString(props, "name") ?? "" };
133+
default:
134+
return null;
135+
}
136+
}
137+
138+
private static ProgressionStatus? ParseProgressionStatus(Dictionary<string, object> props)
139+
{
140+
var s = OptionalString(props, "status");
141+
if (string.IsNullOrEmpty(s)) return null;
142+
return s switch
143+
{
144+
"start" => ProgressionStatus.Start,
145+
"complete" => ProgressionStatus.Complete,
146+
"fail" => ProgressionStatus.Fail,
147+
_ => (ProgressionStatus?)null,
148+
};
149+
}
150+
151+
private static ResourceFlow? ParseResourceFlow(Dictionary<string, object> props)
152+
{
153+
var s = OptionalString(props, "flow");
154+
if (string.IsNullOrEmpty(s)) return null;
155+
return s switch
156+
{
157+
"source" => ResourceFlow.Source,
158+
"sink" => ResourceFlow.Sink,
159+
_ => (ResourceFlow?)null,
160+
};
161+
}
162+
163+
private static string? OptionalString(Dictionary<string, object> props, string key) =>
164+
props.TryGetValue(key, out var v) && v is string s && !string.IsNullOrEmpty(s) ? s : null;
165+
166+
private static int? OptionalInt(Dictionary<string, object> props, string key) =>
167+
props.TryGetValue(key, out var v) ? v switch { int i => i, double d => (int)d, _ => (int?)null } : null;
168+
169+
private static float? OptionalFloat(Dictionary<string, object> props, string key) =>
170+
props.TryGetValue(key, out var v) ? v switch { double d => (float)d, int i => (float)i, _ => (float?)null } : null;
171+
172+
private static decimal? OptionalDecimal(Dictionary<string, object> props, string key) =>
173+
props.TryGetValue(key, out var v) ? v switch { double d => (decimal)d, int i => (decimal)i, _ => (decimal?)null } : null;
174+
175+
// ---- Property collection (UI form inputs → props dictionary) ----
176+
177+
// Number kinds parse via invariant culture (no locale commas) and
178+
// collapse non-fractional doubles to int — keeps Quantity / Score off
179+
// the wire as 1 instead of 1.0.
180+
private static Dictionary<string, object> BuildPropsDictionary(EventSpec spec, Dictionary<string, VisualElement> inputs)
181+
{
182+
var props = new Dictionary<string, object>();
183+
foreach (var field in spec.Fields)
184+
{
185+
string raw = inputs[field.Key] switch
186+
{
187+
DropdownField dd when dd.value != OptionalEnumSentinel => dd.value ?? "",
188+
DropdownField _ => "",
189+
TextField tf => tf.value ?? "",
190+
_ => "",
191+
};
192+
if (string.IsNullOrEmpty(raw)) continue;
193+
if (field.Kind == FieldKind.Number)
194+
{
195+
if (!double.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out var n)) continue;
196+
props[field.Key] = (Math.Abs(n % 1) < double.Epsilon && Math.Abs(n) < int.MaxValue) ? (object)(int)n : n;
197+
}
198+
else props[field.Key] = raw;
199+
}
200+
return props;
201+
}
202+
}
203+
}

examples/audience/Assets/SampleApp/Scripts/AudienceSample.Events.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.

0 commit comments

Comments
 (0)