private IEnumerator WaitForAIAndFollow(Character ai, Character owner)
        {
            if (!ai || !owner)
            {
                yield break;
            }

            var aiWander = ai.GetComponentInChildren <AISWander>();

            if (!aiWander)
            {
                var time = 5f;
                var wait = new WaitForEndOfFrame();
                while (!aiWander && time > 0f)
                {
                    time -= Time.deltaTime;
                    yield return(wait);

                    aiWander = ai.GetComponentInChildren <AISWander>();
                }
            }

            if (aiWander)
            {
                aiWander.FollowTransform = owner.transform;
            }
            else
            {
                SL.LogWarning("WaitForAIAndFollow timeout");
            }
        }
        // Finalize the clip (name / SLPack), and try to replace global game audio if one has the same name.
        internal static AudioClip FinalizeAudioClip(AudioClip clip, string name, SLPack pack = null)
        {
            clip.name = name;

            if (pack != null)
            {
                if (pack.AudioClips.ContainsKey(name))
                {
                    SL.LogWarning("Replacing clip '" + name + "' in pack '" + pack.Name + "'");

                    if (pack.AudioClips[name])
                    {
                        GameObject.Destroy(pack.AudioClips[name]);
                    }

                    pack.AudioClips.Remove(name);
                }

                pack.AudioClips.Add(name, clip);
            }

            if (Enum.TryParse(name, out GlobalAudioManager.Sounds sound))
            {
                ReplaceAudio(sound, clip);
            }

            return(clip);
        }
        private static IEnumerator LoadAudioFromFileCoroutine(string filePath, SLPack pack, Action <AudioClip> onClipLoaded)
        {
            var name    = Path.GetFileNameWithoutExtension(filePath);
            var request = UnityWebRequestMultimedia.GetAudioClip($@"file://{Path.GetFullPath(filePath)}", AudioType.WAV);

            yield return(request.SendWebRequest());

            while (!request.isDone)
            {
                yield return(null);
            }

            if (!string.IsNullOrEmpty(request.error))
            {
                SL.LogWarning(request.error);
                yield break;
            }

            SL.Log($"Loaded audio clip: {Path.GetFileName(filePath)}");

            var clip = DownloadHandlerAudioClip.GetContent(request);

            FinalizeAudioClip(clip, name, pack);

            onClipLoaded?.Invoke(clip);
        }
Beispiel #4
0
        // internal death callback

        internal void OnDeath(Character character)
        {
            if (this.DestroyOnDeath)
            {
                SLPlugin.Instance.StartCoroutine(DestroyOnDeathCoroutine(character));
                return;
            }

            if (LootableOnDeath && this.DropTableUIDs != null && DropTableUIDs.Length > 0)
            {
                if (character.GetComponent <LootableOnDeath>() is LootableOnDeath lootable &&
                    (bool)At.GetField(lootable, "m_wasAlive"))
                {
                    foreach (var tableUID in this.DropTableUIDs)
                    {
                        if (SL_DropTable.s_registeredTables.TryGetValue(tableUID, out SL_DropTable table))
                        {
                            table.GenerateDrops(character.Inventory.Pouch.transform);
                        }
                        else
                        {
                            SL.LogWarning($"Trying to generate drops for '{UID}', " +
                                          $"but could not find any registered SL_DropTable with the UID '{tableUID}'");
                        }
                    }

                    character.Inventory.MakeLootable(this.DropWeapons, this.DropPouchContents, true, false);
                }
            }
        }
        protected override void ActivateLocally(Character _affectedCharacter, object[] _infos)
        {
            if (PhotonNetwork.isNonMasterClientInRoom)
            {
                return;
            }

            if (m_charTemplate != null)
            {
                var pos = this.OwnerCharacter
                            ? this.OwnerCharacter.transform.position
                            : (Vector3)_infos[0];

                var ai = m_charTemplate.Spawn(pos,
                                              Vector3.zero,
                                              this.GenerateRandomUIDForSpawn
                        ? (string)UID.Generate()
                        : this.SLCharacter_UID,
                                              this.ExtraRpcData);

                if (!ai)
                {
                    SL.LogWarning("SpawnSLCharacter.ActivateLocally - spawn failed!");
                }
                else if (TryFollowCaster)
                {
                    StartCoroutine(WaitForAIAndFollow(ai, this.OwnerCharacter));
                }
            }
        }
