The byolib BYOND/Javascript library

Member for

5 years 10 months
Submitted by SuperSayu on Sat, 05/02/2020 - 10:10

Okay nerds strap in this is stupid.

So the BYOND language that I first picked up while playing around with Space Station 13 is fun but it has some glaring flaws, not the least of which is, effectively, no debugger. You can't easily examine variables at runtime, or edit them, etc. By the time I came into it, SS13 had a fantastic debugger already in the code that was built on the browser backend that BYOND ... well honestly, only kind of barely supports. You can design a webpage server side and push it out to people, and you can send them scripts and images and stuff, but you are using a crippled-old version of Internet Explorer for the sake of compatibility and it's... not great.

During and after my time contributing to SS13 (I am not anymore) they were moving to a different browser library or three each of which worked differently. It worked, and it produced nice stuff, I guess, but it really bothered me that it simply wasn't html. For "nanoUI" for example, a template might look like this:

<span>{{=data.angle}}° ({{=data.direction}})</span>

The parser would come through as it does with something like PHP and rip through the whole document turning {{}} blocks into something else. Makes sense, right? I hate it. So on my own, privately, just to get the frustration out I started working on different versions of a library that would do the same thing but with actual HTML.

That was held back, of course, by the fact--again--that the browser is a crippled useless older version of internet explorer.

You see, HTML has a thing that it does in fact permit, called namespaces. You want to create your own custom tags? Great. Just assign them to a custom namespace and the browser won't treat them like any existing tag. You can select all tags in your namespace, or differentiate between tags with the same name by their namespace. So when I discovered I couldn't just create <if> tags, I switched to <byo:if> tags. And even in this crippled browser, that worked. You can style byo:if tags in css and select them in javascript... but there are no builtin functions to help with that. Just like, and this is true, there is no builtin JSON parser in the browser--and JSON is a web standard and has been for a long time. There is now a json parser in the BYOND engine, but still not in the browser, so... yeah.

Anyway. Those are things I can get around by adding my own functions, and pretty soon I had designed a system where you could pass information back and forth between the browser and BYOND through the functions that BYOND itself exposes--basically, links can call BYOND functions and byond can call javascript functions. That little bit of wiggle room was all I needed to create a system based on a (more or less) standards compliant HTML template and a library written in BYOND to take advantage of it.

My template looked like this rather than the above:

<byo:var text_var="type" title_var="parent_type" title_prepend="Parent: " />

Ugly? Hard to read? I accept your criticisms. It's HTML, and it works like HTML. This tag fills itself with the contents of the variable "type", and has a hover-text title that says "Parent: [parent_type]". There are eight byotags so far that handle a variety of parameters, but the centerpiece is that I have native support for lists through the byo:for tag:

<byo:for mask_id="filter" mask_index=0 data_var="vars" template='<div class="varline"><byo:var text_implicit text_index=0 title_implicit title_index=1 class="varn" act="inspect_var"/><div class="editlinks"><byo:link icon="edit.png" act="edit" /><byo:link icon="revert.png" act="revert" /></div><byo:var text_implicit text_index=2 title_implicit title_index=3 class="val" act="inspect_val" /></div>' ></byo:for>

Now I admit, the proper way to do this would be to have a template tag either inside or somewhere on the page (identified by id) rather than having this ugly kludge of html inside of an attribute, and I'll probably do that someday. As much as I've gotten the library usable, it's by no means done. Since I started using it for real in a project (the afore-posted MC:B) I've already had to change things and fix bugs that didn't come up in the test piece. So yes, I admit it's kind of an ugly hack.

But let's talk about what it does. This is the central element in my debugger page, and it lists all the name:value pairs that you pass it. It gives you links to examine elements in a list (both sides, for associative lists) and some additional functions that are, I admit, incomplete. (The edit link doesn't work right now, although the revert does.) But let's look at it a piece at a time.

First and foremost, the data that gets passed to this tag is a list of lists, and each line looks like this: [index, indextype, value, valuetype], every value there being a string. When the template is evaluated, any value marked "implicit" is taken from the current entry, and you use the indexes to select sub-elements in the list. In other words,

<byo:var text_implicit text_index=0 title_implicit title_index=1 class="varn" act="inspect_var"/>

