1. This forum is obsolete and read-only. Feel free to contact us at support.keenswh.com

Tutorial: The Programmable Block Argument

Discussion in 'Programming Guides and Tools' started by Malware, May 28, 2015.

Thread Status:
This last post in this thread was made more than 31 days old.
  1. Malware

    Malware Master Engineer

    Messages:
    9,867
    UPDATE!!!
    Go here for much more in-detail tutorials


    The Basics


    The programmable block argument is a string which is passed to your script's Main function.

    Code:
    public void Main(string argument)
    {
    	Echo(argument);
    }
    If you put the code above into a programmable block, and then drag this programmable block onto a button, you'll be prompted for its argument.

    [​IMG]

    If you enter "Hello" into this dialog, close it, and then click the button, you'll find the text "Hello" in the details section of your programmable block control panel.

    [​IMG]


    The simplest application of this feature is to have your single programmable block do several things. Take for instance the following code:

    Code:
    public void Main(string action)
    {
    	switch (action)
    	{
    	   case "OpenHangarDoor":
    		   RunOpenHangarDoorSequence();
    		   break;
    	   case "CloseHangarDoor":
    		   RunCloseHangarDoorSequence();
    		   break;
    	   default:
    		   Echo("Don't know " + action);
    		   break;
       }
    }
    
    public void RunOpenHangarDoorSequence()
    {
       // Do whatever is needed to open the hangar door
    }
    
    public void CloseHangarDoorSequence()
    {
       // Do whatever is needed to close the hangar door
    }
    
    Now you can assign the same programmable block to two buttons, one with the argument "OpenHangarDoor" and the other with the argument "CloseHangarDoor", and you have two different actions run from a single programmable block.


    Calling another programmable block with an argument

    Another programmable block can be called directly via its bool TryRun(string argument) method. This method will return true if the program was run, or false if the block is off or disabled or for any other (known) reason can't run the program.

    Note: A programmable block cannot run itself, or by proxy cause itself to be run using this method.

    Code:
    public void Main()
    {
    	var otherPb = GridTerminalSystem.GetBlockWithName("Programmable block 2");
      if (otherPb.TryRun("Hello")) {
    Echo("The other PB was run successfully");
    }
    }
    

    The Argument Text Box

    [​IMG]

    The text entered into this text box will be passed to your programmable block only when you click the Run button. This can be used to debug your script, or to create command terminals.
     
    Last edited: Jul 14, 2018
    • Like Like x 4
  2. Malware

    Malware Master Engineer

    Messages:
    9,867
    Known Bugs
    • Some synchronization issues have been reported in multiplayer when it comes to transferring the argument.
    • The timer block is completely resetting the argument, so it is never passed to the programmable block.
    These issues have been fixed on GitHub and are awaiting review. This pull request also finishes off this feature by letting you run the programmable block by pressing enter in the Argument text box, and adding a check box to clear the Argument text box once the programmable block has run. This makes it even easier to make console terminal type scripts.
    (the enter in argument text box feature was nixed, for now - will be readded in the future)

    Multiple arguments

    People have been asking about multiple arguments. These are the reasons I didn't do it that way:
    • The golden 80/20 rule. If less than 20% of your users are going to use a specific feature, don't bother. The absolute majority of scripts will either not use arguments at all, or just use a single command.
    • Performance and memory allocation. Again, the majority of scripts won't need such a feature. Why spend time and memory on the job? Sure it doesn't seem like much, but a script can be called every single tick using a timer set to trigger now, and if you're playing multiplayer with who knows how many programmable blocks running, the garbage collector will have it's job cut out for it trying to keep the memory clean. Yes, it's not really all that much involved, but again, 80/20. To be exact, I was explicitly told by the Keen developers themselves to stay away from unnecessary allocations in these situations.
    • People are going to want various argument formats.
    • People who needs multiple arguments are very likely to be more than capable of rolling their own, then there's a simple case of copy/paste.
     
    Last edited: Jun 12, 2015
  3. Harrekin

    Harrekin Master Engineer

    Messages:
    3,077
    I vote Sticky.

    Thanks again man, concise and awesome.
     
  4. DON78

    DON78 Trainee Engineer

    Messages:
    51
    Same Bug with Buttons. They seem to reset the Argument, too. Passig from a Cockpit works fine, while Buttons do not make anything (vanilla- and mod-buttons tested).
     
  5. Malware

    Malware Master Engineer

    Messages:
    9,867
    @DON78 What buttons? It works perfectly with button panels, I do this all the time? Or are you talking multiplayer in which case it is the synchronization issue I mentioned. The argument is never transmitted. In any case, all such issues should be fixed once the pull request is merged.

    [Edit] I hope ;)
     
  6. Tony Hughes

    Tony Hughes Junior Engineer

    Messages:
    715
    @LordDevious

    Great feature and something that was much needed!

    I'd like to ask; when calling one PB from another via code, does the IL count of each PB called use up it's parent PB allowance or do they each get a separate 50K allocation?

    So, if PBA calls PBB and that calls PBC, is the total IL allowance for the three 50K or 150K?

    Also does execution return to the parent PB, after a child PB is called, or does it stop at the end of the child script?
     
  7. Malware

    Malware Master Engineer

    Messages:
    9,867
    Glad you like it :)

    Here I can only give you an educated guess, but one I'm fairly certain of: As far as your calling PB is concerned, this is a normal method call and shouldn't count up any more than any other method calls. I.e. they're separate.

    Again, this is just a method call like any other, so unless an exception occurs in that other PB, execution returns.
     
  8. Tony Hughes

    Tony Hughes Junior Engineer

    Messages:
    715
    @LordDevious

    So we can now basically setup suites of library functions, selectable by argument, on PBs and every function called, from another PB, will effectively give us an additional 50K IL allowance to our parent PB allowance? If so then that's rather nice and very useful.

    EDIT: I think that I may have misunderstood your reply, on second reading it looks like you're saying that the IL count of the called library functions would add to the parent PB tally after all. I think I'll have a look later and see if I can find out for sure.
     
    Last edited: Jun 4, 2015
  9. Malware

    Malware Master Engineer

    Messages:
    9,867
    n
    Yes, but I'm not sure i would recommend going that route unless absolutely necessary, because there are some overhead in these calls so you may end up with a badly performing script. Such calls should be restricted to functions that do more than just simple utility things.

    Obviously I'm being a bit dramatic here but better safe than sorry :)
     
  10. Tony Hughes

    Tony Hughes Junior Engineer

    Messages:
    715
    Wow, didn't misread it after all. Yes, I can imagine that there would be some performance implications from that, if used extensively. I guess it also means that we can do something like:

    PBA.FunctionA calls PBB.FunctionB.
    PBB.FunctionB calls PBA.FunctionC and/or PBA.FunctionD

    ..which would essentially give us an unbounded IL limit while keeping the code related to the application in more or less the same place. Will definitely have to play around with that.

    Is there any obvious way of returning bool/int/string results from cross PB function calls?

    Sorry for all the questions, and thanks for taking the time to reply.
     
  11. Malware

    Malware Master Engineer

    Messages:
    9,867
    Sorry, return values are not supported. These actions are originally intended for use in the toolbars, not by code, so there's simply no mechanism for it - nor do I think there should be, honestly.

    Ask away, that's what this thread is for! :) You may not always get a near-instant answer like now, but I'll always try to answer any question the best I can.
     
  12. Tony Hughes

    Tony Hughes Junior Engineer

    Messages:
    715
    Cheers.. from the base class that you added, it sort of looks like setting a value in the storage string of a child PB would be accessible from a parent PB, so perhaps results could be returned that way with a little parsing on the caller?
     
  13. Malware

    Malware Master Engineer

    Messages:
    9,867
    ... I really hope I didn't manage to make that value writeable, that would be a very bad mistake indeed. It is extremely important that Storage remains 100% predictable from its owner PB's point of view.

    [Edit] I double checked, the Storage property is not available from another PB.
     
  14. Tony Hughes

    Tony Hughes Junior Engineer

    Messages:
    715
    Oops, sorry to worry you, that wasn't my intention at all.

    To be more specific, I'm thinking more along the lines of writing the result that should be returned to the parent, in the child PB itself, where the storage string is writable, and then reading it from the parent, which from the base class does look possible.

    Maybe there's a wrapper around it that prevents that higher up or maybe it was amended by Keen after merging. Don't have access to the code and I'm unable to run the game at the moment, to check.

    Anyway, regardless of whether that's possible or not, it's still incredibly useful functionality and means that I can do some stuff that I've had on hold for a long time now. Thanks again.
     
  15. Malware

    Malware Master Engineer

    Messages:
    9,867
    Hey, you're right, I didn't even realize that when I wrote it... I didn't really intend to have that value public, but if Keen accepted it it can't be a problem :)
     
  16. Tony Hughes

    Tony Hughes Junior Engineer

    Messages:
    715
    :)
     
  17. Malware

    Malware Master Engineer

    Messages:
    9,867
    ... Except you can't get the program of a PB, can you, so you're just as far.
     
  18. Sarithule

    Sarithule Trainee Engineer

    Messages:
    55
    Code:
    List<TerminalActionParameter> args = new List<TerminalActionParameter>();
    
    public void Main()
    {
        var otherPb = GridTerminalSystem.GetBlockWithName("Programmable block 2");
    
        Echo(Me.CustomName);
        args.Clear();
        args.Add(TerminalActionParameter.Get("Hello"));
        otherPb.ApplyAction("Run", args);
    }
    
    is the TerminalActionParameter struct "built in" or does it need to be initiated in the code and if so what does it look like? Why is args a list? what exactly is args.Add(TerminalActionParameter.Get("Hello")); doing?
    this is a bit confusing. I am new to C# and programing in general btw.
     
  19. Malware

    Malware Master Engineer

    Messages:
    9,867
    The example is complete, the parameter is indeed "built in". All you need to do to test this is to create two programmable blocks, with this script in the first block, and a simple script to echo the argument in the "Programmable block 2" block.

    I understand how this can be confusing. It's an unusual way to call a method. However it is necessary. Args is a list because terminal action parameters is a generic feature for terminal actions, and while the Run action takes only one argument, other actions might have more than one. And more detailed: Args is a list rather than a parameter array for performance and memory reasons. I was told to do it this way by Keen developers when I made this feature.

    A TerminalActionParameter is a structure which can be serialized and transferred over the internet for multiplayer, retaining type information. The args.Add(TerminalActionParameter.Get("Hello")) line creates a single parameter of type string, containing the "Hello" value. This is added to the args list and passed to the action.

    In short, all that nonsense is equivalent to a Run(string method). I do eventually intend to add such a method directly to the IMyProgrammableBlock interface, which hides this complexity, but I don't know when that will be, so for now you need to use the terminal action parameter system directly.
     
    • Like Like x 1
  20. Ronin1973

    Ronin1973 Master Engineer

    Messages:
    4,964
    Thank you Devious and Sarith for examining my request... hopefully it's not falling into the 20% rule. Cheers. :)
     
  21. Sarithule

    Sarithule Trainee Engineer

    Messages:
    55
    So this
    Code:
    List<TerminalActionParameter> args = new List<TerminalActionParameter>();
    
    public void Main(string argument)
    {
        var otherPb = GridTerminalSystem.GetBlockWithName("Programmable block 2");
        var originalArg = argument;
      
        Echo(Me.CustomName);
        args.Clear();
        args.Add(TerminalActionParameter.Get(originalArg));
        otherPb.ApplyAction("Run", args);
    }
    makes the first programing block run the second programing block with the original argument that I passed. Awesome!
    through experimentation I also learned that the program pauses the first code until the second code runs through and then returns to the first to complete what ever comes after otherPb.ApplyAction("Run", args);. Each programing block will only run once per activation time slot, so the second can't call the first again which would form an infinite loop. Correct me if I'm wrong.

    Questions:
    1. Is there a reason you define args outside the main function and then clear it inside the main function instead of just defining it in the main function? is it a performance reason?
    2. What are the actions that can be taken by TerminalActionParameter. I see that there is .Get(), are there any others?
    3. Is there some place where I can find more info on what terminal actions besides "Run" can be used with TerminalActionParameter and how many arguments they take and what those arguments are?

    Thank you for sharing your time and knowledge by the way, I really appreciate it!
     
  22. Malware

    Malware Master Engineer

    Messages:
    9,867
    No correction needed :)

    Yes. While this is not required to make it work, I try to encourage (by example) people to think of where they put their allocations. Now if your script is just a single run command, this does not matter at all. However scripts can potentially be called every single tick of the game. (timer, trigger self now, trigger pb). Imagine a multiplayer server, with several of these scripts, every single one of them new'ing objects at a whim for every frame. Then imagine the poor garbage collector who has to try to keep this clean :) (all scripts run on the server by the way, not the client)

    Nice to know: structs (like the TerminalActionParameter) are not as much a problem as classes (like a List) when it comes to allocations.

    No. The terminal action parameter is a simple information packet. The get method is a helper method so you don't need to write
    Code:
    if (value == null)
    	args.Add(new TerminalActionParameter());
    else {
    	var typeCode = Type.GetTypeCode(value.GetType());
    	AssertTypeCodeValidity(typeCode);
    	args.Add(new TerminalActionParameter(typeCode, value));
    }
    

    I don't think so, but as far as I know there are currently no other actions using parameters. This was a something I needed to add when creating the programmable block argument feature (it's used when you drag a programmable block to a toolbar), but Keen has yet to add any actions themselves using it. It was developed in cooperation with them however, so they are very well aware of them.
     
    • Like Like x 1
  23. Sarithule

    Sarithule Trainee Engineer

    Messages:
    55
    You have answered all my questions very concisely, Thank you very much! :)
     
  24. Malware

    Malware Master Engineer

    Messages:
    9,867
    Glad I could help :)
     
  25. Morphik

    Morphik Apprentice Engineer

    Messages:
    186
    • Like Like x 1
  26. Sarithule

    Sarithule Trainee Engineer

    Messages:
    55
    O, I was wondering, is there a way to access an argument that is passed through a button to a PB. It seems to me that once the PB is dropped into the activation bar and the argument is entered that there is no way to edit it without re dropping and re entering the argument anew. It would be great to be able to check what arguments are passed to what PB run buttons. If there isn't then this would be a super slick feature. *nudge* *nudge* ;) ;)
     
  27. Malware

    Malware Master Engineer

    Messages:
    9,867
    Sorry, no. I agree it'd be a nice feature though. I did look into it originally but I can't be bothered to dig into the UI system to make such an alteration - not now at least. I'll keep it in mind, in the mean time perhaps someone else is willing to do it.
     
  28. spacecadet

    spacecadet Trainee Engineer

    Messages:
    89
    Is it just me or are small ship buttons failing to pass arguments, while large ship button panels succeed? I'm experiencing this with a new design in a single player, offline world.
     
  29. Malware

    Malware Master Engineer

    Messages:
    9,867
    That sounds strange as they are technically the same thing... I will test it and see if I can figure out what is happening. Thank you for the heads up.
     
  30. Malware

    Malware Master Engineer

    Messages:
    9,867
    I'm sorry, I can't seem to reproduce this. I placed a PB and a button on a small ship, simply made the PB echo the argument. When I clicked the button, the PB was run. Are you sure ownership is set up correctly?
     
Thread Status:
This last post in this thread was made more than 31 days old.