linuxgurugamer

Best way to know when a part module is modified, both editor and flight

Recommended Posts

There is this event which notifies whenever a vessel is modified in the editor:

EventData<ShipConstruct> GameEvents.onEditorShipModified = new EventData<ShipConstruct>("onEditorShipModified")
static

Event called whenever the ship in the Editor is modified in any way

But this is called whenever the ship is modified, and give no indication as to which part in the ship was modified.

I've tried using the onEditorPartEvent, but it doesn't get called when a value in the right-click menu is changed (either button or slider).

I need to know when a value in a part is modified, so that the change can be propagated to other identical parts in the vessel.  Right now I need to check the current value against an old value, and then do updates if they are different, but this is wasteful.  It would be better if the part module could be notified if it had been changed.

I've been through the docs, and I don't see any way to know which part was modified, which is sad since I don't want to create unavoidable overhead.

 

I have a similar problem in flight, in that I need to know when a slider is moved, so that the change can be propagated to other parts.  The following code:

GameEvents.onVesselWasModified.Add(onVesselWasModified);

     or 

GameEvents.onVesselStandardModification.Add(onVesselWasModified);

doesn't trigger when a slider is moved.

 

Suggestions?

Thanks in advance

 

Edited by linuxgurugamer

Share this post


Link to post
Share on other sites

If I follow your question, you want to know when a slider is moved in a PartModule - is that a custom part module that you're writing code for?  I do that in the AviationLights mod in the VAB.  There are a couple of steps to it:

Set up the KSPField: here - I tell it *not* to affect symmetry counterparts, since whether or not the change affects symmetry is an option for these parts.  And I suppress the "on modified" callback.  I don't remember why - I think whichever mod I learned this trick in was doing it.

Tell KSP you want a custom callback fired when a slider is moved in the context menu: here

Have a custom callback that does what you want: here (in my case, I'm using one callback for several related fields, so I've got some extra logic to dispatch the change)

 

Share this post


Link to post
Share on other sites

I do exactly as you do @linuxgurugamer on LMP. I must reproduce part syncronization over network so for example if a player extends a ladder it's extended in the other clients aswell.

Some part modules have events (when storing/transmitting science experiments for example) but most part modules don't so I run trough every part module at an interval to check for changes.

I wish every "KSPField" triggered an event when they are modified but unfortunately that would need to be done by Squad

EDIT: Of course if you do your own partmodule you should do what MOARdV say but if you want to detect changes in the squad part modules (ModuleEngines, ModuleLight, etc) you need to do it with a loop

Edited by Dagger

Share this post


Link to post
Share on other sites
18 hours ago, MOARdV said:

If I follow your question, you want to know when a slider is moved in a PartModule - is that a custom part module that you're writing code for?  I do that in the AviationLights mod in the VAB.  There are a couple of steps to it:

Set up the KSPField: here - I tell it *not* to affect symmetry counterparts, since whether or not the change affects symmetry is an option for these parts.  And I suppress the "on modified" callback.  I don't remember why - I think whichever mod I learned this trick in was doing it.

Tell KSP you want a custom callback fired when a slider is moved in the context menu: here

Have a custom callback that does what you want: here (in my case, I'm using one callback for several related fields, so I've got some extra logic to dispatch the change)

 

 

Thank You!!!!!

Now stuffing this into my bag of tricks.

Share this post


Link to post
Share on other sites
On 6/17/2018 at 12:24 PM, MOARdV said:

If I follow your question, you want to know when a slider is moved in a PartModule - is that a custom part module that you're writing code for?  I do that in the AviationLights mod in the VAB.  There are a couple of steps to it:

Set up the KSPField: here - I tell it *not* to affect symmetry counterparts, since whether or not the change affects symmetry is an option for these parts.  And I suppress the "on modified" callback.  I don't remember why - I think whichever mod I learned this trick in was doing it.

Tell KSP you want a custom callback fired when a slider is moved in the context menu: here

Have a custom callback that does what you want: here (in my case, I'm using one callback for several related fields, so I've got some extra logic to dispatch the change)

 

Related question:

How can I know when a resource transfer has been initiated, and when it has been completed?  

My issue is that in order to deal with floating point round off errors, I need to have a shadow resource which contains the actual resources multiplied by a large constant.  All in-game resource usage is done using the shadow resource, which is hidden from the player.  The visible resource is updated on a regular basis.  The problem comes when  you try to transfer from one tank to another.  The transfer is done using the visible resource, but gets replaced by the hidden resource, which is not changed by the transfer.