Beispiel #6
0
        internal void Internal_Apply()
        {
            // add uid to CustomCharacters callback dictionary
            if (!string.IsNullOrEmpty(this.UID))
            {
                if (CustomCharacters.Templates.ContainsKey(this.UID))
                {
                    SL.LogError("Trying to register an SL_Character Template, but one is already registered with this UID: " + UID);
                    return;
                }

                CustomCharacters.Templates.Add(this.UID, this);
            }

            if (this.LootableOnDeath && this.DropTableUIDs?.Length > 0 && !this.DropPouchContents)
            {
                SL.LogWarning($"SL_Character '{UID}' has LootableOnDeath=true and DropTableUIDs set, but DropPouchContents is false!" +
                              $"You should set DropPouchContents to true in this case or change your template behaviour. Forcing DropPouchContents to true.");
                this.DropPouchContents = true;
            }

            OnPrepare();

            SL.Log("Prepared SL_Character '" + Name + "' (" + UID + ").");
        }
Beispiel #7
0
        public void GenerateDrops(Transform container)
        {
            if (!container)
            {
                SL.LogWarning($"Trying to generate drops from '{UID}' but target container is null!");
                return;
            }

            if (this.GuaranteedDrops != null && GuaranteedDrops.Count > 0)
            {
                //SL.Log("Generating Guaranteed drops...");

                foreach (var drop in this.GuaranteedDrops)
                {
                    drop.GenerateDrop(container);
                }
            }

            if (this.RandomTables != null && RandomTables.Count > 0)
            {
                //SL.Log("Generating Random Tables...");

                foreach (var table in this.RandomTables)
                {
                    table.GenerateDrops(container);
                }
            }
        }
        public override SkillSlot ApplyToRow(Transform row, int treeID)
        {
            var col = new GameObject("Col" + this.ColumnIndex);

            col.transform.parent = row;

            var comp = col.AddComponent <SkillSlot>();

            comp.IsBreakthrough = Breakthrough;

            At.SetField(comp, "m_requiredMoney", SilverCost);
            At.SetField(comp as BaseSkillSlot, "m_columnIndex", ColumnIndex);

            var skill = ResourcesPrefabManager.Instance.GetItemPrefab(SkillID) as Skill;

            if (!skill)
            {
                SL.LogWarning("SL_SkillSlot: Could not find skill by id '" + SkillID + "'");
                return(comp);
            }

            At.SetField(comp, "m_skill", skill);

            At.SetField(skill, "m_schoolIndex", treeID);
            //SL.LogWarning("Set " + treeID + " for " + skill.Name + "'s treeID");

            if (this.RequiredSkillSlot != Vector2.zero)
            {
                SetRequiredSlot(comp);
            }

            return(comp);
        }
        public override void ApplyToCharacter(Character trainer, bool loadingFromSave)
        {
            base.ApplyToCharacter(trainer, loadingFromSave);

            var trainertemplate = GameObject.Instantiate(Resources.Load <GameObject>("editor/templates/TrainerTemplate"));

            trainertemplate.transform.parent   = trainer.transform;
            trainertemplate.transform.position = trainer.transform.position;

            // set Dialogue Actor name
            var trainerActor = trainertemplate.GetComponentInChildren <DialogueActor>();

            trainerActor.SetName(this.Name);

            // get "Trainer" component, and set the SkillTreeUID to our custom tree UID
            var trainerComp = trainertemplate.GetComponentInChildren <Trainer>();

            if (this.SkillTree != null)
            {
                At.SetField(trainerComp, "m_skillTreeUID", new UID(SkillTree.UID));
            }
            else
            {
                SL.LogWarning("Setting up an SL_CharacterTrainer (" + this.UID + ") but no SL_SkillTree has been created for it!");
            }

            // setup dialogue tree
            var graphController = trainertemplate.GetComponentInChildren <DialogueTreeController>();
            var graph           = graphController.graph;

            // the template comes with an empty ActorParameter, we can use that for our NPC actor.
            var actors = At.GetField(graph as DialogueTree, "_actorParameters") as List <DialogueTree.ActorParameter>;

            actors[0].actor = trainerActor;
            actors[0].name  = this.Name;

            // setup the actual dialogue now
            var rootStatement = graph.allNodes[0] as StatementNodeExt;

            rootStatement.statement = new Statement(this.InitialDialogue);
            rootStatement.SetActorName(this.Name);

            // the template already has an action node for opening the Train menu.
            // Let's grab that and change the trainer to our custom Trainer component (setup above).
            var openTrainer = graph.allNodes[1] as ActionNode;

            (openTrainer.action as TrainDialogueAction).Trainer = new BBParameter <Trainer>(trainerComp);

            // ===== finalize nodes =====
            graph.allNodes.Clear();
            // add the nodes we want to use
            graph.allNodes.Add(rootStatement);
            graph.allNodes.Add(openTrainer);
            graph.primeNode = rootStatement;
            graph.ConnectNodes(rootStatement, openTrainer);

            // set the trainer active
            trainer.gameObject.SetActive(true);
        }
        protected override void AwakeInit()
        {
            CustomCharacters.Templates.TryGetValue(this.SLCharacter_UID, out m_charTemplate);

            if (m_charTemplate == null)
            {
                SL.LogWarning("SpawnSLCharacter.Awake - m_charTemplate is null, could not find from UID '" + this.SLCharacter_UID + "'");
            }
        }
        public void ApplyToCharacter(Character character)
        {
            if (!CustomCharacters.Templates.TryGetValue(this.TemplateUID, out SL_Character template))
            {
                SL.LogWarning($"Trying to apply an SL_CharacterSaveData to a Character, but could not get any template with the UID '{this.TemplateUID}'");
                return;
            }

            SLPlugin.Instance.StartCoroutine(ApplyCoroutine(character, template));
        }
        // Normal template apply method
        internal void Internal_ApplyTemplate()
        {
            if (!StatusEffectFamilyLibrary.Instance)
            {
                return;
            }

            var library = StatusEffectFamilyLibrary.Instance;

            StatusEffectFamily family;

            if (library.StatusEffectFamilies.Where(it => (string)it.UID == this.UID).Any())
            {
                family = library.StatusEffectFamilies.First(it => (string)it.UID == this.UID);
            }
            else
            {
                family = new StatusEffectFamily();
                library.StatusEffectFamilies.Add(family);
            }

            if (family == null)
            {
                SL.LogWarning("Applying SL_StatusEffectFamily template, null error");
                return;
            }

            if (this.UID != null)
            {
                At.SetField(family, "m_uid", new UID(this.UID));
            }

            if (this.Name != null)
            {
                family.Name = this.Name;
            }

            if (this.StackBehaviour != null)
            {
                family.StackBehavior = (StatusEffectFamily.StackBehaviors) this.StackBehaviour;
            }

            if (this.MaxStackCount != null)
            {
                family.MaxStackCount = (int)this.MaxStackCount;
            }

            if (this.LengthType != null)
            {
                family.LengthType = (StatusEffectFamily.LengthTypes) this.LengthType;
            }
        }
        private IEnumerator DelayedStopCoroutine()
        {
            yield return(new WaitForSeconds(AutoStopTime));

            if (At.GetField(this as PlayVFX, "m_startVFX") is VFXSystem vfx)
            {
                vfx.Stop();
            }
            else
            {
                SL.LogWarning("SL_PlayTimedVFX.DelayedStopCoroutine - vfx was null after delay");
            }
        }
Beispiel #14
0
        // invoked when character is being saved

        internal string INTERNAL_OnPrepareSave(Character character)
        {
            string ret = null;

            try
            {
                ret = OnCharacterBeingSaved?.Invoke(character);
            }
            catch (Exception e)
            {
                SL.LogWarning("Exception invoking OnCharacterBeingSaved for template '" + this.UID + "'");
                SL.LogInnerException(e);
            }

            return(ret);
        }
Beispiel #15
0
        internal void Internal_ApplyTemplate()
        {
            if (string.IsNullOrEmpty(this.UID))
            {
                SL.LogWarning("Cannot prepare an SL_DropTable with a null or empty UID!");
                return;
            }

            if (s_registeredTables.ContainsKey(this.UID))
            {
                SL.LogWarning("Trying to register an SL_DropTable but one already exists with this UID: " + this.UID);
                return;
            }

            s_registeredTables.Add(this.UID, this);
            SL.Log("Registered SL_DropTable '" + this.UID + "'");
        }
Beispiel #16
0
        /// <summary>Use this to check if a key is held this frame, and get the local ID of the player who pressed it.</summary>
        /// <param name="keyName">The name of the key which you registered with.</param>
        /// <param name="playerID">If the key is pressed, this is the local split-player ID that pressed it.</param>
        /// <returns>True if pressed, false if not.</returns>
        public static bool GetKey(string keyName, out int playerID)
        {
            if (s_customKeyDict.TryGetValue(keyName, out KeybindInfo key))
            {
                return(key.GetKey(out playerID));
            }

            if (!s_loggedMissingKeyNames.Contains(keyName))
            {
                SL.LogWarning($"Attempting to get custom keybinding state, but no custom keybinding " +
                              $"with the name '{keyName}' was registered, this will not be logged again.");

                s_loggedMissingKeyNames.Add(keyName);
            }

            playerID = -1;
            return(false);
        }
