C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
//#define HOOKTIME_DEBUG
using Facepunch;
using HarmonyLib;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using Oxide.Plugins.LoottableExtensions;
using ProtoBuf;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;
using static TrainCarUnloadable;
using StaticLootableModels = Oxide.Plugins.LoottableExtensions.StaticLootableModels;
#if CARBON
using Carbon.Base;
using Carbon.Modules;
#endif
/**
* VERSION HISTORY
*
* V 1.0.1
* - replaced Find Objects Of Type with BaseNetworkable.serverEntities.Find
*
* V 1.0.2
* - added gui item selection
* - auto refresh loot after closing the editor
* - added gather manager ui
* - fixed editor sometimes not opening bug
* - loot distribution rework
* - stacksize bugfixes
*
* V 1.0.3
* - fixed category item distribution
* - item.stackable now adjusts with stacksize
* - fixed item amount in crate config ignored
*
* V 1.0.4
* - updated input fields to work with latest oxide version
* - changed input field clamping to match min/max values
* - fixed npc corpse custom loot profiles
* - added gather rates for animals
*
* V 1.0.5
* - fixed plugin not loading on server startup
* - fixed players without admin permission unable to open editor
*
* V 1.0.6
* - fixed invalid cast exception when adjusting item count of npcs
* - added page navigation to crate loot editor
* - added hooks for container population
* - fixed invisible cursor in some input fields
* - improved image loading
*
* V 1.0.7
* - added flags to bypass editor restrictions
* - added addition loot option
* - added default loot config for npcs and some crates
* - added the option to import loot tables
* - fixed a bug where the despawn time of a locked crate would be reset upon reload
*
* V 1.0.7 HOTFIX / V 1.0.8
* - fixed plugin not working after Rust update
*
* V 1.0.9
* - fixed several stacksize related bugs
* - added spray can decal item to ignore list
* - excluded player quarries form quarry config
* - fixed max item amount of crates ignored in some cases
* - loot distribution improvements (again)
* - fixed editor not opening without image library
* - removed stacking of items w/ durability field as it caused confusion
* - replaced GatherControl and StacksizeControl lootable with GenericLootable
* - wear container now only accepts 1 item (no stacked clothes)
* - heli fuel can now be adjusted in the editor
* - smelting speed can now be controlled in the editor
* - updated images for some crates
*
* V 1.0.10
* - added bbq and campfire to smelting config
* - new icons for mini and srappy fuel config
* - added the option to multiply an entire loot table
* - slightly changed category section to look better
* - fixed invalid cast exception when editing category item amounts
* - fixed typo in 'UnlockGahterMultiplier' that caused an error when changing gather multipliers
* - fixed recycler not stopping when items are removed
* - fixed a bug where too much items are in a crate (again)
* - allowed multiple items with same itemid but different skin id
* - added the option to add extra items to an item
* - added air drop config options
* - added scientistnpc_roam to npc config
* - fixed gather items not droping when inventory is full
* - added support for static lootables (wip)
* - disable quick smelt if speed multiplier is 1
* - added category stack size multipliers
*
* V 1.0.11
* - fixed plugin not initializing
*
* V 1.0.12
* - fixed server freezing (caused by infinite loop)
* - fixed unable to edit gather items
* - items can now have drop chances as low as 0.01% (was 1% before)
* - added the option to multiply items of collectables
* - added the option to load the default config for collectables
* - improved loot distribution
* - added indicator for extra items
*
* V 1.0.13
* - fixed unable to edit excavator/quarry items
* - allowed multiple items in excavator config
* - support for static lootables (beta)
* - changed ui parent from hud.menu to overlay
* - fixed recycler bugs
* - added mixing table to smelting speed config
* - added drop chances for gather items
*
* V 1.0.14
* - fixed some typos
* - fixed missing delete button when editing items
* - fixed supply drop bug with loot defender (and fancy drop)
* - added charcoal config for furnaces
* - more config options for static lootables (wip)
*
* V 1.0.15
* - removed deprecated hook OnCollectiblePickup and replaced it
* - cleaned up api code
* - fixed weird console/debug message behavior
* - fixed null reference exception when populating npc corpses
* - added invisible crates
* - slightly changed loot refresh function and included loot wagons
* - added train wagon loot config
*
* V 1.0.16
* - fixed null reference exception in InitializeEditorConfig
* - fixed null reference exception in OnCollectiblePickup
* - added locomotive to list of ignored items
* - added support for custom items
* - fixed issue with xlevels OnCollectible pickup
* - fixed issue when splitting clones
* - fixed issues with simple splitter
* - added hook OnCargoPlaneSignaled to custom supply drops
* - fixed null reference exception when calling a supply drop
*
* V 1.0.17
* - patched for rust update
*
* V 1.0.18
* - fixed problems caused by update
* - re-enabled supply drop
* - re-enabled mixing table
* - fixed npc loot issues
* - fixed error when creating items
* - fixed null reference exception in OnEntitySpawned
*
* V 1.0.19
* - fix IndexOutOfRangeException when selecting last category in stack size editor
* - added TracedAction to find origin of NullReferenceException in timer callback in OnEntitySpawned(LootableCorpse corpse)
* - fixed furnaces not working after server reboot
* - fixed stacking of world items with different skins
*
* V 1.0.20
* - fix bug with category stack size multiplier
* - fix error when npc corpse of BetterNpc NPC spawned
* - added flag to disable stacking hooks
*
* V 1.0.21
* - call hook OnSupplyDropDropped for custom air drops
* - added the option to disable loot refresh (should fix problems with increased entity count)
* - fixed fuel duplication glitch with miner hats
* - fixed stacking bugs with dropped items
*
* V 1.0.22
* - fix problem with BradleyDrops
* - add blueprint option for items
* - items can now have condition
* - fix blueprint stacking issues
* - furnace and airdrop config can be disabled
* - add electrical furnace to smelting config
*
* V 1.0.23
* - fix KeyNotFoundException in OnDispenserGather
* - put gahtered items in hotbar
* - fix issue where adding an item would add it as extra item
* - added gather multipliers for growable plants
*
* V 1.0.24
* - fix global gather multiplier not working for animals
* - fix furnace speed issues
* - add support for HeliSignals
* - add support for DefendableHomes
* - add support for Carbon and it's ImageDatabase
* - rework train loot configuration
* - change plugin type to RustPlugin
*
* V 1.0.25
* - change command system to work with RustPlugin
* - add warning if sandbox is enabled
*
* V 1.0.26
* - remove sandbox check
*
* V 1.0.27
* - extend custom item api
* - add the ability to create custom items in the editor
* - add page navigation for custom items
* - fix smelting speed for electric furnace
* - fix display bug with info message for train loot config
* - add validation to furnace config (only enable when SimpleSplitter is not present)
* - handle splitting of items with container (BagOfHolding compatibility)
* - remove debug message for default loot profile (no more console spam)
* - add the ability to create / load config backups
* - fix gathered items always going into hotbar
*
* V 1.0.28
* - lower min item time for quarry config from 0.1 to 0.01
* - change uint net ids to NetworkableId
*
* V 1.0.29
* - rework npc loot system
* - add npc configs for lauch site, missile silo, power plant
* - persist item.text when splitting items, don't stack items with different item.text
* - fix furnace speed not working when using igniter or button to start furnace
* - fix conflict with deployable nature
* - add support for "everything" quarry
*
* V 1.0.30
* - fix possible NRE in OnPluginLoaded
* - change MiniCopter to PlayerHelicopter
* - add AttackHelicopter to fuel config
* - fix loot additions not working properly
* - rework loot refresh to be more performant
* - misc improvements
*
* V 1.0.31
* - fix supply drop issues
* - add page navigation for static lootables
* - misc fixes/improvements
*
* V 1.0.32
* - modify OnCustomAirdrop hook to resolve issue with VehicleAirdrops
* - performance optimization for stacking hooks
* - performance optimization for npc loot hooks
* - add hook time tracker for debugging
* - npc loot system optimization - should now consume less memory
*
* V 1.0.33
* - add support for legacy furnace
* - fix error when opening ui
*
* V 1.0.34
* - replace discord images
* - fix return behavior of CanCombineDroppedItem
* - remove Conditional for carbon compatibility
* - fix issues with scrap multiplier
*
* V 1.0.35
* - fix issues with military flame thrower
*
* V 1.0.36
* - upgrade to harmony v2
*
* V 1.0.37
* - fix error with npc spawn points that could prevent plugin from loading
*
**/
namespace Oxide.Plugins {
[Info("Loottable", "The_Kiiiing", "1.0.37")]
[Description("A configurable loot table with GUI")]
public class Loottable : RustPlugin {
#region Hook time tracker
#if HOOKTIME_DEBUG
private sealed class HookTimeTracker : IDisposable {
public static readonly Dictionary<string, HookInfo> hookInfo = new();
public static float loadTime;
private readonly string hookName;
private readonly Stopwatch sw;
private List<double> breakpoints;
public HookTimeTracker(string hookName) {
this.hookName = hookName;
breakpoints = Pool.GetList<double>();
sw = Stopwatch.StartNew();
}
public void SetBreakpoint() {
var time = sw.Elapsed.TotalMilliseconds;
if (breakpoints.Count > 0) {
time -= breakpoints[^1];
}
breakpoints.Add(time);
}
public void Dispose() {
sw.Stop();
if (!hookInfo.TryGetValue(hookName, out var info)) {
info = new HookInfo(hookName, true);
hookInfo.Add(hookName, info);
}
info.AddCall(sw.Elapsed.TotalMilliseconds, breakpoints);
Pool.FreeList(ref breakpoints);
}
public class HookInfo {
public string hookName;
public int calls;
public double averageTime;
public double minTime = double.MaxValue;
public double maxTime;
public double totalTime;
public List<HookInfo> breakpoints;
public HookInfo(string hookName) : this (hookName, false) {
}
public HookInfo(string hookName, bool hasBreakpoints) {
this.hookName = hookName;
if (hasBreakpoints) {
breakpoints = new List<HookInfo>();
}
}
public void AddCall(double time, IList<double> breakpointTime) {
calls++;
averageTime = (averageTime + time) / calls;
totalTime += time;
if (time > maxTime) {
maxTime = time;
}
else if (time < minTime) {
minTime = time;
}
if (breakpoints != null && breakpointTime != null) {
for(int i = 0; i < breakpointTime.Count; i++) {
if (i < breakpoints.Count) {
breakpoints[i].AddCall(breakpointTime[i], null);
}
else {
var info = new HookInfo($"Sec {i}");
info.AddCall(breakpointTime[i], null);
breakpoints.Add(info);
}
}
}
}
}
}
[ConsoleCommand("l.hooktime")]
void CmdHookTime(ConsoleSystem.Arg arg) {
var player = arg.Player();
if (player != null && !permission.UserHasPermission(player.UserIDString, PERM_DEBUG)) {
arg.ReplyWith("No permission");
return;
}
var sb = new StringBuilder($"Hook time overview (Plugin uptime {Mathf.RoundToInt(Time.time - HookTimeTracker.loadTime)}s)");
sb.AppendLine();
sb.AppendLine(ToFixed("Hook", 35) + ToFixed("Calls") + ToFixed("Avg") + ToFixed("Min") + ToFixed("Max") + ToFixed("Total"));
foreach (var info in HookTimeTracker.hookInfo.Values) {
sb.AppendLine(ToFixed(info.hookName, 35) + ToFixed(info.calls.ToString()) + ToFixed(info.averageTime.ToString("N6")) + ToFixed(info.minTime.ToString("N6")) + ToFixed(info.maxTime.ToString("N6")) + ToFixed(info.totalTime.ToString("N2")));
if (info.breakpoints != null) foreach(var bp in info.breakpoints) {
sb.AppendLine(" " + ToFixed(bp.hookName, 31) + ToFixed(bp.calls.ToString()) + ToFixed(bp.averageTime.ToString("N6")) + ToFixed(bp.minTime.ToString("N6")) + ToFixed(bp.maxTime.ToString("N6")) + ToFixed(bp.totalTime.ToString("N2")));
}
}
arg.ReplyWith(sb.ToString());
static string ToFixed(string str, int length = 14) {
if (str.Length > length) {
str = "invalid";
}
var fill = length - str.Length;
if (fill > 0) {
return str + new string(' ', fill);
}
return str;
}
}
#endif
#endregion
#region Fields
[PluginReference]
private Plugin ImageLibrary, StaticLootables, LootDefender, FancyDrop, SimpleSplitter, BetterNpc, DefendableHomes, DeployableNature;
private static bool UseStaticLootables => (bool)_instance.StaticLootables;
private const int configVersion = 8;
private const string COMMAND = "loottable";
private const string UI_COMMAND = "loottable.cmd";
private const string BACKUP_COMMAND = "loottable.backup";
private const string PERM_EDIT = "loottable.edit";
private const string PERM_DEBUG = "loottable.debug";
private static readonly NumberFormatInfo DEFAULT_NUMBER_FORMAT = new CultureInfo("en", false).NumberFormat;
private static NumberFormatInfo NUMBER_FORMAT = DEFAULT_NUMBER_FORMAT;
private static new string Name => ((RustPlugin)_instance).Name;
private static EditorConfig Flags;
private static Loottable _instance;
#if CARBON
ImageDatabaseModule imageDatabase;
#endif
#endregion
#region Editor Config
private static void InitializeEditorConfig() {
Flags = Interface.Oxide.DataFileSystem.ReadObject<EditorConfig>(DataFilePath("editorconfig"));
if (Flags == null) {
CErr("Editor config is corrupted, creating a new one");
Flags = new EditorConfig();
}
Flags.Validate();
SaveEditorConfig();
}
private static void SaveEditorConfig() {
Interface.Oxide.DataFileSystem.WriteObject<EditorConfig>(DataFilePath("editorconfig"), Flags);
}
private class EditorConfig {
[JsonProperty("version")]
private int version = configVersion;
public int ItemLimit {
get;
private set;
}
= 1000000;
public MinMaxFloat GatherMultiplier {
get;
private set;
}
= new MinMaxFloat(1f, 1000f);
public MinMaxFloat FurnaceMultiplier {
get;
private set;
}
= new MinMaxFloat(0.1f, 100f);
public MinMaxFloat ItemMultiplier {
get;
private set;
}
= new MinMaxFloat(0.1f, 10f);
[JsonIgnore]
public bool Debug => flags["Debug"];
[JsonIgnore]
public bool UnlockGatherMultiplier => flags["UnlockGatherMultiplier"];
[JsonIgnore]
public bool DisableItemLimit => flags["DisableItemLimit"];
[JsonIgnore]
public bool UnlockFurnaceMultiplier => flags["UnlockFurnaceMultiplier"];
[JsonIgnore]
public bool UnlockItemMultiplier => flags["UnlockItemMultiplier"];
[JsonIgnore]
public bool DisableStackingHooks => flags["DisableStackingHooks"];
[JsonIgnore]
public bool RefreshLootOnExit => flags["RefreshLootOnExit"];
[JsonIgnore]
public bool Update {
get;
}
= false;
public Dictionary<string, bool> flags {
get;
private set;
}
= new Dictionary<string, bool> {
{
"Debug", false
}
, {
"UnlockGatherMultiplier", false
}
, {
"DisableItemLimit", false
}
, {
"UnlockFurnaceMultiplier", false
}
, {
"UnlockItemMultiplier", false
}
, {
"DisableStackingHooks", false
}
, {
"RefreshLootOnExit", true
}
}
;
public EditorConfig() {
}
[JsonConstructor]
public EditorConfig(int version, int ItemLimit, int ItemLimitUnlocked, MinMaxFloat GatherMultiplier, MinMaxFloat FurnaceMultiplier, Dictionary<string, bool> flags) {
if (version != configVersion) Update = true;
this.version = configVersion;
this.ItemLimit = ItemLimit;
this.GatherMultiplier = GatherMultiplier;
this.FurnaceMultiplier = FurnaceMultiplier;
foreach(var flag in flags) this.flags[flag.Key] = flag.Value;
Save();
if (Update) CPrint("Update detected, performing reload of remote content");
}
public void Validate() {
EditorConfig validator = new EditorConfig();
if (ItemLimit == 0) ItemLimit = validator.ItemLimit;
if (GatherMultiplier == MinMaxFloat.Null) GatherMultiplier = validator.GatherMultiplier;
if (FurnaceMultiplier == MinMaxFloat.Null) FurnaceMultiplier = validator.FurnaceMultiplier;
foreach (var flag in flags.ToList()) {
if (!validator.flags.ContainsKey(flag.Key)) flags.Remove(flag.Key);
}
foreach (var vflag in validator.flags) {
if (!flags.ContainsKey(vflag.Key)) flags.Add(vflag.Key, false);
}
}
public bool TrySetFlag(string flag, string value, out bool newValue) {
bool val;
newValue = false;
value = value.Replace("0", Boolean.FalseString).Replace("1", Boolean.TrueString);
if (!flags.ContainsKey(flag)) return false;
if (!Boolean.TryParse(value, out val)) return false;
flags[flag] = val;
newValue = val;
Save();
return true;
}
public void Save() => SaveEditorConfig();
}
private static string DataFilePathFull() => Path.Combine(Interface.Oxide.DataDirectory, Name);
private static string DataFilePathFull(string file) => Path.Combine(Interface.Oxide.DataDirectory, Name, file);
private static string DataFilePathFull(string folder, string file) => Path.Combine(Interface.Oxide.DataDirectory, Name, folder, file);
private static string DataFilePath(string file) => String.Format("{0}/{1}", Name, file);
private static string DataFilePath(string folder, string file) => String.Format("{0}/{1}/{2}", Name, folder, file);
private static long CurrentUnixTime() => ((DateTimeOffset)DateTime.Now).ToUnixTimeSeconds();
#endregion
#region Image Library
private void LoadImages() {
Dictionary<string, string> images = LootManager.GetImageDictionary();
images = images.MergeWith(ConfigManager.GetCustomImages());
Puts($"Import {images.Count} images");
#if CARBON
imageDatabase.Queue(Flags?.Update ?? false, images);
#else
timer.In(1f, () => {
if (!ImageLibrary) PrintError("ERROR: ImageLibrary not found, you can download it here: https://umod.org/plugins/image-library");
else ImageLibrary?.Call("ImportImageList", Title, images, 0UL, Flags?.Update ?? false);
}
);
#endif
}
private CuiImageComponent CreateItemImage(int itemid, ulong skin = 0) {
if (skin == 0) return new CuiImageComponent {
ItemId = itemid
}
;
else return new CuiImageComponent {
ItemId = itemid, SkinId = skin
}
;
}
private CuiRawImageComponent CreateImage(string key) {
#if CARBON
return new CuiRawImageComponent {
Png = imageDatabase.GetImageString(key, silent : true)
}
;
#else
return new CuiRawImageComponent {
Png = ImageLibrary?.Call<string>("GetImage", key)
}
;
#endif
}
#endregion
#region Backup
private void CmdBackup(IPlayer player, string command, string[] args) {
if (!permission.UserHasPermission(player.Id, PERM_DEBUG)) {
player.Reply("You don't have permission to do that");
return;
}
if (args.Length < 2) {
player.Reply($"Invalid args! Usage:\n {command} load <name> - Load backup with the given name from the backups folder" + $"\n {command} create <name> - Create backup of everything with the given name");
// TODO uncomment when enabling scoped backups
//$"\n {command} create <name> <crates|gathering|stacksize|config|all> - Create a backup of the specified configurations with the given name");
return;
}
string name = args[1];
if (args[0] == "load") {
string result = BackupManager.LoadBackupFile(name);
player.Reply(result);
}
else if (args[0] == "create") {
string target = args.Length == 3 ? args[2] : "all";
string result = BackupManager.CreateBackupFile(name, target);
player.Reply(result);
}
}
private static class BackupManager {
public enum BackupScope {
NONE = 0x00, CRATES = 0x01, GATHERING = 0x02, STACKSIZE = 0x04, CONFIG = 0x08, ALL = CRATES | GATHERING | STACKSIZE | CONFIG
}
private const string BACKUP_FOLDER = "backups";
private const string BACKUP_EXTENSION = "lootprofile";
private static bool ExcludeFile(string nameWithExtension) {
// Protect from potentially malicious files
if (!nameWithExtension.EndsWith(".json")) {
return true;
}
// Other unwanted files
return nameWithExtension == "editorconfig.json" || nameWithExtension == "custom_items.json" || nameWithExtension == "custom_items_v2.json";
}
// TODO apply backup scope
public static string CreateBackupFile(string name, string target) {
BackupScope scope;
if (!Enum.TryParse<BackupScope>(target, true, out scope) || scope == BackupScope.NONE) {
return $"Failed to create backup - Invalid selector '{target}'";
}
CheckBackupDirectory();
string backupFile = DataFilePathFull(BACKUP_FOLDER, $"{name}.{BACKUP_EXTENSION}");
if (File.Exists(backupFile)) {
return $"Failed to create backup - File {backupFile} already exists";
}
List<FileBackup> backups = new List<FileBackup>();
try {
foreach (var file in Directory.GetFiles(DataFilePathFull())) {
string fileName = Path.GetFileName(file);
if (ExcludeFile(fileName)) continue;
string contents = File.ReadAllText(file);
var backup = new FileBackup(fileName);
backup.CompressData(contents);
backups.Add(backup);
}
var fileStream = File.Create(backupFile);
SerializeMany(backups, fileStream);
fileStream.Dispose();
}
catch (Exception ex) {
return $"An exception occured while creating a backup: {ex}";
}
return $"Backup {backupFile} successfully created";
}
public static string LoadBackupFile(string name) {
CheckBackupDirectory();
string backupFile = DataFilePathFull(BACKUP_FOLDER, $"{name}.{BACKUP_EXTENSION}");
if (!File.Exists(backupFile)) {
return $"Failed to load backup - file {backupFile} does not exist";
}
try {
var fileStream = File.OpenRead(backupFile);
List<FileBackup> backups = DeserializeMany<FileBackup>(fileStream);
foreach (var backup in backups) {
if (ExcludeFile(backup.OriginalName)) continue;
string fileName = DataFilePathFull(backup.OriginalName);
if (File.Exists(fileName)) {
File.Delete(fileName);
}
string data = backup.DecompressData();
File.WriteAllText(fileName, data);
}
}
catch (Exception ex) {
return $"An exception occured while loading a backup: {ex}";
}
_instance.timer.In(2f, () => ConsoleSystem.Run(ConsoleSystem.Option.Server, "o.reload", Name));
return "Backup sucessfully loaded, plugin will reload to apply the changes";
}
private static void CheckBackupDirectory() {
string path = DataFilePathFull(BACKUP_FOLDER);
if (!Directory.Exists(path)) {
Directory.CreateDirectory(path);
}
}
#region Protobuf
[ProtoContract]
private class FileBackup {
[ProtoMember(1)]
public string OriginalName {
get;
set;
}
[ProtoMember(2)]
public byte[] CompressedData {
get;
set;
}
public FileBackup() {
}
public FileBackup(string fileName) {
OriginalName = fileName;
}
#region Compress / Decompress
public void CompressData(string json) {
var bytes = Encoding.UTF8.GetBytes(json);
var msi = new MemoryStream(bytes);
var mso = new MemoryStream();
var gs = new GZipStream(mso, CompressionMode.Compress);
msi.CopyTo(gs);
gs.Dispose();
CompressedData = mso.ToArray();
// WHY DOES THIS ANCIENT COMPILER NOT SUPPORT FUCKING USINGS?
msi.Dispose();
mso.Dispose();
}
public string DecompressData() {
var msi = new MemoryStream(CompressedData);
var mso = new MemoryStream();
var gs = new GZipStream(msi, CompressionMode.Decompress);
gs.CopyTo(mso);
gs.Dispose();
string result = Encoding.UTF8.GetString(mso.ToArray());
msi.Dispose();
mso.Dispose();
return result;
}
#endregion
}
private static void SerializeMany<T>(IEnumerable<T> data, Stream stream) {
foreach (var obj in data) {
Serializer.SerializeWithLengthPrefix<T>(stream, obj, PrefixStyle.Fixed32);
}
}
private static List<T> DeserializeMany<T>(Stream stream) {
List<T> list = new List<T>();
T obj;
do {
obj = Serializer.DeserializeWithLengthPrefix<T>(stream, PrefixStyle.Fixed32);
if (obj != null) {
list.Add(obj);
}
}
while (obj != null);
return list;
}
#endregion
}
#endregion
#region Custom Hooks
private static bool CanPopulateContainerHook(LootContainer container) {
object result = Interface.CallHook("OnContainerPopulate", container);
return result == null;
}
private static bool CanPopulateCorpseHook(LootableCorpse corpse) {
object result = Interface.CallHook("OnCorpsePopulate", corpse);
return result == null;
}
private static bool CanUseCustomAirdrop(SupplySignal signal, ThrownWeapon item) {
object result = Interface.CallHook("OnCustomAirdrop", signal, item);
return result == null;
}
#endregion
#region Hooks
#region Loot
object OnLootSpawn(LootContainer container) {
#if HOOKTIME_DEBUG
using var htt = new HookTimeTracker("OnLootSpawn");
#endif
var lootConfig = LootManager.GetContainerConfig(container);
if (lootConfig == null || !CanPopulateContainerHook(container)) {
return null;
}
#if HOOKTIME_DEBUG
htt.SetBreakpoint();
#endif
if (lootConfig.Enabled) {
if (lootConfig.lootType != LootableConfig.LootType.Custom) {
container.PopulateLoot();
}
lootConfig.ApplyToCrate(container);
if (container.shouldRefreshContents) {
container.CancelInvoke(container.SpawnLoot);
container.Invoke(container.SpawnLoot, UnityEngine.Random.Range(container.minSecondsBetweenRefresh, container.maxSecondsBetweenRefresh));
}
#if HOOKTIME_DEBUG
htt.SetBreakpoint();
#endif
return false;
}
return null;
}
#endregion
#region Quarries
void OnQuarryToggled(MiningQuarry quarry, BasePlayer player) {
if (!quarry.isStatic) return;
var config = LootManager.GetQuarryConfig(quarry);
if (quarry.IsEngineOn()) config.QuarryInit(ref quarry);
else config.ResetQuarry(ref quarry);
}
void OnQuarryGather(MiningQuarry quarry, Item item) {
if (!quarry.isStatic) return;
var config = LootManager.GetQuarryConfig(quarry);
config.QuarryUpdate(ref quarry, ref item);
}
void OnExcavatorMiningToggled(ExcavatorArm arm) {
var config = LootManager.GetExcavatorConfig();
config.Apply(ref arm);
}
object OnExcavatorGather(ExcavatorArm arm, Item item) {
var config = LootManager.GetExcavatorConfig();
config.GatherUpdate(ref arm, item);
return null;
}
#endregion
#region NPCs
void OnEntitySpawned(LootableCorpse corpse) {
#if HOOKTIME_DEBUG
using var htt = new HookTimeTracker("OnEntitySpawned [LootableCorpse]");
#endif
var config = LootManager.GetNpcConfig(corpse);
#if HOOKTIME_DEBUG
htt.SetBreakpoint();
#endif
if (config == null || !CanPopulateCorpseHook(corpse)) {
return;
}
if (config.Enabled) {
timer.In(0.1f, () => {
if (!corpse.IsDestroyed) {
config.ApplyToCorpse(corpse?.containers[0], true);
}
}
);
}
}
void OnEntitySpawned(BasePlayer npc) {
#if HOOKTIME_DEBUG
using var htt = new HookTimeTracker("OnEntitySpawned [BasePlayer]");
#endif
if (npc.IsNpc) {
LootManager.CacheNpc(npc);
}
}
#endregion
#region Gathering
object OnCollectiblePickup(CollectibleEntity entity, BasePlayer player) {
if (player == null || entity == null) return null;
if (IsDeployableNature(entity)) {
return null;
}
CollectibleConfig config = LootManager.GetCollectibleConfig(entity);
if (config == null) return null;
if (!config.enabled) return null;
config.DoPickup(entity, player);
return false;
}
object OnDispenserGather(ResourceDispenser dispenser, BasePlayer player, Item item) {
BaseEntity ent = dispenser.GetComponent<BaseEntity>();
GatherConfig config = GatherManager.gatherConfig;
if (config == null) return null;
if (!config.enabled) return null;
item.amount = config.GetNewAmount(ent.ShortPrefabName, item.info.itemid, item.amount);
return null;
}
void OnDispenserBonus(ResourceDispenser dispenser, BasePlayer player, Item item) {
BaseEntity ent = dispenser.GetComponent<BaseEntity>();
GatherConfig config = GatherManager.gatherConfig;
if (config == null) return;
if (!config.enabled) return;
item.amount = config.GetNewAmount(ent.ShortPrefabName, item.info.itemid, item.amount);
}
void OnGrowableGathered(GrowableEntity plant, Item item, BasePlayer player) {
GatherConfig config = GatherManager.gatherConfig;
if (config == null) return;
if (!config.enabled) return;
item.amount = config.GetNewGrowableAmount(plant.ShortPrefabName, item.amount);
}
private bool IsDeployableNature(BaseEntity entity) {
if (DeployableNature == null) {
return false;
}
var b = DeployableNature.Call("STCanGainXP", null, entity) as bool?;
if (b == false) {
return true;
}
return false;
}
#endregion
#region Stacking
bool? CanStackItem(Item target, Item item, bool combine = false) {
#if HOOKTIME_DEBUG
using var htt = new HookTimeTracker(combine ? "CanCombineDroppedItem" : "CanStackItem");
#endif
if (Flags.DisableStackingHooks) return null;
if ((target.GetOwnerPlayer().IsUnityNull() || item.GetOwnerPlayer().IsUnityNull()) && !combine) return null;
if (target.info.itemid != item.info.itemid) return false;
if (target.skin != item.skin) return false;
if (target.name != item.name) return false;
if (target.text != item.text) return false;
if (target.condition != item.condition) return false;
if (target.blueprintTarget != item.blueprintTarget) return false;
if (target.info.amountType == ItemDefinition.AmountType.Genetics || item.info.amountType == ItemDefinition.AmountType.Genetics) {
if ((target.instanceData?.dataInt ?? -1) != (item.instanceData?.dataInt ?? -1)) {
return false;
}
}
#if HOOKTIME_DEBUG
htt.SetBreakpoint();
#endif
var heldEntity = item.GetHeldEntity();
if (heldEntity is BaseProjectile weapon && !combine) {
var mag = weapon.primaryMagazine;
if (mag.contents > 0) {
target.parent.GetOwnerPlayer().GiveItem(ItemManager.Create(mag.ammoType, mag.contents));
mag.contents = 0;
}
}
if (heldEntity is Chainsaw chainsaw && !combine) {
if (chainsaw.ammo > 0) {
target.GetOwnerPlayer().GiveItem(ItemManager.Create(chainsaw.fuelType, chainsaw.ammo));
chainsaw.ammo = 0;
}
}
if (heldEntity is FlameThrower flameThrower && !combine) {
if (flameThrower.ammo > 0) {
target.GetOwnerPlayer().GiveItem(ItemManager.Create(flameThrower.fuelType, flameThrower.ammo));
flameThrower.ammo = 0;
}
}
if (heldEntity is BaseLiquidVessel && (!item.contents.IsEmpty() || !target.contents.IsEmpty())) {
return false;
}
#if HOOKTIME_DEBUG
htt.SetBreakpoint();
#endif
// target.info.stackable > target.amount prevents infinite fuel glitch
if ((item.info.shortname == "hat.miner" || item.info.shortname == "hat.candle") && (item.contents?.itemList.Count ?? 0) > 0 && !combine && target.info.stackable > target.amount) {
// Check if all items can be moved to new stack
// Only take out fuel if old stack gets removed
if (item.amount - (item.info.stackable - target.amount) <= 0) {
var content = item.contents.itemList.First();
var drop = ItemManager.Create(content.info, content.amount, content.skin);
item.GetOwnerPlayer().GiveItem(drop);
}
}
return true;
}
Item OnItemSplit(Item item, int amount) {
#if HOOKTIME_DEBUG
using var htt = new HookTimeTracker("OnItemSplit");
#endif
if (Flags.DisableStackingHooks) return null;
if (DefendableHomes != null) {
if (item.skin == 2591851360 || item.skin == 2817854052 || item.skin == 2817854377 || item.skin == 2817854677 || item.skin == 2888602635 || item.skin == 2888602942 || item.skin == 2888603247) return null;
}
#if HOOKTIME_DEBUG
htt.SetBreakpoint();
#endif
item.amount -= amount;
Item split = ItemManager.Create(item.info, amount, item.skin);
split.name = item.name;
split.text = item.text;
split.condition = item.condition;
#if HOOKTIME_DEBUG
htt.SetBreakpoint();
#endif
if (item.IsBlueprint()) {
split.blueprintTarget = item.blueprintTarget;
}
item.MarkDirty();
if (item.info.amountType == ItemDefinition.AmountType.Genetics && item.instanceData != null && item.instanceData.dataInt != 0) {
split.instanceData = new ProtoBuf.Item.InstanceData() {
dataInt = item.instanceData.dataInt, ShouldPool = false
}
;
}
#if HOOKTIME_DEBUG
htt.SetBreakpoint();
#endif
var heldEntity = split.GetHeldEntity();
if (heldEntity is BaseProjectile weapon) {
weapon.primaryMagazine.contents = 0;
weapon.SendNetworkUpdateImmediate();
}
if (heldEntity is Chainsaw chainsaw) {
if (chainsaw.ammo > 0) {
chainsaw.ammo = 0;
chainsaw.SendNetworkUpdateImmediate();
}
}
if (heldEntity is FlameThrower flameThrower) {
if (flameThrower.ammo > 0) {
flameThrower.ammo = 0;
flameThrower.SendNetworkUpdateImmediate();
}
}
if (heldEntity is BaseLiquidVessel) {
split.contents.ForceClear();
//item.contents.Clear();
}
#if HOOKTIME_DEBUG
htt.SetBreakpoint();
#endif
if (item.contents != null) {
if (split.contents == null) {
split.contents = new ItemContainer();
split.contents.ServerInitialize(split, item.contents.capacity);
split.contents.GiveUID();
}
else {
split.contents.capacity = item.contents.capacity;
}
}
split.MarkDirty();
return split;
}
private void OnItemAddedToContainer(ItemContainer container, Item item) {
#if HOOKTIME_DEBUG
using var htt = new HookTimeTracker("OnItemAddedToContainer");
#endif
if (Flags.DisableStackingHooks) return;
BasePlayer player = container.GetOwnerPlayer();
if (player?.IsNpc ?? true) return;
if (player.inventory.containerWear.uid == container.uid && item.amount > 1) {
int amount = item.amount - 1;
item.amount = 1;
Item split = ItemManager.Create(item.info, amount, item.skin);
split.name = item.name;
split.text = item.text;
split.condition = item.condition;
item.MarkDirty();
split.MarkDirty();
split.MoveToContainer(player.inventory.containerMain);
}
}
object CanCombineDroppedItem(DroppedItem item1, DroppedItem item2) {
var res = CanStackItem(item1.item, item2.item, true);
// Return null if hook returned null or true
if (res ?? true) {
return null;
}
return true;
}
#endregion
#region Heli Fuel
private void OnEntitySpawned(PlayerHelicopter heli) {
if (!ConfigManager.IsReady) return;
NextTick(() => {
if (heli.creatorEntity == null) return;
EntityFuelSystem fs = heli?.GetFuelSystem();
if (fs == null) return;
int fuel = -1;
if (heli is ScrapTransportHelicopter) {
fuel = ConfigManager.AirwolfConfig.scrapHeliFuel;
}
else if (heli is Minicopter) {
fuel = ConfigManager.AirwolfConfig.minicopterFuel;
}
else if (heli is AttackHelicopter) {
fuel = ConfigManager.AirwolfConfig.attackHeliFuel;
}
if (fuel < 0) return;
Item fuelItem = fs.GetFuelItem();
if (fuelItem != null) {
if (fuel == 0) {
fuelItem.Remove();
}
else if (fuel != fuelItem.amount) {
fuelItem.amount = fuel;
fuelItem.MarkDirty();
}
}
}
);
}
#endregion
#region Smelting & Mixing
private void OnOvenStarted(BaseOven oven) {
if (!ConfigManager.IsReady || !ConfigManager.FurnaceConfig.enabled) {
return;
}
ConfigManager.FurnaceConfig.UpdateOvenSpeed(oven);
}
private void OnMixingTableToggle(MixingTable table, BasePlayer player) {
if (!ConfigManager.IsReady || !ConfigManager.FurnaceConfig.enabled) return;
ConfigManager.FurnaceConfig.ToggleMixingTable(table);
}
#endregion
#region Recycler
object OnRecyclerToggle(Recycler recycler, BasePlayer player) {
if (!(ConfigManager.FurnaceConfig?.recyclerEnabled ?? false)) return null;
ConfigManager.FurnaceConfig.ToggleRecycler(recycler);
return null;
}
#endregion
#region Air Drop
void OnExplosiveDropped(BasePlayer player, BaseEntity entity, ThrownWeapon item) => OnExplosiveThrown(player, entity, item);
void OnExplosiveThrown(BasePlayer player, BaseEntity entity, ThrownWeapon item) {
if (!ConfigManager.IsReady || !ConfigManager.AirdropConfig.enabled || entity is not SupplySignal signal) {
return;
}
ConfigManager.AirdropConfig.OnSupplySignalThrown(signal, item);
}
#endregion
#region General
void OnServerInitialized() {
LootManager.Initialize();
LoadImages();
StackManager.Initialize();
GatherManager.Initialize();
ConfigManager.Initialize();
TrainLootManager.Initialize();
LootManager.PopulateNpcCache();
LootManager.RefreshCrateLoot(null);
}
void Init() {
_instance = this;
#if HOOKTIME_DEBUG
HookTimeTracker.loadTime = Time.time;
#endif
InitializeEditorConfig();
#if CARBON
DPrint("Using Carbon's built-in Image Database");
imageDatabase = BaseModule.GetModule<ImageDatabaseModule>();
#endif
AddCovalenceCommand(COMMAND, nameof(CMD_loottable));
AddCovalenceCommand(UI_COMMAND, nameof(CMD_cmd));
AddCovalenceCommand(BACKUP_COMMAND, nameof(CmdBackup));
permission.RegisterPermission(PERM_EDIT, this);
permission.RegisterPermission(PERM_DEBUG, this);
UiHelper.EditorCache.Clear();
CustomItemStorage.LoadCustomItems();
if (Flags.DisableStackingHooks) {
Unsubscribe(nameof(CanStackItem));
Unsubscribe(nameof(CanCombineDroppedItem));
Unsubscribe(nameof(OnItemSplit)
// ... [Code preview truncated due to length. Full source in attached file] ...