### 提交目的:

为玩家添加新的自动导航选项并集成新版导航算法。

### 修改内容:
- **新增** 新的导航算法选项及相关功能:
  - 支持启用/禁用新版导航算法。
  - 自动导航选项支持到达目标或输入时自动暂停。
  - 添加曲速启用选项、最低能量限制及使用距离的设置。
  - 添加自动加速选项及其最低能量限制。
  - 黑雾巢穴和火种的跟踪距离参数支持自定义。
- **扩展** `DistanceMapper` 类,增加了对巢穴及火种跟踪距离映射的支持。
- **更新** 玩家界面,以支持导航算法相关的多项新配置:
  - 在 UI 中增加多个与新版导航算法相关的开关、滑块和提示文本。
- **调整** 自动导航逻辑,新增多项参数以提升日志提示和控制细粒度。

### 一般性修改:
- 优化代码逻辑,分类整理导航功能和方法。
- 增加部分注释以提高代码可读性。
- **重构** 部分导航流程代码,减少重复实现,提高可维护性。
This commit is contained in:
JimG
2026-07-22 14:23:13 +08:00
committed by soarqin
parent 4ed33b2638
commit bf73ccd689
3 changed files with 918 additions and 22 deletions
+772 -3
View File
@@ -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<PlayerPatch>
private static PressKeyBind _toggleAllStarsNameKey;
private static PressKeyBind _autoDriveKey;
public static ConfigEntry<bool> UseNewNavigationAlgorithm { get; set; } = null!;
public static ConfigEntry<bool> StopOnArrivalAndInput { get; set; } = null!;
public static ConfigEntry<bool> UseWarper { get; set; } = null!;
public static ConfigEntry<double> UseWarperMinimalEnergy { get; set; } = null!;
public static ConfigEntry<double> UseWarperDistance { get; set; } = null!;
public static ConfigEntry<bool> UseSpeedUp { get; set; } = null!;
public static ConfigEntry<double> UseSpeedUpMinimalEnergy { get; set; } = null!;
public static ConfigEntry<double> DFHiveFollowDistance { get; set; } = null!;
public static ConfigEntry<double> DFCarrierFollowDistance { get; set; } = null!;
public static void Init()
{
_showAllStarsNameKey = KeyBindings.RegisterKeyBinding(new BuiltinKey
@@ -45,7 +61,7 @@ public class PlayerPatch : PatchImpl<PlayerPatch>
_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<PlayerPatch>
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<PlayerPatch>
{
AutoNavigation.ToggleAutoCruise();
}
AutoNavigationG.Update( );
}
public static void Uninit()
@@ -352,6 +370,8 @@ public class PlayerPatch : PatchImpl<PlayerPatch>
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<PlayerPatch>
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<PlayerPatch>
}
*/
}
}
#region
public class AutoNavigationG : PatchImpl<AutoNavigationG>
{
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<SpaceObject> 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<double>(
"Player",
nameof(UseWarperMinimalEnergy),
800,
"使用翘曲最低能量(MJ)");
UseWarperDistance = config.Bind<double>("Player", nameof(UseWarperDistance), 2, "最短翘曲距离(AU)");
UseSpeedUp = config.Bind("Player", nameof(UseSpeedUp), true, "是否加速");
UseSpeedUpMinimalEnergy = config.Bind<double>(
"Player",
nameof(UseSpeedUpMinimalEnergy),
100,
"加速最低能量(MJ)");
DFHiveFollowDistance = config.Bind(
"Player",
nameof(DFHiveFollowDistance),
0.5,
"黑雾蜂巢跟随距离(AU)");
DFCarrierFollowDistance = config.Bind<double>(
"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;
/// <summary>
/// 前置检查与目标检测
/// </summary>
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;
}
/// <summary>
/// 航向指定目标,考虑避障
/// </summary>
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);
}
}
/// <summary>
/// 计算方向,考虑避障
/// </summary>
/// <param name="playerPos">玩家位置</param>
/// <param name="spaceObjects">障碍物集合</param>
/// <returns></returns>
private static VectorLF3 ComputeDirection(VectorLF3 playerPos, List<SpaceObject> 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;
}
/// <summary>
/// 航向并跟随移动目标,进入近距离跟随后无避障
/// <param name="distance">到目标距离</param>
/// <param name="followDistance">目标跟踪距离</param>
/// </summary>
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;
}
/// <summary>
/// 更新航行速度(包括加速、转向、姿态控制、同步 visual_uvel
/// </summary>
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);
}
/// <summary>
/// 姿态控制
/// </summary>
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<CodeInstruction> PlayerController_GameTick_Transpiler(
IEnumerable<CodeInstruction> 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
}
+144 -18
View File
@@ -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<double>
{
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<double>
{
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<double>
{
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<double>
{
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<double>
{
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<double>
{
@@ -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");
+2 -1
View File
@@ -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();
}
}
}