416 lines
16 KiB
C#
416 lines
16 KiB
C#
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<string> VEHICLE_DECAY_SETTINGS = new List<string> {
|
|
"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<string> FIXED_POPULATIONS = new List<string> {
|
|
"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<string> 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<string>();
|
|
try {
|
|
data = Interface.Oxide.DataFileSystem.ReadObject<List<string>>(
|
|
KNOWN_SPAWNS_FILE);
|
|
} catch (Exception e) {
|
|
Puts($"Failed to load known loot spawns ({e.Message}). Assuming none exist.");
|
|
}
|
|
|
|
this.knownSpawns = new HashSet<string>();
|
|
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<string>(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");
|
|
}
|
|
}
|
|
}
|
|
}
|