diff --git a/UXAssist/Patches/PlayerPatch.cs b/UXAssist/Patches/PlayerPatch.cs index f6b3f1c..46a2424 100644 --- a/UXAssist/Patches/PlayerPatch.cs +++ b/UXAssist/Patches/PlayerPatch.cs @@ -1,10 +1,16 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Diagnostics; using System.Reflection.Emit; using BepInEx.Configuration; using CommonAPI.Systems; +using CommonAPI.Systems.ModLocalization; using HarmonyLib; using UnityEngine; +using UnityEngine.UI; using UXAssist.Common; +using Logger = UnityEngine.Logger; +using Object = UnityEngine.Object; namespace UXAssist.Patches; @@ -21,6 +27,16 @@ public class PlayerPatch : PatchImpl private static PressKeyBind _toggleAllStarsNameKey; private static PressKeyBind _autoDriveKey; + public static ConfigEntry UseNewNavigationAlgorithm { get; set; } = null!; + public static ConfigEntry StopOnArrivalAndInput { get; set; } = null!; + public static ConfigEntry UseWarper { get; set; } = null!; + public static ConfigEntry UseWarperMinimalEnergy { get; set; } = null!; + public static ConfigEntry UseWarperDistance { get; set; } = null!; + public static ConfigEntry UseSpeedUp { get; set; } = null!; + public static ConfigEntry UseSpeedUpMinimalEnergy { get; set; } = null!; + public static ConfigEntry DFHiveFollowDistance { get; set; } = null!; + public static ConfigEntry DFCarrierFollowDistance { get; set; } = null!; + public static void Init() { _showAllStarsNameKey = KeyBindings.RegisterKeyBinding(new BuiltinKey @@ -45,7 +61,7 @@ public class PlayerPatch : PatchImpl _autoDriveKey = KeyBindings.RegisterKeyBinding(new BuiltinKey { - key = new CombineKey(0, 0, ECombineKeyAction.OnceClick, true), + key = new CombineKey((int)KeyCode.K, 0, ECombineKeyAction.OnceClick, true), conflictGroup = KeyBindConflict.MOVEMENT | KeyBindConflict.FLYING | KeyBindConflict.SAILING | KeyBindConflict.BUILD_MODE_1 | KeyBindConflict.KEYBOARD_KEYBIND, name = "ToggleAutoCruise", canOverride = true @@ -68,6 +84,7 @@ public class PlayerPatch : PatchImpl HideTipsForSandsChanges.Enable(HideTipsForSandsChangesEnabled.Value); ShortcutKeysForStarsName.Enable(ShortcutKeysForStarsNameEnabled.Value); AutoNavigation.Enable(AutoNavigationEnabled.Value); + AutoNavigationG.Enable(true); Enable(true); } @@ -78,6 +95,7 @@ public class PlayerPatch : PatchImpl { AutoNavigation.ToggleAutoCruise(); } + AutoNavigationG.Update( ); } public static void Uninit() @@ -352,6 +370,8 @@ public class PlayerPatch : PatchImpl public static void ToggleAutoCruise() { + if(UseNewNavigationAlgorithm.Value) + return; AutoCruiseEnabled.Value = !AutoCruiseEnabled.Value; if (!DSPGame.IsMenuDemo && GameMain.isRunning) { @@ -380,6 +400,8 @@ public class PlayerPatch : PatchImpl new CodeInstruction(OpCodes.Ldarg_0), Transpilers.EmitDelegate((PlayerController controller) => { + if (UseNewNavigationAlgorithm.Value) + return; /* Update target astro if changed */ _speedUp = false; var player = controller.player; @@ -592,4 +614,751 @@ public class PlayerPatch : PatchImpl } */ } -} + + #region 新的导航算法 + + public class AutoNavigationG : PatchImpl + { + private const double Epsilon = 1e-18; + private const int DarkFogAstroIdStart = 1000000; + private const int StarArriveThresholdRadius = 4000; + private const int PlanetArriveThresholdRadius = 100; + private const double StarSafeRedis = 2000; + private const double PlanetSafeRedis = 2000; + private const double HiveSafeRedis = GalaxyData.AU * 0.5; + + private const double FollowModeUseWarpDistance = GalaxyData.AU * 1.5; + private const double ResidualCompensationMaxDistance = GalaxyData.AU * 0.5; + private const double ResidualCompensationSpeedRatio = 0.5; + private const double ResidualCompensationMinSpeed = 100; + + private static bool EnableTag { get; set; } + private static bool PauseTag { get; set; } + private static Text UiTipText { get; set; } + + private static int TargetId { get; set; } + private static VectorLF3 TargetUniversePosition { get; set; } + private static VectorLF3 TargetUniverseVelocity { get; set; } + private static ESpaceGuideType TargetType { get; set; } + private static double TargetArriveThresholdRadius { get; set; } + + private static List SpaceObjects { get; set; } = new(64); + private static EMovementState LastMovementState { get; set; } + private static bool EscapeMode { get; set; } + private static int EscapeAstroId { get; set; } + private static bool FollowModeLock { get; set; } + + private static Stopwatch Stopwatch { get; } = new( ); + private static double TickToUs { get; } = 1000000.0 / Stopwatch.Frequency; + private static long FrameTotalTicks { get; set; } + private static int FrameCount { get; set; } + private static long SlowFrameTicks { get; set; } + private static int SlowFrameCount { get; set; } + private static double SlowFrameLimit { get; } = 500 / TickToUs; + + protected override void OnEnable( ) + { + StartG( ); + } + + public static void Awake(ConfigFile config) + { + UseNewNavigationAlgorithm = config.Bind( + "Player", + nameof(UseNewNavigationAlgorithm), + true, + "使用新导航算法"); + StopOnArrivalAndInput = config.Bind("Player", nameof(StopOnArrivalAndInput), true, "到达自动停止"); + UseWarper = config.Bind("Player", nameof(UseWarper), true, "是否使用翘曲"); + UseWarperMinimalEnergy = config.Bind( + "Player", + nameof(UseWarperMinimalEnergy), + 800, + "使用翘曲最低能量(MJ)"); + UseWarperDistance = config.Bind("Player", nameof(UseWarperDistance), 2, "最短翘曲距离(AU)"); + UseSpeedUp = config.Bind("Player", nameof(UseSpeedUp), true, "是否加速"); + UseSpeedUpMinimalEnergy = config.Bind( + "Player", + nameof(UseSpeedUpMinimalEnergy), + 100, + "加速最低能量(MJ)"); + DFHiveFollowDistance = config.Bind( + "Player", + nameof(DFHiveFollowDistance), + 0.5, + "黑雾蜂巢跟随距离(AU)"); + DFCarrierFollowDistance = config.Bind( + "Player", + nameof(DFCarrierFollowDistance), + 1000, + "黑雾火种跟随距离"); + } + + public static void StartG( ) + { + Text originText = UIRoot.instance.uiGame.generalTips.modeText; + UiTipText = Object.Instantiate(originText, originText.transform.parent); + UiTipText.gameObject.SetActive(false); + UiTipText.rectTransform.anchoredPosition = new Vector2(0f, 160f); + UiTipText.text = "自动导航中"; + } + + public static void Update( ) + { + if (EnableTag is not true && _autoDriveKey.keyValue) + { + StartAutoNavigation( ); + } + } + + private static void StartAutoNavigation( ) + { + if (DSPGame.IsMenuDemo || ! GameMain.isRunning) + return; + + EnableTag = true; + Reset( ); + UIRealtimeTip.Popup("自动导航启动", sound: false); + UiTipText?.gameObject.SetActive(true); + } + + private static void StopAutoNavigation( ) + { + if (DSPGame.IsMenuDemo || ! GameMain.isRunning || ! StopOnArrivalAndInput.Value) + return; + EnableTag = false; + string text = + "自动导航关闭," + + $"帧数量 {FrameCount} ,慢帧数量 {SlowFrameCount} " + + $"平均帧时 {FrameTotalTicks * TickToUs / FrameCount:F2} us,平均慢帧时 {SlowFrameTicks * TickToUs / SlowFrameCount:F2} us"; + UIRealtimeTip.Popup(text); + UiTipText?.gameObject.SetActive(false); + } + + private static void Reset( ) + { + PauseTag = false; + TargetId = 0; + LastMovementState = EMovementState.Walk; + EscapeMode = false; + FollowModeLock = false; + + FrameTotalTicks = 0; + FrameCount = 0; + SlowFrameTicks = 0; + SlowFrameCount = 0; + } + + private static void Pause( ) => PauseTag = true; + + private static void Resume( ) => PauseTag = false; + + private static bool IsEnable => EnableTag && ! PauseTag; + + /// + /// 前置检查与目标检测 + /// + private static bool PreCheckAndRefreshTarget(Player player) + { + if (! UseNewNavigationAlgorithm.Value || AutoNavigationEnabled.Value) + { + StopAutoNavigation( ); + return true; + } + if (IsEnable is not true) + return true; + if (player.mecha.thrusterLevel < 2) + { + StopAutoNavigation( ); + return true; + } + if (VFInput._pullUp.pressing + || VFInput._pushDown.pressing + || VFInput._moveLeft.pressing + || VFInput._moveRight.pressing + || VFInput._moveForward.pressing) + { + StopAutoNavigation( ); + return true; + } + + PlayerNavigation navigation = player.navigation; + int indicatorAstroId = navigation.indicatorAstroId; + if (indicatorAstroId != 0) + { + TargetId = indicatorAstroId; + if (indicatorAstroId > DarkFogAstroIdStart) + { + TargetType = ESpaceGuideType.DFHive; + AstroData astro = GameMain.spaceSector.astros[indicatorAstroId - DarkFogAstroIdStart]; + VectorLF3 lPos = default; + astro.VelocityU(ref lPos, out Vector3 velocity); + TargetUniverseVelocity = velocity; + TargetUniversePosition = astro.uPos; + TargetArriveThresholdRadius = DFHiveFollowDistance.Value * GalaxyData.AU; + } else + { + bool isStar = indicatorAstroId % 100 == 0; + TargetType = isStar ? ESpaceGuideType.Star : ESpaceGuideType.Planet; + if (isStar) + { + StarData star = GameMain.galaxy.StarById(indicatorAstroId / 100); + TargetUniversePosition = star.uPosition; + TargetArriveThresholdRadius = star.radius + StarArriveThresholdRadius; + } else + { + PlanetData planet = GameMain.galaxy.PlanetById(indicatorAstroId); + TargetUniversePosition = planet.uPosition; + TargetArriveThresholdRadius = planet.realRadius + PlanetArriveThresholdRadius; + } + } + } else if (navigation.indicatorEnemyId != 0) + { + TargetId = navigation.indicatorEnemyId; + TargetType = ESpaceGuideType.DFCarrier; + EnemyData ptr = GameMain.data.spaceSector.enemyPool[navigation.indicatorEnemyId]; + GameMain.data.spaceSector.TransformFromAstro_ref(ptr.astroId, out VectorLF3 uPosition, ref ptr.pos); + TargetUniversePosition = uPosition; + TargetUniverseVelocity = ptr.vel; + TargetArriveThresholdRadius = DFCarrierFollowDistance.Value; + } else + { + StopAutoNavigation( ); + return true; + } + return false; + } + + /// + /// 航向指定目标,考虑避障 + /// + private static void SailToTarget(PlayerController controller, double distance) + { + if (controller.movementStateInFrame != EMovementState.Sail) + return; + + StarData localStar = GameMain.localStar; + SpaceObjects.Clear( ); + if (localStar != null) + { + if (localStar.astroId != TargetId) + { + SpaceObjects.Add( + new SpaceObject( + astroId: localStar.astroId, + position: localStar.uPosition, + radius: localStar.radius, + radiusOffset: StarSafeRedis)); + } + foreach (PlanetData planet in localStar.planets) + { + if (planet.astroId == TargetId) + continue; + SpaceObjects.Add( + new SpaceObject( + astroId: planet.astroId, + position: planet.uPosition, + radius: planet.realRadius, + radiusOffset: PlanetSafeRedis)); + } + if (TargetType is not ESpaceGuideType.DFHive) + { + EnemyDFHiveSystem hiveSys = GameMain.spaceSector.dfHives[localStar.index]; + while (hiveSys != null) + { + if (hiveSys is { realized: true, hiveAstroId: > DarkFogAstroIdStart } + && hiveSys.hiveAstroId != TargetId) + { + SpaceObjects.Add( + new SpaceObject( + astroId: hiveSys.hiveAstroId, + position: GameMain.spaceSector.astros[hiveSys.hiveAstroId - DarkFogAstroIdStart].uPos, + radius: 0, + radiusOffset: HiveSafeRedis)); + } + hiveSys = hiveSys.nextSibling; + } + } + } + + Player player = controller.player; + double currentSpeed = player.uVelocity.magnitude; + VectorLF3 currentDir = player.uVelocity / currentSpeed; + VectorLF3 targetDir = ComputeDirection(player.uPosition, SpaceObjects); + if (player.warping) + UpdateSailVelocityAndRotation(controller, currentDir, currentSpeed, targetDir, currentSpeed); + else + { + Mecha mecha = player.mecha; + bool canWarp = UseWarper.Value + && mecha.coreEnergy > UseWarperMinimalEnergy.Value * 1000 * 1000 + && player.mecha.thrusterLevel >= 3 + && player.mecha.HasWarper( ) + && GameMain.localPlanet == null + && distance > GalaxyData.AU * UseWarperDistance.Value; + if (canWarp && player.mecha.UseWarper( )) + { + UpdateSailVelocityAndRotation(controller, currentDir, currentSpeed, targetDir, currentSpeed); + player.warpCommand = true; + VFAudio.Create("warp-begin", player.transform, Vector3.zero, true); + } else + UpdateSailVelocityAndRotation(controller, currentDir, currentSpeed, targetDir, mecha.maxSailSpeed); + } + } + + /// + /// 计算方向,考虑避障 + /// + /// 玩家位置 + /// 障碍物集合 + /// + private static VectorLF3 ComputeDirection(VectorLF3 playerPos, List spaceObjects) + { + const double lookForward = GalaxyData.AU * 1; + + VectorLF3 toTarget = TargetUniversePosition - playerPos; + double toTargetSqr = toTarget.sqrMagnitude; + if (toTargetSqr < Epsilon) + return default; + VectorLF3 baseDir = toTarget.normalized; + if (spaceObjects.Count == 0) + return baseDir; + + var upRef = VectorLF3.unit_y; + VectorLF3 avoid = default; + VectorLF3 liftRaw = default; + double sumW = 0; + foreach (SpaceObject spaceObject in spaceObjects) + { + const double enterEscapeAltitude = 200; + double quitEscapeAltitude = Math.Max(600, spaceObject.Radius * 1.5); + + double safeR = spaceObject.Radius + spaceObject.RadiusOffset; + if (safeR <= 0.0) + continue; + VectorLF3 toObject = spaceObject.Position - playerPos; + double toObjectSqr = toObject.sqrMagnitude; + if (toObjectSqr > toTargetSqr) + continue; + + double toObjectMagnitude = Math.Sqrt(toObjectSqr); + if (toObjectMagnitude > lookForward) + continue; + + double toObjectAltitude = toObjectMagnitude - spaceObject.Radius; + double forward = VectorLF3.Dot(toObject, baseDir); + VectorLF3 pushVector = baseDir * forward - toObject; + + // 逃逸模式 + if (EscapeMode is not true && toObjectAltitude < enterEscapeAltitude) + { + EscapeMode = true; + EscapeAstroId = spaceObject.AstroId; + } + if (EscapeMode) + { + if (spaceObject.AstroId != EscapeAstroId) + continue; + if (toObjectAltitude > quitEscapeAltitude) + { + EscapeMode = false; + EscapeAstroId = 0; + } else + { + VectorLF3 escapeDirection = - toObject / toObjectMagnitude; + VectorLF3 cDirection = VectorLF3.Dot(escapeDirection, baseDir) < - 0.5 ? + pushVector.sqrMagnitude < Epsilon ? default : pushVector.normalized + : baseDir; + double bias = (quitEscapeAltitude - toObjectAltitude) / (quitEscapeAltitude - enterEscapeAltitude); + bias = bias switch + { + < 0.0 => 0.0, + > 1.0 => 1.0, + _ => bias + }; + VectorLF3 direction = escapeDirection * bias + cDirection * (1 - bias); + return direction.sqrMagnitude < Epsilon ? escapeDirection : direction.normalized; + } + } + + // 仅考虑前方的天体 + if (forward <= 0.0) + continue; + double pushSqr = pushVector.sqrMagnitude; + double pushMagnitude = Math.Sqrt(pushSqr); + if (pushSqr < Epsilon) + continue; + if (pushMagnitude >= safeR) + continue; + + // 距离权重计算避障力度并合成 + double pushW = (safeR - pushMagnitude) / safeR; + pushW = Clamp(pushW, 0, 1); + double forwardW = 1 - (forward - safeR) / (lookForward - safeR); + forwardW = Clamp(forwardW, 0, 1); + double weight = pushW * pushW * (3.0 - 2.0 * pushW) * forwardW * forwardW; + VectorLF3 pushDir = pushVector / pushMagnitude; + avoid += pushDir * weight; + sumW += weight; + + // 计算抬升,防止对称抵消,可以考虑增加门限降低成本 + VectorLF3 n = VectorLF3.Cross(baseDir, pushDir); + if (VectorLF3.Dot(n, upRef) < 0) + n = - n; + liftRaw += n * weight; + } + + double avoidSqr = avoid.sqrMagnitude; + if (avoidSqr < Epsilon) + return baseDir; + + // 判断抬升,在合成避障力度相对不足时抬升 + double needLift = 0; + if (sumW > Epsilon) + { + double avoidMagnitude = Math.Sqrt(avoidSqr); + double ratio = avoidMagnitude / sumW; + needLift = 1 - Clamp(ratio, 0, 1); + } + const double needLiftMin = 0.4; + needLift = (needLift - needLiftMin) / (1 - needLiftMin); + needLift = Clamp(needLift, 0, 1); + VectorLF3 lift = default; + if (needLift > 0) + { + const double liftScale = 0.25; + const double liftMax = 0.6; + lift = liftRaw * needLift * liftScale; + if (lift.sqrMagnitude > liftMax * liftMax) + lift = lift.normalized * liftMax; + } + + VectorLF3 mixed = baseDir + avoid + lift; + double mixedSqr = mixed.sqrMagnitude; + return mixedSqr < Epsilon ? baseDir : mixed.normalized; + } + + /// + /// 航向并跟随移动目标,进入近距离跟随后无避障 + /// 到目标距离 + /// 目标跟踪距离 + /// + private static void FollowMovingTarget(PlayerController controller, double distance) + { + double arriveRadius = TargetArriveThresholdRadius; + double exitLockRadius = arriveRadius * 1.05; + double axialTolerance = arriveRadius * 0.8; + double lateralTolerance = arriveRadius * 0.8; + double axialSlowDownDistance = arriveRadius * 10; + double lateralSlowDownDistance = arriveRadius * 5; + + Player player = controller.player; + double mechaMaxSailSpeed = player.mecha.maxSailSpeed; + VectorLF3 playerPos = player.uPosition; + VectorLF3 playerVelocity = player.uVelocity; + double playerSpeed = playerVelocity.magnitude; + VectorLF3 targetPos = TargetUniversePosition; + VectorLF3 targetVelocity = TargetUniverseVelocity; + VectorLF3 toTarget = targetPos - playerPos; + VectorLF3 toTargetDir = toTarget / distance; + VectorLF3 targetVelocityDir = SafeNorm(targetVelocity, toTargetDir); + + if (distance > FollowModeUseWarpDistance) + { + SailToTarget(controller, distance); + return; + } + if (player.warping) + player.warpCommand = false; + + switch (distance) + { + case >= FollowModeUseWarpDistance: + SailToTarget(controller, distance); + return; + case < FollowModeUseWarpDistance when player.warping: + player.warpCommand = false; + return; + case < FollowModeUseWarpDistance when distance > exitLockRadius: + FollowModeLock = false; + break; + case < FollowModeUseWarpDistance when distance <= arriveRadius: + FollowModeLock = true; + break; + } + // 球面锁速 + if (FollowModeLock) + { + UpdateSailVelocityAndRotation( + controller: controller, + currentDir: playerVelocity / playerSpeed, + currentSpeed: playerSpeed, + targetDir: targetVelocityDir, + targetSpeed: targetVelocity.magnitude, + forceSpeedUp: true); + return; + } + + // 分解误差并分量贴靠 + double axialError = VectorLF3.Dot(toTarget, targetVelocityDir); + VectorLF3 lateralVector = toTarget - targetVelocityDir * axialError; + double lateralError = lateralVector.magnitude; + VectorLF3 lateralDir = lateralError > Epsilon ? lateralVector / lateralError : default; + + // 轴向 + double axialEffectiveError = Math.Max(Math.Abs(axialError) - axialTolerance, 0.0); + double axialApproachSpeed = mechaMaxSailSpeed * axialEffectiveError / (axialEffectiveError + axialSlowDownDistance); + VectorLF3 axialDir = axialError >= 0 ? targetVelocityDir : - targetVelocityDir; + + // 横向 + double lateralEffectiveError = Math.Max(lateralError - lateralTolerance, 0.0); + double lateralApproachSpeed = + mechaMaxSailSpeed * lateralEffectiveError / (lateralEffectiveError + lateralSlowDownDistance); + + VectorLF3 relativeVelocityDesired = axialDir * axialApproachSpeed + lateralDir * lateralApproachSpeed; + + // 速度补偿 + if (distance > arriveRadius) + { + double cWeight = (distance - arriveRadius) / ResidualCompensationMaxDistance; + cWeight = Clamp(cWeight, 0, 1); + cWeight = cWeight * cWeight * (3.0 - 2.0 * cWeight); + double cSpeed = player.mecha.maxSailSpeed * cWeight * ResidualCompensationSpeedRatio + + ResidualCompensationMinSpeed; + double dot = VectorLF3.Dot(relativeVelocityDesired, toTargetDir); + if (dot < cSpeed) + relativeVelocityDesired += toTargetDir * cSpeed; + } + + // 合成目标速度 + VectorLF3 desiredVelocity = targetVelocity + relativeVelocityDesired; + double desiredSpeed = Math.Min(desiredVelocity.magnitude, mechaMaxSailSpeed); + VectorLF3 desiredDirection = SafeNorm(desiredVelocity, toTargetDir); + VectorLF3 playerDir = SafeNorm(playerVelocity, desiredDirection); + UpdateSailVelocityAndRotation( + controller: controller, + currentDir: playerDir, + currentSpeed: playerSpeed, + targetDir: desiredDirection, + targetSpeed: desiredSpeed, + forceSpeedUp: true); + } + + private static VectorLF3 SafeNorm(VectorLF3 vector, VectorLF3 fallback, double sqr = double.NaN) + { + sqr = double.IsNaN(sqr) ? vector.sqrMagnitude : sqr; + return sqr < Epsilon ? fallback : vector.normalized; + } + + /// + /// 更新航行速度(包括加速、转向、姿态控制、同步 visual_uvel) + /// + private static void UpdateSailVelocityAndRotation( + PlayerController controller, + VectorLF3 currentDir, + double currentSpeed, + VectorLF3 targetDir, + double targetSpeed, + bool forceSpeedUp = false) + { + PlayerMove_Sail sail = controller.actionSail; + Player player = controller.player; + Mecha mecha = player.mecha; + VectorLF3 currentVel = currentDir * currentSpeed; + double desiredSpeed = Math.Min(targetSpeed, mecha.maxSailSpeed); + double stepSpeed = currentSpeed; + + // 加减速 + bool speedUp = forceSpeedUp || (UseSpeedUp.Value && mecha.coreEnergy > UseSpeedUpMinimalEnergy.Value * 1000 * 1000); + if (speedUp && desiredSpeed > currentSpeed) + { + double dSpeed = Clamp(currentSpeed * 0.02, 7.0, sail.max_acc); + dSpeed = Math.Min(dSpeed, desiredSpeed - currentSpeed); + dSpeed = Math.Min(dSpeed, mecha.maxSailSpeed - currentSpeed); + if (dSpeed > 0) + stepSpeed = currentSpeed + dSpeed * sail.UseSailEnergy(dSpeed); + } else if (desiredSpeed < currentSpeed) + { + VectorLF3 dVelocityBrake = currentVel * 0.008; + sail.UseSailEnergy(ref dVelocityBrake, 1.5); + stepSpeed = Math.Max(0, (currentVel - dVelocityBrake).magnitude); + } + + // 平滑变速 + VectorLF3 targetVelocity = targetDir * stepSpeed; + float angle = Vector3.Angle(targetVelocity, currentVel); + var t = (float)(1.6 / Mathf.Max(10, angle)); + VectorLF3 dVelocity = (VectorLF3)Vector3.Slerp(currentVel, targetVelocity, t) - currentVel; + sail.UseSailEnergy(ref dVelocity, 0.36); + VectorLF3 newVelocity = currentVel + dVelocity; + + sail.input_aff_1 = 1.0; + player.uVelocity = newVelocity; + UpdateRotation(controller: controller); + } + + /// + /// 姿态控制 + /// + private static void UpdateRotation(PlayerController controller) + { + Player player = controller.player; + PlayerMove_Sail sail = controller.actionSail; + VectorLF3 newVelocity = player.uVelocity; + // 使用摄像机up方向 + if (newVelocity.magnitude > Epsilon) + { + VectorLF3 forward = newVelocity.normalized; + Vector3 cameraUp = controller.actionSail.sailPoser.targetURot * Vector3.up; + VectorLF3 cameraUpWorld = cameraUp; + double dot = VectorLF3.Dot(cameraUpWorld, forward); + VectorLF3 projectedUp = cameraUpWorld - forward * dot; + Vector3 newUp; + if (projectedUp.sqrMagnitude < Epsilon) + { + Vector3 cameraRight = controller.actionSail.sailPoser.targetURot * Vector3.right; + newUp = Vector3.Cross(cameraRight, forward); + if (newUp.sqrMagnitude < Epsilon) + newUp = Vector3.up; // 极端情况回退 + else + newUp.Normalize( ); + } else + newUp = ((Vector3)projectedUp).normalized; + Quaternion targetRot = Quaternion.LookRotation(forward, newUp); + float rotAngle = Quaternion.Angle(player.uRotation, targetRot); + float rotT = Mathf.Min(0.15f, 10f / Mathf.Max(10f, rotAngle)); // 每帧最多转15% + player.uRotation = Quaternion.Slerp(player.uRotation, targetRot, rotT); + } + // 同步 visual_uvel + PlanetData localPlanet = GameMain.localPlanet; + if (localPlanet != null) + { + VectorLF3 planetVel = localPlanet.GetUniversalVelocityAtLocalPoint(GameMain.gameTime, player.position); + sail.visual_uvel = newVelocity - planetVel; + } else + sail.visual_uvel = newVelocity; + } + + private static bool UpdateMovementState(EMovementState state) + { + if (LastMovementState == state) + return false; + LastMovementState = state; + return true; + } + + private static double Clamp(double value, double min, double max) => value < min ? min : value > max ? max : value; + + [HarmonyPatch(typeof(GameMain), "OnDestroy"), HarmonyPrefix] + private static void DestroyPrefix( ) => Reset( ); + + [HarmonyPatch(typeof(GameMain), nameof(GameMain.Pause)), HarmonyPrefix] + private static void PausePrefix( ) => Pause( ); + + [HarmonyPatch(typeof(GameMain), nameof(GameMain.Resume)), HarmonyPrefix] + private static void ResumePrefix( ) => Resume( ); + + [HarmonyTranspiler] + [HarmonyPatch(typeof(PlayerController), nameof(PlayerController.GameTick))] + private static IEnumerable PlayerController_GameTick_Transpiler( + IEnumerable instructions, + ILGenerator generator) + { + var matcher = new CodeMatcher(instructions, generator); + matcher.MatchForward( + false, + new CodeMatch( + OpCodes.Callvirt, + AccessTools.Method(typeof(BuildModel), nameof(BuildModel.EarlyGameTickIgnoreActive))) + ).Advance(1).InsertAndAdvance( + new CodeInstruction(OpCodes.Ldarg_0), + Transpilers.EmitDelegate((PlayerController controller) => + { + switch (controller.movementStateInFrame) + { + case EMovementState.Walk: + case EMovementState.Drift: + if (PreCheckAndRefreshTarget(controller.player) || DetermineTargetLocal( )) + return; + controller.input0.z = 1f; + break; + case EMovementState.Fly: + if (PreCheckAndRefreshTarget(controller.player) || DetermineTargetLocal( )) + return; + controller.input1.y = 1f; + controller.input0.y = 1f; + break; + } + }) + ); + return matcher.InstructionEnumeration( ); + + // 粗略判断抵达 + static bool DetermineTargetLocal( ) + { + if (TargetId != 0 && (TargetType != ESpaceGuideType.Planet || GameMain.localPlanet?.astroId != TargetId)) + return false; + StopAutoNavigation( ); + return true; + } + } + + [HarmonyPatch(typeof(PlayerMove_Sail), nameof(PlayerMove_Sail.GameTick)), HarmonyPostfix] + // ReSharper disable once InconsistentNaming + private static void PlayerMoveSailPostfix(PlayerMove_Sail __instance) + { + PlayerController controller = __instance.controller; + Player player = __instance.player; + if (PreCheckAndRefreshTarget(player)) + return; + bool isStateChanged = UpdateMovementState(controller.movementStateInFrame); + VectorLF3 playerPos = player.uPosition; + VectorLF3 targetVector = TargetUniversePosition - playerPos; + double distance = targetVector.magnitude; + if (isStateChanged) + __instance.sailPoser.targetURotWanted = Quaternion.LookRotation(targetVector); + + Stopwatch.Restart( ); + // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault + switch (TargetType) + { + case ESpaceGuideType.Star: + case ESpaceGuideType.Planet: + // 判断抵达 + if (distance < TargetArriveThresholdRadius) + { + if (player.warping) + player.warpCommand = false; + StopAutoNavigation( ); + return; + } + SailToTarget(controller, distance); + break; + case ESpaceGuideType.DFHive: + case ESpaceGuideType.DFCarrier: + FollowMovingTarget(controller, distance); + break; + } + + Stopwatch.Stop( ); + long ticks = Stopwatch.ElapsedTicks; + FrameTotalTicks += ticks; + FrameCount ++; + if (ticks > SlowFrameLimit) + { + SlowFrameTicks += ticks; + SlowFrameCount ++; + } + } + } + + public readonly struct SpaceObject(int astroId, VectorLF3 position, double radius, double radiusOffset) + { + public int AstroId { get; } = astroId; + public VectorLF3 Position { get; } = position; + public double Radius { get; } = radius; + public double RadiusOffset { get; } = radiusOffset; + } + + #endregion +} \ No newline at end of file diff --git a/UXAssist/UIConfigWindow.cs b/UXAssist/UIConfigWindow.cs index 08bb47d..9acf146 100644 --- a/UXAssist/UIConfigWindow.cs +++ b/UXAssist/UIConfigWindow.cs @@ -131,8 +131,15 @@ public static class UIConfigWindow I18N.Add("Shortcut keys for showing stars' name", "Shortcut keys for showing stars' name", "启用显示所有星系名称的快捷键"); I18N.Add("Auto navigation on sailings", "Auto navigation on sailings", "宇宙航行时自动导航"); I18N.Add("Enable auto-cruise", "Enable auto-cruise", "启用自动巡航"); + I18N.Add("New Navigation Algorithm", "New Navigation Algorithm", "新导航算法"); + I18N.Add("Stop AutoNavigation on arrival or input", "Stop AutoNavigation on arrival or input", "抵达或输入关闭导航"); I18N.Add("Auto boost", "Auto boost", "自动加速"); + I18N.Add("Auto boost Minimal Energy", "Auto boost Minimal Energy (MJ)", "自动加速最低能量(MJ)"); + I18N.Add("Use Warp", "Use Warp", "启用曲速"); I18N.Add("Distance to use warp", "Distance to use warp (AU)", "使用曲速的距离(AU)"); + I18N.Add("Use Warp Minimal Energy", "Use Warp Minimal Energy (MJ)", "使用曲速最低能量(MJ)"); + I18N.Add("Dark Fog Hive Follow Distance", "Dark Fog Hive Follow Distance (AU)", "黑雾巢穴跟踪距离(AU)"); + I18N.Add("Dark Fog Carrier Follow Distance", "Dark Fog Carrier Follow Distance (M)", "黑雾火种跟踪距离(M)"); I18N.Add("Treat stack items as single in monitor components", "Treat stack items as single in monitor components", "在流速计中将堆叠物品视为单个物品"); I18N.Add("Initialize This Planet", "Initialize this planet", "初始化本行星"); I18N.Add("Initialize This Planet Confirm", "This operation will destroy all buildings and revert terrains on this planet, are you sure?", "此操作将会摧毁本行星上的所有建筑并恢复地形,确定吗?"); @@ -183,13 +190,45 @@ public static class UIConfigWindow public override int ValueToIndex(float value) => Mathf.RoundToInt(value + 10f); } - private class DistanceMapper : MyWindow.ValueMapper - { - public override int Min => 1; - public override int Max => 40; - public override double IndexToValue(int index) => index * 0.5; - public override int ValueToIndex(double value) => Mathf.RoundToInt((float)(value * 2.0)); - } + private class DistanceMapper : MyWindow.ValueMapper + { + public override int Min => 1; + public override int Max => 40; + + public override double IndexToValue(int index) => index * 0.5; + + public override int ValueToIndex(double value) => Mathf.RoundToInt((float)(value * 2.0)); + } + + private class DistanceMapperHive : MyWindow.ValueMapper + { + public override int Min => 1; + public override int Max => 50; + + public override double IndexToValue(int index) => index * 0.1; + + public override int ValueToIndex(double value) => Mathf.RoundToInt((float)(value * 10)); + } + + private class DistanceMapperCarrier : MyWindow.ValueMapper + { + public override int Min => 100; + public override int Max => 3000; + + public override double IndexToValue(int index) => index; + + public override int ValueToIndex(double value) => Mathf.RoundToInt((float)value); + } + + private class EnergyMapper : MyWindow.ValueMapper + { + public override int Min => 50; + public override int Max => 1000; + + public override double IndexToValue(int index) => index; + + public override int ValueToIndex(double value) => Mathf.RoundToInt((float)value); + } private class UpsMapper : MyWindow.ValueMapper { @@ -763,24 +802,111 @@ public static class UIConfigWindow { y += 36f; - wnd.AddCheckBox(x, y, tab4, PlayerPatch.AutoNavigationEnabled, "Auto navigation on sailings"); - y += 27f; + MyCheckBox autoNavCheckBox = wnd.AddCheckBox(x, y, tab4, PlayerPatch.AutoNavigationEnabled, "Auto navigation on sailings"); + y += 27f; var autoCruiseCheckBox = wnd.AddCheckBox(x + 20f, y, tab4, PlayerPatch.AutoCruiseEnabled, "Enable auto-cruise", 13); y += 27f; var autoBoostCheckBox = wnd.AddCheckBox(x + 20f, y, tab4, PlayerPatch.AutoBoostEnabled, "Auto boost", 13); y += 27f; txt = wnd.AddText2(x + 20f, y, tab4, "Distance to use warp", 15, "text-distance-to-warp"); var navDistanceSlider = wnd.AddSlider(x + 20f + txt.preferredWidth + 5f, y + 6f, tab4, PlayerPatch.DistanceToWarp, new DistanceMapper(), "0.0", 100f); - PlayerPatch.AutoNavigationEnabled.SettingChanged += NavSettingChanged; - wnd.OnFree += () => { PlayerPatch.AutoNavigationEnabled.SettingChanged -= NavSettingChanged; }; - NavSettingChanged(null, null); - void NavSettingChanged(object o, EventArgs e) - { - autoCruiseCheckBox.SetEnable(PlayerPatch.AutoNavigationEnabled.Value); - autoBoostCheckBox.SetEnable(PlayerPatch.AutoNavigationEnabled.Value); - navDistanceSlider.SetEnable(PlayerPatch.AutoNavigationEnabled.Value); - } + y += 36f; + MyCheckBox newAlgorithmCheckBox = wnd.AddCheckBox( + x, + y, + tab4, + PlayerPatch.UseNewNavigationAlgorithm, + "New Navigation Algorithm"); + y += 27f; + MyCheckBox autoStopCheckBox = wnd.AddCheckBox( + x, + y, + tab4, + PlayerPatch.StopOnArrivalAndInput, + "Stop AutoNavigation on arrival or input"); + y += 27f; + MyCheckBox useWarpCheckBox = wnd.AddCheckBox(x, y, tab4, PlayerPatch.UseWarper, "Use Warp"); + y += 27f; + txt = wnd.AddText2(x + 20f, y, tab4, "Use Warp Minimal Energy", 15, "Use Warp Minimal Energy"); + var warpEnergySlider = wnd.AddSideSlider( + x + 20f + txt.preferredWidth + 5f, + y + 6f, + tab4, + PlayerPatch.UseWarperMinimalEnergy, + new EnergyMapper( ), + "0", + 200f, + - 100f).WithFontSize(13); + y += 27f; + txt = wnd.AddText2(x + 20f, y, tab4, "Distance to use warp", 15, "text-distance-to-warp"); + var newNavDistanceSlider = wnd.AddSlider( + x + 20f + txt.preferredWidth + 5f, + y + 6f, + tab4, + PlayerPatch.UseWarperDistance, + new DistanceMapper( ), + "0.0", + 200f); + y += 27f; + MyCheckBox speedUpCheckBox = wnd.AddCheckBox(x, y, tab4, PlayerPatch.UseSpeedUp, "Auto boost"); + y += 27f; + txt = wnd.AddText2(x + 20f, y, tab4, "Auto boost Minimal Energy", 15, "Auto boost Minimal Energy"); + var speedUpEnergySlider = wnd.AddSideSlider( + x + 20f + txt.preferredWidth + 5f, + y + 6f, + tab4, + PlayerPatch.UseSpeedUpMinimalEnergy, + new EnergyMapper( ), + "0", + 200f, + - 100f).WithFontSize(13); + y += 27f; + txt = wnd.AddText2(x, y, tab4, "Dark Fog Hive Follow Distance", 15, "Dark Fog Hive Follow Distance"); + var dfHiveSlider = wnd.AddSideSlider( + x + txt.preferredWidth + 5f, + y + 6f, + tab4, + PlayerPatch.DFHiveFollowDistance, + new DistanceMapperHive( ), + "0.0", + 200f, + - 100f).WithFontSize(13); + y += 27f; + txt = wnd.AddText2(x, y, tab4, "Dark Fog Carrier Follow Distance", 15, "Dark Fog Carrier Follow Distance"); + var dfCarrierSlider = wnd.AddSideSlider( + x + txt.preferredWidth + 5f, + y + 6f, + tab4, + PlayerPatch.DFCarrierFollowDistance, + new DistanceMapperCarrier( ), + "0.0", + 200f, + - 100f).WithFontSize(13); + + PlayerPatch.AutoNavigationEnabled.SettingChanged += NavSettingChanged; + PlayerPatch.UseNewNavigationAlgorithm.SettingChanged += NavSettingChanged; + wnd.OnFree += ( ) => { PlayerPatch.AutoNavigationEnabled.SettingChanged -= NavSettingChanged; }; + wnd.OnFree += ( ) => { PlayerPatch.UseNewNavigationAlgorithm.SettingChanged -= NavSettingChanged; }; + NavSettingChanged(null, null); + + void NavSettingChanged(object o, EventArgs e) + { + autoNavCheckBox.SetEnable(! PlayerPatch.UseNewNavigationAlgorithm.Value); + autoCruiseCheckBox.SetEnable(PlayerPatch.AutoNavigationEnabled.Value); + autoBoostCheckBox.SetEnable(PlayerPatch.AutoNavigationEnabled.Value); + navDistanceSlider.SetEnable(PlayerPatch.AutoNavigationEnabled.Value); + + newAlgorithmCheckBox.SetEnable(! PlayerPatch.AutoNavigationEnabled.Value); + autoStopCheckBox.SetEnable(PlayerPatch.UseNewNavigationAlgorithm.Value); + useWarpCheckBox.SetEnable(PlayerPatch.UseNewNavigationAlgorithm.Value); + warpEnergySlider.SetEnable(PlayerPatch.UseNewNavigationAlgorithm.Value); + newNavDistanceSlider.SetEnable(PlayerPatch.UseNewNavigationAlgorithm.Value); + speedUpCheckBox.SetEnable(PlayerPatch.UseNewNavigationAlgorithm.Value); + speedUpEnergySlider.SetEnable(PlayerPatch.UseNewNavigationAlgorithm.Value); + dfHiveSlider.SetEnable(PlayerPatch.UseNewNavigationAlgorithm.Value); + dfCarrierSlider.SetEnable(PlayerPatch.UseNewNavigationAlgorithm.Value); + } } var tab5 = wnd.AddTab(trans, "Dyson Sphere"); diff --git a/UXAssist/UXAssist.cs b/UXAssist/UXAssist.cs index c45d62f..4bdc69a 100644 --- a/UXAssist/UXAssist.cs +++ b/UXAssist/UXAssist.cs @@ -233,6 +233,7 @@ public class UXAssist : BaseUnityPlugin, IModCanSave DysonSpherePatch.AutoConstructMultiplier = Config.Bind("DysonSphere", "AutoConstructMultiplier", 1, "Dyson Sphere auto-construct speed multiplier"); UIPatch.PlanetVeinUtilizationEnabled = Config.Bind("UI", "PlanetVeinUtilization", false, "Planet vein utilization"); + PlayerPatch.AutoNavigationG.Awake(this.Config); I18N.Init(); I18N.Add("UXAssist Config", "UXAssist Config", "UX助手设置"); @@ -287,4 +288,4 @@ public class UXAssist : BaseUnityPlugin, IModCanSave LogisticsPatch.OnUpdate(); } -} +} \ No newline at end of file