Ideally, I'd like to know when the "In", "Out", and "Stop" buttons are pressed, so that I can do the hidden transfer behind the scenes.  I would also need to know the two parts the transfer is going between

Thanks in advance

 

Share this post


Link to post
Share on other sites

The simplest method might be to attach a listener MonoBehaviour to the UIPartActionResourceTransfer prefab (UIPartActionController.Instance.resourceTransferItemPrefab), when the listener script is awakened you can do whatever checks are necessary to accomplish what you want. All of the relevant information should be public: the part that the transfer control is attached to, the resource,  all of the other parts that are currently part of the transfer.

You could attach your own listeners to the In/Out/Stop buttons, they are also public, and that would be the simplest way of determining when they are clicked on.

Share this post


Link to post
Share on other sites
5 minutes ago, DMagic said:

The simplest method might be to attach a listener MonoBehaviour to the UIPartActionResourceTransfer prefab (UIPartActionController.Instance.resourceTransferItemPrefab), when the listener script is awakened you can do whatever checks are necessary to accomplish what you want. All of the relevant information should be public: the part that the transfer control is attached to, the resource,  all of the other parts that are currently part of the transfer.

You could attach your own listeners to the In/Out/Stop buttons, they are also public, and that would be the simplest way of determining when they are clicked on.

Ok.  ummm, about to show my ignorance.  How?

Oh, never mind, I've found enough examples to figure it out

Share this post


Link to post
Share on other sites

Something like this:

    [KSPAddon(KSPAddon.Startup.Flight, true)]
    public class AddListener : MonoBehaviour
    {
        public static EventData<UIPartActionResourceTransfer> onTransferSpawn = new EventData<UIPartActionResourceTransfer>("onTransferSpawn");

        private void Start()
        {
            onTransferSpawn.Add(OnTransferSpawned);

            //This probably only needs to be attached once, but using AddOrGetComponent insures that you don't
            //end up with multiple copies of your listener
            UIPartActionController.Instance.resourceTransferItemPrefab.gameObject.AddOrGetComponent<TransferListener>();
        }

        private void OnTransferSpawned(UIPartActionResourceTransfer transfer)
        {
            //Check if part or resources are relevant, check for other parts, etc...

            Part p = transfer.Part;
            PartResource r = transfer.Resource;
            List<UIPartActionResourceTransfer> targets = transfer.Targets;

            //Add your own listeners to the in, out, and stop buttons
            transfer.flowOutBtn.onClick.AddListener(OnOutClick);
            //etc...
        }

        private void OnOutClick()
        {

        }

    }

    public class TransferListener : MonoBehaviour
    {
        private void Awake()
        {
            AddListener.onTransferSpawn.Fire(gameObject.GetComponentInParent<UIPartActionResourceTransfer>());
        }
    }

The listener MonoBehaviour gets attached to the prefab, so whenever KSP spawns a new resource transfer control element, it will make a copy of that prefab, with your listener alongside it. Then when your listener script starts it will go through its Awake, Start, etc... methods and you can use one of those to find the UIPartActionResourceTransfer component, which should be on the same game object, and fire an event with that component as an argument. Then your event listener can do something with that transfer component.

Modifying prefabs in this way can be a fantastically useful tool when you are trying to do something with stock KSP mechanisms. The only problem is the inconsistent ways that KSP uses to handle prefabs, sometimes they are easy to find and public like this, other times you have to get them by name from AssetBase, and other times they are just really hard to find or private.

Share this post


Link to post
Share on other sites

Thank you so much.

and IFI Life Support thanks you as well.

Share this post


Link to post
Share on other sites
17 hours ago, DMagic said:

Something like this:


    [KSPAddon(KSPAddon.Startup.Flight, true)]
    public class AddListener : MonoBehaviour
    {
        public static EventData<UIPartActionResourceTransfer> onTransferSpawn = new EventData<UIPartActionResourceTransfer>("onTransferSpawn");

        private void Start()
        {
            onTransferSpawn.Add(OnTransferSpawned);

            //This probably only needs to be attached once, but using AddOrGetComponent insures that you don't
            //end up with multiple copies of your listener
            UIPartActionController.Instance.resourceTransferItemPrefab.gameObject.AddOrGetComponent<TransferListener>();
        }

        private void OnTransferSpawned(UIPartActionResourceTransfer transfer)
        {
            //Check if part or resources are relevant, check for other parts, etc...

            Part p = transfer.Part;
            PartResource r = transfer.Resource;
            List<UIPartActionResourceTransfer> targets = transfer.Targets;

            //Add your own listeners to the in, out, and stop buttons
            transfer.flowOutBtn.onClick.AddListener(OnOutClick);
            //etc...
        }

        private void OnOutClick()
        {

        }

    }

    public class TransferListener : MonoBehaviour
    {
        private void Awake()
        {
            AddListener.onTransferSpawn.Fire(gameObject.GetComponentInParent<UIPartActionResourceTransfer>());
        }
    }

The listener MonoBehaviour gets attached to the prefab, so whenever KSP spawns a new resource transfer control element, it will make a copy of that prefab, with your listener alongside it. Then when your listener script starts it will go through its Awake, Start, etc... methods and you can use one of those to find the UIPartActionResourceTransfer component, which should be on the same game object, and fire an event with that component as an argument. Then your event listener can do something with that transfer component.

Modifying prefabs in this way can be a fantastically useful tool when you are trying to do something with stock KSP mechanisms. The only problem is the inconsistent ways that KSP uses to handle prefabs, sometimes they are easy to find and public like this, other times you have to get them by name from AssetBase, and other times they are just really hard to find or private.

I'm going to document this as I get it figured out, it's not obvious and may help someone else.

The UIPartActionResourceTransfer get spawned the first time a PAW menu is brought up on a second part when a transfer is possible.

Whenever  a PAW menu is brought up on a second part when a transfer is possible, the event is called, which calls (in the above script) OnTransferSpawned().

Entering OnTransferSpawned:

  • transfer.Part is null
  • transfer.Resource is null
  • transfer.targets has no entries

Save the UIPartActionResourceTransfer in a local variable, it will be used when starting the transfer

When clicking either In or Out buttons:

  • transfer.Part contains the name of the part which was initially clicked on
  • transfer.Resource now contains the resource being transferred
  • transfer.state indicates the flow state (in, out or none) for the part
  • The transfer.targets list contains the first part clicked, as well as all other parts involved in the current transfer
    • Using t2 for each entry in the transfer.targets list:
    • t2.Part contains the name of the part
    • t2.Resource contains the resource being transferred
    • t2.state indicates the flow state for the part

The transfer rate appears to be based on the volume of the destination tank.  After a number of tests, it seems to take 20 seconds to fill a tank completely.  So, 1/20 of a tank per second

Share this post


Link to post
Share on other sites

Those values are probably null because the listener event is being fired in the Awake method. You could use Start instead, or start a coroutine and wait a few frames to allow for everything to be initialized.

Share this post


Link to post
Share on other sites
On 6/30/2018 at 12:08 PM, DMagic said:

Those values are probably null because the listener event is being fired in the Awake method. You could use Start instead, or start a coroutine and wait a few frames to allow for everything to be initialized.

Nope.  I have it working.  Those vars dont get initialized until there are at lease two parts selected.  When you think about it, it makes sense.  There isnt a valid part if there is only one selected.

Inside a FixedUpdate method, you can monitor it and see when it gets initialized.

ill post my code tomorrow.

Share this post


Link to post
Share on other sites

@linuxgurugamer you ever get this all sorted, google searching led me here having a similar issue. I only want a certain calc to run when the part is changed by tweakscale

Share this post


Link to post
Share on other sites
2 hours ago, gomker said:

@linuxgurugamer you ever get this all sorted, google searching led me here having a similar issue. I only want a certain calc to run when the part is changed by tweakscale

Yes, ping me if I dont post it tomorrow.

Share this post


Link to post
Share on other sites
On 7/15/2018 at 10:59 PM, linuxgurugamer said:

Yes, ping me if I dont post it tomorrow.

Ping :) 

Share this post


Link to post
Share on other sites
On 7/15/2018 at 9:17 PM, gomker said:

@linuxgurugamer you ever get this all sorted, google searching led me here having a similar issue. I only want a certain calc to run when the part is changed by tweakscale

Here you go.  What this is doing is coordinating a hidden resource with a visible resource, so that when one is changed, the other will be updated.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;

namespace IFILifeSupport
{

    [KSPAddon(KSPAddon.Startup.Flight, false)]
    public class AddTransferListener : MonoBehaviour
    {
        public static EventData<UIPartActionResourceTransfer> onTransferSpawn = new EventData<UIPartActionResourceTransfer>("onTransferSpawn");
        UIPartActionResourceTransfer t;

