diff --git a/plugins/LoginUI/FieldPositionDiagnostics.cs b/plugins/LoginUI/FieldPositionDiagnostics.cs new file mode 100644 index 0000000..d2c0372 --- /dev/null +++ b/plugins/LoginUI/FieldPositionDiagnostics.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections; +using System.Diagnostics; +using System.Globalization; +using System.Reflection; +using System.Text; +using BD2.GameNames; +using HarmonyLib; +using UnityEngine; +using static Bd2LoginUI.LoginRuntime; + +namespace Bd2LoginUI; + +internal static class FieldPositionDiagnostics +{ + private const BindingFlags All = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; + private static string LastTarget = "none"; + private static float LastTargetTime, LastRecoveryTime = -100; + private static int RecoveryCount; + + internal static void Install(Harmony harmony) + { + Bind(harmony, "SetCharPosition", nameof(PositionPrefix), 1); + Bind(harmony, "MoveSafeMapAtInvalidPosition", nameof(RecoveryPrefix), 0); + try + { + MethodInfo setter = typeof(GameFieldManager).GetGameProperty("CurrentMapInfo", All)?.GetSetMethod(true); + if (setter == null) throw new MissingMethodException(); + harmony.Patch(setter, prefix: new HarmonyMethod(typeof(FieldPositionDiagnostics), nameof(MapPrefix))); + Type scene = typeof(SceneManager); + MethodInfo load = scene?.GetGameMethod("ClearAndLoadMapScene", All); + if (load == null || load.GetParameters().Length != 3) throw new MissingMethodException(); + harmony.Patch(load, prefix: new HarmonyMethod(typeof(FieldPositionDiagnostics), nameof(ScenePrefix))); + Log?.LogInfo("Field transition diagnostics installed: CurrentMapInfo setter, ClearAndLoadMapScene"); + } + catch (Exception error) { Log?.LogWarning("Field transition diagnostic binding unavailable: exception=" + error.GetType().Name); } + } + + private static void Bind(Harmony harmony, string name, string prefix, int parameters) + { + try + { + MethodInfo method = typeof(GameFieldManager).GetGameMethod(name, All); + if (method == null || method.GetParameters().Length != parameters) throw new MissingMethodException(); + harmony.Patch(method, prefix: new HarmonyMethod(typeof(FieldPositionDiagnostics), prefix)); + Log?.LogInfo("Field position diagnostic installed: target=" + name); + } + catch (Exception error) { Log?.LogWarning("Field position diagnostic binding unavailable: target=" + name + " exception=" + error.GetType().Name); } + } + + private static object Read(object target, string name) + { + if (target == null) return null; + Type type = target.GetType(); + return type.GetGameProperty(name, All)?.GetValue(target) ?? type.GetGameField(name, All)?.GetValue(target); + } + + private static string Format(object value) => value is Vector3 point + ? string.Format(CultureInfo.InvariantCulture, "({0:R},{1:R},{2:R})", point.x, point.y, point.z) + : Convert.ToString(value, CultureInfo.InvariantCulture) ?? "unavailable"; + + private static string ActiveScene() + { + try + { + return UnityEngine.SceneManagement.SceneManager.GetActiveScene().name; + } + catch { return "unavailable"; } + } + + private static void PositionPrefix(object __0) + { + try + { + object mapId = Read(__0, "MapId"); + if (mapId == null || Convert.ToInt32(mapId, CultureInfo.InvariantCulture) == 0) return; + LastTarget = "map=" + Format(mapId) + " xyz=" + Format(Read(__0, "PlayerPosition")); + LastTargetTime = Time.realtimeSinceStartup; + Log?.LogInfo("Field position target: " + LastTarget + " active_scene=" + ActiveScene() + " callers=" + Callers()); + } + catch { } + } + + private static string Callers() + { + var callers = new StringBuilder(); + foreach (StackFrame frame in new StackTrace(false).GetFrames() ?? Array.Empty()) + { + MethodBase method = frame.GetMethod(); + if (method?.DeclaringType == null || method.DeclaringType == typeof(FieldPositionDiagnostics)) continue; + if (callers.Length > 0) callers.Append(" <- "); + callers.Append(method.DeclaringType.FullName).Append('.').Append(method.Name); + if (callers.Length >= 1600) break; + } + return callers.ToString(); + } + + private static void MapPrefix(object __instance, object __0) + { + try { Log?.LogInfo("Field map changed: previous=" + Format(Read(Read(__instance, "CurrentMapInfo"), "Id")) + + " target=" + Format(Read(__0, "Id")) + " scene=" + Format(Read(__0, "MapScenePath")) + " active_scene=" + ActiveScene() + " callers=" + Callers()); } + catch { } + } + + private static void ScenePrefix(object __0, bool __1) + { + try { Log?.LogInfo("Field scene requested: pack=" + Format(Read(__0, "PackId")) + + " map=" + Format(Read(__0, "Id")) + " scene=" + Format(Read(__0, "MapScenePath")) + + " force=" + __1 + " active_scene=" + ActiveScene() + " callers=" + Callers()); } + catch { } + } + + private static void RecoveryPrefix(object __instance) + { + try + { + float now = Time.realtimeSinceStartup; + RecoveryCount++; + if (now - LastRecoveryTime < 5) return; + LastRecoveryTime = now; + object map = Read(__instance, "CurrentMapInfo"); + Component player = Read(__instance, "PlayerController") as Component; + Component character = player == null ? null : player.GetComponent("CharacterController"); + object move = Read(player, "PlayerMoveController"); + object agent = Read(move, "Navagent"); + var callers = new StringBuilder(); + foreach (StackFrame frame in new StackTrace(false).GetFrames() ?? Array.Empty()) + { + MethodBase method = frame.GetMethod(); + if (method?.DeclaringType == null || method.DeclaringType == typeof(FieldPositionDiagnostics)) continue; + if (callers.Length > 0) callers.Append(" <- "); + callers.Append(method.DeclaringType.FullName).Append('.').Append(method.Name); + if (callers.Length >= 1600) break; + } + object controller = Read(__instance, "FieldMapObjectController"); + bool hasController = controller is UnityEngine.Object unity ? unity != null : controller != null; + Log?.LogWarning("Field position recovery: count=" + RecoveryCount + + " pack=" + Format(Read(map, "PackId")) + " map=" + Format(Read(map, "Id")) + + " player_xyz=" + (player == null ? "unavailable" : Format(player.transform.position)) + + " scene_loaded=" + Format(Read(__instance, "IsLoadedField")) + + " active_scene=" + ActiveScene() + + " grounded=" + Format(Read(character, "isGrounded")) + + " collision_flags=" + Format(Read(character, "collisionFlags")) + + " move_type=" + Format(Read(move, "MoveType")) + + " nav_on_mesh=" + Format(Read(agent, "isOnNavMesh")) + + " nav_area_mask=" + Format(Read(agent, "areaMask")) + + " encount_count=" + Format((Read(__instance, "FieldEncountPointList") as ICollection)?.Count) + + " map_controller=" + hasController + " last_target={" + LastTarget + "} target_age_s=" + + (now - LastTargetTime).ToString("F3", CultureInfo.InvariantCulture) + " callers=" + callers); + } + catch (Exception error) { Log?.LogWarning("Field position diagnostic unavailable: exception=" + error.GetType().Name); } + } +} diff --git a/plugins/LoginUI/GateTimelinePosition.cs b/plugins/LoginUI/GateTimelinePosition.cs new file mode 100644 index 0000000..3c2330d --- /dev/null +++ b/plugins/LoginUI/GateTimelinePosition.cs @@ -0,0 +1,88 @@ +using System; +using System.Reflection; +using BD2.GameNames; +using HarmonyLib; +using static Bd2LoginUI.LoginRuntime; + +namespace Bd2LoginUI; + +// A gate quest may end its cinema in a different scene. The gate's final +// camera coroutine must use that scene's position, rather than the old gate. +internal static class GateTimelinePosition +{ + private const BindingFlags All = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; + private static GameFieldManager PendingField; + private static GameFieldManager TransitionField; + private static MapPositionData PendingPosition; + private static int PendingGateMap; + private static long Transition; + private static bool PositionApplied; + + internal static void Install(Harmony harmony) + { + try + { + MethodInfo timelineMove = typeof(TimelineSignalManager).GetGameMethod("CheckForMoveMap", All); + MethodInfo complete = typeof(GameFieldManager).GetGameMethod("MoveMapComplete", All); + MethodInfo gateMove = Game.Method(field => field.MoveMap(default(GateSpotData))); + MethodInfo startMove = Game.Method(field => field.MoveStartMap()); + MethodInfo setPosition = Game.Method(field => field.SetCharPosition(default(MapPositionData))); + if (timelineMove == null || timelineMove.GetParameters().Length != 1 || + complete == null || complete.GetParameters().Length != 3) throw new MissingMethodException(); + harmony.Patch(timelineMove, prefix: new HarmonyMethod(typeof(GateTimelinePosition), nameof(TimelineMovePrefix))); + harmony.Patch(complete, prefix: new HarmonyMethod(typeof(GateTimelinePosition), nameof(CompletePrefix))); + harmony.Patch(gateMove, prefix: new HarmonyMethod(typeof(GateTimelinePosition), nameof(BeginPrefix))); + harmony.Patch(startMove, prefix: new HarmonyMethod(typeof(GateTimelinePosition), nameof(BeginPrefix))); + harmony.Patch(setPosition, postfix: new HarmonyMethod(typeof(GateTimelinePosition), nameof(PositionPostfix))); + Log?.LogInfo("Gate timeline final position patch installed"); + } + catch (Exception error) { Log?.LogError("Gate timeline final position patch failed: " + error); } + } + + private static void BeginPrefix(GameFieldManager __instance) + { + Transition++; + TransitionField = __instance; + PendingField = null; + PositionApplied = false; + } + + private static void TimelineMovePrefix(TimelineSignalManager __instance, TimelineMapMoveData __0) + { + PendingField = null; + PositionApplied = false; + GameFieldManager field = GameFieldManager.Instance; + if (field == null || field != TransitionField || !field.IsMoveMapProcessing || !field.IsMoveMapByGateSpot || + __instance.IsTimelineEndToBattle || __0 == null || string.IsNullOrEmpty(__0.sceneName)) return; + var target = MapInfo.FindMapBySceneName(__0.sceneName); + if (target == null || field.CurrentMapInfo == null) return; + PendingGateMap = field.CurrentMapInfo.Id; + PendingPosition = new MapPositionData { MapId = target.Id, PlayerPosition = __0.playerPos }; + PendingField = field; + Log?.LogInfo("Gate timeline final position recorded: transition=" + Transition + " gate_map=" + PendingGateMap + + " final_map=" + PendingPosition.MapId + " scene=" + __0.sceneName); + } + + private static void PositionPostfix(GameFieldManager __instance, MapPositionData __0) + { + if (PendingField != __instance || !__instance.IsMoveMapByGateSpot || !__instance.IsMoveMapProcessing || + !__instance.IsLoadedField || __instance.CurrentMapInfo == null || + __instance.CurrentMapInfo.Id != PendingPosition.MapId || __0.MapId != PendingPosition.MapId || + __0.PlayerPosition != PendingPosition.PlayerPosition) return; + PositionApplied = true; + } + + private static void CompletePrefix(GameFieldManager __instance, ref MapPositionData __0) + { + if (PendingField != __instance) return; + PendingField = null; + TransitionField = null; + if (!PositionApplied || !__instance.IsMoveMapProcessing || !__instance.IsMoveMapByGateSpot || + __instance.CurrentMapInfo == null || __instance.CurrentMapInfo.Id != PendingPosition.MapId || + __0.MapId != PendingGateMap || __0.MapId == PendingPosition.MapId || + (TimelineSignalManager.Instance != null && TimelineSignalManager.Instance.IsTimelineEndToBattle)) return; + int original = __0.MapId; + __0 = PendingPosition; + Log?.LogInfo("Gate timeline final position applied: transition=" + Transition + " gate_map=" + original + " final_map=" + __0.MapId); + } +} diff --git a/plugins/LoginUI/Plugin.cs b/plugins/LoginUI/Plugin.cs index cad771b..1b80835 100644 --- a/plugins/LoginUI/Plugin.cs +++ b/plugins/LoginUI/Plugin.cs @@ -94,6 +94,8 @@ public sealed class Plugin : BaseUnityPlugin Harmony harmony = new Harmony(Guid); SessionDiagnostics.Install(harmony); EventRequestDiagnostics.Install(harmony); + FieldPositionDiagnostics.Install(harmony); + GateTimelinePosition.Install(harmony); EventHubPresentation.Install(harmony); harmony.Patch(awake, postfix: new HarmonyMethod(typeof(SessionRecovery), nameof(IntroAwakePostfix))); HarmonyMethod maintenancePrefix = new HarmonyMethod(typeof(LoginController), nameof(SendMaintenancePrefix));