Jump to content

Action Groups Extended Development Thread and External Interface


Diazo

Recommended Posts

As of KSP 1.0 I need to overhaul this thread. Please see the External.cs code page on my GitHub for external commands supported in the current version of AGX.

External Interface undergoing overhaul. Please confirm with me directly before using the methods below as some of them are changing. (Jan 04/15)

Action Groups Extended external interface for version 1.20a or newer (Update Dec. 04/14)

Welcome and thank you for looking at integrating your mod with mine. If you have any questions or need a hand please feel free to leave a message.

I have this setup so that you don't have to make a hard dependency on AGX so you only have to maintain one version of your mod. Rather, this interface uses reflection so your mod can dynamically determine if AGX is installed when KSP starts.

However, that means you need to copy-paste some code into your own mod for this to work. Copy the code inside the code blocks in its entirety for the function you want to add to your mod.

First, make some room in the same namespace as your mod so your code can call the methods I paste below and make sure it is using System.Reflection (on the next line after the using UnityEngine works good).


using UnityEngine; //already present for you own mod's use
using System.Reflection; //make sure this line is present so AGX's reflection interface works.

Note that AGX supports all action groups so you can handle groups 1 through 10 through AGX if it is installed. However, if you want you can send action commands for groups 1 through 10 to default KSP and AGX will still work correctly.

There are 11 methods current offered (as of Version 1.20a) as follows:

Is AGX installed? This method returns a bool, true if AGX is installed, false if not. Use this as a gate so your code knows if action groups 11 through 250 are present or not.

public static bool AGExtInstalled()
{
try //try-catch is required as the below code returns a NullRef if AGX is not present.
{
Type calledType = Type.GetType("ActionGroupsExtended.AGExtExternal, AGExt");
return (bool)calledType.InvokeMember("AGXInstalled", BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static, null, null, null);
}
catch
{
return false;
}
}

Toggle an action group on the vessel that has focus. This works for all groups including groups 1-10 so you can send all action commands to AGX, although AGX is compatible with default KSP so action commands from groups 1 through 10 can be sent either place. Does not return anything. Pass group to toggle as int.

public void AGExtToggleGroup(int group)
{
Type calledType = Type.GetType("ActionGroupsExtended.AGExtExternal, AGExt");
calledType.InvokeMember("AGXToggleGroup", BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static, null, null, new System.Object[] { group });
}

Activate or Deactivate an action group on current vessel. Note that AGX tracks actions individually within a group and that trying to activate an action that is already activated will do nothing. Pass group to activate as int, forceDir as true to force activate, forceDir as false to force deactivate.

public void AGXActivateGroup(int group, bool forceDir)
{
Type calledType = Type.GetType("ActionGroupsExtended.AGExtExternal, AGExt");
calledType.InvokeMember("AGXActivateGroup", BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static, null, null, new System.Object[] { group, forceDir });
}

Is an action group activated on the current vessel? Note that AGX tracks individual actions to see if they are activated, not action groups. On an action group with multiple actions assigned, this function returns false if all actions are deactivated and true if any (but not necessarily all) actions are activated.

public bool AGXGroupState(int group)
{
Type calledType = Type.GetType("ActionGroupsExtended.AGExtExternal, AGExt");
bool GroupAct = (bool)calledType.InvokeMember("AGXGroupState", BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static, null, null, new System.Object[] { group});
return GroupAct;
}

Get a list of actions in a specific group on the current vessel. Returns a List<BaseAction> object. Pass group to get actions from as int.

public List<BaseAction> AGXGroupActions(int group)
{
Type calledType = Type.GetType("ActionGroupsExtended.AGExtExternal, AGExt");
List<BaseAction> RetActs = (List<BaseAction>)calledType.InvokeMember("AGXGroupActions", BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static, null, null, new System.Object[] { group });
return RetActs;
}

Return a list of all assigned actions on the current vessel. Note this does not return which group the actions are in at the moment. (Current priority is to return the group as well but have not quite figured it out yet.) Returns a List<BaseAction> object and does not take any parameters.

public List<BaseAction> AGXAllActions()
{
Type calledType = Type.GetType("ActionGroupsExtended.AGExtExternal, AGExt");
List<BaseAction> RetActs = (List<BaseAction>)calledType.InvokeMember("AGXAllActions", BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static, null, null, new System.Object[] { });
return RetActs;
}

The following methods are the same as above, except they take a FlightID parameter (as uint) for which vessel to use. This parameter is found at Vessel.rootPart.FlightID and passing the vessel that currently has focus is acceptable. Note that these methods return a bool, if true the command completed, if false there was something wrong. Most likely the vessel was out of physics range and so unloaded and AGX could not activate the actions it was asked too. Note that if AGX is passed a group with no assigned actions to activate, it will return true for sucsessful even though it did not actually activate anything.

public bool AGX2VslToggleGroup(uint FlightID, int group)
{
Type calledType = Type.GetType("ActionGroupsExtended.AGExtExternal, AGExt");
bool GroupAct = (bool)calledType.InvokeMember("AGX2VslToggleGroup", BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static, null, null, new System.Object[] { FlightID, group });
return GroupAct;
}

public bool AGX2VslActivateGroup(uint FlightID, int group, bool forceDir)
{
Type calledType = Type.GetType("ActionGroupsExtended.AGExtExternal, AGExt");
bool GroupAct = (bool)calledType.InvokeMember("AGX2VslActivateGroup", BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static, null, null, new System.Object[] { FlightID, group, forceDir });
return GroupAct;
}

public bool AGX2VslGroupState(uint FlightID, int group)
{
Type calledType = Type.GetType("ActionGroupsExtended.AGExtExternal, AGExt");
bool GroupAct = (bool)calledType.InvokeMember("AGX2VslGroupState", BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static, null, null, new System.Object[] { FlightID, group });
return GroupAct;
}

public List<BaseAction> AGExtGet2VslGroupActions(uint FlightID, int group)
{
Type calledType = Type.GetType("ActionGroupsExtended.AGExtExternal, AGExt");
List<BaseAction> RetActs = (List<BaseAction>)calledType.InvokeMember("AGX2VslGroupActions", BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static, null, null, new System.Object[] { FlightID, group });
return RetActs;
}

public List<BaseAction> AGExtGet2VslAllActions(uint FlightID)
{
Type calledType = Type.GetType("ActionGroupsExtended.AGExtExternal, AGExt");
List<BaseAction> RetActs = (List<BaseAction>)calledType.InvokeMember("AGX2VslAllActions", BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static, null, null, new System.Object[] { FlightID });
return RetActs;
}

Note I am planning to expand this, notably to allow activation by group name instead of group number but I am unsure on how quickly I can add those methods.

There is a self-contained module with it's source here that I used for testing where you can see how things actually worked for me.

D.

edit: THis still needs documentation:

 public static List<Part> AGX2VslPartsWithActions(uint flightID, int group)
{
Type calledType = Type.GetType("ActionGroupsExtended.AGExtExternal, AGExt");
List<Part> RetActs = (List<Part>)calledType.InvokeMember("AGX2VslListOfPartsInGroup", BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static, null, null, new System.Object[] {flightID, group });
return RetActs;
}
public static List<PartModule> AGX2VslPartModulesWithActions(uint flightID, int group)
{
Type calledType = Type.GetType("ActionGroupsExtended.AGExtExternal, AGExt");
List<PartModule> RetActs = (List<PartModule>)calledType.InvokeMember("AGX2VslListOfPartModulesInGroup", BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static, null, null, new System.Object[] {flightID, group });
return RetActs;
}

public static List<Part> AGXPartsWithActions(int group)
{
Type calledType = Type.GetType("ActionGroupsExtended.AGExtExternal, AGExt");
List<Part> RetActs = (List<Part>)calledType.InvokeMember("AGXListOfPartsInGroup", BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static, null, null, new System.Object[] { group });
return RetActs;
}
public static List<PartModule> AGXPartModulessWithActions(int group)
{
Type calledType = Type.GetType("ActionGroupsExtended.AGExtExternal, AGExt");
List<PartModule> RetActs = (List<PartModule>)calledType.InvokeMember("AGXListOfPartModulesInGroup", BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static, null, null, new System.Object[] { group });
return RetActs;
}

Edited by Diazo
Link to comment
Share on other sites

  • 3 months later...

To activate an action group, pass the following method the action group to activate as an integer:

public void AGExtActivateGroup(int group)
{
Type calledType = Type.GetType("ActionGroupsExtended.AGExtExternal, AGExt");
calledType.InvokeMember("AGXActivateGroup", BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static, null, null, new System.Object[] { group });
}

Hmm.. the stock action groups operate by being settable to true OR settable to false by passing in a boolean flag. When you press the action group hotkeys 1-9, they actually in fact TOGGLE the action group's state. (If you press 1 twice, action group 1 goes from false to true to false again).

kOS makes use of that by binding the action groups to magic variable names, so you can, for example do


set AG1 to true.
set AG1 to false.

I'm not sure how to implement something like set ag11 to false with what you're describing. I can activate it (set it to true) but how do I de-activate it?

Link to comment
Share on other sites

Oh, hmmm.

The method I linked to 'activate' an actiongroup is actually the toggle method. It's smart enough that if the group is deactivated, it activates it, if activated it deactivates it. Effectively passing that method group 5 is the same as hitting the 5 key on the keyboard. (Poor choice of wording on my part to use activate there.)

As for your current setup, I don't expose 'activate' or 'deactivate' methods at the moment. It will be really quick to do so, it will just have to wait until I'm home from work.

I think you are actually the first person to interface externally like this, I will have to ask you to put up with being my tester for it and finding all these things I overlooked.

Thanks for the feedback,

D.

Link to comment
Share on other sites

Oh, hmmm.

The method I linked to 'activate' an actiongroup is actually the toggle method. It's smart enough that if the group is deactivated, it activates it, if activated it deactivates it. Effectively passing that method group 5 is the same as hitting the 5 key on the keyboard. (Poor choice of wording on my part to use activate there.)

Ah, then technically it should be possible to do it with the API that's there already (although I do think it would be a really good idea to change the name of the AGExtActivateGroup() method to AGExtToggleGroup(), which shouldn't be a problem with backward compatibility if nobody else but me has looked at your API yet).

If I have access to a toggle method and a query method, then I can use them together to implement a "set to true" and "set to false", as follows:

newVal is the new desired value for the action group, either True or False:

- If (query-the-action-group != newVal), then toggle-the-action-group.

- else do nothing.

Thanks for the information.

I'm normally reluctant to insert code into kOS designed to *specifically* support exception cases for another particular mod, because I think that crosses a line that can be dangerous given the vast plethora of mods out there. If each pairing of mods had merely 1 line of code in either of the mods of the pair that makes one exception for the other, that would cause the existence of N factorial number of lines, where N is the number of mods existing. It's the full-connectivity network graph problem.

But, action groups are *SO* integral to using kOS and *SO* tightly connected to it, that I think making an exception to enable more action groups is worth it.

Edited by Steven Mading
Link to comment
Share on other sites

First, thank you for making an exception for my mod. Actually, I think doing so will provide indirect support for other mods as for most mods, they can assign that mods action to one of my action groups and then activate the mod that way rather then needing a dedicated kOS line of code.

Won't work for all mods of course, but I would think it would for a lot of them.

On the method call to operate an action group, my first thought was to add an integer for operation type.

So:

AGExtActivateGroup(1, 0); //toggle group 1
AGExtActivateGroup(1, 1); //activate group 1
AGExtActivateGroup(1, 2); //deactivate group 1

I was originally going to leave the method as is and add a second method that took a true/false to operate (for activate/deactivate), but that would be another method you have to copy into your code. I'd rather keep it so that you only have to copy a single activation method, even if it makes it more complex, then make you copy-paste multiple activation methods into your code.

I don't know of a way in C# to have different overloads without making multiple methods so this is the best way I can think of at the moment to have a single method to operate an action group for all three cases (toggle, activate, deactivate).

Or do you want me to set it up as 3 methods?

So:

AGExtToggleGroup(1); //toggle group 1
AGExtActivateGroup(1); //activate group 1
AGExtDeactivateGroup(1); //deactivate group 1

This means you would have to copy 3 methods instead of 1 into your code though. Either way, the method's syntax is going to change once I get home and can tweak this for you so please don't go too far on your end yet.

However, as the one interfacing from outside my mod, what are your current thoughts on what you want to see? I am going to have methods available to toggle/activate/deactivate an action group, it's just a matter of what the best way to present it is.

D.

Edited by Diazo
Link to comment
Share on other sites

