Skip to content

Commit

Permalink
Fix #182: ModulePartInventory now accounts for changes to a part's ma…
Browse files Browse the repository at this point in the history
…ss or volume

-for example, changing a part's variant (rcs thruster blocks, structural tube) or its resource level could change its mass, but when you store it in inventory the mass would always use the value from the prefab
  • Loading branch information
JonnyOThan committed Jan 26, 2024
1 parent c9eb4d1 commit 8ae4846
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 2 deletions.
6 changes: 6 additions & 0 deletions GameData/KSPCommunityFixes/Settings.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,12 @@ KSP_COMMUNITY_FIXES
// Fix active vessel orbit moving randomly when engaging timewarp while under heavy CPU load.
TimeWarpOrbitShift = true

// Fixes mass and volume of parts stored in inventories, especially when part variants or
// non-default resource amounts are used.
// Prevents changing the variant of parts in inventory if it can modify the cost or mass.
// Fixes the part info tooltip so that it shows the stored part's data instead of the prefab.
InventoryPartMass = true
// ##########################
// Obsolete bugfixes
// ##########################
Expand Down
167 changes: 167 additions & 0 deletions KSPCommunityFixes/BugFixes/InventoryPartMass.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
using HarmonyLib;
using KSP.Localization;
using KSP.UI.Screens;
using KSP.UI.Screens.Editor;
using System;
using System.Collections.Generic;

