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 System.IO;
using Oxide.Ext.RogueRust.Plugins;
using Oxide.Ext.RogueRust.SDK;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("RogueRustFurnaceSplitter", "RogueAssassin", "2.1.3")]
[Description("Optimized RogueRust furnace splitting with automatic fuel management, configurable stack distribution, ETA display, and responsive UI.")]
public sealed class RogueRustFurnaceSplitter : RogueRustPlugin
{
private const string PluginVersion = "2.1.3";
private static readonly VersionNumber CurrentVersion = new VersionNumber(2, 1, 3);
[PluginReference]
private Plugin UIScaleManager;
private class OvenSlot
{
/// The item in this slot. May be null.
public Item Item;
/// The slot position
public int? Position;
/// The slot's index in the itemList list.
public int Index;
/// How much should be added/removed from stack
public int DeltaAmount;
}
public class OvenInfo
{
public float ETA;
public float FuelNeeded;
}
private class StoredData
{
public Dictionary AllPlayerOptions { get; private set; } = new Dictionary();
}
private class PlayerOptions
{
public bool Enabled;
public Dictionary TotalStacks = new Dictionary();
}
public enum MoveResult
{
Ok,
SlotsFilled,
NotEnoughSlots
}
private StoredData storedData = new StoredData();
private Dictionary allPlayerOptions => storedData.AllPlayerOptions;
private Dictionary initialStackOptions = new Dictionary();
private PluginConfig config;
[RoguePermission]
private const string permUse = "roguerustfurnacesplitter.use";
private const string legacyPermUse = "furnacesplitter.use";
private const string PlayerDataKey = "RogueRustFurnaceSplitter/player-options";
private readonly Dictionary openUis = new Dictionary();
private readonly Dictionary> looters = new Dictionary>();
private readonly Queue queuedUiUpdates = new Queue();
private readonly HashSet queuedUiUpdateSet = new HashSet();
private void Init()
{
LoadLanguageFiles();
LogInformation("Lifecycle",
$"{Name} {PluginVersion} initialized. " +
$"Config={Name}.json; Data=RogueRust/RogueRustFurnaceSplitter/; Lang=/RogueRust/RogueRustFurnaceSplitter/messages.json");
}
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();
if (oven != null && oven.allowByproductCreation) // ignore pumpkins, lanterns, etc
{
if (!initialStackOptions.ContainsKey(oven.ShortPrefabName))
{
//Puts($"Add oven [{oven.ShortPrefabName}] - fuelSlots: {oven.fuelSlots}, inputSlots: {oven.inputSlots}, outputSlots: {oven.outputSlots}");
initialStackOptions[oven.ShortPrefabName] = oven.inputSlots;
}
if (!config.ovens.ContainsKey(oven.ShortPrefabName))
{
config.ovens[oven.ShortPrefabName] = new PluginConfig.OvenConfig {
enabled = false, // global '*' is enabled by default, disable all oven configs
fuelMultiplier = 1.0f
};
saveCfg = true;
}
}
}
if (saveCfg)
SaveConfig();
Unsubscribe(nameof(OnServerSave));
if (config.savePlayerData)
{
storedData = LoadData(PlayerDataKey, delegate { return new StoredData(); }) ?? new StoredData();
MigrateLegacyPlayerData();
Subscribe(nameof(OnServerSave));
}
}
private void OnPlayerDisconnected(BasePlayer player, string reason)
{
DestroyUI(player);
}
private void OnServerSave()
{
// The world save already costs a frame; write this file on a worker thread (RogueRust 4.4.60+).
SaveData(background: true);
}
private void SaveData(bool background = false)
{
if (!config.savePlayerData) return;
if (background) SaveDataInBackground(PlayerDataKey, storedData);
else SaveData(PlayerDataKey, storedData);
}
private void InitPlayer(BasePlayer player)
{
PlayerOptions options;
if (!allPlayerOptions.TryGetValue(player.userID, out options))
{
options = new PlayerOptions
{
Enabled = true,
TotalStacks = new Dictionary()
};
allPlayerOptions[player.userID] = options;
}
foreach (var kv in initialStackOptions)
{
if (!options.TotalStacks.ContainsKey(kv.Key))
options.TotalStacks.Add(kv.Key, kv.Value);
}
}
private bool _uiUpdateScheduled;
private void ProcessQueuedUiUpdates()
{
_uiUpdateScheduled = false;
using (Measure("RogueRustFurnaceSplitter", "UiUpdateBatch"))
{
int processed = 0;
const int batchSize = 12;
while (queuedUiUpdates.Count > 0 && processed < batchSize)
{
BaseOven oven = queuedUiUpdates.Dequeue();
queuedUiUpdateSet.Remove(oven);
if (!oven || oven.IsDestroyed)
{
processed++;
continue;
}
OvenInfo ovenInfo = GetOvenInfo(oven);
List ovenLooters = GetLooters(oven);
if (ovenLooters != null)
{
for (int i = 0; i < ovenLooters.Count; i++)
{
BasePlayer player = ovenLooters[i];
if (player != null && !player.IsDestroyed && HasPermission(player) && GetEnabled(player))
CreateUi(player, oven, ovenInfo);
}
}
processed++;
}
}
if (queuedUiUpdates.Count > 0)
ScheduleUiUpdateProcessing();
}
private void ScheduleUiUpdateProcessing()
{
if (_uiUpdateScheduled) return;
_uiUpdateScheduled = true;
Delay(TimeSpan.Zero, ProcessQueuedUiUpdates, "furnacesplitter-ui-update");
}
public OvenInfo GetOvenInfo(BaseOven oven)
{
OvenInfo result = new OvenInfo();
PluginConfig.OvenConfig ovenCfg = GetOvenConfig(oven.ShortPrefabName);
float ETA = GetTotalSmeltTime(oven) / oven.smeltSpeed;
if (oven.fuelType != null)
{
float fuelMultiplier = ovenCfg != null ? ovenCfg.fuelMultiplier : 1.0f;
float fuelUnits = oven.fuelType.GetComponent().fuelAmount;
float neededFuel = (float)Math.Ceiling(ETA * (oven.cookingTemperature / 200.0f) / fuelUnits);
result.FuelNeeded = neededFuel * fuelMultiplier;
}
result.ETA = ETA;
return result;
}
private void Unload()
{
_uiUpdateScheduled = false;
queuedUiUpdates.Clear();
queuedUiUpdateSet.Clear();
looters.Clear();
SaveData();
if (openUis.Count > 0)
{
ulong[] playerIds = openUis.Keys.ToArray();
for (int i = 0; i < playerIds.Length; i++)
DestroyUI(BasePlayer.FindByID(playerIds[i]));
}
}
private bool GetEnabled(BasePlayer player)
{
PlayerOptions options;
if (!allPlayerOptions.TryGetValue(player.userID, out options))
{
InitPlayer(player);
options = allPlayerOptions[player.userID];
}
return options.Enabled;
}
private void SetEnabled(BasePlayer player, bool enabled)
{
PlayerOptions options;
if (!allPlayerOptions.TryGetValue(player.userID, out options))
{
InitPlayer(player);
options = allPlayerOptions[player.userID];
}
options.Enabled = enabled;
CreateUiIfFurnaceOpen(player);
Debounce("furnacesplitter-data-save", TimeSpan.FromSeconds(2), () => SaveData(background: true));
}
private bool IsSlotCompatible(Item item, BaseOven oven, ItemDefinition itemDefinition)
{
ItemModCookable cookable = item.info.GetComponent();
if (item.amount < item.info.stackable && item.info == itemDefinition)
return true;
if (oven.allowByproductCreation && oven.fuelType.GetComponent().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)
{
// Fires once per fuel tick for every burning oven/furnace/campfire on the server. The queued
// update only refreshes the looter panel, so skip ovens nobody is looking at.
if (oven == null || looters.Count == 0 || !looters.ContainsKey(oven))
return;
if (IsOvenCompatible(oven))
QueueUiUpdate(oven);
}
private void QueueUiUpdate(BaseOven oven)
{
if (oven == null || oven.IsDestroyed || !queuedUiUpdateSet.Add(oven)) return;
queuedUiUpdates.Enqueue(oven);
ScheduleUiUpdateProcessing();
}
private List GetLooters(BaseOven oven)
{
List players;
return looters.TryGetValue(oven, out players) ? players : null;
}
private void AddLooter(BaseOven oven, BasePlayer player)
{
List list;
if (!looters.TryGetValue(oven, out list))
{
list = new List();
looters[oven] = list;
}
if (!list.Contains(player))
list.Add(player);
}
private void RemoveLooter(BaseOven oven, BasePlayer player)
{
List list;
if (!looters.TryGetValue(oven, out list))
return;
list.Remove(player);
if (list.Count == 0)
looters.Remove(oven);
}
private object CanMoveItem(Item item, PlayerInventory inventory, ItemContainerId targetContainerId, int targetSlotIndex, int splitAmount)
{
if (item == null || inventory == null)
return null;
BasePlayer player = inventory.baseEntity;
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; // ignore moving items within player inventory or container that is not an oven
ItemContainer container = oven.inventory;
ItemContainer originalContainer = item.GetRootContainer();
if (container == null || originalContainer == null || originalContainer?.entityOwner is BaseOven)
return null; // ignore invalid container or moving items within oven
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; // ignore splitting for different item types but allow item to be added to oven
}
Func