Last active
December 5, 2020 04:11
-
-
Save gotmachine/5b6fa92012bc6b545319c1117d9afcd1 to your computer and use it in GitHub Desktop.
PhysicsHold : KSP proof of concept mod for making landed vessels physicsless.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using KSP.UI.Screens.Flight; | |
using System; | |
using System.Collections; | |
using System.Collections.Generic; | |
using System.Diagnostics; | |
using System.Reflection; | |
using UnityEngine; | |
using Debug = UnityEngine.Debug; | |
/* | |
This add a "Landed physics hold" PAW button on all command parts (and root part if no command part found), | |
available when the vessel is landed and has a surface speed less than 1 m/s (arbitrary, could more/less). | |
When enabled, all rigibodies on the vessel are made kinematic by forcing the stock "packed" (or "on rails") | |
state normally used during "physics easing" and non-physics timewarp. | |
When enabled, all joint/force/torque physics are disabled, making the vessel an unmovable object fixed at | |
a given altitude/longitude/latitude. You can still collide with it, but it will not react to collisions. | |
Working and tested : | |
- Undocking : the dominant vessel will stay on physics hold, the undocking vessel will be physics enabled | |
- Decoupling : works by insta-restoring physics, so far no issues detected | |
- Initizalization issue with wheels taken care of | |
- EVAing / boarding (hack in place to enable the kerbal portraits UI) | |
- Control input (rotation, translation, throttle...) is forced to zero by the stock vessel.packed check | |
- KIS attaching parts *seems* to work but they currently aren't made kinematic until a scene reload | |
Not working : | |
- KAS definitely doesn't work. It probably can, since it is able to handle things in timewarp, but that | |
will require modifications on its side. | |
Untested / likely to have issues : | |
- ModuleGrappleNode (Klaw) | |
- Stock robotics have some packed-dependant initialization code. | |
*/ | |
namespace PhysicsHold | |
{ | |
public class PhysicsHold : VesselModule | |
{ | |
private static string cacheAutoLOC_459494; | |
private static FieldInfo framesAtStartupFieldInfo; | |
private static MethodInfo KerbalPortrait_CanEVA; | |
private static FieldInfo physicsHoldField; | |
private static bool initDone = false; | |
[KSPField(isPersistant = true)] public bool physicsHold; | |
private List<CommandPart> commandParts; | |
private Vessel lastDecoupledPartVessel; | |
private bool hasEverBeenUnpacked; | |
private bool isEnabled = true; | |
public override bool ShouldBeActive() | |
{ | |
return | |
HighLogic.LoadedSceneIsFlight | |
&& vessel.loaded | |
&& !vessel.isEVA | |
&& vessel.id != Guid.Empty // exclude flags | |
&& isEnabled; | |
} | |
#region LIFECYCLE | |
protected override void OnAwake() | |
{ | |
if (!initDone) | |
{ | |
initDone = true; | |
physicsHoldField = GetType().GetField(nameof(physicsHold)); | |
try | |
{ | |
cacheAutoLOC_459494 = (string)typeof(KerbalPortrait).GetField("cacheAutoLOC_459494", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null); | |
} | |
catch (Exception e) | |
{ | |
Debug.LogWarning($"Cant find the EVA available tooltip AUTOLOC\n{e}"); | |
cacheAutoLOC_459494 = string.Empty; | |
} | |
try | |
{ | |
framesAtStartupFieldInfo = typeof(Vessel).GetField("framesAtStartup", BindingFlags.Instance | BindingFlags.NonPublic); | |
} | |
catch (Exception e) | |
{ | |
Debug.LogError($"Cant find the Vessel.framesAtStartup field\n{e}"); | |
initDone = false; | |
} | |
try | |
{ | |
KerbalPortrait_CanEVA = typeof(KerbalPortrait).GetMethod("CanEVA", BindingFlags.Instance | BindingFlags.NonPublic); | |
} | |
catch (Exception e) | |
{ | |
Debug.LogError($"Cant find the KerbalPortrait.CanEVA method\n{e}"); | |
initDone = false; | |
} | |
if (!initDone) | |
{ | |
isEnabled = enabled = false; | |
} | |
} | |
} | |
protected override void OnStart() | |
{ | |
// not allowed for EVA kerbals and flags (note : vessel.IsEVA isn't always set in OnStart()) | |
if (vessel.isEVA || vessel.id == Guid.Empty) | |
{ | |
isEnabled = enabled = false; | |
return; | |
} | |
hasEverBeenUnpacked = !physicsHold; | |
SetupCommandParts(); | |
GameEvents.onPartCouple.Add(OnPartCouple); // before docking/coupling | |
GameEvents.onPartCoupleComplete.Add(OnPartCoupleComplete); // after docking/coupling | |
GameEvents.onPartDeCouple.Add(OnPartDeCouple); // before coupling | |
GameEvents.onPartDeCoupleComplete.Add(OnPartDeCoupleComplete); // after coupling | |
GameEvents.onPartUndock.Add(OnPartUndock); // before docking | |
GameEvents.onVesselsUndocking.Add(OnVesselsUndocking); // after docking | |
GameEvents.onPartDestroyed.Add(OnPartDestroyed); | |
} | |
private void ClearEvents() | |
{ | |
GameEvents.onPartCouple.Remove(OnPartCouple); | |
GameEvents.onPartCoupleComplete.Remove(OnPartCoupleComplete); | |
GameEvents.onPartDeCouple.Remove(OnPartDeCouple); | |
GameEvents.onPartDeCoupleComplete.Remove(OnPartDeCoupleComplete); | |
GameEvents.onPartUndock.Remove(OnPartUndock); | |
GameEvents.onVesselsUndocking.Remove(OnVesselsUndocking); | |
GameEvents.onPartDestroyed.Remove(OnPartDestroyed); | |
} | |
public void OnDestroy() | |
{ | |
ClearEvents(); | |
} | |
#endregion | |
#region UPDATE | |
// could use BaseField.OnValueModified instead, but a | |
// polling pattern is easier. | |
public void FixedUpdate() | |
{ | |
if (physicsHold) | |
{ | |
if (vessel.Landed) | |
{ | |
if (!vessel.packed) | |
{ | |
vessel.GoOnRails(); | |
} | |
} | |
else | |
{ | |
physicsHold = false; | |
hasEverBeenUnpacked = true; | |
} | |
} | |
} | |
public void Update() | |
{ | |
// physics holding is only allowed while landed, and moving at less than 1 m/s | |
bool holdAllowed = vessel.Landed && vessel.srfSpeed < 1.0; | |
foreach (CommandPart commandPart in commandParts) | |
{ | |
commandPart.field.guiActive = holdAllowed; | |
} | |
if (physicsHold) | |
{ | |
// keep the vessel forever in "physics hold" mode by resetting the "last off rails" frame to the current one. | |
framesAtStartupFieldInfo.SetValue(vessel, Time.frameCount); | |
// remove the "physics hold" control lock | |
// note that we don't set the private Vessel.physicsHoldLock field to false, resulting in the Vessel.HoldPhysics property staying true | |
// The only impact (in stock) is that g-force / dynamic pressure checks for breaking deployable parts (solar panels/radiators/antennas) | |
// won't run, which is good. | |
if (vessel.isActiveVessel) | |
{ | |
InputLockManager.RemoveControlLock("physicsHold"); | |
} | |
} | |
} | |
// KerbalPortrait will prevent the "go to EVA" button from working if Part.packed is true (no such limitation on the "crew transfer" PAW UI) | |
// This is done in Update(), so we un-do it in LateUpdate() | |
public void LateUpdate() | |
{ | |
if (!vessel.isActiveVessel || !physicsHold || KerbalPortraitGallery.Instance == null) | |
return; | |
foreach (KerbalPortrait kp in KerbalPortraitGallery.Instance.Portraits) | |
{ | |
if (kp.hoverArea.Hover && !kp.evaButton.interactable && kp.crewMember != null) | |
{ | |
kp.crewMember.InPart.packed = false; | |
try | |
{ | |
kp.evaButton.interactable = (bool)KerbalPortrait_CanEVA.Invoke(kp, null); | |
} | |
catch (Exception) { } | |
kp.crewMember.InPart.packed = true; | |
if (kp.evaButton.interactable) | |
kp.evaTooltip.textString = cacheAutoLOC_459494; | |
} | |
} | |
} | |
#endregion | |
#region GAMEEVENTS | |
// Called when a docking/coupling action is about to happen. Gives access to old and new vessel | |
// Remove PAW buttons from the command parts and disable ourselves when the vessel | |
// is about to be removed following a docking / coupling operation. | |
private void OnPartCouple(GameEvents.FromToAction<Part, Part> data) | |
{ | |
LogDebug($"OnPartCouple on {vessel.vesselName}, docked vessel : {data.from.vessel.vesselName}, dominant vessel : {data.to.vessel.vesselName}"); | |
// in the case of KIS-adding parts, from / to vessel are the same : we ignore the event | |
if (data.from.vessel == data.to.vessel) | |
return; | |
// "from" is the part on the vessel that will be removed following coupling/docking | |
if (data.from.vessel == vessel) | |
{ | |
foreach (CommandPart commandPart in commandParts) | |
{ | |
commandPart.ClearBaseField(); | |
} | |
commandParts.Clear(); | |
ClearEvents(); | |
isEnabled = enabled = false; | |
} | |
} | |
// Called after a docking/coupling action has happend. All parts now belong to the same vessel. | |
private void OnPartCoupleComplete(GameEvents.FromToAction<Part, Part> data) | |
{ | |
LogDebug($"OnPartCoupleComplete on {vessel.vesselName} from {data.from.vessel.vesselName} to {data.to.vessel.vesselName}"); | |
if (data.from.vessel != vessel) | |
return; | |
// add any command part we don't already know about | |
foreach (Part part in vessel.Parts) | |
{ | |
if (part.HasModuleImplementing<ModuleCommand>() && !commandParts.Exists(p => p.part == part)) | |
{ | |
commandParts.Add(new CommandPart(this, part)); | |
} | |
} | |
} | |
// called before a new vessel is created following intentional decoupler use or a joint failure | |
// the part.vessel reference is still the old, non separated vessel | |
private void OnPartDeCouple(Part part) | |
{ | |
if (part.vessel != vessel) | |
return; | |
foreach (CommandPart commandPart in commandParts) | |
{ | |
commandPart.ClearBaseField(); | |
} | |
commandParts.Clear(); | |
lastDecoupledPartVessel = vessel; // see why in OnPartDeCoupleComplete | |
if (physicsHold) | |
{ | |
physicsHold = false; | |
if (!hasEverBeenUnpacked) | |
{ | |
hasEverBeenUnpacked = true; | |
SetupWheels(); | |
} | |
framesAtStartupFieldInfo.SetValue(vessel, Time.frameCount - 100); | |
vessel.GoOffRails(); | |
} | |
} | |
// called after a new vessel is created following intentional decoupler use or a joint failure | |
// we have no way to identify the "old" vessel from which the part comes from, so we have saved | |
// the vessel reference in OnPartCouple. Note that OnPartDeCouple/OnPartDeCoupleComplete are called | |
// at the begining/end of Part.decouple(), so it's safe to do. | |
// Also, GameEvents.onPartDeCoupleNewVesselComplete with access to both old and new vessel has been | |
// introduced in KSP 1.10 but for the sake of making this work in 1.8 - 1.9 we don't use it | |
private void OnPartDeCoupleComplete(Part newVesselPart) | |
{ | |
if (lastDecoupledPartVessel == null || lastDecoupledPartVessel != vessel) | |
return; | |
lastDecoupledPartVessel = null; | |
SetupCommandParts(); | |
} | |
// called before undocking, all parts still belong to the original vessel | |
private void OnPartUndock(Part part) | |
{ | |
if (part.vessel != vessel) | |
return; | |
if (physicsHold) | |
{ | |
if (!hasEverBeenUnpacked) | |
{ | |
hasEverBeenUnpacked = true; | |
SetupWheels(); | |
} | |
// Things will go awfully wrong for the undocked vessel if the initial vessel is still packed when Part.Undock() is called. | |
// So we immediately unpack it by calling GoOffRails() while setting "framesAtStartup" in order to have PhysicsHoldExpired() return true. | |
framesAtStartupFieldInfo.SetValue(vessel, Time.frameCount - 100); | |
vessel.GoOffRails(); | |
} | |
} | |
// called after a new vessel is created following undocking | |
// here we do have to do the huge mess we do for uncoupling. Since we have access to the old vessel, we can | |
// just remove all parts that are now on the new vessel. | |
private void OnVesselsUndocking(Vessel oldVessel, Vessel newVessel) | |
{ | |
LogDebug($"OnVesselsUndocking called on {vessel.vesselName}, oldVessel {oldVessel.vesselName}, newVessel {newVessel.vesselName}"); | |
if (vessel != oldVessel) | |
return; | |
foreach (Part part in newVessel.Parts) | |
{ | |
int commandPartIndex = commandParts.FindIndex(p => p.part == part); | |
if (commandPartIndex >= 0) | |
{ | |
commandParts[commandPartIndex].ClearBaseField(); | |
commandParts.RemoveAt(commandPartIndex); | |
} | |
} | |
// Force the dominant vessel to stay packed. | |
// Landed has been reset by the GoOffRails() call in OnPartUndock(), but by forcing Landed, since we didn't | |
// set physicsHold to false, the next FixedUpdate() will call GoOnRails() and immediately re-pack the vessel. | |
if (physicsHold) | |
{ | |
vessel.Landed = true; | |
} | |
} | |
private void OnPartDestroyed(Part part) | |
{ | |
int partIndex = commandParts.FindIndex(p => p.part == part); | |
if (partIndex >= 0) | |
{ | |
commandParts[partIndex].ClearBaseField(); | |
commandParts.RemoveAt(partIndex); | |
} | |
} | |
#endregion | |
#region PAW UI BUTTONS | |
/// <summary> | |
/// Add our PAW button to every command part, or to the root part if no command part is found. | |
/// </summary> | |
private void SetupCommandParts() | |
{ | |
if (commandParts == null) | |
commandParts = new List<CommandPart>(); | |
foreach (Part part in vessel.Parts) | |
{ | |
if (part.HasModuleImplementing<ModuleCommand>()) | |
{ | |
commandParts.Add(new CommandPart(this, part)); | |
} | |
} | |
if (commandParts.Count == 0) | |
{ | |
commandParts.Add(new CommandPart(this, vessel.rootPart)); | |
} | |
} | |
private class CommandPart | |
{ | |
public Part part; | |
public BaseField field; | |
public CommandPart(PhysicsHold instance, Part part) | |
{ | |
this.part = part; | |
field = new BaseField(new UI_Toggle(), physicsHoldField, instance); | |
part.Fields.Add(field); | |
field.uiControlFlight = new UI_Toggle(); | |
field.guiName = "Landed physics hold"; | |
field.guiActive = part.vessel.Landed; // don't really care, this is updated in Update() | |
field.guiActiveUnfocused = true; | |
field.guiUnfocusedRange = 500f; | |
field.uiControlFlight.requireFullControl = false; | |
} | |
public void ClearBaseField() | |
{ | |
bool hasRemovedFields = false; | |
try | |
{ | |
List<BaseField> fields = (List<BaseField>)typeof(BaseFieldList).GetField("_fields", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(part.Fields); | |
for (int i = fields.Count - 1; i >= 0; i--) | |
{ | |
if (fields[i] == field) | |
{ | |
fields.RemoveAt(i); | |
hasRemovedFields = true; | |
} | |
} | |
} | |
catch (Exception e) | |
{ | |
Debug.LogWarning($"Error removing basefield {field.name} on part {part.name}\n{e}"); | |
hasRemovedFields = true; | |
} | |
finally | |
{ | |
if (hasRemovedFields && UIPartActionController.Instance != null) | |
{ | |
// Ideally we should remove the UI item corresponding to the basefield, | |
// but that isn't so easy. Destroying the PAWs is good enough given | |
// how unfrequently this is called. | |
for (int i = UIPartActionController.Instance.windows.Count - 1; i >= 0; i--) | |
{ | |
if (UIPartActionController.Instance.windows[i].part == part) | |
{ | |
UIPartActionController.Instance.windows[i].gameObject.DestroyGameObjectImmediate(); | |
UIPartActionController.Instance.windows.RemoveAt(i); | |
} | |
} | |
for (int i = UIPartActionController.Instance.hiddenWindows.Count - 1; i >= 0; i--) | |
{ | |
if (UIPartActionController.Instance.hiddenWindows[i].part == part) | |
{ | |
UIPartActionController.Instance.hiddenWindows[i].gameObject.DestroyGameObject(); | |
UIPartActionController.Instance.hiddenWindows.RemoveAt(i); | |
} | |
} | |
} | |
} | |
} | |
} | |
#endregion | |
#region SPECIFIC HACKS | |
/// <summary> | |
/// Wheels have a wheelSetup() method being called by a coroutine launched from OnStart(). That coroutine is waiting indefinitely | |
/// for part.packed to become false, which won't happen if the vessel is in hold since the scene start. This is an issue if we want | |
/// to undock the vessel, as wheels have a onVesselUndocking callback that will nullref if the setup isn't done. | |
/// So, when we undock a packed vessel, if that vessel has never been unpacked, we manually call wheelSetup(), and cancel the | |
/// coroutine (it will nullref if called twice). | |
/// </summary> | |
private void SetupWheels() | |
{ | |
foreach (ModuleWheelBase wheel in vessel.FindPartModulesImplementing<ModuleWheelBase>()) | |
{ | |
// TODO : cache the fieldinfo / methodinfo | |
if (!((bool)typeof(ModuleWheelBase).GetField("setup", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(wheel))) | |
{ | |
wheel.StopAllCoroutines(); | |
typeof(ModuleWheelBase).GetMethod("wheelSetup", BindingFlags.Instance | BindingFlags.NonPublic).Invoke(wheel, null); | |
} | |
} | |
} | |
// ModuleDockingNode FSM hacking, not needed but keeping it for reference in case we want to try enabling | |
// docking a packed vessel | |
private IEnumerator SetupDockingNodesHoldState() | |
{ | |
foreach (Part part in vessel.Parts) | |
{ | |
// part.dockingPorts is populated from Awake() | |
foreach (PartModule partModule in part.dockingPorts) | |
{ | |
if (!(partModule is ModuleDockingNode dockingNode)) | |
continue; | |
while (dockingNode.on_undock == null) | |
{ | |
yield return null; | |
} | |
dockingNode.on_undock.OnCheckCondition += (KFSMState st) => part.vessel.FindVesselModuleImplementing<PhysicsHold>().physicsHold == false; | |
} | |
} | |
yield break; | |
} | |
#endregion | |
#region UTILS | |
[Conditional("DEBUG")] | |
private static void LogDebug(string message) | |
{ | |
Debug.Log(message); | |
} | |
#endregion | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment