C#:
using Facepunch;
using HarmonyLib;
using Network;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using UnityEngine;
using UnityEngine.Events;
namespace Oxide.Plugins
{
[Info("ServerPrefabs", "0xF", "1.1.1")]
public class ServerPrefabs : RustPlugin
{
/// <summary>
/// Verbose debug logging gate. Sourced from the config key "Enable debug logging"
/// on load; consumer plugins may also flip it at runtime for diagnostics.
/// </summary>
public static bool DEBUG = false;
#region Consts
private const string PREFAB_PATH_PREFIX = "assets/prefabs/custom/server/";
private const string PREFAB_PATH_FORMAT = $"{PREFAB_PATH_PREFIX}{{0}}/{{1}}.prefab";
// Rust save-file format markers. Layout mirrors Facepunch SaveRestore exactly;
// do not change values - they must match what the game reads/writes.
// 4-byte magic header, ASCII "SAVR".
private static readonly sbyte[] SAVE_HEADER = { 83, 65, 86, 82 };
private const char MARKER_WIPE_ID = (char)74; // 'J' - optional wipe-id section
private const char MARKER_CREATED_TIME = (char)68; // 'D' - optional created-time section
private const char MARKER_CUSTOM_DATA = (char)6756; // per-entity ICustomEntity.SaveData block
// Sanity bounds for length-prefixed reads from a save file. A single serialized
// entity / custom-data block should never approach this; anything larger means a
// corrupt or hostile file, so we skip rather than allocate/seek blindly.
private const uint MAX_ENTITY_DATA_BYTES = 64u * 1024u * 1024u;
private const uint MAX_CUSTOM_DATA_BYTES = 64u * 1024u * 1024u;
#endregion
#region Variables
private static Dictionary<string, List<GameObject>> AllObjects { get; set; } = new Dictionary<string, List<GameObject>>();
#endregion
#region Interfaces & Structs
/// <summary>
/// Implement on a component added to a custom prefab's <see cref="GameObject"/> to be
/// notified once the prefab has been registered with the game.
/// </summary>
public interface ICustomPrefab
{
/// <summary>Called after the prefab is registered (manifest, string pool, preprocess).</summary>
public void OnCustomPrefabRegistered();
}
/// <summary>Optional behaviour flags for a custom entity. Combine with bitwise OR.</summary>
[Flags]
public enum EntityFlags
{
/// <summary>No special behaviour (default).</summary>
None = 0,
/// <summary>Entity is excluded from disk saving (sets <c>enableSaving = false</c>).</summary>
DisableSaving = 1 << 0,
/// <summary>Entity is not added to the spawnable-entity command list.</summary>
NoSpawnCommand = 1 << 1,
}
/// <summary>
/// Implement on a custom <see cref="BaseEntity"/> to take part in the entity lifecycle.
/// Set <see cref="CustomFlags"/> to opt into behaviours; the nested handler interfaces add
/// save/network hooks.
/// </summary>
public interface ICustomEntity : ICustomPrefab
{
/// <summary>Called after the entity prefab is registered and added to the manifest.</summary>
public void OnCustomEntityRegistered();
/// <summary>
/// Optional behaviour flags (e.g. <see cref="EntityFlags.DisableSaving"/>,
/// <see cref="EntityFlags.NoSpawnCommand"/>). Defaults to <see cref="EntityFlags.None"/>;
/// override only if you need non-default behaviour. Named <c>CustomFlags</c> to avoid
/// colliding with the inherited <c>BaseEntity.Flags</c> member.
/// </summary>
public EntityFlags CustomFlags => EntityFlags.None;
/// <summary>Customise the network-facing proto written for this entity.</summary>
public interface SaveForNetwork
{
/// <summary>Populate extra fields on the outgoing network <paramref name="entity"/> proto.</summary>
public void SaveForNetwork(ProtoBuf.Entity entity);
}
/// <summary>Persist and restore arbitrary per-entity data alongside the standard save.</summary>
public interface SaveData
{
/// <summary>Write custom data to <paramref name="stream"/> during a save.</summary>
public void Save(Stream stream);
/// <summary>Read custom data from <paramref name="stream"/> during a load.</summary>
public void Load(Stream stream);
}
}
public struct SaveLoadInfo
{
public ProtoBuf.Entity entity;
public Stream stream;
}
#endregion
#region Internal
private static void LogDebug(string message)
{
if (DEBUG)
DebugEx.Log($"[ServerPrefabs] [DEBUG] {message}");
}
private static void LogError(string message, Exception ex = null)
{
Debug.LogError($"[ServerPrefabs] [ERROR] {message}");
if (ex != null)
Debug.LogError(ex);
}
private static void LogWarning(string message, StackTraceLogType stackTrace = StackTraceLogType.None)
{
DebugEx.LogWarning($"[ServerPrefabs] [WARNING] {message}", stackTrace);
}
public class CustomEntityComponent : EntityComponent<BaseEntity>
{
public uint clientPrefabID;
private ICustomEntity Interface => GetBaseEntity() as ICustomEntity;
public override void SaveComponent(BaseNetworkable.SaveInfo info)
{
if (info.forDisk)
return;
if (clientPrefabID > 0)
info.msg.baseNetworkable.prefabID = clientPrefabID;
(Interface as ICustomEntity.SaveForNetwork)?.SaveForNetwork(info.msg);
}
}
public class Scaler : EntityComponent<BaseEntity>
{
public override void SaveComponent(BaseNetworkable.SaveInfo info)
{
base.SaveComponent(info);
info.msg.baseEntity.scale = transform.localScale;
}
public override void LoadComponent(BaseNetworkable.LoadInfo info)
{
base.LoadComponent(info);
if (transform.localScale != info.msg.baseEntity.scale)
{
transform.localScale = info.msg.baseEntity.scale;
baseEntity.networkEntityScale = true;
}
}
}
/// <summary>
/// Component that gives a custom entity a built-in, network-replicated item container.
/// Set <see cref="Capacity"/> and an optional <see cref="Handler"/> before the entity spawns.
/// </summary>
public class EntityInventory : EntityComponent<BaseEntity>
{
public ItemContainer Inventory { get; private set; }
public int Capacity { get; set; } = 6;
public IHandler Handler { get; set; }
public override void InitShared()
{
base.InitShared();
Inventory = CreateInventory(baseEntity, false, Capacity);
}
public override void SaveComponent(BaseNetworkable.SaveInfo info)
{
base.SaveComponent(info);
if (Inventory == null || !info.forDisk)
return;
info.msg.storageBox = Pool.Get<ProtoBuf.StorageBox>();
info.msg.storageBox.contents = Inventory.Save();
}
public override void LoadComponent(BaseNetworkable.LoadInfo info)
{
base.LoadComponent(info);
if (info.msg.storageBox != null)
{
if (Inventory != null)
{
Inventory.Load(info.msg.storageBox.contents);
Inventory.capacity = Capacity;
}
}
}
public ItemContainer CreateInventory(BaseEntity owner, bool giveUID, int capacity)
{
ItemContainer newInventory = new ItemContainer
{
entityOwner = owner,
allowedContents = ItemContainer.ContentsType.Generic
};
newInventory.SetOnlyAllowedItem(null);
newInventory.maxStackSize = 0;
newInventory.ServerInitialize(null, capacity);
if (giveUID)
{
newInventory.GiveUID();
}
newInventory.onDirty += OnInventoryDirty;
if (Handler != null)
{
newInventory.onItemAddedRemoved = Handler.OnItemAddedOrRemoved;
newInventory.canAcceptItem = Handler.ItemFilter;
Handler.OnCreated(newInventory);
}
return newInventory;
}
private void OnInventoryDirty()
{
baseEntity.InvalidateNetworkCache();
Handler?.OnInventoryDirty();
}
/// <summary>Callbacks for reacting to and filtering changes in an <see cref="EntityInventory"/>.</summary>
public interface IHandler
{
/// <summary>Called once the backing <see cref="ItemContainer"/> has been created.</summary>
void OnCreated(ItemContainer container);
/// <summary>Called when the container is marked dirty (contents changed).</summary>
void OnInventoryDirty();
/// <summary>Called when an item is added to or removed from the container.</summary>
void OnItemAddedOrRemoved(Item item, bool added);
/// <summary>Return <c>true</c> to allow <paramref name="item"/> into <paramref name="position"/>.</summary>
bool ItemFilter(BasePlayer player, Item item, int position);
}
}
/// <summary>
/// Entry point. Returns the per-plugin <see cref="PluginContext"/> used to create and
/// register custom prefabs/entities. Typical use:
/// <c>ServerPrefabs.For(this).CreatePrefab("mything").AddEntityComponent<MyEntity>(clientPrefabId).Register();</c>
/// </summary>
public static PluginContext For(Plugin plugin) => PluginContext.Array.Get(plugin);
public class PluginContext : IDisposable
{
private Plugin plugin;
public List<CustomPrefab> CustomPrefabs { get; }
private PluginContext(Plugin plugin)
{
this.plugin = plugin;
this.CustomPrefabs = new List<CustomPrefab>();
}
public void Dispose()
{
_all.Remove(this);
}
/// <summary>
/// Create a custom prefab owned by this plugin. <paramref name="name"/> is required and
/// becomes part of the generated prefab path; <paramref name="strPrefab"/> optionally clones
/// an existing game prefab as the base GameObject.
/// </summary>
public CustomPrefab CreatePrefab(string name, string strPrefab = null)
{
if (string.IsNullOrEmpty(name))
throw new NullReferenceException("The name can't be null or empty!");
CustomPrefab prefab = new CustomPrefab(CustomPrefab.CreateGameObject(name, strPrefab), this.plugin);
CustomPrefabs.Add(prefab);
return prefab;
}
/// <summary>
/// Create an empty <see cref="PrefabBundle"/> owned by this plugin. Add prefabs to it,
/// then call <see cref="PrefabBundle.Register"/> once to register (and load) them all together.
/// </summary>
public PrefabBundle CreateBundle() => new PrefabBundle(this);
internal void OnPluginRemoved()
{
// Save all entities before destroying anything - killing one prefab's instances
// can take down another prefab's not-yet-saved entities.
try
{
List<BaseCustomEntityPrefab> entityPrefabs = Pool.Get<List<BaseCustomEntityPrefab>>();
try
{
foreach (CustomPrefab customPrefab in CustomPrefabs)
{
if (customPrefab is BaseCustomEntityPrefab entityPrefab && entityPrefab.entity)
entityPrefabs.Add(entityPrefab);
}
SaveAllPrefabEntities(entityPrefabs);
}
finally
{
Pool.FreeUnmanaged(ref entityPrefabs);
}
}
catch (Exception ex)
{
LogError("Error saving prefab entities on plugin removal", ex);
}
foreach (CustomPrefab customPrefab in CustomPrefabs)
customPrefab.OnPluginRemoved();
CustomPrefabs.Clear();
}
#region Static
private static List<PluginContext> _all { get; } = new List<PluginContext>();
internal static ReadOnlyCollection<PluginContext> All => _all.AsReadOnly();
internal static Memoized<PluginContext, Plugin> Array { get; } = new Memoized<PluginContext, Plugin>((Plugin plugin) =>
{
PluginContext instance = new PluginContext(plugin);
_all.Add(instance);
return instance;
});
#endregion
}
/// <summary>
/// Groups several <see cref="CustomPrefab"/>s so they can be registered (and their saved
/// entities loaded) together in one call. Build the bundle, add prefabs, then call
/// <see cref="Register"/> once:
/// <code>
/// var bundle = ServerPrefabs.For(this).CreateBundle();
/// bundle.CreatePrefab("a").AddEntityComponent<MyEntity>(idA);
/// bundle.CreatePrefab("b").AddEntityComponent<MyEntity>(idB);
/// bundle.Register();
/// </code>
/// </summary>
public class PrefabBundle
{
private readonly PluginContext context;
/// <summary>Prefabs in this bundle, in the order they were added.</summary>
public List<CustomPrefab> Prefabs { get; } = new List<CustomPrefab>();
internal PrefabBundle(PluginContext context)
{
this.context = context ?? throw new ArgumentNullException(nameof(context));
}
/// <summary>
/// Create a prefab on the owning plugin's context and add it to this bundle.
/// Returns the prefab so you can chain <c>AddEntityComponent</c> etc.
/// </summary>
public CustomPrefab CreatePrefab(string name, string strPrefab = null)
{
CustomPrefab prefab = context.CreatePrefab(name, strPrefab);
return Add(prefab);
}
/// <summary>Add an already-created prefab to this bundle. Idempotent. Returns the prefab.</summary>
public CustomPrefab Add(CustomPrefab prefab)
{
if (prefab == null)
throw new ArgumentNullException(nameof(prefab));
prefab.Bundle = this;
if (!Prefabs.Contains(prefab))
Prefabs.Add(prefab);
return prefab;
}
internal void Replace(CustomPrefab old, CustomPrefab updated)
{
int index = Prefabs.IndexOf(old);
if (index >= 0)
Prefabs[index] = updated;
else if (!Prefabs.Contains(updated))
Prefabs.Add(updated);
}
/// <summary>
/// Register every prefab in the bundle, then load their saves together: each entity
/// prefab is preloaded (ReadSaveFileAndPreloadEntities) into one batch and the spawn is
/// processed once (ProcessSaveEntities) for the whole bundle. One prefab failing does
/// not stop the rest.
/// </summary>
public void Register()
{
LogDebug($"Registering bundle of {Prefabs.Count} prefabs");
List<BaseCustomEntityPrefab> entityPrefabs = new List<BaseCustomEntityPrefab>();
foreach (CustomPrefab prefab in Prefabs)
{
if (prefab is BaseCustomEntityPrefab entityPrefab)
{
entityPrefab.DeferSaveLoad = true;
entityPrefabs.Add(entityPrefab);
}
}
foreach (CustomPrefab prefab in Prefabs)
{
try
{
prefab.Register();
}
catch (Exception ex)
{
LogError($"Error registering bundled prefab {prefab?.prefabPath}", ex);
}
}
if (entityPrefabs.Count > 0)
{
try
{
LoadSaveFiles(entityPrefabs);
}
catch (Exception ex)
{
LogError("Error loading bundle saves", ex);
}
}
}
}
internal class Memoized<TResult, TArgs>
{
public Memoized(Func<TArgs, TResult> factory)
{
if (factory == null)
{
throw new ArgumentNullException("factory");
}
this._factory = factory;
this._cache = new Dictionary<TArgs, TResult>();
}
public bool TryGetFromCache(TArgs args, out TResult result)
{
return this._cache.TryGetValue(args, out result);
}
public bool Remove(TArgs args)
{
return this._cache.Remove(args);
}
public TResult Get(TArgs args)
{
TResult tresult;
if (this._cache.TryGetValue(args, out tresult))
{
return tresult;
}
TResult tresult2 = this._factory(args);
this._cache.Add(args, tresult2);
return tresult2;
}
private readonly Func<TArgs, TResult> _factory;
private readonly Dictionary<TArgs, TResult> _cache;
}
public class CustomPrefab
{
public Plugin plugin { get; private set; }
public GameObject gameObject { get; }
public string name { get => gameObject.name; }
public uint prefabID { get; }
public string prefabPath { get; }
public string guid { get => prefabPath; }
/// <summary>Bundle this prefab belongs to, if any. Set when added via <see cref="PrefabBundle"/>.</summary>
internal PrefabBundle Bundle { get; set; }
internal CustomPrefab(GameObject gameObject, Plugin plugin)
{
this.gameObject = gameObject;
this.plugin = plugin;
this.prefabPath = string.Format(PREFAB_PATH_FORMAT, this.plugin.Name, gameObject.name).ToLower();
this.RegisterPathInStringPool();
this.prefabID = StringPool.Get(prefabPath);
}
internal static GameObject CreateGameObject(string name, string strPrefab = null)
{
if (string.IsNullOrEmpty(name))
throw new NullReferenceException("The name can't be null or empty!");
GameObject gameObject;
if (!string.IsNullOrEmpty(strPrefab))
{
gameObject = GameManager.server.CreatePrefab(strPrefab);
if (gameObject == null)
throw new ArgumentException($"Couldn't create base prefab from path \"{strPrefab}\"", nameof(strPrefab));
if (gameObject.TryGetComponent(out BaseEntity entity))
UnityEngine.Object.DestroyImmediate(entity);
gameObject.name = name;
}
else
{
gameObject = new GameObject(name);
}
gameObject.layer = (int)Rust.Layer.Reserved3;
UnityEngine.Object.DontDestroyOnLoad(gameObject);
return gameObject;
}
private void RegisterPathInStringPool()
{
if (!StringPool.toNumber.ContainsKey(prefabPath))
StringPool.Add(prefabPath);
}
private void AddToManifest()
{
string prefabPath = this.prefabPath;
FileSystem.Backend.cache.Add(prefabPath, gameObject);
GameManifest.pathToGuid[prefabPath] = prefabPath;
GameManifest.guidToPath[prefabPath] = prefabPath;
GameManifest.guidToObject[prefabPath] = gameObject;
}
private void RemoveFromManifest()
{
string prefabPath = this.prefabPath;
FileSystem.Backend.cache.Remove(prefabPath);
GameManager.server.preProcessed.prefabList.Remove(prefabPath);
GameManifest.pathToGuid.Remove(prefabPath);
GameManifest.guidToPath.Remove(prefabPath);
GameManifest.guidToObject.Remove(prefabPath);
if (StringPool.toNumber.TryGetValue(prefabPath, out uint hash))
{
StringPool.toString.Remove(hash);
StringPool.toNumber.Remove(prefabPath);
}
if (GameManifest.Current.entities.Contains(prefabPath))
GameManifest.Current.entities = GameManifest.Current.entities.Except(new string[] { prefabPath }).ToArray();
}
private void DestroyInstances(bool immediate = false)
{
DestroyInstancesFor(name, immediate);
if (prefabPath != name)
DestroyInstancesFor(prefabPath, immediate);
}
private static void DestroyInstancesFor(string key, bool immediate)
{
if (string.IsNullOrEmpty(key) || !AllObjects.TryGetValue(key, out List<GameObject> list))
return;
foreach (GameObject gameObject in list)
{
if (gameObject == null)
continue;
BaseNetworkable networkable = gameObject.GetComponent<BaseNetworkable>();
if (networkable && !networkable.IsDestroyed)
networkable.Kill();
if (immediate)
UnityEngine.Object.DestroyImmediate(gameObject);
else
UnityEngine.Object.Destroy(gameObject);
}
AllObjects.Remove(key);
}
internal void Destroy(bool immediate = false)
{
if (immediate)
UnityEngine.Object.DestroyImmediate(gameObject);
else
UnityEngine.Object.Destroy(gameObject);
}
internal virtual void OnPluginRemoved()
{
try
{
DestroyInstances();
}
catch (Exception ex)
{
LogError($"Error destroying prefab instances for {prefabPath}", ex);
}
try
{
PrefabAttribute.AttributeCollection attributeCollection = PrefabAttribute.server.Find(prefabID);
if (attributeCollection != null)
{
attributeCollection.attributes?.Clear();
attributeCollection.cache?.Clear();
PrefabAttribute.server.Invalidate(prefabID);
}
}
catch (Exception ex)
{
LogError($"Error destroying prefab instantiates for {prefabPath}", ex);
}
try
{
RemoveFromManifest();
}
catch (Exception ex)
{
LogError($"Error removing prefab from manifest: {prefabPath}", ex);
}
Destroy();
}
/// <summary>
/// Attach a <typeparamref name="T"/> entity component (plus networking and scaler components)
/// to this prefab, upgrading it to a <see cref="CustomEntityPrefab{T}"/>.
/// <paramref name="clientPrefabID"/> is the client-side prefab id used for networking.
/// </summary>
public CustomEntityPrefab<T> AddEntityComponent<T>(uint clientPrefabID) where T : BaseEntity
{
BaseEntity entity = gameObject.AddComponent<T>();
if (entity != null)
{
entity.prefabID = this.prefabID;
entity._prefabName = prefabPath;
entity.impactEffect = new GameObjectRef();
if (entity is ICustomEntity ce && ce.CustomFlags.HasFlag(EntityFlags.DisableSaving))
entity.enableSaving = false;
CustomEntityComponent component = gameObject.AddComponent<CustomEntityComponent>();
component.clientPrefabID = clientPrefabID;
gameObject.AddComponent<Scaler>();
}
CustomEntityPrefab<T> prefab = new CustomEntityPrefab<T>(gameObject, plugin);
PluginContext context = For(this.plugin);
context.CustomPrefabs.Remove(this);
context.CustomPrefabs.Add(prefab);
prefab.Bundle = this.Bundle;
this.Bundle?.Replace(this, prefab);
this.Bundle = null;
return prefab;
}
/// <summary>
/// Register the prefab with the game (string pool, manifest, preprocess) so it can be
/// spawned. Call once after configuring the prefab. Errors are caught and logged.
/// </summary>
public virtual void Register()
{
try
{
if (this.plugin == null)
{
LogError($"Registration failed: plugin is null for custom prefab {prefabPath}");
return;
}
LogDebug($"Registering prefab '{prefabPath}' for plugin '{plugin.Name}'");
try
{
GameManager.server.preProcessed.Process(prefabPath, gameObject, false);
}
catch (Exception ex)
{
LogError($"Failed to preprocess prefab: {prefabPath}", ex);
}
try
{
AddToManifest();
}
catch (Exception ex)
{
LogError($"Failed to add prefab to manifest: {prefabPath}", ex);
RemoveFromManifest();
UnityEngine.Object.Destroy(gameObject);
return;
}
try
{
if (gameObject.TryGetComponent(out ICustomPrefab customPrefab))
customPrefab.OnCustomPrefabRegistered();
}
catch (Exception ex)
{
LogError($"Error calling OnCustomPrefabRegistered for {prefabPath}", ex);
}
gameObject.name = prefabPath;
gameObject.SetActive(false);
LogDebug($"Successfully registered prefab: {prefabPath}");
}
catch (Exception ex)
{
LogError($"Unexpected error in RegisterPrefab for '{prefabPath}'", ex);
}
}
}
public abstract class BaseCustomEntityPrefab : CustomPrefab
{
public BaseEntity entity { get; }
/// <summary>
/// True when the entity opts out of disk saving via <see cref="EntityFlags.DisableSaving"/>.
/// Such prefabs skip both save and load entirely.
/// </summary>
public bool SavingDisabled => entity is ICustomEntity ce && ce.CustomFlags.HasFlag(EntityFlags.DisableSaving);
/// <summary>
/// When set, <see cref="Register"/> skips loading this prefab's save file. Used by
/// <see cref="PrefabBundle"/> so it can preload every prefab into one batch and process
/// the spawn once for all of them.
/// </summary>
internal bool DeferSaveLoad { get; set; }
internal BaseCustomEntityPrefab(GameObject gameObject, Plugin plugin) : base(gameObject, plugin)
{
entity = gameObject.GetComponent<BaseEntity>();
}
private string _saveFilePath;
public string GetSaveFilePath()
{
if (_saveFilePath == null)
{
string fileName = $"{Path.GetFileNameWithoutExtension(prefabPath)}.sav";
string directoryPath = Path.Combine(Interface.Oxide.DataDirectory, nameof(ServerPrefabs), plugin.Name);
_saveFilePath = Path.Combine(directoryPath, fileName);
// Migrate a save from the old flat layout (data/ServerPrefabs/<name>.sav).
try
{
string legacyPath = Path.Combine(Interface.Oxide.DataDirectory, nameof(ServerPrefabs), fileName);
if (!File.Exists(_saveFilePath) && File.Exists(legacyPath))
{
Directory.CreateDirectory(directoryPath);
File.Move(legacyPath, _saveFilePath);
LogWarning($"Moved save \"{fileName}\" to \"{directoryPath}\"");
}
}
catch (Exception ex)
{
LogError($"Failed to migrate legacy save for {prefabPath}", ex);
}
}
return _saveFilePath;
}
}
public class CustomEntityPrefab<T> : BaseCustomEntityPrefab where T : BaseEntity
{
public new T entity { get; }
internal CustomEntityPrefab(GameObject gameObject, Plugin plugin) : base(gameObject, plugin)
{
entity = gameObject.GetComponent<T>();
}
private void LoadSaveFile()
{
if (SavingDisabled)
return;
string saveFilePath = GetSaveFilePath();
if (!File.Exists(saveFilePath))
return;
if (!Rust.Application.isServerStarted && !World.LoadedFromSave)
return;
if (prefabID == default)
return;
ServerPrefabs.LoadSaveFile(saveFilePath);
}
public override void Register()
{
try
{
LogDebug($"Registering entity '{prefabPath}' for plugin '{plugin?.Name}'");
base.Register();
ICustomEntity customEntity = entity as ICustomEntity;
try
{
if (customEntity == null || !customEntity.CustomFlags.HasFlag(EntityFlags.NoSpawnCommand))
GameManifest.Current.entities = GameManifest.Current.entities.AddItem(name).ToArray();
}
catch (Exception ex)
{
LogError($"Error adding entity to GameManifest: {prefabPath}", ex);
}
try
{
if (customEntity != null)
customEntity.OnCustomEntityRegistered();
}
catch (Exception ex)
{
LogError($"Error calling OnCustomEntityRegistered for {prefabPath}", ex);
}
try
{
if (!DeferSaveLoad)
LoadSaveFile();
}
catch (Exception ex)
{
LogError($"Error loading entity save for {prefabPath}", ex);
}
LogDebug($"Successfully registered entity: {prefabPath}");
}
catch (Exception ex)
{
LogError($"Unexpected error in RegisterEntity for '{prefabPath}'", ex);
}
}
}
private static bool LoadSaveFile(string strFilename)
{
Dictionary<global::BaseEntity, SaveLoadInfo> dictionary = new Dictionary<BaseEntity, SaveLoadInfo>();
try
{
if (!File.Exists(strFilename))
{
LogWarning($"Couldn't load \"{strFilename}\" - file doesn't exist");
return false;
}
using (FileStream fileStream = File.OpenRead(strFilename))
{
HashSet<NetworkableId> hashSet = Pool.Get<HashSet<NetworkableId>>();
HashSet<ulong> seenPlayerIds = Pool.Get<HashSet<ulong>>();
try
{
foreach (BaseNetworkable networkable in BaseNetworkable.serverEntities)
{
if (networkable != null && networkable.net != null)
hashSet.Add(networkable.net.ID);
}
if (!ReadSaveFileAndPreloadEntities(fileStream, dictionary, hashSet, seenPlayerIds))
{
// Read failed/aborted - return the pooled protos we already parsed
// into the dictionary so they don't leak.
SaveLoad.Free(dictionary);
return false;
}
}
finally
{
Pool.FreeUnmanaged(ref hashSet);
Pool.FreeUnmanaged(ref seenPlayerIds);
}
}
LogWarning($"Spawning {dictionary.Count} entities from save \"{strFilename}\"", StackTraceLogType.None);
SaveLoad.ProcessSaveEntities(dictionary);
Rust.Application.isLoadingSave = false;
return true;
}
catch (Exception ex2)
{
LogError($"Error loading save ({strFilename})", ex2);
return false;
}
}
/// <summary>
/// Load several prefabs' save files as one batch: preload every prefab's entities
/// (ReadSaveFileAndPreloadEntities) into a single dictionary, then ProcessSaveEntities once
/// for the whole set. One prefab's file failing does not abort the rest.
/// </summary>
private static bool LoadSaveFiles(IEnumerable<BaseCustomEntityPrefab> prefabs)
{
Dictionary<BaseEntity, SaveLoadInfo> dictionary = new Dictionary<BaseEntity, SaveLoadInfo>();
try
{
bool canLoad = Rust.Application.isServerStarted || World.LoadedFromSave;
HashSet<NetworkableId> hashSet = Pool.Get<HashSet<NetworkableId>>();
HashSet<ulong> seenPlayerIds = Pool.Get<HashSet<ulong>>();
try
{
foreach (BaseNetworkable networkable in BaseNetworkable.serverEntities)
{
if (networkable != null && networkable.net != null)
hashSet.Add(networkable.net.ID);
}
foreach (BaseCustomEntityPrefab prefab in prefabs)
{
try
{
if (prefab == null || prefab.entity == null || prefab.SavingDisabled || prefab.prefabID == default || !canLoad)
continue;
string saveFilePath = prefab.GetSaveFilePath();
if (!File.Exists(saveFilePath))
{
LogDebug($"No save file found for prefab: {prefab.prefabPath}");
continue;
}
using (FileStream fileStream = File.OpenRead(saveFilePath))
{
if (!ReadSaveFileAndPreloadEntities(fileStream, dictionary, hashSet, seenPlayerIds))
LogWarning($"Aborted reading save \"{saveFilePath}\" - keeping already-preloaded entities");
}
}
catch (Exception ex)
{
LogError($"Error reading save for bundled prefab {prefab?.prefabPath}", ex);
}
}
}
finally
{
Pool.FreeUnmanaged(ref hashSet);
Pool.FreeUnmanaged(ref seenPlayerIds);
}
LogWarning($"Spawning {dictionary.Count} entities from bundle saves", StackTraceLogType.None);
SaveLoad.ProcessSaveEntities(dictionary);
Rust.Application.isLoadingSave = false;
return true;
}
catch (Exception ex2)
{
LogError("Error loading bundle saves", ex2);
SaveLoad.Free(dictionary);
return false;
}
}
private static bool ReadSaveFileAndPreloadEntities(FileStream fileStream, Dictionary<BaseEntity, SaveLoadInfo> dictionary, HashSet<NetworkableId> hashSet, HashSet<ulong> seenPlayerIds)
{
using (BinaryReader binaryReader = new BinaryReader(fileStream))
{
if (binaryReader.ReadSByte() != SAVE_HEADER[0] || binaryReader.ReadSByte() != SAVE_HEADER[1] || binaryReader.ReadSByte() != SAVE_HEADER[2] || binaryReader.ReadSByte() != SAVE_HEADER[3])
{
LogWarning("Invalid save (missing header)");
return false;
}
if (binaryReader.PeekChar() == MARKER_WIPE_ID)
{
binaryReader.ReadChar();
string wipeId = binaryReader.ReadString();
}
if (binaryReader.PeekChar() == MARKER_CREATED_TIME)
{
binaryReader.ReadChar();
long createdTime = (long)binaryReader.ReadInt32();
}
if (binaryReader.ReadUInt32() != Rust.Protocol.save)
{
if (!config.AllowOutOfDateSaves)
{
LogWarning("Skipped loading the save due to an outdated version. You can allow it with a parameter in the config.");
return false;
}
}
Rust.Application.isLoadingSave = true;
while (fileStream.Position < fileStream.Length)
{
SaveLoadInfo saveLoadInfo = new SaveLoadInfo();
uint entDataLength = binaryReader.ReadUInt32();
long position = fileStream.Position;
// Guard against a corrupt/hostile length prefix before allocating or seeking.
if (entDataLength > MAX_ENTITY_DATA_BYTES || position + entDataLength > fileStream.Length)
{
LogWarning($"Aborting save read - invalid entity length {entDataLength} at position {position} (file length {fileStream.Length})");
break;
}
ProtoBuf.Entity entData = new ProtoBuf.Entity();
try
{
entData.ReadFromStream(fileStream, (int)entDataLength, false);
saveLoadInfo.entity = entData;
position = fileStream.Position;
}
catch (Exception ex)
{
LogWarning($"Skipping entity since it could not be deserialized - stream position: {position} size: {entDataLength}");
Debug.LogError(ex);
fileStream.Position = position + (long)((ulong)entDataLength);
Pool.Free(ref entData);
continue;
}
if (binaryReader.PeekChar() == MARKER_CUSTOM_DATA)
{
binaryReader.ReadChar();
uint customDataLength = binaryReader.ReadUInt32();
// Guard the custom-data block length the same way.
if (customDataLength > MAX_CUSTOM_DATA_BYTES || fileStream.Position + customDataLength > fileStream.Length)
{
LogWarning($"Aborting save read - invalid custom-data length {customDataLength} at position {fileStream.Position} (file length {fileStream.Length})");
Pool.Free(ref entData);
break;
}
try
{
byte[] data = binaryReader.ReadBytes((int)customDataLength);
MemoryStream stream = new MemoryStream(data);
saveLoadInfo.stream = stream;
}
catch (Exception ex)
{
LogWarning($"[2] Skipping entity since it could not be deserialized - stream position: {position} size: {customDataLength}");
Debug.LogError(ex);
fileStream.Position = position + (long)((ulong)customDataLength);
continue;
}
}
position = fileStream.Position;
if (entData.basePlayer != null && seenPlayerIds.Contains(entData.basePlayer.userid))
{
LogWarning($"Skipping entity {entData.baseNetworkable.uid} - it's a player {entData.basePlayer.userid} who is in the save multiple times");
Facepunch.Pool.Free<global::ProtoBuf.Entity>(ref entData);
if (saveLoadInfo.stream != null)
saveLoadInfo.stream.Dispose();
}
else if (entData.baseNetworkable.uid.IsValid && hashSet.Contains(entData.baseNetworkable.uid))
{
LogWarning($"Skipping entity {entData.baseNetworkable.uid} {StringPool.Get(entData.baseNetworkable.prefabID)} - uid is used multiple times");
Facepunch.Pool.Free<ProtoBuf.Entity>(ref entData);
if (saveLoadInfo.stream != null)
saveLoadInfo.stream.Dispose();
}
else
{
if (entData.baseNetworkable.uid.IsValid)
{
hashSet.Add(entData.baseNetworkable.uid);
}
global::BaseEntity baseEntity = GameManager.server.CreateEntity(StringPool.Get(entData.baseNetworkable.prefabID), entData.baseEntity.pos, Quaternion.Euler(entData.baseEntity.rot), true);
if (baseEntity)
{
baseEntity.InitLoad(entData.baseNetworkable.uid);
baseEntity.PreServerLoad();
dictionary.Add(baseEntity, saveLoadInfo);
if (entData.basePlayer != null)
seenPlayerIds.Add(entData.basePlayer.userid);
}
}
}
}
return true;
}
private static void PruneDeadInstances()
{
List<string> emptyKeys = null;
foreach (KeyValuePair<string, List<GameObject>> pair in AllObjects)
{
pair.Value.RemoveAll(go => go == null);
if (pair.Value.Count == 0)
(emptyKeys ??= new List<string>()).Add(pair.Key);
}
if (emptyKeys != null)
{
foreach (string key in emptyKeys)
AllObjects.Remove(key);
}
}
private static void GetAllCustomEntityPrefabs(List<BaseCustomEntityPrefab> list)
{
foreach (PluginContext plugi
// ... [Code preview truncated due to length. Full source in attached file] ...