Jump to content

KSP 1.2.2 GameEvents Extension


Recommended Posts

In KSP 1.2.2 there is an extension for GameEvents that allows mods to register their own GameEvents and for any Mod to find and register a callback for said GameEvents.
How to use (example):
Define your GameEvent as a static in Mod A:

public static EventData<Part, ProtoCrewMember> onKerbalFrozen;

and Initialize it somewhere early before ModB tries to find and register it.

onKerbalFrozen = new EventData<Part, ProtoCrewMember>("onKerbalFrozen");



Find the GameEvent and register a callback in ModB:
 

private EventData<Part, ProtoCrewMember> onKerbalFrozenEvent;
...

//in say Awake or Start:

onKerbalFrozenEvent = GameEvents.FindEvent<EventData<Part, ProtoCrewMember>>("onKerbalFrozen");
if (onKerbalFrozenEvent != null) onKerbalFrozenEvent.Add(onKerbalFrozen);

//don't forget to remove them in OnDestroy:
if (onKerbalFrozenEvent != null) onKerbalFrozenEvent.Remove(onKerbalFrozen);

 

Fire your event at the appropriate time from Mod A:

DFGameEvents.onKerbalFrozen.Fire(this.part, CrewMember);

 

ModB callback will be called.

Edited by JPLRepo
Link to comment
Share on other sites

I just made an update to above instructions.
It's important you define and initialize your GameEvent EARLY so that ModB will find it.
So it's a good idea to define it and then init it New say in Start of a KSPAddon MonoBehaviour at MainMenu for example.

Link to comment
Share on other sites

15 hours ago, JPLRepo said:

It's important you define and initialize your GameEvent EARLY so that ModB will find it.

Any drawbacks you are aware of, if the GameEvent is initialized in the static definition?

That was it is guaranteed to be initialized before first use, be it a listener or the mod that fire the event.

public static EventData<Part, ProtoCrewMember> onKerbalFrozen = new EventData<Part, ProtoCrewMember>("onKerbalFrozen");

 

Edited by ShotgunNinja
Link to comment
Share on other sites

1 minute ago, ShotgunNinja said:

Any drawbacks you are aware of, if the GameEvent is initialized in the static definition?

That was it is guaranteed to be initialized before first use, be it a listener or the mod that fire the event.


public static EventData<Part, ProtoCrewMember> onKerbalFrozen = new EventData<Part, ProtoCrewMember>("onKerbalFrozen");

 

Yes. Unless you reference it first it won't initialize. And the first time you Fire it it won't work.

Link to comment
Share on other sites

Ah okay. So I am assuming a GameEvent is registered in the set of available ones when its constructor is called, and when you call FindEvent<>() it will not find it if the constructor wasn't called before. Is that correct?

I'm asking these question because I wonder about the scenario when monobehaviours from mod A and B are both set to run on main menu, and ordering problems.

Link to comment
Share on other sites

4 minutes ago, ShotgunNinja said:

Ah okay. So I am assuming a GameEvent is registered in the set of available ones when its constructor is called, and when you call FindEvent<>() it will not find it if the constructor wasn't called before. Is that correct?

I'm asking these question because I wonder about the scenario when monobehaviours from mod A and B are both set to run on main menu, and ordering problems.

In that case initialize your Event before MainMenu (although I haven't tried)... as most use cases for this would be after MainMenu.
 

Link to comment
Share on other sites

If it's really a big deal you can run it using the KSPAddon.Instantly property so it runs before the MainMenu, but at the main menu the game proper isn't actually loaded yet

Off the top of my head I can't think of a use case where you'd fire one of these during the main menu scene, the earliest you'd fire them is during the game loading process, but that comes after the Main Menu scene so everything should be initialized already.

I realize there probably is a scenario where you would want to fire one of these events during the Main Menu, but as the save-game hasn't loaded yet at this point it feels like a "square-peg/round-hole" situation and probably there's a better way to do what you want to.

D.

Link to comment
Share on other sites

4 minutes ago, Diazo said:

Off the top of my head I can't think of a use case where you'd fire one of these during the main menu scene, the earliest you'd fire them is during the game loading process, but that comes after the Main Menu scene so everything should be initialized already.

I ll most likely add an event at the end of MM loading and that may be one those few cases :)

Link to comment
Share on other sites

  • 1 month later...

a question. Is it only addins (i.e. monobehaviour) type plugins where events are relevant, or could you create an event in a partsModule, for when some specific action is taken or something? I'm asking as I cannot figure out the implications of having events being created by multiple modules.....

Link to comment
Share on other sites

Well, not having tested it I see the STATIC keyword on the event method where it is created.

