It’s almost possible to write arbitrary programs in Forge… but not quite. I’d like to post a thought experiment – something that’s almost possible – as sort of a roundabout means of offering perspective on what the current system is capable of, and what we might be able to do with just a few enhancements.
[Note: Forge’s scripting is actually a specific kind of system called a “trigger system,” so I’m going to deviate from Forge terminology just a bit, for clarity. What Forge calls a “script” would more properly be called a “trigger.” Forge otherwise appears to use terminology consistent with more established systems, like those seen in Age of Empires II: The Age of Kings and StarCraft: Brood War.]
It’s almost possible to work around the limit of one condition and four actions per trigger, and the limit itself doesn’t necessarily make it impossible to write meaningful code. Given that any given trigger has access to one private number variable, twenty-six global number variables, and a dizzying array of player- and team-scoped number variables that are available under confusing and seemingly inconsistent circumstances, it’s almost possible to write actual assembly-language programs in Forge. I was even thinking about writing a PC-based tool that could take assembly code and convert it to a spec for a Forge map. 
Consider the notion of one assembly subroutine per Forge object, with subroutines divided across multiple triggers to work around the four-action limit. Your first trigger runs on a Message/Power Multi-Condition, listening for one message received and three power channels. This would be a static method call, but it would resemble a virtual method call in assembly: the message channel is a function table, and the power channel values are a three-bit non-zero index within that table. The first trigger must immediately clear those three power channels and set the local number variable to 1. Subsequent triggers on the same object check that local number variable as their condition, treating it as a program counter (i.e. “how far into this process are we?”); these triggers can run (up to) three opcodes and then increment the program counter. At the end of the subroutine, the program counter must be reset to zero, and some action must be taken to trigger the next part of your overall program.
C++ virtual method call as represented in assembly
mov ecx, esi; <mark>// this = esi, given esi instanceof SomeClass</mark>
mov eax, [esi]; <mark>// eax = function table for virtual methods on SomeClass</mark>
mov eax, [eax + 0x14]; <mark>// eax = fifth function pointer in the table; // 5 * 4 == 0x14</mark>
jmp eax; <mark>// “jump to eax” // esi->VirtualMethod05();</mark>
Subroutine call as represented in Forge
Enable power alpha;
Disable power barvo;
Enable power charlie; <mark>// bin 101 == 5</mark>
Send message alpha; <mark>// SomeClass::StaticMethod05();</mark>
This setup means that all message channels are reserved, but twenty-three power channels remain available for state-keeping. Multi-threading may be possible if more triplets of power channels are reserved for call indexes (one triplet per thread), though subroutines can only be hardcoded to a specific thread (e.g. Subroutine 1 MUST be on Thread 1).
For actual data operations within a Forge subroutine, you would need to reserve some global number variables for use as assembly registers; a multi-threaded program will require more such variables than a single-threaded program. If we want eax, ebx, ecx, edx, ebp, and edi, then three threads cost 18 globals out of the 26 we have available! Programming with a reduced number of registers would be tricky, but could be possible. In limited cases, it may be possible to use variable storage on the player who activated a call stack, but this relies on a call stack only being player-initiated; and it relies on pulling and editing numbers from a designated OBJECTS collection, and the mechanics and availability of these are very, very unclear.
There are a few more complications. Firstly, the concept of a “call” or of a “call stack” is something of an illusion here, in that a subroutine cannot return to a caller; rather, you’d have one subroutine leading into the next, hardcoded, from the start of a thread (i.e. the actual gameplay condition, such as a switch being pressed) to the thread’s end.
The second-largest problem (and the largest problem that doesn’t break everything) is that branching within a subroutine is nearly impossible, since Halo 5’s limited trigger syntax means that we can only check one number at a time. The most that can be done is to write a value plus offset directly into the program counter (e.g. set self number to eax offset by 16 and then have subsequent triggers compare self number to 16, 17, 18, 19,…), and this only allows <mark>((UInt15)eax == const UInt15)</mark> checks; comparisons and not-equal checks are impossible. (Actually, I believe conditions can offset a compared number as well, so for ONLY one branch in a subroutine, > and >= comparisons are possible, maybe along with < and <= for negative numbers in a separate branch. Still, that’s very limited.) For the most part, branches within subroutines must be switch-cases that can be set up in just four opcodes.
(UInt15 isn’t a typo, btw. We get 16-bit numbers and we can’t use the sign bit for this purpose.)
But then, programmers have overcome plenty of larger obstacles; people have even created programming languages that are deliberately limited bordering on (and often crossing into) the obnoxious, just for the challenge of using them. Being limited to one non-switch-case comparison per subroutine isn’t the worst thing in the world, and I’m sure a sufficiently enterprising coder could tolerate it.
However, there’s one problem that just breaks everything. No workarounds.
All triggers on an object are run almost in parallel, in that conditions are checked on all triggers before actions run on any of them. This means that you can’t use “daisy-chained triggers” to get around the limit of four actions per trigger, and this seemingly makes longer programs impossible to write. So much for my “accurately recreate Gen I Pokemon battles in Forge” idea. :\
What would it take to be able to write full programs in Forge?
Literally just the ability to add an arbitrary number of conditions and actions to any trigger. I can’t guess why we’re limited to one and four, respectively; variable-length data structures are fairly trivial from a programming standpoint, and it’s not like it’d be a burden on the netcode; the triggers themselves can’t be modified at run-time, so you only need to synch the data at match start. There’s obviously something I’m missing, and I’m actually curious as to what.
Anywho, this was fun to puzzle through and I figured it might be interesting to any fellow turbonerds out there. Posting it on the web is about all I can do with it, owing to none of it actually working.
Miss you bro.