Merged to one repo cause platform is limited to 5 repos

This commit is contained in:
IgorEisberg 2023-07-08 23:42:42 +03:00
parent b35f28cd81
commit f5d3431f71
32 changed files with 1952 additions and 0 deletions

View File

@ -0,0 +1,38 @@
using System.Collections.Generic;
using System.Linq;
using Mono.Cecil;
namespace IcyClawz.CustomInteractions
{
public static class Prepatch
{
public static IEnumerable<string> TargetDLLs => new[] { "Assembly-CSharp.dll" };
public static void Patch(ref AssemblyDefinition assembly)
{
var type = assembly.MainModule.GetType("GClass2654"); // DynamicInteraction
if (type != null)
{
type.IsSealed = false;
var field = type.Fields.SingleOrDefault(c => c.Name == "action_0");
if (field != null)
{
field.IsFamily = true;
field.IsInitOnly = false;
}
var ctor = type.Methods.SingleOrDefault(c => c.Name == ".ctor");
if (ctor != null)
{
var param = ctor.Parameters.SingleOrDefault(c => c.Name == "callback");
if (param != null)
{
param.IsOptional = true;
param.HasDefault = true;
param.Constant = null;
}
}
}
//assembly.Write("Assembly-CSharp-CustomInteractions.dll");
}
}
}

View File

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<AssemblyName>IcyClawz.CustomInteractions.Prepatch</AssemblyName>
<Version>1.1.1</Version>
<RootNamespace>IcyClawz.CustomInteractions</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Reference Include="Mono.Cecil">
<HintPath>..\Shared\Mono.Cecil.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

30
CustomInteractions.sln Normal file
View File