namespace KSPCommunityFixes.BugFixes
{
// https://github.com/KSPModdingLibs/KSPCommunityFixes/issues/182

class InventoryPartMass : BasePatch
{
protected override Version VersionMin => new Version(1, 12, 0);

protected override void ApplyPatches(List<PatchInfo> patches)
{
patches.Add(new PatchInfo(
PatchMethodType.Prefix,
AccessTools.Method(typeof(ModuleInventoryPart), nameof(ModuleInventoryPart.UpdateCapacityValues)),
this));

var EditorPartIcon_Create_ArgTypes = new Type[]
{
typeof(EditorPartList),
typeof(AvailablePart),
typeof(StoredPart),
typeof(float),
typeof(float),
typeof(float),
typeof(Callback<EditorPartIcon>),
typeof(bool),
typeof(bool),
typeof(PartVariant),
typeof(bool),
typeof(bool)
};

patches.Add(new PatchInfo(
PatchMethodType.Postfix,
AccessTools.Method(typeof(KSP.UI.Screens.EditorPartIcon), nameof(KSP.UI.Screens.EditorPartIcon.Create), EditorPartIcon_Create_ArgTypes),
this));

patches.Add(new PatchInfo(
PatchMethodType.Postfix,
AccessTools.Method(typeof(InventoryPartListTooltip), nameof(InventoryPartListTooltip.CreateInfoWidgets)),
this));

// Making packedVolume persistent helps track what cargo modules *should* be if they were changed from the prefab before being added to the inventory
StaticHelpers.EditPartModuleKSPFieldAttributes(typeof(ModuleCargoPart), nameof(ModuleCargoPart.packedVolume), field => field.isPersistant = true);
}

// the stock version of this function uses values from the prefab only, which is incorrect when mass modifiers are used (e.g. ModulePartVariants) or the packed volume is changed (e.g. TweakScale) or the resource levels are changed
static bool ModuleInventoryPart_UpdateCapacityValues_Prefix(ModuleInventoryPart __instance)
{
__instance.volumeOccupied = 0.0f;
__instance.massOccupied = 0.0f;
foreach (StoredPart storedPart in __instance.storedParts.ValuesList)
{
if (storedPart != null && storedPart.snapshot != null)
{
__instance.massOccupied += GetPartSnapshotMass(storedPart.snapshot) * storedPart.quantity; // This won't be correct if different parts in the stack have different mass modifiers, but really they shouldn't have been stacked in the first place
__instance.volumeOccupied += GetPartSnapshotVolume(storedPart.snapshot) * storedPart.quantity; // see above.
}
}
__instance.UpdateMassVolumeDisplay(true, false);
return false;
}

static float GetPartSnapshotMass(ProtoPartSnapshot partSnapshot)
{
double mass = partSnapshot.mass;

foreach (var resource in partSnapshot.resources)
{
mass += resource.amount * resource.definition.density;
}

return (float)mass;
}

static float GetPartSnapshotVolume(ProtoPartSnapshot partSnapshot)
{
// fetch the volume from the cargo module snapshot
foreach (var moduleSnapshot in partSnapshot.modules)
{
if (moduleSnapshot.moduleName != nameof(ModuleCargoPart)) continue;

float packedVolume = 0;
if (moduleSnapshot.moduleValues.TryGetValue(nameof(ModuleCargoPart.packedVolume), ref packedVolume))
{
return packedVolume;
}
}

// otherwise we have to fall back to the prefab volume (this is stock behavior)
ModuleCargoPart moduleCargoPart = partSnapshot.partPrefab.FindModuleImplementing<ModuleCargoPart>();
if (moduleCargoPart != null)
{
return moduleCargoPart.packedVolume;
}
return 0f;
}

// the game doesn't handle swapping variants very well for parts in inventories - mass and cost modifiers are not applied, etc.
// It would be possible but messy and bug-prone to go modify the partsnapshot in the inventory when you swap variants
// To sidestep the whole thing, just disallow changing variants for parts that have cost or mass modifiers while they're in inventory.
static void EditorPartIcon_Create_Postfix(EditorPartIcon __instance, AvailablePart part, bool inInventory)
{
if (!inInventory || part.Variants == null || __instance.btnSwapTexture == null) return;

foreach (var variant in part.Variants)
{
if (variant.cost != 0 || variant.mass != 0)
{
__instance.btnSwapTexture.gameObject.SetActive(false);
return;
}
}
}

// The stock method gets the ModuleInfo strings from the prefab. ModuleCargoPart reports the dry mass and packed volume of the part, and
// swapping variants in the editor parts list will change this so that it doesn't reflect the state of the part that's actually in inventory.
// We don't have a good way to get an updated moduleinfo from the part in inventory (it requires a live part, and it's not stored in the part snapshot)
static void InventoryPartListTooltip_CreateInfoWidgets_Postfix(InventoryPartListTooltip __instance)
{
string moduleTitle = KSPUtil.PrintModuleName(nameof(ModuleCargoPart));

// find the widget corresponding to ModuleCargoPart
foreach (var moduleInfo in __instance.partInfo.moduleInfos)
{
if (moduleInfo.moduleName == moduleTitle)
{
foreach (var widget in __instance.extInfoModules)
{
if (widget.gameObject.activeSelf && widget.textName.text == moduleInfo.moduleDisplayName)
{
widget.textInfo.text = GetModuleCargoPartInfo(__instance.inventoryStoredPart);
return;
}
}
}
}
}

// this is effectively ModuleCargoPart.GetInfo but can operate on a storedPart instead
static string GetModuleCargoPartInfo(StoredPart storedPart)
{
float packedVolume = GetPartSnapshotVolume(storedPart.snapshot);
int stackableQuantity = storedPart.snapshot.moduleCargoStackableQuantity;

string text = "";
text = ((!(packedVolume < 0f)) ? Localizer.Format("#autoLOC_8002220") : Localizer.Format("#autoLOC_6002641"));
text += "\n\n";
text = text + Localizer.Format("#autoLOC_8002186") + " " + storedPart.snapshot.mass.ToString("F3") + " t\\n";
if (packedVolume > 0f)
{
text = text + Localizer.Format("#autoLOC_8004190", Localizer.Format("#autoLOC_8003414"), Localizer.Format("<<1>><<2>>", packedVolume.ToString("0.0"), "L")) + "\n";
}
if (stackableQuantity > 1)
{
text = text + Localizer.Format("#autoLOC_8004190", Localizer.Format("#autoLOC_8003418"), stackableQuantity.ToString("0")) + "\n";
}
return text;
}
}
}
5 changes: 3 additions & 2 deletions KSPCommunityFixes/KSPCommunityFixes.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
<Compile Include="BugFixes\AsteroidInfiniteMining.cs" />
<Compile Include="BugFixes\ChutePhantomSymmetry.cs" />
<Compile Include="BugFixes\CorrectDragForFlags.cs" />
<Compile Include="BugFixes\InventoryPartMass.cs" />
<Compile Include="BugFixes\PropellantFlowDescription.cs" />
<Compile Include="BugFixes\LadderToggleableLight.cs" />
<Compile Include="BugFixes\MapSOCorrectWrapping.cs" />
Expand Down Expand Up @@ -217,13 +218,13 @@
<AVCFilename>KSPCommunityFixes.version</AVCFilename>
</PropertyGroup>
<!--MSBuild targets-->
<Target Name="BeforeBuild" Condition="'$(Configuration)' == 'Release'">
<Target Name="UpdateVersion" BeforeTargets="CoreBuild" Condition="'$(Configuration)' == 'Release'">
<GetAVCVersion Path="$(RepoRootPath)\GameData\$(GameDataFolderName)\$(AVCFilename)">
<Output PropertyName="AVCFullVersion" TaskParameter="FullVersion" />
</GetAVCVersion>
<UpdateAssemblyVersion Path="$(MSBuildProjectDirectory)\Properties\AssemblyInfo.cs" Version="$(AVCFullVersion)" />
</Target>
<Target Name="AfterBuild">
<Target Name="Deploy" AfterTargets="CoreCompile">
<Error Condition="'$(ReferencePath)' == '' OR !Exists('$(ReferencePath)')" Text="ReferencePath=$(ReferencePath) os empty or isn't a valid path" />
<CallTarget Targets="CopyToKSP" />
<CallTarget Targets="CopyBinariesToRepo" Condition="'$(Configuration)' == 'Release' AND '$(CopyReleaseBinariesToRepo)' == 'true'" />
Expand Down
27 changes: 27 additions & 0 deletions KSPCommunityFixes/Library/StaticHelpers.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using System.Reflection;
using HarmonyLib;
using KSP.UI.TooltipTypes;
using UnityEngine;

Expand Down Expand Up @@ -51,5 +53,30 @@ public static TooltipController_Text AddUITooltip(GameObject go)
tooltip.prefab = _tooltipTextPrefab ??= AssetBase.GetPrefab<Tooltip_Text>("Tooltip_Text");
return tooltip;
}

public static bool EditPartModuleKSPFieldAttributes(Type partModuleType, string fieldName, Action<KSPField> editAction)
{
BaseFieldList<BaseField, KSPField>.ReflectedData reflectedData;
try
{
MethodInfo BaseFieldList_GetReflectedAttributes = AccessTools.Method(typeof(BaseFieldList), "GetReflectedAttributes");
reflectedData = (BaseFieldList<BaseField, KSPField>.ReflectedData)BaseFieldList_GetReflectedAttributes.Invoke(null, new object[] { partModuleType, false });
}
catch
{
return false;
}

for (int i = 0; i < reflectedData.fields.Count; i++)
{
if (reflectedData.fields[i].Name == fieldName)
{
editAction.Invoke(reflectedData.fieldAttributes[i]);
return true;
}
}

return false;
}
}
}

0 comments on commit 8ae4846

Please sign in to comment.