Jump to content

KSP 1.2.2 GameEvents Extension


Recommended Posts

1 hour ago, xEvilReeperx said:

Now, subscribers could be prevented from GC cleanup of course but since the only subscriber to my example event is the event's owner, controlling its lifetime is much simpler

Those are exactly the objects I'm concerned with.  But I think that's because I'm thinking of a different architecture.  I was still thinking of the dual event system.  I'm not sure that the single event system will do what we are looking for it to do: Using @Diazo's pseudo code:

AGX:

public static List<bool> masterList;
public static EventData<List<bool>> referenceMasterList;
referenceMasterList.Add(handleReference);

handleReference(List<bool> refList)
{
  // by having AGX store the reference created in kOS
  // the reference changes each time the event is fired
  // which means that if you have 2 mods that fire the
  // event, the reference will be overwritten and only
  // the newest reference will actually be used.
  refList = masterList; //does this work?
}


kOS:

public List<bool> kOSlistReference;
referenceListEvent = GameEvents.FindEvent<EventData<List<bool>>>("referenceMasterList");
referenceListEvent.Fire(kOSlistReference);  // now kOSlistReference is the reference, but no way to know the reference changed


// ALTERNATIVELY:
AGX:

public static List<bool> masterList;
public static EventData<List<bool>> referenceMasterList;
public static EventData<string> requestMasterList;
requestMasterList.Add(updateReference);

updateReference(string requestingModName)
{
  // requestingModName is just a parameter for the sake of having a parameter
  referenceMasterList.Fire(masterList); // trigger event sending out the reference to the internal master list.
}


kOS:

public List<bool> kOSlistReference;
referenceMasterList.Add(OnUpdateReference);
requestMasterList.Fire(kOSlistReference);

OnUpdateReference(List<bool> referenceList) {
  kOSListReference = referenceList;
}

OnDestroy() {
  // if we don't do this, even though all of the normal Unity events
  // get unsubscribed, this event will still be subscribed and will
  // prevent the GC from relaiming any memory not explicitly freed
  // as part of the GameObject's destruction
  requestMasterList.Remove(OnUpdateReference);
}

 

Edited by hvacengi
Link to comment
Share on other sites

If all requesters are sharing the same reference, why provide a unique identifier at all? If you want AGX to call a method and pass a list/dictionary/whatever reference, why not do that directly instead of dealing with two separate GameEvents? Here's how I'd envision that version:

Spoiler

[KSPAddon(KSPAddon.Startup.Flight, false)]
internal class ModB : MonoBehaviour
{
    private Dictionary<int, bool> _agxStates = Enumerable.Range(0, 32).ToDictionary(i => i, i => false);
 
    private void Awake()
    {
        var ge = GameEvents.FindEvent<EventData<Action<Dictionary<int, bool>>>>("AGX_GetActionGroupStateList");

        if (ge == null)
            Debug.LogWarning("AGX not found; AG states not available");
        else ge.Fire(d => _agxStates = d); // grab ref to AG state dictionary. AGX will keep this updated
    }

    private void Update()
    {
        Debug.Log("Current active toggles: " +
                    string.Join(",", _agxStates.Where(kvp => kvp.Value).Select(i => i.Key.ToString()).ToArray()));
    }
}



[KSPAddon(KSPAddon.Startup.Flight, false)]
class AGX : MonoBehaviour
{
    class Forwarder
    {
        private readonly EventData<Action<Dictionary<int, bool>>>.OnEvent _del;

        public Forwarder([NotNull] EventData<Action<Dictionary<int, bool>>>.OnEvent del)
        {
            if (del == null) throw new ArgumentNullException("del");
            _del = del;
        }

        public void OnEvent(Action<Dictionary<int, bool>> data)
        {
            _del(data);
        }
    }

    [KSPAddon(KSPAddon.Startup.Instantly, true)]
    private class InitialSetup : MonoBehaviour // make sure the GameEvent is created early
    {
        private void Awake()
        {
            GrabActionGroupStateDict.Add(new Forwarder(OnRequestForAGStateDictionary).OnEvent);
            Destroy(gameObject);
        }
    }

