diff --git a/oxide/plugins/FinchWorld.cs b/oxide/plugins/FinchWorld.cs new file mode 100644 index 0000000..e92a6fe --- /dev/null +++ b/oxide/plugins/FinchWorld.cs @@ -0,0 +1,415 @@ +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"); + } + } + } +} diff --git a/oxide/plugins/GoodNightsSleep.cs b/oxide/plugins/GoodNightsSleep.cs index 5f2c881..0e5ad7a 100644 --- a/oxide/plugins/GoodNightsSleep.cs +++ b/oxide/plugins/GoodNightsSleep.cs @@ -168,14 +168,26 @@ namespace Oxide.Plugins { private void CheckSleepiness() { var today = this.GetDay(); + var now = this.GetTime(); + if (this.lastYawnDay == today) return; - var now = this.GetTime(); if ((now > GoodNightsSleep.BED_TIME) && (now < GoodNightsSleep.BED_TIME + 1f)) { + Puts("Players standing on their beds should be sleepy"); this.lastYawnDay = today; foreach (var player in BasePlayer.activePlayerList) { - Effect.server.Run(GoodNightsSleep.YAWN_SOUND, player.transform.position); + var bag = this.FindNearbySleepingBag(player); + if (bag == null) { + Puts($"{player.displayName} isn't standing on a sleeping bag"); + continue; + } + if (!this.IsInBase(bag)) { + Puts($"{player.displayName}'s sleeping bag isn't in a base"); + continue; + } + + player.ChatMessage("You feel your eyelids drooping..."); } } } @@ -263,14 +275,15 @@ namespace Oxide.Plugins { var position = bag.transform.position + Vector3.up * 0.5f; // check that there's some kind of player-placed block under the sleeping bag - RaycastHit hit; - Physics.Raycast(position, Vector3.down, out hit, 2f); - if (hit.collider.GetComponentInParent() == null) { - Puts("Sleeping bag is not on a foundation"); - return false; + foreach (var hit in Physics.RaycastAll(position, Vector3.down, 2f)) { + if (!hit.collider) continue; + var obj = hit.collider.gameObject; + + if (obj.GetComponentInParent()) return true; } - return true; + Puts("Sleeping bag is not on a foundation"); + return false; } private bool IsUnderRoof(SleepingBag bag) { diff --git a/oxide/plugins/RereadConfigs.cs b/oxide/plugins/RereadConfigs.cs index b6acc36..c74d951 100644 --- a/oxide/plugins/RereadConfigs.cs +++ b/oxide/plugins/RereadConfigs.cs @@ -7,8 +7,10 @@ namespace Oxide.Plugins { [Description("Re-read the server configs after loading to override gamemode settings")] public class RereadConfigs : RustPlugin { + private const float DELAY = 10f; + void OnServerInitialized() { - timer.Once(10f, () => { + timer.Once(DELAY, () => { Server.Command("server.readcfg"); }); } diff --git a/oxide/plugins/TungstonsDumper.cs b/oxide/plugins/TungstonsDumper.cs new file mode 100644 index 0000000..d4f3e11 --- /dev/null +++ b/oxide/plugins/TungstonsDumper.cs @@ -0,0 +1,115 @@ +using Newtonsoft.Json; +using Oxide.Core; +using Oxide.Core.Plugins; +using System.Collections.Generic; +using System; +using System.IO; +using System.Reflection; +using UnityEngine; + +namespace Oxide.Plugins { + + [Info("TungstonsDumper", "TungstonMiner", "0.1.0")] + [Description("Add commands to dump data helpful in writing mods.")] + public class TungstonsDumper : RustPlugin { + + // Constants /////////////////////////////////////////////////////////////////////////////// + + private const string PREFAB_LOG = "tungstonsdumper-prefabs.json"; + + private const string TYPE_LOG = "tungstonsdumper-types.json"; + + // Hooks /////////////////////////////////////////////////////////////////////////////////// + + void OnServerInitialized() { + Puts("Tungston's Dumper is active"); + } + + // Console Commands //////////////////////////////////////////////////////////////////////// + + [ConsoleCommand("dump.types")] + private void DumpTypes(ConsoleSystem.Arg arg) { + if (arg.Connection.authLevel < 2) { + arg.ReplyWith("You must be an admin to run this command"); + return; + } + + var types = new List() { + GameManager.server.GetType(), + typeof(BasePlayer), + typeof(BaseEntity), + typeof(Item), + typeof(RustPlugin) + }; + + var data = new Dictionary(); + foreach (var type in types) { + this.ExtractTypeData(type, data); + } + + this.WriteData(data, TYPE_LOG); + arg.ReplyWith($"Dumped type data to {TYPE_LOG}"); + } + + // [ConsoleCommand("dump.prefabs")] + // private void DumpPrefabs(ConsoleSystem.Arg arg) { + // if (arg.Connection.authLevel < 2) { + // arg.ReplyWith("You must be an admin to run this command"); + // return; + // } + // + // var result = new List(); + // foreach (var (id, attributeSet) in PrefabAttribute.server.prefabs) { + // result.Add(new { + // name = attributeSet.name + // }); + // } + // + // this.WriteData(result, PREFAB_LOG); + // arg.ReplyWith($"Dumped prefabs to {PREFAB_LOG}"); + // } + + // Helper Methods ////////////////////////////////////////////////////////////////////////// + + private void ExtractTypeData(Type type, Dictionary data) { + if (type == null) return; + if (data.ContainsKey(type.FullName)) return; + + this.ExtractTypeData(type.BaseType, data); + + var result = new Dictionary() { + { "BaseType", type.BaseType != null ? type.BaseType.FullName : "None" }, + { "Fields", new List() }, + { "Properties", new List() }, + { "Methods", new List() } + }; + + var flags = + System.Reflection.BindingFlags.Public | + System.Reflection.BindingFlags.NonPublic | + System.Reflection.BindingFlags.Instance | + System.Reflection.BindingFlags.DeclaredOnly; + + foreach (var field in type.GetFields(flags)) { + ((List) result["Fields"]).Add(field.Name); + } + + foreach (var property in type.GetProperties(flags)) { + ((List) result["Properties"]).Add(property.Name); + } + + foreach (var method in type.GetMethods(flags)) { + ((List) result["Methods"]).Add(method.Name); + } + + data[type.FullName] = result; + } + + private void WriteData(object obj, string fileName) { + File.WriteAllText( + Path.Combine(Interface.Oxide.LogDirectory, fileName), + JsonConvert.SerializeObject(obj, Formatting.Indented) + ); + } + } +}