From fb589460288a2e7b01abf4305ffeae3e6a3d4c2a Mon Sep 17 00:00:00 2001 From: Andrew Miner Date: Tue, 2 Sep 2025 21:23:59 -0600 Subject: [PATCH] Add current state of development --- cfg/server.cfg | 76 ++++++++++ oxide/plugins/GoodNightsSleep.cs | 240 +++++++++++++++++++++++++++++++ oxide/plugins/RereadConfigs.cs | 16 +++ oxide/plugins/ZombieHorde.cs | 46 ++++++ oxide/plugins/plant.cs | 231 +++++++++++++++++++++++++++++ 5 files changed, 609 insertions(+) create mode 100644 cfg/server.cfg create mode 100644 oxide/plugins/GoodNightsSleep.cs create mode 100644 oxide/plugins/RereadConfigs.cs create mode 100644 oxide/plugins/ZombieHorde.cs create mode 100644 oxide/plugins/plant.cs diff --git a/cfg/server.cfg b/cfg/server.cfg new file mode 100644 index 0000000..e072393 --- /dev/null +++ b/cfg/server.cfg @@ -0,0 +1,76 @@ +// Ideally, this should be true, but that prevents you from breaking your own base parts +server.pve "False" + +// Base Decay +decay.upkeep_inside_decay_scale "0.01" +decay.bracket_0_costfraction "0.0050" +decay.bracket_1_costfraction "0.0075" +decay.bracket_2_costfraction "0.0100" +decay.bracket_3_costfraction "0.0150" + +// Corpse Decay +server.corpsedespawn "1440" // 1 days + +// Vehicle Decay +baseridableanimal.decayminutes "1440" // 1 day +basesubmarine.deepwaterdecayminutes "10080" // 7 days +basesubmarine.outsidedecayminutes "43200" // 30 days +bike.outsidedecayminutes "10080" // 7 days +hotairballoon.outsidedecayminutes "2880" // 2 days +modularcar.outsidedecayminutes "43200" // 30 days +motorrowboat.decaystartdelayminutes "0" // right away +motorrowboat.deepwaterdecayminutes "1440" // 1 day +motorrowboat.outsidedecayminutes "10080" // 7 days +playerhelicopter.insidedecayminutes "43200" // 30 days +playerhelicopter.outsidedecayminutes "10080" // 7 days +ridablehorse.decayminutes "1440" // 1 day +snowmobile.outsidedecayminutes "43200" // 30 days +tugboat.tugdecayminutes "43200" // 30 days + +// Disable holiday events +halloween.enabled "False" +xmas.enabled "False" +easter.enabled "False" + +// Minimal respawn time +server.respawntimeadditionbag "0" // Extra cooldown for bags +server.respawntimeadditionbed "0" // Extra cooldown for beds + +// Security +ownerid 76561198025237120 "TungstonMiner" "sysadmin" + +// Tax Rates +server.workbench1taxrate "0" +server.workbench2taxrate "0" +server.workbench3taxrate "0" + +// Animal Populations + +// Animal populations should reflect realistic proportion of predators to prey. They should also +// appear at about twice the natural population density to provide a good balance between realism +// and fun gameplay. + +bear.population "0.33" // per sqkm, 3 total +boar.population "2.77" // per sqkm, 25 total +chicken.population "11.11" // per sqkm, 100 total +polarbear.population "0.11" // per sqkm, 1 total +crocodile.population "0.22" // per sqkm, 2 total +panther.population "0.11" // per sqkm, 1 total +ridablehorse.population "1.00" // per sqkm, 9 total +snakehazard.population "5.55" // per sqkm, 50 total +tiger.population "0.00" // per sqkm, none, only panthers +stag.population "5.55" // per sqkm, 50 total +wolf.population "0.55" // per sqkm, 5 total + +// Vehicle Populations + +bike.motorbikemonumentpopulation "0.50" // 50% chance per monument +bike.pedalmonumentpopulation "2.00" // 2 per monument +bike.pedalroadsidepopulation "2.22" // per sqkm, 20 total +hotairballoon.population "0.11" // per sqkm, 1 total +minicopter.population "0.22" // per sqkm, 2 total +modularcar.population "1.11" // per sqkm, 10 total +motorrowboat.population "1.11" // per sqkm, 10 total +rhib.rhibpopulation "0.00" // per sqkm, 0 total +scraptransporthelicopter.population "0.11" // per sqkm, 1 total +traincar.population "0.66" // per sqkm, 6 total diff --git a/oxide/plugins/GoodNightsSleep.cs b/oxide/plugins/GoodNightsSleep.cs new file mode 100644 index 0000000..31ff84d --- /dev/null +++ b/oxide/plugins/GoodNightsSleep.cs @@ -0,0 +1,240 @@ +using Facepunch; +using Oxide.Core.Plugins; +using UnityEngine; +using System; + +class LayerMask { + public const int Deployed = 1 << 8; + public const int Construction = 1 << 21; +} + +namespace Oxide.Plugins { + + [Info("GoodNightsSleep", "TungstonMiner", "0.1.0")] + [Description("An accelerated way to sleep through the night.")] + public class GoodNightsSleep : RustPlugin { + + // the earliest time you can go to sleep + public const float BED_TIME = 22f; + + // the amount you heal per second from sleeping + public const float HEAL_RATIO = 1f; + + // the ratio at which time passes (e.g., 10 means 10x usual speed) + public const float SLEEP_SPEED = 20; + + // the amount of wall-clock time (in seconds) between sleep steps + public const float STEP_SIZE = 1f; + + // the time you wake up in the morning + public const float WAKE_TIME = 7f; + + // a an actual instance of sleeping through the night + private class SleepSession { + + // in-game time when we last completed a step + private float lastStepAt = 0f; + + // callback for when the session is over + private Action onComplete = null; + + // the player doing the sleeping + private BasePlayer player = null; + + // the plugin itself + private RustPlugin plugin = null; + + // the timer controlling the steps + private Timer timer = null; + + public SleepSession(BasePlayer player, Action onComplete) { + this.onComplete = onComplete; + this.player = player; + } + + // Lifecycle Methods /////////////////////////////////////////////// + + public void Begin(Timer timer) { + this.Abort(); + + this.timer = timer; + this.lastStepAt = this.GetTime(); + this.player.StartSleeping(); + this.player.ChatMessage("You slowly start to drift off to sleep."); + } + + public void KeepSleeping() { + if (this.ShouldAbort()) { + this.Abort(); + return; + } + + if (this.ShouldWake()) { + this.End(); + return; + } + + var now = this.GetTime(); + var sleepDuration = (now - this.lastStepAt) * 60 * 60; // in seconds + this.player.Heal(sleepDuration * GoodNightsSleep.HEAL_RATIO); + + this.lastStepAt = now; + this.SetTime( + this.GetTime() + ( + GoodNightsSleep.STEP_SIZE * GoodNightsSleep.SLEEP_SPEED / 60 / 60 + ) + ); + } + + public void Abort() { + if (this.timer == null) return; + + this.timer.Destroy(); + this.timer = null; + this.onComplete?.Invoke(); + } + + public void End() { + this.player.EndSleeping(); + this.Abort(); + } + + // Helper Methods ////////////////////////////////////////////////// + + private float GetTime() { + return TOD_Sky.Instance.Cycle.Hour; + } + + private void SetTime(float time) { + ConsoleSystem.Run(ConsoleSystem.Option.Server, $"env.time {time}"); + } + + private bool ShouldAbort() { + if (!this.player.IsConnected) return true; + if (this.player.IsDead()) return true; + if (!this.player.IsSleeping()) return true; + return false; + } + + private bool ShouldWake() { + if (this.ShouldAbort()) return false; + if (this.GetTime() > GoodNightsSleep.WAKE_TIME) return true; + return false; + } + }; + + // the specific instance of sleeping through the night + private SleepSession session = null; + + // Hooks /////////////////////////////////////////////////////////////////////////////////// + + void OnServerInitialized() { + Puts("Good Night's Sleep is active"); + } + + // Commands //////////////////////////////////////////////////////////////////////////////// + + [ChatCommand("sleep")] + private void SleepCommand(BasePlayer player, string command, string[] args) { + if (!this.IsSleepy(player)) return; + + this.session = new SleepSession(player, () => this.OnSleepSessionEnded()); + this.session.Begin(timer.Every(STEP_SIZE, () => this.session.KeepSleeping())); + } + + // Helper Methods ////////////////////////////////////////////////////////////////////////// + + private void OnSleepSessionEnded() { + this.session.Abort(); + this.session = null; + } + + private bool IsSleepy(BasePlayer player) { + var now = this.GetTime(); + + if (!player) return false; + if (!player.IsConnected) return false; + + if (player.IsDead()) { + player.ChatMessage("You're too dead to sleep."); + return false; + } + + if ((now > GoodNightsSleep.WAKE_TIME) && (now < GoodNightsSleep.BED_TIME)) { + player.ChatMessage("You aren't feeling sleepy yet."); + return false; + } + + if (player.IsSleeping() || (this.session != null)) { + player.ChatMessage("You are already asleep."); + return false; + } + + var bag = this.FindNearbySleepingBag(player); + if (bag == null) { + player.ChatMessage("There's nowhere to sleep nearby."); + return false; + } + + if (!this.IsInBase(bag)) { + player.ChatMessage("You're too uneasy to sleep here."); + return false; + } + + return true; + } + + private SleepingBag FindNearbySleepingBag(BasePlayer player) { + foreach (var bag in UnityEngine.Object.FindObjectsOfType()) { + if (bag == null) continue; + if (bag.deployerUserID != player.userID) continue; + + var distance = Vector3.Distance(player.transform.position, bag.transform.position); + if (distance < 2f) { + return bag; + } + } + + return null; + } + + private float GetTime() { + return TOD_Sky.Instance.Cycle.Hour; + } + + private bool IsInBase(SleepingBag bag) { + return ( + this.IsOnFoundation(bag) && + this.IsUnderRoof(bag) && + this.IsNearToolchest(bag) && + true + ); + } + + private bool IsNearToolchest(SleepingBag bag) { + var privlidges = Pool.GetList(); + Vis.Entities(bag.transform.position, 25f, privlidges, LayerMask.Deployed); + var result = privlidges.Count > 0; + Pool.FreeList(ref privlidges); + return result; + } + + private bool IsOnFoundation(SleepingBag bag) { + // get a bit off the floor so we can look down + var position = bag.transform.position + Vector3.up * 0.1f; + + // 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) return false; + + return true; + } + + private bool IsUnderRoof(SleepingBag bag) { + RaycastHit hit; + var position = bag.transform.position; + return Physics.Raycast(position, Vector3.up, out hit, LayerMask.Construction); + } + } +} diff --git a/oxide/plugins/RereadConfigs.cs b/oxide/plugins/RereadConfigs.cs new file mode 100644 index 0000000..b6acc36 --- /dev/null +++ b/oxide/plugins/RereadConfigs.cs @@ -0,0 +1,16 @@ +using Oxide.Core.Plugins; +using UnityEngine; + +namespace Oxide.Plugins { + + [Info("RereadConfigs", "TungstonMiner", "0.1.0")] + [Description("Re-read the server configs after loading to override gamemode settings")] + public class RereadConfigs : RustPlugin { + + void OnServerInitialized() { + timer.Once(10f, () => { + Server.Command("server.readcfg"); + }); + } + } +} diff --git a/oxide/plugins/ZombieHorde.cs b/oxide/plugins/ZombieHorde.cs new file mode 100644 index 0000000..72192ec --- /dev/null +++ b/oxide/plugins/ZombieHorde.cs @@ -0,0 +1,46 @@ +using Oxide.Core.Plugins; +using UnityEngine; + +namespace Oxide.Plugins { + + [Info("ZombieHorde", "TungstonMiner", "0.1.0")] + [Description("Create a zombie horde at midnight every seventh day")] + public class ZombieHorde : RustPlugin { + + private bool isHordeActive = false; + + void OnServerInitialized() { + Puts("ZombieHorde loaded!"); + timer.Every(10f, () => this.CheckTime()); + } + + void CheckTime() { + var sky = TOD_Sky.Instance; + var day = sky.Cycle.Day; + var hour = sky.Cycle.Hour; + var isNight = sky.IsNight; + + if ((!this.isHordeActive) && (hour < 0.5) && (day % 7 == 0)) { + this.StartHorde(); + this.isHordeActive = true; + } + + if (this.isHordeActive && !isNight) { + this.EndHorde(); + this.isHordeActive = false; + } + } + + void StartHorde() { + Puts($"Starting horde at {TOD_Sky.Instance.Cycle.Hour}"); + Server.Command("halloween.murdererpopulation 10"); + Server.Command("spawn.fill_populations"); + } + + void EndHorde() { + Puts($"Ending horde at {TOD_Sky.Instance.Cycle.Hour}"); + Server.Command("halloween.murdererpopulation 0"); + Server.Command("spawn.delete_populations Murderer.Population"); + } + } +} diff --git a/oxide/plugins/plant.cs b/oxide/plugins/plant.cs new file mode 100644 index 0000000..a6daee5 --- /dev/null +++ b/oxide/plugins/plant.cs @@ -0,0 +1,231 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using Oxide.Core; +using UnityEngine; + +namespace Oxide.Plugins { + [Info("PlantBiomeCustomizer", "TungstonMiner", "1.0.0")] + [Description("Customize placement of plants around the map by biome")] + public class PlantBiomeTuner : RustPlugin { + + // Config ////////////////////////////////////////////////////////////////////////////////// + + PluginConfig configData; + + public class PluginConfig { + // Key = arbitrary rule name (e.g., "berry_blue", "pumpkins_desert", etc.), value = rule definition. + public Dictionary rules = new Dictionary(); + public static PluginConfig Default() { return new PluginConfig(); } + } + + public class PlantRule { + public bool enabled = true; + public string[] prefabFilters = Array.Empty(); // case-insensitive substrings matched against population identity + public string[] biomes = Array.Empty(); // names matching TerrainMeta.BiomeType flags + public float densityMultiplier = 1.0f; // multiply existing density/weights + } + + protected override void LoadDefaultConfig() { + configData = PluginConfig.Default(); + SaveConfig(); + } + + protected override void SaveConfig() { + Config.WriteObject(configData, true); + } + + protected override void LoadConfig() { + base.LoadConfig(); + try { + configData = Config.ReadObject(); + } catch { + PrintError("Config read error; generating new default config."); + configData = PluginConfig.Default(); + } + SaveConfig(); + } + + + // Hooks /////////////////////////////////////////////////////////////////////////////////// + + void OnServerInitialized(bool initial) { + NextTick(ApplyAll); + } + + + // Commands //////////////////////////////////////////////////////////////////////////////// + + [ConsoleCommand("plantbiometuner.apply")] + void CmdApply(ConsoleSystem.Arg arg) { + if (!arg.IsAdmin) return; + ApplyAll(); + arg.ReplyWith("PlantBiomeTuner: applied settings and refilled populations."); + } + + // Core Logic ////////////////////////////////////////////////////////////////////////////// + + void ApplyAll() { + int changed = 0; + int matched = 0; + + var populations = Resources.FindObjectsOfTypeAll(); + if (populations == null || populations.Length == 0) { + PrintWarning("No SpawnPopulation objects found (map not ready yet?). Will retry next tick."); + NextTick(ApplyAll); + return; + } + + if (configData.rules == null || configData.rules.Count == 0) { + Puts("PlantBiomeTuner: no rules configured."); + return; + } + + foreach (var kv in configData.rules) { + string ruleKey = kv.Key; + PlantRule rule = kv.Value; + if (rule == null || !rule.enabled) continue; + + var filters = (rule.prefabFilters ?? Array.Empty()) + .Where(s => !string.IsNullOrWhiteSpace(s)) + .Select(s => s.Trim()) + .ToArray(); + if (filters.Length == 0) continue; + + int biomeMask = BuildBiomeMask(rule.biomes); + if (biomeMask == 0) { + PrintWarning($"[{ruleKey}] Biome mask is empty; skipping."); + continue; + } + + foreach (var population in populations) { + if (population == null) continue; + + string id = GetPopulationIdentity(population); + if (string.IsNullOrEmpty(id)) continue; + + if (!filters.Any(f => id.IndexOf(f, StringComparison.OrdinalIgnoreCase) >= 0)) continue; + + matched++; + + try { + bool anyChange = false; + anyChange |= TrySetBiomeMask(population, biomeMask); + anyChange |= TrySetDensity(population, rule.densityMultiplier); + + if (anyChange) { + changed++; + Puts($"[{ruleKey}] Updated '{id}' (biomes={string.Join(",", rule.biomes)}, density x{rule.densityMultiplier:0.##})"); + } + } catch (Exception e) { + PrintWarning($"[{ruleKey}] Failed to update '{id}': {e.Message}"); + } + } + } + + try { + ConsoleSystem.Run(ConsoleSystem.Option.Server, "spawn.delete_populations"); + ConsoleSystem.Run(ConsoleSystem.Option.Server, "spawn.fill_populations"); + } catch (Exception e) { + PrintWarning($"Failed to refill populations: {e.Message}"); + } + + Puts($"PlantBiomeTuner: matched {matched} populations; applied changes to {changed}."); + } + + int BuildBiomeMask(IEnumerable names) { + if (names == null) return 0; + int mask = 0; + foreach (var n in names) { + if (string.IsNullOrWhiteSpace(n)) continue; + try { + var value = (TerrainMeta.BiomeType)Enum.Parse(typeof(TerrainMeta.BiomeType), n.Trim(), true); + mask |= (int)value; // combine provided flags; no manual shifts + } catch { + PrintWarning($"Unknown biome '{n}'. Ensure names match TerrainMeta.BiomeType."); + } + } + return mask; + } + + string GetPopulationIdentity(SpawnPopulation population) { + try { + var res = population.Resource; + if (res != null) { + var prefab = res.prefab; + if (prefab != null) { + var path = prefab.resourcePath; + if (!string.IsNullOrEmpty(path)) return path; + } + } + } catch { + // ignore + } + return population.name ?? population.GetType().Name; + } + + bool TrySetBiomeMask(SpawnPopulation population, int biomeMask) { + bool changed = false; + + var field = population.GetType().GetField("BiomePreference") ?? + population.GetType().GetField("TopologyPreference"); + if (field != null && field.FieldType == typeof(int)) { + int current = (int) field.GetValue(population); + if (current != biomeMask) { + field.SetValue(population, biomeMask); + changed = true; + } + return changed; + } + + var prop = population.GetType().GetProperty("BiomePreference"); + if (prop != null && prop.PropertyType == typeof(int) && prop.CanWrite) { + int current = (int) (prop.GetValue(population) ?? 0); + if (current != biomeMask) { + prop.SetValue(population, biomeMask, null); + changed = true; + } + return changed; + } + + return false; + } + + bool TrySetDensity(SpawnPopulation population, float multiplier) { + if (Mathf.Approximately(multiplier, 1f)) return false; + + bool changed = false; + + var densityField = population.GetType().GetField("Density"); + if (densityField != null && densityField.FieldType == typeof(float)) { + float d = (float)densityField.GetValue(population); + float nd = Mathf.Max(0f, d * multiplier); + if (!Mathf.Approximately(d, nd)) { + densityField.SetValue(population, nd); + changed = true; + } + } + + var sptField = population.GetType().GetField("SpawnPerTick"); + if (sptField != null && sptField.FieldType == typeof(float)) { + float v = (float)sptField.GetValue(population); + float nv = Mathf.Max(0f, v * multiplier); + if (!Mathf.Approximately(v, nv)) { + sptField.SetValue(population, nv); + changed = true; + } + } + + var clusterField = population.GetType().GetField("ClusterDensities"); + if (clusterField != null && typeof(float[]).IsAssignableFrom(clusterField.FieldType)) { + var arr = (float[])clusterField.GetValue(population); + if (arr != null && arr.Length > 0) { + for (int i = 0; i < arr.Length; i++) arr[i] = Mathf.Max(0f, arr[i] * multiplier); + changed = true; + } + } + + return changed; + } + } +}