Applying hobby knowledge to work
For years now, Iâve been programming as a hobby. Itâs my passion to create, even if the things I make arenât perceived as useful. One thing Iâve noticed is that over the years, you start looking back on older projects as a reference for anything youâre currently working on.
Recently, my employer asked me to finish a plugin he was working on for the CMS we offer to clients. Thereâs a library in the frontend code for the CMS that the creators wrote themselves that looked strangely familiar to something Iâve worked with before.
But before I get to that, I have to explain why this was so familiar.
Classes in early JavaScript
JavaScript didnât get any native support for classes until ES6 (which released in 2015) despite already having the new
keyword which could call a function and return a new object with said function as the .prototype.constructor
property.
var a = function (name) {
this.name = name;
this.sayName = function () {
console.log('I am ' + this.name);
};
};
var Class = new a('Alyxia');
Class.sayName();
// => I am Alyxia
In 2006, Dean Edwards (who I can find surprisingly little about), published Base.js, a single file you could add to your website to ease the process of creating class-like objects in JavaScript.
var object = Base.extend({
value: 'some data',
method: function () {
alert('Hello World!');
},
});
object.method();
// ==> Hello World!
This way, anything that calls extend
on Base
, is also inherently extendable. So in this example, object.extend(...)
would be completely valid, and the resulting object would inherit any methods and properties from object
.
This system worked as follows, where A
is Base
and C
is your class definition:
- Upon calling
B = A.extend(C)
, copy all properies and methods ofC
into a new object - Set
C
âs prototype to that ofA
- Link the inherited methods from
A
intoC
It sounds complex, but in practice, itâs dead simple and quite intuitive.
Simple Inheritance
In 2008, John Resig, who is best known for his work on world-renowned library jQuery, wrote an entire yet simple inheritance system in JavaScript for the in-progress book he was writing.
After seeing many attempts at âclass systemsâ in JavaScript, two were his favourites, namely Dean Edwardsâ Base.js, and something Iâd never heard about before called Prototype.
Resig wrote a blog post about his attempt here if youâre interested in the way the thing looked. All in all itâs pretty similar to Base.js, except Resigâs attempt had a considerably shorter implementation for the extend
function.
Impact
The majority of game engines in the modern day are written in object oriented languages. This was no different back in 2010, with well known engines like Unity and Unreal Engine being written in C++. To clearly separate concerns in your game, itâs typically recommended to use classes as well. (e.g. a base class Entity
that Player
and Enemy
extend from)
In late 2010, Polish developer Dominic Szablewski, otherwise known as phoboslab, open sourced v1.14 of his game library Impact.
Impact, which is largely a âbring your own glueâ game engine, was obviously also written before JavaScript had any coherent concept of classes. Thatâs why Impactâs core file directly included John Resigâs implementation of the class system. Basically every system in the game, like entities, the input manager, the sound system, collision/background maps, were all written using ig.Class.extend(...)
.
This meant that, for example, fonts were simple to implement. Fonts are image files. The font specific functionality is in ig.Font
, which is defined asâŚ
ig.Font = ig.Image.extends(...)
So even in Impactâs own source code, it makes use of this system to separate concerns. The handy thing about this is that game developers could do the same thing; create their own extendable classes or extensions of existing ones.
There was one core difference in comparison to Resigâs implementation that Szablewski added himself: ig.Class.inject
.
Injecting into classes
What made Impact so different is its ability to inject into classes at runtime. The best way to explain is to just give an example:
ig.Person = ig.Class.extend({
init: function (name) {
this.name = name;
},
sayName: function () {
console.log(this.name);
},
});
var me = new ig.Person('Alyxia');
me.sayName();
// => Alyxia
ig.Person.inject({
sayName: function () {
console.log('Hello, my name is ' + this.name);
},
});
me.sayName();
// => Hello, my name is Alyxia
To explain:
- Create a class named
Person
with a method that logs out the name - Create a new instance of
Person
namedme
with nameAlyxia
- Call
sayName
, which logs out the personâs name - Inject into
ig.Person
to change the functionality ofsayName
, permanently altering all existing and future instances ofPerson
- Call
sayName
again, showing our new output
Technically, if you were to build a game with Impact, your game is inherently moddable.
Speaking ofâŚ
CrossCode
CrossCode is an action RPG released in 2018, which started development in 2011.
Iâve been in love with the game since I discovered it, and as such Iâve been a part of the modding community since late 2018. After people discovered how to run their own code before/during the game⌠modding became extremely easy. This is because it used Impact as the game engine. 95% of the gameâs core functionality is defined in Impact classes that are accessible through the global objects ig
and sc
.
Nowadays, a good chunk of the mods for the game are written in TypeScript. The requirement for comfortably being able to access stuff on ig
and sc
, and additionally call extend
and inject
on any classes, is writing type definitions for everything.
Community member dmitmel made great effort toward this goal during the development of the first big TypeScript mod, crosscode-ru, which is a Russian translation of the game. To properly mod the game in TS, he wrote type definitions (labeled the âUltimate CrossCode Typedefsâ, later in this post referenced as UCCTD) which eventually resulted in a single repository specifically meant for containing typedefs for the entire game.
The most important and core part of these typedefs are the definitions of Impactâs class system.
For global classes in the game, like sc.ItemListBox
, we can define them like this:
declare global {
namespace sc {
interface ItemListBox extends ig.GuiElementBase {
list: sc.ButtonListBox;
clear(this: this, skip?: Nullable<boolean>): void;
addButton(this: this, gui: ig.FocusGui): void;
getChildren(this: this): ig.FocusGui[];
}
interface ItemListBoxConstructor extends ImpactClass<ItemListBox> {
new (
topPadding: number,
noHeader: boolean,
buttonInteract: ig.ButtonInteractEntry,
): ItemListBox;
}
var ItemListBox: ItemListBoxConstructor;
}
}
Here we see a couple things:
- The
ItemListBox
interface properly extends fromig.GuiElementBase
, which is defined elsewhere in a similar fashion. Second, all the properties in this interface will be available to instances of the class. (so after callingnew
on it) Third, theItemListBoxConstructor
acts as our âcallable constructorâ and contains the arguments we pass to the constructor. Last, theItemListBox
var is a reference to our constructor and is the thing that eventually appears on thesc
namespace.
So, now weâve got type definitions for sc.ItemListBox
so we can call it as normal code, with type checking. Why am I highlighting this project though�
The actual gist of the post
At work, we use CraftCMS to build websites for customers. The point is that we architecture the CMS in such a way that eventually, customers can completely self-manage their websites content-wise, so we wonât even have to intervene with over 60% of changes they want made.
Craft supports plugins. Thereâs many companies and organisations making these plugins, but itâs perfectly normal for a regular developer to make their own plugin for some niche feature if it doesnât already exist. We had to do this for a couple customers, namely store owners, so they can specify their storeâs opening times in a nice table and add exceptions to those specified times.
So, I was put to work. I got most of the functionality out of the way pretty easily since one of my coworkers had done the majority of the work before I got started on it, but the main problem now was rendering nicely editable tables with time selectors so people authoring one of these opening hour tables wouldnât have to type so much manually. Additionally, it had to be possible to create a new period below an existing one so you could, for example, say that from January through March you were opened from 8am to 5pm, and from that point onwards from 9am to 4pm.
To create one of these editable tables, most plugin developers use Craftâs very own Craft.EditableTable
which conveniently exists on the window
. Cool!
As clueless me traveled to the Craft GitHub repository to take a look at the source code for this thing, I stumbled across something weird. The definition of the object looked likeâŚ
Craft.EditableTable = Garnish.Base.extend({
/* ... */
});
Huh. Thatâs weird. Whatâs Garnish? I delved further and further through Craftâs AssetBundles until I found a folder labeled garnish
, and looked in there. After a bit of poking through there, I found a file called Base.js
, which started withâŚ
/*!
Base.js, version 1.1a
Copyright 2006-2010, Dean Edwards
License: http://www.opensource.org/licenses/mit-license.php
*/
Ah. Well, isnât that interesting.
Now, this is all fine and dandy, but my Craft pluginâs frontend logic is using TypeScript. If only there was a solution to this problemâŚ
The Ultimate Craft CMS Typedefs
When I saw the way Base.js classes worked, I immediately knew I could cobble something functional together by referencing UCCTDâs Impact class definitions. There was just one striking difference between Impact classes and Base.js classes: the second argument to extend
. This is what allows the developer to declare static properties on a class. (.extend({ /* instance properties */}, { /* static properties */})
)
Impact classes do not have this, so I couldnât just use UCCTDâs definitions out of the box. I ended up turning to my good friend Nax who eventually figured out how to properly type this functionality. After this was done, I created the empty stub type for Garnishâs Base class, and with that, I can now write typings for Craftâs classes.
The way theyâre defined is pretty much identical to what Iâve shown off for UCCTD, with two differences:
- naming (
ImpactClass
becomesBaseClass
) - two generics instead of 1 in
BaseClass
(BaseClass<Instance, Static>
)
And thatâs how I applied knowledge I garnered from hobby projects over the years to something I actually need to do in my work. The overlap is just a small coincidence, but itâs still really cool to me.
Honestly, for one of my first long-form posts, this is awful. Way too much talking for a point I couldâve explained in a <=100 word post on some social media. This post also took me several months to finish due to how unmotivated I was to write it. Let it be known that posts like this are not my forte, but I just wanted to write about something. Hope it turned out OK.