Hmm, I'm not really clear on why we have to cut-n-paste code in the first place, rather than calling a method from your DLL.

Is it because you didn't want to make the method Static?

I think it would be a lot cleaner to have a static method we have to pass-in the reference to our "this" as an argument, rather than cut-n-paste code from you to us.

i.e. this:

AGExtToggleGroup(this, 1). // toggle group 1 on me.

As to needing 3 different methods, I really think you can collapse the Activate and Deactivate into one method that takes a boolean like so:

AGExtSetGroup(11, true); // activate group 11.

AGExtSetGroup(11, false); // de-activate group 11.

(or if you use the static method passing "this", as shown above, it would be:)

AGExtSetGroup(this, 11, true); // activate group 11.

AGExtSetGroup(this, 11, false); // de-activate group 11.

I'm not really a C# expert. I only started using C# *because* wanted to join the kOS devs. Prior to that I did do a lot of Java and C++, though, so a lot of what C# does was immediately intuitive and obvious to me as it borrowed from those two languages extensively. But it is a bit less flexible about a lot of things, in ways that I find annoying and arrogant on the part of Microsoft ("because there are ways to misuse this language feature, we'll disable it, thus preventing all the legitimate good uses of this language feature too").

Link to comment
Share on other sites

Okay, we are now getting into areas where I'm not 100% sure of my answers. I'm just a guy on the internet and the KSP mods I've done are my entire effective programming experience.

1) Cut and paste code

I understood that this was necessary to avoid having to compile 2 version of kOS. You can certainly directly link to my .dll if you want. (I think the methods are public? I have no problem with making them public if the are not.)

The downside is that if you hard-link like that, if my mod is not present, that version of kOS will error and not load when KSP asks it to.

The workaround for this is Reflection with is where the .calledType and .invoke stuff comes from. That means you only have to maintain one version of kOS.

2) Static method and 'this'.

Sorry, totally over my head. I know what a static method is, but I don't understand how the 'this' keyword comes into play here.

Are you talking about different vessels? One limitation of AGX is that it is for the currently active vessel, the one returned by FlightGlobals.ActiveVessel (I can add support for nearby vessels if that is something kOS does, but it will have to wait until I finish moving my data storage over to the scenarioModule.)

3) Number of methods

So you'd like to see AGXToggleGroup(1) and AGXSetGroup(1,true) as the methods used to operate action groups? Works for me.

D.

Link to comment
Share on other sites

Okay, we are now getting into areas where I'm not 100% sure of my answers. I'm just a guy on the internet and the KSP mods I've done are my entire effective programming experience.

1) Cut and paste code

I understood that this was necessary to avoid having to compile 2 version of kOS. You can certainly directly link to my .dll if you want. (I think the methods are public? I have no problem with making them public if the are not.)

The downside is that if you hard-link like that, if my mod is not present, that version of kOS will error and not load when KSP asks it to.

The workaround for this is Reflection with is where the .calledType and .invoke stuff comes from. That means you only have to maintain one version of kOS.

I think erendrake should look over this issue, as he's more experienced with how you do Reflection in C#. Let's leave the question of whether it's cut-n-paste alone for the time being. I'll bring it up with him later.

2) Static method and 'this'.

Sorry, totally over my head. I know what a static method is, but I don't understand how the 'this' keyword comes into play here.

It's a common means of making a static method behave LIKE it was a dynamic one. If you pass in a reference to 'this', and have the static method use that 'this' reference, you can make the static method operate on that instance of the object.

For example:


// a non-static function that prints the fields myField1 and myFielld2:
public void PrintStuff()
{
Debug.Log( myField1 + ", " + myField2);
}
// The same thing done with a static function instead:
public static void PrintStuff2( TheType theObj )
{
Debug.Log( theObj.myField1 + ", " + theObj.myField2);
}
// Now, instead of saying:
// this.PrintStuff()
// you can get the same exact effect from:
// PrintStuff2( this );

The point is that it lets you use a static method in places where it's required (like a delegate callback), while still actually operating on the instance of an object rather than operating globally like static methods usually do.

In this way I wouldn't have to add your member function to my class with cut-n-paste. Instead I'd call your static method, and pass it a reference to my class instance ('this').

