fix(plugin): preserve cinematic destinations during gate transitions and diagnose field recovery

This commit is contained in:
2026-10-06 14:13:29 +08:00
parent 327009d69f
commit ced789adc1
3 changed files with 243 additions and 0 deletions
+153
View File
@@ -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<StackFrame>())
{
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<StackFrame>())
{
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); }
}
}
+88
View File
@@ -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<GameFieldManager>(field => field.MoveMap(default(GateSpotData)));
MethodInfo startMove = Game.Method<GameFieldManager>(field => field.MoveStartMap());
MethodInfo setPosition = Game.Method<GameFieldManager>(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);
}
}
+2
View File
@@ -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));