Initial version of FinchWorld and TungstonsDumper

This commit is contained in:
Andrew Miner
2025-09-06 22:08:17 -06:00
parent 6ebc151742
commit 5098847b06
4 changed files with 554 additions and 9 deletions
+415
View File
@@ -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<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");
}
}
}
}