using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Furnace Splitter", "FastBurst&fix_Nzych", "2.5.5")]
[Description("Splits up resources in furnaces automatically and shows useful furnace information")]
public class FurnaceSplitter : RustPlugin
{
[PluginReference]
private Plugin UIScaleManager;
private class OvenSlot
{
public Item Item;
public int? Position;
public int Index;
public int DeltaAmount;
}
public class OvenInfo
{
public float ETA;
public float FuelNeeded;
}
private class StoredData
{
public Dictionary<ulong, PlayerOptions> AllPlayerOptions { get; private set; } = new Dictionary<ulong, PlayerOptions>();
}
private class PlayerOptions
{
public bool Enabled;
public Dictionary<string, int> TotalStacks = new Dictionary<string, int>();
}
public enum MoveResult
{
Ok,
SlotsFilled,
NotEnoughSlots
}
private StoredData storedData = new StoredData();
private Dictionary<ulong, PlayerOptions> allPlayerOptions => storedData.AllPlayerOptions;
private Dictionary<string, int> initialStackOptions = new Dictionary<string, int>();
private PluginConfig config;
private const string permUse = "furnacesplitter.use";
private readonly Dictionary<ulong, string> openUis = new Dictionary<ulong, string>();
private readonly Dictionary<BaseOven, List<BasePlayer>> looters = new Dictionary<BaseOven, List<BasePlayer>>();
private readonly Stack<BaseOven> queuedUiUpdates = new Stack<BaseOven>();
private void Init()
{
permission.RegisterPermission(permUse, this);
}
private void OnServerInitialized()
{
var saveCfg = false;
foreach (var prefab in GameManifest.Current.entities)
{
var gameObj = GameManager.server.FindPrefab(prefab);
if (gameObj == null) continue;
var oven = gameObj.GetComponent<BaseOven>();
if (oven != null && oven.allowByproductCreation)
{
if (!initialStackOptions.ContainsKey(oven.ShortPrefabName))
{
initialStackOptions[oven.ShortPrefabName] = oven.inputSlots;
}
if (!config.ovens.ContainsKey(oven.ShortPrefabName))
{
config.ovens[oven.ShortPrefabName] = new PluginConfig.OvenConfig {
enabled = false,
fuelMultiplier = 1.0f
};
saveCfg = true;
}
}
}
if (saveCfg)
SaveConfig();
Unsubscribe(nameof(OnServerSave));
if (config.savePlayerData)
{
storedData = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name) ?? new StoredData();
Subscribe(nameof(OnServerSave));
}
}
private void OnPlayerDisconnected(BasePlayer player, string reason)
{
DestroyUI(player);
}
private void OnServerSave()
{
SaveData();
}
private void SaveData()
{
if (!config.savePlayerData) return;
Interface.Oxide.DataFileSystem.WriteObject(Name, storedData);
}
private void InitPlayer(BasePlayer player)
{
if (!allPlayerOptions.ContainsKey(player.userID))
{
allPlayerOptions[player.userID] = new PlayerOptions
{
Enabled = true,
TotalStacks = new Dictionary<string, int>()
};
}
PlayerOptions options = allPlayerOptions[player.userID];
foreach (var kv in initialStackOptions)
{
if (!options.TotalStacks.ContainsKey(kv.Key))
options.TotalStacks.Add(kv.Key, kv.Value);
}
}
private void OnTick()
{
while (queuedUiUpdates.Count > 0)
{
BaseOven oven = queuedUiUpdates.Pop();
// Фикс: добавлена проверка на null и IsDestroyed
if (oven == null || oven.IsDestroyed)
continue;
// Фикс: безопасный вызов GetOvenInfo с try-catch
OvenInfo ovenInfo;
try
{
ovenInfo = GetOvenInfo(oven);
}
catch (Exception ex)
{
PrintWarning($"GetOvenInfo failed for {oven.ShortPrefabName}: {ex.Message}");
continue;
}
GetLooters(oven)?.ForEach(player =>
{
if (player != null && !player.IsDestroyed && HasPermission(player) && GetEnabled(player))
{
CreateUi(player, oven, ovenInfo);
}
});
}
}
public OvenInfo GetOvenInfo(BaseOven oven)
{
OvenInfo result = new OvenInfo();
if (oven.fuelType == null)
{
result.ETA = 0f;
result.FuelNeeded = 0f;
return result;
}
PluginConfig.OvenConfig ovenCfg = GetOvenConfig(oven.ShortPrefabName);
float fuelMultiplier = ovenCfg != null ? ovenCfg.fuelMultiplier : 1.0f;
float smeltSpeed = oven.smeltSpeed > 0 ? oven.smeltSpeed : 1f;
float ETA = GetTotalSmeltTime(oven) / smeltSpeed;
ItemModBurnable burnable = oven.fuelType.ItemModBurnable; // БЫЛО: oven.fuelType.GetComponent<ItemModBurnable>()
if (burnable == null)
{
result.ETA = ETA;
result.FuelNeeded = 0f;
return result;
}
float fuelUnits = burnable.fuelAmount;
if (fuelUnits <= 0f)
{
result.ETA = ETA;
result.FuelNeeded = 0f;
return result;
}
float neededFuel = (float)Math.Ceiling(ETA * (oven.cookingTemperature / 200.0f) / fuelUnits);
result.FuelNeeded = neededFuel * fuelMultiplier;
result.ETA = ETA;
return result;
}
private void Unload()
{
SaveData();
foreach (var kv in openUis.ToDictionary(kv => kv.Key, kv => kv.Value))
{
BasePlayer player = BasePlayer.FindByID(kv.Key);
DestroyUI(player);
}
}
private bool GetEnabled(BasePlayer player)
{
if (!allPlayerOptions.ContainsKey(player.userID))
InitPlayer(player);
return allPlayerOptions[player.userID].Enabled;
}
private void SetEnabled(BasePlayer player, bool enabled)
{
if (allPlayerOptions.ContainsKey(player.userID))
allPlayerOptions[player.userID].Enabled = enabled;
CreateUiIfFurnaceOpen(player);
}
private bool IsSlotCompatible(Item item, BaseOven oven, ItemDefinition itemDefinition)
{
var cookable = item.info.ItemModCookable; // БЫЛО: item.info.GetComponent<ItemModCookable>()
if (item.amount < item.info.stackable && item.info == itemDefinition)
return true;
// БЫЛО: oven.fuelType.GetComponent<ItemModBurnable>()?.byproductItem
if (oven.allowByproductCreation && oven.fuelType != null && oven.fuelType.ItemModBurnable?.byproductItem == item.info)
return true;
if (cookable == null || cookable.becomeOnCooked == itemDefinition)
return true;
if (CanCook(cookable, oven))
return true;
return false;
}
private void OnFuelConsume(BaseOven oven, Item fuel, ItemModBurnable burnable)
{
if (IsOvenCompatible(oven))
queuedUiUpdates.Push(oven);
}
private List<BasePlayer> GetLooters(BaseOven oven)
{
if (looters.ContainsKey(oven))
return looters[oven];
return null;
}
private void AddLooter(BaseOven oven, BasePlayer player)
{
if (!looters.ContainsKey(oven))
looters[oven] = new List<BasePlayer>();
var list = looters[oven];
list.Add(player);
}
private void RemoveLooter(BaseOven oven, BasePlayer player)
{
if (!looters.ContainsKey(oven))
return;
looters[oven].Remove(player);
}
private object CanMoveItem(Item item, PlayerInventory inventory, ItemContainerId targetContainerId, int targetSlotIndex, int splitAmount)
{
if (item == null || inventory == null)
return null;
BasePlayer player = inventory.GetComponent<BasePlayer>();
if (player == null)
return null;
BaseOven oven = inventory.loot.entitySource as BaseOven;
if (oven == null)
return null;
ItemContainer targetContainer = inventory.FindContainer(targetContainerId);
if (targetContainer != null && !(targetContainer?.entityOwner is BaseOven))
return null;
ItemContainer container = oven.inventory;
ItemContainer originalContainer = item.GetRootContainer();
if (container == null || originalContainer == null || originalContainer?.entityOwner is BaseOven)
return null;
BaseOven.MinMax? allowedSlots = oven.GetAllowedSlots(item);
if (allowedSlots == null)
return null;
for (int i = allowedSlots.Value.Min; i <= allowedSlots.Value.Max; i++)
{
Item slot = oven.inventory.GetSlot(i);
if (slot != null && slot.info.shortname != item.info.shortname)
return null;
}
Func<object> splitFunc = () =>
{
if (player == null || !HasPermission(player) || !GetEnabled(player))
return null;
PlayerOptions playerOptions = allPlayerOptions[player.userID];
if (container == null || originalContainer == null || container == item.GetRootContainer())
return null;
var cookable = item.info.ItemModCookable; // БЫЛО: ItemModCookable cookable = item.info.GetComponent<ItemModCookable>();
if (oven == null || cookable == null || IsOutputItem(oven, item))
return null;
int totalSlots = oven.inputSlots;
if (playerOptions.TotalStacks.ContainsKey(oven.ShortPrefabName))
{
totalSlots = playerOptions.TotalStacks[oven.ShortPrefabName];
}
if (cookable.lowTemp > oven.cookingTemperature || cookable.highTemp < oven.cookingTemperature)
return null;
MoveSplitItem(item, oven, totalSlots, splitAmount);
return true;
};
object returnValue = splitFunc();
if (HasPermission(player) && GetEnabled(player))
{
if (oven != null && IsOvenCompatible(oven))
{
if (returnValue is bool && (bool)returnValue)
AutoAddFuel(inventory, oven);
queuedUiUpdates.Push(oven);
}
}
return returnValue;
}
private MoveResult MoveSplitItem(Item item, BaseOven oven, int totalSlots, int splitAmount)
{
ItemContainer container = oven.inventory;
int numOreSlots = totalSlots;
int totalMoved = 0;
int itemAmount = item.amount > splitAmount ? splitAmount : item.amount;
int totalAmount = Math.Min(itemAmount + container.itemList.Where(slotItem => slotItem.info == item.info).Take(numOreSlots).Sum(slotItem => slotItem.amount), Math.Abs(item.info.stackable * numOreSlots));
if (numOreSlots <= 0)
{
return MoveResult.NotEnoughSlots;
}
int totalStackSize = Math.Min(totalAmount / numOreSlots, item.info.stackable);
int remaining = totalAmount - totalAmount / numOreSlots * numOreSlots;
List<int> addedSlots = new List<int>();
List<OvenSlot> ovenSlots = new List<OvenSlot>();
for (int i = 0; i < numOreSlots; ++i)
{
Item existingItem;
int slot = FindMatchingSlotIndex(oven, container, out existingItem, item.info, addedSlots);
if (slot == -1)
{
return MoveResult.NotEnoughSlots;
}
addedSlots.Add(slot);
OvenSlot ovenSlot = new OvenSlot
{
Position = existingItem?.position,
Index = slot,
Item = existingItem
};
int currentAmount = existingItem?.amount ?? 0;
int missingAmount = totalStackSize - currentAmount + (i < remaining ? 1 : 0);
ovenSlot.DeltaAmount = missingAmount;
if (currentAmount + missingAmount <= 0)
continue;
ovenSlots.Add(ovenSlot);
}
foreach (OvenSlot slot in ovenSlots)
{
if (slot.Item == null)
{
Item newItem = ItemManager.Create(item.info, slot.DeltaAmount, item.skin);
slot.Item = newItem;
newItem.MoveToContainer(container, slot.Position ?? slot.Index);
}
else
{
slot.Item.amount += slot.DeltaAmount;
}
totalMoved += slot.DeltaAmount;
}
container.MarkDirty();
if (totalMoved >= item.amount)
{
item.Remove();
item.GetRootContainer()?.MarkDirty();
return MoveResult.Ok;
}
else
{
item.amount -= totalMoved;
item.GetRootContainer()?.MarkDirty();
return MoveResult.SlotsFilled;
}
}
private void AutoAddFuel(PlayerInventory playerInventory, BaseOven oven)
{
// Фикс: проверка на null для fuelType
if (oven.fuelType == null) return;
int neededFuel = (int)Math.Ceiling(GetOvenInfo(oven).FuelNeeded);
neededFuel -= oven.inventory.GetAmount(oven.fuelType.itemid, false);
List<Item> playerFuel = Facepunch.Pool.Get<List<Item>>();
try
{
playerInventory.FindItemsByItemID(playerFuel, oven.fuelType.itemid);
int fuelSlotIndex = 0;
if (neededFuel <= 0 || playerFuel.Count <= 0)
return;
foreach (Item fuelItem in playerFuel)
{
var existingFuel = oven.inventory.GetSlot(fuelSlotIndex);
if (existingFuel != null && existingFuel.amount >= existingFuel.info.stackable)
{
if (fuelSlotIndex < oven.fuelSlots)
fuelSlotIndex++;
else
break;
}
Item largestFuelStack = oven.inventory.itemList.Where(item => item.info == oven.fuelType).OrderByDescending(item => item.amount).FirstOrDefault();
int toTake = Math.Min(neededFuel, (oven.fuelType.stackable * oven.fuelSlots) - (largestFuelStack?.amount ?? 0));
if (toTake > fuelItem.amount)
toTake = fuelItem.amount;
if (toTake <= 0)
break;
neededFuel -= toTake;
int currentFuelAmount = oven.inventory.GetAmount(oven.fuelType.itemid, false);
if (currentFuelAmount >= oven.fuelType.stackable * oven.fuelSlots)
break;
if (toTake >= fuelItem.amount)
{
fuelItem.MoveToContainer(oven.inventory, fuelSlotIndex);
}
else
{
Item splitItem = fuelItem.SplitItem(toTake);
if (!splitItem.MoveToContainer(oven.inventory, fuelSlotIndex))
break;
}
if (neededFuel <= 0)
break;
}
}
finally
{
Facepunch.Pool.Free(ref playerFuel);
}
}
private int FindMatchingSlotIndex(BaseOven oven, ItemContainer container, out Item existingItem, ItemDefinition itemType, List<int> indexBlacklist)
{
existingItem = null;
int firstIndex = -1;
int inputSlotsMin = oven._inputSlotIndex;
int inputSlotsMax = oven._inputSlotIndex + oven.inputSlots;
Dictionary<int, Item> existingItems = new Dictionary<int, Item>();
for (int i = inputSlotsMin; i < inputSlotsMax; ++i)
{
if (indexBlacklist.Contains(i))
continue;
Item itemSlot = container.GetSlot(i);
if (itemSlot == null || itemType != null && itemSlot.info == itemType)
{
if (itemSlot != null)
existingItems.Add(i, itemSlot);
if (firstIndex == -1)
{
existingItem = itemSlot;
firstIndex = i;
}
}
}
if (existingItems.Count <= 0 && firstIndex != -1)
{
return firstIndex;
}
else if (existingItems.Count > 0)
{
var largestStackItem = existingItems.OrderByDescending(kv => kv.Value.amount).First();
existingItem = largestStackItem.Value;
return existingItem.position;
}
existingItem = null;
return -1;
}
private void OnLootEntity(BasePlayer player, BaseEntity entity)
{
BaseOven oven = entity as BaseOven;
if (oven == null || !HasPermission(player) || !IsOvenCompatible(oven))
return;
AddLooter(oven, player);
if (GetEnabled(player))
queuedUiUpdates.Push(oven);
else
CreateUi(player, oven, new OvenInfo());
}
private void OnLootEntityEnd(BasePlayer player, BaseCombatEntity entity)
{
BaseOven oven = entity as BaseOven;
if (oven == null || !IsOvenCompatible(oven))
return;
DestroyUI(player);
RemoveLooter(oven, player);
}
private void OnEntityKill(BaseNetworkable networkable)
{
BaseOven oven = networkable as BaseOven;
if (oven != null)
{
DestroyOvenUI(oven);
}
}
private void OnOvenToggle(BaseOven oven, BasePlayer player)
{
if (IsOvenCompatible(oven))
queuedUiUpdates.Push(oven);
}
private void CreateUiIfFurnaceOpen(BasePlayer player)
{
BaseOven oven = player.inventory.loot?.entitySource as BaseOven;
if (oven != null && IsOvenCompatible(oven))
queuedUiUpdates.Push(oven);
}
private CuiElementContainer CreateUi(BasePlayer player, BaseOven oven, OvenInfo ovenInfo)
{
PlayerOptions options = allPlayerOptions[player.userID];
int totalSlots = GetTotalStacksOption(player, oven) ?? oven.inputSlots;
string remainingTimeStr;
string neededFuelStr;
if (ovenInfo.ETA <= 0)
{
remainingTimeStr = "0s";
neededFuelStr = "0";
}
else
{
remainingTimeStr = FormatTime(ovenInfo.ETA);
neededFuelStr = ovenInfo.FuelNeeded.ToString("##,###");
}
float uiScale = 1.0f;
float[] playerUiInfo = UIScaleManager?.Call<float[]>("API_CheckPlayerUIInfo", player.UserIDString);
if (playerUiInfo?.Length > 0)
{
uiScale = playerUiInfo[2];
}
string contentColor = "0.7 0.7 0.7 1.0";
int contentSize = Convert.ToInt32(10 * uiScale);
string toggleStateStr = (!options.Enabled).ToString();
string toggleButtonColor = !options.Enabled
? "0.415 0.5 0.258 0.4"
: "0.8 0.254 0.254 0.4";
string toggleButtonTextColor = !options.Enabled
? "0.607 0.705 0.431"
: "0.705 0.607 0.431";
string buttonColor = "0.75 0.75 0.75 0.1";
string buttonTextColor = "0.77 0.68 0.68 1";
int nextDecrementSlot = totalSlots - 1;
int nextIncrementSlot = totalSlots + 1;
DestroyUI(player);
Vector2 uiPosition = new Vector2(
((((config.UiPosition.x) - 0.5f) * uiScale) + 0.5f),
(config.UiPosition.y - 0.02f) + 0.02f * uiScale);
Vector2 uiSize = new Vector2(0.1785f * uiScale, 0.111f * uiScale);
CuiElementContainer result = new CuiElementContainer();
string rootPanelName = result.Add(new CuiPanel
{
Image = new CuiImageComponent
{
Color = "0 0 0 0"
},
RectTransform =
{
AnchorMin = uiPosition.x + " " + uiPosition.y,
AnchorMax = uiPosition.x + uiSize.x + " " + (uiPosition.y + uiSize.y)
}
}, "Hud.Menu");
string headerPanel = result.Add(new CuiPanel
{
Image = new CuiImageComponent
{
Color = "0.75 0.75 0.75 0.1"
},
RectTransform =
{
AnchorMin = "0 0.775",
AnchorMax = "1 1"
}
}, rootPanelName);
result.Add(new CuiLabel
{
RectTransform =
{
AnchorMin = "0.051 0",
AnchorMax = "1 0.95"
},
Text =
{
Text = lang.GetMessage("title", this, player.UserIDString),
Align = TextAnchor.MiddleLeft,
Color = "0.77 0.7 0.7 1",
FontSize = Convert.ToInt32(13 * uiScale)
}
}, headerPanel);
string contentPanel = result.Add(new CuiPanel
{
Image = new CuiImageComponent
{
Color = "0.65 0.65 0.65 0.06"
},
RectTransform =
{
AnchorMin = "0 0",
AnchorMax = "1 0.74"
}
}, rootPanelName);
// Фикс: защита от null для fuelType.displayName
string fuelName = oven.fuelType != null ? oven.fuelType.displayName.english.ToLower() : "fuel";
result.Add(new CuiLabel
{
RectTransform =
{
AnchorMin = "0.02 0.7",
AnchorMax = "0.98 1"
},
Text =
{
Text = string.Format("{0}: " + (ovenInfo.ETA > 0 ? "~" : "") + remainingTimeStr + " (" + neededFuelStr + " " + fuelName + ")", lang.GetMessage("eta", this, player.UserIDString)),
Align = TextAnchor.MiddleLeft,
Color = contentColor,
FontSize = contentSize
}
}, contentPanel);
result.Add(new CuiButton
{
RectTransform =
{
AnchorMin = "0.02 0.4",
AnchorMax = "0.25 0.7"
},
Button =
{
Command = "furnacesplitter.enabled " + toggleStateStr,
Color = toggleButtonColor
},
Text =
{
Align = TextAnchor.MiddleCenter,
Text = options.Enabled ? lang.GetMessage("turnoff", this, player.UserIDString) : lang.GetMessage("turnon", this, player.UserIDString),
Color = toggleButtonTextColor,
FontSize = Convert.ToInt32(11 * uiScale)
}
}, contentPanel);
result.Add(new CuiButton
{
RectTransform =
{
AnchorMin = "0.27 0.4",
AnchorMax = "0.52 0.7"
},
Button =
{
Command = "furnacesplitter.trim",
Color = buttonColor
},
Text =
{
Align = TextAnchor.MiddleCenter,
Text = lang.GetMessage("trim", this, player.UserIDString),
Color = contentColor,
FontSize = Convert.ToInt32(11 * uiScale)
}
}, contentPanel);
result.Add(new CuiButton
{
RectTransform =
{
AnchorMin = "0.02 0.05",
AnchorMax = "0.07 0.35"
},
Button =
{
Command = "furnacesplitter.totalstacks " + nextDecrementSlot,
Color = buttonColor
},
Text =
{
Align = TextAnchor.MiddleCenter,
Text = "<",
Color = buttonTextColor,
FontSize = contentSize
}
}, contentPanel);
result.Add(new CuiLabel
{
RectTransform =
{
AnchorMin = "0.08 0.05",
AnchorMax = "0.19 0.35"
},
Text =
{
Align = TextAnchor.MiddleCenter,
Text = totalSlots.ToString(),
Color = contentColor,
FontSize = contentSize
}
}, contentPanel);
result.Add(new CuiButton
{
RectTransform =
{
AnchorMin = "0.19 0.05",
AnchorMax = "0.25 0.35"
},
Button =
{
Command = "furnacesplitter.totalstacks " + nextIncrementSlot,
Color = buttonColor
},
Text =
{
Align = TextAnchor.MiddleCenter,
Text = ">",
Color = buttonTextColor,
FontSize = contentSize
}
}, contentPanel);
result.Add(new CuiLabel
{
RectTransform =
{
AnchorMin = "0.27 0.05",
AnchorMax = "1 0.35"
},
Text =
{
Align = TextAnchor.MiddleLeft,
Text = string.Format("({0})", lang.GetMessage("totalstacks", this, player.UserIDString)),
Color = contentColor,
FontSize = contentSize
}
}, contentPanel);
openUis.Add(player.userID, rootPanelName);
CuiHelper.AddUi(player, result);
return result;
}
private string FormatTime(float totalSeconds)
{
int hours = (int)Math.Floor(totalSeconds / 3600);
int minutes = (int)Math.Floor(totalSeconds / 60 % 60);
int seconds = (int)Math.Floor(totalSeconds % 60);
if (hours <= 0 && minutes <= 0)
return seconds + "s";