Skip to content

Cover Buttons- Nexus Client

Over on the nexus-scripting thread on the discord server, Steve asked about modifying the room items tab to add additional functions to the prop items there to enable cover functions -- ie take cover, shoulderroll if you are Scoundrel, cover destruction and the like.  This is in fact possible, but it's beyond my abilities for the moment. I said that it would be easier to assign cover items to the bottom buttons. And then, because I was curious, I tried it out to see if I could make it work.  I'm still not done with it, but I've made a start, and I've learned a ton about how various things work. I thought I'd go through and go over it.  Fair warning- this is going to go on and on, and I'll have to break it up into several posts- hence making it it's own thread rather than hijacking Nexus Miniscripts or something.

So step one. Can I get a list of cover in the room I'm in?  Answer- kinda.  So when you enter a room, or look around you get a GMCP message.  That's Generic Mud Communication Protocol, and it's what the game uses to send data to clients in the background. You normally don't see it, but you can check a box in the Advanced Tab in the client settings to toggle them on. You can see a list of what IRE uses GMCP for here: https://nexus.ironrealms.com/GMCP

So there are three messages we want to look at to deal with cover items:

Char.Items.List

Char.Items.Add

Char.Items.Remove

The first lists all the items in the location, which can be a container, your inventory or the room.

The second two deal with items added or removed from a location. It turns out there are some problems with using these second two for our purposes, but we'll come back to that later. The first one looks like this:

[GMCP]:Char.Items.List { "location": "room", "items": [{ "id": "34512", "name": "a stoic Jin Marshal", "attrib": "mx" }, { "id": "51410", "name": "a quick-witted Elgan Marshal","attrib": "mx" }, { "id": "54851","name": "an intimidating Marshal in drab power armor","attrib": "mx" }, { "id": "75256","name": "a fiery Krona Marshal", "attrib": "mx" }, { "id": "33856", "name":"a humming snow machine" }, { "id": "60068","name": "a Poffy Tango bobblehead doll" }, {"id": "4017", "name": "a skull-bumpered Maddoxson hoverbike" }, { "id": "34564","name": "a large snowmound" }, { "id":"51843", "name": "a large snowmound" }, {"id": "44394", "name": "a largesnowmound" }, { "id": "31592", "name": "a u-shaped bank of controls" }, { "id": "38354","name": "a complicated control bank" }, { "id":"49328", "name": "a deployed turret with a mounted laser barrel", "attrib": "mx" }, { "id":"57689", "name": "a heavily modified Bushraki hoverbike" }, { "id": "58253", "name": "an information counter" } ] }

That's a lot of stuff! Let's look a little closer. So the GMCP method is Char.Items.List. The pieces of that are args (short for arguments) the name used for it is args.gmcp_args  Since it's a javascript object you use dot notation to get the pieces. So args.gmcp_args.location is "room". That's what we want, so that's what we are going to look for. The next piece is args.gmcp_args.items  You can see it's surrounded by square brackets [] which means it's an array - essentially a list of things accessed by number: item 0, item 1, etc. Inside that each item in the list is surrounded with curly brackets {}, which means it's an object. Objects, among other things, have named properties. So each item in the list has an id, name and in some cases an attrib.

