Forums / Games / Halo 5: Guardians

Programming in Forge: A Failed Thought Experiment

OP DavidJCobb

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. :P

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; // this = esi, given esi instanceof SomeClass
mov eax, [esi]; // eax = function table for virtual methods on SomeClass
mov eax, [eax + 0x14]; // eax = fifth function pointer in the table; // 5 * 4 == 0x14
jmp eax; // "jump to eax" // esi->VirtualMethod05();

Subroutine call as represented in Forge
Enable power alpha;
Disable power barvo;
Enable power charlie; // bin 101 == 5
Send message alpha; // SomeClass::StaticMethod05();

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 ((UInt15)eax == const UInt15) 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.
Small update before I go offline: this system could still work as described, maybe, but if so it would be very slow -- only three opcodes per tick -- and I'm not 100% sure it'd still be thread-safe.
I am not too familiar with forge - but it should be possible to build a simple Turing machine implementation and work off of that, surely?
A Turing machine is just a computer, right? A machine that can theoretically compute anything given infinite storage and power.

I guess all this means that Forge is nearly Turing-complete.
DavidJCobb wrote:
A Turing machine is just a computer, right? A machine that can theoretically compute anything given infinite storage and power.

I guess all this means that Forge is nearly Turing-complete.
I think it is very possible that Forge is Turing complete. If you can store a state (light is on or off) and then compare that state and execute something based on that information then that is a great start. So for example, a series of 8 lights that represent a binary state - if forge could detect when this set of 8 lights are all off then you could create conditional statements. Anything with a binary toggle would be great for storage.

As I said I'm not too familiar with Forge so you would be able to conceptualise how that would work better than I can!
You, my good sir, have given me an amazing idea. What if someone was to make a software that converts your typed program in a familiar programming language such as C++ or JavaScript and outputs what scripts you need, their configuration and on what objects they go.

How nice this would be for people that know a programming language is unexplainable. It would save so much time in the long run.
Well since we are on a programming kick, I might as well through down Cobb. ;) Miss you bro.

My lower level programming skills are off and can't build on your analysis of replicating true programming in Forge. But with what we do have now, we can slowly move towards figuring that out as a community. I've started a project when the update initially came out called the Forge Dev Kit. https://github.com/RayBenefield/Forge-Dev-Kit

Noticing the potential of the system, there is a lot that can be done to potentially create standard programming patterns. I've already created a For Loop pattern that I rely on to circumvent the "64 objects" per action filter. I think one day it will definitely be possible to take something like Javascript, interpret it into Forge Scripts. I look forward to that day, lol... I may actually be part of that initiative.
Probably the most complex thing I've scripted was an item shop. It converts your current score to a player-based variable, which then compares that to a global variable (the item price). If the player variable is greater than or equal to the global variable, it triggers an action, reduces player score, and adds 1 to a different global variable. This allows for progression-based buying (Like buying a PP first, then a PR, then a Plasma Caster, all on the same button).

Is this even relevant? You tell me, cuz y'all too damn smart for me.