mirror of
https://github.com/djdevin/recnet-plugin.git
synced 2026-09-08 14:41:30 -07:00
attempting to patch some more noise calls
This commit is contained in:
+171
-7
@@ -1,7 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using BestHTTP;
|
||||
using HarmonyLib;
|
||||
using Il2CppInterop.Runtime.InteropTypes.Arrays;
|
||||
|
||||
namespace RecNetPlugin.Patches;
|
||||
|
||||
@@ -9,8 +13,9 @@ namespace RecNetPlugin.Patches;
|
||||
// for (and which leaks play data off-box). Prefix every event-logging entrypoint and swallow the call
|
||||
// so nothing is ever queued, batched or sent.
|
||||
//
|
||||
// One knob, see [Analytics] in the .cfg:
|
||||
// Disable Amplitude Analytics -> default true; skip every AmplitudeAnalyticsClient.Log* method.
|
||||
// One knob for all telemetry, `[Analytics] Disable Telemetry`, default true — it gates this file plus
|
||||
// BacktracePatch and UnityTelemetryPatch. Deliberately not split per vendor: nobody wants Amplitude
|
||||
// gone but the collector alive, and four switches for one intention is four ways to be half-configured.
|
||||
//
|
||||
// Target resolution: AmplitudeAnalytics.AmplitudeAnalyticsClient in RecRoom.Analytics.Runtime.dll,
|
||||
// concrete (it derives from SingletonMonoBehaviour<T>, so there is no abstract-interface dispatch
|
||||
@@ -24,6 +29,15 @@ namespace RecNetPlugin.Patches;
|
||||
// never even called. Those events are the pre-serialized batch the client parks in the
|
||||
// `pending_room_stats` PlayerPref (visible in the DUID-PROBE log), which points at
|
||||
// LogSerializedEventAsync rather than LogEventAsync.
|
||||
//
|
||||
// Why the Log* prefixes alone STILL are not enough — and why `BlockAnalyticsUploadPatch` below is the
|
||||
// part that actually stops the traffic: uploads to api2.amplitude.com kept showing up in mitmproxy
|
||||
// with LogEventAsync/LogIdentifyAsync visibly blocked in LogOutput.log. The Log* methods only *queue*;
|
||||
// the queue is persisted (`pending_room_stats`) and drained later by the client's own flush coroutines
|
||||
// (Flush / AMEAMPDLJPN / PPOCFIHNKPP), which reach the network through the transport interface
|
||||
// `FOMPBHDLPDO` — concrete impl `MHBPNDOGOLG` in RecNet.Runtime.dll, i.e. BestHTTP. So a batch queued
|
||||
// in an earlier session ships on the next launch no matter what we do to the Log* doors. Blocking at
|
||||
// the BestHTTP layer catches every path, present and future, and costs no obfuscated names.
|
||||
[HarmonyPatch]
|
||||
public static class AmplitudePatch
|
||||
{
|
||||
@@ -42,24 +56,24 @@ public static class AmplitudePatch
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(AmplitudeAnalytics.AmplitudeAnalyticsClient), "LogEventAsync")]
|
||||
private static bool LogEventAsyncPrefix() =>
|
||||
Plugin.DisableAmplitudeAnalytics.Value && Block("LogEventAsync");
|
||||
Plugin.DisableTelemetry.Value && Block("LogEventAsync");
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(AmplitudeAnalytics.AmplitudeAnalyticsClient), "LogPrevSessionEventAsync")]
|
||||
private static bool LogPrevSessionEventAsyncPrefix() =>
|
||||
Plugin.DisableAmplitudeAnalytics.Value && Block("LogPrevSessionEventAsync");
|
||||
Plugin.DisableTelemetry.Value && Block("LogPrevSessionEventAsync");
|
||||
|
||||
// The likely culprit for the room_stats/perf_stats batch — takes the already-serialized
|
||||
// Dictionary<string, object> that gets parked in `pending_room_stats`.
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(AmplitudeAnalytics.AmplitudeAnalyticsClient), "LogSerializedEventAsync")]
|
||||
private static bool LogSerializedEventAsyncPrefix() =>
|
||||
Plugin.DisableAmplitudeAnalytics.Value && Block("LogSerializedEventAsync");
|
||||
Plugin.DisableTelemetry.Value && Block("LogSerializedEventAsync");
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(AmplitudeAnalytics.AmplitudeAnalyticsClient), "LogIdentifyAsync")]
|
||||
private static bool LogIdentifyAsyncPrefix() =>
|
||||
Plugin.DisableAmplitudeAnalytics.Value && Block("LogIdentifyAsync");
|
||||
Plugin.DisableTelemetry.Value && Block("LogIdentifyAsync");
|
||||
|
||||
// The odd one out: static, and it returns a promise instead of void. Skipping it with a null
|
||||
// __result would hand the caller something it will chain .Then() on, so we substitute an
|
||||
@@ -69,7 +83,7 @@ public static class AmplitudePatch
|
||||
[HarmonyPatch(typeof(AmplitudeAnalytics.AmplitudeAnalyticsClient), "LogOutOfSessionEvent")]
|
||||
private static bool LogOutOfSessionEventPrefix(ref LAHBDKNMNHN __result)
|
||||
{
|
||||
if (!Plugin.DisableAmplitudeAnalytics.Value)
|
||||
if (!Plugin.DisableTelemetry.Value)
|
||||
return true;
|
||||
|
||||
var resolved = ResolvedPromise();
|
||||
@@ -116,4 +130,154 @@ public static class AmplitudePatch
|
||||
|
||||
return _resolvedPromiseGetter.Invoke(null, null) as LAHBDKNMNHN;
|
||||
}
|
||||
|
||||
// Analytics hosts we refuse to talk to, matched two different ways because they identify
|
||||
// themselves two different ways:
|
||||
//
|
||||
// AmplitudeDomains — a registrable domain plus everything under it, so api2.amplitude.com
|
||||
// and api.eu.amplitude.com are both covered.
|
||||
// CollectorLabelPrefixes — the *first* hostname label, for the telemetry collector that lives
|
||||
// on whatever domain the deployment uses (datacollection.recflare.net,
|
||||
// datacollection.rec.net, datacollection-eu.…). The domain varies, the
|
||||
// label doesn't, so match on the label and stay deployment-agnostic.
|
||||
//
|
||||
// Split into separate lists because they're *matched* differently, not configured differently —
|
||||
// one knob covers the lot.
|
||||
private static readonly string[] AmplitudeDomains = { "amplitude.com" };
|
||||
private static readonly string[] CollectorLabelPrefixes = { "datacollection" };
|
||||
|
||||
// Backtrace and Unity's perf-events don't normally reach BestHTTP at all — Backtrace goes through
|
||||
// UnityWebRequest (see BacktracePatch) and Unity's is native (see UnityTelemetryPatch). They're
|
||||
// listed here anyway because it costs a string comparison to be right if that ever changes, and
|
||||
// because "the host block covers every host we don't want talked to" is easier to reason about
|
||||
// than a list with holes in it. Only perf-events is named, not all of cloud.unity3d.com — the
|
||||
// client uses other Unity services.
|
||||
private static readonly string[] BacktraceDomains = { "backtrace.io" };
|
||||
private static readonly string[] UnityTelemetryHosts = { "perf-events.cloud.unity3d.com" };
|
||||
|
||||
private static bool IsUnderAnyDomain(string host, string[] domains) =>
|
||||
domains.Any(d => host.Equals(d, StringComparison.OrdinalIgnoreCase)
|
||||
|| host.EndsWith("." + d, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
private static bool IsAmplitudeHost(string host) => IsUnderAnyDomain(host, AmplitudeDomains);
|
||||
|
||||
private static bool IsCollectorHost(string host)
|
||||
{
|
||||
var dot = host.IndexOf('.');
|
||||
var firstLabel = dot < 0 ? host : host.Substring(0, dot);
|
||||
|
||||
return CollectorLabelPrefixes.Any(p => firstLabel.StartsWith(p, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
// One knob for the lot. The vendors are split into separate lists above because they're matched
|
||||
// differently, not because they're configured differently.
|
||||
private static bool IsBlockedHost(string host)
|
||||
{
|
||||
if (string.IsNullOrEmpty(host) || !Plugin.DisableTelemetry.Value)
|
||||
return false;
|
||||
|
||||
return IsAmplitudeHost(host)
|
||||
|| IsCollectorHost(host)
|
||||
|| IsUnderAnyDomain(host, BacktraceDomains)
|
||||
|| UnityTelemetryHosts.Any(h => host.Equals(h, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
// The part that actually stops the traffic — see the note at the top of the file. Prefixes the
|
||||
// same BestHTTP entrypoint SendRequestPatch hooks (HTTPManager is BestHTTP's own type, so no
|
||||
// obfuscated names are involved and this survives game upgrades) and, for analytics hosts, hands
|
||||
// the caller a synthetic 200 instead of sending anything.
|
||||
//
|
||||
// Faking success rather than failure is deliberate: the transport resolves its promise, the flush
|
||||
// coroutine considers the batch delivered, and the client clears `pending_room_stats` — so nothing
|
||||
// accumulates and nothing retries. Failing the request instead would leave the batch queued and
|
||||
// re-attempted every session.
|
||||
[HarmonyPatch(typeof(HTTPManager), "SendRequest", [typeof(HTTPRequest)])]
|
||||
public static class BlockAnalyticsUploadPatch
|
||||
{
|
||||
private static bool Prefix(HTTPRequest request, ref HTTPRequest __result)
|
||||
{
|
||||
// SendRequest returns the request it was handed; callers chain off it, so hand it back
|
||||
// even though we never send it.
|
||||
__result = request;
|
||||
|
||||
return !Drop(request);
|
||||
}
|
||||
|
||||
// Second net, one layer down. Every SendRequest overload funnels into SendRequestImpl, and
|
||||
// IL2CPP is free to inline the tiny SendRequest(HTTPRequest) body into its callers — a hook on
|
||||
// it then never fires for those call sites (gotcha: a Harmony patch that loads clean can still
|
||||
// never run). SendRequestImpl is the last managed-visible chokepoint before the connection, so
|
||||
// anything that slipped past the hook above is caught here.
|
||||
[HarmonyPatch(typeof(HTTPManager), "SendRequestImpl", [typeof(HTTPRequest)])]
|
||||
public static class ImplPatch
|
||||
{
|
||||
private static bool Prefix(HTTPRequest request) => !Drop(request);
|
||||
}
|
||||
|
||||
// True when the request was blocked (caller should skip the original).
|
||||
private static bool Drop(HTTPRequest request)
|
||||
{
|
||||
if (!IsBlockedHost(request.Uri.Host))
|
||||
return false;
|
||||
|
||||
// Once per host normally; every request under [Advanced] Debug, since "did this specific
|
||||
// upload get dropped or did it slip past?" is exactly the question a mitmproxy trace
|
||||
// raises, and a deduped line can't answer it.
|
||||
if (Plugin.Debug.Value)
|
||||
Plugin.Log.LogInfo($"[ANALYTICS] dropped {request.MethodType} {request.Uri.AbsoluteUri}");
|
||||
else if (_loggedBlocked.Add("upload:" + request.Uri.Host))
|
||||
Plugin.Log.LogInfo($"[ANALYTICS] dropping uploads to {request.Uri.Host}");
|
||||
|
||||
try
|
||||
{
|
||||
CompleteWithFakeSuccess(request);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Couldn't synthesize the response — still don't send. The request's callback never
|
||||
// fires, so whatever promise the transport made stays pending; that's a stalled flush
|
||||
// coroutine at worst, versus telemetry leaving the box.
|
||||
Plugin.Log.LogWarning($"[ANALYTICS] blocked {request.Uri.Host} but could not fake a response: {e.Message}");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void CompleteWithFakeSuccess(HTTPRequest request)
|
||||
{
|
||||
var body = FakeBodyFor(request);
|
||||
|
||||
var response = new HTTPResponse(request, new Il2CppSystem.IO.MemoryStream(), false, false)
|
||||
{
|
||||
StatusCode = 200,
|
||||
Message = "OK",
|
||||
Data = new Il2CppStructArray<byte>(Encoding.UTF8.GetBytes(body)),
|
||||
};
|
||||
|
||||
request.Response = response;
|
||||
request.State = HTTPRequestStates.Finished;
|
||||
|
||||
// BestHTTP would normally fire this from HTTPManager's update loop a frame or more later.
|
||||
// Firing it inline is safe here because the callback is assigned before SendRequest is
|
||||
// called, and the promise it resolves already exists by then.
|
||||
request.Callback?.Invoke(request, response);
|
||||
}
|
||||
|
||||
// Whatever the endpoint would have said on a good day. Amplitude's real shapes are known:
|
||||
// /identify answers with the literal "success", the v2 batch endpoint with a small JSON
|
||||
// envelope. The collector's shape isn't known, so we fall back to `{"success":true}` — the
|
||||
// envelope every first-party RecNet endpoint uses (it's what the real deviceId endpoint
|
||||
// returns, see the DUID case study in CLAUDE.md) and a far better guess than an empty body,
|
||||
// which the RecNet HTTP wrapper rejects outright with "Response was empty".
|
||||
private static string FakeBodyFor(HTTPRequest request)
|
||||
{
|
||||
if (!IsAmplitudeHost(request.Uri.Host))
|
||||
return "{\"success\":true}";
|
||||
|
||||
return request.Uri.AbsoluteUri.Contains("/identify", StringComparison.OrdinalIgnoreCase)
|
||||
? "success"
|
||||
: "{\"code\":200,\"events_ingested\":0,\"payload_size_bytes\":0,\"server_upload_time\":"
|
||||
+ DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + "}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Backtrace.Unity.Json;
|
||||
using Backtrace.Unity.Model;
|
||||
using HarmonyLib;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
namespace RecNetPlugin.Patches;
|
||||
|
||||
// Backtrace crash reporting — the uploads to submit.backtrace.io.
|
||||
//
|
||||
// This one does not go through BestHTTP, so `AmplitudePatch`'s host block never sees it:
|
||||
// `Backtrace.Unity.dll` references `UnityEngine.UnityWebRequestModule` and nothing else HTTP-shaped.
|
||||
// The whole SDK is unobfuscated (it's a third-party package, so no per-build name churn to survive),
|
||||
// and every submission it makes — crash reports, minidumps, metrics — funnels through the four
|
||||
// `BacktraceHttpClient.Post` overloads. That's the concrete class; `IBacktraceHttpClient` is the
|
||||
// interface and patching it would silently never run (gotcha 3 in CLAUDE.md).
|
||||
//
|
||||
// The two overload shapes need different treatment, because of who owns the send:
|
||||
//
|
||||
// void Post(url, jObject, onComplete) - fire-and-forget, the SDK sends internally. We skip it and
|
||||
// invoke the callback with a 200 ourselves.
|
||||
// UnityWebRequest Post(...) x3 - builds the request and hands it back; *the caller* sends it
|
||||
// (`yield return request.SendWebRequest()`). Skipping the
|
||||
// original would hand the caller a null to dereference, so
|
||||
// instead we let it build whatever it likes and repoint the
|
||||
// finished request at a black hole.
|
||||
//
|
||||
// Not covered: `RecRoomNativeClient` installs a native crash handler, and a minidump uploaded from
|
||||
// native code on the next launch never passes through here. If submit.backtrace.io still shows a
|
||||
// multipart minidump POST with everything below firing, that's the path it took.
|
||||
[HarmonyPatch]
|
||||
public static class BacktracePatch
|
||||
{
|
||||
// Loopback port 1: nothing listens there, so the send fails with connection-refused in
|
||||
// microseconds without a packet leaving the machine, and the SDK takes its ordinary offline path.
|
||||
private const string BlackHoleUrl = "http://127.0.0.1:1/blocked-by-recnet-plugin";
|
||||
|
||||
private static readonly HashSet<string> _loggedBlocked = new();
|
||||
|
||||
// Fire-and-forget path (metrics). The SDK sends this one itself, so skipping the original is
|
||||
// enough — but the callback has to be answered or the submission queue keeps the batch it just
|
||||
// handed us and retries it forever. (statusCode, isError, response): a 200 with no error is what
|
||||
// it waits for before clearing the batch.
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(BacktraceHttpClient), nameof(BacktraceHttpClient.Post),
|
||||
typeof(string), typeof(BacktraceJObject), typeof(Il2CppSystem.Action<long, bool, string>))]
|
||||
private static bool PostWithCallbackPrefix(string __0, Il2CppSystem.Action<long, bool, string> __2)
|
||||
{
|
||||
if (!Plugin.DisableTelemetry.Value)
|
||||
return true;
|
||||
|
||||
LogBlocked(__0);
|
||||
__2?.Invoke(200, false, "{}");
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(BacktraceHttpClient), nameof(BacktraceHttpClient.Post),
|
||||
typeof(string), typeof(BacktraceJObject))]
|
||||
private static void PostJObjectPostfix(string __0, UnityWebRequest __result) => Neuter(__0, __result);
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(BacktraceHttpClient), nameof(BacktraceHttpClient.Post),
|
||||
typeof(string), typeof(string),
|
||||
typeof(Il2CppSystem.Collections.Generic.IEnumerable<string>),
|
||||
typeof(Il2CppSystem.Collections.Generic.IDictionary<string, string>))]
|
||||
private static void PostJsonPostfix(string __0, UnityWebRequest __result) => Neuter(__0, __result);
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(BacktraceHttpClient), nameof(BacktraceHttpClient.Post),
|
||||
typeof(string), typeof(Il2CppSystem.Collections.Generic.List<IMultipartFormSection>))]
|
||||
private static void PostFormPostfix(string __0, UnityWebRequest __result) => Neuter(__0, __result);
|
||||
|
||||
// Leave the request the SDK built exactly as it is — handlers, headers, body — and change only
|
||||
// where it points. Rebuilding it ourselves would mean guessing which handlers the caller goes on
|
||||
// to dereference; this way the coroutine keeps its shape and just gets an error back.
|
||||
private static void Neuter(string url, UnityWebRequest request)
|
||||
{
|
||||
if (!Plugin.DisableTelemetry.Value || request == null)
|
||||
return;
|
||||
|
||||
LogBlocked(url);
|
||||
request.url = BlackHoleUrl;
|
||||
}
|
||||
|
||||
// Once per host normally, every submission under [Advanced] Debug — same rule as the analytics
|
||||
// host block, and for the same reason: a deduped line can't answer "did *this* upload get
|
||||
// dropped?" when you're staring at a proxy trace.
|
||||
private static void LogBlocked(string url)
|
||||
{
|
||||
if (Plugin.Debug.Value)
|
||||
{
|
||||
Plugin.Log.LogInfo($"[BACKTRACE] dropped submission to {url}");
|
||||
return;
|
||||
}
|
||||
|
||||
var host = HostOf(url);
|
||||
if (_loggedBlocked.Add(host))
|
||||
Plugin.Log.LogInfo($"[BACKTRACE] telemetry disabled — dropping submissions to {host}");
|
||||
}
|
||||
|
||||
private static string HostOf(string url)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new Uri(url).Host;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return url;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using System;
|
||||
|
||||
namespace RecNetPlugin.Patches;
|
||||
|
||||
// Unity's own telemetry — the uploads to perf-events.cloud.unity3d.com.
|
||||
//
|
||||
// !! THIS DOES NOT WORK on the 20230414 build, and that is a known, accepted limitation — don't spend
|
||||
// another afternoon on it. Confirmed at runtime: the setters below are refused, `enabled` reads back
|
||||
// True on all five attempts, and perf-events uploads keep flowing. It's left in because it costs
|
||||
// nothing, is the correct thing to do if a future build stops refusing, and the read-back logs the
|
||||
// truth either way rather than pretending. The parts of `Disable Telemetry` that carry the actual win
|
||||
// are Amplitude, the collector and Backtrace — all confirmed dropping — and they take out the bulk of
|
||||
// the noise. If perf-events ever has to go for real, it needs a hosts-file/DNS block or a native hook;
|
||||
// there is no managed lever.
|
||||
//
|
||||
// There is nothing to hook here, and that's the point: `UnityEngine.Analytics.Analytics` and
|
||||
// `PerformanceReporting` are thin managed shims over native engine code, and the uploads happen inside
|
||||
// the player, not on any managed send path a Harmony prefix could sit on. Blocking this one at the HTTP
|
||||
// layer is equally hopeless — it never touches BestHTTP or UnityWebRequest. What it *does* have is a
|
||||
// documented opt-out, so we flip the switches at startup and read them back.
|
||||
//
|
||||
// Performance Reporting is the exception/crash reporter, Analytics is the event stream; both feed
|
||||
// perf-events, so both go off. `limitUserTracking` and `deviceStatsEnabled` cover the case where
|
||||
// something re-enables the event stream behind our back — with those set, what it can collect is
|
||||
// nothing worth sending.
|
||||
internal static class UnityTelemetryPatch
|
||||
{
|
||||
// Applied from Plugin.Load and again on each scene load until it sticks — these are native
|
||||
// properties whose setters can be refused (service not initialised yet, build flags), so
|
||||
// "set it once at load and assume" is exactly how this silently does nothing.
|
||||
private const int MaxAttempts = 5;
|
||||
|
||||
private static bool _done;
|
||||
private static int _attempts;
|
||||
|
||||
public static void Apply()
|
||||
{
|
||||
if (_done || !Plugin.DisableTelemetry.Value || _attempts >= MaxAttempts)
|
||||
return;
|
||||
|
||||
_attempts++;
|
||||
|
||||
try
|
||||
{
|
||||
UnityEngine.Analytics.PerformanceReporting.enabled = false;
|
||||
UnityEngine.Analytics.Analytics.enabled = false;
|
||||
UnityEngine.Analytics.Analytics.deviceStatsEnabled = false;
|
||||
UnityEngine.Analytics.Analytics.limitUserTracking = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// No _done: a later scene load gets another go, up to MaxAttempts.
|
||||
if (_attempts >= MaxAttempts)
|
||||
Plugin.Log.LogWarning($"[UNITY-TELEMETRY] gave up flipping the opt-out switches after {_attempts} attempts: {e.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
// The read-back is the proof, not the assignment above. A refused setter is silent.
|
||||
var perf = UnityEngine.Analytics.PerformanceReporting.enabled;
|
||||
var analytics = UnityEngine.Analytics.Analytics.enabled;
|
||||
|
||||
_done = !perf && !analytics;
|
||||
|
||||
if (_done)
|
||||
Plugin.Log.LogInfo(
|
||||
$"[UNITY-TELEMETRY] disabled — PerformanceReporting.enabled={perf} Analytics.enabled={analytics} " +
|
||||
$"deviceStats={UnityEngine.Analytics.Analytics.deviceStatsEnabled} " +
|
||||
$"limitUserTracking={UnityEngine.Analytics.Analytics.limitUserTracking}");
|
||||
else if (_attempts >= MaxAttempts)
|
||||
// Info, not a warning: this is the known outcome on this build (see the header), not a
|
||||
// fault to go chasing. It stays logged so a build that *does* accept the switches is
|
||||
// visible as a change rather than a surprise.
|
||||
Plugin.Log.LogInfo(
|
||||
$"[UNITY-TELEMETRY] switches refused after {_attempts} attempts — " +
|
||||
$"PerformanceReporting.enabled={perf} Analytics.enabled={analytics}. " +
|
||||
"Known limitation: perf-events.cloud.unity3d.com uploads continue. The rest of Disable Telemetry is unaffected.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user