Basic usage
client/verb
onResize(elem as text)
set instant = 1, hidden = 1
var/size = winget(src,elem,"inner-size")
world.log << "[elem] resized to [size]"
The above code snippet defines a verb that takes an argument identifying an element in the user's interface, and then tells you its current size by querying the user's interface. The verb will pause execution and wait until the client replies with the size of the element, and then print that size to the log.
Let's tie this behavior to a window's "on size change" event.
Now when we run this, you'll see a message informing you of the new size of the window each time you resize it.
Intermediate Usage
There are a few problems with this approach. For starters, let's change the id of the window element to "main". If you were to run the project having just changed the window's id, the on size change event is still telling the server to check the size of "default".
Embedded wingets allow us to set up more responsive UIs. Let's change the on size change event, and make sure that if we change the name of the window, we're not breaking absolutely everything:
You can see here that we passed a new argument. Instead of explicitly typing out the name of the window, we put "[[id]]" in its place. This is an embedded winget. When the command fires, the embedded winget is processed on the client-side, and the value of the winget query is substituted for the embedded winget.
Let's run it. You can try changing the id of the window to whatever you want, and the embedded winget will keep the command working smoothly.
Going Further
Now that we've unlocked the power of embedded wingets, let's take them a little further. Let's start using them to streamline client/server communication.
You know how our onResize() command on the server has to wait for the client to trigger it, then wait again for the client to tell it how big the element that triggered it is? Let's communicate all of that the first time, and speed the whole process up. This will give us more responsive client->server actions.
client/verb
onResize(elem as text,size as text)
set instant = 1, hidden = 1
world.log << "[elem] resized to [size]"
And now our on-size-change event should look like this:
Now have you noticed something different about this? When you run this code, suddenly the "x" between the width and height are missing. That's because embedded wingets are trying to make your life easier. Size and position coordinates in DM are usually separated by an x or a comma. When winget() returns them, you need to break them down into their halves and then convert them from text to numbers:
//Before embedded wingets:
client/verb
onResize(elem as text)
set instant = 1, hidden = 1
var/size = winget(src,elem,"inner-size")
//process the size string as numbers:
var/list/l = splittext(size,"x")
var/width = text2num(l[1])
var/height = text2num(l[2])
world.log << "[elem] resized to [width]x[height]"
Embedded wingets are meant for command processing. This means you can define your verb arguments to do all of this conversion for you:
//After embedded wingets:
client/verb
onResize(elem as text, width as num, height as num)
set instant = 1, hidden = 1
world.log << "[elem] resized to [width]x[height]"
All position and size parameters allow you to select which component of you want, so you don't need to pass both x and y if you only need one:
[[inner-width.x]] [[inner-width.y]]
Advanced Usage
Embedded wingets allow you to offload a lot of work you would have to do on the server. Let's look at some use-cases for them.
Hidden Elements as Data Storage
You can use labels, buttons, and text inputs to store data on the client without having to keep track of it on the server at all. This is particularly useful for cloned commands. You could, for instance, create a system where interacting with a chest in your game clones a window and populates it with the contents of that chest. A hidden label in the cloned window might contain a "\ref" id for the container. Buttons on this cloned window could get the ref id for the object via an embedded winget and pass it to a verb when it is pressed: [[parent.hiddenlabel.text]]. Grabbing this refid and using it to locate() the specified object will allow you to work with the object.
You could also use this approach to store data between seamless transitions or reboots. Just be aware that object references do not persist between sessions or worlds.
Parent Navigation
You can use parent prior to an id can be used to get a sibling id within a UI element. So you can grab the text of a button within the same window via [[parent.otherbutton.text]].
JSON / Escaping
You can alter how the client escapes embedded data by adding "as" to the embed:
[[property as num]] / [[property as arg]] / [[property as text]].
The options follow:
arg escaped string params json json-dm
Client-side Macros
You can expand the information available to embedded wingets by using ui elements for data storage.
As an example, we're going to set up an advanced macro that keeps track of the keyboard state so that you don't have to keep that information on the server.
Let's set up our verbs:
client/verb
onKey(key as text, shift as num, caps_lock as num, scroll_lock as num, num_lock as num)
set instant = 1, hidden = 1
world.log << "+[key] [shift] ([caps_lock] [scroll_lock] [num_lock])"
onKeyUp(key as text, shift as num, caps_lock as num, scroll_lock as num, num_lock as num)
set instant = 1, hidden = 1
world.log << "-[key] [shift] ([caps_lock] [scroll_lock] [num_lock])"
Now let's set up a checkbox as a data storage for the shift key, and we're gonna call it "shiftbtn". We can make it invisible if we want, but you can leave it visible for testing.
Our macro setup is straightforward. We're going to have an Any macro, but we're also going to have a macro for SHIFT alone. The SHIFT macro will change the checkbox we just set up to store whether the shift key is being held down. This change occurs ONLY on the client-side, with no server notification whatsoever, so it's borderline instantaneous.
Finally, the Any macro is going to read this checkbox's value as one of its arguments, allowing you to determine whether the Shift key is being held down during any other keypress. As a bonus, I included the 515 keyboard mode special wingets for reference for how to read caps, num, and scroll lock states.
NONE+SHIFT .winset "main.shiftbtn.is-checked=1" NONE+SHIFT+UP .winset "main.shiftbtn.is-checked=0" Any onKey [[* as text]] [[main.shiftbtn.is-checked as num]] [[caps-lock as num]] [[scroll-lock as num]] [[num-lock as num]] Any+UP onKeyUp [[* as text]] [[main.shiftbtn.is-checked as num]] [[caps-lock as num]] [[scroll-lock as num]] [[num-lock as num]]
You can also use this to get modifier keys when pressing buttons, interacting with checkboxes, or calling verbs. from a panel or right click context menu. Neat, right?
You can store object references on the client in invisible controls, and pass them to verbs, which will attempt to validate the objects.
To pass an object to the client's interface for storage:
And to pass the object along with a verb, we'd use the "as raw" embedded winget:
testanything [[window.label.text as raw]]
Your clients ain't trustworthy.
Remember that your clients can't be trusted to give you data that is valid, so if you are just passing object handles, you need to do some validation.
I'm going to share a minimal object validation approach here that you can adapt to your needs. In a nutshell, what it does, is store a hash on any object that you want to validate on the client. This hash is stored on the datum that's been passed by reference to the client, and will be used as an addendum to the ref id of the object when the verb is called.
So instead of just waiting to receive an object reference, we're going to receive an object reference, and a hash string for each object we want to pass to the verb. The server will then validate that the object's server-side hash matches the client hash the client provided. Validation doesn't stop here though. You want to make sure that your players are eligible to interact with objects in the verb as well. So if they call a verb to punch a player, just knowing which player they want to punch isn't enough. You actually need to check the user's state to see if they are capable of punching that player, and the other player's state to know if they are capable of being punched by the initiating player.
We can now set up a system for passing objects to the client for temporary storage. The hashes serve two functions: When an object is deleted, its refid may be recycled. Because client_hash is stored on the datum the first time we set it, we're less likely to get an object that is just wearing the recycled nametag of our previous object we told the client about. The second function, is to make it harder for players to fuck with your verbs by passing invalid objects to them.
To store an object on the client:
To send the reference back to the server:
To validate and handle the object on the server: