C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
#if CARBONusing HarmonyLib;
#else
#endif
using HarmonyLib;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using UnityEngine;
using Rust;
using static Oxide.Plugins.CustomItemDefinitions;
namespace Oxide.Plugins {
[Info("Customizable Protection", "0xF [dsc.gg/0xf-plugins]", "2.1.8")]
public class CustomizableProtection : RustPlugin {
#region Consts
public const ItemContainer.Flag NoSortify = (ItemContainer.Flag)(8192) | (ItemContainer.Flag)(16384);
private const ItemDefinition.Flag CUSTOMATTIRE_FLAG = (ItemDefinition.Flag)(65536);
#endregion
#region Variables
public static CustomizableProtection PluginInstance;
#endregion
#region PluginReference
[PluginReference]
private Plugin RustTranslationAPI;
#endregion
#region Permissions
const string giveCommandPerm = "customizableprotection.give";
const string iconShowPerm = "customizableprotection.icon.show";
const string iconHidePerm = "customizableprotection.icon.hide";
#endregion
#region Harmony
public static class Harmony {
private static string Name;
#if CARBON
private static HarmonyLib.Harmony Instance;
#else
private static HarmonyLib.Harmony Instance;
#endif
public static void Init(string name) {
Name = name;
#if CARBON
Instance = new HarmonyLib.Harmony(name);
#else
Instance = new HarmonyLib.Harmony(name);
#endif
}
public static void Patch() {
Instance.Patch(AccessTools.Method(typeof(BasePlayer), "UpdateProtectionFromClothing"), new HarmonyMethod(typeof(Patches), "UpdateProtectionFromClothing"));
Instance.Patch(AccessTools.Method(typeof(Item), "Save"), postfix : new HarmonyMethod(typeof(Patches), "Item_Save"));
}
public static void UnpatchAll() {
Instance.UnpatchAll(Name);
}
}
private static class Patches {
internal static bool UpdateProtectionFromClothing(BasePlayer __instance) {
PluginInstance.UpdatePlayerProtection(__instance);
return false;
}
internal static void Item_Save(Item __instance, ref ProtoBuf.Item __result) {
if (!SaveRestore.IsSaving && __instance.info.category == ItemCategory.Attire && __result.streamerName != null && __result.streamerName.StartsWith("\n")) {
ItemContainer container = __instance.parent;
if (container != null) {
BasePlayer player = container.playerOwner;
if (player != null && !player.IsNpc && !player.inventory.loot.IsLooting()) __result.name = __result.streamerName;
}
}
}
}
#endregion
#region Hooks
private void Init() {
PluginInstance = this;
DamageTypes = new Dictionary<Rust.DamageType, KeyValuePair<string, int>>();
var names = Enum.GetNames(typeof(Rust.DamageType));
for (int i = 0; i < names.Length; i++) {
Rust.DamageType damageType = (Rust.DamageType)i;
if (!DamageTypeExceptions.Contains(damageType)) DamageTypes.Add(damageType, new KeyValuePair<string, int>(names[i], i));
}
DamageTypesDictionary = DamageTypes.Values.ToDictionary(x => x.Key, x => x.Value);
DamageTypesDictionary.Add("OxygenExposure", -1);
;
permission.RegisterPermission(giveCommandPerm, this);
permission.RegisterPermission(iconShowPerm, this);
permission.RegisterPermission(iconHidePerm, this);
LoadConfig();
config.DataItems.Initialize();
RegisterCustomItemDefinitions();
foreach (var pair in DamageTypesDictionary) {
if (!config.ProtectionMultipliers.Types.ContainsKey(pair.Key)) {
config.ProtectionMultipliers.Types.Add(pair.Key, 1.0f);
}
if (!config.Default.ContainsKey(pair.Key)) {
config.Default.Add(pair.Key, 0.0f);
}
}
SaveConfig();
}
void Loaded() {
Harmony.Init(this.Name);
Harmony.Patch();
}
private void OnServerInitialized() {
LoadIcon();
BasePlayer.activePlayerList.ToList().ForEach(player => AddUiIcon(player));
foreach (var player in BasePlayer.activePlayerList) UpdatePlayerProtection(player);
}
void Unload() {
Harmony.UnpatchAll();
BasePlayer.allPlayerList.ToList().ForEach(player => {
DestroyUI(player);
DestroyIcon(player);
}
);
}
private void OnUserPermissionGranted(string playerId, string perm) {
if (perm != iconShowPerm && perm != iconHidePerm) return;
BasePlayer player = BasePlayer.Find(playerId);
if (player == null) return;
OnPermissionsChanged(player);
}
private void OnUserPermissionRevoked(string playerId, string perm) {
if (perm != iconShowPerm && perm != iconHidePerm) return;
BasePlayer player = BasePlayer.Find(playerId);
if (player == null) return;
OnPermissionsChanged(player);
}
private void OnGroupPermissionGranted(string groupName, string perm) {
if (perm != iconShowPerm && perm != iconHidePerm) return;
BasePlayer.activePlayerList.ToList().ForEach(p => OnPermissionsChanged(p));
}
private void OnGroupPermissionRevoked(string groupName, string perm) {
if (perm != iconShowPerm && perm != iconHidePerm) return;
BasePlayer.activePlayerList.ToList().ForEach(p => OnPermissionsChanged(p));
}
private void OnPlayerSleepEnded(BasePlayer player) => UpdatePlayerProtection(player);
void OnEntityTakeDamage(BaseCombatEntity entity, HitInfo info) {
if (entity == null) return;
BasePlayer player = entity.ToPlayer();
if (player == null) return;
if (player.baseProtection == null) return;
if (info.UseProtection) {
HitArea boneArea = info.boneArea;
if (boneArea == (HitArea)(-1) && info.HitPositionWorld != null && info.Initiator != null) {
float minDistance = float.MaxValue;
for (int i = 0; i < player.skeletonProperties.bones.Length; i++) {
var bone = player.skeletonProperties.bones[i];
var distance = Vector3.Distance(info.HitPositionWorld, player.transform.TransformPoint(bone.bone.transform.position));
if (distance < minDistance) {
minDistance = distance;
boneArea = bone.area;
}
}
}
float[] amounts = CalculateProtection(player, boneArea);
if (amounts.Max() > 0) {
for (int i = 0; i < amounts.Length; i++) {
if (amounts[i] != 0f) {
info.damageTypes.Scale((Rust.DamageType)i, 1f - amounts[i]);
}
}
info.UseProtection = false;
}
}
}
private void OnRunPlayerMetabolism(PlayerMetabolism metabolism, BaseCombatEntity ownerEntity, float delta) {
BasePlayer player = ownerEntity as BasePlayer;
if (!player) return;
if (player.AirFactor() == 1f) return;
float oe = CalculateOxygenExposure(player);
if (oe > 0) metabolism.oxygen.MoveTowards(0f, -(delta * 0.1f * Mathf.Clamp(oe, 0, 1)));
}
private void OnItemAddedToContainer(ItemContainer container, Item item) {
if (!item.info.isWearable) return;
if (!container.HasFlag(ItemContainer.Flag.IsPlayer)) return;
if (container.playerOwner?.inventory?.loot?.IsLooting() == true) return;
NextTick(() => {
ItemDefinition itemDef = item.info;
if (itemDef == null) return;
CustomItem customItem;
if (itemDef.HasFlag(CUSTOMATTIRE_FLAG) && TryGetCustomItem(itemDef, out customItem)) {
var nameLangKey = $"{itemDef.shortname}:Name";
var itemName = GetMessage(nameLangKey, container.playerOwner?.UserIDString);
if (itemName == nameLangKey) itemName = customItem.Name;
SetName(item, customItem.Protection, itemName);
if (customItem.SkinId != 0) item.skin = customItem.SkinId;
item.MarkDirty();
}
else {
Dictionary<string, float> data;
if (TryGetDefaultProtection(item, out data)) {
SetName(item, data);
item.MarkDirty();
}
}
}
);
}
private void OnPlayerLootEnd(PlayerLoot playerLoot) {
var player = (BasePlayer)playerLoot.gameObject.ToBaseEntity();
if (playerLoot.entitySource == RelationshipManager.ServerInstance) {
DestroyUI(player);
playerLoot.containers.Where(c => c.entityOwner == RelationshipManager.ServerInstance && c.capacity == 0).ToList().ForEach(c => c.Kill());
}
}
void OnLootEntity(BasePlayer player, BaseEntity entity) {
NextTick(() => {
foreach (var item in player.inventory.AllItems()) {
item.MarkDirty();
}
}
);
}
private void OnPlayerRespawned(BasePlayer player) => AddUiIcon(player);
private void OnPlayerConnected(BasePlayer player) => AddUiIcon(player);
#endregion
#region Commands
[ConsoleCommand("cp.reload")]
private void ConsoleCommandReload(ConsoleSystem.Arg arg) {
if (arg.Player() != null) {
if (!arg.Player().IsAdmin) {
SendReply(arg, "This command is only allowed to administrators");
return;
}
}
Server.Command($"o.reload {Name}");
if (arg.Player() != null) {
SendReply(arg, "Items successfully reloaded!");
return;
}
Puts("Items successfully reloaded");
}
[ConsoleCommand("cp.create")]
private void ConsoleCommandAdd(ConsoleSystem.Arg arg) {
if (arg.Player() != null) {
if (!arg.Player().IsAdmin) {
SendReply(arg, "This command is only allowed to administrators");
return;
}
}
if (arg.Args.IsNullOrEmpty() || arg.Args?.Count() != 2) {
if (arg.Player() != null) {
SendReply(arg, "Usage: cp.create <name> <shortname>");
return;
}
PrintWarning("Usage: cp.create <name> <shortname>");
return;
}
var name = arg.Args[0];
var shortname = arg.Args[1];
if (config.DataItems.CustomFs.ExistsDatafile(name)) {
if (arg.Player() != null) {
SendReply(arg, "A file with this name already exists, delete it or create it under another name.");
return;
}
PrintWarning("A file with this name already exists, delete it or create it under another name.");
return;
}
if (!config.DataItems.Default.ContainsKey(shortname)) {
if (arg.Player() != null) {
SendReply(arg, $"No protection options for shortname '{shortname}', try another one, you can find available shortnames in the 'Default' folder");
return;
}
PrintWarning($"No protection options for shortname '{shortname}', try another one, you can find available shortnames in the 'Default' folder");
return;
}
CustomItem newItem = new CustomItem() {
Shortname = shortname, SkinId = (ulong)new System.Random().Next(1000, 999999), Name = name, Description = "", Protection = config.DataItems.Default[shortname]
}
;
config.DataItems.CustomFs.WriteObject<CustomItem>(name, newItem);
config.DataItems.Custom[name] = newItem;
if (arg.Player() != null) {
SendReply(arg, $"Custom item with name '{name}' added!");
return;
}
Puts($"Custom item with name '{name}' added!");
}
[ConsoleCommand("cp.give")]
private void ConsoleCommandGive(ConsoleSystem.Arg arg) {
string nickname = "SERVER";
BasePlayer player = arg.Player();
if (player != null) {
if (!player.IsAdmin && !permission.UserHasPermission(player.UserIDString, giveCommandPerm)) {
arg.ReplyWith("You don't have permission to use this command.");
return;
}
nickname = player.displayName;
}
if (arg.Args.IsNullOrEmpty() || arg.Args?.Count() > 2 || arg.Args?.Count() < 1) {
if (player != null) {
arg.ReplyWith("Usage: cp.give <steamid/nickname> <custom item name>");
return;
}
PrintWarning("Usage: cp.give <steamid/nickname> <custom item name>");
return;
}
BasePlayer playerToGive = arg.Player();
if (arg.Args?.Count() == 2) {
playerToGive = BasePlayer.Find(arg.Args[0]);
if (playerToGive == null) {
if (player != null) {
arg.ReplyWith($"Player '{arg.Args[0]}' is offline or not exists.");
return;
}
PrintError($"Player '{arg.Args[0]}' is offline or not exists.");
return;
}
}
string uniqueShortname = arg.Args?.Last();
CustomItem customItem;
if (TryGetCustomItemByKey(uniqueShortname, out customItem)) {
Item itemToGive = ItemManager.CreateByName(uniqueShortname, 1, customItem.SkinId);
var nameLangKey = $"{uniqueShortname}:Name";
var itemName = GetMessage(nameLangKey, playerToGive.UserIDString);
if (itemName == nameLangKey) itemName = customItem.Name;
if (!playerToGive.inventory.GiveItem(itemToGive)) {
itemToGive.Remove(0f);
arg.ReplyWith("Couldn't give item (inventory full?)");
PrintWarning($"{nickname}[{(player == null ? "0" : player.UserIDString)}] failed to give a custom item [{uniqueShortname}] to player {playerToGive.displayName}[{(player == null ? "0" : playerToGive.UserIDString)}]!");
return;
}
SetName(itemToGive, customItem.Protection, itemName);
itemToGive.skin = customItem.SkinId;
itemToGive.MarkDirty();
if (arg.Player() != null) arg.ReplyWith($"Item [{uniqueShortname}] added to {playerToGive.displayName}'s inventory!");
PrintWarning($"{nickname}[{(player == null ? "0" : player.UserIDString)}] gave {playerToGive.displayName}[{(playerToGive == null ? "0" : playerToGive.UserIDString)}] custom item [{uniqueShortname}]!");
}
else {
if (arg.Player() != null) {
arg.ReplyWith($"Item with name \"{uniqueShortname}\" was not found!");
return;
}
PrintError($"Item with name \"{uniqueShortname}\" was not found!");
}
}
#endregion
#region Classes
public class CustomItem {
public class UISettings {
[JsonProperty(PropertyName = "Name Color (or use <color></color> in name)")]
public string NameColor {
get;
set;
}
= "1 1 1 1";
[JsonProperty(PropertyName = "Frame Color")]
public string FrameColor {
get;
set;
}
= "0.94 0.75 0.15 1";
}
[JsonProperty(PropertyName = "Shortname")]
public string Shortname {
get;
set;
}
= "";
[JsonProperty(PropertyName = "SkinId")]
public ulong SkinId {
get;
set;
}
= 0;
[JsonProperty(PropertyName = "SkinId to recognize (MUST BE UNIQUE)")]
public ulong RecognizeSkinId {
get;
set;
}
= (ulong)UnityEngine.Random.Range(1000, 10000000);
[JsonProperty(PropertyName = "Name")]
public string Name {
get;
set;
}
= "";
[JsonProperty(PropertyName = "Description")]
public string Description {
get;
set;
}
= "";
[JsonProperty(PropertyName = "UI Settings")]
public UISettings UiSettings {
get;
set;
}
= new UISettings();
[JsonProperty(PropertyName = "Protection")]
public Dictionary<string, float> Protection {
get;
set;
}
= new Dictionary<string, float>();
}
#endregion
#region Fields
private static List<Rust.DamageType> DamageTypeExceptions = new List<Rust.DamageType> {
Rust.DamageType.Generic, Rust.DamageType.LAST, Rust.DamageType.Decay, Rust.DamageType.AntiVehicle, Rust.DamageType.Collision, Rust.DamageType.Fun_Water
}
;
private static Dictionary<Rust.DamageType, KeyValuePair<string, int>> DamageTypes = new Dictionary<Rust.DamageType, KeyValuePair<string, int>>();
private static Dictionary<string, int> DamageTypesDictionary;
#endregion
#region Methods
void RegisterCustomItemDefinitions() {
foreach (var item in config.DataItems.Custom.Values) {
ItemDefinition parentDefinition = ItemManager.FindItemDefinition(item.Shortname);
if (parentDefinition == null) continue;
CustomItemDefinitions.RegisterPluginItemDefinition(new CustomItemDefinition {
shortname = item.Shortname, parentItemId = parentDefinition.itemid, maxStackSize = 1, flags = CUSTOMATTIRE_FLAG, defaultName = item.Name, defaultDescription = item.Description, defaultSkinId = item.SkinId, itemMods = parentDefinition.itemMods
}
, this);
}
}
private void SetName(Item item, Dictionary<string, float> data, string itemName = null) {
if (item == null || data == null) return;
item.name = itemName;
item.streamerName = null;
if (!config.ItemSelectInfo) return;
BasePlayer owner = item.GetOwnerPlayer();
Dictionary<string, string> translation = null;
if (RustTranslationAPI != null && (itemName == null || itemName == string.Empty)) translation = RustTranslationAPI.Call<Dictionary<string, string>>("GetTranslations", lang.GetLanguage(owner?.UserIDString));
if (itemName == null || itemName == string.Empty) {
if (translation == null || !translation.TryGetValue($"{item.info.shortname}", out itemName)) itemName = item.info.displayName.translated;
}
float[] protectionAmounts = GetProtectionForItem(item);
Dictionary<string, float> FieldsForRender = DamageTypes.Values.ToDictionary(x => GetMessage(x.Key, owner?.UserIDString), x => (float)Math.Round(protectionAmounts[x.Value] * 100, 2));
FieldsForRender.Add(GetMessage(LangKeys.OxygenExposure, owner?.UserIDString), GetOxygenExposureForItem(item) * 100);
FieldsForRender = FieldsForRender.Where(i => i.Value != 0f).ToDictionary(i => i.Key, i => i.Value);
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append(string.Concat(Enumerable.Repeat("\n", FieldsForRender.Count)));
stringBuilder.Append($"\n{itemName}");
stringBuilder.Append("\n\t\t\t\t\t\t\t\t\t\t\t\t" + GetMessage(LangKeys.ItemInfoPanelHeader, owner?.UserIDString));
foreach (var pair in FieldsForRender) {
string keyWithoutRichtext = Regex.Replace(pair.Key, "<.*?>", String.Empty);
stringBuilder.Append("\n\t\t\t\t\t\t\t\t\t\t\t\t ");
stringBuilder.Append($" {keyWithoutRichtext} {pair.Value}%");
}
item.streamerName = stringBuilder.ToString();
item.MarkDirty();
}
public bool TryGetCustomItem(Item item, out CustomItem customItem) => TryGetCustomItem(item.info, out customItem);
private bool TryGetCustomItem(ItemDefinition info, out CustomItem customItem) {
if (!info.HasFlag(CUSTOMATTIRE_FLAG) || !TryGetCustomItemByKey(info.shortname, out customItem)) {
customItem = null;
return false;
}
return true;
}
public bool HasCustomItemWithKey(string key) {
return config.DataItems.Custom.ContainsKey(key);
}
public bool TryGetCustomItemByKey(string key, out CustomItem customItem) {
return config.DataItems.Custom.TryGetValue(key, out customItem);
}
public bool TryGetProtection(Item item, out Dictionary<string, float> data) {
return TryGetProtection(item.info, out data);
}
public bool TryGetProtection(ItemDefinition info, out Dictionary<string, float> data) {
CustomItem customItem;
if (TryGetCustomItem(info, out customItem)) {
data = customItem.Protection;
return true;
}
else return TryGetDefaultProtection(info.shortname, out data);
}
public bool TryGetDefaultProtection(Item item, out Dictionary<string, float> data) => TryGetDefaultProtection(item.info.shortname, out data);
public bool TryGetDefaultProtection(string shortname, out Dictionary<string, float> data) {
return config.DataItems.Default.TryGetValue(shortname, out data);
}
public void UpdatePlayerProtection(BasePlayer player) {
if (player == null) return;
var amounts = CalculateProtection(player, (HitArea)(-1));
player.baseProtection.amounts = amounts;
}
public float GetOxygenExposureForItem(Item item) {
float @return = config.Default["OxygenExposure"];
Dictionary<string, float> protection = null;
if (TryGetProtection(item, out protection)) @return += protection["OxygenExposure"];
@return *= config.ProtectionMultipliers.Types["OxygenExposure"];
@return *= config.ProtectionMultipliers.Common;
@return *= 0.01f;
return Mathf.Clamp(@return, 0, 1);
}
public float CalculateOxygenExposure(BasePlayer player) {
float @return = config.Default["OxygenExposure"];
foreach (Item item in player.inventory.containerWear.itemList) {
Dictionary<string, float> protection = null;
if (!TryGetProtection(item, out protection)) continue;
@return += protection["OxygenExposure"];
}
@return *= config.ProtectionMultipliers.Types["OxygenExposure"];
@return *= config.ProtectionMultipliers.Common;
@return *= 0.01f;
return Mathf.Clamp(@return, 0, 1);
}
public float[] GetProtectionForItem(Item item) {
float[] protectionAmounts = new float[(int)Rust.DamageType.LAST];
foreach (var pair in config.Default) {
if (pair.Value == 0) continue;
var value = pair.Value * config.ProtectionMultipliers.Types[pair.Key] * config.ProtectionMultipliers.Common;
var index = DamageTypesDictionary[pair.Key];
if (index >= 0) protectionAmounts[index] = value;
}
Dictionary<string, float> protection;
if (TryGetProtection(item, out protection)) {
foreach (var pair in protection) {
if (pair.Value == 0) continue;
var index = DamageTypesDictionary[pair.Key];
if (index >= 0) protectionAmounts[index] += pair.Value;
}
}
for (int i = 0; i < protectionAmounts.Length; i++) {
if (i == 22) {
protectionAmounts[i] = 1f;
continue;
}
Rust.DamageType damageType = (Rust.DamageType)i;
if (!DamageTypes.ContainsKey(damageType)) continue;
string DamageTypeName = DamageTypes[damageType].Key;
protectionAmounts[i] *= config.ProtectionMultipliers.Types[DamageTypeName];
protectionAmounts[i] *= config.ProtectionMultipliers.Common;
protectionAmounts[i] *= 0.01f;
protectionAmounts[i] = (config.LockNormalValues ? Mathf.Clamp(protectionAmounts[i], 0, 1) : protectionAmounts[i]);
}
return protectionAmounts;
}
public float[] CalculateProtection(BasePlayer player, HitArea hitArea) {
float[] protectionAmounts = new float[(int)Rust.DamageType.LAST];
foreach (var pair in config.Default) {
if (pair.Value == 0) continue;
var value = pair.Value * config.ProtectionMultipliers.Types[pair.Key] * config.ProtectionMultipliers.Common;
var index = DamageTypesDictionary[pair.Key];
if (index >= 0) protectionAmounts[index] = value;
}
foreach (Item item in player.inventory.containerWear.itemList) {
ItemDefinition itemDefinition = item.info;
ItemModWearable itemModWearable = itemDefinition.ItemModWearable;
if (itemModWearable != null && (hitArea == (HitArea)(-1) || itemModWearable.ProtectsArea(hitArea))) {
Dictionary<string, float> data;
if (!TryGetProtection(item, out data)) continue;
foreach (var pair in data) {
if (pair.Value == 0) continue;
var index = DamageTypesDictionary[pair.Key];
if (index >= 0) {
protectionAmounts[index] += pair.Value;
}
}
}
}
var itemListCount = player.inventory.containerWear.itemList.Where(item => item.position < player.inventory.containerWear.capacity).Count();
for (int i = 0; i < protectionAmounts.Length; i++) {
if (i == 22) {
protectionAmounts[i] = 1f;
continue;
}
Rust.DamageType damageType = (Rust.DamageType)i;
if (!DamageTypes.ContainsKey(damageType)) continue;
string DamageTypeName = DamageTypes[damageType].Key;
if (hitArea == (HitArea)(-1) && i != 17) protectionAmounts[i] = itemListCount == 0 ? 0 : protectionAmounts[i] / itemListCount;
protectionAmounts[i] *= config.ProtectionMultipliers.Types[DamageTypeName];
protectionAmounts[i] *= config.ProtectionMultipliers.Common;
protectionAmounts[i] *= 0.01f;
protectionAmounts[i] = (config.LockNormalValues ? Mathf.Clamp(protectionAmounts[i], 0, 1) : protectionAmounts[i]);
}
return protectionAmounts;
}
#endregion
#region UI
private string IconString = null;
private byte[] IconData = Convert.FromBase64String("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");
private void LoadIcon() => IconString = FileStorage.server.Store(IconData, FileStorage.Type.png, CommunityEntity.ServerInstance.net.ID).ToString();
private void OnPermissionsChanged(BasePlayer player) {
if (permission.UserHasPermission(player.UserIDString, iconHidePerm) && !permission.UserHasPermission(player.UserIDString, iconShowPerm)) DestroyIcon(player);
else AddUiIcon(player);
}
private void AddUiIcon(BasePlayer player) {
if (config.IconPosition == 0) return;
if (permission.UserHasPermission(player.UserIDString, iconHidePerm) && !permission.UserHasPermission(player.UserIDString, iconShowPerm)) return;
if (config.IconPosition > 5) {
PrintWarning("The maximum position to the right is 5, it was set to 5");
config.IconPosition = 5;
}
else if (config.IconPosition < -8) {
PrintWarning("The maximum position to the left is -8, it was set to -8");
config.IconPosition = -8;
}
int x = (config.IconPosition > 0 ? 121 : -201) + (config.IconPosition * 64);
var cui = new CUI();
string UI = cui.AddPanel(color : "1 0.96 0.88.15", anchorMin : ".5 0", anchorMax : ".5 0", offsetMin : $"{x} 18", offsetMax : $"{x + 60} 78", parent : "Hud.Menu", name : "CustomizableProtection_Icon");
cui.AddImage(IconString, anchorMin : "0 0", anchorMax : "1 1", parent : UI);
cui.AddButton("customizableprotection.ui open", parent : UI);
cui.RenderWithDestroy(player);
}
void SetupInventorySelector(BasePlayer player) {
var cui = new CUI();
string fullscreen = cui.AddPanel(anchorMin : "0 0", anchorMax : "1 1", parent : "Hud.Menu", name : "CustomizableProtection_InventorySelectorContainer");
var wear = player.inventory.containerWear;
for (int i = wear.capacity; i >= 0; i--) {
Item item = wear.GetSlot(6 - i);
if (item == null) continue;
string color = "1 1 1 1";
CustomItem customItem = null;
if (item.info != null && item.info.HasFlag(CUSTOMATTIRE_FLAG) != null && TryGetCustomItem(item.info, out customItem)) color = customItem.UiSettings.FrameColor;
cui.AddButton(command : $"customizableprotection.ui view {item.uid.Value}", color : color, sprite : "assets/content/ui/ui.box.tga", imageType : UnityEngine.UI.Image.Type.Tiled, anchorMin : ".5 0", anchorMax : ".5 0", offsetMin : $"{-266 - 54 * i} 114", offsetMax : $"{-214 - 54 * i} 166", parent : fullscreen);
}
var belt = player.inventory.containerBelt;
for (int i = 0; i < belt.capacity; i++) {
Item item = belt.GetSlot(i);
if (item == null || !item.info.isWearable) continue;
string color = "1 1 1 1";
CustomItem customItem = null;
if (item.info != null && item.info.HasFlag(CUSTOMATTIRE_FLAG) != null && TryGetCustomItem(item.info, out customItem)) color = customItem.UiSettings.FrameColor;
cui.AddButton(command : $"customizableprotection.ui view {item.uid}", color : color, sprite : "assets/content/ui/ui.box.tga", imageType : UnityEngine.UI.Image.Type.Tiled, anchorMin : ".5 0", anchorMax : ".5 0", offsetMin : $"{-201 + 64 * i} 17", offsetMax : $"{-139 + 64 * i} 79", parent : fullscreen);
}
var main = player.inventory.containerMain;
int startY = 85 + 64 * (main.capacity / 6);
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 6; j++) {
Item item = main.GetSlot(i * 6 + j);
if (item == null || !item.info.isWearable) continue;
string color = "1 1 1 1";
CustomItem customItem = null;
if (item.info != null && item.info.HasFlag(CUSTOMATTIRE_FLAG) != null && TryGetCustomItem(item.info, out customItem)) color = customItem.UiSettings.FrameColor;
cui.AddButton(command : $"customizableprotection.ui view {item.uid}", color : color, sprite : "assets/content/ui/ui.box.tga", imageType : UnityEngine.UI.Image.Type.Tiled, anchorMin : ".5 0", anchorMax : ".5 0", offsetMin : $"{-201 + 64 * j} {startY - 64 * (i + 1)}", offsetMax : $"{-139 + 64 * j} {startY - 2 - 64 * i}", parent : fullscreen);
}
}
cui.RenderWithDestroy(player);
}
void ShowItemInfo(BasePlayer player, ulong uid) {
Item item = player.inventory.FindItemByUID(new ItemId(uid));
if (item == null) return;
ItemDefinition definition = item.info;
if (definition == null) return;
string itemName = string.Empty;
string itemNameColor = "1 1 1 1";
string itemDescription = string.Empty;
int itemId = 0;
bool isCustomItem = false;
CustomItem customItem;
if (TryGetCustomItem(definition, out customItem)) {
var nameLangKey = $"{definition.shortname}:Name";
var descriptionLangKey = $"{definition.shortname}:Description";
itemName = GetMessage(nameLangKey, item.GetOwnerPlayer()?.UserIDString);
if (itemName == nameLangKey) itemName = customItem.Name;
itemName = customItem.Name;
itemNameColor = customItem.UiSettings.NameColor;
itemDescription = GetMessage(descriptionLangKey, player.UserIDString);
if (itemDescription == descriptionLangKey) itemDescription = customItem.Description;
isCustomItem = true;
}
ItemDefinition itemDefinition = item.info;
Dictionary<string, string> translation = null;
if (RustTranslationAPI != null && (itemName == string.Empty || itemDescription == string.Empty)) translation = RustTranslationAPI.Call<Dictionary<string, string>>("GetTranslations", lang.GetLanguage(player.UserIDString));
if (itemName == string.Empty) {
if (translation == null || !translation.TryGetValue($"{itemDefinition.shortname}", out itemName)) itemName = itemDefinition.displayName.translated;
}
if (itemDescription == string.Empty) {
if (translation == null || !translation.TryGetValue($"{itemDefinition.shortname}.desc", out itemDescription)) itemDescription = itemDefinition.displayDescription.translated;
}
itemId = itemDefinition.itemid;
var cui = new CUI();
string fullscreen = cui.AddContainer(anchorMin : "0 0", anchorMax : "1 1", parent : "Hud.Menu", name : "CustomizableProtection_ItemInfoContainer");
string blur = cui.AddColorPanel(color : "0 0 0 0.5", material : "assets/content/ui/uibackgroundblur-ingamemenu.mat", imageType : UnityEngine.UI.Image.Type.Tiled, anchorMin : "0.5 0", anchorMax : "0.5 0", offsetMin : "-200 360", offsetMax : "182 650", parent : fullscreen);
string infoPanel = cui.AddColorPanel(color : ".2.2.2 0.95", sprite : "assets/content/ui/ui.background.rounded.png", imageType : UnityEngine.UI.Image.Type.Tiled, anchorMin : "0.5 0", anchorMax : "0.5 0", offsetMin : "-200 360", offsetMax : "182 650", parent : fullscreen);
string nameTextbox = cui.AddText(color : itemNameColor, text : itemName, font : CUI.Font.RobotoCondensedBold, fontSize : 16, align : TextAnchor.MiddleCenter, anchorMin : "0 1", anchorMax : "0 1", offsetMin : "10 -30", offsetMax : "252 -10", parent : infoPanel);
string descriptionPanel = cui.AddColorPanel(color : "0.147 0.147 0.147 0.95", sprite : "assets/content/ui/ui.background.rounded.png", imageType : UnityEngine.UI.Image.Type.Tiled, anchorMin : "0 1", anchorMax : "0 1", offsetMin : "10 -280", offsetMax : "262 -40", parent : infoPanel);
string descriptionTextbox = cui.AddText(text : itemDescription, font : CUI.Font.RobotoCondensedRegular, fontSize : 12, align : TextAnchor.UpperLeft, anchorMin : "0 0", anchorMax : "0 0", offsetMin : "10 10", offsetMax : "242 230", parent : descriptionPanel);
string iconPanel = cui.AddColorPanel(color : "0.14 0.14 0.14 0.95", sprite : "assets/content/ui/ui.background.rounded.png", imageType : UnityEngine.UI.Image.Type.Tiled, anchorMin : "1 1", anchorMax : "1 1", offsetMin : "-110 -110", offsetMax : "-10 -10", parent : infoPanel);
string icon = cui.AddIcon(itemId : itemId, skin : item.skin, anchorMin : "1 1", anchorMax : "1 1", offsetMin : "-110 -110", offsetMax : "-10 -10", parent : infoPanel);
string statsPanel = cui.AddColorPanel(color : ".2.2.2 0.95", sprite : "assets/content/ui/ui.background.rounded.png", imageType : UnityEngine.UI.Image.Type.Tiled, anchorMin : "0.5 0", anchorMax : "0.5 0", offsetMin : "190 235", offsetMax : "500 650", parent : fullscreen);
string statsHeader = cui.AddText(text : GetMessage(LangKeys.ItemInfoPanelHeader, player.UserIDString), font : CUI.Font.RobotoCondensedBold, fontSize : 14, align : TextAnchor.MiddleCenter, anchorMin : "0 1", anchorMax : "0 1", offsetMin : "10 -20", offsetMax : "290 -10", parent : statsPanel);
string statsContainer = cui.AddColorPanel(sprite : "assets/content/ui/ui.background.rounded.png", imageType : UnityEngine.UI.Image.Type.Tiled, anchorMin : "0 0", anchorMax : "1 1", offsetMin : "10 10", offsetMax : "-10 -30", parent : statsPanel);
float[] protectionAmounts = GetProtectionForItem(item);
Dictionary<string, float> FieldsForRender = DamageTypes.Values.ToDictionary(x => x.Key, x => (float)Math.Round(protectionAmounts[x.Value] * 100, 2));
FieldsForRender.Add(GetMessage(LangKeys.OxygenExposure, player.UserIDString), GetOxygenExposureForItem(item) * 100);
FieldsForRender = FieldsForRender.Where(i => i.Value != 0f).ToDictionary(i => i.Key, i => i.Value);
for (int i = 0; i < FieldsForRender.Count; i++) {
var pair = FieldsForRender.ElementAt(i);
cui.AddText(text : GetMessage(pair.Key, player.UserIDString), font : CUI.Font.RobotoCondensedBold, fontSize : 12, align : TextAnchor.MiddleLeft, anchorMin : "0 1", anchorMax : "0 1", offsetMin : $"10 {-10 + (-18 * i)}", offsetMax : $"210 {-10 + (-18 * i + 10)}", parent : statsContainer);
cui.AddText(text : $"{pair.Value}%", font : CUI.Font.RobotoCondensedRegular, fontSize : 12, align : TextAnchor.MiddleCenter, anchorMin : "0 1", anchorMax : "0 1", offsetMin : $"215 {-10 + (-18 * i)}", offsetMax : $"280 {-10 + (-18 * i + 10)}", parent : statsContainer);
}
cui.RenderWithDestroy(player);
}
[ConsoleCommand("customizableprotection.ui")]
private void ui(ConsoleSystem.Arg arg) {
if (arg?.Args?.Count() == 0 || arg.Player() == null) return;
BasePlayer player = arg.Player();
switch (arg.Args[0]) {
case "open" : DestroyUI(player);
try {
if (player.inventory.loot.entitySource == RelationshipManager.ServerInstance) player.inventory.loot.containers.Where(c => c.entityOwner == RelationshipManager.ServerInstance && c.capacity == 0).ToList().ForEach(c => c.Kill());
}
catch {
}
OpenEmptyContainer(player);
SetupInventorySelector(player);
break;
case "view" : ShowItemInfo(player, Convert.ToUInt64(arg.Args[1]));
break;
}
}
void OpenEmptyContainer(BasePlayer player) {
ItemContainer container = new ItemContainer();
container.isServer = true;
container.flags = NoSortify;
container.allowedContents = ItemContainer.ContentsType.Generic;
container.maxStackSize = 0;
container.capacity = 0;
container.entityOwner = RelationshipManager.ServerInstance;
container.SetFlag(ItemContainer.Flag.NoItemInput, true);
container.GiveUID();
player.inventory.loot.Clear();
player.inventory.loot.PositionChecks = false;
player.inventory.loot.entitySource = container.entityOwner;
player.inventory.loot.itemSource = null;
player.inventory.loot.MarkDirty();
player.inventory.loot.AddContainer(container);
player.inventory.loot.SendImmediate();
player.ClientRPCPlayer(null, player, "RPC_OpenLootPanel", "generic_resizable");
}
// ... [Code preview truncated due to length. Full source in attached file] ...