(If you are already lost because you are unsure what arrays and object are, go here:  https://www.w3schools.com/js/js_arrays.asp , https://www.w3schools.com/js/js_objects.asp)

So, how do I capture that list?  Nexus provides the onGMCP function which will fire anytime a GMCP message is received by the client. In fact you can make as many onGMCP functions as you want and put them in different packages, and they will all trigger. Use caution that you know what each of them is doing if you chose to operate this way. For this, because I want to share, I made a shiny new "Cover" package and gave it it's very own onGMCP function, the first part of which looks like this:

if (args.gmcp_method == "Char.Items.List"){
    if (args.gmcp_args.location == "room"){
        poetic.cover = {};
        poetic.roomitems = args.gmcp_args.items;
        poetic.roomkeys = Object.keys(poetic.roomitems);
poetic.roomkeys.forEach(function(item){
//client.print("Room item "+poetic.roomitems[item].id+" "+poetic.roomitems[item].name);
    if (!poetic.roomitems[item].attrib){
trophy =poetic.roomitems[item].name.match(/a stuffed and preserved/);
        if (!trophy && !poetic.notcover.includes(poetic.roomitems[item].name)){
            roomitem = poetic.roomitems[item]
            poetic.cover[roomitem.id] = {id: roomitem.id, name: roomitem.name, button: 1};
            //client.print("Cover item"+Array.from(Object.values(poetic.cover[roomitem.id])));
}        //end item sorting
}        //end unattributed items
})                //end forEach
        if(keysets.activekeyset == "cover"){
            keysets.clearButtons();     
send_command("coverbuttons");    
}        //end coverkeys
}                //end location = room
}                //end Char.Items.List


So lets go through that. If the method is "Char.Items.List" do what's in the brackets. Otherwise do nothing.

On the second line we ask if args.gmcp_args is "room". Again, do what's inside the curly brackets, or do nothing.

The third line we create an empty object to hold the cover items. Poetic is the object that I'm using for my system. So we've made another object inside of that. The reason (one of them) to use objects like this is that if you define an object in a script somewhere, it's available in any script you want to use it in.

The fourth line we set a roomitems variable equal to the item list we get from GMCP. This is to make it a little more readable, and so we can use the room item list elsewhere if we want.

The fifth line gets the keys to the items in the object, so that we can iterate over them.

If we ask for args.gmcp_args.location we get "room". What happens when we ask for args.gmcp_args.items?  We get "objectObject" for each of the things in the list.  Hence the bit on the 6th line where it asks "forEach"  So for each item in the array, do this.  (I'll be honest, even though I know why it does it the objectObject response makes me crazy).

The next line is commented out.  // is a javacript comment. You can use it to clarify what you are doing (which I should have more of in there) or to keep code from running. In Nexus client.print displays text on the screen- in this case so I could see what I was getting for debugging purposes.

The following line is an if statement:  if (!poetic.roomitems[item].attrib){

Remember we mentioned above that some items have attributes(attrib)? As far as I know, none of the props have attributes. The ! At the beginning is a negative statement. It says, in effect, "if the item does not have an attribute, continue". 

Everybody still with me? I told you this was going to be long.

We'll gloss over the next lines a little. So we are sorting the items as they come in to see if they could potentially be cover. Things with attributes are out. Things that include "stuffed and preserved" are out. That gets us down to things that are pretty certain to be props. But not all props are cover. So I have an array of things that are "notcover". I don't have the whole list yet, cause I haven't tested every single thing. There are probably a few things that could be sorted out like the trophies. For now, when I get the "you can't cover behind that" message I grab the item name and add it to the array. After I try flipping it. Just for more fun- some things are only cover if you flip them. To make it extra confusing- some things you can flip, but they aren't cover no matter which position they are in. Some things are climbable, and I haven't done much with that at all yet, though I'm trying to build a list.

Once we have them sorted, and we are pretty sure they are cover, we add them to the cover object:

            roomitem = poetic.roomitems[item]
            poetic.cover[roomitem.id] = {id: roomitem.id, name: roomitem.name, button: 1};

We set a variable again, to make things a little easier to read. roomitem is the entry in the array we get from the forEach statement- poetic.roomitems[item] where item is a numbered entry in the array.

For each of those we make a new entry in poetic.cover with the key set to the item ID number roomitem.id   We use bracket notation there, because you cannot use a variable to access object properties via dot notation. poetic.roomitems.item will fail because it wants an entry literally named "item".

So each item that passes our test becomes poetic.cover[cover item ID#]  That way we can ask for them later by item ID number and not have to search or sort.  For each of those things we store an object that contains the id number, the name, and button 1. The button is just a placeholder, we'll set the button numbers in the next bit.

That's pretty much that. Now we have a mostly correct list of things that are cover. Next up- bend the buttons to our will!


[Cassandra]: Poet will be unsurprised to learn that she has unread news.

Comments

  • This makes me glad I'm on mudlet. 
  • Albion said:
    This makes me glad I'm on mudlet. 
    I personally prefer Nexus due to the visualization offered. It helps me stay grounded and up-to-date, especially in space.
    Name: Vundara
    Faction: Scatterhome
    Class: Scoundrel
    Race: Nusriza
  • Now that I'm back from my trip, I'll start working up the next post about how the buttons work. In the meantime, here's the link to download the package if you want to try it. It is definitely not done yet, and it may break unexpectedly.  I renamed the onLoad function, so it won't run on startup, in case you want to leave it off.  CSI is the alias to fire it up. Feel free to DM me with bugs and suggestions.

    https://drive.google.com/file/d/1zdV9besWXJMPxOVRP4nT3gULntvwKL8j/view?usp=sharing
    [Cassandra]: Poet will be unsurprised to learn that she has unread news.
  • edited February 2020

    In the first post we talked about how to get the items from the room and sort them down to things you can actually take cover behind. This post we are going to talk buttons.

    The buttons run across the bottom of the screen and are tied to the F-keys. You can, of course, click on them with the mouse as well.  I normally don't, because once you change them in a script, they stop respecting the "Show tooltips" check box and insist on showing a help popup over the main window when the mouse is over the button. If and when I get around to adding right click functions to the buttons, I'll see if I can find a way to turn that off.

    I did not mention above, but you probably want to set the number of buttons to 10. The system will work if you use 12, but if it puts anything in those buttons, it won't clear it out again. If you are like me and you find the buttons a little too small with 10 up at once, try turning the avatar pic off.  You can go with fewer buttons, but remember that you want a button for each cover item.

    Ok, so. Here are the button functions from Nexus:

    buttons_set_label(id, text) - Set the text label on a button.
    buttons_set_commands(id, cmds) - Sets the command sent to the game when the button is pressed.
    buttons_set_highlight(id, on_off) - Set whether the button is highlighted or not.

    For all of those the id is the button number 1, 2, etc. as an integer. The text label is a string.

    If you modify the buttons in the Nexus settings window, you can attach a script to the buttons, but there does not appear to be any way to do so via scripting. So we can have the button send a command, but not execute JavaScript directly.

    The last function there is to set the highlight on the button on or off, again you send the button number, and then true or false. True turns the highlight on, false turns it off.  The default highlight is a shade of gray.

    So gray highlights on black buttons is massively underwhelming and at this point I fell down a rabbit hole…

    We had a discussion earlier on the nexus-scripting channel about modifying the highlights and found that the buttons each have a Cascading Style Sheet(CSS) style and an id. So you can target them with JS and change the background color.  Unfortunately the button CSS is also tied to page events, so when the page redraws, the default highlight comes right back. In order to fix that you have to redefine the CSS for the button highlights. I'm not going to go through the whole process, but I spent two days on it and then went and asked on Stackoverflow where someone with more wizardry than I explained how to do it and make it stick.  This is how you do it:

    //below overwrites the CSS sheet to change the button highlight color
    function overwriteStyles(styles) {
      var styleOverwrites = document.getElementById('style-overwrites');
      if (styleOverwrites === null) {
        styleOverwrites = document.createElement('style');
        styleOverwrites.id = 'style-overwrites';
        document.head.appendChild(styleOverwrites);
      }
      styleOverwrites.innerHTML = styles;
    // Save styles to local storage
      localStorage.setItem('overridden-styles', styles);
    }
    // Here styles are grabbed form local storage and loaded for persistence
    document.addEventListener("DOMContentLoaded", function(event) {
      var styles = localStorage.getItem('overriden-styles');
      console.log(styles);
      if(styles != null)
      {
       overwriteStyles(styles);
      }
    });
    // Example of overwriting styles
    overwriteStyles(`
      #bottom_buttons > div.bottom_button.highlighted {
        background-color: #e67300;
      }
    `);

    Yeesh. That was way more work than it should have been. Maybe when they update Nexus they could (pretty please?) give us a simple way to set a custom highlight color- or better yet more than one. It'd be nice to be able to recolor the individual buttons based on their status.  This only works for all the buttons at once.

    In any case, I picked a nice orangey color for the new highlights and they are way better. You could change #e67300 at the end of that there to another color code if you wanted something else.

    So in the last bit of code in the first post there's an if statement that asks

     if(keysets.activekeyset == "cover"){

    I have a keyset object that manages remapping the keys, primarily for spaceflight and hacking. It will change the buttons around based on my status. I pulled the core of it out and stuck it in this package so the cover keys will work. You can use the KEYMAP command to switch to something else, but you'll probably want to edit keySystemInit so that the buttons contain commands that will work for you.  If the buttons get switched around for some reason- KEYMAP COVER should bring them back.

    So it asks if we are using the cover buttons, and then if so it runs a function to clear all the button entries- makes them all blank and unassigned, and then calls the 'coverbuttons' command. I made this as an alias with a script attached, you could make it a function and it would work exactly the same.

    coverkeys = Object.keys(poetic.cover);
    //client.print("Cover Keys: "+coverkeys);
    x=1;
    keysets.clearHighlights();
     
    coverkeys.forEach(function(item){
        //client.print("Item: "+item);
        poetic.cover[item].button = x;
        //client.print(poetic.cover[item].name+" assigned to button "+poetic.cover[item].button);
        buttons_set_label(x, poetic.cover[item].name);
        buttons_set_commands(x, "coverButtonPress "+x+" "+poetic.cover[item].id);
        if (poetic.coveritem == item){
            buttons_set_highlight(x, true);
        }
        x++;
    })

    This function builds and assigns the buttons. It gets the keys to the items in poetic.cover which we set from the GMCP message when we entered the room and sets the variable x to 1.  It clears all the highlighted buttons and then 'forEach' item it once again runs through each entry. The first thing it does is replace the placeholder button number with the button each item is assigned to and then it sets the button label for that button to the item name. Then it sets the command assigned to the button to be 'coverButtonPress x id#'  So when you press button one, it calls the coverButtonPress function and sends it 1 so we know what button to operate on, and the system ID of the cover item we are interacting with.

    It checks at the end there to see if we are already in cover behind one of the items in the room so that it highlights correctly.

    I'll go over what happens when you press the buttons next time.


    [Cassandra]: Poet will be unsurprised to learn that she has unread news.
  • I'm keeping up with this like it's a great scientific expedition into the unknown. I can't wait for the next issue! 
  • If you all are curious what Nexus can look like when you hack the crap out of it- here's a demo video. Includes some PvE, the map tab I built for caches, cover buttons and starflight tab.


    [Cassandra]: Poet will be unsurprised to learn that she has unread news.
  • Yo! That's dope. I would totally go nexus if this was standard stuff and I had no access to a computer.
    (Scatterhome): You say, "Do you like things up your ass? Is your record clean? Are you looking for a job in the near future but not right now? Smuggle drugs for Solus and get stuffed across the galaxy."

  • Last time around we got the buttons built and the highlights rewritten. So now what happens when you click the button?

    So, there's only one cover item in the room I'm in, but button one is set to send this as its command: coverButtonPress 1 53482

    The coverButtonPress function looks like this:

    button = args[0];
    itemID = args[1];
     
    if (!poetic.incover){
        send_command("take cover "+itemID);
        poetic.coveritem = itemID;
        poetic.covername = poetic.cover[itemID].name;
    }else if(poetic.coveritem == itemID){
        send_command("leave cover");
        buttons_set_highlight(button, false);
    }else{
    if (poetic.characterclass == "Scoundrel"){
        send_command("guile shoulderroll "+itemID+" ");
        poetic.rollto = itemID;
        }else{
            display_notice("You are already in cover!");
        }
    }

    That takes our button press and receives 1 as args[0] and 53482 as args[1]. The button is set to 1 and the itemID as 53482.  It then checks to see if we are already in cover- if (!poetic.incover){  If we are not, it sends the command to take cover behind item 53482.  We set the variable for our cover item to 53482 and the covername to the name associated with 54382 in the cover object.  Initially I had it set the button highlight for button 1 as well, but if you fail to take cover- like if the item is not a cover item, the button still highlights, and that's no good. So now it waits for the line where you actually take cover. 

    If, instead, we are in cover it checks first to see if the button is assigned to the item we are hiding behind, and if so, it sends "leave cover", and removes the highlight from that button. If it's possible to fail to leave cover(??) I should move that somewhere else. 

    If the button is assigned to another cover item, it checks to see if the character is a Scoundrel, via a variable we set in the init function. Nexus has a default "my_class" variable that contains your character class, so you can use get_variable(); to get it, like so- poetic.characterclass=get_variable("my_class");  Scoundrels can move between cover with shoulderroll, so if you click on a different cover item, it will send shoulderroll and the item ID to move to, as well as a rollto variable so we can use the shoulderroll success trigger to swap the highlights.

    For everyone else, it just tells you that you are already in cover and does nothing.

    The trigger for taking cover is a regular expression and looks like this: ^(?:You vault over (.*) and take cover behind it\.|You take cover behind (.*)\.|You dive behind (.*)\.)$  There are three messages you may get when you take cover, and this will trigger on any one of them, and then capture the name of what we hide behind. There's no good way to check if it's the right item, since we don't get an ID, but I check to see if the name matches. This is the code tied to the trigger:

    poetic.incover = true;
     
    if (args[1]){
        cover=args[1];
    }else if (args[2]){
        cover=args[2];
    }else if (args[3]){
        cover=args[3];
    }
     
    if (poetic.cover[poetic.coveritem] != undefined){
        if  (cover == poetic.cover[poetic.coveritem].name){
        keysets.clearHighlights();
        buttons_set_highlight(poetic.cover[poetic.coveritem].button, true);
        }
    }

    We set a flag to show we are in cover, and check each of the lines to see which one will give us the name of what we hid behind. We check to see if that’s in our cover object and if the name matches what we meant to hide behind. --I'm not sure if, or how, you'd end up trying to take cover behind something else, so there isn't any actual error handling here yet--  If so it clears any existing highlights and sets a new one based on the button tied to our item ID number.

    Shoulder roll I didn't bother with error checking at all yet, it just swaps the highlights. We did have a bit of confusion with Steve's highlights not moving correctly, while my test alt worked just fine. The reason? Brief messages. So the trigger was looking for the 'fluff' line for shoulder roll and missing the brief message. Once I updated the trigger regex, it works just fine either way.
    ^(?:With supreme agility, you execute a perfect rolling maneuver, emerging from behind .* and diving straight behind (.*)\.$|^You\: Guile Cover \(.*\)\.$)

    That's pretty much the whole thing. Click the button, take cover, get a pretty highlight. 

    I will note that I have really noted Nexus having problems saving things properly during my work on this. Like just a second ago I went to copy the regex for shoulder roll, that I fixed to include the brief combat message, and found that it had reverted back to the one without that in it. Also despite the fact that the 'notcover' array is exactly the same in the init function and in the temp function that I use to update it- Nexus will routinely load an old copy of the array from I don't even know where. Page cache maybe? I'll have to run the notcover function and update the array to have it properly remove items that should already have been in the list.

    Also, there can be performance problems. I note that I move room to room with a bit of lag with the buttons on, since it has to process all the items every time I move. This is also the problem with the Char.Items.Add and Char.Items.Remove GMCP messages. If someone dies and drops 300 junk, the system would try to check each and every one of those things to see if it was a new cover item (IE from a scoundrel using Improvised Cover). This would hang everything up for several seconds. I stuck in a time delay as a quick fix, so once it gets one new item, it stops checking for a little bit. GMCP also seems to send Add-Remove-Add repeatedly for the same item, which doesn't help at all.

    Anything that loops is a problem in Nexus because everything runs in the same thread. There are some ways around it, but I'm not that sophisticated yet.

    In any case, I'll go over adding and removing things in the next post.


    [Cassandra]: Poet will be unsurprised to learn that she has unread news.
  • This should be the last post on this topic, unless someone has questions.

    So the only bit left is things added and removed from the room. Scoundrels can whip up cover from nowhere with Improvised Cover and various classes can destroy it. If the cover in the room changes, we want the buttons to reflect that.

    We get the add/remove from GMCP as seen below.

    //[GMCP]: Char.Items.Add { "location": "room", "item": { "id": "40697", "name": "a low wall" } }
    if (args.gmcp_method == "Char.Items.Add"){
         send_command("roomItemAdd");
    }
     
    //[GMCP]: Char.Items.Remove { "location": "room", "item": { "id": "69950", "name": "a ticket counter" } }
    if (args.gmcp_method == "Char.Items.Remove"){
            if (args.gmcp_args.location == "room"){
                if (Object.keys(poetic.cover).includes(args.gmcp_args.item.id)){
                                client.print("Item removed from room "+args.gmcp_args.item.id);
                                x= poetic.cover[args.gmcp_args.item.id].button;
                                buttons_set_label(x, "");
                                buttons_set_commands(x, "");
                                buttons_set_highlight(x, false);
                                delete poetic.cover[args.gmcp_args.item.id];        //remove the cover item
                }        //end includes
        }        //end room item remove  
    }        //end Char.Items.Remove

    You will note that when an item is added, we call a function and when one is removed, we just have some code. I should really make those match.  I noted in the last post that a sudden influx of items, generally from someone dying, would hang everything up. So the command that calls the function is how we get around that.  So when something is added, it runs the roomItemAdd function:

    if (typeof timeout === "undefined"){
        timeout = false;
    }
     
    if (!timeout){
              timeout = true;
              send_GMCP("Char.Items.Room","");
              window.setTimeout(dontRun, 50);
             //client.print("Checking room items");
    }
     
    function dontRun(){
         timeout=false;
        //client.print("Restoring room item check");
    }

    The first bit sets a timeout flag to false if it doesn't exist. We check if we are timed out before we do anything else. Then we send a GMCP request. send_GMCP will let you ask the game to return GMCP data, in this case the list of items in the room. This will trigger our original GMCP code and rebuild all the buttons from scratch.  The timeout = true will prevent us trying to do it again for 50 milliseconds after which the dontRun function will return and set it back to false, which seems to be long enough to ignore people dropping junk.

    For the remove item, we check to see if the ID of the item removed matches one of our cover objects- 

    if (Object.keys(poetic.cover).includes(args.gmcp_args.item.id)){
      And if so it clears the button label, command and highlight if present. This leaves us with a blank button among the rest. (It might be better to do the same as the add item, and rebuild them all, though that could potentially leave you hiding behind something that's now assigned to a button other than what you used to take cover).  It also removes the item from our cover object-
    delete poetic.cover[args.gmcp_args.item.id];
    Since the cover items are keyed by their ID numbers we can just call delete on the ID to remove the entry from our object.

    Thanks for sticking with me through all that. I'll post a link to updated code as I make additional changes.

    [Cassandra]: Poet will be unsurprised to learn that she has unread news.
  • Just as a note- having experienced a couple of people, including myself, die during the Xenohunt. The timeout is not effective in stopping the client from locking up when someone drops a lot of junk. I'll need to revisit that. My current thought is to just ignore GMCP room add/remove entirely and only update from combat messages or manually. (ie send look).
    [Cassandra]: Poet will be unsurprised to learn that she has unread news.
Sign In or Register to comment.