Means, "Take index 0 from the current list and fill the contents of the tag with that. Take index 1 from the current list and have that be the title (hover text)." Note that just because you are inside of a for loop doesn't mean that you can't get the rest of the data; I don't in this example because I don't need to, but you could just as easily have every list entry have a class, hovertext, link, action, etc, that is determined by a global variable rather than its current context, which is the internal word I use to describe the parent element whose data we are suckling to get implicit values.

And let's be clear about one thing at this juncture: although the javascript does not literally rebuild the page every single update, if data was sent last update and is missing from this update, it goes away. So if you remove an entry from the list, it is gone, automatically. There is no need to explicitly go around removing things.

Meanwhile, the act variables determine what happens when you click on something. Behind the scenes, this creates a standard anchor link tag that is used to send a message to byond; in the case of this debugger, it sends the link straight to that debugger, but for other variants of the browser library, it sends the message to the Topic() proc of the atom whose page you are browsing. You have to specify the byond ID of the element, of course, but the library takes care of that when the page is first loaded. The upshot is that if you click on a variable in the tag above, it will attempt to examine that specific variable or list entry, as determined by some standard ID variables that are automatically put into the link when used inside a byo:for. And yes--act can be determined by a variable, as can the arguments list, and they can be determined implicitly by the context just like the text and title.

The system is more flexible than it seems. Here is one test case from when I was creating the library:

<byo:for autohide=1 child_type="option" inner_id="sel" data_var="rows" template='<byo:var text_implicit text_index=0 act="remove" />'>
<select onchange="ByoSelect();" id="sel"><option>---</option></select>
</byo:for>

The additions here do pretty much what they appear to do. The autohide attribute means that if the list is empty, the contents of the entire byo:for tag are hidden--there is no dropdown box at all. The byo:for looks for the tag with the id of "sel" and puts the data inside of that, rather than just dumping it inside itself. Each copy of the template is put inside an <option> tag rather than a <span>; I mean, I could just as easily have wrapped the template in an additional tag, but this is a handy helper. The content inside the byo:for is otherwise untouched; the select operates normally. The only thing that is not terribly straightforward is the "ByoSelect()" addition to the javascript, which is necessary because clicking on an option in a dropdown box doesn't register as a click for the purposes of links. The ByoSelect() function emulates clicking on something as soon as it is selected, meaning that the "remove" act is activated.

While speaking of things that do exactly what they said on the cover, let's consider the <byo:if> that I already mentioned at the beginning.

Given the way things work, if you just want to show or hide one specific variable, you already have that in the functionality of the byo:var tag; pass it an empty string and nothing appears there. But it is equally important to show or hide large blocks of the page, and that's what the byo:if tag does. There are three variants: if true, if false, and if match. If true shows only when something is defined and logically true; if false triggers if the variable is undefined or logically false. If match shows only if the value matches a sentry value you specify in an attribute. For brevity's sake, I also created an if-variant called <byo:screen> which is if-match with a default data variable name of "screen"--in other words, you can create a single html page that contains several screens worth of content, and select between them with the screen var.

It all looks about as you would expect, given the rest of the library:

<byo:if class="contblock" true_var="contblock"><center><byo:var text_var="cont_n" text_append=" items in contents." title="View Contents" act="inspect_val" oid="contents" /></center></byo:if>

This section is on the debugger page, always--but only atoms can have a contents list, and some atoms don't use it. Many important entries that I need to debug don't have or need them, such as controllers, GUI elements, and similar. My debugger has flags so you can turn off this informational block if you want, so if you make no use of the contents list at all for a specific type, you can pretend it isn't even there. (I also filter out a lot of common variables to make the interface cleaner, although you can turn that back on; similarly, position and icon variables are set aside and dealt with in dedicated blocks)

And screens? That's as simple as <byo:screen match="list">. I love it.

I am less in love with the way I am currently using the <byo:else> tag. Don't get me wrong--it's good. You can put an else tag immediately after a byo:for or a byo:if tag, and the else will activate if the for list is empty or the if/screen tag is false--even, if one were really mad, a byo:var tag, in which case it will trigger if that tag receives no data. (Ideally a for or var will be on auto-hide, obviously, for best visual effect). That means that if you normally display a list and that list is empty, you can have a graceful "Nothing here" message instead of a blank box.

But... once you start actually using the library you want else to be a catchall, not just looking at the most recent entry. If you have three screen tags in a row, you would hope that an else tag that follows the three of them means "If none of the above"... but it only means "if not this last thing", which is a disappointment. I will probably get to that eventually, but it's not there yet.