Are you talking about different vessels? One limitation of AGX is that it is for the currently active vessel, the one returned by FlightGlobals.ActiveVessel (I can add support for nearby vessels if that is something kOS does, but it will have to wait until I finish moving my data storage over to the scenarioModule.)

Hmm... that will be a problem with kOS. We've been trying very hard to make it possible to run kOS scripts on more than one vessel at the *same time* without switching between them with the '[' and ']' keys. (So you can have two vessels, each running a docking program, where each vessel helps point itself toward its partner vessel, for example - or have two vessels leapfrogging over each other across the landscape, for example, or launch two robotic fire-and-forget missiles and have them both flying at the same time.) To accomplish this, we had to almost entirely eradicate "FlightGlobals.ActiveVessel" from our thinking - because the vessel on which the kOS script is running cannot be assumed to always be the vessel the player has active. Granted, it typically does have to be within 2.5 km of the active vessel, or it gets unloaded and kOS can't function, but it should be possible for vessels near enough to your active vessel to be loaded to be running kOS scripts.

Thus we'd need for there to be some way to tell AGX which vessel we meant. We can't just always assume a kOS script is running on the ActiveVessel.

3) Number of methods

So you'd like to see AGXToggleGroup(1) and AGXSetGroup(1,true) as the methods used to operate action groups?

Yeah that seems fine.

Link to comment
Share on other sites

I think erendrake should look over this issue, as he's more experienced with how you do Reflection in C#. Let's leave the question of whether it's cut-n-paste alone for the time being. I'll bring it up with him later.

Works for me. What I have I pretty much hacked together so I know it works, but I have no clue how 'correct' it is for what I'm trying to accomplish with it.

It's a common means of making a static method behave LIKE it was a dynamic one. If you pass in a reference to 'this', and have the static method use that 'this' reference, you can make the static method operate on that instance of the object.

For example:


// a non-static function that prints the fields myField1 and myFielld2:
public void PrintStuff()
{
Debug.Log( myField1 + ", " + myField2);
}
// The same thing done with a static function instead:
public static void PrintStuff2( TheType theObj )
{
Debug.Log( theObj.myField1 + ", " + theObj.myField2);
}
// Now, instead of saying:
// this.PrintStuff()
// you can get the same exact effect from:
// PrintStuff2( this );

The point is that it lets you use a static method in places where it's required (like a delegate callback), while still actually operating on the instance of an object rather than operating globally like static methods usually do.

In this way I wouldn't have to add your member function to my class with cut-n-paste. Instead I'd call your static method, and pass it a reference to my class instance ('this').

Ah, k. Neat trick that I will have to remember.

Hmm... that will be a problem with kOS. We've been trying very hard to make it possible to run kOS scripts on more than one vessel at the *same time* without switching between them with the '[' and ']' keys. (So you can have two vessels, each running a docking program, where each vessel helps point itself toward its partner vessel, for example - or have two vessels leapfrogging over each other across the landscape, for example, or launch two robotic fire-and-forget missiles and have them both flying at the same time.) To accomplish this, we had to almost entirely eradicate "FlightGlobals.ActiveVessel" from our thinking - because the vessel on which the kOS script is running cannot be assumed to always be the vessel the player has active. Granted, it typically does have to be within 2.5 km of the active vessel, or it gets unloaded and kOS can't function, but it should be possible for vessels near enough to your active vessel to be loaded to be running kOS scripts.

Thus we'd need for there to be some way to tell AGX which vessel we meant. We can't just always assume a kOS script is running on the ActiveVessel.

Well, I will keep this in mind while I move my data over to the scenarioModule. I'm pretty sure I already know how to implement running an action group on a loaded, but non-focused, vessel with my new data structure.

I assume that changing the call so that calls made to AGX are

AGXToggleGroup(Vessel, Group); //Vessel as vessel object, Group as int for action group to toggle

would not be an issue?

D.

Edited by Diazo
Link to comment
Share on other sites

Oh, by the way if it's going to take you a while to get around to this, no hurry. I'm not in a position to be ready on my side to implement it yet anyway, as I'm working on other bugs first. Just remind me with a PM when you've got something for me to try.

Link to comment
Share on other sites

Will do.

It will be a while on my end, I've just started moving from saving data from partModule to scenarioModule so I need to get that done before I look at doing anything to the external interface methods.

D.

Link to comment
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.

×
×
  • Create New...