C#:
using Facepunch;
using Facepunch.Extend;
using Network;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
using Rust;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using UnityEngine;
namespace Oxide.Plugins {
[Info("Custom Entities", "Nikedemos", "1.0.20")]
[Description("A robust framework for registering, spawning, loading and saving entity prefabs")]
public class CustomEntities : RustPlugin {
#region CONST
public const string FORMAT_FILENAME = "{0}.{1}";
public const string PREFAB_PREFIX = "assets/custom/";
public const string PREFAB_SPHERE = "assets/prefabs/visualization/sphere.prefab";
public const string PREFAB_WOOD_STORAGE_BOX = "assets/prefabs/deployable/woodenbox/woodbox_deployed.prefab";
public const string PREFAB_FX_IMPACT_METAL_BLUNT = "assets/bundled/prefabs/fx/impacts/blunt/metal/metal1.prefab";
public const string PREFAB_FX_IMPACT_METAL_SLASH = "assets/bundled/prefabs/fx/impacts/slash/metal/metal1.prefab";
public const string PREFAB_FX_IMPACT_METAL_STAB = "assets/bundled/prefabs/fx/impacts/stab/metal/metal1.prefab";
public const string PREFAB_FX_IMPACT_METAL_BULLET = "assets/bundled/prefabs/fx/impacts/bullet/metal/metal1.prefab";
public const string PREFAB_PLAYER = "assets/prefabs/player/player.prefab";
public const string PREFAB_FX_IMPACT_METAL_PHYSICAL = "assets/bundled/prefabs/fx/impacts/physics/phys-impact-metal-hollow-hard.prefab";
public const string PREFAB_FX_EXPLOSION = "assets/bundled/prefabs/fx/impacts/additive/explosion.prefab";
public const string PREFAB_FX_FIRE = "assets/bundled/prefabs/fx/impacts/additive/fire.prefab";
public const string CMD_SPAWN_AT = "spawn_at";
public const string CMD_PURGE_PREFAB = "purge_prefab";
public const string CMD_PURGE_PLUGIN = "purge_plugin";
public const string CMD_COUNT_PLUGIN = "count_plugin";
public const string PREFIX_CUSTOMENTITIES = "customentities";
public const string PERM_ADMIN = PREFIX_CUSTOMENTITIES + ".admin";
//required for the commands above
public const string CONVAR_VERBOSE_LOGGING = "verbose_logging";
#endregion
#region STATIC
public static CustomEntities Instance;
public static bool Unloading = false;
public static Effect ReusableEffect;
public static readonly ReadOnlyDictionary<DamageType, float> DEFAULT_PROTECTION_AMOUNTS = new ReadOnlyDictionary<DamageType, float>(new Dictionary<DamageType, float> {
[DamageType.Generic]
= 0F, //Generic
[DamageType.Hunger]
= 0F, //Hunger
[DamageType.Thirst]
= 0F, //Thirst
[DamageType.Cold]
= 0F, //Cold
[DamageType.Drowned]
= 0F, //Drowned
[DamageType.Heat]
= 0F, //Heat
[DamageType.Bleeding]
= 0F, //Bleeding
[DamageType.Poison]
= 0F, //Poison
[DamageType.Suicide]
= 0F, //Suicide
[DamageType.Bullet]
= 0F, //Bullet
[DamageType.Slash]
= 0F, //Slash
[DamageType.Blunt]
= 0F, //Blunt
[DamageType.Fall]
= 0F, //Fall
[DamageType.Radiation]
= 0F, //Radiation
[DamageType.Bite]
= 0F, //Bite
[DamageType.Stab]
= 0F, //Stab
[DamageType.Explosion]
= 0F, //Explosion
[DamageType.RadiationExposure]
= 0F, //RadiationExposure
[DamageType.ColdExposure]
= 0F, //ColdExposure
[DamageType.Decay]
= 0F, //Decay
[DamageType.ElectricShock]
= 0F, //ElectricShock
[DamageType.Arrow]
= 0F, //Arrow
[DamageType.AntiVehicle]
= 0F, //AntiVehicle
[DamageType.Collision]
= 0F, //Collision
[DamageType.Fun_Water]
= 0F //Fun Water
}
);
#endregion
#region LANG
public const string MSG_PREFAB_REGISTERING_CUSTOM = nameof(MSG_PREFAB_REGISTERING_CUSTOM);
public const string MSG_PREFAB_REGISTERING_MODIFIED = nameof(MSG_PREFAB_REGISTERING_MODIFIED);
public const string MSG_PREFAB_REGISTRATION_EXCEPTION = nameof(MSG_PREFAB_REGISTRATION_EXCEPTION);
public const string MSG_BUNDLE_UNREGISTRATION_REMOVED_VANILLA = nameof(MSG_BUNDLE_UNREGISTRATION_REMOVED_VANILLA);
public const string MSG_BUNDLE_UNREGISTRATION_REMOVED_CUSTOM = nameof(MSG_BUNDLE_UNREGISTRATION_REMOVED_CUSTOM);
public const string MSG_SAVING_SAVEFILES = nameof(MSG_SAVING_SAVEFILES);
public const string MSG_SAVED_ENTITIES_1 = nameof(MSG_SAVED_ENTITIES_1);
public const string MSG_SAVING_DATAFILE_EXCEPTION = nameof(MSG_SAVING_DATAFILE_EXCEPTION);
public const string MSG_LOADING_NO_SAVEFILE = nameof(MSG_LOADING_NO_SAVEFILE);
public const string MSG_LOADING_YES_DATAFILE = nameof(MSG_LOADING_YES_DATAFILE);
public const string MSG_LOADING_FRESH_WIPE_DETECTED = nameof(MSG_LOADING_FRESH_WIPE_DETECTED);
public const string MSG_LOADING_EMPTY_SAVEFILE = nameof(MSG_LOADING_EMPTY_SAVEFILE);
public const string MSG_LOADING_ENTITIES_FROM_SAVEFILE = nameof(MSG_LOADING_ENTITIES_FROM_SAVEFILE);
public const string MSG_LOADING_ERROR_ENTITY_ALREADY_EXISTS = nameof(MSG_LOADING_ERROR_ENTITY_ALREADY_EXISTS);
public const string MSG_LOADING_ERROR_ENTITY_COULDNT_SPAWN = nameof(MSG_LOADING_ERROR_ENTITY_COULDNT_SPAWN);
public const string MSG_LOADING_SPAWNING_ENTITIES = nameof(MSG_LOADING_SPAWNING_ENTITIES);
public const string MSG_LOADED_ENTITIES = nameof(MSG_LOADED_ENTITIES);
public const string MSG_LOADING_DATAFILE_LOAD_EXCEPTION = nameof(MSG_LOADING_DATAFILE_LOAD_EXCEPTION);
public const string MSG_LOADING_NEW_SAVEFILE = nameof(MSG_LOADING_NEW_SAVEFILE);
public const string MSG_LOADING_DATAFILE_NEW_EXCEPTION = nameof(MSG_LOADING_DATAFILE_NEW_EXCEPTION);
public const string MSG_SAVING_BACKUP_EXCEPTION = nameof(MSG_SAVING_BACKUP_EXCEPTION);
public const string MSG_CMD_PROVIDE_PREFAB = nameof(MSG_CMD_PROVIDE_PREFAB);
public const string MSG_CMD_PROVIDE_PLUGIN_NAME = nameof(MSG_CMD_PROVIDE_PLUGIN_NAME);
public const string MSG_CMD_PURGE_PREFAB_KILLED_ENTITIES = nameof(MSG_CMD_PURGE_PREFAB_KILLED_ENTITIES);
public const string MSG_CMD_PURGE_PREFAB_NO_ENTITIES_FOUND = nameof(MSG_CMD_PURGE_PREFAB_NO_ENTITIES_FOUND);
public const string MSG_CMD_PURGE_PLUGIN_NO_PLUGINS_REGISTERED = nameof(MSG_CMD_PURGE_PLUGIN_NO_PLUGINS_REGISTERED);
public const string MSG_CMD_PURGE_PLUGIN_MAKE_SURE_LOADED = nameof(MSG_CMD_PURGE_PLUGIN_MAKE_SURE_LOADED);
public const string MSG_CMD_PURGE_PLUGIN_NOTHING_FOUND = nameof(MSG_CMD_PURGE_PLUGIN_NOTHING_FOUND);
public const string MSG_CMD_PURGE_PLUGIN_NO_ENTITIES_FOUND = nameof(MSG_CMD_PURGE_PLUGIN_NO_ENTITIES_FOUND);
public const string MSG_CMD_PURGE_PLUGIN_KILLED_ENTITIES = nameof(MSG_CMD_PURGE_PLUGIN_KILLED_ENTITIES);
public const string MSG_TRY_COMPO_REPLACE_ERROR = nameof(MSG_TRY_COMPO_REPLACE_ERROR);
public const string MSG_TRY_COMPO_REPLACE_ERROR_GAMEOBJECT_NULL = nameof(MSG_TRY_COMPO_REPLACE_ERROR_GAMEOBJECT_NULL);
public const string MSG_TRY_COMPO_REPLACE_ERROR_NO_OLD_COMPO_ATTACHED = nameof(MSG_TRY_COMPO_REPLACE_ERROR_NO_OLD_COMPO_ATTACHED);
public const string MSG_TRY_COMPO_REPLACE_ERROR_ALREADY_HAS_NEW_COMPO = nameof(MSG_TRY_COMPO_REPLACE_ERROR_ALREADY_HAS_NEW_COMPO);
public const string MSG_TRY_COMPO_REPLACE_DEBUG_HEADER = nameof(MSG_TRY_COMPO_REPLACE_DEBUG_HEADER);
public const string MSG_TRY_COMPO_REPLACE_DEBUG_FIELD_INFO = nameof(MSG_TRY_COMPO_REPLACE_DEBUG_FIELD_INFO);
public const string MSG_TRY_COMPO_REPLACE_DEBUG_FOUND_SELF_REFERENCE_THIS_COMPO = nameof(MSG_TRY_COMPO_REPLACE_DEBUG_FOUND_SELF_REFERENCE_THIS_COMPO);
public const string MSG_TRY_COMPO_REPLACE_DEBUG_FOUND_SELF_REFERENCE_OTHER_COMPO = nameof(MSG_TRY_COMPO_REPLACE_DEBUG_FOUND_SELF_REFERENCE_OTHER_COMPO);
public const string MSG_TRY_COMPO_REPLACE_DEBUG_SUBCOMPO_NOT_FOUND_IN_DESTINATION = nameof(MSG_TRY_COMPO_REPLACE_DEBUG_SUBCOMPO_NOT_FOUND_IN_DESTINATION);
private static readonly Dictionary<string, string> LangMessages = new Dictionary<string, string> {
[MSG_PREFAB_REGISTERING_CUSTOM]
= "Registering custom entity {0} : {1} as prefab \"{2}\"...", [MSG_PREFAB_REGISTERING_MODIFIED] = "Registering modified entity based on prefab \"{0}\" as prefab \"{1}\"...", [MSG_PREFAB_REGISTRATION_EXCEPTION] = "Exception while trying to register prefab: {0}\n{1}", [MSG_BUNDLE_UNREGISTRATION_REMOVED_VANILLA] = "Removed {0} instances of various vanilla entities handled by the savefile from world.", [MSG_BUNDLE_UNREGISTRATION_REMOVED_CUSTOM] = "Removed {0} instances of a custom prefab \"{1}\" from world.", [MSG_SAVING_SAVEFILES] = "Saving {0} binary savefiles...", [MSG_SAVED_ENTITIES_1] = "INFO: Saved {0} out of {1} entities ({2} custom, {3} vanilla. {4} removed because invalid) to \"{5}\"", [MSG_SAVING_DATAFILE_EXCEPTION] = "ERROR: {0} while trying to save \"{1}\": {2}\n{3}", [MSG_LOADING_NO_SAVEFILE] = "INFO: Savefile \"{0}\" doesn't exist, nothing to load.", [MSG_LOADING_YES_DATAFILE] = "Loading savefile from \"{0}\"...", [MSG_LOADING_FRESH_WIPE_DETECTED] = "INFO: The ID of the current Community Entity ({0}) is different than the one from the savefile ({1}) - this usually means a fresh wipe or switching between maps mid-wipes. A new savefile will be generated.", [MSG_LOADING_EMPTY_SAVEFILE] = "INFO: The savefile \"{0}\" contains no entities, nothing to load.", [MSG_LOADING_ENTITIES_FROM_SAVEFILE] = "Loading {0} entities from the savefile \"{1}\"...", [MSG_LOADING_ERROR_ENTITY_ALREADY_EXISTS] = "ERROR: The savefile \"{0}\" contains an entity with prefab ID {1} ({2}), Net.ID {3}, but another entity with that Net.ID already exists. Skipping to the next entity...", [MSG_LOADING_ERROR_ENTITY_COULDNT_SPAWN] = "ERROR: The savefile \"{0}\" contains an entity with prefab ID {1} ({2}), Net.ID {3} that could not be spawned. Skipping to the next entity...", [MSG_LOADING_SPAWNING_ENTITIES] = "Spawning {0} entities from the save file at {1}...", [MSG_LOADED_ENTITIES] = "INFO: Loaded {0} out of {1} entities ({2} custom, {3} vanilla) from \"{4}\"", [MSG_LOADING_DATAFILE_LOAD_EXCEPTION] = "ERROR: {0} while trying to load \"{1}\": {2}\n{3}", [MSG_LOADING_NEW_SAVEFILE] = "INFO: Generating default (blank) savefile at \"{0}\"...", [MSG_LOADING_DATAFILE_NEW_EXCEPTION] = "ERROR: {0} while trying to create a new datafile \"{1}\": {2}\n{3}", [MSG_CMD_PROVIDE_PREFAB] = "Please provide the short prefab name, or the initial part of it.", [MSG_CMD_PROVIDE_PLUGIN_NAME] = "Please provide the plugin name, or the initial part of it.", [MSG_CMD_PURGE_PREFAB_KILLED_ENTITIES] = "Purged {0} entities matching short prefab name \"{1}\"", [MSG_CMD_PURGE_PREFAB_NO_ENTITIES_FOUND] = "Could not find any entities matching short prefab name \"{0}\"", [MSG_SAVING_BACKUP_EXCEPTION] = "ERROR: {0} while trying to save backups of the datafile \"{1}\": {2}\n{3}", [MSG_CMD_PURGE_PLUGIN_NO_PLUGINS_REGISTERED] = "There's currently no Custom Entities plugins registered.", [MSG_CMD_PURGE_PLUGIN_MAKE_SURE_LOADED] = "Make sure it's loaded in. If it isn't, just delete the appropriate savefile from /oxide/data/CustomEntities/ and it will have the same effect.", [MSG_CMD_PURGE_PLUGIN_NOTHING_FOUND] = "Could not find any Plugins matching the provided name \"{0}\"", [MSG_CMD_PURGE_PLUGIN_NO_ENTITIES_FOUND] = "Could not find any entities from the Plugin \"{0}\"", [MSG_CMD_PURGE_PLUGIN_KILLED_ENTITIES] = "Purged {0} entities from the Plugin \"{1}\"", [MSG_TRY_COMPO_REPLACE_ERROR] = "ERROR WHILE TRYING TO REPLACE `{0}` with `{1}`:", [MSG_TRY_COMPO_REPLACE_ERROR_GAMEOBJECT_NULL] = "The GameObject is null!", [MSG_TRY_COMPO_REPLACE_ERROR_NO_OLD_COMPO_ATTACHED] = "There's no `{0}` component attached to the GameObject!", [MSG_TRY_COMPO_REPLACE_ERROR_ALREADY_HAS_NEW_COMPO] = "The GameObject already has a `{0}` component attached (and there's only one allowed!)", [MSG_TRY_COMPO_REPLACE_DEBUG_HEADER] = "`{0}` is a subclass of `{1}`, replacing matching fields:\n", [MSG_TRY_COMPO_REPLACE_DEBUG_FIELD_INFO] = " set `{0}` to `{1}`", [MSG_TRY_COMPO_REPLACE_DEBUG_FOUND_SELF_REFERENCE_THIS_COMPO] = " FOUND SELF REFERENCE IN THE OLD COMPONENT in the field `{0}`. SETTING NEW COMPONENT'S SELF REFERENCE TO ITSELF.", [MSG_TRY_COMPO_REPLACE_DEBUG_FOUND_SELF_REFERENCE_OTHER_COMPO] = " FOUND REFERENCE TO THE OLD COMPONENT IN ANOTHER COMPONENT IN THE HIERARCHY ({0}) in the field `{1}`. SETTING THAT COMPONENT'S REFERENCE TO THE NEW COMPONENT.", [MSG_TRY_COMPO_REPLACE_DEBUG_SUBCOMPO_NOT_FOUND_IN_DESTINATION] = " COULD NOT MATCH THE SOURCE SUB-COMPONENT WITH THE DESTINATION COMPONENT ({0}) in the field `{1}`. THE DESTINATION COMPONENT DOESN'T EXIST. THIS IS BAD."
}
;
private static string MSG(string msg, string userID = null, params object[] args) {
if (args == null) {
return Instance.lang.GetMessage(msg, Instance, userID);
}
else {
return string.Format(Instance.lang.GetMessage(msg, Instance, userID), args);
}
}
#endregion
#region COVALENCE COMMANDS
private void CommandCountPlugin(IPlayer iplayer, string command, string[] args) {
if (args.Length == 0) {
iplayer.Reply(MSG(MSG_CMD_PROVIDE_PLUGIN_NAME, iplayer.Id));
return;
}
string pluginName = args[0].ToLower();
BinaryData.PlayerRequestedPluginCount(iplayer, pluginName);
}
private void CommandPurgePlugin(IPlayer iplayer, string command, string[] args) {
if (args.Length == 0) {
iplayer.Reply(MSG(MSG_CMD_PROVIDE_PLUGIN_NAME, iplayer.Id));
return;
}
string pluginName = args[0].ToLower();
BinaryData.PlayerRequestedPluginPurge(iplayer, pluginName);
}
private void CommandPurgePrefab(IPlayer iplayer, string command, string[] args) {
if (args.Length == 0) {
iplayer.Reply(MSG(MSG_CMD_PROVIDE_PREFAB, iplayer.Id));
return;
}
string prefabName = args[0].ToLower();
BaseEntity[] iterateOver = BaseNetworkable.serverEntities.OfType<BaseEntity>().ToArray();
int countKilled = 0;
for (int i = 0; i < iterateOver.Length; i++) {
BaseEntity entity = iterateOver[i];
if (entity == null) {
continue;
}
if (entity.IsDestroyed) {
continue;
}
if (entity.net == null) {
continue;
}
if (!entity.ShortPrefabName.StartsWith(prefabName)) {
continue;
}
entity.Kill(BaseNetworkable.DestroyMode.None);
countKilled++;
}
if (countKilled == 0) {
iplayer.Reply(MSG(MSG_CMD_PURGE_PREFAB_NO_ENTITIES_FOUND, iplayer.Id, prefabName));
return;
}
iplayer.Reply(MSG(MSG_CMD_PURGE_PREFAB_KILLED_ENTITIES, iplayer.Id, countKilled, prefabName));
}
private void CommandSpawnAtPlayerEyes(IPlayer iplayer, string command, string[] args) {
BasePlayer player = iplayer.Object as BasePlayer;
if (player == null) {
return;
}
if (args.Length == 0) {
iplayer.Reply(MSG(MSG_CMD_PROVIDE_PREFAB, iplayer.Id));
return;
}
string prefabName = args[0].ToLower();
string commandResponse = ConVar.Entity.svspawn(prefabName, player.eyes.position, Vector3.zero);
iplayer.Reply(commandResponse);
}
#endregion
#region HOOK SUBSCRIPTIONS
void Init() {
lang.RegisterMessages(LangMessages, this);
}
void OnServerInitialized() {
Instance = this;
ReusableEffect = new Effect();
permission.RegisterPermission(PERM_ADMIN, this);
AddCovalenceCommand(CMD_PURGE_PREFAB, nameof(CommandPurgePrefab), PERM_ADMIN);
AddCovalenceCommand(CMD_PURGE_PLUGIN, nameof(CommandPurgePlugin), PERM_ADMIN);
AddCovalenceCommand(CMD_SPAWN_AT, nameof(CommandSpawnAtPlayerEyes), PERM_ADMIN);
AddCovalenceCommand(CMD_COUNT_PLUGIN, nameof(CommandCountPlugin), PERM_ADMIN);
CustomConvars.Init();
BinaryData.Init();
CastingNonAlloc.Init();
CustomPrefabs.Init();
}
void Unload() {
if (Instance == null) {
return;
}
Unloading = true;
//after this point, no saving and unregistering possible
CustomPrefabs.Unload();
BinaryData.Unload();
//doesn't do much either.
CastingNonAlloc.Unload();
CustomConvars.Unload();
//unload top-level static
Unloading = false;
Instance = null;
ReusableEffect = null;
}
void OnServerSave() {
if (Instance == null) {
return;
}
BinaryData.SaveAll();
}
void OnItemAddedToContainer(ItemContainer container, Item item) {
if (Instance == null) {
return;
}
List<BaseEntity> maybeCustomList = GetOwnerThingCustomSaveList(container);
if (maybeCustomList == null) {
return;
}
CustomPrefabs.EnsureMovedToCustomSaveListRecursively(item, maybeCustomList);
}
void OnItemRemovedFromContainer(ItemContainer container, Item item) {
if (Instance == null) {
return;
}
List<BaseEntity> maybeCustomList = GetOwnerThingCustomSaveList(container);
if (maybeCustomList == null) {
return;
}
CustomPrefabs.EnsureMovedToVanillaSaveListRecursively(item);
}
void OnEntityKill(BaseEntity entity) {
if (Instance == null) {
return;
}
if (Unloading) {
return;
}
//only vanilla
List<BaseEntity> checkForVanillaEnityList = CustomPrefabs.TryGetEntityCustomSaveList(entity, CustomPrefabs.SaveListEntityType.Vanilla);
if (checkForVanillaEnityList == null) {
return;
}
CustomPrefabs.ForgetEntityFromSaveListAndCache(entity, checkForVanillaEnityList);
}
#endregion
#region CUSTOM PREFAB MANAGER
public static class CustomPrefabs {
private static GameObjectRef _emptyImpactGameObjectRef = null;
private static Dictionary<string, GameObject> _prefabsPreProcessedCustom = null;
private static Dictionary<GameObject, BaseEntity> _preProcessedGoToEntityCache = null;
private static ListHashSet<GenericPrefabRecipe> _cachedRecipes = null;
private static Dictionary<Plugin, BinaryData> _pluginToBinaryData = null;
private static List<string> _gameManifestEntityList = null;
//this is not vanilla, just for building the vanilla manifest array
private static Dictionary<ulong, List<BaseEntity>> _vanillaEntityToCustomSaveList = null;
public static Dictionary<string, uint> ModifiedPrefabFullNameToModifiedPrefabIDHandledByBundles = null;
public static Dictionary<string, List<BaseEntity>> ModifiedPrefabFullNameToCustomSaveList = null;
public static void Init() {
_emptyImpactGameObjectRef = new GameObjectRef();
_prefabsPreProcessedCustom = new Dictionary<string, GameObject>();
_preProcessedGoToEntityCache = new Dictionary<GameObject, BaseEntity>();
_gameManifestEntityList = new List<string>();
_cachedRecipes = new ListHashSet<GenericPrefabRecipe>();
_pluginToBinaryData = new Dictionary<Plugin, BinaryData>();
_vanillaEntityToCustomSaveList = new Dictionary<ulong, List<BaseEntity>>();
ModifiedPrefabFullNameToModifiedPrefabIDHandledByBundles = new Dictionary<string, uint>();
ModifiedPrefabFullNameToCustomSaveList = new Dictionary<string, List<BaseEntity>>();
}
public static void Unload() {
_emptyImpactGameObjectRef = null;
_prefabsPreProcessedCustom = null;
_preProcessedGoToEntityCache = null;
_gameManifestEntityList = null;
_cachedRecipes = null;
_pluginToBinaryData = null;
_vanillaEntityToCustomSaveList = null;
ModifiedPrefabFullNameToModifiedPrefabIDHandledByBundles = null;
ModifiedPrefabFullNameToCustomSaveList = null;
}
public static BaseEntity TryGetPreprocessedPrototypeFromVanilla(string prefabName) => TryGetPreprocessedPrototypeFrom(GameManager.server.preProcessed.prefabList, prefabName);
public static BaseEntity TryGetPreprocessedPrototypeFromCustom(string prefabName) => TryGetPreprocessedPrototypeFrom(_prefabsPreProcessedCustom, prefabName);
private static BaseEntity TryGetPreprocessedPrototypeFrom(Dictionary<string, GameObject> preProcessedDictionary, string prefabName) {
GameObject goResult;
if (!preProcessedDictionary.TryGetValue(prefabName, out goResult)) {
return null;
}
//is it in the cache?
BaseEntity baseEntityResult;
if (!_preProcessedGoToEntityCache.TryGetValue(goResult, out baseEntityResult)) {
//get the component... if non null, add to cache
baseEntityResult = goResult.GetComponent<BaseEntity>();
if (baseEntityResult != null) {
_preProcessedGoToEntityCache.Add(goResult, baseEntityResult);
}
}
//now you get it or a null
return baseEntityResult;
}
//public API
public static void ForgetEntityFromSaveListAndCache(BaseEntity vanillaEntity, List<BaseEntity> maybeCustomList) {
maybeCustomList.Remove(vanillaEntity);
_vanillaEntityToCustomSaveList.Remove(vanillaEntity.net.ID.Value);
}
public enum SaveListEntityType {
Vanilla, Custom, VanillaAndCustom
}
public static List<BaseEntity> TryGetEntityCustomSaveList(BaseEntity vanillaOrCustomEntity, SaveListEntityType entityType) {
if (!BaseNetworkableEx.IsValid(vanillaOrCustomEntity)) {
return null;
}
bool checkForVanilla = (entityType == SaveListEntityType.Vanilla) || (entityType == SaveListEntityType.VanillaAndCustom);
bool checkForCustom = (entityType == SaveListEntityType.Custom) || (entityType == SaveListEntityType.VanillaAndCustom);
if (checkForCustom) {
ICustomEntity asInterface = vanillaOrCustomEntity as ICustomEntity;
if (asInterface != null) {
//no need to check for vanilla any more
return asInterface.SaveListInDataFile;
}
}
if (checkForVanilla) {
List<BaseEntity> maybeCustomList;
if (!_vanillaEntityToCustomSaveList.TryGetValue(vanillaOrCustomEntity.net.ID.Value, out maybeCustomList)) {
//neither custom nor vanilla.
return null;
}
return maybeCustomList;
}
return null;
}
public static void EnsureMovedToVanillaSaveList(BaseEntity vanillaEntity) {
ICustomEntity asInterface = vanillaEntity as ICustomEntity;
if (asInterface != null) {
return;
}
List<BaseEntity> maybeAlreadyExistingList = TryGetEntityCustomSaveList(vanillaEntity, SaveListEntityType.Vanilla);
if (maybeAlreadyExistingList != null) {
//it exists in some other custom save list, so let's forget it first...
ForgetEntityFromSaveListAndCache(vanillaEntity, maybeAlreadyExistingList);
}
vanillaEntity.EnableSaving(true);
//this will add it back
}
public static void EnsureMovedToVanillaSaveListRecursively(Item item) {
ForEachItemEntityRecursively(item, (entity) => EnsureMovedToVanillaSaveList(entity));
}
public static void EnsureMovedToCustomSaveListRecursively(Item item, List<BaseEntity> targetList) {
ForEachItemEntityRecursively(item, (entity) => EnsureMovedToCustomSaveList(entity, targetList));
}
private static void EnsureMovedToCustomSaveListRecursivelyEntireContainer(ItemContainer itemContainer, List<BaseEntity> saveList) {
if (itemContainer == null) {
return;
}
var itemListCount = itemContainer.itemList?.Count ?? 0;
if (itemListCount == 0) {
return;
}
for (var i = 0; i < itemContainer.itemList.Count; i++) {
var item = itemContainer.itemList[i];
if (item == null) {
continue;
}
EnsureMovedToCustomSaveListRecursively(item, saveList);
}
}
private static void ForEachEntityInTransformHierarchyRecursivelyAndAlsoConsideringItemsInInventories(BaseEntity entity, Action<BaseEntity> entityAction, List<BaseEntity> saveList) {
entityAction(entity);
//still!
if (entity == null) {
return;
}
//so we have several "obvious" cases here
//in fact, maybe we should re-visit MetaphysicsEnchantedItems to get everything that has a container, presumably?
//first obvious case:
var someSortOfContainer = entity as IItemContainerEntity;
//this covers a broad range of things that have "ItemContainer inventory".
if (someSortOfContainer != null) {
//this will do nothing if the inventory is null or empty
EnsureMovedToCustomSaveListRecursivelyEntireContainer(someSortOfContainer.inventory, saveList);
}
else {
var asDroppedItem = entity as WorldItem;
if (asDroppedItem != null) {
//this will do nothing if the asDroppedItem.item is null.
EnsureMovedToCustomSaveListRecursively(asDroppedItem.item, saveList);
}
else {
var asPlayer = entity as BasePlayer;
if (asPlayer != null) {
var playerInventory = asPlayer.inventory;
if (playerInventory != null) {
EnsureMovedToCustomSaveListRecursivelyEntireContainer(playerInventory.containerMain, saveList);
EnsureMovedToCustomSaveListRecursivelyEntireContainer(playerInventory.containerBelt, saveList);
EnsureMovedToCustomSaveListRecursivelyEntireContainer(playerInventory.containerWear, saveList);
//any backpacks? almost forgot!
var getBackpack = playerInventory.GetBackpackWithInventory();
if (getBackpack != null) {
EnsureMovedToCustomSaveListRecursivelyEntireContainer(getBackpack.contents, saveList);
}
}
}
else {
var asDroppedItemContainer = entity as DroppedItemContainer;
if (asDroppedItemContainer != null) {
EnsureMovedToCustomSaveListRecursivelyEntireContainer(asDroppedItemContainer.inventory, saveList);
}
else {
//and last chance... so far, that I can see, as much as edge cases go.
var asLootableCorpse = entity as LootableCorpse;
if (asLootableCorpse != null) {
var inventories = asLootableCorpse.containers;
if (inventories != null) {
int invLength = inventories.Length;
for (var i = 0; i < invLength; i++) {
var inventory = inventories[i];
EnsureMovedToCustomSaveListRecursivelyEntireContainer(inventory, saveList);
}
}
}
}
}
}
}
if (entity.children == null) {
return;
}
var childrenCount = entity.children.Count;
for (int i = 0; i < childrenCount; i++) {
ForEachEntityInTransformHierarchyRecursivelyAndAlsoConsideringItemsInInventories(entity.children[i], entityAction, saveList);
}
}
private static void ForEachItemEntityRecursively(Item item, Action<BaseEntity> entityAction) //so the action will be either move to custom, or move to vanilla... and keep passing the action
{
if (item == null) {
return;
}
//sub entity...
if (item.instanceData != null) {
if (item.instanceData.subEntity.IsValid) {
BaseEntity subEnt = BaseNetworkable.serverEntities.Find(item.instanceData.subEntity) as BaseEntity;
if (subEnt != null) {
entityAction(subEnt);
}
}
}
//world entity...
BaseEntity worldEntity = item.GetWorldEntity();
if (worldEntity != null) {
entityAction(worldEntity);
}
//held entity...
BaseEntity heldEntity = item.GetHeldEntity();
if (heldEntity != null) {
entityAction(heldEntity);
}
//and then, if your item container is not null, keep going.
if (item.contents == null) {
return;
}
if (item.contents.itemList.IsNullOrEmpty()) {
return;
}
int count = item.contents.itemList.Count;
for (int i = 0; i < count; i++) {
Item containerItem = item.contents.itemList[i];
if (containerItem == null) {
continue;
}
ForEachItemEntityRecursively(containerItem, entityAction);
}
}
/*
*
public static void EnsureMovedToCustomSaveListRecursively(Item item, List<BaseEntity> targetList)
{
ForEachItemEntityRecursively(item, (entity) => EnsureMovedToCustomSaveList(entity, targetList));
}
*/
public static void EnsureMovedToCustomSaveListRecursivelyTopDown(BaseEntity vanillaEntity, List<BaseEntity> targetList) {
bool first = true;
ForEachEntityInTransformHierarchyRecursivelyAndAlsoConsideringItemsInInventories(vanillaEntity, (entity) => {
EnsureMovedToCustomSaveListMindingIfFirstOneIsForced(entity, targetList, ref first);
}
, targetList);
}
public static void EnsureMovedToCustomSaveListMindingIfFirstOneIsForced(BaseEntity vanillaEntity, List<BaseEntity> targetList, ref bool first) {
//so. subentities, and stuff like that,
//their "enableSaving" status WILL matter.
//i.e. here, in this lambda thingy,
//if entity.enableSaving is false, we should NOT do this.
//HOWEVER. For the first one, i.e. the topmost one,
//we already know that we WANT it there.
//so it needs to be ignored for that one.
//so keep passing a bool.
//
/*
if (!first)
{
if (vanillaEntity.enableSaving == false)
{
Instance.PrintWarning($">>> DEBUG ENSURE MOVED: NOT MOVING `{vanillaEntity.PrefabName}` TO SAVE LIST, BECAUSE IT'S ENABLE SAVING IS FALSE!");
return;
}
}*/
first = false;
EnsureMovedToCustomSaveList(vanillaEntity, targetList);
vanillaEntity.enableSaving = false;
}
public static void EnsureMovedToCustomSaveList(BaseEntity vanillaEntity, List<BaseEntity> targetList) {
if (vanillaEntity == null) {
return;
}
if (vanillaEntity.net == null) {
return;
}
if (vanillaEntity.IsDestroyed) {
return;
}
ICustomEntity asInterface = vanillaEntity as ICustomEntity;
if (asInterface != null) {
//check if it's something handled by bundles
var tryGetList = ModifiedPrefabFullNameToCustomSaveList.TryGetValue(vanillaEntity.PrefabName, out var maybeSaveList);
if (!tryGetList) {
return;
}
if (maybeSaveList != targetList) {
return;
}
}
if (targetList.Contains(vanillaEntity)) {
//already contains
return;
}
//already a part of a list?
List<BaseEntity> maybeAlreadyExistingList = TryGetEntityCustomSaveList(vanillaEntity, SaveListEntityType.Vanilla);
if (maybeAlreadyExistingList != null) {
if (maybeAlreadyExistingList == targetList) {
return;
//already where it needs to be
}
//it exists in some other custom save list, so let's forget it first...
ForgetEntityFromSaveListAndCache(vanillaEntity, maybeAlreadyExistingList);
}
vanillaEntity.EnableSaving(false);
//this will remove from vanilla list if enabled - use after spawning
targetList.Add(vanillaEntity);
_vanillaEntityToCustomSaveList.Add(vanillaEntity.net.ID.Value, targetList);
}
public static List<BaseEntity> EnsureMovedToCustomSaveListSameAs(BaseEntity vanillaEntity, ICustomEntity sameAsCustomEntity) {
if (sameAsCustomEntity.SaveListInDataFile == null) {
//null for some reason, so false just to be on the safe side
return null;
}
EnsureMovedToCustomSaveList(vanillaEntity, sameAsCustomEntity.SaveListInDataFile);
return sameAsCustomEntity.SaveListInDataFile;
}
public static bool RegisterAndLoadBundle(CustomPrefabBundle bundle, string optionalSuffix = default(string)) {
//step 0: ensure the data...
BinaryData binaryData = BinaryData.SummonBinaryData(bundle.Owner, optionalSuffix);
binaryData.PrefabBundle = bundle;
List<CustomPrefabRecipe> customRecipes = Pool.Get<List<CustomPrefabRecipe>>();
List<ModifiedPrefabRecipe> modifiedRecipes = Pool.Get<List<ModifiedPrefabRecipe>>();
//step 1: separate the grain from the hull
for (int i = 0; i < binaryData.PrefabBundle.Recipes.Length; i++) {
GenericPrefabRecipe recipe = binaryData.PrefabBundle.Recipes[i];
if (recipe is CustomPrefabRecipe customRecipe) {
customRecipes.Add(customRecipe);
}
else if (recipe is ModifiedPrefabRecipe modifiedRecipe) {
modifiedRecipes.Add(modifiedRecipe);
}
}
bool allGood = true;
//step 2: register custom prefabs from recipes...
for (int i = 0; i < customRecipes.Count; i++) {
CustomPrefabRecipe recipe = customRecipes[i];
Instance.PrintWarning(MSG(MSG_PREFAB_REGISTERING_CUSTOM, null, recipe.EntityType.Name, (recipe.BaseCombat == null ? nameof(BaseEntity) : nameof(BaseCombatEntity)), recipe.FullPrefabName));
try {
RegisterPrefabCustom(recipe, binaryData);
}
catch (Exception e) {
Instance.PrintError(MSG(MSG_PREFAB_REGISTRATION_EXCEPTION, null, e.Message, e.StackTrace));
allGood = false;
}
}
//step 3: register modified recipes. We're doing that second since we might wanna modify a custom prefab too
for (int i = 0; i < modifiedRecipes.Count; i++) {
ModifiedPrefabRecipe recipe = modifiedRecipes[i];
Instance.PrintWarning(MSG(MSG_PREFAB_REGISTERING_MODIFIED, null, recipe.OriginalFullPrefabName, recipe.FullPrefabName));
try {
RegisterPrefabModified(recipe, binaryData);
}
catch (Exception e) {
Instance.PrintError(MSG(MSG_PREFAB_REGISTRATION_EXCEPTION, null, e.Message, e.StackTrace));
allGood = false;
}
}
Pool.FreeUnmanaged(ref customRecipes);
Pool.FreeUnmanaged(ref modifiedRecipes);
//step 4: now that everything is registered, load.
//now they should all know their appropriate save lists.
binaryData.Load();
return allGood;
}
public static bool SaveAndUnregisterBundle(CustomPrefabBundle cookbook, string optionalSuffix = default(string)) {
//and this should be the exact opposite.
//first, save...
//this will ensure the data wrapper, just in case it wasn't before
BinaryData binaryData = BinaryData.SummonBinaryData(cookbook.Owner, optionalSuffix);
binaryData.Save(true);
//this will save everything
List<CustomPrefabRecipe> customRecipes = Pool.Get<List<CustomPrefabRecipe>>();
List<ModifiedPrefabRecipe> modifiedRecipes = Pool.Get<List<ModifiedPrefabRecipe>>();
for (int i = 0; i < binaryData.PrefabBundle.Recipes.Length; i++) {
GenericPrefabRecipe recipe = binaryData.PrefabBundle.Recipes[i];
if (recipe is CustomPrefabRecipe customRecipe) {
customRecipes.Add(customRecipe);
}
else if (recipe is ModifiedPrefabRecipe modifiedRecipe) {
modifiedRecipes.Add(modifiedRecipe);
}
}
bool allGood = true;
for (int i = 0; i < modifiedRecipes.Count; i++) {
ModifiedPrefabRecipe recipe = modifiedRecipes[i];
if (!UnregisterPrefabInternal(recipe)) {
allGood = false;
}
}
for (int i = 0; i < customRecipes.Count; i++) {
CustomPrefabRecipe recipe = customRecipes[i];
//caveat: this removes from _modified... cache!
if (!UnregisterPrefabInternal(recipe)) {
allGood = false;
}
}
Pool.FreeUnmanaged(ref customRecipes);
Pool.FreeUnmanaged(ref modifiedRecipes);
//and now we need to kill off vanilla entities that might still linger about
int countKilled = 0;
for (int i = 0; i < binaryData.CustomEntitySaveList.Count; i++) {
BaseEntity entity = binaryData.CustomEntitySaveList[i];
if (entity == null) {
continue;
}
if (entity.IsDestroyed) {
continue;
}
entity.Kill(BaseNetworkable.DestroyMode.None);
countKilled++;
}
if (countKilled > 0) {
if (CustomConvars.VerboseLogging) {
Instance.PrintWarning(MSG(MSG_BUNDLE_UNREGISTRATION_REMOVED_VANILLA, null, countKilled));
}
}
BinaryData.ForgetBinaryData(cookbook.Owner);
return allGood;
}
//and the rest are private methods
private static bool RegisterPrefabModified(ModifiedPrefabRecipe recipe, BinaryData data) {
var tryFindPrototype = TryGetPreprocessedPrototypeFromVanilla(recipe.OriginalFullPrefabName);
if (tryFindPrototype == null) {
tryFindPrototype = TryGetPreprocessedPrototypeFromCustom(recipe.OriginalFullPrefabName);
}
if (tryFindPrototype == null) {
return false;
}
var newGo = UnityEngine.Object.Instantiate(tryFindPrototype.gameObject, null, true);
var newEntity = newGo.GetComponent<BaseEntity>();
//we know entity won't be null because TryGetPreprocessed looks for an entity in first place
if (recipe.ModificationFunction != null) {
if (!recipe.ModificationFunction(newEntity)) {
return false;
}
}
//and again - because new entity could've been replaced!
newEntity = newGo.GetComponent<BaseEntity>();
AddToGameManifest(recipe.FullPrefabName, newGo, recipe.EnableInSpawnCommand);
AddToPreprocessed(recipe.FullPrefabName, newGo);
if (recipe.BaseCombat != null) {
ApplyBaseCombatEntityProperties((BaseCombatEntity)newEntity, recipe.ShortPrefabName, recipe.BaseCombat);
}
var modifiedPrefabStringPoolID = AddToStringPool(recipe.FullPrefabName);
//MUST DISABLE VANILLA SAVING BECAUSE NOW IT HAS A NON-EXISTING PREFAB ID!
//in this case, it will help identify the modified prefab in the dictionary, given its full prefab name.
newEntity.prefabID = modifiedPrefabStringPoolID;
newEntity._prefabName = recipe.FullPrefabName;
ICustomEntity asInterface = (newEntity as ICustomEntity);
if (asInterface != null) {
//I think this is what we were missing!
asInterface.SaveListInDataFile = data.CustomEntitySaveList;
}
if (recipe.SaveHandling == ModifiedSaveHandling.SaveInVanillaSaveList) {
newEntity.EnableSaving(true);
}
else {
newEntity.EnableSaving(false);
if (recipe.SaveHandling == ModifiedSaveHandling.SaveInBundleSaveList) {
ModifiedPrefabFullNameToModifiedPrefabIDHandledByBundles.Add(recipe.FullPrefabName, modifiedPrefabStringPoolID);
ModifiedPrefabFullNameToCustomSaveList.Add(recipe.FullPrefabName, data.CustomEntitySaveList);
//add the compo only if needed
var newCompo = newGo.AddComponent<ModifiedBundleSaveBehaviour>();
newCompo.RecipeFullPrefabName = recipe.FullPrefabName;
newCompo.AddToCustomSaveList = true;
}
}
if (asInterface != null) {
asInterface.OnCustomPrefabPrototypeEntityRegistered();
}
newGo.SetActive(false);
UnityEngine.Object.DontDestroyOnLoad(newGo);
if (!_cachedRecipes.Contains(recipe)) {
_cachedRecipes.Add(recipe);
}
return true;
}
private static bool RegisterPrefabCustom(CustomPrefabRecipe recipe, BinaryData data) {
GameObject newGo = new GameObject(recipe.ShortPrefabName) {
layer = (int)recipe.Layer
}
;
BaseEntity newEntity = newGo.AddComponent(recipe.EntityType) as BaseEntity;
AddToGameManifest(recipe.FullPrefabName, newGo, recipe.EnableInSpawnCommand);
AddToPreprocessed(recipe.FullPrefabName, newGo);
if (recipe.BaseCombat != null) {
ApplyBaseCombatEntityProperties((BaseCombatEntity)newEntity, recipe.ShortPrefabName, recipe.BaseCombat);
}
newEntity.impactEffect = _emptyImpactGameObjectRef;
newEntity.prefabID = AddToStringPool(recipe.FullPrefabName);
//naughty reflection
newEntity._prefabName = recipe.F
// ... [Code preview truncated due to length. Full source in attached file] ...