Introduction
Howdy! I know a lot of y’all are excited about TMT-X, and even more of you probably weren’t even aware of it. I’m creating this dev log to document the process, keep people updated on progress, and get people a bit hyped for it . They won’t be weekly, nor necessarily all come out at the ends of weeks, but rather whenever I feel it makes sense to cover what has changed and what’s next.
So let’s start with this: In case you weren’t aware, I’ve paused development of Kronos to make this new engine that’s a complete rewrite of TMT, intended for advanced users. Once it’s finished I’ll migrate Kronos to it and continue it’s development.
Major (Old) Progress
Since this is the first log, let’s start with not-so-brief explanation of some of the major features in TMT-X that have already been implemented. Early development was very fast paced, so there’s quite a lot. I apologize in advance for just how long this first post is - future ones should be much shorter, but this post is effectively a nice list of why I’m so excited for this project, at least.
Proxies
In TMT you have a tmp
object that is constructed every single frame by going through every layer and running all non-blacklisted functions found there, including the ones that won’t be used this frame, and re-calculating things even when the data they depend on hasn’t changed. In addition to the unnecessary calculations, tmp
has been a source of many errors by things being incorrectly blacklisted or not.
TMT-X attempts to resolve this by using getters. When it was first implemented this was using vuex getters, but that has changed and I’ll discuss why later on in this post. The end result is the same though: function results are only calculated when they’re requested AND the values they use have changed since the last time it’s been calculated. This’ll have the greatest impact on complicated formulas based on data that doesn’t change every frame - for example, based on buyable amounts.
You’ll still need to specify which functions shouldn’t be cached in this way - typically the ones that do something rather than just calculate a value (for example the buy
function on an upgrade). However, unlike TMT where you list the names of blacklisted functions, in TMT-X you can wrap the function in a noCache()
call, allowing the dev to not worry about function names they pick.
That all got technical, so here’s a summary of why this is useful to developers (besides the performance): You’ll never need to use tmp
again, only layers
. Additionally, this makes this
way more useful in most functions you’ll write. Let’s take an example:
Assume you’re writing the display of an upgrade, and you want to access the effect of said upgrade. If you used this.effect()
in TMT, it’d needlessly re-calculate the effect and cost performance. To avoid
that performance cost you’d need to write one of these lines:
tmp[this.layer].upgrades[this.id].effect
upgradeEffect(this.layer, this.id)
In TMT-X you can just do this.effect
, and it’ll give you the value with the minimal amount of re-calculations necessary, with a shorter amount of code than anything possible in TMT. Not you can still use the utility functions for easier migrations from TMT to TMT-X, as well as making it still easy to access things from other feature types or things from other layers.
There’s another useful thing you can now do with this
: use setters. The best example of this is when making a buy function for a buyable. In TMT you’d typically need to write something like this:
setBuyableAmount(this.layer, this.id, getBuyableAmount(this.layer, this.id).add(1))
Sheesh. That’s a lot of code to need to have in every single buyable, and it takes longer to read through when looking for bugs or typos. In TMT-X that entire line could just be this:
this.amount = this.amount.add(1)
Shorter and way easier to read and understand!
There is also a proxy for player
. This allows us to perform NaN checks upon setting values, rather than when they’re read. I’ll go into the usefulness of that later on in this post. Originally it also handled automatically using Vue.set
, however that’s no longer necessary in Vue 3 (details below).
Coerced Components
In TMT you have this tabFormat system that lacks all the power of Vue for the sake of writing arrays rather than HTML-like code. In TMT-X every major feature has at least one customizable display, and every customizable display goes through this component coercion process that allows you to provide one of many different things:
- The name of the component to render. Intended use is for when you write your own .vue files with your own components
- A Vue component object
- A Vue component template string. It will automatically be made into a Vue component by attaching
layers
,player
, and all the utility methods to it. Keep in mind this option will make your component “stateless” , although that typically will be true. - Raw text. This will be wrapped in a
div
or whatever is appropriate for the space it’s going into and then converted into a Vue component as described in option 3.
The end result is allowing you to use just as much customization as you’re need and are comfortable with.
This’ll be stated in the guide/documentation for TMT-X, but for optimal performance you should access variables from within the Vue template, rather than construct the string dynamically. That is, instead of doing either of these:
return `You have ${player.points} points`;
return "You have " + player.points + " points";
you should instead do this:
return `You have {{ player.points }} points`;
Tabs
The tabs system has been revamped entirely. Instead of explicit left side and right side values, the player
now has a tabs
array that can have an indefinite amount of values: Each being either the name of a layer to display, or the name of a component to display.
This array can be modified however you like, but as a utility the Tree component has a property to make it “append” tabs rather than just switching the open tab. What this means is you can use a tree to toggle tabs on/off individually. You can also disable the back button, which is recommended if using the tree in this way. The back button will by default close itself and all tabs to its right.
Additionally, tabs can now be “minimized”, so you don’t need to deal with changing layouts based on desktop or mobile.
NaN detection
NaN values are one of the most common issues in mods and can be both frustrating to the player to deal with, and for the creator to debug. In TMT you only get notified when a value becomes NaN, but you can’t access a stack trace to see when/where/why it was changed to NaN. Additionally, the report only shows the name of the variable, which isn’t that useful if its something like “points”, which exists on every layer as well as the player itself.
In TMT-X, NaN values are detected as soon as they’re set thanks to the player proxy, and give a stack trace that shows you exactly where it happened. You can use your browser’s built-in debugging tools to pause on error and investigate what the issue is.
For users, the modal also gives you a lot more information as well as control over how you want to handle the situation. I’ll just let the screen speak for itself:
Dynamic Layers
Notoriously several people attempted to make mods in TMT that add new layers as you progress. To my knowledge no one’s really succeeded, although Jacorb’s RNG Tree comes fairly close - same number of layers regardless, but the content within them is determined by a seed on the player object.
TMT-X aims to support dynamic layers out of box. Instead of calling addLayer() in each layer file, there is a getInitialLayers function in mod.js that takes a save file and determines what layers should exist in it. New layers can then be added dynamically using addLayer - just make sure to add something into the player object so you can remember to add the layer in again next time the save is loaded.
Layers can also be removed via a removeLayer function. Unfortunately, there isn’t really support for modifying layers - the addLayer function prepares layers and if a layer changed it’d have to re-process it…
Which is why we do have a reloadLayer function, which just removes a layer and then re-adds it with a new configuration object, in one action. Voila, dynamic layers
Saves Manager
The saves manager allows you to manage multiple saves, which can be very useful while testing and balancing. Create, duplicate, export, import, and remove saves. It shows what version of the game they’re from, and how long they’ve been played, and you can switch seamlessly between them - it doesn’t even require a refresh! They can also be reordered and renamed so players can keep saves apart.
When creating new saves, you can create them from a save bank as well. Any .txt files placed in the repositories’ saves
folder will automatically be placed into this list to be loaded by players.
Modals
You may have noticed by now, but there are some interfaces I’ve shown here that don’t appear to be tabs. In TMT-X you can now also create and show Modals, that appear above everything else until closed. They are very customizable, and easy to hide/show. Layers can even specify that they should appear in a modal instead of a tab - great for achievements!
Build Tools
Finally, all of this is built in a proper web development environment. You’ll need to install node, but in exchange you’ll get the benefits of hot reloading, optimized builds, and so much more. You can use .vue files, SCSS, and typescript seamlessly. Download NPM packages to use them in this project with ease. While it’s not built yet, there are plans to add a github action to still allow you to deploy to githack or github pages either completely automatically or at the click of a button, as well.
Recent Progress
Vue 3 migration
Initially this project was made in Vue 2 because that’s what I’m familiar with. Vue 3 is a newer version that changes a lot of things fundamentally, so both versions are widely used.
While testing I was having issues with performance after switching to/from tabs with lots of elements, after hot reloading. While it wouldn’t affect players, I wanted to fix the issue so development wouldn’t be impacted. I eventually determined it would be fixed by Vue 3’s new proxy system, so I migrated to Vue 3… it was pretty difficult, to be honest.
Wait, Vue uses proxies too? Yeah, with Vue 3 reactive objects use proxies, which make it so developers don’t need to use Vue.set anymore. Very nice. The issue is my proxies and their proxies did not like each other. The layers proxy wasn’t actually reactive itself - it just used vuex getters to handle the whole “don’t recalculate stuff unless it’s changed” behavior - those getters were reactive, but layers wasn’t. The player proxy, however, absolutely was reactive - and necessarily so. Specifically, it was the vuex state object.
This problem took me a long time to resolve, with a lot of trial-and-error, googling, and contemplation of just sticking on Vue 2. In the end I solved it by make my player proxy not actually a proxy of the player object - it created a new object, which would just store a reference to the player object (and another thing which we’ll get to later). If you accessed a property from this proxy it’d return it like normal, unless it was an object - then it would create a proxy for that as well, with its own pointer to the actual player state (well, specifically the part of the player state it would be proxying). So now the player proxy is just filled with nested proxies, that all can look up values from the actual player object as necessary.
This had an additional unexpected benefit: I realized I could also store the path of all the properties used to get to this part of the player object. This allowed my NaN checks to store the exact path of the value that became NaN. So instead of it saying something called “points” became NaN, the message can say specifically player.p.points became NaN, for example. Very cool!
And for the record, it fixed the performance issue Although the Vue 3 devtools extension keeps freezing in the timeline view for me. It could be my browser, though (Vivaldi).
Removing Vuex
Vuex is the state management library I’d initially used in TMT-X. It has a reactive state, which allows things to update as things in Vuex change. That’s perfect, and part of how the whole “only recalculate when necessary” stuff works. I was also already familiar with using this kind of state management from my experience using Redux with React. (Although I don’t think I’ll ever go back to React if I can help it!)
Unfortunately, the needs of TMT-X made me have to use Vuex in unintended ways. For one, with the amount of values that were changing every frame, using mutations - basically sending events for every change - would have absolutely murdered performance. And the key benefit of using mutations - getting a history that can be viewed and played back - would be painful to use when needing to navigate every changed value on every frame. So… I just didn’t use mutations. I manipulated the Vuex state directly, even though that’s a really big No-No.
Additionally, I was using Vuex for getters - a lot of getters. More than its intended to use. I also needed to add/remove getters in order to support dynamic layers. Vuex only supports this by using its modules feature - you register a “sub-state” basically, which can have its own setters. This was a bit of a workaround, and would add some unfortunate unusable properties to the player project.
Finally, this is a pretty big library with a lot of features I’m just not using. It’s weird to have a large library that I don’t use most of, and the parts I do use I don’t use correctly. I was primed to look for alternative solutions, and when I migrated to Vue 3 the answer became clear…
Vue 3 allows you to create reactive objects manually! As well as computed values that aren’t attached to components! Those could easily replace my game state and vuex getters, respectively, and support exactly the features I need with no extra footprint - I already needed those parts of Vue for the various Vue components in the project. This ended up working incredibly well and I’m very happy with it.
Adding TypeScript definitions
This brings us back to where I’m at now. Progress has been slow because I’m working on pretty fundamental changes that require revisiting code and understanding it really well, so I can find ways to change it without breaking functionality. This is a much slower process than just adding new features. Unfortunately this current task also falls under that category. Vue 3 supports typescript out of the box, so I’m adding definitions to everything. These will basically allow the program to catch some errors during compile time rather than runtime - it won’t be perfect, but it’ll help catch a lot of bugs, especially from beginner programmers. Most IDEs will also help you while coding by showing you accepted values and maybe even offering higher priority auto complete to properties with the right type.
However, this task can be fairly complicated, particularly with how our proxies work. There’s effectively 2 versions of most objects - one that supports functions that will eventually be wrapped in computed()
calls, and the proxied equivalent that just returns the straight values. There’s a lot of really complex types I need to make to support this kind of stuff, and being as restrictive as I can so the most bugs can be caught in advance. I will say this - it’s already helped me catch some bugs, and change some things to support more use cases.
It’s also been slow for another reason, though. Almost everything in TMT-X can be added upon by the modders. This has the unfortunate side effect that I cannot know everything that will be on every object, so I end up needing to add a [key: string]: any
to all these objects. I’m trying my best to make things restrictive elsewhere, but that line really bothers me, to write and it’s basically everywhere.
Conclusion
Needless to say, progress has slowed a bit due to me getting a full time job, and working on these forums. I’m excited to continue working on the TS definitions though, and I think the pace will accelerate again after that. See you then!