Beispiel #17
0
        internal bool GetShouldSpawn()
        {
            if (this.ShouldSpawn != null)
            {
                try
                {
                    return(ShouldSpawn.Invoke());
                }
                catch (Exception ex)
                {
                    SL.LogWarning("Exception invoking ShouldSpawn callback for " + this.Name + " (" + this.UID + "), not spawning.");
                    SL.LogInnerException(ex);
                    return(false);
                }
            }

            return(true);
        }
        public override void ApplyActualTemplate()
        {
            base.ApplyActualTemplate();

            if (string.IsNullOrEmpty(this.IdentifierName))
            {
                SL.LogWarning("Cannot register an SL_ItemSpawn without an InternalName set!");
                return;
            }

            if (s_registeredSpawnSources.ContainsKey(this.IdentifierName))
            {
                SL.LogWarning($"An SL_ItemSpawn with the UID '{IdentifierName}' has already been registered!");
                return;
            }

            s_registeredSpawnSources.Add(this.IdentifierName, this);
        }
Beispiel #19
0
        /// <summary>Use this to add a new Keybinding to the game.</summary>
        /// <param name="name">The name for the keybinding displayed in the menu.</param>
        /// <param name="category">The category to add to</param>
        /// <param name="controlType">What type of control this is</param>
        /// <param name="type">What type(s) of input it will accept</param>
        public static void AddAction(string name, KeybindingsCategory category, ControlType controlType = ControlType.Keyboard, InputType type = InputType.Button)
        {
            bool initialized = (bool)At.GetPropertyStatic(typeof(ReInput), "initialized");

            if (initialized)
            {
                SL.LogWarning("Tried to add Custom Keybinding too late. Add your keybinding earlier, such as in your BaseUnityPlugin.Awake() method.");
                return;
            }

            if (s_customKeyDict.ContainsKey(name))
            {
                SL.LogWarning($"Attempting to add a keybind '{name}', but one with this name has already been registered.");
                return;
            }

            var customKey = new KeybindInfo(name, category, controlType, type);

            s_customKeyDict.Add(name, customKey);
        }
        /// <summary>Replace a global sound with the provided AudioClip.</summary>
        public static void ReplaceAudio(GlobalAudioManager.Sounds sound, AudioClip clip)
        {
            if (!GAMInstance)
            {
                SL.LogWarning("Cannot find GlobalAudioManager Instance!");
                return;
            }

            if (ReplacedClips.Contains(sound))
            {
                SL.Log($"The Sound clip '{sound}' has already been replaced, replacing again...");
            }

            try
            {
                DoReplaceClip(sound, clip);
            }
            catch (Exception e)
            {
                SL.LogError($"Exception replacing clip '{sound}'.\r\nMessage: {e.Message}\r\nStack: {e.StackTrace}");
            }
        }
