Skip to content

Commit 92d670a

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 f4651bc commit 92d670a

6 files changed

Lines changed: 1388 additions & 0 deletions

File tree

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

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)