@ -0,0 +1,30 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.2.32616.157
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomInteractions", "CustomInteractions\CustomInteractions.csproj", "{F296FF55-A8D8-47E3-90C0-6975CA0787F9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomInteractions.Prepatch", "CustomInteractions.Prepatch\Prepatch.csproj", "{E8C5359B-95CE-4547-B72E-C9BEBB9220CC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{F296FF55-A8D8-47E3-90C0-6975CA0787F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F296FF55-A8D8-47E3-90C0-6975CA0787F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F296FF55-A8D8-47E3-90C0-6975CA0787F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F296FF55-A8D8-47E3-90C0-6975CA0787F9}.Release|Any CPU.Build.0 = Release|Any CPU
{E8C5359B-95CE-4547-B72E-C9BEBB9220CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E8C5359B-95CE-4547-B72E-C9BEBB9220CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E8C5359B-95CE-4547-B72E-C9BEBB9220CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E8C5359B-95CE-4547-B72E-C9BEBB9220CC}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BF50C8C8-82A6-496A-B4A4-DB383A88705D}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,206 @@
using EFT.InventoryLogic;
using EFT.UI;
using JetBrains.Annotations;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using UnityEngine;
using DynamicInteraction = GClass2654;
using EmptyInteractions = GClass2655<System.Enum>;
namespace IcyClawz.CustomInteractions
{
[EditorBrowsable(EditorBrowsableState.Never)]
public interface ICustomInteractionsProvider { } // Do not implement this directly
public interface IItemCustomInteractionsProvider : ICustomInteractionsProvider
{
IEnumerable<CustomInteraction> GetCustomInteractions(ItemUiContext uiContext, EItemViewType viewType, Item item);
}
public static class CustomInteractionsManager
{
internal static readonly List<ICustomInteractionsProvider> Providers = new List<ICustomInteractionsProvider>();
public static void Register(ICustomInteractionsProvider provider)
{
if (!Providers.Contains(provider))
{
Providers.Add(provider);
}
}
}
public class CustomInteraction
{
internal readonly CustomInteractionImpl Impl;
public CustomInteraction()
{
Impl = new CustomInteractionImpl();
}
public Func<string> Caption { get => Impl.Caption; set => Impl.Caption = value; }
public Func<Sprite> Icon { get => Impl.Icon; set => Impl.Icon = value; }
public Action Action { get => Impl.Action; set => Impl.Action = value; }
public Func<CustomSubInteractions> SubMenu { get => Impl.SubMenu; set => Impl.SubMenu = value; }
public Func<bool> Enabled { get => Impl.Enabled; set => Impl.Enabled = value; }
public Func<string> Error { get => Impl.Error; set => Impl.Error = value; }
}
internal sealed class CustomInteractionImpl : DynamicInteraction
{
public CustomInteractionImpl()
: base(UnityEngine.Random.Range(0, int.MaxValue).ToString("x4")) { }
public Func<string> Caption { get; set; }
public new Func<Sprite> Icon { get; set; }
public Action Action { get => action_0; set => action_0 = value; }
public Func<CustomSubInteractions> SubMenu { get; set; }
public Func<bool> Enabled { get; set; }
public Func<string> Error { get; set; }
public bool IsInteractive() =>
Enabled?.Invoke() ?? true;
}
public abstract class CustomSubInteractions
{
internal readonly CustomSubInteractionsImpl Impl;
public CustomSubInteractions(ItemUiContext uiContext)
{
Impl = new CustomSubInteractionsImpl(uiContext);
}
public bool ExaminationRequired { get => Impl._ExaminationRequired; set => Impl._ExaminationRequired = value; }
public void Add(CustomInteraction customInteraction) =>
Impl.AddCustomInteraction(customInteraction);
public void Remove(CustomInteraction customInteraction) =>
Impl.RemoveCustomInteraction(customInteraction);
public void CallRedraw() =>
Impl.CallRedraw();
public void CallRedraw(string templateId) =>
Impl.CallRedraw(templateId);
}
internal sealed class CustomSubInteractionsImpl : EmptyInteractions
{
public CustomSubInteractionsImpl(ItemUiContext uiContext)
: base(uiContext) { }
public IEnumerable<CustomInteractionImpl> CustomInteractions => DynamicInteractions.OfType<CustomInteractionImpl>();
public bool _ExaminationRequired { get; set; } = true;
public override bool ExaminationRequired => _ExaminationRequired;
public override bool HasIcons => CustomInteractions.Any(customInteraction => customInteraction.Icon != null);
public void CallRedraw() =>
itemUiContext_0.RedrawContextMenus(null);
}
internal static class AbstractInteractionsExtensions
{
private static Dictionary<string, DynamicInteraction> GetDynamicInteractions<T>(this GClass2655<T> instance) where T : Enum =>
typeof(GClass2655<T>).GetField("dictionary_1", BindingFlags.NonPublic | BindingFlags.Instance)
.GetValue(instance) as Dictionary<string, DynamicInteraction>;
public static void AddCustomInteraction<T>(this GClass2655<T> instance, CustomInteraction customInteraction) where T : Enum =>
instance.GetDynamicInteractions()[customInteraction.Impl.Key] = customInteraction.Impl;
public static void RemoveCustomInteraction<T>(this GClass2655<T> instance, CustomInteraction customInteraction) where T : Enum =>
instance.GetDynamicInteractions().Remove(customInteraction.Impl.Key);
}
internal static class InteractionButtonsContainerExtensions
{
private static readonly FieldInfo ButtonsContainerField =
typeof(InteractionButtonsContainer).GetField("_buttonsContainer", BindingFlags.NonPublic | BindingFlags.Instance);
private static RectTransform GetButtonsContainer(this InteractionButtonsContainer instance) =>
ButtonsContainerField.GetValue(instance) as RectTransform;
private static readonly FieldInfo ButtonTemplateField =
typeof(InteractionButtonsContainer).GetField("_buttonTemplate", BindingFlags.NonPublic | BindingFlags.Instance);
private static SimpleContextMenuButton GetButtonTemplate(this InteractionButtonsContainer instance) =>
ButtonTemplateField.GetValue(instance) as SimpleContextMenuButton;
private static readonly FieldInfo CurrentButtonField =
typeof(InteractionButtonsContainer).GetField("simpleContextMenuButton_0", BindingFlags.NonPublic | BindingFlags.Instance);
private static void SetCurrentButton(this InteractionButtonsContainer instance, SimpleContextMenuButton button) =>
CurrentButtonField.SetValue(instance, button);
private static readonly MethodInfo CreateButtonMethod =
typeof(InteractionButtonsContainer).GetMethod("method_1", BindingFlags.NonPublic | BindingFlags.Instance);
private static SimpleContextMenuButton CreateButton(this InteractionButtonsContainer instance,
string key, string caption, SimpleContextMenuButton template, RectTransform container,
[CanBeNull] Sprite sprite, [CanBeNull] Action onButtonClicked, [CanBeNull] Action onMouseHover,
bool subMenu = false, bool autoClose = true) =>
(SimpleContextMenuButton)CreateButtonMethod.Invoke(instance, new object[] {
key, caption, template, container, sprite, onButtonClicked, onMouseHover, subMenu, autoClose
});
private static readonly MethodInfo CloseSubMenuMethod =
typeof(InteractionButtonsContainer).GetMethod("method_4", BindingFlags.NonPublic | BindingFlags.Instance);
private static void CloseSubMenu(this InteractionButtonsContainer instance) =>
CloseSubMenuMethod.Invoke(instance, null);
private static readonly MethodInfo AddButtonMethod =
typeof(InteractionButtonsContainer).GetMethod("method_5", BindingFlags.NonPublic | BindingFlags.Instance);
private static void AddButton(this InteractionButtonsContainer instance, SimpleContextMenuButton button) =>
AddButtonMethod.Invoke(instance, new object[] { button });
public static void AddCustomButton(this InteractionButtonsContainer instance, CustomInteractionImpl customInteractionImpl)
{
bool isInteractive = customInteractionImpl.IsInteractive();
SimpleContextMenuButton button = null;
button = instance.CreateButton(
customInteractionImpl.Key,
customInteractionImpl.Caption?.Invoke() ?? string.Empty,
instance.GetButtonTemplate(),
instance.GetButtonsContainer(),
customInteractionImpl.Icon?.Invoke(),
() =>
{
if (isInteractive)
{
customInteractionImpl.Execute();
}
},
() =>
{
instance.SetCurrentButton(button);
instance.CloseSubMenu();
if (isInteractive)
{
var subMenu = customInteractionImpl.SubMenu?.Invoke();
if (subMenu != null)
{
instance.SetSubInteractions(subMenu.Impl);
}
}
},
customInteractionImpl.SubMenu != null,
false
);
button.SetButtonInteraction(
(isInteractive, customInteractionImpl.Error?.Invoke() ?? string.Empty)
);
instance.AddButton(button);
}
}
}

View File

@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<AssemblyName>IcyClawz.CustomInteractions</AssemblyName>
<Version>1.1.1</Version>
<RootNamespace>IcyClawz.CustomInteractions</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Reference Include="Aki.Reflection">
<HintPath>..\Shared\Aki.Reflection.dll</HintPath>
</Reference>
<Reference Include="Assembly-CSharp">
<HintPath>..\Shared\Assembly-CSharp-CustomInteractions.dll</HintPath>
</Reference>
<Reference Include="BepInEx">
<HintPath>..\Shared\BepInEx.dll</HintPath>
</Reference>
<Reference Include="Sirenix.Serialization">
<HintPath>..\Shared\Sirenix.Serialization.dll</HintPath>
</Reference>
<Reference Include="UnityEngine">
<HintPath>..\Shared\UnityEngine.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.CoreModule">
<HintPath>..\Shared\UnityEngine.CoreModule.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@ -0,0 +1,61 @@
using Aki.Reflection.Patching;
using BepInEx;
using EFT.UI;
using System.Linq;
using System.Reflection;
using DynamicInteraction = GClass2654;
using ItemContext = GClass2466;
using ItemInfoInteractions = GClass2655<EFT.InventoryLogic.EItemInfoButton>;
namespace IcyClawz.CustomInteractions
{
[BepInPlugin("com.IcyClawz.CustomInteractions", "IcyClawz.CustomInteractions", "1.1.1")]
public class Plugin : BaseUnityPlugin
{
private void Awake()
{
new ItemUiContextPatch().Enable();
new InteractionButtonsContainerPatch().Enable();
}
}
public class ItemUiContextPatch : ModulePatch
{
protected override MethodBase GetTargetMethod() =>
typeof(ItemUiContext).GetMethod("GetItemContextInteractions", BindingFlags.Public | BindingFlags.Instance);
[PatchPostfix]
private static void Postfix(ref ItemInfoInteractions __result, ItemUiContext __instance, ItemContext itemContext)
{
foreach (var provider in CustomInteractionsManager.Providers.OfType<IItemCustomInteractionsProvider>())
{
var customInteractions = provider.GetCustomInteractions(__instance, itemContext.ViewType, itemContext.Item);
if (customInteractions != null)
{
foreach (CustomInteraction customInteraction in customInteractions)
{
__result.AddCustomInteraction(customInteraction);
}
}
}
}
}
public class InteractionButtonsContainerPatch : ModulePatch
{
protected override MethodBase GetTargetMethod() =>
typeof(InteractionButtonsContainer).GetMethod("method_3", BindingFlags.NonPublic | BindingFlags.Instance);
[PatchPrefix]
private static bool Prefix(ref InteractionButtonsContainer __instance, DynamicInteraction interaction)
{
if (interaction is CustomInteractionImpl customInteractionImpl)
{
__instance.AddCustomButton(customInteractionImpl);
return false;
}
return true;
}
}
}

25
ItemAttributeFix.sln Normal file
View File

@ -0,0 +1,25 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.2.32616.157
MinimumVisualStudioVersion = 10.0.40219.1
Project("{1A806857-2204-4EF9-ADDA-36F4C5688DC1}") = "ItemAttributeFix", "ItemAttributeFix\ItemAttributeFix.csproj", "{F3186B2D-F170-4E31-98BF-4CC8CDAD565D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{F3186B2D-F170-4E31-98BF-4CC8CDAD565D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F3186B2D-F170-4E31-98BF-4CC8CDAD565D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F3186B2D-F170-4E31-98BF-4CC8CDAD565D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F3186B2D-F170-4E31-98BF-4CC8CDAD565D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BD4846A6-29F2-4AD6-91A5-7A0CA28D88CC}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<AssemblyName>IcyClawz.ItemAttributeFix</AssemblyName>
<Version>1.0.0</Version>
<RootNamespace>IcyClawz.ItemAttributeFix</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Reference Include="Aki.Reflection">
<HintPath>..\Shared\Aki.Reflection.dll</HintPath>
</Reference>
<Reference Include="Assembly-CSharp">
<HintPath>..\Shared\Assembly-CSharp.dll</HintPath>
</Reference>
<Reference Include="BepInEx">
<HintPath>..\Shared\BepInEx.dll</HintPath>
</Reference>
<Reference Include="UnityEngine">
<HintPath>..\Shared\UnityEngine.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.CoreModule">
<HintPath>..\Shared\UnityEngine.CoreModule.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@ -0,0 +1,35 @@
using Aki.Reflection.Patching;
using BepInEx;
using EFT.UI;
using System.Reflection;
namespace IcyClawz.ItemAttributeFix
{
[BepInPlugin("com.IcyClawz.ItemAttributeFix", "IcyClawz.ItemAttributeFix", "1.0.0")]
public class Plugin : BaseUnityPlugin
{
private void Awake()
{
new CompactCharacteristicPanelPatch().Enable();
}
}
public class CompactCharacteristicPanelPatch : ModulePatch
{
private static readonly FieldInfo ItemAttributeField =
typeof(CompactCharacteristicPanel).GetField("ItemAttribute", BindingFlags.NonPublic | BindingFlags.Instance);
private static readonly FieldInfo StringField =
typeof(CompactCharacteristicPanel).GetField("string_0", BindingFlags.NonPublic | BindingFlags.Instance);
protected override MethodBase GetTargetMethod() =>
typeof(CompactCharacteristicPanel).GetMethod("SetValues", BindingFlags.Public | BindingFlags.Instance);
[PatchPostfix]
private static void PatchPostfix(ref CompactCharacteristicPanel __instance)
{
ItemAttributeClass attribute = ItemAttributeField.GetValue(__instance) as ItemAttributeClass;
StringField.SetValue(__instance, attribute.FullStringValue());
}
}
}

25
ItemContextMenuExt.sln Normal file
View File

@ -0,0 +1,25 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.2.32616.157
MinimumVisualStudioVersion = 10.0.40219.1
Project("{16C72132-089A-4D00-BDA9-5DFC76E223AC}") = "ItemContextMenuExt", "ItemContextMenuExt\ItemContextMenuExt.csproj", "{F9FF8A66-A96D-45D2-BBD5-D976F101CCF7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{F9FF8A66-A96D-45D2-BBD5-D976F101CCF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F9FF8A66-A96D-45D2-BBD5-D976F101CCF7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F9FF8A66-A96D-45D2-BBD5-D976F101CCF7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F9FF8A66-A96D-45D2-BBD5-D976F101CCF7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B44F10FA-AB5A-4200-80B2-4ED636043F28}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,185 @@
using Comfort.Common;
using EFT;
using EFT.InventoryLogic;
using EFT.UI;
using IcyClawz.CustomInteractions;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
using ILightTemplate = GInterface240;
using LightsState = GStruct143;
using ResourceCache = GClass2014;
namespace IcyClawz.ItemContextMenuExt
{
internal static class PlayerExtensions
{
private static readonly FieldInfo InventoryControllerField =
typeof(Player).GetField("_inventoryController", BindingFlags.NonPublic | BindingFlags.Instance);
public static InventoryControllerClass GetInventoryController(this Player player) =>
InventoryControllerField.GetValue(player) as InventoryControllerClass;
}
internal static class LightComponentExtensions
{
public static int GetModesCount(this LightComponent component) =>
((ILightTemplate)component.Item.Template).ModesCount;
}
public sealed class CustomInteractionsProvider : IItemCustomInteractionsProvider
{
internal const string IconsPrefix = "Characteristics/Icons/";
internal static StaticIcons StaticIcons => EFTHardSettings.Instance.StaticIcons;
public IEnumerable<CustomInteraction> GetCustomInteractions(ItemUiContext uiContext, EItemViewType viewType, Item item)
{
if (viewType != EItemViewType.Inventory)
{
yield break;
}
{
var component = item.GetItemComponent<FireModeComponent>();
if (component != null)
{
// Firing mode
yield return new CustomInteraction()
{
Caption = () => "Firing mode",
Icon = () => StaticIcons.GetAttributeIcon(EItemAttributeId.Weapon),
SubMenu = () => new FireModeSubMenu(uiContext, component),
Enabled = () => component.AvailableEFireModes.Length > 1,
Error = () => "This weapon is incapable of selective fire"
};
yield break;
}
}
{
var component = item.GetItemComponent<LightComponent>();
if (component != null)
{
// Turn on/off
yield return new CustomInteraction()
{
Caption = () => (component.IsActive ? "TurnOff" : "TurnOn").Localized(),
Icon = () => ResourceCache.Pop<Sprite>(IconsPrefix + (component.IsActive ? "TurnOff" : "TurnOn")),
Action = () =>
{
Singleton<GUISounds>.Instance.PlayUISound(EUISoundType.MenuContextMenu);
ComponentUtils.SetLightState(component, !component.IsActive, component.SelectedMode);
uiContext.RedrawContextMenus(new[] { item.TemplateId });
}
};
// Switch mode
yield return new CustomInteraction()
{
Caption = () => "Switch mode",
Icon = () => StaticIcons.GetAttributeIcon(EItemAttributeId.EncodeState),
SubMenu = () => new LightModeSubMenu(uiContext, component),
Enabled = () => component.GetModesCount() > 1,
Error = () => "This device has no alternative modes"
};
yield break;
}
}
}
}
internal class FireModeSubMenu : CustomSubInteractions
{
public FireModeSubMenu(ItemUiContext uiContext, FireModeComponent component)
: base(uiContext)
{
foreach (var fireMode in component.AvailableEFireModes)
{
Add(new CustomInteraction()
{
Caption = () => fireMode.ToString().Localized(),
Action = () =>
{
Singleton<GUISounds>.Instance.PlayUISound(EUISoundType.MenuContextMenu);
ComponentUtils.SetFireMode(component, fireMode);
},
Enabled = () => fireMode != component.FireMode
});
}
}
}
internal class LightModeSubMenu : CustomSubInteractions
{
public LightModeSubMenu(ItemUiContext uiContext, LightComponent component)
: base(uiContext)
{
foreach (var lightMode in Enumerable.Range(0, component.GetModesCount()))
{
Add(new CustomInteraction()
{
Caption = () => $"Mode {lightMode + 1}",
Action = () =>
{
Singleton<GUISounds>.Instance.PlayUISound(EUISoundType.MenuContextMenu);
ComponentUtils.SetLightState(component, component.IsActive, lightMode);
},
Enabled = () => lightMode != component.SelectedMode
});
}
}
}
internal static class ComponentUtils
{
public static void SetFireMode(FireModeComponent component, Weapon.EFireMode fireMode)
{
var player = GamePlayerOwner.MyPlayer;
if (player != null && player.HandsController is Player.FirearmController fc && component.Item == fc.Item)
{
if (fc.Item.MalfState.State == Weapon.EMalfunctionState.None)
{
fc.ChangeFireMode(fireMode);
}
else
{
fc.FirearmsAnimator.MisfireSlideUnknown(false);
player.GetInventoryController().ExamineMalfunction(fc.Item, false);
}
return;
}
component.SetFireMode(fireMode);
}
public static void SetLightState(LightComponent component, bool isActive, int lightMode)
{
var player = GamePlayerOwner.MyPlayer;
if (player != null && player.HandsController is Player.FirearmController fc && component.Item.IsChildOf(fc.Item))
{
var state = new LightsState
{
Id = component.Item.Id,
IsActive = isActive,
LightMode = lightMode
};
fc.SetLightsState(new[] { state });
return;
}
component.IsActive = isActive;
component.SelectedMode = lightMode;
if (player != null)
{
foreach (var tcvc in player.GetComponentsInChildren<TacticalComboVisualController>())
{
if (ReferenceEquals(tcvc.LightMod, component))
{
tcvc.UpdateBeams();
break;
}
}
}
}
}
}

View File

@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<AssemblyName>IcyClawz.ItemContextMenuExt</AssemblyName>
<Version>1.0.1</Version>
<RootNamespace>IcyClawz.ItemContextMenuExt</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Reference Include="Assembly-CSharp">
<HintPath>..\Shared\Assembly-CSharp.dll</HintPath>
</Reference>
<Reference Include="BepInEx">
<HintPath>..\Shared\BepInEx.dll</HintPath>
</Reference>
<Reference Include="Comfort">
<HintPath>..\Shared\Comfort.dll</HintPath>
</Reference>
<Reference Include="IcyClawz.CustomInteractions">
<HintPath>..\Shared\IcyClawz.CustomInteractions.dll</HintPath>
</Reference>
<Reference Include="ItemComponent.Types">
<HintPath>..\Shared\ItemComponent.Types.dll</HintPath>
</Reference>
<Reference Include="ItemTemplate.Types">
<HintPath>..\Shared\ItemTemplate.Types.dll</HintPath>
</Reference>
<Reference Include="UnityEngine">
<HintPath>..\Shared\UnityEngine.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.CoreModule">
<HintPath>..\Shared\UnityEngine.CoreModule.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@ -0,0 +1,14 @@
using BepInEx;
using IcyClawz.CustomInteractions;
namespace IcyClawz.ItemContextMenuExt
{
[BepInPlugin("com.IcyClawz.ItemContextMenuExt", "IcyClawz.ItemContextMenuExt", "1.0.1")]
public class Plugin : BaseUnityPlugin
{
private void Awake()
{
CustomInteractionsManager.Register(new CustomInteractionsProvider());
}
}
}

25
ItemSellPrice.sln Normal file
View File

@ -0,0 +1,25 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.2.32616.157
MinimumVisualStudioVersion = 10.0.40219.1
Project("{1A9E4EC8-A583-446A-8918-2608C278F194}") = "ItemSellPrice", "ItemSellPrice\ItemSellPrice.csproj", "{FA2EAB19-7A7F-452F-80B3-DEAD18E43D88}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{FA2EAB19-7A7F-452F-80B3-DEAD18E43D88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FA2EAB19-7A7F-452F-80B3-DEAD18E43D88}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FA2EAB19-7A7F-452F-80B3-DEAD18E43D88}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FA2EAB19-7A7F-452F-80B3-DEAD18E43D88}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B6B3C802-40C3-4FF0-9BAA-1DAC503CA619}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,217 @@
using Aki.Reflection.Utils;
using Comfort.Common;
using EFT;
using EFT.InventoryLogic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
using CurrencyUtil = GClass2181;
namespace IcyClawz.ItemSellPrice
{
public static class ItemSellPrice
{
private static readonly Dictionary<string, string[]> DisplayNames = new Dictionary<string, string[]>()
{
{ "ch", new[] { "售价({0}", "无法出售给商人" } },
{ "cz", new[] { "Prodejní cena ({0})", "Nemůže být prodán obchodníkům" } },
{ "en", new[] { "Selling price ({0})", "Cannot be sold to traders" } },
{ "es", new[] { "Precio de venta ({0})", "No puede ser vendido a los vendedores" } },
{ "es-mx", new[] { "Precio de venta ({0})", "No puede ser vendido a comerciantes" } },
{ "fr", new[] { "Prix de vente ({0})", "Ne peut pas être vendu aux marchands" } },
{ "ge", new[] { "Verkaufspreis ({0})", "Kann nicht an Händler verkauft werden" } },
{ "hu", new[] { "Eladási ár ({0})", "Kereskedőknek nem adható el" } },
{ "it", new[] { "Prezzo di vendita ({0})", "Non vendibile ai mercanti" } },
{ "jp", new[] { "販売価格({0}", "トレーダーには販売できません" } },
{ "kr", new[] { "판매 가격 ({0})", "상인에게 판매 불가" } },
{ "pl", new[] { "Cena sprzedaży ({0})", "Nie można sprzedawać handlarzom" } },
{ "po", new[] { "Preço de venda ({0})", "Não pode ser vendido a comerciantes" } },
{ "ru", new[] { "Цена продажи ({0})", "Невозможно продать торговцам" } },
{ "sk", new[] { "Predajná cena ({0})", "Nedá sa predať obchodníkom" } },
{ "tu", new[] { "Satış fiyatı ({0})", "Tüccarlara satılamaz" } }
};
private static readonly FieldInfo SupplyDataField =
typeof(TraderClass).GetField("supplyData_0", BindingFlags.NonPublic | BindingFlags.Instance);
private static ISession Session => ClientAppUtils.GetMainApp().GetClientBackEndSession();
public static async void UpdateSupplyData(this TraderClass trader)
{
Result<SupplyData> result = await Session.GetSupplyData(trader.Id);
if (result.Failed)
{
Debug.LogError("Failed to download supply data");
return;
}
trader.SetSupplyData(result.Value);
}
public static SupplyData GetSupplyData(this TraderClass trader) =>
SupplyDataField.GetValue(trader) as SupplyData;
public static void SetSupplyData(this TraderClass trader, SupplyData supplyData) =>
SupplyDataField.SetValue(trader, supplyData);
public static void AddTraderOfferAttribute(this Item item)
{
List<ItemAttributeClass> attributes = new List<ItemAttributeClass>
{
new ItemAttributeClass(EItemAttributeId.MoneySum)
{
Name = EItemAttributeId.MoneySum.GetName(),
DisplayNameFunc = () => GetDisplayName(item),
Base = () => GetBase(item),
StringValue = () => GetStringValue(item),
FullStringValue = () => GetFullStringValue(item),
DisplayType = () => EItemAttributeDisplayType.Compact
}
};
attributes.AddRange(item.Attributes);
item.Attributes = attributes;
}
private sealed class TraderOffer
{
public string Name;
public int Price;
public string Currency;
public double Course;
public int Count;
public TraderOffer(string name, int price, string currency, double course, int count)
{
Name = name;
Price = price;
Currency = currency;
Course = course;
Count = count;
}
}
private static TraderOffer GetTraderOffer(Item item, TraderClass trader)
{
var result = trader.GetUserItemPrice(item);
if (result == null)
{
return null;
}
return new TraderOffer(
trader.LocalizedName,
result.Value.Amount,
CurrencyUtil.GetCurrencyCharById(result.Value.CurrencyId),
trader.GetSupplyData().CurrencyCourses[result.Value.CurrencyId],
item.StackObjectsCount
);
}
private static List<TraderOffer> GetAllTraderOffers(Item item)
{
if (!Session.Profile.Examined(item))
{
return null;
}
switch (item.Owner?.OwnerType)
{
case EOwnerType.RagFair:
case EOwnerType.Trader:
if (item.StackObjectsCount > 1 || item.UnlimitedCount)
{
item = item.CloneItem();
item.StackObjectsCount = 1;
item.UnlimitedCount = false;
}
break;
}
List<TraderOffer> offers = new List<TraderOffer>();
foreach (TraderClass trader in Session.Traders)
{
if (GetTraderOffer(item, trader) is TraderOffer offer)
{
offers.Add(offer);
}
}
offers.Sort((x, y) => (y.Price * y.Course).CompareTo(x.Price * x.Course));
return offers;
}
private static TraderOffer GetBestTraderOffer(Item item)
{
if (GetAllTraderOffers(item) is List<TraderOffer> offers)
{
return offers.FirstOrDefault();
}
else
{
return null;
}
}
public static string GetDisplayName(Item item)
{
string language = Singleton<SharedGameSettingsClass>.Instance?.Game?.Settings?.Language?.GetValue();
if (language == null || !DisplayNames.ContainsKey(language))
{
language = "en";
}
if (GetBestTraderOffer(item) is TraderOffer offer)
{
return string.Format(DisplayNames[language][0], offer.Name);
}
else
{
return DisplayNames[language][1];
}
}
public static float GetBase(Item item)
{
if (GetBestTraderOffer(item) is TraderOffer offer)
{
return offer.Price;
}
else
{
return 0.01f;
}
}
public static string GetStringValue(Item item)
{
if (GetBestTraderOffer(item) is TraderOffer offer)
{
string value = $"{offer.Currency} {offer.Price}";
if (offer.Count > 1)
{
value += $" ({offer.Count})";
}
return value;
}
else
{
return string.Empty;
}
}
public static string GetFullStringValue(Item item)
{
if (GetAllTraderOffers(item) is List<TraderOffer> offers)
{
string[] lines = new string[offers.Count];
for (int i = 0; i < offers.Count; i++)
{
TraderOffer offer = offers[i];
lines[i] = $"{offer.Name}: {offer.Currency} {offer.Price}";
}
return string.Join(Environment.NewLine, lines);
}
else
{
return string.Empty;
}
}
}
}

View File

@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<AssemblyName>IcyClawz.ItemSellPrice</AssemblyName>
<Version>1.0.2</Version>
<RootNamespace>IcyClawz.ItemSellPrice</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Reference Include="Aki.Common">
<HintPath>..\Shared\Aki.Common.dll</HintPath>
</Reference>
<Reference Include="Aki.Reflection">
<HintPath>..\Shared\Aki.Reflection.dll</HintPath>
</Reference>
<Reference Include="Assembly-CSharp">
<HintPath>..\Shared\Assembly-CSharp.dll</HintPath>
</Reference>
<Reference Include="BepInEx">
<HintPath>..\Shared\BepInEx.dll</HintPath>
</Reference>
<Reference Include="Comfort">
<HintPath>..\Shared\Comfort.dll</HintPath>
</Reference>
<Reference Include="UnityEngine">
<HintPath>..\Shared\UnityEngine.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.CoreModule">
<HintPath>..\Shared\UnityEngine.CoreModule.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

82
ItemSellPrice/Plugin.cs Normal file
View File

@ -0,0 +1,82 @@
using Aki.Reflection.Patching;
using BepInEx;
using EFT.InventoryLogic;
using System.Reflection;
namespace IcyClawz.ItemSellPrice
{
[BepInPlugin("com.IcyClawz.ItemSellPrice", "IcyClawz.ItemSellPrice", "1.0.2")]
public class Plugin : BaseUnityPlugin
{
private void Awake()
{
new TraderPatch().Enable();
new ItemPatch().Enable();
new AmmoPatch().Enable();
new GrenadePatch().Enable();
new SecureContainerPatch().Enable();
}
}
public class TraderPatch : ModulePatch
{
protected override MethodBase GetTargetMethod() =>
typeof(TraderClass).GetConstructors()[0];
[PatchPostfix]
private static void PatchPostfix(ref TraderClass __instance)
{
__instance.UpdateSupplyData();
}
}
public class ItemPatch : ModulePatch
{
protected override MethodBase GetTargetMethod() =>
typeof(Item).GetConstructors()[0];
[PatchPostfix]
private static void PatchPostfix(ref Item __instance)
{
__instance.AddTraderOfferAttribute();
}
}
public class AmmoPatch : ModulePatch
{
protected override MethodBase GetTargetMethod() =>
typeof(BulletClass).GetConstructors()[0];
[PatchPostfix]
private static void PatchPostfix(ref BulletClass __instance)
{
__instance.AddTraderOfferAttribute();
}
}
public class GrenadePatch : ModulePatch
{
protected override MethodBase GetTargetMethod() =>
typeof(GrenadeClass).GetConstructors()[0];
[PatchPostfix]
private static void PatchPostfix(ref GrenadeClass __instance)
{
__instance.AddTraderOfferAttribute();
}
}
public class SecureContainerPatch : ModulePatch
{
protected override MethodBase GetTargetMethod()
{
return typeof(ItemContainerClass).GetConstructors()[0];
}
[PatchPostfix]
private static void PatchPostfix(ref ItemContainerClass __instance)
{
__instance.AddTraderOfferAttribute();
}
}
}

25
MagazineInspector.sln Normal file
View File

@ -0,0 +1,25 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.2.32616.157
MinimumVisualStudioVersion = 10.0.40219.1
Project("{1528224F-F248-466A-95DF-C841EA46D89E}") = "MagazineInspector", "MagazineInspector\MagazineInspector.csproj", "{F4F0DAB6-3A97-4656-A61F-945D63BAB4F8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{F4F0DAB6-3A97-4656-A61F-945D63BAB4F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F4F0DAB6-3A97-4656-A61F-945D63BAB4F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F4F0DAB6-3A97-4656-A61F-945D63BAB4F8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F4F0DAB6-3A97-4656-A61F-945D63BAB4F8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BEB55B48-4A92-4A7E-8B4E-58FFADC9FE2C}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,157 @@
using Aki.Reflection.Utils;
using Comfort.Common;
using EFT;
using EFT.InventoryLogic;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using InGameStatus = GClass1756;
namespace IcyClawz.MagazineInspector
{
public static class MagazineInspector
{
private static readonly Dictionary<string, string> DisplayNames = new Dictionary<string, string>()
{
{ "ch", "上膛弹药" },
{ "cz", "Nabité střelivo" },
{ "en", "Loaded ammo" },
{ "es", "Munición cargada" },
{ "es-mx", "Munición cargada" },
{ "fr", "Munitions chargées" },
{ "ge", "Geladene Munition" },
{ "hu", "Töltött lőszer" },
{ "it", "Munizioni caricate" },
{ "jp", "装填弾薬" },
{ "kr", "장전 탄약" },
{ "pl", "Załadowana amunicja" },
{ "po", "Munição carregada" },
{ "ru", "Заряженные боеприпасы" },
{ "sk", "Nabitá munícia" },
{ "tu", "Yüklü mühimmat" }
};
private static ISession Session => ClientAppUtils.GetMainApp().GetClientBackEndSession();
private static Profile ActiveProfile => InGameStatus.InRaid ? ClientPlayerOwner.MyPlayer.Profile : Session.Profile;
public static void AddAmmoCountAttribute(this MagazineClass magazine)
{
ItemAttributeClass attribute = magazine.Attributes.Find(attr => attr.Id is EItemAttributeId.MaxCount);
if (attribute == null)
{
return;
}
attribute.DisplayNameFunc = GetDisplayName;
attribute.Base = () => GetBase(magazine);
attribute.StringValue = () => GetStringValue(magazine);
attribute.FullStringValue = () => GetFullStringValue(magazine);
}
private static int? GetAmmoCount(MagazineClass magazine, Profile profile, out bool magChecked)
{
if (!InGameStatus.InRaid || magazine.Count == 0)
{
magChecked = true;
return magazine.Count;
}
magChecked = profile.CheckedMagazines.ContainsKey(magazine.Id);
if (magChecked)
{
bool equipped = profile.Inventory.Equipment.GetAllSlots().Any(slot => ReferenceEquals(slot.ContainedItem, magazine));
if (magazine.Count >= (equipped ? magazine.MaxCount - 1 : magazine.MaxCount))
{
return magazine.Count;
}
int skill = Mathf.Max(
profile.MagDrillsMastering,
profile.CheckedMagazineSkillLevel(magazine.Id),
magazine.CheckOverride
);
if (skill > 1 || (skill == 1 && magazine.MaxCount <= 10))
{
return magazine.Count;
}
}
return null;
}
public static string GetDisplayName()
{
string language = Singleton<SharedGameSettingsClass>.Instance?.Game?.Settings?.Language?.GetValue();
if (language == null || !DisplayNames.ContainsKey(language))
{
language = "en";
}
return DisplayNames[language];
}
public static float GetBase(MagazineClass magazine)
{
if (GetAmmoCount(magazine, ActiveProfile, out _) is int ammoCount)
{
return ammoCount;
}
else
{
return 0f;
}
}
public static string GetStringValue(MagazineClass magazine)
{
string value;
if (GetAmmoCount(magazine, ActiveProfile, out _) is int ammoCount)
{
value = ammoCount.ToString();
}
else
{
value = "?";
}
return $"{value}/{magazine.MaxCount}";
}
public static string GetFullStringValue(MagazineClass magazine)
{
Profile profile = ActiveProfile;
int? ammoCount = GetAmmoCount(magazine, profile, out bool magChecked);
if (magChecked)
{
List<Item> cartridges = new List<Item>(magazine.Cartridges.Items);
string[] lines = new string[cartridges.Count];
int i = cartridges.Count - 1;
foreach (Item cartridge in cartridges)
{
string count;
if (ammoCount != null)
{
count = cartridge.StackObjectsCount.ToString();
}
else
{
count = "?";
}
string name;
if (profile.Examined(cartridge))
{
name = cartridge.LocalizedName();
}
else
{
name = "Unknown item".Localized();
}
lines[i--] = $"{count} × {name}";
}
return string.Join(Environment.NewLine, lines);
}
else
{
return string.Empty;
}
}
}
}

View File

@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<AssemblyName>IcyClawz.MagazineInspector</AssemblyName>
<Version>1.0.1</Version>
<RootNamespace>IcyClawz.MagazineInspector</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Reference Include="Aki.Common">
<HintPath>..\Shared\Aki.Common.dll</HintPath>
</Reference>
<Reference Include="Aki.Reflection">
<HintPath>..\Shared\Aki.Reflection.dll</HintPath>
</Reference>
<Reference Include="Assembly-CSharp">
<HintPath>..\Shared\Assembly-CSharp.dll</HintPath>
</Reference>
<Reference Include="BepInEx">
<HintPath>..\Shared\BepInEx.dll</HintPath>
</Reference>
<Reference Include="Comfort">
<HintPath>..\Shared\Comfort.dll</HintPath>
</Reference>
<Reference Include="UnityEngine">
<HintPath>..\Shared\UnityEngine.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.CoreModule">
<HintPath>..\Shared\UnityEngine.CoreModule.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@ -0,0 +1,27 @@
using Aki.Reflection.Patching;
using BepInEx;
using System.Reflection;
namespace IcyClawz.MagazineInspector
{
[BepInPlugin("com.IcyClawz.MagazineInspector", "IcyClawz.MagazineInspector", "1.0.1")]
public class Plugin : BaseUnityPlugin
{
private void Awake()
{
new MagazinePatch().Enable();
}
}
public class MagazinePatch : ModulePatch
{
protected override MethodBase GetTargetMethod() =>
typeof(MagazineClass).GetConstructors()[0];
[PatchPostfix]
private static void PatchPostfix(ref MagazineClass __instance)
{
__instance.AddAmmoCountAttribute();
}
}
}

25
MunitionsExpert.sln Normal file
View File

@ -0,0 +1,25 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.2.32616.157
MinimumVisualStudioVersion = 10.0.40219.1
Project("{12CC1CB2-4C8C-40A8-834F-237AB5B50B90}") = "MunitionsExpert", "MunitionsExpert\MunitionsExpert.csproj", "{F1E2DE6A-7145-4ED7-8AB0-DE2FE9E33CCA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{F1E2DE6A-7145-4ED7-8AB0-DE2FE9E33CCA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F1E2DE6A-7145-4ED7-8AB0-DE2FE9E33CCA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F1E2DE6A-7145-4ED7-8AB0-DE2FE9E33CCA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F1E2DE6A-7145-4ED7-8AB0-DE2FE9E33CCA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {BA86C600-4494-49AD-A227-017B8142B09C}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,195 @@
using Comfort.Common;
using EFT.HandBook;
using EFT.InventoryLogic;
using EFT.UI.DragAndDrop;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using UnityEngine;
using UnityEngine.UI;
namespace IcyClawz.MunitionsExpert
{
internal enum EAmmoExtraAttributeId
{
Damage,
PenetrationPower,
ArmorDamage,
FragmentationChance,
RicochetChance
}
internal static class ImageExtensions
{
public static Sprite ToSprite(this System.Drawing.Image instance)
{
byte[] data;
using (MemoryStream ms = new MemoryStream())
{
instance.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
data = ms.ToArray();
}
Texture2D texture = new Texture2D(instance.Width, instance.Height);
ImageConversion.LoadImage(texture, data);
return Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), Vector2.zero);
}
}
internal static class IconCache
{
private static readonly Dictionary<EAmmoExtraAttributeId, Sprite> Cache = new Dictionary<EAmmoExtraAttributeId, Sprite>(5);
public static void Initialize()
{
Cache.Add(EAmmoExtraAttributeId.Damage, Properties.Resources.Damage.ToSprite());
Cache.Add(EAmmoExtraAttributeId.PenetrationPower, Properties.Resources.PenetrationPower.ToSprite());
Cache.Add(EAmmoExtraAttributeId.ArmorDamage, Properties.Resources.ArmorDamage.ToSprite());
Cache.Add(EAmmoExtraAttributeId.FragmentationChance, Properties.Resources.FragmentationChance.ToSprite());
Cache.Add(EAmmoExtraAttributeId.RicochetChance, Properties.Resources.RicochetChance.ToSprite());
}
public static Sprite Get(Enum id) =>
id is EAmmoExtraAttributeId extraId ? Cache[extraId] : null;
}
internal static class AmmoTemplateExtensions
{
private static readonly FieldInfo CachedQualitiesField =
typeof(AmmoTemplate).GetField("_cachedQualities", BindingFlags.NonPublic | BindingFlags.Instance);
public static List<ItemAttributeClass> GetCachedQualities(this AmmoTemplate instance) =>
CachedQualitiesField.GetValue(instance) as List<ItemAttributeClass>;
public static void AddExtraAttributes(this AmmoTemplate instance)
{
instance.SafelyAddQualityToList(new ItemAttributeClass(EAmmoExtraAttributeId.Damage)
{
Name = EAmmoExtraAttributeId.Damage.ToString(),
DisplayNameFunc = () => "Damage",
Base = () => instance.Damage * instance.ProjectileCount,
StringValue = () =>
{
int totalDamage = instance.Damage * instance.ProjectileCount;
if (instance.ProjectileCount > 1)
{
return $"{instance.ProjectileCount} × {instance.Damage} = {totalDamage}";
}
else
{
return totalDamage.ToString();
}
},
DisplayType = () => EItemAttributeDisplayType.Compact
});
instance.SafelyAddQualityToList(new ItemAttributeClass(EAmmoExtraAttributeId.PenetrationPower)
{
Name = EAmmoExtraAttributeId.PenetrationPower.ToString(),
DisplayNameFunc = () => "Penetration power",
Base = () => instance.PenetrationPower,
StringValue = () =>
{
int armorClass = instance.GetPenetrationArmorClass();
string armorClassStr = armorClass > 0 ? $"Class {armorClass}" : "Unarmored";
return $"{armorClassStr} ({instance.PenetrationPower})";
},
DisplayType = () => EItemAttributeDisplayType.Compact
});
instance.SafelyAddQualityToList(new ItemAttributeClass(EAmmoExtraAttributeId.ArmorDamage)
{
Name = EAmmoExtraAttributeId.ArmorDamage.ToString(),
DisplayNameFunc = () => "Armor damage",
Base = () => instance.ArmorDamage,
StringValue = () => $"{instance.ArmorDamage}%",
DisplayType = () => EItemAttributeDisplayType.Compact
});
instance.SafelyAddQualityToList(new ItemAttributeClass(EAmmoExtraAttributeId.FragmentationChance)
{
Name = EAmmoExtraAttributeId.FragmentationChance.ToString(),
DisplayNameFunc = () => "Fragmentation chance",
Base = () => instance.FragmentationChance,
StringValue = () => $"{Math.Round(instance.FragmentationChance * 100, 1)}%",
DisplayType = () => EItemAttributeDisplayType.Compact
});
instance.SafelyAddQualityToList(new ItemAttributeClass(EAmmoExtraAttributeId.RicochetChance)
{
Name = EAmmoExtraAttributeId.RicochetChance.ToString(),
DisplayNameFunc = () => "Ricochet chance",
Base = () => instance.RicochetChance,
StringValue = () => $"{Math.Round(instance.RicochetChance * 100, 1)}%",
DisplayType = () => EItemAttributeDisplayType.Compact
});
}
public static int GetPenetrationArmorClass(this AmmoTemplate instance)
{
var armorClasses = Singleton<BackendConfigSettingsClass>.Instance.Armor.ArmorClass;
for (int i = armorClasses.Length - 1; i >= 0; i--)
{
if (armorClasses[i].Resistance <= instance.PenetrationPower)
{
return i;
}
}
return 0;
}
}
internal static class ColorUtils
{
private const byte ALPHA = 38;
private static readonly Color[] ArmorClassColors = new Color[]
{
new Color32(120, 40, 135, ALPHA), // Unarmored => Violet
new Color32(0, 60, 170, ALPHA), // Class 1 => Blue
new Color32(0, 150, 150, ALPHA), // Class 2 => Cyan
new Color32(70, 140, 0, ALPHA), // Class 3 => Green
new Color32(170, 170, 0, ALPHA), // Class 4 => Yellow
new Color32(140, 70, 0, ALPHA), // Class 5 => Orange
new Color32(170, 20, 0, ALPHA) // Class 6+ => Red
};
public static Color GetArmorClassColor(int armorClass) =>
ArmorClassColors[Mathf.Clamp(armorClass, 0, ArmorClassColors.Length - 1)];
}
internal static class ItemViewExtensions
{
private static readonly FieldInfo BackgroundColorField =
typeof(ItemView).GetField("BackgroundColor", BindingFlags.NonPublic | BindingFlags.Instance);
private static void SetBackgroundColor(this ItemView instance, Color color) =>
BackgroundColorField.SetValue(instance, color);
public static void OverrideColor(this ItemView instance, Item item)
{
if (item is BulletClass bullet && bullet.PenetrationPower > 0)
{
int armorClass = bullet.AmmoTemplate.GetPenetrationArmorClass();
instance.SetBackgroundColor(ColorUtils.GetArmorClassColor(armorClass));
}
}
}
internal static class EntityIconExtensions
{
private static readonly FieldInfo ColorPanelField =
typeof(EntityIcon).GetField("_colorPanel", BindingFlags.NonPublic | BindingFlags.Instance);
private static Image GetColorPanel(this EntityIcon instance) =>
ColorPanelField.GetValue(instance) as Image;
public static void OverrideColor(this EntityIcon instance, Item item)
{
if (item is BulletClass bullet && bullet.PenetrationPower > 0)
{
int armorClass = bullet.AmmoTemplate.GetPenetrationArmorClass();
instance.GetColorPanel().color = ColorUtils.GetArmorClassColor(armorClass);
}
}
}
}