Beispiel #21
0
        public static object LoadFromXml(Stream stream, Type baseType)
        {
            try
            {
                var xml = GetXmlSerializer(baseType);

                object obj = null;

                using (var reader = new StreamReader(stream))
                {
                    obj = xml.Deserialize(reader);
                }

                return(obj);
            }
            catch (Exception ex)
            {
                SL.LogWarning("Exception loading XML stream!");
                SL.LogInnerException(ex);
                return(null);
            }
        }
        /// <summary>
        /// Load an Audio Clip from a given byte array, and optionally put it in the provided SL Pack.<br/><br/>
        /// WARNING: AudioClips loaded from byte arrays are currently unreliable and may be glitched, use at own risk!
        /// </summary>
        /// <param name="data">The byte[] array from <see cref="File.ReadAllBytes(string)"/> on the wav file path.</param>
        /// <param name="name">The name to give to the audio clip.</param>
        /// <param name="pack">Optional SL Pack to put the audio clip inside.</param>
        /// <returns>The loaded audio clip, if successful.</returns>
        public static AudioClip LoadAudioClip(byte[] data, string name, SLPack pack = null)
        {
            SL.LogWarning("WARNING: AudioClips loaded from embedded .zip archives are currently unreliable and may be glitched, use at own risk!");

            try
            {
                var clip = ConvertByteArrayToAudioClip(data, name);

                if (!clip)
                {
                    return(null);
                }

                return(FinalizeAudioClip(clip, name, pack));
            }
            catch (Exception ex)
            {
                SL.LogWarning("Exception loading AudioClip!");
                SL.LogInnerException(ex);
                return(null);
            }
        }
        public void GenerateItems(Transform container)
        {
            if (this.DropTableUIDsToAdd == null)
            {
                SL.LogWarning($"Trying to generate drops from an SL_DropTableAddition '{IdentifierName}', but the DropTableUIDsToAdd is null!");
                return;
            }

            foreach (string tableUID in this.DropTableUIDsToAdd)
            {
                SL_DropTable.s_registeredTables.TryGetValue(tableUID, out SL_DropTable table);

                if (table == null)
                {
                    SL.LogWarning($"SL_DropTableAddition: Could not find any SL_DropTable with UID '{tableUID}'!");
                    continue;
                }

                //SL.Log("Generating from '" + table.UID + "'");
                table.GenerateDrops(container);
            }
        }
Beispiel #24
0
        public static Type GetBaseTypeOfXmlDocument(Stream stream)
        {
            string typeName = "";

            using (var reader = XmlReader.Create(stream))
            {
                while (reader.Read()) // just get the first element (root) then break.
                {
                    if (reader.NodeType == XmlNodeType.Element)
                    {
                        // the real type might be saved as an attribute
                        if (!string.IsNullOrEmpty(reader.GetAttribute("type")))
                        {
                            typeName = reader.GetAttribute("type");
                        }
                        else
                        {
                            typeName = reader.Name;
                        }
                        break;
                    }
                }
            }

            s_typesByName.TryGetValue(typeName, out Type type);

            if (type == null)
            {
                type = SLTypes.FirstOrDefault(it => it.Name == typeName);
                if (type == null)
                {
                    SL.LogWarning("Could not get Type from document with base node '" + typeName + "'!");
                    return(null);
                }
            }

            return(type);
        }
Beispiel #25
0
        /// <summary>
        /// Save an SL_Type object to xml.
        /// </summary>
        public static void SaveToXml(string dir, string saveName, object obj)
        {
            if (!string.IsNullOrEmpty(dir))
            {
                if (!Directory.Exists(dir))
                {
                    Directory.CreateDirectory(dir);
                }

                dir += "/";
            }

            saveName = ReplaceInvalidChars(saveName);

            string path = dir + saveName + ".xml";

            if (File.Exists(path))
            {
                //SL.LogWarning("SaveToXml: A file already exists at " + path + "! Deleting...");
                File.Delete(path);
            }

            var xml = GetXmlSerializer(obj.GetType());

            using (var file = File.OpenWrite(path))
            {
                try
                {
                    xml.Serialize(file, obj);
                }
                catch (Exception ex)
                {
                    SL.LogWarning("Exception saving object to XML!");
                    SL.LogInnerException(ex);
                }
            }
        }
        internal void GenerateItem()
        {
            if (PhotonNetwork.isNonMasterClientInRoom)
            {
                return;
            }

            if (s_activeSavableSpawns.Any(it => it.SpawnIdentifier == this.IdentifierName))
            {
                SL.LogWarning("Trying to spawn two SL_ItemSpawns with the same Identifier: " + this.IdentifierName);
                return;
            }

            var prefab = ResourcesPrefabManager.Instance.GetItemPrefab(this.ItemID);

            if (!prefab)
            {
                SL.LogWarning($"SL_ItemSpawn: Could not find any item by ID '{ItemID}'!");
                return;
            }

            SL.Log($"SL_ItemSpawn '{this.IdentifierName}' spawning...");

            var item = ItemManager.Instance.GenerateItemNetwork(this.ItemID);

            if (!ForceNonPickable)
            {
                s_activeSavableSpawns.Add(new ItemSpawnInfo
                {
                    SpawnIdentifier = this.IdentifierName,
                    ItemID          = this.ItemID,
                    ItemUID         = item.UID
                });
            }

            ApplyToItem(item);
        }
