using System; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using HarmonyLib; using S3.Core; namespace S3.Modules.Profiler; /// /// Resolves vanilla targets at runtime so game-version method drift degrades telemetry rather /// than preventing the Profiler module from loading. /// public static class HitchProbePatches { readonly struct Target { public readonly string Type; public readonly string Method; public readonly string Id; public Target(string type, string method, string id) { Type = type; Method = method; Id = id; } } static readonly Target[] Targets = { new("RollingStock.CarCuller", "Update", "cars.culler_update"), new("RollingStock.CarCuller", "ProcessPending", "cars.process_pending"), new("RollingStock.CarCuller", "OnCarCullingGroupStateChanged", "cars.cull_transition"), new("Model.Car", "ModelLoadRetain", "cars.model_retain"), new("Model.Car", "HandleModelsLoaded", "cars.model_loaded"), new("Model.Car", "UnloadModels", "cars.model_unload"), new("Model.Car", "SetCullerDistanceBand", "cars.distance_band"), new("Model.Car", "PositionWheelBoundsFront", "cars.position_wheels"), new("Helpers.SceneryAssetInstance", "SetLoaded", "scenery.set_loaded"), new("Helpers.SceneryAssetInstance", "DidLoadModel", "scenery.did_load_model"), new("Helpers.SceneryAssetInstance", "CullingSphereStateChanged", "scenery.cull_transition"), new("Helpers.Culling.CullingManager", "Update", "culling.update"), new("Helpers.Culling.CullingManager", "FixedUpdate", "culling.fixed_update"), new("Cameras.StrategyCameraController", "Update", "camera.strategy_update"), new("Cameras.StrategyCameraController", "UpdateCameraPosition", "camera.update_position"), new("Cameras.StrategyCameraController", "FindGround", "camera.find_ground"), new("TrainController", "FixedUpdate", "train.fixed_update"), new("TrainController", "CarDidPosition", "train.car_did_position"), new("WorldStreamer2.Streamer", "Update", "streamer.update"), new("WorldStreamer2.Streamer", "CheckPositionTiles", "streamer.check_tiles"), new("WorldStreamer2.Streamer", "LoadLevelAsyncManage", "streamer.load_pump"), new("WorldStreamer2.Streamer", "SceneLoading", "streamer.scene_loading"), new("WorldStreamer2.Streamer", "SceneUnloading", "streamer.scene_unloading"), new("WorldStreamer2.StreamerLoadingManager", "Update", "streamer.manager_update"), new("WorldStreamer2.TerrainCullingSystem", "Update", "terrain.culling_update"), new("WorldStreamer2.TerrainCullingSystem", "CheckVisibility", "terrain.check_visibility"), new("WorldStreamer2.PhysicCullingSystem", "Update", "physics_culling.update"), new("WorldStreamer2.PhysicCullingSystem", "CheckVisibility", "physics_culling.check_visibility"), }; static readonly Dictionary ProbeIds = new(); public static void Install(Harmony harmony) { ProbeIds.Clear(); var prefix = new HarmonyMethod(typeof(HitchProbePatches), nameof(Prefix)); var postfix = new HarmonyMethod(typeof(HitchProbePatches), nameof(Postfix)); int patched = 0; int missed = 0; foreach (Target target in Targets) { Type? type = FindType(target.Type); if (type == null) { Log.Warn($"[profiler] Hitch probe type missing: {target.Type}"); missed++; continue; } bool found = false; foreach (MethodInfo method in AccessTools.GetDeclaredMethods(type)) { if (method.Name != target.Method || method.IsAbstract || method.ContainsGenericParameters) continue; found = true; if (ProbeIds.ContainsKey(method)) continue; try { ProbeIds[method] = target.Id; harmony.Patch(method, prefix, postfix); patched++; } catch (Exception e) { ProbeIds.Remove(method); Log.Warn($"[profiler] Hitch probe failed: {type.FullName}.{method.Name}: {e.Message}"); missed++; } } if (!found) { Log.Warn($"[profiler] Hitch probe method missing: {target.Type}.{target.Method}"); missed++; } } Log.Info($"[profiler] Hitch probes installed: {patched}; unavailable: {missed}."); } public static void Prefix(MethodBase __originalMethod, out long __state) { __state = HitchSampler.Active && ProbeIds.ContainsKey(__originalMethod) ? Stopwatch.GetTimestamp() : 0; } public static void Postfix(MethodBase __originalMethod, long __state) { if (__state == 0 || !HitchSampler.Active) return; if (ProbeIds.TryGetValue(__originalMethod, out string? id)) HitchSampler.Record(id, Stopwatch.GetTimestamp() - __state); } static Type? FindType(string fullName) { foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) { try { Type? type = assembly.GetType(fullName, false); if (type != null) return type; } catch { } } return null; } }