        private void Start()
        {
            Log.Info("Transfer.Start");

            onTransferSpawn.Add(OnTransferSpawned);

            //This probably only needs to be attached once, but using AddOrGetComponent insures that you don't
            //end up with multiple copies of your listener
            UIPartActionController.Instance.resourceTransferItemPrefab.gameObject.AddOrGetComponent<TransferListener>();
        }

        void OnDestroy()
        {
            Log.Info("Transfer.OnDestroy");
        }
          
        void DisplayTransferInfo(UIPartActionResourceTransfer t, bool descend = false)
        {
#if false
            Log.Info("\r\n\r\n DisplayTransferInfo, descend: " + descend);
            Log.Info("Transfer.OnTransferSpawned, transfer.lastUT: " + t.lastUT);

            Part p = t.Part;
            PartResource r = t.Resource;
            List<UIPartActionResourceTransfer> targets = t.Targets;
            
            if (t.Part != null)
                Log.Info("transfer.Part: " + t.Part.partInfo.title);
            
            if (t.Resource != null)
                Log.Info("Transfer.Resource: " + t.Resource.resourceName);
            Log.Info("Transfer.state: " + t.state);
            if (t.Part != null)
            {
                double m = r.maxAmount;
                Log.Info("Transfer rate: " + m + " per second");
            }
            foreach (UIPartActionResourceTransfer t1 in targets)
            {
                Log.Info("t: " + t1);
                Log.Info("Transfer.Destination transfer.Part: " + t1.Part.partInfo.title);
                Log.Info("Transfer.Destination transfer.Resource: " + t1.Resource.resourceName);
                if (descend)
                    DisplayTransferInfo(t1);
            }
#endif
        }

        Part source;
        double maxRequested;
        string hiddenResource;
        bool transferInProgress = false;
        double lastUT;

        void GetTransferValues(UIPartActionResourceTransfer transfer)
        {
            maxRequested = 0;
            if (t.state == UIPartActionResourceTransfer.FlowState.In)
            {
                var r = t.Part.Resources[hiddenResource];
                maxRequested += r.maxAmount;
            }
            if (t.state == UIPartActionResourceTransfer.FlowState.Out)
                source = t.Part;

            List<UIPartActionResourceTransfer> targets = t.Targets;
            foreach (UIPartActionResourceTransfer t1 in targets)
            {
                if (t1.state == UIPartActionResourceTransfer.FlowState.In)
                {
                    var r = t1.Part.Resources[hiddenResource];
                    maxRequested += r.maxAmount;
                }
                if (t1.state == UIPartActionResourceTransfer.FlowState.Out)
                    source = t1.Part;
            }
            lastUT = transfer.lastUT;
            maxRequested = maxRequested / 20;
            Log.Info("GetTransferValues, maxRequested/sec: " + maxRequested);
            Log.Info("GetTransferValues, source: " + source.partInfo.title);
        }

        private void OnTransferSpawned(UIPartActionResourceTransfer transfer)
        {
            Log.Info("OnTransferSpawned");
            t = transfer;

            DisplayTransferInfo(t, true);

            //Add your own listeners to the in, out, and stop buttons
            transfer.flowInBtn.onClick.AddListener(OnInClick);
            transfer.flowOutBtn.onClick.AddListener(OnOutClick);
            transfer.flowStopBtn.onClick.AddListener(OnStopClick);          
        }

        bool LifeSupportResource(string resource)
        {
            var b = IFI_Resources.DISPLAY_RESOURCES.Contains(resource);
            Log.Info("Transfer.LifesupportResource, res: " + resource);
            if (b)
            {
                hiddenResource = IFI_Resources.RESOURCES[IFI_Resources.DISPLAY_RESOURCES.IndexOf(resource)];
                Log.Info("Transfer.LifeSupportResource, hiddenResource: " + hiddenResource);
                GetTransferValues(t);
            }
            return b;
        }

        private void OnInClick()
        {
            Log.Info("Transfer.OnInClick, transfer.lastUT: " + t.lastUT);
            DisplayTransferInfo(t, true);
            transferInProgress = LifeSupportResource(t.Resource.resourceName);

        }

        private void OnStopClick()
        {
            Log.Info("Transfer.OnStopClick, transfer.lastUT: " + t.lastUT);
            DisplayTransferInfo(t, true);
            transferInProgress = false;
            t.FlowStop();

        }

        private void OnOutClick()
        {
            Log.Info("Transfer.OnOutClick, transfer.lastUT: " + t.lastUT);
            DisplayTransferInfo(t, true);
            transferInProgress = LifeSupportResource(t.Resource.resourceName);
        }

