Иконка ресурса

FREE ServerPrefabs : 1.1.1 1.1.1

Нет прав для скачивания
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&lt;MyEntity&gt;(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&lt;MyEntity&gt;(idA);
        /// bundle.CreatePrefab("b").AddEntityComponent&lt;MyEntity&gt;(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] ...

Автор
15FPS
Скачиваний
0
Просмотры
1
Первый выпуск
Обновление
Оценка
0.00 звёзд 0 оценок
Сверху