Those are the highlights of the library, but there are three tags I haven't really discussed--link, icon, and meta. Link and icon are just a fancy tag names for things the library already does; you have already seen me put an "act" on a byo:var, and if you look closely at the list earlier, you'll see a link tag with an "icon" attribute. Internally, the two of them are little different from byo:vars. In contrast, the byo:meta tag is just a placeholder for some special things like the ability to set the page title, and some page debug options.

It is worth mentioning, though, that the icon attribute will place an image tag with the specified filename as the first child of the element--if you combine it with text variables, it will remain there even when the text changes or is removed. The intention was, more or less, that if you wanted to represent an in-game object in the browser, it would help to display an icon immediately next to it... but it's also a handy way to make an image-based link with only one tag instead of the two that html normally requires (a and img), which is how i used it above.

I suppose I would be remiss if I didn't share part of the code from BYOND's side, as well. It's straightforward:

/obj/structure/fort/tower/central
	BrowserUpdate(var/browser/B, var/mob/player/wizard/W,var/initial)
		if(get_dist(W,src) > 3)
			B.close()
			return
		if(initial)
			B.data["screen"]="main"
		if(B && B.viewer)
			switch(B.data["screen"])
				if("main")
					B.data = list("screen"="main", "player"=owner.master.name, 
						"mana"=owner.mana, "max_mana"=owner.max_mana,
						"mana_req"=owner.mana_req)
				if("spells") // not actually done
					B.data = list("screen"="spells")
			B.update()

	Interact(var/mob/player/wizard/W)
		W << "You connect your mind to the [src]."
		Stage.GetBrowser(src,W,'spelltower.html')

And that's one fully functional page, from the browser's side. The core of it, of course, is the one "data" var of the browser, which is converted to JSON and sent over to the browser every update cycle. The browser internals on byond's side are not significantly more complex:

/browser
	(...)
	proc/getData(var/initial)
		return target.BrowserUpdate(src,viewer,initial)
	proc/show(var/mob/M)
		if(viewer)
			if(viewer != M)
				M << "[viewer] is already working with the [target]."
			else
				M << "You're already looking at the [target]."
			return
		viewer = M
		for(var/r in resources)
			viewer << browse_rsc(r)
		viewer << browse(template,"window=[wind];[display_opts]")
		spawn(2)
			viewer << output("\ref[src]","[wind].browser:byolib_setup") // sets up links
			viewer << output(null,"[wind].browser:initialize") // sets up the document
			winset(viewer,wind,"on-close=\".winclosed \ref[src]\"")
			getData(1)
			Stage.browsers |= src

	proc/update()
		viewer << output(json_encode(data),"[wind].browser:update")

	Topic(var/href,var/list/href_list)
		target.Topic(href,href_list)

And that's it, except a few internal vars... and a ton of javascript. Obviously the debugger and any other complicated application have much more code behind them, but the library itself is not complicated. The debugger itself, of course, is mostly an exercise in turning variables into text, properly identifying which variable you are talking about when you receive a request, filtering large lists, etc... it's a lot of specific cases and not a lot that's interesting.

The javascript portion... is more interesting, but convoluted and hard to summarize easily. I look at the attributes and try to create a list that describes how to navigate the update packet, assuming that it is formatted as expected. Then, because javascript is just that kind of programming language, I create an update function (or rather, a list of them) and attach it to each byo:tag. That update function unwinds the list of specifiers to get exactly the requested data--it doesn't have to reparse the attributes, it just churns through the list that was set up when the element was initialized. The only time you have to re-evaluate things is with something like the byo:for, which creates new tags whenever it receives new data. While, ideally, I would find some way to minimize that... in truth I am not running up against a performance limit, and have put it off. If nothing else, that's all client-side work; if there is anything that's scary performance-wise, it's on the server side.

Anyway, that's it. I know nobody asked for this description of a project, and I doubt anyone is going to really seriously care about it--the library is kind of fragile and doesn't handle errors well, in addition to having some edge cases it can't handle--but I'm happy with it and if I could be of any use to people with it, well, that'd make me happy. In the meantime, it's just a project I've worked on, off and on, for the last few years. But it's good enough that even though I started the debugger project with no byondcode written for MC:B, I had a working display in hours. It won't be finished until I come up with a good way to edit things, but... still, I am happy with how straightforward it is.

At least, straightforward to me. I wrote it, though, so maybe I'm just mad.