-
Notifications
You must be signed in to change notification settings - Fork 9
Definitions
On this page the workings of definitions are explained. This starts by explaining how it works and ends with a description on how to add them yourself
In order to understand how it works one must first understand why they are needed. Therefore it needs to be explained how the game is built.
The game structure is loosely devided into three main parts:
- The actual JavaScript code
- The assets (images, music, etc.)
- Data which connects assets with JavaScript code and mostly consists of JSON files (maps, character configs, etc)
All three parts are built in a different way: While the assets are delivered as they are (or only slightly compressed) the JSON files are minified but not obfuscated. This means that you can restore a readable form by simply using prettifier like JSBeautifier.
On the other hand, all the JavaScript code is both minified and optimized (obfuscated). This means that even if you use a prettifier it will still result in hard to read code.
This example shows an example of how readable code is turned into the optimized form (prettified):
function addEntity(entity) {
if (entity !== undefined) {
ig.game.entities.push(entity);
}
ig.debug("This is a string")
}
is turned into
function ga(a) {
a && ig.q.entities.push(a)
ig.debug("This is a string");
}
There are a few things to note in this example:
- The functionality of the code never changes.
- It is deterministic. The same code will always result in the same output.
- Function parameters are almost always called a, b, c, d, ....
- One name will always turn into the same optimized name. This means that if two entirely different variables are both called
game
they will both turn intoq
. (Note that the optimized name changes every update) - There is a set of names that will never be renamed. The most important names that are not converted are
sc
,ig
,update
andstart
. There are many others and can easily be identified just by looking at the optimized code.
Definitions are trying to find a connection between optimized names and their actual ones. Usually simple string operations can be applied to find the target. However, in the case of CrossCode this is hard to do due to constantly changing names, length and even parts of code that are edited, added or removed. While still possible to do using regular expressions this is hard to do, incosistent and impossible to maintain.
So instead of using string operations it relies on compiler theory.
Every compilation starts with code stored as text. It has not been analysed or processed yet.
In the first step the raw text is converted into words and symbols, the so-called tokens. Inside these tokens, their position in the code is also stored for improved error handling.
For example:
"console.log(\"Test abc\");"
is turned into
"console", ".", "log", "(", "\"Test abc\"", ")", ";"
Then each token is analysed in order to have a basic descriptions of its content. For example, a string will be tagged as Literal
.
After that, the compiler now tries to connect the tokens to each other in order to understand what they mean. This is done by generating an AST, an Abstract Syntax Tree, which has a single root node which can be executed.
For example:
console.log("Test abc");
is turned into
{
"type": "Program",
"body": [
{
"type": "ExpressionStatement",
"expression": {
"type": "CallExpression",
"callee": {
"type": "MemberExpression",
"object": {
"type": "Identifier",
"name": "console"
},
"property": {
"type": "Identifier",
"name": "log"
},
"computed": false
},
"arguments": [
{
"type": "Literal",
"value": "Test abc",
"raw": "\"Test abc\""
}
]
}
}
],
"sourceType": "module"
}
(positions removed for simplicity; Interactive example at ASTExplorer.net)
After that, depending on the type of compiler, it is either executed directly (interpreter), or it is converted into machine code for later execution (compiler).
By using existing libraries CCLoader converts the CC code into an AST. Definitions can use that tree in order to find definitions. It does this using the following steps:
- Iterating over each node
- On each node it checks if the condition for that definition applies. It does this by first checking the
type
property of the node and if that matches it iterates over each of the conditions in thevalues
property in the definition. - On every condition it loads the value defined by name and compares it to the searched value.
For example, if the name is
arguments.0.raw
it first loads theargument
property of the current node, then picks the0
entry of the array and last but not least reads the string stored inraw
- If all conditions match it then tries to load the optimized name from the current node by following the
pattern
. This followes the same rule as the condition names. If the value could not be found it will write it as undefined. - If the definition was found it is removed from the list of definitions to find.
- If any definition is not found yet continue with step 1.
In order to find definitions it is highly recomended to use CCDefedit as it contains algorithms to help you finding them without the need to understand the JavaScript AST.
- In order to make use of the algorithms the game first needs to be loaded. This can be done through
Load > CrossCode
. - You need to identify an optimized variable
- Search for an easy to identify code nearby. The first example on this page is the prime example of this since strings will never change and are often shortly after important pieces of code. (Log functions)
- Put the value of the easy to identify code into the
Pattern
of CCDefedit. If this value is a string, make sure to include the quotes. - Put the optimized name of the target into
Searched
- Click
Search
- If the
Result Pattern
field is empty you need to start again from step 3. and make sure the easy to identify code is unique. - Once found, you need to add a definition you need to put the value of
Result Pattern
intoPattern
,Pattern
into the left field ofValues
and the value of the easy to identify code into the right one - Setting the
Reftype
is important too:
-
static
: This is probably the setting you want. This loads the value of the target from the parent. For example, if the definition targetsgame
and the parent isig
it will load the value ofgame
from theig
object at runtime. This is only possible if the object is already created at the time the definitions are applied. -
dynamic
: This is deprecated and allows the same as static but it is only resolved once it has been called. The downside to this is that it creates a function that has to be called every time the value is needed. -
raw
: This is also deprecated and allows to retrieve the raw name of the target. For example, when queryingname
you will receive"q"
. Instead of usingraw
prefer the runtime option ofentries.<nameOfDefinition>
, which is automatically added even whenstatic
is chosen asReftype
.
- Click save and replace the definitions.db file with the downloaded one.