Statics can only exist once, so if you have a partModule with an "EventA" event, because it's static there is only one "EventA", even if there are multiple copies of the partModule loaded.

So you would need to pass the part or partModule triggering the event as the EventData to keep things straight.

I also see no mention of inheriting from MonoBehavior being necessary. I assume there is reflection trickery involved here finding all static methods with the EventData return type.

If I ever use this, my first attempt would be making a static class to hold my static event calls, then in a KSPAddon.MainMenu initialize them.

Then when they fire, just call the event from whatever code is running at the time. Since they are a static method in a static class I can access the .Fire method at any time.

I see a LOT of problems trying to add this in a partModule as the event has to exist before the other mod subscribes to it.

D.

Edited by Diazo
Link to comment
Share on other sites

14 hours ago, Diazo said:

I see a LOT of problems trying to add this in a partModule as the event has to exist before the other mod subscribes to it.

Static constructor ? This does not look like it needs any Unity code so it should init fine there.

Link to comment
Share on other sites

14 hours ago, Diazo said:

Well, not having tested it I see the STATIC keyword on the event method where it is created.

Statics can only exist once, so if you have a partModule with an "EventA" event, because it's static there is only one "EventA", even if there are multiple copies of the partModule loaded.

So you would need to pass the part or partModule triggering the event as the EventData to keep things straight.

I also see no mention of inheriting from MonoBehavior being necessary. I assume there is reflection trickery involved here finding all static methods with the EventData return type.

If I ever use this, my first attempt would be making a static class to hold my static event calls, then in a KSPAddon.MainMenu initialize them.

Then when they fire, just call the event from whatever code is running at the time. Since they are a static method in a static class I can access the .Fire method at any time.

I see a LOT of problems trying to add this in a partModule as the event has to exist before the other mod subscribes to it.

D.

There should be no problems if you:
Use a static class to hold your events and initialize them.
Fire them from teh partmodule or subscribe to them in the partmodule.

And yes, reflection is used.

Link to comment
Share on other sites

  • 2 weeks later...

Okay, not sure what is going on here, running some test code to understand things before I start actually using this.

Due to how I want to use this, I am going to end up with a Static event handler class, my current test case is as follows:

namespace ActionGroupsExtended
{
    public static class AGXEventHandler
    {
        public static EventData<string> onTestEvent;

        public static void init()
        {
            Debug.Log("AGX event init fired!");
            onTestEvent = new EventData<string>("onTestEvent");
            Debug.Log("AGX event init2fired!");
            onTestEvent.Add(testAGXmethod);
            Debug.Log("AGX event init 3fired!");
        }

        public static void testAGXmethod(string str)
        {
            Debug.Log("AGX sees event fire!");
        }
    }
}

the "init()" method is called from within another class running as a KSPAddon.Instantly so that runs very early in KSP's loading sequence. I did move this to KSPAddonMainMenu but the same error was thrown.

The error in question is thrown by the "onTestEvent.Add(testAGXmethod);" line and is as follows:

NullReferenceException: Object reference not set to an instance of an object
  at EventData`1+EvtDelegate[System.String]..ctor (.OnEvent evt) [0x00000] in <filename unknown>:0 
  at EventData`1[System.String].Add (.OnEvent evt) [0x00000] in <filename unknown>:0 
  at ActionGroupsExtended.AGXEventHandler.init () [0x00000] in <filename unknown>:0 
  at ActionGroupsExtended.AGXInstantly.Start () [0x00000] in <filename unknown>:0 

Note despite the reference to the Event's .ctor method, this nullRef is happening when I try to .Add(testAGXmethod) as I see the "AGX event init2fired!" in the log just before the error and "AGX event init 3fired!" does not show.

What am I missing here?

D.

Link to comment
Share on other sites

EDIT: See this post for the answer and example code on how to sync data between two mods by passing a reference.

 

 

To answer my question, the method called by the event firing cannot be static.

So in my example above, once I made the testAGXemthod(string str) non-static, things appear to work.

At least this step does, now to move on and find the next issue.

D.

edit: My next question is handling "requests". Using my AGX example, ModB wants to know which action groups have actions assigned. This is going to require two events, one to trigger the request, one to return the data. I'm thinking something like:

public static EventData<string,ConfigNode> onAGXDataRequest;
public static EventData<string,ConfigNode> onAGXDataReturn;

ModB would trigger onAGXDataRequest when it wants data, then AGX would trigger onAGXDataReturn with the data. The String would be an identifier, remember that ALL mods that subscribe to onAGXDataReturn will see the data so you need a way to tell which request is yours. The ConfigNode will contain the data requested and then the data returned.