View File

@ -0,0 +1,55 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<AssemblyName>IcyClawz.MunitionsExpert</AssemblyName>
<Version>1.0.0</Version>
<RootNamespace>IcyClawz.MunitionsExpert</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Reference Include="Aki.Reflection">
<HintPath>..\Shared\Aki.Reflection.dll</HintPath>
</Reference>
<Reference Include="Assembly-CSharp">
<HintPath>..\Shared\Assembly-CSharp.dll</HintPath>
</Reference>
<Reference Include="BepInEx">
<HintPath>..\Shared\BepInEx.dll</HintPath>
</Reference>
<Reference Include="Comfort">
<HintPath>..\Shared\Comfort.dll</HintPath>
</Reference>
<Reference Include="Sirenix.Serialization">
<HintPath>..\Shared\Sirenix.Serialization.dll</HintPath>
</Reference>
<Reference Include="UnityEngine">
<HintPath>..\Shared\UnityEngine.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.CoreModule">
<HintPath>..\Shared\UnityEngine.CoreModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.ImageConversionModule">
<HintPath>..\Shared\UnityEngine.ImageConversionModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.UI">
<HintPath>..\Shared\UnityEngine.UI.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>

96
MunitionsExpert/Plugin.cs Normal file
View File

@ -0,0 +1,96 @@
using Aki.Reflection.Patching;
using BepInEx;
using EFT.HandBook;
using EFT.InventoryLogic;
using EFT.UI;
using EFT.UI.DragAndDrop;
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
namespace IcyClawz.MunitionsExpert
{
[BepInPlugin("com.IcyClawz.MunitionsExpert", "IcyClawz.MunitionsExpert", "1.0.0")]
public class Plugin : BaseUnityPlugin
{
private void Awake()
{
IconCache.Initialize();
new StaticIconsPatch().Enable();
new AmmoTemplatePatch().Enable();
new ItemViewPatch().Enable();
new EntityIconPatch().Enable();
}
}
internal class StaticIconsPatch : ModulePatch
{
protected override MethodBase GetTargetMethod() =>
typeof(StaticIcons).GetMethod("GetAttributeIcon", BindingFlags.Public | BindingFlags.Instance);
[PatchPrefix]
private static bool PatchPrefix(ref Sprite __result, Enum id)
{
var icon = IconCache.Get(id);
if (icon != null)
{
__result = icon;
return false;
}
return true;
}
}
internal class AmmoTemplatePatch : ModulePatch
{
protected override MethodBase GetTargetMethod() =>
typeof(AmmoTemplate).GetMethod("GetCachedReadonlyQualities", BindingFlags.Public | BindingFlags.Instance);
[PatchPrefix]
private static bool PatchPrefix(ref List<ItemAttributeClass> __result, AmmoTemplate __instance)
{
if (__instance.GetCachedQualities() != null)
{
__result = null;
return false;
}
return true;
}
[PatchPostfix]
private static void PatchPostfix(ref List<ItemAttributeClass> __result, AmmoTemplate __instance)
{
if (__result == null)
{
__result = __instance.GetCachedQualities();
return;
}
__instance.AddExtraAttributes();
}
}
internal class ItemViewPatch : ModulePatch
{
protected override MethodBase GetTargetMethod() =>
typeof(ItemView).GetMethod("UpdateColor", BindingFlags.NonPublic | BindingFlags.Instance);
[PatchPrefix]
private static void PatchPrefix(ref ItemView __instance)
{
__instance.OverrideColor(__instance.Item);
}
}
internal class EntityIconPatch : ModulePatch
{
protected override MethodBase GetTargetMethod() =>
typeof(EntityIcon).GetMethod("Show", BindingFlags.Public | BindingFlags.Instance);
[PatchPostfix]
private static void PatchPostfix(ref EntityIcon __instance, Item item)
{
__instance.OverrideColor(item);
}
}
}

View File

@ -0,0 +1,113 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace IcyClawz.MunitionsExpert.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("IcyClawz.MunitionsExpert.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap ArmorDamage {
get {
object obj = ResourceManager.GetObject("ArmorDamage", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap Damage {
get {
object obj = ResourceManager.GetObject("Damage", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap FragmentationChance {
get {
object obj = ResourceManager.GetObject("FragmentationChance", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap PenetrationPower {
get {
object obj = ResourceManager.GetObject("PenetrationPower", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap RicochetChance {
get {
object obj = ResourceManager.GetObject("RicochetChance", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
}
}

View File

@ -0,0 +1,136 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="ArmorDamage" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\ArmorDamage.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="Damage" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\Damage.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="FragmentationChance" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\FragmentationChance.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="PenetrationPower" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\PenetrationPower.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="RicochetChance" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\RicochetChance.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
</root>

Binary file not shown.

After

Width:  |  Height:  |  Size: 649 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 B