0% found this document useful (0 votes)
8 views5 pages

Guide Scripting

Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
8 views5 pages

Guide Scripting

Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 5

Guide to DC Script Editing

This is a small intro on editing scripts. I’ll approach this from the perspective of a
non-programmer, but someone who has some idea about the game’s basic file structure.
This is meant as a basic overview, but I’ll briefly mention
some ‘advanced’ techniques I used for my mod that have
proven to be quite useful.

Basics
The game’s code is originally written in a scripting language
called DC. It’s meant to make writing and editing scripts
more accessible to designers and other non-programmers.
The language is based on a flavor called ‘Lisp’.

We do not have access to the game’s source code at all. We


only have the compiled .bin files. Compilation is the
process of taking human-readable source code and
translating it into machine instructions.Generally, this
might look something like this (not actual code):

Source:
self.set-int32(secs-final-countdown, 20)

Compiled:
00000000​ 15 01 00 00 00 LookupPointer r1, set-int32
00000008​ 4A 31 01 00 01 LoadStaticU64Imm r49, secs-final-countdown
00000010​ 0C 32 14 00 02 LoadU16Imm r50, 20
00000018​ 4A 33 02 00 03 LoadStaticU64Imm r51, self
00000020​ 1C 01 01 03 04 CallFf r1, r1, 3
00000028​ 00 00 00 00 05 Return r0

Note that what ends up in the actual compiled file are only the numbers (instructions)
starting in the 2nd column.
The source is written by a human while the compiled version is generated by the compiler
and run by some machine, in our case this machine is the game’s engine. So, if we want to
make changes to a script, we have to edit the compiled version.
DC Script binaries
First, we have to get the compiled files. Most scripts you’ll want to edit start with ‘ss-’ for
state script, but some code may be in other files aswell. When you open one of these files in
a text-editor however, it won’t look like what I put in the compiled section above. It’ll be raw
binary data. That makes sense, as the engine doesn’t care about text, it cares about the raw
numbers which are associated with data & instructions.
In order to make sense of the data, you’ll need a disassember. A disassembler takes the
raw binary data that you’ll find inside a .bin file and makes it human readable. Now, that does
not mean it creates source code. That would be a decompiler which does not exist (yet) for
these scripts. You’ll get something that looks like the code above. The only public
disassembler (that I know of right now) is the t2r-dc-disasm by icemesh. You’ll need to build
that project yourself, for which you’ll probably want a tool like Visual Studio.

Once you have disassembled a script, you’ll usually get a big pile of code that you don’t
quite know what to do with. Generally, you’ll get function definitions ‘(defun ‘<xyz>)’, a
header ‘(define-state-script ss-<xyz>)’, including the scripts various properties (location,
initial-state, options, declarations), and several states/tracks/lambdas after the header.

Functions are blocks of code that can be reused (called) throughout this or other scripts.
They’re used when you need to perform some action many times and don’t want to repeat
yourself. Functions can take arguments (meaning they get some input that they ‘process’)
and can return a value (meaning they can give back some output).

As mentioned above, the header defines properties of the script. The most important thing
here are the declarations, which can often give some clue about how the script actually
works, and are also something you might be editing a lot. Variable declarations include a
type, which defines what kind of data a variable can store. You might already be familiar with
ints, floats, booleans, symbols, SIDs, etc.. The can also be initialized with some default
value.

Below the header you might find code like this:​