    public static readonly EventData<Action<Dictionary<int, bool>>> GrabActionGroupStateDict =
        new EventData<Action<Dictionary<int, bool>>>("AGX_GetActionGroupStateList");


    private static readonly Dictionary<int, bool> CurrentActionGroupStates = new Dictionary<int, bool>();

    // just fake some random data
    private IEnumerator Start()
    {
        for (int i = 0; i < 32; ++i)
            CurrentActionGroupStates[i] = false;

        while (true)
        {
            for (int i = 0; i < 32; ++i)
                CurrentActionGroupStates[i] = UnityEngine.Random.Range(0, 2) == 1;

            yield return new WaitForSeconds(1f);
        }
    }


    private static void OnRequestForAGStateDictionary(Action<Dictionary<int, bool>> data)
    {
        data(CurrentActionGroupStates);
    }
}
Link to comment
Share on other sites

Hm, I like the delegate (Action) idea.  I hadn't thought about that, but it would eliminate the risk of the extra references.  I would presume that there would still be a potential need for the ability to simply subscribe to events instead of polling the list for other mods, but that would be a really clean solution for kOS's needs.

Link to comment
Share on other sites

Alright, you guys are now talking way over my head. Can you give me some google keywords so I can read up on what you are talking about?

Trying with the extent of the knowledge I have gets me nowhere.

//using a List<float> because I'm autofilling the list in the other mod every 5 seconds with a timestamp.
List<float> tempList = new List<float>();
MA2testEvent.Fire(tempList);
Debug.Log("MA Call event end " + tempList.Count);
//tempList.Count prints zero to the log, but I know the list has value in it on the other end.
  
  
//handler method on the other end:
  public void testAGXmethod(List<float> lst)
        {
            Debug.Log("AGX sees event fire!");
            lst = numList; //numlist is a static master list that will get it's data modified. This should set the reference passed by the event to equal the master list but doesn't seem to. For test purposes I'm adding Time.realtimeSinceStartup as a float every 5 seconds.
        }

I then tried "MA2testEvent.Fire(ref tempList);", but that would not compile on an "Argument 1 may not be passed with the 'ref' keyword".

So I am now stumped and need to read up on what you guys are talking about before I can go any further.

 

My full test code is below, what was supposed to happen was that:

1) AGX makes public static List<float> numList; which gets the current Time.realtimeSinceStartup added to it every 5 seconds.

2) ModB calls the event with a List<float> object that is added to the public static List<EventTestListClass> testListContainer; object. EventTestListClass is a simple data storage class with 2 variables.

3) The CallEvent() and PrintContainer() methods are manually called via action groups with the mouse. (Hooked into existing code for another mod here.)

Everything seemed to work except that no reference link was made and empty List<float> objects kept getting added instead of a reference to the master numList object in AGX.

public class AGXEventHandler
    {
        public static EventData<List<float>> onTestEvent;
        public static List<float> numList;

        public static AGXEventHandler myEventHandler;
        public static AGXFlight myFlightAddon;
        public static AGXEditor myEditorAddon;

        public void init()
        {
            Debug.Log("AGX event init fired!");
            onTestEvent = new EventData<List<float>>("onTestEvent");
            Debug.Log("AGX event init 1fired!");
            numList = new List<float>();
            Debug.Log("AGX event init 1A fired!");
            numList.Add(Time.realtimeSinceStartup);
            Debug.Log("AGX event init2fired!");
            onTestEvent.Add(testAGXmethod);
            Debug.Log("AGX event init 3fired!");
        }

        public void testAGXmethod(List<float> lst)
        {
            Debug.Log("AGX sees event fire!");
            lst = numList;
        }
    }
    [KSPAddon(KSPAddon.Startup.Flight, false)]
    public class AGXEventTestFlight : PartModule
    {
        bool timeCheck;
        public void Start()
        {
            StartCoroutine("numCounter");
        }

        public IEnumerator numCounter()
        {
            timeCheck = false;
            
            AGXEventHandler.numList.Add(Time.realtimeSinceStartup);
            Debug.Log("AGX event add time" + AGXEventHandler.numList.Count);
            yield return new WaitForSeconds(5f);
            timeCheck = true;
            
        }
        public void Update()
        {
            if(timeCheck)
            {
                StartCoroutine("numCounter");
            }
        }

    }
          
          
          