However, this is messy because it breaks the code up in ModB. The request event gets fired, then there is a break in the code until ModB sees the data returned when the second event fires.

For something like kOS that needs instant data I'm not sure this works too well but I can't think of any better ideas. Anyone have any thoughts on this?

D.

Edited by Diazo
Link to comment
Share on other sites

4 hours ago, Diazo said:

edit: My next question is handling "requests". Using my AGX example, ModB wants to know which action groups have actions assigned. This is going to require two events, one to trigger the request, one to return the data. I'm thinking something like:


public static EventData<string,ConfigNode> onAGXDataRequest;
public static EventData<string,ConfigNode> onAGXDataReturn;

ModB would trigger onAGXDataRequest when it wants data, then AGX would trigger onAGXDataReturn with the data. The String would be an identifier, remember that ALL mods that subscribe to onAGXDataReturn will see the data so you need a way to tell which request is yours. The ConfigNode will contain the data requested and then the data returned.

 

Why not spin it around and make AGX the thing that listens for the event? Have the requester fire the event instead, AGX can fill in the desired info. You'd only need one event this way as well

Link to comment
Share on other sites

@xEvilReeperx That sounds exactly what I want to do.

I just don't have a clue what you mean by "fill in" data. I'm going to head to google after this post and see what I find, but my current understanding is:

Make onTestEvent.

In AGX: onTestEvent.Add(onEventFire);

In ModB call: onTestEvent.Fire();

That causes onTestEvent to be triggered by ModB and onEventFire to run in AGX and can pass data from ModB to AGX, but I'm blanking on how that gets data back to ModB.

 

Ignoring code for a second, what I'm trying to accomplish is:

A kOS script needs to know if action group 15 is on or off, that data is stored in AGX so it queries AGX "What is AG15s state?" AGX then returns "AG15 is on/off" depending on the groups current state. Currently we've worked out the raw reflection ourselves to get something that works, but that is messy and requires coordinating code between the two mods so I was hoping to streamline/simplify things with these new events, but it's not turning out to be as striaghtforward as I'd hoped.

D.

Link to comment
Share on other sites

I'm posting this question here as well as my response to #1939

Is there any reason that the event is required to return a ConfigNode based structure? Could you instead create a base event that returns a commonly known reference type, like a list? That would allow you to create an event that serves as an "initialization" or "heartbeat" event, that simply gives out the reference to the list.

public static EventData<string,ConfigNode> onAGXInitRequest;
public static EventData<string,List<bool>> onAGXInitReturn;

Under this structure, an object would subscribe to the onAGXInitReturn in a method that stores the reference to the list. Then it would fire the onAGXInitRequest event to signal to AGX that it needs to pass out the reference. AGX would then fire the onAGXInitReturn event, passing with it the reference to an internal list of the state of each action group. Then AGX would update the values for the actiongroup list (without creating a new list) allowing for instant action group status inspection. You could even go so far as to implement IList<bool> in your own class, allowing you to simply respond to the standard list methods without needing to change your own implementation

Link to comment
Share on other sites

That would work? That opens up possibilities.

As for what gets passed, I just threw out ConfigNode because that's what KSP uses to store data, the event could be attached to anything.

Now, I'm on my mobile and can't run test code, just to be clear what should happen (non-working pseudo-code):

In AGX:

public class EventHandler
{
	public static List<bool> actionGroupStates
    public static EventData<> ActionGroupStateRequest
	public static EventData<List<bool>> ActionGroupsStateData
	
    ActionGroupStateRequest.Add(requestFire)
    
    public void requestFire()
    {
    	ActionGroupsStateData.Fire(actionGroupStates)
    }
}

then in kOS:

[KSPAddon.Flight]
public class kOSFlight
{
	public List<bool> actionGroupsState;
  
  public void Start()
    {
      ActionGroupStateData.Add(handleData);	
      ActionGroupStateRequest.Fire();
    }
  
  public void handleData(List<bool> groupsList)
  {
    actionGroupsState = groupsList;
  }
}

So after all this code is run, when AGX updates EventHandler.actionGroupStates, those changes will be reflected in kOSFlight.actionGroupsState without any other code required?

Interesting and will be quite useful. However that requires some code on the kOS end of things to handle it, the question becomes which is the "better" way to handle things, this way via events or the existing reflection system we have setup?

One of my goals here is to make AGX easier to integrate with on a basic level for smaller mods, it's quite possible there will be a set of events for basic use of AGX, then stay with reflection for the more complex stuff kOS uses.

(No comment on the IList thing, that will have to wait until I'm at my KSP computer.)

D.

Edited by Diazo
Link to comment
Share on other sites

34 minutes ago, Diazo said:

So after all this code is run, when AGX updates EventHandler.actionGroupStates, those changes will be reflected in kOSFlight.actionGroupsState without any other code required?

Yeah, that should work so long as the value you pass through the event is a "reference type".  Technically there are a few tricks that could mess that up, like cloning values passed to the events.  I'm not in a spot to verify that Unity and/or KSP don't do something like that with event parameters, but I should be able to check it out tonight.  The important thing to remember is that the reference is to the object itself, and not to the variable.  So if you reassign the variable inside the AGX code, you should fire the event again to ensure that other mods update their reference.

Quote

Interesting and will be quite useful. However that requires some code on the kOS end of things to handle it, the question becomes which is the "better" way to handle things, this way via events or the existing reflection system we have setup?

One of my goals here is to make AGX easier to integrate with on a basic level for smaller mods, it's quite possible there will be a set of events for basic use of AGX, then stay with reflection for the more complex stuff kOS uses.

Well, reflection by it's very nature is complicated, and while it doesn't need to be avoided I think it makes sense to use simple alternatives if they are available.  In part that's because it relies on hard coded relationships to assemblies you don't control.  Another component however is performance.  I really wanted to outright modify the way that kOS calls the AGX method, because currently the method delegate is instantiated every time it is called, which I understand is expensive.  And in my opinion, anything that can be moved into a more common system which can be accessed using explicitly typed methods/fields is an improvement in both performance and code readability.  By common I mean a class or interface defined by an assembly that you know both mods have access to, essentially anything in the base Mono/.net framework.

 

After I finished typing out the above, I had the realization that there is one other thing we have to be careful with for events: object life cycle.  If an object is subscribed to an event and never unsubscribes, I don't believe that the GC will ever clean up that object (because the event code still has a reference to the object).  Which means that either the event should be cleared on scene change, or other mods need to be instructed to be careful to unsubscribe as part of the `OnDestroy` logic.  That does add a bit of complication when explaining how to implement the system, but I still think it's easier than explaining how reflection works.

Edited by hvacengi
Link to comment
Share on other sites

Hmmm. Lots of things to think on and test. Any tests on my end will have to wait until the weekend as I really won't have enough time to work on this until then.

First, on the object life cycle I think we just need to be clear that the OnDisable() method is used. (I think that is what Unity calls it's on destroy method.) This is true not just for this but for other things as well (toolbar buttons, etc.) so it's something a modder will need to know regardless. I think the benefits of having the event initialized as soon as the game loads to avoid race conditions is of a bigger benefit then destroying/recreating the event would be during the game to destroy orphaned delegates from event.

I'm not aware of any method where the event itself can check for null and remove delegates from itself?

Second, for the object passed I think it's going to be a "container" object that never changes, just the data inside. I can't see any other way to handle multiple vessels. For instance, a static Dict<Vessel,List<bool>> created when the game loads that could be passed by public EventData<Dict<Vessel,List<bool>> actionGroupStates and then AGX would add/remove vessels to that dictionary as they came into or left physics range and kOS should see them automatically update. This would require significant testing of course, but I think everything in that event is a reference type so this works in theory.

It would also require null checks all over the place as AGX would be adding/removing vessels to the dictionary without warning kOS about it.

Lastly, this is my first real foray into events so my terminology is iffy. Notably I'm not sure I'm using 'delegate' right, is the event itself a delegate, or the methods that get attached to the event via .add() the delegates?

D.

Link to comment
Share on other sites

Well, if you go with the one GameEvent, life cycle and all that shouldn't be a concern because the only thing that listens for the event is the event's owner anyway. Maybe it'll make more sense with a code example. Keep in mind that whatever parameter you decide to pass will need to be a reference type like @hvacengisaid

5 hours ago, Diazo said:

Ignoring code for a second, what I'm trying to accomplish is:

A kOS script needs to know if action group 15 is on or off, that data is stored in AGX so it queries AGX "What is AG15s state?" AGX then returns "AG15 is on/off" depending on the groups current state. Currently we've worked out the raw reflection ourselves to get something that works, but that is messy and requires coordinating code between the two mods so I was hoping to streamline/simplify things with these new events, but it's not turning out to be as striaghtforward as I'd hoped.

    [KSPAddon(KSPAddon.Startup.MainMenu, false)]
    class ModB : MonoBehaviour
    {
        private EventData<Dictionary<int, bool>> _actionGroupStateQuery;
  
        private void Awake()
        {
            _actionGroupStateQuery = GameEvents.FindEvent<EventData<Dictionary<int, bool>>>("AGX_QueryActionGroupStates");

            var currentStates = new Dictionary<int, bool>();

            _actionGroupStateQuery.Fire(currentStates);

            foreach (var state in currentStates)
                Debug.Log("Action group: " + state.Key + ": " + state.Value);
        }
    }


    class AGX
    {
        // note how AGX is the only thing that listens to this event. Mods who want to know stuff will fire it instead of listening
        public static readonly EventData<Dictionary<int, bool>>  QueryActionGroupStates =
            new EventData<Dictionary<int, bool>>("AGX_QueryActionGroupStates"); 

        // get around static method limitation of GameEvents
        private class Forwarder
        {
            private readonly EventData<Dictionary<int, bool>>.OnEvent _del;

            public Forwarder(EventData<Dictionary<int, bool>>.OnEvent del)
            {
                _del = del;
            }

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

        // make sure initialization happens early
        [KSPAddon(KSPAddon.Startup.Instantly, true)]
        private class Initialize : MonoBehaviour
        {
            private void Awake()
            {
                Init();
                Destroy(gameObject);
            }
        }


        private static void Init()
        {
            var forwarder = new Forwarder(OnQueryReceived);
            QueryActionGroupStates.Add(forwarder.OnEvent);
        }

        // somebody wants to know some stuff
        private static void OnQueryReceived(Dictionary<int, bool> data)
        {
            for (int i = 0; i < 32; ++i)
                data[i] = i % 2 == 0;
        }
    }

 

Link to comment
Share on other sites

19 minutes ago, xEvilReeperx said:

Well, if you go with the one GameEvent, life cycle and all that shouldn't be a concern because the only thing that listens for the event is the event's owner anyway.

Not necessarily.  If the event is declared static, it will not get cleaned with the life cycle of the instantiating object.  And you have opportunity to create circular references too.  Event1 refers to AGX which refers to Event2 which refers to kOS which refers to Event1... and so on.  It's my understanding that the Mono GC used by Unity is pretty simple, and just walks each object looking for references to other objects, removing any object that has no references pointing to it.  I ran into this issue with some KSP events that kOS subscribed to.  I found using the debugger that the event delegate was getting called well after the object itself should have been cleaned up (across scene transitions even).

Link to comment
Share on other sites

Hmmm.

Subject to lots and lots of testing, if I understand how referencing works, @xEvilReeperx code should work, but just use it to set the reference. If so, only a single event is required, not two.

 

Pseudo-code, leaving out a lot of steps:

AGX:

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

handleReference(List<bool> refList)
{
  refList = masterList; //does this work?
}


kOS:

public List<bool> kOSlistReference;
referenceListEvent = GameEvents.FindEvent<EventData<List<bool>>>("referenceMasterList");
referenceListEvent.Fire(kOSlistReference);

If this works, as long as the event and master data object exist in AGX, everything should be okay I think? Any changes AGX makes to masterList will also happen to kOSlistReference as far as I understand this.

The only catch is will kOSlistReference send itself to GC when the class it is a part of does, or will a kOSlistReference = null need to be added to the OnDisable() method to allow GC to happen?

Lots of ideas I really need to start testing on, Saturday can't come soon enough.

D.

Edited by Diazo
Link to comment
Share on other sites

3 minutes ago, hvacengi said:

Not necessarily.  If the event is declared static, it will not get cleaned with the life cycle of the instantiating object.

You shouldn't try to link the life cycle of a GameEvent-type class with anything but static because it won't be cleaned up anyway. They register themselves in a static, private dictionary by name. The only time a previously created EventData will be GC is if a new, identically-named one is created and no other references to the previous one exist. For example, this event will exist for the lifetime of KSP, long beyond its creator, because EventData<T> registers itself in a static dictionary inside BaseGameEvent:

    [KSPAddon(KSPAddon.Startup.MainMenu, true)]
    class HelloWorld : MonoBehaviour
    {
        private void Awake()
        {
            var testTheEvent = new EventData<string>("helloworld");
        }
    }

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

Link to comment
Share on other sites

2 minutes ago, xEvilReeperx said:

You shouldn't try to link the life cycle of a GameEvent-type class with anything but static because it won't be cleaned up anyway. They register themselves in a static, private dictionary by name. The only time a previously created EventData will be GC is if a new, identically-named one is created and no other references to the previous one exist. For example, this event will exist for the lifetime of KSP, long beyond its creator, because EventData<T> registers itself in a static dictionary inside BaseGameEvent:

 

Interesting, so making a permanent EventHandler class was the correct way to go. I did so in order to make the events exist as early as possible during game load to avoid race conditions, but this is another reason to do so.

D.

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...