ID:2910287
 
Winget() allows you to get information from the client about the state of Dream Seeker's interface.

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?
Brand Spankin' New in BYOND 515.1631

You can store object references on the client in invisible controls, and pass them to verbs, which will attempt to validate the objects.

client/verb
testobj(someref as obj)
world.log << "[someref:name] is an obj!"

testmob(someref as mob)
world.log << "[someref:name] is a mob!"

testturf(someref as turf)
world.log << "[someref:name] is a turf!"

testarea(someref as area)
world.log << "[someref:name] is an area!"

testanything(someref as anything)
world.log << "[someref:type] is... I dunno, but it's cool!"


To pass an object to the client's interface for storage:

winset(usr,"window.label","text=\ref[someobject]")


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.

#define HASH_ID_ROLLOVER 16777216
#define DATUM_HASH_SALT "Baby horse doo doo do doo do do" //you really want a better salt
var/hash_id = 0

datum/var/tmp/client_hash = null

//used to give a datum a unique client hash
proc/client_hash(datum/ref)
//return the datum's client_hash value if it already exists, or create a new one if it does not.

return "\ref[datum] [ref.client_hash ||= datum_hash(ref)]"

//creates a unique client hash
proc/datum_hash(datum/ref)
//manage the global hash counter
var/hash_id = global.hash_id
if(++global.hash_id >= HASHID_ROLLOVER) //this prevents hash id stalling when we exceed the max number of integral ids
global.hash_id = 0

/* hashes are composed of the following elements:
datum reference - uniquely identifies the object id
time it was hashed - attempts to reduce collisions from deleted and rehashed objects
id of the hash - attempts to further reduce collisions from deleted and rehashed objects
salt - a secret value that is added to hashes to make them harder for exploits to manipulate

note: If you control your server, a salt will only need to be changed if it is exposed
If you do not control your server, adding a random value to the salt at world startup may
be wise to make DMB string table inspection harder and to ensure that hashes do not persist between server sessions
*/


return md5("\ref[datum] [world.time] [hash_id] [DATUM_HASH_SALT]")

//used in verb arguments to help you receive hashed objects from client-side commands
#define hashed_datum(varname) anything, ##varname##_hash as text

//used in verb bodies to help you validate hashed objects passed to the verb by the client
#define valid_datum(varname) (##varname##_hash && ##varname:client_hash==##varname##_hash)


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:
winset(usr,"main.storagelbl",list("text"=client_hash(storingobj)))


To send the reference back to the server:

doSomething [[main.storagelbl.text as raw]]


To validate and handle the object on the server:
client/verb
doSomething(storedobj as hashed_datum(storedobj))
set hidden = 1, set instant = 1
if(!valid_datum(storedobj))
world.log << "Invalid object!"
return
world.log << "Valid object!"
//continue verb validation and function here.