[SOLVED] problem creating an alternative Teleport effect class - please help

Discuss modding questions and implementation details.
Post Reply
meritamas
Posts: 148
Joined: Sun Apr 07, 2019 6:16 am
Location: Slovak Republic

[SOLVED] problem creating an alternative Teleport effect class - please help

Post by meritamas »

I am trying to create an Advanced Teleportation mod that would allow the player to create multiple anchors and choose which one he wants to teleport to.

I think I managed to develop an idea on how that could be achieved, but ran into an error right in the beginning. I created a class that would eventually become the advanced teleportation effect class but right now it is not very different to a clean carbon copy of the original Teleport class (except for the namespace and class name and I have also changed the "[fsObject("v1")]" to "[fsObject("v2")]" just in case that was the error but it didn't fix it.).

The problem is: when I add the aforementioned script (verbatim quote lower) to the mod as an asset, the entire mod ceases to function. For some reason it isn't loaded or invoked or whatever. This is the main method of the main mod class:

Code: Select all

[Invoke(StateManager.StateTypes.Game, 0)]
        public static void Init(InitParams initParams)
        {
            MessageToPlayer("Advanced Teleport Init Inited");  // a helper to write stuff to the HUD
            StartInit(initParams.ModTitle);        // init the class as a first step before work begins            

            //BaseEntityEffect ourTeleportEffect = new TestTeleport();
           
            EndInit();          // Finish Mod Initialization            
        }
        
If the below script is added to the mod as an asset, then I can see no message on the HUD - so presumably, the code doesn't reach the first line.
If I remove the script as a mod asset, it starts to work again - I see a message on the HUD.
If I re-add the script, I ceases to work, remove again, works again.

In all cases, added script asset or no, the mod manager seems to recognize the mod because it is there listed among the other mods.

Does anyone know why that could be? Thanks.

I am including the script file below. Perhaps it is a nuance I failed to notice. Perhaps I am making an obvious mistake.

Code: Select all

// Project:         Daggerfall Tools For Unity
// Copyright:       Copyright (C) 2009-2020 Daggerfall Workshop
// Web Site:        http://www.dfworkshop.net
// License:         MIT License (http://www.opensource.org/licenses/mit-license.php)
// Source Code:     https://github.com/Interkarma/daggerfall-unity
// Original Author: Gavin Clayton (interkarma@dfworkshop.net)
// Contributors:    
// 
// Notes:
//

using UnityEngine;
using DaggerfallConnect;
using DaggerfallWorkshop;
using DaggerfallWorkshop.Game;
using DaggerfallWorkshop.Game.Entity;
using DaggerfallWorkshop.Game.Serialization;
using DaggerfallWorkshop.Game.UserInterfaceWindows;
using DaggerfallWorkshop.Game.MagicAndEffects;
using DaggerfallWorkshop.Game.MagicAndEffects.MagicEffects;
using FullSerializer;

namespace MTAdvancedTeleportation
{
    /// <summary>
    /// Teleport
    /// </summary>
    public class TestTeleport : IncumbentEffect
    {
        public static readonly string EffectKey = "Teleport-Effect";

        #region Fields

        // Constants
        const int teleportOrSetAnchor = 4000;
        const int achorMustBeSet = 4001;

        // Effect data to serialize
        bool anchorSet = false;
        PlayerPositionData_v1 anchorPosition;
        int forcedRoundsRemaining = 1;

        // Volatile references
        SerializablePlayer serializablePlayer = null;
        PlayerEnterExit playerEnterExit = null;

        #endregion

        #region Overrides

        public override void SetProperties()
        {
            properties.Key = EffectKey;
            properties.ClassicKey = MakeClassicKey(43, 255);
            properties.GroupName = TextManager.Instance.GetText("ClassicEffects", "teleport");
            properties.SpellMakerDescription = DaggerfallUnity.Instance.TextProvider.GetRSCTokens(1602);
            properties.SpellBookDescription = DaggerfallUnity.Instance.TextProvider.GetRSCTokens(1302);
            properties.AllowedTargets = EntityEffectBroker.TargetFlags_Self;
            properties.AllowedElements = EntityEffectBroker.ElementFlags_MagicOnly;
            properties.AllowedCraftingStations = MagicCraftingStations.SpellMaker;
            properties.ShowSpellIcon = false;
            properties.MagicSkill = DFCareer.MagicSkills.Mysticism;
        }

        protected override int RemoveRound()
        {
            return forcedRoundsRemaining;
        }

        public override int RoundsRemaining
        {
            get { return forcedRoundsRemaining; }
        }

        protected override bool IsLikeKind(IncumbentEffect other)
        {
            return (other is TestTeleport);
        }

        public override void Start(EntityEffectManager manager, DaggerfallEntityBehaviour caster = null)
        {
            base.Start(manager, caster);
            CacheReferences();
        }

        public override void Resume(EntityEffectManager.EffectSaveData_v1 effectData, EntityEffectManager manager, DaggerfallEntityBehaviour caster = null)
        {
            base.Resume(effectData, manager, caster);
            CacheReferences();
        }

        protected override void BecomeIncumbent()
        {
            base.BecomeIncumbent();
            PromptPlayer();
        }

        protected override void AddState(IncumbentEffect incumbent)
        {
            // Prompt from incumbent as it has the position data for teleport
            (incumbent as TestTeleport).PromptPlayer();
        }

        public override void End()
        {
            anchorPosition = null;
            base.End();
        }

        #endregion

        #region Private Methods

        void PromptPlayer()
        {
            // Get peered entity gameobject
            DaggerfallEntityBehaviour entityBehaviour = GetPeeredEntityBehaviour(manager);
            if (!entityBehaviour)
                return;

            // Target must be player - no effect on other entities
            if (entityBehaviour != GameManager.Instance.PlayerEntityBehaviour)
                return;

            // Prompt for outcome
            DaggerfallMessageBox mb = new DaggerfallMessageBox(DaggerfallUI.Instance.UserInterfaceManager, DaggerfallMessageBox.CommonMessageBoxButtons.AnchorTeleport, teleportOrSetAnchor, DaggerfallUI.Instance.UserInterfaceManager.TopWindow);
            // QoL, does not match classic. No magicka refund, though
            mb.AllowCancel = true;
            mb.OnButtonClick += EffectActionPrompt_OnButtonClick;
            mb.Show();
        }

        void SetAnchor()
        {
            // Validate references
            if (!serializablePlayer || !playerEnterExit)
                return;

            // Get position information
            anchorPosition = serializablePlayer.GetPlayerPositionData();
            if (playerEnterExit.IsPlayerInsideBuilding)
            {
                anchorPosition.exteriorDoors = playerEnterExit.ExteriorDoors;
                anchorPosition.buildingDiscoveryData = playerEnterExit.BuildingDiscoveryData;
            }

            anchorSet = true;
        }

        void TeleportPlayer()
        {
            // Validate references
            if (!serializablePlayer || !playerEnterExit)
                return;

            // Is player in same interior as anchor?
            if (IsSameInterior())
            {
                // Just need to move player
                serializablePlayer.RestorePosition(anchorPosition);
            }
            else
            {
                // Cache scene before departing
                if (!playerEnterExit.IsPlayerInside)
                    SaveLoadManager.CacheScene(GameManager.Instance.StreamingWorld.SceneName);      // Player is outside
                else if (playerEnterExit.IsPlayerInsideBuilding)
                    SaveLoadManager.CacheScene(playerEnterExit.Interior.name);                      // Player inside a building
                else // Player inside a dungeon
                    playerEnterExit.TransitionDungeonExteriorImmediate();

                // Need to load some other part of the world again - player could be anywhere
                PlayerEnterExit.OnRespawnerComplete += PlayerEnterExit_OnRespawnerComplete;
                playerEnterExit.RestorePositionHelper(anchorPosition, false, true);

                // Restore building summary data
                if (anchorPosition.insideBuilding)
                    playerEnterExit.BuildingDiscoveryData = anchorPosition.buildingDiscoveryData;

                // When moving anywhere other than same interior trigger a fade so transition appears smoother
                DaggerfallUI.Instance.FadeBehaviour.FadeHUDFromBlack();
            }

            // End and resign
            // Player will need to create a new teleport with a new anchor from here
            // This is same behaviour as classic
            forcedRoundsRemaining = 0;
            ResignAsIncumbent();
            anchorSet = false;
        }

        #endregion

        #region Helpers

        // Cache required references
        bool CacheReferences()
        {
            // Get peered SerializablePlayer and PlayerEnterExit
            if (!serializablePlayer)
                serializablePlayer = caster.GetComponent<SerializablePlayer>();

            if (!playerEnterExit)
                playerEnterExit = caster.GetComponent<PlayerEnterExit>();

            if (!serializablePlayer || !playerEnterExit)
            {
                Debug.LogError("Teleport effect could not find both SerializablePlayer and PlayerEnterExit components.");
                return false;
            }

            return true;
        }

        // Checks if player is in same building or dungeon interior as anchor
        bool IsSameInterior()
        {
            // Reject if outside or anchor not set
            if (!playerEnterExit.IsPlayerInside || anchorPosition == null)
                return false;

            // Test depends on if player is inside a building or a dungeon
            if (playerEnterExit.IsPlayerInsideBuilding && anchorPosition.insideBuilding)
            {
                // Compare building key
                if (anchorPosition.buildingDiscoveryData.buildingKey == playerEnterExit.BuildingDiscoveryData.buildingKey)
                {
                    // Also compare map pixel, in case we're unlucky https://forums.dfworkshop.net/viewtopic.php?f=24&t=2018
                    DaggerfallConnect.Utility.DFPosition anchorMapPixel = DaggerfallConnect.Arena2.MapsFile.WorldCoordToMapPixel(anchorPosition.worldPosX, anchorPosition.worldPosZ);
                    DaggerfallConnect.Utility.DFPosition playerMapPixel = GameManager.Instance.PlayerGPS.CurrentMapPixel;
                    if (anchorMapPixel.X == playerMapPixel.X && anchorMapPixel.Y == playerMapPixel.Y)
                        return true;
                }
            }
            else if (playerEnterExit.IsPlayerInsideDungeon && anchorPosition.insideDungeon)
            {
                // Compare map pixel of dungeon (only one dungeon per map pixel allowed)
                DaggerfallConnect.Utility.DFPosition anchorMapPixel = DaggerfallConnect.Arena2.MapsFile.WorldCoordToMapPixel(anchorPosition.worldPosX, anchorPosition.worldPosZ);
                DaggerfallConnect.Utility.DFPosition playerMapPixel = GameManager.Instance.PlayerGPS.CurrentMapPixel;
                if (anchorMapPixel.X == playerMapPixel.X && anchorMapPixel.Y == playerMapPixel.Y)
                    return true;
            }

            return false;
        }

        #endregion

        #region Event Handlers

        private void PlayerEnterExit_OnRespawnerComplete()
        {
            // Must have a caster and it must be the player
            if (caster == null || caster != GameManager.Instance.PlayerEntityBehaviour)
                return;

            // Get peered SerializablePlayer and PlayerEnterExit if they haven't been cached yet
            if (!CacheReferences())
                return;

            // Restore final position and unwire event
            serializablePlayer.RestorePosition(anchorPosition);
            PlayerEnterExit.OnRespawnerComplete -= PlayerEnterExit_OnRespawnerComplete;

            // Restore scene cache on arrival
            if (!playerEnterExit.IsPlayerInside)
                SaveLoadManager.RestoreCachedScene(GameManager.Instance.StreamingWorld.SceneName);      // Player is outside
            else if (playerEnterExit.IsPlayerInsideBuilding)
                SaveLoadManager.RestoreCachedScene(playerEnterExit.Interior.name);                      // Player inside a building
        }

        private void EffectActionPrompt_OnButtonClick(DaggerfallMessageBox sender, DaggerfallMessageBox.MessageBoxButtons messageBoxButton)
        {
            sender.CloseWindow();

            if (messageBoxButton == DaggerfallMessageBox.MessageBoxButtons.Anchor)
            {
                SetAnchor();
            }
            else if (messageBoxButton == DaggerfallMessageBox.MessageBoxButtons.Teleport)
            {
                if (!anchorSet || anchorPosition == null)
                {
                    DaggerfallMessageBox mb = new DaggerfallMessageBox(DaggerfallUI.Instance.UserInterfaceManager, DaggerfallUI.Instance.UserInterfaceManager.TopWindow);
                    mb.SetTextTokens(achorMustBeSet);
                    mb.ClickAnywhereToClose = true;
                    mb.Show();
                    forcedRoundsRemaining = 0;
                    ResignAsIncumbent();
                    return;
                }
                TeleportPlayer();
            }
        }

        #endregion

        #region Serialization

        [fsObject("v2")]
        public struct SaveData_v1
        {
            public bool anchorSet;
            public PlayerPositionData_v1 anchorPosition;
            public int forcedRoundsRemaining;
        }

        public override object GetSaveData()
        {
            SaveData_v1 data = new SaveData_v1();
            data.anchorSet = anchorSet;
            data.anchorPosition = anchorPosition;
            data.forcedRoundsRemaining = forcedRoundsRemaining;

            return data;
        }

        public override void RestoreSaveData(object dataIn)
        {
            if (dataIn == null)
                return;

            SaveData_v1 data = (SaveData_v1)dataIn;
            anchorSet = data.anchorSet;
            anchorPosition = data.anchorPosition;
            forcedRoundsRemaining = data.forcedRoundsRemaining;
        }

        #endregion
    }
}
Last edited by meritamas on Thu May 21, 2020 12:44 pm, edited 1 time in total.
Interest in expanding and improving the Magic system, Capitalism and an Unleveled World.

My main quest in the DFU community is my (Mostly) Magic Mod.

User avatar
BadLuckBurt
Posts: 948
Joined: Sun Nov 05, 2017 8:30 pm

Re: problem creating an alternative Teleport effect class - please help

Post by BadLuckBurt »

Did you build this as a mod and try it in-game? I get the following error in output_log.txt:

Code: Select all

Error (272): The property or indexer `DaggerfallWorkshop.Game.PlayerEnterExit.BuildingDiscoveryData' cannot be used in this context because the set accessor is inaccessible
Error (): 0(150,14): MTAdvancedTeleportation.TestTeleport.TeleportPlayer()
Commenting out or removing this bit in TeleportPlayer() makes the error go away:

Code: Select all

                // Restore building summary data
                if (anchorPosition.insideBuilding)
                    playerEnterExit.BuildingDiscoveryData = anchorPosition.buildingDiscoveryData;
I don't know if that still allows your mod to work though so I'll let you be the judge of that :)
DFU on UESP: https://en.uesp.net/w/index.php?title=T ... fall_Unity
DFU Nexus Mods: https://www.nexusmods.com/daggerfallunity
My github repositories with mostly DFU related stuff: https://github.com/BadLuckBurt

.

meritamas
Posts: 148
Joined: Sun Apr 07, 2019 6:16 am
Location: Slovak Republic

Re: problem creating an alternative Teleport effect class - please help

Post by meritamas »

BadLuckBurt wrote: Sun May 17, 2020 1:01 pm Did you build this as a mod and try it in-game? I get the following error in output_log.txt:
Thanks for the prompt reply.
Yes, I built this as a mod.
Whatever is doing this somehow neutralizes the entire mod even if I don't even have a single reference to that part of the code... I am just beginning with the development of this mod and cannot yet judge if I can work around this.

Where can I look for that output_log.txt file you referred so I can check if I have such a line there, too?


Another thing that came to me was that it could be related to generic method references found in CacheReferences()
LypyL wrote: Sat Jun 18, 2016 7:52 am Known Issues

generic functions / classes - similar to the above, due to an issue with the compiler scripts that try to define generic functions will not compile.

Code: Select all

// Cache required references
        bool CacheReferences()
        {
            // Get peered SerializablePlayer and PlayerEnterExit
            if (!serializablePlayer)
                serializablePlayer = caster.GetComponent<SerializablePlayer>();

            if (!playerEnterExit)
                playerEnterExit = caster.GetComponent<PlayerEnterExit>();

            if (!serializablePlayer || !playerEnterExit)
            {
                Debug.LogError("Teleport effect could not find both SerializablePlayer and PlayerEnterExit components.");
                return false;
            }

            return true;
        }
If that is so, can somebody suggest a workaround?
Interest in expanding and improving the Magic system, Capitalism and an Unleveled World.

My main quest in the DFU community is my (Mostly) Magic Mod.

User avatar
BadLuckBurt
Posts: 948
Joined: Sun Nov 05, 2017 8:30 pm

Re: problem creating an alternative Teleport effect class - please help

Post by BadLuckBurt »

meritamas wrote: Sun May 17, 2020 2:52 pm
BadLuckBurt wrote: Sun May 17, 2020 1:01 pm Did you build this as a mod and try it in-game? I get the following error in output_log.txt:
Thanks for the prompt reply.
Yes, I built this as a mod.
Whatever is doing this somehow neutralizes the entire mod even if I don't even have a single reference to that part of the code... I am just beginning with the development of this mod and cannot yet judge if I can work around this.

Where can I look for that output_log.txt file you referred so I can check if I have such a line there, too?
You're welcome. Check this post by Interkarma on where to find the output_log:

viewtopic.php?f=5&t=2360&p=27552#p27552

I don't know about the CacheReferences stuff but Unity seems fine with that. I rebuilt the mod after I commented out the part of the code I mentioned and it loaded up fine, I just don't know what this mod does exactly or what to test so I left it at that.
DFU on UESP: https://en.uesp.net/w/index.php?title=T ... fall_Unity
DFU Nexus Mods: https://www.nexusmods.com/daggerfallunity
My github repositories with mostly DFU related stuff: https://github.com/BadLuckBurt

.

meritamas
Posts: 148
Joined: Sun Apr 07, 2019 6:16 am
Location: Slovak Republic

Re: problem creating an alternative Teleport effect class - please help

Post by meritamas »

BadLuckBurt wrote: Sun May 17, 2020 3:07 pm You're welcome. Check this post by Interkarma on where to find the output_log:

viewtopic.php?f=5&t=2360&p=27552#p27552

I don't know about the CacheReferences stuff but Unity seems fine with that. I rebuilt the mod after I commented out the part of the code I mentioned and it loaded up fine, I just don't know what this mod does exactly or what to test so I left it at that.
Thanks for the link, I should be able to find the file now. I can also confirm that commenting out the lines you mentioned causes the mod to function again. Thanks.

The thing is, I wanted to create an alternative version of teleport that supports several anchors. I thought, creating such a class would be a quite complex task, so the best approach would be to begin from what we have in the game and about which we can be certain that it works and work up from there.

So I tried to copy the thing and apply some changes.
I tried to test. It didn't work. I tried several variations.
Eventually I was like: 'okay, lets just get a class that is part of the mod, but behaves in exactly the same way as the game's Teleport class'. If that one works, I know the issue is with something I changed. If it doesn't, I know the issue is elsewhere.
It didn't work. So, now I am here trying to find out why the game Teleport class works and my quasi-copy TestTeleport class doesn't (or whether I goofed up in my attempt to make a perfect replica of Teleport).

The code we commented out, if I can discern correctly, has something to do with restoring some data if the anchor was made inside a building. So, presumably it can be worked around, but still, I'd like to know what the issue is because the underlying issue can potentially cause further problems down the road.
Interest in expanding and improving the Magic system, Capitalism and an Unleveled World.

My main quest in the DFU community is my (Mostly) Magic Mod.

User avatar
BadLuckBurt
Posts: 948
Joined: Sun Nov 05, 2017 8:30 pm

Re: problem creating an alternative Teleport effect class - please help

Post by BadLuckBurt »

I remember a similar mod for Morrowind. I'll leave it to someone with more experience / knowledge about this part of DFU to answer the rest of your questions though, I'm pretty clueless about the whole Teleport thing.
DFU on UESP: https://en.uesp.net/w/index.php?title=T ... fall_Unity
DFU Nexus Mods: https://www.nexusmods.com/daggerfallunity
My github repositories with mostly DFU related stuff: https://github.com/BadLuckBurt

.

meritamas
Posts: 148
Joined: Sun Apr 07, 2019 6:16 am
Location: Slovak Republic

Re: problem creating an alternative Teleport effect class - please help

Post by meritamas »

I think I have found the culprit.

Code: Select all

public PlayerGPS.DiscoveredBuilding BuildingDiscoveryData
        {
            get { return buildingDiscoveryData; }
            internal set { buildingDiscoveryData = value; }
        }
This piece of code is from the PlayerEnterExit class. It seems to imply that the property BuildingDiscoveryData can only be set by code that is part of the original game, but not mods. This would explain the behavior I experienced: that I tried to use the 'exact' same class as part of a mod and it didn't work.

I left the line commented out and I have found one minor and one major inconvenience, otherwise teleportation seems to work fine using my own copy-cat class. It teleports me back to my outdoor anchor, but also to my indoor anchor. Even if I left the map pixel, even if I have already entered another building since creating it. I tried crating the anchor, entering another building, then leaving the map pixel, then saving, then loading and then teleporting back.

The minor inconvenience is that some data about the building I am teleporting into is lost. It is probable that these are the data stored in BuildingDiscoveryData. E.g. my anchor was created in a Tavern and now that I am back after the save-reload, none of the usual folk are there, no bartender, no anybody. Gone. I exit, re-enter, everything seems like normal. (UPDATE 19 May 2020: the simplest way I can think of would be to make that internal tag vanish from the game source code - I'll try it if I get the time but should work)

The major inconvenience is that if I save the game with my modded-in incumbent effect active, then exit DFU, restart game and load, the game hangs. If I first load another save, which triggers the mod to load and then load the game with the modded-in effect, it works fine. It seems that incumbent effects are loaded before mods are, at least the way I was doing it so far. So it seems like a good idea to
- get the mod loaded before the first game is loaded (UPDATE 19 May 2020: solved)
- look around if something can be done so a non-modded game wouldn't hang when trying to load a game with the modded-in incumbent effect (UPDATE 19 May 2020: whis one might be unsolvable, but the issue can likely be mitigated by implementing a way to remove the incumbent effect from the player so if he choses to remove the mod, he could first remove the effect via the modded game, then remove the mod and then continue; adding support so the mod would recognize an anchor that was set by a non-modded game should also be possible)
Interest in expanding and improving the Magic system, Capitalism and an Unleveled World.

My main quest in the DFU community is my (Mostly) Magic Mod.

Post Reply