First, I want to say thank you, whoever you are, for reading my writeup.
If you're from hackmud, then I appreciate you taking the time to read this, even if you don't end up running the script.
If you're from my Discord, I know this isn't the usual thing I write, but I hope you find it entertaining anyways.
If you're a family or a friend, then you've probably heard me yap about this a lot, but hopefully this is a lot more interesting written out.
And if you're some stranger who happened to stumble upon this randomly, well, I hope you're not too lost. I'll try to make sure everyone's on the same page before we dive too deep.
Hackmud (according to the Steam page) is
[...] is a retro cyberpunk RPG where you play a digital persona in a chaotic, unpredictable, and often goofy abandoned internet. Enter a 90s hacker movie filled with scripts, scams, and sabotage. Explore the world of digital secrets and forge your legacy through cunning, creativity, or disruption.
This insinuates that it plays something along the lines of a a multiplayer Hacknet.
In reality, it's more like an interactive fiction with obscure, science fantasy lore contained in multiple stacked puzzle boxes, wrapped in a multiplayer sandbox Javascript environment. You, and all the other players are thrown into this world, and your only goal is to play in whatever manner interests you, specifically.
Some users try to piece together the deep, labyrinthian lore into something semi coherent. Some users lean into the role of "big bad" so much that entire game events are created just to take you down. Some like to create fun scripts.
And some like to create tools.
It's the language of choice for the game. Even if you've never heard of Javascript, you've used it every day of your life. It underpins the functionality of effectively every website on the internet.
ComCODE teaches technical and economic literacy through open-ended gameplay. By immersing players in interactive digital ecosystems, ComCODE transforms the gaming experience into an active learning journey, producing real-world skills.
If you're making a game with the intent to teach someone a useful language, then choosing one of the most widely used languages in the world is a good choice. It's so popular, in fact, that it's reach has extended to servers, desktop applications, and even (ironically, and to the dismay of everyone trying to explain the difference between the two languages) inside Java itself.
However.
I do not like Javascript.
This isn't a particularly hot take; I'm not the only one who has this sentiment. Lots of people think Javascript is dumb. [1] [2] [3] [4] [5] [6] [7] But hackmud doesn't offer alternatives; if I wanted to write a script — snippets of code that would allow me to interface in the game world — my only choice is Javascript.
Sort of.
While Javascript is an incredibly popular language, it's not the only coding language out there. Rust, Golang, C++, Java, Haskell, Swift, and Zig, are just a few of the languages someone might code in. And a bunch of nerds very smart people realized that it could be pretty useful if you could take all of the stuff people have written in other languages, and just sort of... jam it on to the browser, somehow.
Enter WebAssembly.
Web Assembly (or WASM for short (or Web Assembly Sembly Membly for long)) is a "compile target", which just means "where you want to run something". When you wrote a language in C++, if you wanted it to run on Windows, your target for compilation would be Windows. WebAssembly lets you target web browsers specifically, which is a thing most computers have.
Lots of cool projects have been written in various compiled languages, including other languages. Javascript (or, at least the engine it runs on) was written in a language. So, theoretically, if you were a crazy insane person with an incredible rack and who played a hacking game but wasn't really vibing with the language of choice, you could use Javascript + WebAssembly to port a C project that was a whole different language.
Theoretically.
My language of choice is lua. It's a
[...] powerful, efficient, lightweight, embeddable scripting language. It supports several programming styles: procedural, object-oriented, functional, data-driven, and data description.
What this really means is a 12 year old kid looking to make video games would latch on to this incredibly simple language, and continue to use it sporadically until their 20's, at which point they'd stubbornly attempt to use it in every scenario that involved coding, whether it made sense or not.
Coding a Minecraft mod? Let's see if we can use lua. Writing a desktop application? There's probably a library that let's us use lua. Playing a hacking game and not really vibing with the language the developers intended for you to use? Fuck it, let's slam lua into that bitch.
And that's what I did!
About a year ago.
You might be wondering, "Why do a writeup then? Especially now, nearly a full year after you got it working?" Well... I didn't. Not really.
You see, I've touched C code — the language that lua is written in — only a handful of times, and honestly? I've hated it every time. Every tutorial assumes you're using 18 quintillion programs I don't have installed, or fail to mention that you need to re-polarize the flare reducer on their helium plasma flange, because obviously anyone writing in C already knows to do this one, obvious simple thing.
So my process of porting lua involved me haphazardly throwing things at the wall until they'd stick, and then writing as much Javascript and lua code as I could to circumvent all the weird and nasty bugs I created.
But, in some interpretations, it functioned. Enough so that I was able to write 350,000 characters worth of code across over 80 scripts. For comparison, in game, up until quite recently, you started out with the ability to write a single script that had 500 characters. You did, of course, have the ability to gain more character by equipping upgrades, but there was always a limit.
With lua, I had no limits.
A lot of my code was spaghetti. Not only was I fighting the character limits in game, but I was also fighting my lack of knowledge of both WASM, and C. As well, the version of Javascript that hackmud uses is stripped down, to prevent sandbox breaks; a lot of the information I'd find online simply wasn't applicable because the environment I was running in didn't have those options.
But I was able to work around a lot of the bugs I ran into, which was a good thing, because I really, really didn't want to dive back into the C. In a perfect world, I'd write all my scripts entirely in lua, and forget that there was anything underneath it.
Unfortunately, there was one bug I kept running into. One that was incredibly difficult to nail down, and would pop up at the most inconvenient times in the most inconvenient places. It'd happen once, destroy my database, then not happen again for a week.
If I were willing to have my output absolutely flooded with messages every single time I ran something, I might've been able to track it down. But unfortunately, I really didn't wanna. So I didn't. And then, I stopped playing hackmud.
Beyond the fact I struggle to stay focused on any one particular game for a long period of time, hackmud as a game also changes pretty slowly. I'd often play for a few months at a time, then play something else for a few months only to slowly drift back.
Because of that, one of my side projects in the game was a guide that could be accessed by running a script called what_is.this. Not only was it for new users, but it was also a tool to refresh myself on things I may have forgotten during our absence.
Unlike in C, a lot of my code in lua is usually pretty slick. I've spent a lot of time refining my exact style, and I try to keep to it even if I know no one else is ever going to see it. That being said, because the guide grew a bit out of scope of it's inital premise, the code itself was a bit... wonky.
Upon relaunching the game for the first time in a few months, I decided to clean up the guide; I'd rewrite the logic and write some new articles to fufill some of the requests that had been made during our absence.
A day or two went by, and a lot of the work that needed to be done was reformulating old, ad-hoc whateverthefuck into a nice and neat schema. It was sort of boring, but there was a sort of satisfaction in it, the same kind one might derive from playing a game like Powerwashing Simulator.
Instead of every article just being a big string containing everything I'd want to display, with coloring and what not baked in, I seperated everything out. Each article became the article's data, as well as flags that said whether an article was under construction, or lore heavy, or contained dangerous player scripts. Adding or removing any of these was as easy as changing a true
to a false
.
I even created a system that would handle auto coloring "hyperlinks" — AKA, instead of having to go through all 130+ articles each time I added a new one and change some text from blue to pink, it'd do it all for me. All I had to do was reupload all the articles, and my new and improved script would be available for everyone to run!
Except. I was running into a problem.
The bug I'd decided to ignore all those months ago was coming back to bite me in the ass right at the finish line. The hard part was already done; all I needed to do was upload some data. But I couldn't get more than 13 or 14 articles in without the whole db wiping.
I was not having fun.
But, a silver lining. For the first time ever, I was able to consistently recreate the bug, even if the exact process to do so felt a bit... arbitrary.
From my experience, what I seemingly needed to do to recreate the bug was
[...] to upload 14 articles from a blank db, then, when sys.specs uploads, I have to reupload it again, but by hand (I'm using AHK to speed up the process, but it breaks character encoding on two articles for some reason, so I have to copy paste in a fixed version of the article, which updates the db instead of adding to it), then I have to upload three more articles, and it crashes on the "upgrade_slots" article
Odd, but, doable. So I did it over and over, placing prints in various places to try to determine where exactly the code was failing. Was it on the lua side? Was it on the Javascript side? Was it in lua54.core? lua54.exe? antimony.lib? aiphos.dencode? Each attempt to find the bug involved a 5 minute setup process, but I was determined to squash this fucker once and for all.
And then.
Straight to acceptance. See, I knew that my code was built on a house of cards the whole time, and I think I'd always sort of been waiting for the other shoe to drop. In fact, I never really stopped patching bugs, and I was confident this would be another quick fix. I wasn't looking forward to fixing the C code, but at least I could-
Ah.
Okay, that's... not great. I hated having to crawl into bed on that note, but tomorrow I-
Alright, we didn't go to bed. I was a little too focused on the problem. But my code was creating a pointer to a function at memory address 0x1
, and luatojs was returning literally the number 1. I wasn't sure what that meant, or what to do with that info, but it felt like progress.
I had a starting point.
An answer wasn't revealed to me while we slept, unfortunately, but regardless, a few hours after waking I had a theory I felt confident about. My cargo cult-esque methods of uploading articles was uneeded; I believed that the problem would arise from any big enough chunk of data that got passed back and forth. And a few minutes of testing corroborated that theory.
If that was the problem, then all I had to do was increase the size of the memoryspace I was working in.
Unfortunately, to do that, I'd need to dive back into the C code.
Now, those of you out there who know WASM might be confused by this. "Couldn't you just run Memory.grow()?", you may ask. And, had I written my inital port correctly, then yes, that's all I would've needed to do.
In fact, if it'd been written correctly, emscripten (the specific compiler I was using) would've handled that for me. Memory management shouldn't have been something I'd need to worry about at all. But I didn't write it correctly.
You see, I had a vague understanding of how to put something into memory and have the C code read it, but at the time, I wasn't quite able to figure out how to tell either the Javascript or C code where to read from. My strategy of hijacking the functionality of emscripten_run_script_string
meant that I just didn't have access to the memory pointers (the location in memory where my data was).
Had I been using emscripten_run_script_string
correctly, that wouldn't have been a problem. It's true purpose is to take a string from C, and eval
it in Javascript, and to then send the result back as a string. But hackmud doesn't have eval
, and so I had to write my own interpretation of emscripten_run_script_string
, and effectively, I was using a screwdriver as a hammer.
As well, my method of building a string on the C side never called malloc
or free
; the functions provided by C to allocate memory, or free the allocated memory. C didn't realize the memory I was using was being used, because I never told it I was using it. Javascript didn't know, because C never told it.
This was fine... for small chunks of data. But for a big chunk of data (say, 20 or so articles poorly encoded into a megasuper string), it would simply overflow. Javascript would fail to parse it, and C would get a response that didn't have a 1, 2, or 3 as the first byte. Instead of putting a boolean, a number, or a string onto the lua stack, I was getting the return value instead.
A single number 1, interpreted as a memory address for a function.
function 0x1.
I didn't want to dive back into the C.
I really didn't want to.
So instead, I decided that I'd rewrite lua in it's entirety, using a luaparser written in Javascript. The parser didn't do any of the logic of lua, it would simply read through a string, and break it down into it's component pieces for me.
An assignment operation. Addition. Subtraction. A function call.
I figured, all I had to do was read through the output, and do each operation as I came upon it. Each one was an instruction that I'd simply translate by hand in Javascript. There was even already a project that did something like that, although I wasn't able to port it due to it's size and complexity.
I felt confident about this.
It took nearly a full day for me to come to terms with it. The parser was not an easier solution. Fengari wouldn't work. There were no other implementations that would let me avoid this.
If I wanted lua in hackmud, I needed to fix my bug, and if I wanted to fix my bug, I needed to dive back into the C code.
To understand the bug well enough to fix it, I'd need to actually understand my C code. And to do that, I'd need to relearn WASM and emscripten. So I began pouring through documentation.
Documentation can be satisfying to read through, sometimes. Clear examples, an easy to navigate site, and a logical layout can all lend itself to a pleasant learning experience.
Emscripten is not that.
Their examples are lacking, when they even bother to have any. There's no flow to the documentation, meaning it's a pain in the ass to find where they may have written information on any one particular subject. The website is bloated with only tangetially related information, as though someone was using it as a dumping ground for anything that might possibly come up some day.
Overall, I wasn't enjoying my time there. But with the docs, as well as random piecemeal tutorials for semi related subjects, I was making progress. And 10 20 47 tabs later, I managed to write a new interpret
function, one that I felt made much more sense than the previous one.
//Take a string from lua, and run it in JS, then take the result from JS, and pass it back to lua.
int lua_tojs(lua_State *L) {
if (!lua_istable(L, -1)) {
return luaL_error(L, "Value provided to lua_tojs was not of type 'table'.");
}
//Get the size of the table.
const lua_Unsigned len = lua_rawlen(L, -1);
//Allocate memory for the table. It should contain lua_Numbers, which are doubles (usually).
int *values = malloc(len * sizeof(lua_Number));
//If we fail to allocate, we throw a lua error, which is then handled by JS.
if (values == NULL) {
return luaL_error(L, "Size of table is too large for WASM instance. Failed to allocate memory.");
}
//Iterate through the table, pulling out all of the numbers.
for (int i = 0; i <= len; i++) {
//Take the value of our table (always at -1) and get the value at the index
//i + 1 (for 1-indexing). Our table gets pushed down at the same when we add
//the value from the table to the top of the stack, but then we pop it, which
//moves our table back up to the top.
lua_rawgeti(L, -1, i + 1);
//If it's not a number, free the space we allocated before throwing.
if (!lua_isnumber(L, -1)) {
free(values);
return luaL_error(L, "Value in table for lua_tojs was not of type 'number'.");
}
//Our value is now at the top of the stack. We get it, place it in our array,
//and then remove it from the top of the stack.
values[i] = luaL_checknumber(L, -1);
//lua_pop removes n items, not item n.
lua_pop(L, 1);
}
//Pass in the pointer to our array. JS reads it, then spits back out another pointer
//to a new set of data, which we place back into lua to be processed. C doesn't do anything
//other than verifying types.
const int *result = read_data(values);
//Once JS has finished reading our values, we can free that memory.
free(values);
//We don't know how long our new table is, so we're going to have to iterate over it with
//a while loop until we find our termination pattern (\0\0\0\0). We create our i variable
//outside of the loop, and create a new table to begin pushing values into.
int i = 0;
int termination = 0;
lua_newtable(L);
while (1>0) {
//Increment out termination counter if we keep finding consecutive zeros.
//Reset it if we find anything else.
if (result[i] == 0) {
termination++;
} else {
termination = 0;
}
//If we found four 0's in a row, then we are all done. We can stop looping and return
//to lua.
if (termination == 4) {
break;
}
//Add our current number to the top of the stack.
lua_pushnumber(L, result[i]);
//Take the value at the top of the stack, and put into our table, which is below it.
//We +1, because lua is 1-indexed, and rawseti pops the value we just pushed, which
//was at the top. This moves our table back up, ready for another push.
lua_rawseti(L, -2, i + 1);
}
return 1;
}
//exported function. Give it a string (our lua code), and interpret it.
//When successful, returns 1.
int interpret(const char* string) {
int status, result;
lua_State *L = luaL_newstate();
//Failed to create a luastate.
if (L == NULL) return -1;
luaL_openlibs(L);
//the luatojs function is what lets us call out from lua back to js,
//while still in the middle of our lua.
lua_pushcfunction(L, lua_tojs);
lua_setglobal(L, "luaToJS");
lua_gc(L, LUA_GCSTOP);
luaL_dostring(L, string);
lua_close(L);
return 1;
}
For those of you who have no idea what they're looking at, let me break it down for you. (And for those of you who do know what they're looking at are and looking to copy paste it for some reason, know that this is old, not working code!)
Rather than taking my data from lua, converting it into a bytes, turning those bytes into a string, then sending that to Javascript, I was skipping the string part entirely. Instead, I was giving C an array (a sequential list) of bytes, and sending that straight over to Javascript. Javascript would decode it, do whatever it needed to, and send the data back.
I felt it was elegant. It was similar to my old method, but I was saving a step. Previously, if I wanted to send over the number 12, I'd need to create a string, append the string with 3 (my "identification" byte), then convert the 12 into a string (because I could only send strings back and forth), then undo the whole process.
Now if I wanted to send 12, I'd send 2 (my new "indentification" byte), then I'd send 12.
That's it.
All I had to do now was write an encoder and decoder for both Javascript and lua. And since I wanted to keep things consistant, I'd write a single encoder and decoder, and try to make both languages do as close to the same thing as possible, so that it would be easy to debug.
I was thinking ahead. Consistant behavior on both ends, and a lack of failure points meant that this would be a walk in the park.
It wasn't not working, per say, but it definitely wasn't functional. I couldn't tell why, either; it simply seemed to do nothing, rather than run any lua code I provided it. Annoying, mainly because diagnosing a lack of an error is a lot harder than diagnosing with one.
Still, having a semi working implementation was satisfying, and without the nightmarish, unintenitonally obfuscated code I'd had before due to the lack of character upgrades, it was a lot easier to keep track of the logic.
You see, since my last attempt, there had been an update in hackmud that changed the default amount of characters in a script from 500 to 2000. As well, I had moved everything over to a user specifically created for this purpose, which meant it had lots of character count upgrades.
I had nearly 10 thousand characters to play with, instead of a measly 2000 or so. And by the time the project was finished, I had upgraded it to 18 thousand characters.
My sandbox huge, and I was building one hell of a sand castle.
Well, once I sorted this bug.
On the C side, lua works as a stack based system; stuff goes onto the stack, you run one of the lua C functions, and lua takes stuff off the top.
So, if I wanted to call my_function
in lua with two arguments, I'd first push the name of the function, then argument 1, then argument 2. Then I run it, and it pulls everything off the stack, and puts my results onto the stack.
So, to get the values from running lua_tojs
, I needed to first open up the table I provided it, then take each number and put it into an array that I could send to Javascript. But I couldn't. For some reason, I was running into an error.
Over, and over, and over I tried things. I reread the lua documentation. I started at my C function. I was so sure that I was pulling items correctly, but every time I tried it, an error would pop up saying that I was passing nil
(lua's version of a "nothing" value).
I defintely wasn't making an empty table, so where were the numbers going? Was I looking in the wrong place?
What? Was? Happening?
The keen eyed among you may have noticed an issue in the C code I posted above. Generally, a for loop is
for (int i = 0; i < length_of_thing; i++)
However long your thing is, you check if i
is less than that thing. If I have 5 items, then i
will count up by 1, starting from 0.
0, 1, 2, 3, 4.
That's five numbers right there.
i
starts at 0, and ends 1 under whatever I'm counting. Four is less than five. Five is not less than five.
But what if I wrote <=
instead? Well, then I'd do one extra loop, of course. Except, there'd be no sixth item to grab.
Or, you might say that the sixth item... is nil.
This stupid little piece of shit equal sign wasted over 16 real life hours. I hadn't thought to check the for loop because, well, I've written hundreds- no, thousands of for loops in my life. Why would I double check something so simple?
I... was at a loss for words.
Literally. I screamed so loudly when I found out that I actually lost my voice slightly the following day.
But, as frustrating as it was to realize that I'd been so thoroughly conquered by a math symbol, it meant I was able to continue forging forward.
And forge I did.
Running into an out of memory error from lua? It's an issue with my decoder. Fixed.
I'm reading arbitrary sections of memory? Turns out I'm freeing the same chunk of memory twice. Fixed.
Every problem I encountered, I'd find a solution within the hour. And even if I couldn't, I always had something to try. I never hit a wall. My fingers were flying across the keyboard, and I was truely and utterly locked in.
And then.
On the downside, I'd be unable to use pretty much any of the code I had originally been using, since it was all bespoke. But the upside meant that my new translation code would be easy to write and easy to debug, and also not hurt to look at.
The translation process was nearly painless. There was a minor hiccup when I realized that Javascript would automatically convert all keys in an object to strings (lua doesn't do this), which meant that what came back didn't match what I sent, but eventually I realized that didn't actually matter.
As nice as it'd be to ignore the Javascript underneath the lua, I couldn't ever fully forget it. That's the language game scripts run in. If I wanted to play in a puzzle, or save things in the database, I would always need to be aware of that fact.
And so it went.
I also needed to write some special handler code for certain, hackmud specific features, but due to the extensible way I was writing the decoder, all I'd need to do would be to add another "action" to my list of "actions".
Am I trying to run a Javascript function from lua? Action 3.
Am I trying to run save something to the database? Action 7.
Each new feature was an if check, and a few lines of code. And with the heavy, heavy commenting, I'd never run into another case of trying to piece apart the logic of a Nova from yesteryear.
I did get a little distracted adding some bells and whistles that weren't really needed, but after a handful more days, I finally finished it. lua54.exe was open for business.
Kind of.
Bugs happen. Bugs will likely keep happening. This write up could easily be another five to ten chapters of every new hurdle I encountered and surmounted, but when it comes down to it, I did. I surmounted each and every one, and I'll continue to do so.
You might ask why there's anything left to surmount? Surely, the code is now running. What is there to fix? Well, like any artist with their art, I can't help but to desire to improve it. I want to make it better.
Faster.
More robust.
But at the end of the day, what I set out to do, I accomplished. As much as I dreaded the task of reimplementing my port of lua into hackmud, I still did it. My code is on Github if you'd like to take a look.
And maybe in another year, there will be a write up about how my old code was a joke; infantile, overengineered crap that any old shmuck could make. I know I ragged a bunch on my previous implementation, but that's progress. Ever improving, ever growing.
But for now, I can confidentally say: I'm proud of what I've done here.
And with that, I'm off to go gamble at zac.casino with a bunnybat in my lap, confident that my upgrades are safe because I use xena.defender.
All through lua, of course.