Beispiel #27
0
        internal static void Internal_SetField(Type type, string fieldName, object instance, object value)
        {
            if (type == null)
            {
                return;
            }

            var fi = GetFieldInfo(type, fieldName);

            if (fi == null)
            {
                SL.LogWarning($"Could not find FieldInfo for Type '{type?.FullName ?? "<null>"}', field '{fieldName}'!");
                return;
            }

            if (fi.IsStatic)
            {
                fi.SetValue(null, value);
            }
            else
            {
                fi.SetValue(instance, value);
            }
        }
        private static void DoReplaceClip(GlobalAudioManager.Sounds _sound, AudioClip _newClip)
        {
            if (!_newClip)
            {
                SL.LogWarning($"The replacement clip for '{_sound}' is null");
                return;
            }

            //var path = GAMInstance.GetPrefabPath(_sound);
            var path      = (string)At.Invoke(GAMInstance, "GetPrefabPath", _sound);
            var resource  = Resources.Load <GameObject>("_Sounds/" + path);
            var component = resource.GetComponent <AudioSource>();

            component.clip = _newClip;

            resource.hideFlags |= HideFlags.DontUnloadUnusedAsset;

            if (!ReplacedClips.Contains(_sound))
            {
                ReplacedClips.Add(_sound);
            }

            SL.Log("Replaced " + _sound + " AudioSource with custom clip!");
        }
        internal IEnumerator ApplyCoroutine(Character character, SL_Character template)
        {
            yield return(new WaitForSeconds(0.5f));

            if (this.Silver > 0)
            {
                character.Inventory.AddMoney(this.Silver);
            }

            if (!string.IsNullOrEmpty(FollowTargetUID))
            {
                var followTarget = CharacterManager.Instance.GetCharacter(FollowTargetUID);
                var aisWander    = character.GetComponentInChildren <AISWander>();
                if (followTarget && aisWander)
                {
                    aisWander.FollowTransform = followTarget.transform;
                }
                else
                {
                    SL.LogWarning("Failed setting follow target!");
                }
            }

            if (WasDead)
            {
                At.SetField(character, "m_loadedDead", true);

                if (character.GetComponentInChildren <LootableOnDeath>() is LootableOnDeath loot)
                {
                    At.SetField(loot, "m_wasAlive", false);
                }
            }

            if (ItemSaves != null && ItemSaves.Count > 0)
            {
                foreach (var itemSave in ItemSaves)
                {
                    switch (itemSave.Type)
                    {
                    case CharItemSaveData.EquipSaveType.Pouch:
                        var item = ItemManager.Instance.GenerateItemNetwork(itemSave.ItemID);
                        if (item)
                        {
                            item.ChangeParent(character.Inventory.Pouch.transform);
                            item.RemainingAmount = itemSave.Quantity;
                        }
                        break;

                    case CharItemSaveData.EquipSaveType.Equipped:
                        SL_Character.TryEquipItem(character, itemSave.ItemID);
                        break;

                    case CharItemSaveData.EquipSaveType.Backpack:
                        item = ItemManager.Instance.GenerateItemNetwork(itemSave.ItemID);
                        if (item && character.Inventory.EquippedBag)
                        {
                            item.ChangeParent(character.Inventory.EquippedBag.Container.transform);
                            item.RemainingAmount = itemSave.Quantity;
                        }
                        break;
                    }
                }
            }

            if (character.GetComponent <CharacterStats>() is CharacterStats stats)
            {
                stats.SetHealth(this.Health);
            }

            if (this.StatusData != null)
            {
                var statusMgr = character.GetComponentInChildren <StatusEffectManager>(true);
                if (statusMgr)
                {
                    foreach (var statusData in this.StatusData)
                    {
                        var data = statusData.Split('|');

                        var status = ResourcesPrefabManager.Instance.GetStatusEffectPrefab(data[0]);
                        if (!status)
                        {
                            continue;
                        }

                        var dealer = CharacterManager.Instance.GetCharacter(data[1]);
                        var effect = statusMgr.AddStatusEffect(status, dealer);

                        var remaining = float.Parse(data[2]);
                        At.SetField(effect, "m_remainingTime", remaining);
                        if (effect.StatusData != null)
                        {
                            At.SetField(effect.StatusData, "m_remainingLifespan", remaining);
                        }
                    }
                }
            }

            template.INTERNAL_OnSaveApplied(character, this.ExtraRPCData, this.ExtraSaveData);
        }
        // ========== parsing from CustomSpawnInfo ===========

        internal static SL_CharacterSaveData FromSpawnInfo(CustomSpawnInfo info)
        {
            // should probably debug this if it happens
            if (info.Template == null || !info.ActiveCharacter)
            {
                SL.LogWarning("Trying to save a CustomSpawnInfo, but template or activeCharacter is null!");
                return(null);
            }

            var character = info.ActiveCharacter;
            var template  = info.Template;

            // capture the save data in an instance
            var data = new SL_CharacterSaveData()
            {
                SaveType      = template.SaveType,
                TemplateUID   = template.UID,
                ExtraSaveData = template.INTERNAL_OnPrepareSave(character),
                ExtraRPCData  = info.ExtraRPCData,

                CharacterUID = character.UID,
                WasDead      = character.IsDead,
                Forward      = character.transform.forward,
                Position     = character.transform.position,
                Silver       = character.Inventory.ContainedSilver,
            };

            if (character.Inventory)
            {
                data.SetSavedItems(character);
            }

            if (character.GetComponentInChildren <AISWander>() is AISWander aiWander)
            {
                if (aiWander.FollowTransform && aiWander.FollowTransform.GetComponent <Character>() is Character followTarget)
                {
                    data.FollowTargetUID = followTarget.UID.ToString();
                }
            }

            try
            {
                data.Health = character.Health;

                if (character.StatusEffectMngr)
                {
                    var statuses = character.StatusEffectMngr.Statuses.ToArray().Where(it => !string.IsNullOrEmpty(it.IdentifierName));
                    data.StatusData = new string[statuses.Count()];

                    int i = 0;
                    foreach (var status in statuses)
                    {
                        var sourceChar = (UID)At.GetField(status, "m_sourceCharacterUID")?.ToString();
                        data.StatusData[i] = $"{status.IdentifierName}|{sourceChar}|{status.RemainingLifespan}";
                        i++;
                    }
                }
            }
            catch { }

            return(data);
        }