Spun off from #41 — the lifecycle conversion's remaining piece.
Problem
`BugpunchClient.cs` still inherits `MonoBehaviour` for one reason: 10 `UnitySendMessage` receiver methods that native Java + Obj-C++ call into. Without these, BugpunchClient could be a plain static-state holder.
Receivers in BugpunchClient.cs:
- `OnPollUpgradeRequested(string)` — native poller flips upgradeToWebSocket flag.
- `OnShowChatBoardRequested(string)` — legacy native shell entry.
- `OnRecordingBarChatTapped(string)` — native recording bar chat icon.
- `OnChatBannerDismissed(string)` — native chat banner X tap.
- `OnChatBannerOpened(string)` — native chat banner body tap.
- `OnRecordingBarFeedbackTapped(string)` — native recording bar feedback icon.
- `DirectiveRunScript(string)` — native crash-directive run_script action.
- `OnPollScripts(string)` — poll response with scheduled scripts.
- `OnApprovedScriptRequest(string)` — native chat scriptRequest approval.
- `OnApprovedDataRequest(string)` — native chat dataRequest approval.
Plus `gameObject.AddComponent()` in the streamer lazy-init path needs a host GameObject for the streamer MonoBehaviour.
Why split off
#41 closed the four big lifecycle conversions:
- `Update` → PlayerLoop entry.
- `OnApplicationPause` → `Application.focusChanged`.
- `OnDestroy` → `Application.quitting` + idempotent `Teardown()`.
- 4 `StartCoroutine` poll loops → async Tasks.
Plus three siblings (`BugpunchSceneTick`, `BugpunchSurfaceRecorder`, `CrashDirectiveHandler`) fully converted to static classes.
The receiver migration is a different shape of work — coordinated native + C# changes — and needs per-Unity-version validation that's better scoped on its own.
Scope
Android (AndroidJavaProxy)
For each receiver, replace native-side calls of `BugpunchUnity.sendMessage("BugpunchClient", "X", json)` with a Java callback interface that the C# side registers via `AndroidJavaProxy`.
Sketch:
// In BugpunchPoller.java / BugpunchChatBanner.java / BugpunchReportOverlay.java etc.
public interface BugpunchCallbacks {
void onPollUpgradeRequested();
void onChatBannerDismissed();
void onChatBannerOpened();
void directiveRunScript(String payload);
// ... one method per receiver ...
}
private static volatile BugpunchCallbacks sCallbacks;
public static void registerCallbacks(BugpunchCallbacks cb) {
sCallbacks = cb;
}
// Call site replaces BugpunchUnity.sendMessage(\"BugpunchClient\", \"DirectiveRunScript\", payload):
BugpunchCallbacks cb = sCallbacks;
if (cb != null) cb.directiveRunScript(payload);
C# side:
class BugpunchCallbacks : AndroidJavaProxy
{
public BugpunchCallbacks() : base(\"au.com.oddgames.bugpunch.BugpunchCallbacks\") { }
void onPollUpgradeRequested() => BugpunchClient.HandleUpgradeToWebSocket();
void onChatBannerDismissed() => BugpunchPoller.NotifyBannerDismissed();
void onChatBannerOpened() => BugpunchPoller.NotifyBannerOpened();
void directiveRunScript(string payload) => CrashDirectiveHandler.RunScript(payload);
// ...
}
Risk: AndroidJavaProxy under IL2CPP has known historical bugs (delegate marshalling, method resolution, threading) that need per-Unity-version validation. Targets to confirm: 2022.3 LTS, 6000.0 LTS, 6000.x current.
iOS (P/Invoke callbacks)
For each receiver, the existing `UnitySendMessage("BugpunchClient", "X", json)` calls become P/Invoke callback dispatches. The C# side registers a static delegate at startup; the native side stores the function pointer and calls it directly.
Sketch:
// In BugpunchPoller.mm / BugpunchChatBanner.mm etc.
typedef void (*BPCallbackString)(const char* payload);
typedef void (*BPCallbackVoid)(void);
static BPCallbackVoid s_onPollUpgrade;
static BPCallbackString s_directiveRunScript;
// ... one per receiver ...
extern \"C\" void Bugpunch_RegisterCallbacks(
BPCallbackVoid onPollUpgrade,
BPCallbackVoid onChatBannerDismissed,
BPCallbackVoid onChatBannerOpened,
BPCallbackString directiveRunScript,
// ...
) {
s_onPollUpgrade = onPollUpgrade;
// ...
}
C# side (P/Invoke decls + delegate registration):
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void BPVoidCallback();
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] delegate void BPStringCallback(IntPtr utf8);
[DllImport(\"__Internal\")] static extern void Bugpunch_RegisterCallbacks(
BPVoidCallback onPollUpgrade,
BPVoidCallback onChatBannerDismissed,
BPVoidCallback onChatBannerOpened,
BPStringCallback directiveRunScript
// ...
);
[AOT.MonoPInvokeCallback(typeof(BPVoidCallback))]
static void OnPollUpgrade() => BugpunchClient.HandleUpgradeToWebSocket();
[AOT.MonoPInvokeCallback(typeof(BPStringCallback))]
static void DirectiveRunScript(IntPtr payloadPtr)
{
var payload = Marshal.PtrToStringUTF8(payloadPtr);
CrashDirectiveHandler.RunScript(payload);
}
Risk: `MonoPInvokeCallback` requires static methods. State-bearing callbacks need to look up the active client via `BugpunchClient.Instance` (or a static dispatcher). Already true after #41's lifecycle conversions made everything static-friendly.
C# side cleanup
Once both lanes register callbacks instead of sending Unity messages:
- Drop `: MonoBehaviour` from `BugpunchClient`. Convert remaining instance fields to static. Adjust `Initialize` to allocate a regular C# object instead of `AddComponent`.
- The host GameObject still exists (the IDE service MonoBehaviours `HierarchyService`, `WebRTCStreamer`, etc. need a host for `AddComponent`). Rename it to `[Bugpunch Services Host]` and decouple BugpunchClient from it.
- `Disconnect()` keeps a static reference to the host GameObject for explicit teardown via `UnityEngine.Object.Destroy`.
Out of scope
- Migrating the IDE service MonoBehaviours (`HierarchyService`, `WebRTCStreamer`, etc.) themselves to static classes. Those are insulated from QA-script Destroy by living on a shared host GameObject; can be a follow-up if the AddComponent host bothers anyone.
- Per-receiver behavioural changes — strict 1:1 migration. Any UX tweaks are separate.
Acceptance criteria
- All 10 receivers in BugpunchClient.cs replaced with native callback registration paths (Android + iOS).
- BugpunchClient drops `: MonoBehaviour`, becomes static-state-bearing class.
- Verified on Unity 2022.3 LTS, 6000.0 LTS, 6000.x current — IL2CPP build, AndroidJavaProxy + MonoPInvokeCallback both wired and functional.
- No regression in: poll-mode upgrade, chat banner dismiss/open, recording-bar chat / feedback taps, directive run-script, scheduled-script execution, approved chat script / data requests.
Spun off from #41 — the lifecycle conversion's remaining piece.
Problem
`BugpunchClient.cs` still inherits `MonoBehaviour` for one reason: 10 `UnitySendMessage` receiver methods that native Java + Obj-C++ call into. Without these, BugpunchClient could be a plain static-state holder.
Receivers in BugpunchClient.cs:
Plus `gameObject.AddComponent()` in the streamer lazy-init path needs a host GameObject for the streamer MonoBehaviour.
Why split off
#41 closed the four big lifecycle conversions:
Plus three siblings (`BugpunchSceneTick`, `BugpunchSurfaceRecorder`, `CrashDirectiveHandler`) fully converted to static classes.
The receiver migration is a different shape of work — coordinated native + C# changes — and needs per-Unity-version validation that's better scoped on its own.
Scope
Android (AndroidJavaProxy)
For each receiver, replace native-side calls of `BugpunchUnity.sendMessage("BugpunchClient", "X", json)` with a Java callback interface that the C# side registers via `AndroidJavaProxy`.
Sketch:
C# side:
Risk: AndroidJavaProxy under IL2CPP has known historical bugs (delegate marshalling, method resolution, threading) that need per-Unity-version validation. Targets to confirm: 2022.3 LTS, 6000.0 LTS, 6000.x current.
iOS (P/Invoke callbacks)
For each receiver, the existing `UnitySendMessage("BugpunchClient", "X", json)` calls become P/Invoke callback dispatches. The C# side registers a static delegate at startup; the native side stores the function pointer and calls it directly.
Sketch:
C# side (P/Invoke decls + delegate registration):
Risk: `MonoPInvokeCallback` requires static methods. State-bearing callbacks need to look up the active client via `BugpunchClient.Instance` (or a static dispatcher). Already true after #41's lifecycle conversions made everything static-friendly.
C# side cleanup
Once both lanes register callbacks instead of sending Unity messages:
Out of scope
Acceptance criteria