        void FixedUpdate()
        {
            if (transferInProgress && t != null && t.Resource != null && lastUT != t.lastUT && t.state != UIPartActionResourceTransfer.FlowState.None)
            {
                double timeSlice = t.lastUT - lastUT;
                Log.Info("timeSlice: " + timeSlice);
                double requestAmt = maxRequested * timeSlice;
                double totalTransferred = 0;               

                // Get the available resources for transfer
                double availResForTransfer = source.RequestResource(hiddenResource, requestAmt, ResourceFlowMode.NO_FLOW);

                // now transfer the resources into the new parts.

                if (t.state == UIPartActionResourceTransfer.FlowState.In)
                { 
                    double percentageOfTotal = t.Part.Resources[hiddenResource].maxAmount / 20f * timeSlice / requestAmt;
                    var amtToTransfer = -1 * availResForTransfer * percentageOfTotal;
                    
                    var transferredAmt = t.Part.RequestResource(hiddenResource, amtToTransfer, ResourceFlowMode.NO_FLOW);
                    totalTransferred += transferredAmt;
                    //Log.Info("Transfer 1, after: Part: " + t.Part.partInfo.title + ",  amount: " + t.Part.Resources[hiddenResource].amount + ", transferred: " + totalTransferred);
                }


                List<UIPartActionResourceTransfer> targets = t.Targets;
                foreach (UIPartActionResourceTransfer t1 in targets)
                {
                    if (t1.state == UIPartActionResourceTransfer.FlowState.In)
                    {
                        double percentageOfTotal = t1.Part.Resources[hiddenResource].maxAmount / 20f * timeSlice / requestAmt;

                        var amtToTransfer = -1 * availResForTransfer * percentageOfTotal;
                        var beforeAmt = t1.Part.Resources[hiddenResource].amount;

                        Log.Info("Transfer 2, before, amount: " + t1.Part.Resources[hiddenResource].amount + ", transferred: " + totalTransferred);

                        var transferredAmt = t1.Part.RequestResource(hiddenResource, amtToTransfer, ResourceFlowMode.NO_FLOW);
                        totalTransferred += transferredAmt;
                        //Log.Info("Transfer 2, after:  amount: " + t1.Part.Resources[hiddenResource].amount + ", transferred: " + totalTransferred);
                        var afterAmt = t1.Part.Resources[hiddenResource].amount;
                        Log.Info("Transfer 2, after:  amount: " + afterAmt + ", Logged transfer amt: " + transferredAmt + ", actual diff (after - before): " + (afterAmt - beforeAmt));
                        IFI_Resources.UpdatePartInfo(t1.Part);
                    }
                }

                // Now return the unused resources
                Log.Info("Transfer Returning, availResources: " + availResForTransfer + ", total transferred: " + totalTransferred + ", returning: " + (availResForTransfer + totalTransferred).ToString());
                Log.Info("Transfer, before returning avail resources,  amount: " + source.Resources[hiddenResource].amount);
                source.RequestResource(hiddenResource, -1 * (availResForTransfer + totalTransferred), ResourceFlowMode.NO_FLOW);
                Log.Info("Transfer, after returning avail resources,  amount: " + source.Resources[hiddenResource].amount);
                lastUT = t.lastUT;
               
                if (totalTransferred == 0)
                    OnStopClick();
                IFI_Resources.UpdatePartInfo(t.Part);
                foreach (UIPartActionResourceTransfer t1 in targets)
                    IFI_Resources.UpdatePartInfo(t1.Part);
                
                    Log.Info("Transfer --------------------------------------------------------------------------------------");
            }
            else
            if (transferInProgress)
            {
                if (t != null)
                {
                    if (t.Resource == null)
                        Log.Info("Transfer t.Resource is null");
                    if (lastUT == t.lastUT)
                        Log.Info("Transfer lastUT unchanged, Planetarium.GetUniversalTime: " + Planetarium.GetUniversalTime());
                    if (t.state == UIPartActionResourceTransfer.FlowState.None)
                        Log.Info("Transfer t.state == UIPartActionResourceTransfer.FlowState.None");
                }
                else
                    Log.Info("t is null");
                Log.Info("Transfer --------------------------------------------------------------------------------------");
            }
            
        }
    }

    public class TransferListener : MonoBehaviour
    {
        private void Awake()
        {
            AddTransferListener.onTransferSpawn.Fire(gameObject.GetComponentInParent<UIPartActionResourceTransfer>());
        }
    }
}

 

 

Share this post


Link to post
Share on other sites
This thread is quite old. Please consider starting a new thread rather than reviving this one.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.