IsaacScript in 2022 - has it gotten better?
I feel like it's only right for me to revisit this tool once more, half a year since my last post on it.
It's been a pretty long while since my last post on IsaacScript. I had quite a few gripes with it and its creator, and how it was advertised and handed out to beginners. But it's been half a year since then, and a lot of my opinions on it have since changed, so I felt like it was about time to make a follow-up post, additionaly responding to some parts of Zamiel's reponse post.
As a refresher: IsaacScript is a suite of tools that lets you use TypeScript in Isaac modding. Typically the language you'd use would be Lua, but IsaacScript uses TS2L, a transpiler which will convert TS code to Lua.
You can do practically anything in TS that you could in Lua, and the standard library abstracts a lot of usual Isaac modding tedium away. There are functions and custom callbacks to handle a lot for you from smelting trinkets to creating custom grid entities.
It's also quite opinionated; by default, it comes with a custom prettier config that's forced on you and a git config that forces rebase on. Now, that's not to say that heavily opinionated tools are a bad thing - but we'll get into that later.
So how has it changed since we've last taken a look at it, some 8 months later?
How has IsaacScript changed?
IsaacScript is such a quickly evolving tool that I'd have no chance of documenting every change made over such a long period of time. However, I'll try to highlight the ones that matter for this post.
This is a pretty big one that messes with my ability to even document said changes. IsaacScript's development has been moved to a monorepo as of May 23rd, 2022. Now, before we even get into my thoughts on that, it's worth noting that this has caused the old commit history for the previously separate projects to vanish.
Before the merge, the IsaacScript GitHub organization hosted many repositories for different purposes - one for say, the Isaac API definitions, another for the standard library, another for the CLI tool, etc. Once the merge happened, the repositories were archived and a link was added to each one, redirecting users to the monorepo instead. The issue is though, that at one point, all the old, archived repositories were either privated or deleted, making it impossible to see their Git history.
The organization now consists of only 2 repositories - one for the website, and another for the monorepo.
This, uhm, sucks??? Any and all commit, issue and pull request information is now completely hidden, and potentially lost to time, with only some being preserved. This means, for instance, I can't check what was changed in the week after my complaints were heard, and perhaps never will. The only changelogs I get are the officially provided ones which do not have the granularity of Git commits.
Zamiel has told me that the Git history was merged into the monorepo, but he refused to elaborate how or where - I never got any definitive answer as to how I could access it.
That aside though; monorepos by themselves are a pretty bad idea. There's barely any upsides, and many, many downsides. I won't go too far into detail here; but in short, most of the claimed upsides of monorepos are barely valid, and the downsides consist of having to basically push Git, IDEs and all tools that interact with your repository to their absolute limits.
I'd recommend giving this post a read if you're interested:
As of November 7th, 2021, IsaacScript now adds a notice to every transpiled file. This seems to be in direct response to my post - a big complaint I had is that there would be no information for someone peeking through mods to find out IsaacScript was used in the files.
This is a good change! I'm fully able to admit that this is something that was worth doing for a pretty long while, and I'm glad it's now implemented.
TSTL updates and improvements
Quite a lot of what I complained about has changed - the transpiled Lua files no longer create global variables, performance improvements, less bloat, stuff like that. That's great to hear; however it still doesn't justify using a transpiler in my eyes.
This is a bit of a funny one.
A few months ago, Yarn support was added. Yarn is an alternate package manager to NPM, but its differences don't really matter. What matters is it was a 3-4 line change.
I, myself, use PNPM when possible. PNPM is another alternate package manager to NPM, focusing more on familiarity to NPM than anything else. I brought up the concern to the IsaacScript Discord server, hoping that it'd be added since it'd just be another 3-4 lines:
But, of course, this didn't go so easily:
So, sure. I guess adding 2 lines to benefit users in the future is too much effort. I wasn't willing to argue for another hour or so; so I left it at that.
Fast forward a few days later:
No reply to this message is given. A few more days later, PNPM support is added.
PNPM's name is misspelled as PNMP in the changelogs, and no further mention of it is made in the server.
A StageAPI-like interface
IsaacScript has recently taken to implementing a StageAPI-like system that lets you create custom floors and custom rooms with those floors. However, it does this without requiring a dynamic dependency like StageAPI does, requiring the end user to download it manually. How, you might ask? Well, it's kind of dumb.
What StageAPI does
StageAPI works by "emulating" floors and their rooms. What matters here most is the way custom rooms are implemented.
You see, custom rooms that spawn under certain conditions cannot be added to Isaac - the way this is handled in StageAPI is rooms are coded into what are called "luarooms", Lua representations of those rooms, and passed to StageAPI. Upon going into a room that should be a custom room, it "emulates" it, recreating the room with the given data inside. This is a really hacky method, but the consequences of the alternative are, in my opinion, much worse.
What IsaacScript does
IsaacScript instead relies on hard data. Mods that use its StageAPI-equivalent define actual rooms in the game that are loaded in conventionally, except with a weight of 0 so they never spawn without needing to. They are placed down on floors using a hack with the
goto command, which requires passing in the ID of the room.
The problem with this method is that the ID of the room cannot be deterministically obtained - you can use variant IDs that are global across all mods to get rooms specific to a mod, but variant ID overlaps are an issue. Only one mod gets access to a specific variant, and if an overlap is to happen, Isaac will silently increment the mod's variant.
The catch is there's no way to dynamically get said variant ID through the API, which is why StageAPI does what it does.
IsaacScript's solution to this is something quite quirky. Zamiel has created a GitHub repository that contains a text file. Inside that text file is a set of variant IDs and which mods those IDs belong to. If you wanted to register a variant for a mod, you would have to submit a pull request to said repo.
The issues with this are as follows:
- What do we do about existing mods? These will continue to use variant IDs that could potentially overlap with IsaacScript mods' ones, and you would never know, and end up potentially crashing the game.
- Requiring GitHub accounts and pull request verification is a tedious process that not only requires a GitHub account but also could potentially lead to having to expose confidential information about upcoming mods or their authors.
- This will get messy really quickly, as pull requests will have to get managed manually. If the maintainer of the repo, for instance, goes on vacation for a week, this could potentially delay the release dates of custom stage mods.
I brought these issues up to Zamiel, IsaacScript's creator, proposing a simple online database instead, and was met with a simple "shut up", pretty much.
He addressed my issues, but.. not well.
The response to my first issue was simply "IsaacScript mods that have overlaps won't compile". Which isn't a very good solution, considering IsaacScript's userbase in the grand scheme of things is miniscule (estimated to be around 5%).
The responses to my second and third issue were that everyone can get a GitHub account, they're free! And if you're worried about confidential info, make a throwaway account and mark a variant ID as a placeholder instead. Completely ignoring the fact that GitHub is owned by Microsoft, potentially one of the most data hungry companies out there.
I think that about summarizes how well that's going.
UPDATE: I wrote this section before this was finalized; what's weird to me now is that the GitHub repository isn't a standalone repository, but rather the IsaacScript documentation website?? This makes another problem arise, which is that the variant text file is much harder to find. I had to spend a good 5 minutes to find where it was stored knowing that it exists.
Zamiel has also stated when discussing this that since there's (practically) no upper cap on variants, it's fine to include spammy or obsolete entries, but the current finalized table restricts it to a (seemingly arbitrary) limit of "prefixes between 101 and 999, inclusive", and this is hardcoded in the actual CLI tool.
The TS config file makes this limit between 101 and 109, and while this is probably a typo, it just shows how well-tested this feature is. I can't imagine how many tiny things like this are prevalent in practice, considering how this is something I noticed by just skimming through the code.
For some reason, the variants are also multiplied by 10,000?? Why this is, we'll probably never know, as it's never elaborated on in the code or otherwise in any form; but it only makes overlaps less rare as multiples of such a high number are inevitably going to be used more.
Upon bringing either of these up to Zamiel, he just dismissed the issues, focusing more on tiny mistakes in my phrasing.
To make this even worse, I'm also pretty sure that despite Zamiel saying that IsaacScript mods won't compile if they use an already-used variant, there is no code whatsoever to make that happen. Very glad that he's keeping his promises.
Right, where was I?
So okay, to wrap up; IsaacScript hasn't changed too much. But this isn't why i started writing this - what's important to me to address is how my thoughts have changed.
How have my thoughts changed?
I feel like the only good way to address this is to go through what the original post went through again, but condensed a lot better. Both my thoughts and the Lua tools have changed quite drastically, so let's just do this right over again, shall we?
Let's go ahead and look through the features page! It's been updated quite a few times since my original post, and so have my counter-arguments, so let's just take a quick browse.
The page starts out with a bold claim; basically describing Lua to be a garbage worth-for-nothing language that noone should ever use and TypeScript as the reincarnation of Christ that fixes all your life problems and solves world hunger.
They say that every programming language has some pros and some cons. However, after five years of programming mods in Lua for The Binding of Isaac: Afterbirth+ and The Binding of Isaac: Repentance, I have not found very many pros. It is incredibly easy to shoot yourself in the foot after making even the smallest typo. And it doesn't have the tooling that we take for granted when programming in other modern languages.
I'll simply choose not to address this since it's very opinionated, to say the least. I can somewhat agree that TypeScript is better than Lua, but language preferences are a lot more complicated than just their respective feature-sets. Case in point, despite my previous claim, working in Lua is still generally a lot more comfortable to me than TypeScript.
I can say basically the same things that are written out here, but from my own perspective. TypeScript has a big, bulky and quite daunting type system that you can replicate TypeScript itself in - sometimes you want a simpler, less strict language that won't call you mean words for not knowing every quirk of the type system. That's not to say that this is my actual counter-argument; I just think it holds about the same weight as the initial claim.
Since my last article, two EmmyLua typings of the Isaac API have appeared - one made by me by transpiling the IsaacScript types, and another by filloax. To say that this problem has been solved would be an understatement - you have enough typings to make yourself a strongly typed breakfast, lunch and dinner.
I've worked with these toolings myself and they're really nice. While sure, IsaacScript does this for you out-of-the-box, since my last post there's now only a single VSCode extension you need to install to leverage the typings.
Zamiel's first response to this is always that the definitions are "limited"; you don't get modern IDE type system benefits like type narrowing or errors when you pass in, say, invalid ranges rather than types, but in the end, both of these aren't things you can't live without.
He makes a big deal out of crashes as an example; like passing
-1 to functions that don't expect it, but in reality, how often are you going to be doing that? You'll do it once, realize it crashes the game, and never again. And it's miles easier to debug crashes now that Lua crashes have proper stacktraces.
This is now easily addressable - the previously-mentioned typings always denote when a function crashes or does something unexpected in the function description. Moving on.
Automatic mod reloading is.. not a very big feature. I personally don't see the appeal, and I say this as someone who frequently uses it in web development with auto-reloading tools. And even there, there's a bigger point - parts of the page like CSS can be reloaded without reloading the whole page, but here it just, runs
luamod each time you save.
I can see the appeal, but I personally don't care too much, and you can easily implement this yourself with Run on Save and salvaged IS code if you care so much.
The last point I've come to understand a lot more. The key difference between
require is that
include doesn't cache anything and just, runs the code, while
require caches everything, more aggressively than you'd usually want it to.
Now, this may make you think that there's no point to using
require - but it helps quite a bit with code organization. If you want to run a function from file A in file B,
includeing the file would be a bad idea - it would unnecessarily run the entire file twice, and not carry over any variables.
requireing it, on the other hand, would only run it once, and still pass you the function.
IsaacScript gets around the bugs of
require where its paths are global and the cache is never erased, even across
luamods. IsaacScript is, to say the least, not.. needed to do this.
Nowadays, it's quite common to see
require patches that change it to well, not do that. It's code that you write once and reuse for every mod, and it works just fine. Sure, again, it's something that comes out of the box with IsaacScript, but it's not that big of a deal, really.
Sure. I get this one. That's +1 to IsaacScript. Though it's worth pointing out that the link in the image is broken due to the monorepo migration.
I haven't used it myself, but there's a Prettier Lua plugin out there you can use if you so want to. I don't know how good it is, but I'm happy to be proven wrong if it's actually awful.
Zamiel has told me that it's "broken", and once I pressed him to elaborate, he told me it didn't support bitwise operators. Not sure how that constitutes broken-ness; but I'm sure bitwise operators aren't so essential to your everyday modding that you could live without them.
In my post, I argued that Luacheck achieves the same that ESLint does. In Zamiel's response though, he brought this up:
I've used Luacheck myself for the past 5 years, and I would highly recommend it to anyone who works with Lua projects. However, the tool hasn't gotten many updates since the maintainer of the project died back in 2018. And the tool is very limited compared to what you would expect from a linter in any other language. As a fun fact, the severe limitations of Luacheck were one of the direct reasons that caused me to get frustrated enough to create IsaacScript in the first place.
On the other hand, ESLint is hands down the most popular and best linter in the world. It is actively maintained and has the ability to write custom plugins to do anything you want. The ESLint ecosystem has tons of existing rules, plugins, and even a support Discord for when things go wrong. This isn't exactly an apples to oranges comparison.
Zamiel's argument about the project not getting updates is entirely invalid. There's a community fork. Its last update, as of writing, was 15 days ago.
However, I will agree that Luacheck is inferior to ESLint. But at the end of the day, again, it's not that big of a deal, and certainly not something worth making a transpiler over. If you wanted good linting for Lua, you would contribute to linters for Lua, rather than running to your beloved TypeScript and ESLint and transpiling those over instead.
This makes sense, and is completely fair. It's worth mentioning though that this is also still possible in Lua with many, many libraries written to serve this purpose. One of the most often-requested callbacks, a callback for picking up an item, has a dedicated mod you can both dynamically and statically depend on and use seamlessly. It also works better than TypeScript can offer you because of conerns over variable name overlap.
With IsaacScript, your code will look something like this. While in Lua, with the above library, all you'll have to do is this.
This is because IsaacScript is concerned too much about being self-contained, in the event that an imaginary second library appears that uses the exact same callback name. Why a second library would appear despite the extremely simple functionality of the callback is beyond me.
It's not a massive difference, but it's one that would surely confuse more beginners than just passing callbacks the same way they always have before.
This is true. Having a more powerful standard library is a big advantage. But IsaacScript's standard library has the opposite problem of Lua and the Isaac API - it's severely bloated. This has the disadvantages of being much harder to maintain, bugs appearing more often, and making the documentation a lot harder to navigate. As of writing, the page count of the isaacscript-common documentation stands at approximately 860 pages in total (estimated by pressing Ctrl+P in each page and seeing the page count).
Lua stands at a good standard library balance of being small enough where you can memorize it all without much effort. It's a very nice part of it, albeit coming at quite a large cost of having to write functions for simple tasks, say, whether a string includes a substring.
IsaacScript takes fixing this to the extreme, where you will most definitely not be able to memorize it all. You could argue that this isn't necessary for a standard library, and you could find whatever you're looking for specifically, but this becomes hard when your documentation is a mess with arbitrary categories and no codebase namespacing. As it is, the standard library is 1 step away from having literally everything exposed under one namespace, which isn't great when you have functions like
isOdd included alongside everything else.
You could also argue that this all doesn't matter; if you wanted to, for instance, set the player's soul heart count, you'd start typing
setSoulHeart and let autocomplete handle the rest. This logic really doesn't work too well. There's many ways to spell or phrase the same thing, and VSCode's autocomplete is far from perfect. It'll only get worse the more functions, classes, enums and interfaces there are.
Yeah, this is fair. There's libraries that do this in Lua, but honestly I haven't seen any cover every use-case I could think of, leaving me to implement my own additions. Though once again, I have to question why Zamiel has to do this rather than improving existing Lua solutions.
This has been integrated into vanilla in a patch. This only matters in the context of IsaacScript because of the additional transpilation layer obfuscating the actual error line.
We've gone over this before, and I'm not doing it again, so I'm just going to pretend this section doesn't exist. The majority of it is also possible with a language server, and not because TypeScript is some legendary language that invented all of this.
Right, that's dealt with. The amount of features IsaacScript has over Lua has only decreased over time with toolings for Lua Isaac modding catching up to speed greatly. Now let's head to the next important section!
Things have only gotten worse since my last post.
Zamiel will constantly insert IsaacScript in every conversation. If someone asks for how to do something, anything, the response you'll most commonly hear from him will be something along the lines of "If you're using IsaacScript, use
doThing. Else, re-implement this manually in Lua: (link to the source of the function)". Someone will say that they wished that a callback was in Isaac, and Zamiel will just insert a link to the IsaacScript docs of that callback.
This is behaviour that's highly annoying and drives a lot of beginners away, since, I would like to emphasize, IsaacScript is not for beginners. People will come into the channel half an hour later, looking for help installing it, and get told off by Zamiel, telling them to go use Lua instead. He's frequently said that if you don't know what a CLI application is, you shouldn't be using IsaacScript. Yet he frequently pushes it to new people on the server anyways, misunderstanding the demographic of the server.
He will constantly start arguments, and in those arguments often start belittling people.
Generally not a great person to be around, I'll be honest.
He will also be very stubborn about his vision of IsaacScript, as mentioned before in the PNPM part, which makes disconnecting him from the project just that much harder.
Ignoring that though, he also very frequently doesn't practice what he preaches; he will constantly nag people about the most miniscule things like not testing their code, not linting their code or not committing properly and then not test his code, not lint his code, forget his own forced commit standard and dump lots of changes under "fixing" with incorrect or incomplete commit names.
At best, his behavior comes off as petty, and at worst it's disrespectful and unnecessarily aggressive. You may ask why I write this here instead of bringing it up with Zamiel, and it's because otherwise he won't ever listen - he refuses to aknowledge any of his behavior as toxic and instead reports every single message criticising IsaacScript as "toxic".
UPDATE: Since the writing of this post, Zamiel has been banned from the Modding of Isaac Discord server.
UPDATE 2: Since the publishing of this post, Zamiel has banned me from the IsaacScript Discord server.
Has IsaacScript gotten better for beginners?
Short answer: no.
In the response to my previous post, Zamiel noted that IsaacScript can be quite useful for beginners, actually:
On the one hand, I think that Jill is right: adding the complexity of a compiler could be overwhelming or counterproductive for someone new to programming. But on the other hand, I think that Jill is wrong: having a compiler can be invaluable for a beginner.
Frequently, people come to the Isaac modding Discord and post code-snippets requesting help. Over and over, the root cause of many issues (maybe even most issues?) are mistyped variables, mistyped functions, or other type errors. Sometimes I notice that these troubleshooting sessions go on for over an hour before the root cause is discovered.
Yet if you ask any beginner to use IsaacScript, quite commonly, they'll get confused at the installation step, and Zamiel will offer no help, in fact, once again, stating that if you don't know what a CLI application is, you shouldn't be using IsaacScript.
I understand where he's coming from, I do - but from experience, having a red squiggly line tell you that something's wrong when you can't comprehend the error at hand isn't really helpful. It will eliminate errors that come from variable typos, but that's about it - if you pass the wrong type, chances are, you'll get an error something along the lines of "can't apply value of type
number | CollectibleType", rather than "want an item ID here".
It also doesn't help that IsaacScript is quite heavily opinionated. It forces you to use Bash for its scripts, despite being mainly for Windows, changes your Git config for you to force it to rebases and Unix-style newlines, has a custom Prettier, ESLint and CSpell config, all of which VSCode will nag you to install when using a workspace for the first time, and generally, if you ever ask for help with code formatted not in the way he personally likes, that will be the first thing Zamiel points out.
And while sure, all of these are optional, they're not presented as such; you're just given these magical files and pretty much told to never question them. The ESLint config is an exception to this; being somewhat monolithic rather than modular: the user is given a single .eslintrc.js file that links back to IsaacScript's config, rather than just embedding the IsaacScript config there.
While you could argue that this is better for a beginner as it means they have to make less choices, overwhelming them a lot less and forming better opinions from the start, there's a difference between providing lots of options and forcing lots of options.
The ideal way to make highly-costumizable software beginner-friendly is with sensible defaults, rather than overwhelming options; for example the Micro editor pulls this off well while Vim does not, the fish shell pulls this off while ZSH does not, the list goes on. However, it's important not to lean into the locking into choices area of things when designing software - GNOME is infamously bad at this, letting you costumize practically nothing unless you decide to use plugins. (can you tell that I'm a Linux user yet?)
While this doesn't change how beginner-friendly it is in the long run, what it does change is the user experience past being a beginner. People who learnt from IsaacScript will be disappointed or surprised when the rest of the world doesn't follow the exact same conventions to the tee, while experienced programmers will be disappointed IsaacScript limits you as much as it does.
That's also not to mention that IsaacScript's userbase is much smaller, and thus beginners will get a lot less support. While sure, the compiler will error if something is about to go wrong, chances are they'll still ask someone about what to do once it does, and help is much less likely to be available in a 200 member server than a 2,000 member one.
So I'd still argue that in its current state, IsaacScript is unsuitable for beginners to start modding Isaac with, and that it's still probably not the best idea to direct newbies to IsaacScript as soon as possible.
I'd say IsaacScript hasn't improved, and has only gotten worse. I still wouldn't recommend anyone use it, but if you're using it and you're enjoying it, that's great and I'm not going to belittle you for it.
As it stands, IsaacScript has many flaws that make its very few features over Lua practically worthless. Despite Zamiel constantly claiming that IsaacScript is "objectively better", it comes down to preference and he refuses to accept this, despite it being highly opinionated and the many glaring flaws (standard library bloat, transpilation sizes, etc.) that he refuses to acknowledge.
I'm happy to see the amount of effort that has been put into the project to make it as comprehensive and polished of a tool as it is, I just really wish that the same effort could've went towards improving Lua toolings instead.
Please, once again, if you believe I made factual errors in this article, be sure to contact me! I'll be more than glad to correct anything I might've gotten wrong.