using Oxide.Core; using Oxide.Core.Plugins; using Rust; using System; using System.Collections.Generic; using UnityEngine; /* Goal: I'd like to be able to play Rust as though it's a pure survival game which is as realistic as possible. To that end, I'd like inanimate objects to *stay put* unless the player moves them. No hyper-fast entropy of any kind. Conversely, once a player moves something, it should *stay moved*: no respawning groceries in "abandoned" supermarkets. Similarly, once a plant is picked, it won't grow back. If you eat all the corn, then corn is now extinct on your island. Even then, growing your own crops takes a (slightly) more realistic amount of time, so you really have to plan ahead. Current Support: - vehicles last forever once spawned, and never re-spawn once destroyed - wild crops do not respawn in the same locations - loot does not respawn in the same locations once harvested - loot crates don't magically disappear once looted - players can put their own stuff back into loot crates Future Support: - similar sorts of constraints around monuments - loot piles stay put once looted Open Issues: - car parts baskets need to have model swapped when they're empty - same with food crates - not sure whether this is going to have the desired effect on monument loot */ namespace Oxide.Plugins { [Info("FinchWorld", "TungstonMiner", "0.1.0")] [Description("Simulate a real island where resources, once looted, are gone forever")] public class FinchWorld : RustPlugin { // Constants /////////////////////////////////////////////////////////////////////////////// private const int FOREVER = 60 * 24 * 60; // 60 days, in minutes private const string KNOWN_SPAWNS_FILE = "FinchWorld.KnownSpawns"; private const float KNOWN_SPAWN_SAVE_INTERVAL = 60f; // in seconds private const float SPAWN_RADIUS = 10f; private static readonly List VEHICLE_DECAY_SETTINGS = new List { "basesubmarine.deepwaterdecayminutes", "basesubmarine.outsidedecayminutes", "bike.outsidedecayminutes", "hotairballoon.outsidedecayminutes", "modularcar.outsidedecayminutes", "motorrowboat.decaystartdelayminutes", "motorrowboat.deepwaterdecayminutes", "motorrowboat.outsidedecayminutes", "playerhelicopter.insidedecayminutes", "playerhelicopter.outsidedecayminutes", "ridablehorse.decayminutes", "snowmobile.outsidedecayminutes", "traincar.decayminutes", "traincarunloadable.decayminutesafterunload", "tugboat.tugdecayminutes", "tugboat.tugdecaystartdelayminutes" }; private static readonly List FIXED_POPULATIONS = new List { "modularcar.population", "minicopter.population", "scraptransporthelicopter.population", "traincar.population", "hotairballoon.population", "rhib.rhibpopulation", "motorrowboat.population", "bike.motorbikemonumentpopulation", "bike.pedalmonumentpopulation", "bike.pedalroadsidepopulation", "metaldetectorsource.population" }; // Member Variables //////////////////////////////////////////////////////////////////////// private HashSet knownSpawns = null; private Timer knownSpawnTimer = null; private bool enforceKnownSpawnRules = true; private bool shouldCloneJunkPiles = true; // Hooks /////////////////////////////////////////////////////////////////////////////////// // prevent anything from decaying, ever object CanDecay(BaseCombatEntity entity) { return false; } // deal with various non-permanent entities spawning when the shouldn't object OnEntitySpawned(BaseNetworkable networkable) { var entity = networkable as BaseEntity; if (entity == null) return null; var isJunkPile = entity.ShortPrefabName.StartsWith("junkpile_"); if (isJunkPile) { this.CloneJunkPile(entity); return null; } if (this.knownSpawns == null) return null; var isControlledEntity = ( (entity is LootContainer) || (entity is CollectibleEntity) || (entity is OreResourceEntity) ); if (!isControlledEntity) return null; Vector3 position = entity.transform.position; int x = Mathf.FloorToInt(position.x / SPAWN_RADIUS); int y = Mathf.FloorToInt(position.y / SPAWN_RADIUS); int z = Mathf.FloorToInt(position.y / SPAWN_RADIUS); var key = $"{x},{y},{z}"; if (this.knownSpawns.Contains(key)) return false; this.knownSpawns.Add(key); return null; } // // modify loot containers to be persistent storage // object OnLootEntity(BasePlayer player, BaseEntity entity) { // var container = entity as LootContainer; // if (container == null) return null; // // container.destroyOnEmpty = false; // container.lootDefinition = null; // container.panelName = "generic"; // same as small wooden box // // container.inventory.SetFlag(ItemContainer.Flag.NoItemInput, false); // container.inventory.SetFlag(ItemContainer.Flag.IsLocked, false); // container.inventory.canAcceptItem = (item, slot) => true; // // container.SendNetworkUpdate(); // push changes to client // // return null; // } // adjust settings to promote object persistence void OnServerInitialized() { this.AdjustVehiclePersistence(); this.SetUpKnownSpawnSaving(); this.SuppressBuildingDecay(); this.SuppressPopulationReplenishment(); Puts("FinchWorld is active"); } // stop tracking loot spawns void OnUnload() { this.TearDownKnownSpawnSaving(); } // Helper Methods ////////////////////////////////////////////////////////////////////////// // change vehicle settings so they last forever void AdjustVehiclePersistence() { Puts("Adjusting vehicle persistence..."); foreach (var setting in VEHICLE_DECAY_SETTINGS) { ConsoleSystem.Run(ConsoleSystem.Option.Server, $"{setting} {FOREVER}"); } } // make plants take a week (instead of a day) to grow to maturity void AdjustAgriculture() { Puts("Adjusting agricultural settings..."); ConsoleSystem.Run(ConsoleSystem.Option.Server, "server.planttick 120"); ConsoleSystem.Run(ConsoleSystem.Option.Server, "server.planttickscale 0.2857"); } // clones junk piles so the game doesn't automatically nuke them void CloneJunkPile(BaseEntity entity) { Puts($"Setting timer to clone a {entity.ShortPrefabName}"); timer.Once(3f, () => { try { if (!this.shouldCloneJunkPiles) { Puts("Skipping because we shouldn't clone junk piles right now"); return; } this.shouldCloneJunkPiles = false; this.enforceKnownSpawnRules = false; Puts($"Cloning junk pile at {entity.transform.position}"); var clonedEntity = this.DeepCloneEntity(entity); if (clonedEntity != null) { Puts("Moving the old pile under ground"); this.MoveUnderTerrain(entity); } } finally { this.enforceKnownSpawnRules = true; this.shouldCloneJunkPiles = true; } }); } // clones an entire item tree Item DeepCloneItem(Item source) { var target = ItemManager.Create(source.info, source.amount, source.skin); target.condition = source.condition; target.maxCondition = source.maxCondition; target.name = source.name; if (source.instanceData != null) { Puts($"Dumping fields on item.instanceData ({source.instanceData.GetType()})"); var fields = source.instanceData.GetType().GetFields( System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); foreach (var field in fields) { Puts(field.Name); } Puts($"Dumping properties on item.instanceData ({source.instanceData.GetType()})"); var properties = source.instanceData.GetType().GetProperties( System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); foreach (var property in properties) { Puts(property.Name); } // target.instanceData = new ProtoBuf.Item.InstanceData() { // dataInt = source.instanceData.dataInt, // subEntity = source.instanceData.subEntity, // blueprintTarget = source.instanceData.blueprintTarget, // blueprintAmount = source.instanceData.blueprintAmount, // ShouldPool = false, // ShouldReset = false // }; } if (source.contents != null) { target.contents = new ItemContainer(); target.contents.ServerInitialize(null, source.contents.capacity); target.contents.GiveUID(); foreach (var sourceChild in source.contents.itemList) { var targetChild = this.DeepCloneItem(sourceChild); if (targetChild == null) { Puts($"Failed to clone child item {sourceChild.name}"); return null; } if (!targetChild.MoveToContainer(target.contents)) { Puts($"Failed to move cloned child item to parent item {target.name}"); target.Remove(); targetChild.Remove(); return null; } } } return target; } // clone an entire entity tree BaseEntity DeepCloneEntity(BaseEntity source) { var target = GameManager.server.CreateEntity( source.PrefabName, source.transform.position, source.transform.rotation, true // start active ); if (target == null) { Puts($"Failed to target {source.ShortPrefabName} at {source.transform.position}"); return null; } if (target.GetType() != source.GetType()) { Puts($"Clone type ({target.GetType()}) " + $"doesn't match the original ({source.GetType()})"); return null; } target.enableSaving = true; target.Spawn(); target.SendNetworkUpdate(); var sourceContainer = source as LootContainer; var targetContainer = target as LootContainer; if ((sourceContainer != null) && (targetContainer != null)) { targetContainer.destroyOnEmpty = false; targetContainer.lootDefinition = null; targetContainer.panelTitle = "FinchWorld Loot"; targetContainer.inventory.SetFlag(ItemContainer.Flag.NoItemInput, false); targetContainer.inventory.SetFlag(ItemContainer.Flag.IsLocked, false); targetContainer.inventory.canAcceptItem = (item, slot) => true; targetContainer.SendNetworkUpdate(); for (var index = 0; index < sourceContainer.inventory.capacity; index++) { var sourceItem = sourceContainer.inventory.GetSlot(index); if (sourceItem == null) continue; var targetItem = this.DeepCloneItem(sourceItem); if (targetItem == null) continue; if (!targetItem.MoveToContainer(targetContainer.inventory)) { targetItem.Remove(); } } } foreach (var sourceChild in source.children) { var sourceChildEntity = sourceChild as BaseEntity; if (sourceChildEntity == null) continue; var targetChildEntity = this.DeepCloneEntity(sourceChildEntity); if (targetChildEntity == null) { Puts( $"Aborting clone: failed to clone child entity " + $"({sourceChildEntity.ShortPrefabName}) at " + $"{sourceChildEntity.transform.position}" ); target.Kill(); return null; } targetChildEntity.SetParent(target); } return target; } // Reload spawn locations from disk void LoadKnownSpawns() { if (this.knownSpawns != null) { Puts("Cannot load loot spawns as they've already been loaded."); return; } var data = new List(); try { data = Interface.Oxide.DataFileSystem.ReadObject>( KNOWN_SPAWNS_FILE); } catch (Exception e) { Puts($"Failed to load known loot spawns ({e.Message}). Assuming none exist."); } this.knownSpawns = new HashSet(); foreach (var key in data) { this.knownSpawns.Add(key); } } // move a given entity (and any children) underground void MoveUnderTerrain(BaseEntity entity) { entity.transform.position = new Vector3( entity.transform.position.x, entity.transform.position.y - 20f, entity.transform.position.z ); foreach (var child in entity.children) { this.MoveUnderTerrain(child); } } // Save spawn locations to disk void SaveKnownSpawns() { var data = new List(this.knownSpawns); Interface.Oxide.DataFileSystem.WriteObject(KNOWN_SPAWNS_FILE, data); } // Start the spawn tracking system void SetUpKnownSpawnSaving() { if (this.knownSpawns != null) { Puts("Aborting loot spawn setup because it's already running."); return; } this.LoadKnownSpawns(); this.knownSpawnTimer = timer.Every(KNOWN_SPAWN_SAVE_INTERVAL, () => this.SaveKnownSpawns()); } // Stop the spawn tracking system void TearDownKnownSpawnSaving() { this.SaveKnownSpawns(); this.knownSpawnTimer?.Destroy(); this.knownSpawnTimer = null; } // don't allow player buildings to decay void SuppressBuildingDecay() { Puts("Suppressing accelerated building decay..."); ConsoleSystem.Run(ConsoleSystem.Option.Server, "decay.upkeep false"); ConsoleSystem.Run(ConsoleSystem.Option.Server, "decay.scale 0"); } // don't allow non-renewable populations to repopulate void SuppressPopulationReplenishment() { Puts("Suppressing magically appearing vehicles"); foreach (var population in FIXED_POPULATIONS) { ConsoleSystem.Run(ConsoleSystem.Option.Server, $"{population} 0"); } } } }