------------------------------------------------------- code from other mod:
          
          [KSPAddon(KSPAddon.Startup.Flight, false)]
    public class EventTest : PartModule
    {
        public static List<EventTestListClass> testListContainer;

        public void Start()
        {
            testListContainer = new List<EventTestListClass>();
        }

        public static void CallEvent()
        {
            Debug.Log("MA Call event start");
            EventData<List<float>> MA2testEvent = GameEvents.FindEvent<EventData<List<float>>>("onTestEvent");
            List<float> tempList = new List<float>();
            MA2testEvent.Fire(tempList); //tried (ref tempList) and would not compile
            testListContainer.Add(new EventTestListClass(Time.realtimeSinceStartup, tempList));
            Debug.Log("MA Call event end " + tempList.Count);
        }

        public static void PrintContainer()
        {
            Debug.Log("MA container print start " + Time.realtimeSinceStartup + testListContainer.Count);
            for(int i = 0;i<testListContainer.Count;i++)
            {
                Debug.Log("MA entry " + i + "|" + testListContainer[i].numList.Count);
                foreach(float fl in testListContainer[i].numList)
                {
                    Debug.Log("MA entry sub " + testListContainer[i].initTime + "|" + fl);
                }
            }
            Debug.Log("MA Container print end");

        }
        

    }

    public class EventTestListClass //use for event testing
    {
        public List<float> numList;
        public float initTime;

        public EventTestListClass()
        {
            numList = new List<float>();
            initTime = Time.realtimeSinceStartup;
        }
        public EventTestListClass(float tmr, List<float> tempList)
        {
            numList = tempList;
            initTime = tmr;
        }
    }

So that's it for me, I need to increase my C# knowledge before I can take this any farther and understand what you two are talking about.

D.

Edited by Diazo
Link to comment
Share on other sites

12 hours ago, Diazo said:

My full test code is below, what was supposed to happen was that:

1) AGX makes public static List<float> numList; which gets the current Time.realtimeSinceStartup added to it every 5 seconds.

2) ModB calls the event with a List<float> object that is added to the public static List<EventTestListClass> testListContainer; object. EventTestListClass is a simple data storage class with 2 variables.

3) The CallEvent() and PrintContainer() methods are manually called via action groups with the mouse. (Hooked into existing code for another mod here.)

Everything seemed to work except that no reference link was made and empty List<float> objects kept getting added instead of a reference to the master numList object in AGX.