(state ("assault-setup-level")​
​ (on (start)​
​ (track ("main")

States are just what they sound like, they’re a state that the script can be in, e.g.
“assault-setup-level”, "assault-preparation", "assault-wave-active", etc. You can see how
these directly correspond to states that the game can be in. Scripts can also define a (state
(“--all--”), which, as the name implies, will run no matter the state.
The next line defines the event that this track will run on. Here we have the start event, one
of the most common ones. This means this track will run once when this state is entered.
There is also the update event, which is run once per frame. These are built into the engine.
Custom events can be captured like this (on (event ‘<xyz>).
This means once this script enters the state this event is captured in, it’ll listen for some
other part of the code sending out that event. Once that happens it’ll run the code inside.

Lambdas are the blocks inside tracks that define the actual instructions to be run once the
script hits that point. They consist of an instruction part and a symbol part. The symbol part
is not shown in the disassembly, but it can be viewed in the binary.

Let’s look at one line of code from the disassembly understand what it means:
loc_0 - CODE: 0x00007BE0 15 00 00 00 DC::kLookupInteger/kLookupFloat: R0 R0 -> hashid: 0x381E35AD0689E81D(#381E35AD0689E81D)

Let’s look at each column seperately:


●​ Loc_0: location of this instruction inside this lambda, this is important once we start
jumping around code
●​ 0x00007BE0: the actual location of this instruction inside the binary file. If you open
the .bin inside a hex editor and search for this offset, you’ll find the instructions of the
lambda.
●​ 15 00 00 00: This is the actual instruction. Every instruction is exactly 4 bytes and
another 4 bytes of 0-padding (not shown in the disassembly). Every 4-byte instruction
is structured like this:
○​ Opcode (here 15): The operation to be performed. 15 (read 1-5, not fifteen, as
this is all hexadecimal), corresponds to ‘LookupFloat’.
○​ Destination Register (here 00): The index of the register where we want to put
the result of our operation.
○​ Operand1 (here 00): The first operand. Opcodes might take zero, one or zwo
operands. Here we want to look up the first (we start counting at 0) hash-id
from the symbol table.
○​ Operand2 (here 00): The second operand. The instruction 15 doesn’t take a
2nd operand, so this will always be 00.
●​ DC::kLookupInteger/kLookupFloat: The meaning of the instruction.
●​ First R0: The destination register as explained above
●​ Second R0: The index in the hash table (0)
●​ Hashid: 0x381E35AD0689E81D(0x381E35AD0689E81D): This is the actual symbol
that is located at position 0 inside the symbol table which the disassembler
conveniently displays for us. The disassembler tries to resolve the name of the
hashid inside the sidbase.bin file, but it wasn’t able to do so here, so all we have is a
hash value. The disassembler can also show you other types of values from the
symbol table, most commonly floats.

So, this is already a majority of the stuff that you’ll be looking at while editing scripts. Most
instructions are not super difficult to understand, so I’ll not explain them here. Reading and
writing assembly is a skill you have to learn by doing, so I highly recommend starting small
and making simple changes. Assembly languages are generally quite similar, so you’ll
probably find a good amount of resources online that can be transferred to DC.
Editing
Now that we know the structure of the files, we can do some editing. First of all, you’ll need
to know what you want your mod to do. I’ll use my recent mod for skipping timers as a
starting point.

The first step is finding what script to even edit. In my case, it didn’t take me too long to find
ss-assault-manager.bin (as well as all the other game modes), but it might take a significant
amount of time to find the right one. I also highly recommend having a complete
disassembled version of the codebase somewhere to make it easy to search through. If you
can’t make one yourself, you can ask me to send you one (or the script I used to generate
it).

Once you have your script, you gotta find the right location inside the script. In my case, I
needed to listen to a button being pressed. This has to happen continually, so the natural
choice is one of the (on (update) events. The event inside the “--all--” state is always a
good choice as this will always run, making it easy to see the impact.

Inside this event, you have several lambdas. Fortunately for us, the game contains a very
large amount of debug- or dead code. This is code that we can overwrite freely. The function
‘is-final-build?’ checks if we’re running the final build of the game, which you are if you’re
reading this, meaning all the code starting at that function can be happily replaced as it
would never run in our version anyway. There are also several functions and events that are
defined but never used throughout the games’ code, which we can also hijack for our
purposes. I found a nice big lambda inside the update event that I could overwrite. The
larger the lambda or function, the better, as we can’t extend the script in any way (yet…), just
overwrite the bits that are already there. Again, fortunately for us, the compiler isn’t incredibly
efficient in register usage, so we can often gain some instructions by just moving stuff into
registers more efficiently.

To edit a function you have to use a hex editor. You’ll have to look up the offset of the
instruction where you want to start editing and jump to that location inside the .bin file using
your hex editor. Once there, you can begin replacing instructions with your own. There really
isn’t much more to it. If you need to load symbols that aren’t being called inside the function
already, you have to edit the symbol table. The symbol table is a block of data underneath
the instructions of the function that lays out what data that function uses. You can replace
hashes in here freely, as long as they’re not needed in the function anymore after you’ve
edited the instructions. Once you’ve edited your script, you have to repack it (make sure to
respect the original folder structure), and place the .psarc file in your mods folder as usual.

Messing stuff up in scripting usually means a crash. Some crashes occur immediately on
load, some of them only once the borked code is attempted to be run. From here it’s pure
trial and error. The learning curve is very steep but it’s also incredibly rewarding. If you have
any questions about script editing, I’m very happy to help, but I also just started about 2
weeks ago, so I will definitely not have all the information. Other members of the server will
probably help you out.
‘Advanced’ techniques
This is a list of some more complex editing I had to do for my last mod that I didn’t want to
keep for myself. This section is really only meant as a reference and not as a guide. But I
just wanted to have them written down somewhere. A lot of this stuff will hopefully become
easier to do once there are better tools.

Moving the symbol table pointer


As mentioned above, a lambda fundamentally consists of an instruction pointer and a
symbol table pointer. You can find the location of the Lambda “struct” that contains these
pointers by just searching for the either pointer; there should be a place in the header where
they appear right next to each other.

Sometimes you run into the situation where you have plenty of instructions left that you could
edit in your lambda, but not enough symbols. You could simply try to find another function to
call, but I’ve actually found it very useful to just edit the symbol table pointer. So just look up
the address of the current start of the table, and move it back as far as you’d like. This will
replace instruction space with symbol table space, and allow you to add more symbols.

Redirecting Functions
This one is a bit more complex. Lambdas aren’t really “inside” tracks/states as they appear
in source/disassembly code. In reality, the track contains a bunch of pointers to lambdas.
This is nice for reusability, but it becomes a problem when we want to edit some lambda in
one place, but the script contains another reference for it. We would now be editing two parts
of the code, which might be a very bad time. In order to fix this, we have to redirect the
unwanted lambda pointer to another lambda.
Here are the steps to do this:
●​ Find some other function that would be unproblematic to call at the location where
the script is currently referencing your edited lambda where you don’t want it to
●​ Find the lambda pointer (place where instruction pointer and symbol pointer are
defined)
●​ Search for the address where the lambda pointer is stored
●​ Save the address that is stored at THAT address
●​ Now go to the instruction pointer of the function you want to redirect
●​ Again, find the lambda pointer
●​ Search for the address of the lambda pointer
●​ Replace the value you find there with the value you saved previously

The undesired lambda reference should now be replaced with a harmless lambda, meaning
it’s not referenced more than once. You can similarly use this technique to insert lambdas
into tracks that would usually not have them.

You might also like