The problem is that your event is storing the list in the local variable (the method's parameter), rather than the variable accessed by the other mod.  In order to store the reference in the other mod's class, you must assign it from within that class, or using the classes public field.  I've added comments about this to applicable sections of your code here: https://gist.github.com/hvacengi/554ecf0463ab4045af24da95f3dec091

You can make it work the way you implemented it if you move things around so that it uses a 2 event system.  But using the anonymous method/Action technique that @xEvilReeperx showed reduces the number of events that need to be subscribed.  I've taken the liberty of combining his example with your example, with lots of comments to try and help see how to make it work.  You can see it here: https://gist.github.com/hvacengi/9a7ef88ac0b4010cabf4a2653adc1352

The most important note from my testing is that events appear to fire imediately, and are not queued up for the next physics tick.  Immediately after firing the event, my list reference was updated.

Edited by hvacengi
Link to comment
Share on other sites

@hvacengi Good to see a proof of concept that this works.

I still have to do a bunch of reading around the Action<> keyword and the magic of (l => testList = l);  in the event .Fire() method to actually understand what is going on, but it looks like we are on the right path. (Already have 10 tabs open to various pages on TechNet and the C# reference guide....)

This is really my first time working with either of those things and I want to actually understand what is going on so I get AGX's external interface right rather then just copy/paste code from you and @xEvilReeperx.

That example linked in your last post will be invaluable though, so thank you for that.

D.

Link to comment
Share on other sites

1 minute ago, Diazo said:

I still have to do a bunch of reading around the Action<> keyword and the magic of (l => testList = l);  in the event .Fire() method to actually understand what is going on, but it looks like we are on the right path. (Already have 10 tabs open to various pages on TechNet and the C# reference guide....)

Yeah, it can take a bit to wrap your head around how C# handles delegates, particularly around the magic syntax of generics and anonymous methods.  If you remove the anonymous method (the "l => testList = l" bit), you can instead do something like this:

public void RefreshList(List<float> listReference)
{
  testList = listReference;
}

// and change the Fire call to:
testEvent.Fire(RefreshList); // passing a method that takes the same parameters as the generic Action's definition

I actually prefer that method when I write code, just because I find it easier to troubleshoot/debug named methods.

Link to comment
Share on other sites

Adding 'generics' and 'anonymous methods' to my reading list.

As for your code snippet alternative, I think I almost understood that!

Going to run some more test cases when I get home, I think I'm starting to lay down the foundations of how I'm going to structure the events on AGX's side of things.

Note that I'm thinking the finished product will have to pass a dictionary to account for multiple vessels. So Dictionary<Vessel, List<bool>> would be the actual object type passed for action group states.

Or would Dictionary<Vessel,Dictionary<int,bool>> be a better alternative?

We'll see where this goes, ultimately this is part of a larger overhaul to AGX so nothing is going to be 100% in very short term.

D.

Edited by Diazo
Link to comment
Share on other sites

1 hour ago, Diazo said:

Note that I'm thinking the finished product will have to pass a dictionary to account for multiple vessels. So Dictionary<Vessel, List<bool>> would be the actual object type passed for action group states.

Or would Dictionary<Vessel,Dictionary<int,bool>> be a better alternative?

I tend to do dictionary keys based on the vessel's id (which is a GUID), but it could be an unnecessary or useless optimization.

The other thing to remember here is that you can define however many parameters you want the event method to use.  So you could do something like:

public static EventData<GUID, Action<List<bool>>> fetchAGXActionGroupEvent = new EventData<GUID, Action<List<bool>>>("fetchAGXActionGroupEvent");

public void FetchAGXActionGroupList(GUID vesselID, Action<List<bool>> registerAction)
{
  var agList = // whatever means you want to use internally to get the list
  registerAction(agList).
}

// on whatever is requesting the list reference:
public List<bool> AGXList;
var testEvent = GameEvents.FindEvent<EventData<GUID, Action<List<bool>>>>("fetchAGXActionGroupEvent");
testEvent.Fire(this.vessel.id, lst => AGXList = lst); // only requests info applicable to this object
Link to comment
Share on other sites

Multiple parameters will be handy and I am certainly going to use them.

The problem I keep coming back to is how to handle data on a non-focused vessel. I don't think I have a good answer yet.

How do we get AGX to send over multiple action group states?

How do we handle a non-focus vessel coming into range mid-flight? Leaving range?

I think the basic "how do we pass data?" is answered, now we need to look at what format we want to use.

 

Also, do you know for sure how a GUID compare works? IE:

public vessel VesselA;
public GUID vesselAGUID = VesselA.vesselID;
VesselA.Destroy()

public vessel VesselB; //But this is the "same" vessel coming back into physics range.
if(vesselAGUID == VesselB.vesselID) //will this return true or false?

D.

Edited by Diazo
Link to comment
Share on other sites

How to use GameEvents to pass an object reference to use to transfer data

Alright. Having figured all this out I want to summarize everything in a mini-guide for use by others in the future.

The point of all this is rather then having to fire an event anytime a piece of data changes, we pass the reference to said object by the event so that reference can be used as a link between two mods for real-time updating of data.

I am using my AGX mod as a test so I will copy-paste the code over directly rather then try to rename everything to generic Foo names and typo something.

Note that this example uses STATIC heavily. Not absolutely required except on the event itself, but it does make things a lot simpler.

In the mod that holds the data to sync:

public static Dictionary<Guid, Dictionary<int, bool>> masterActionGroupStates; //this object is a list of Vessels, each with an attached dictionary of action groups and their current on/off state. This is our Master data object that the other mod will sync to.

//I used such a complicated data string for testing, any data types that are in stock KSP are valid for this. Any object that is a Reference Type will work, Value Types will pass correct data but will not stay synced.

public static EventData<Action<Dictionary<Guid, Dictionary<int, bool>>>> onTestEvent; //our event, created by AGX. Note the magic ACTION keyword, google "c# action" if you want to know more, but it is necessary. Note the data type must match.

masterActionGroupStates = new Dictionary<Guid, Dictionary<int, bool>>(); //initialize object, probalby in Start()
onTestEvent = new EventData<Action<Dictionary<Guid, Dictionary<int, bool>>>>("onTestEvent"); //initialize object, probably in Start()
onTestEvent.Add(testAGXmethod); //assign method to event to run when fired.

public void testAGXmethod(Action<Dictionary<Guid, Dictionary<int, bool>>> myObj) //note the data type must match
        {
            myObj(masterActionGroupStates); //note we do not use = here, we are running myObj as a method and passing it to the event. masterActionGroupStates will show in the code below as the "toLink" object on the linkOverEvent method.
        }

In the other mod that needs to read the data:

public static Dictionary<Guid, Dictionary<int, bool>> testDict; //our data object that will be synced. MUST match data type.
EventData<Action<Dictionary<Guid, Dictionary<int, bool>>>> MA2testEvent; //our event reference in this mod. Data type match again, note the ACTION keyword.

//the following 3 lines of code must run AFTER the initializations happen in the data holding mod.
testDict = new Dictionary<Guid, Dictionary<int, bool>>();//initialize object
MA2testEvent = GameEvents.FindEvent<EventData<Action<Dictionary<Guid, Dictionary<int, bool>>>>>("onTestEvent"); //find our event. 
MA2testEvent.Fire(linkOverEvent); //fire our event, note we are passing a method, not a data obejct.

public static void linkOverEvent(Dictionary<Guid, Dictionary<int, bool>> toLink) //our method that we pass by event to the other mod.
        {
            testDict = toLink; //where we link the reference between testDict and the event data.
        }

 

At this point, the testDict obejct and the masterActionGroupStates object refer to the same data and any changes made in one mod will be immediately carried over to the other mod. This is true two-way communication, both mods have full read-write access at this point.

What is actually happening is that the ACTION keyword allows us to pass a method as an object, so the linkOverEvent method gets passed from the other mod to AGX and then it "runs" inside AGX, so when AGX "runs" linkOverEvent when the event fires, it passes its half of the data link (masterActionGroupStates) to it which can then be matched to the other half "testDict" because for the duration of the Event.Fire() code running, linkOverEvent exists in both mods at the same time.

 

Now, in my case, I need to restrict access so it is read only, therefore:

private static Dictionary<Guid, Dictionary<int, bool>> masterActionGroupStates; //master container for referencing action group states by other mods
        public static Dictionary<Guid, Dictionary<int, bool>> MasterActionGroupStates //this is the reference passed to other mods by Event, read-only
        {
            get
            {
                return masterActionGroupStates;
            }
        }

and anywhere the example above uses masterActionGroupStates, use MasterActionGroupStates instead and other mods will have read-only access to the data.

 

Thanks to the use of statics, this reference is persistent. Until either mod uses a "=" action on the object on their side of the link, the two objects will show the same data. (Be careful a method you call on the object doesn't do this on you unintentionally.)

Big thanks to @hvacengi and @xEvilReeperx, this is actually mostly their work, I've just assembled everything and put this guide together.

D.

 

Edited by Diazo
Link to comment
Share on other sites

  • 3 months later...

Hi all,

I have one question.

I would like to know if it is possible to implement the following scenario:

Mod A, B, C...D . All of them are publishing an event with the same id.

Can I then subscribe from the Mod Z to that ID receiving the events fired from each mod?

 

 

Link to comment
Share on other sites

15 minutes ago, jrodriguez said:

Hi all,

I have one question.

I would like to know if it is possible to implement the following scenario:

Mod A, B, C...D . All of them are publishing an event with the same id.

Can I then subscribe from the Mod Z to that ID receiving the events fired from each mod?

 

 

Yep. Sure can.

Link to comment
Share on other sites

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.

×
×
  • Create New...