A fork of RyanJGray's DevTools to provide runtime utility in OpenCAGE to improve workflows for modders working in Alien: Isolation.
-
Hot reload - press a key (INSERT by default) to restart the current level.
-
Debug text - makes the
DebugTextandDebugTextStackingscript entities work. Retail builds of the game disable these entities and strip the code that draws them, so the ASI re-enables them in memory and draws their text itself:textfollowed by any linked or non-default inputs in brackets, positioned byalignment, styled bysizeandcolour, hidden afterdurationseconds (or kept untilstopwhen the duration is -1, live-updating its inputs). Stacking text is a block on the middle left, newest at the bottom, five entries deep. Everything clears on level change.DebugPositionMarkerdraws XYZ axes at itsworld_poswhile started, andDebugEnvironmentMarkerdraws itstextthere in itscolourandsize. -
Zone loading - the game only streams in the zones around the player and whatever the camera's view ray lands on, so a free camera (e.g. Cinematic Tools) can fly into areas that never load. With
LoadAllZoneson, every zone of the level is registered as viewed each frame and streams in. The ASI also exportsOpenCAGE_SetForceZoneLoading(bool)so other injected tools can switch this on while their camera is active; Cinematic Tools does so. Live Link's camera sync switches it on too while OpenCAGE drives the camera. Each of these only turns its own request on and off, and every zone streams in while any of them wants it (so Cinematic Tools turning its camera off leavesLoadAllZoneson). -
Live Link - OpenCAGE edits the scripting of the level the game is running, without a reload: entities added, removed, re-parameterised or moved in the editor change in the running game, and the entity inspector can call an entity's methods (
start,trigger, ...) in it. The game camera can also follow OpenCAGE's 3D viewport camera, or the viewport the game camera, and the CAGEAnimation editor's Animation Mode can hold or play an animation in the game; OpenCAGE's flowgraphs can light up the links the game's scripts follow as they run. It serves a WebSocket on127.0.0.1:8765(local connections only) that OpenCAGE connects to (launch the game from OpenCAGE with Enable Live Link ticked; OpenCAGE connects by itself, and its Live Link button next to Launch Game turns it off and on). See Live Link below.
Settings are read from OpenCAGE_Utils.ini next to AI.exe. The file is optional; every key falls back to its default.
[RuntimeUtils]
HotReload=1 ; 1/0 - reload the current level on a key press
HotReloadKey=INSERT ; a key name (INSERT, DELETE, HOME, END, PAGEUP, PAGEDOWN, F1..F12, A..Z, 0..9)
; or a Windows virtual-key code, decimal or hex (e.g. 0x2D)
DebugText=1 ; 1/0 - DebugText entities draw their text on screen
DebugTextStacking=1 ; 1/0 - DebugTextStacking entities draw their text on screen
DebugEnvironmentMarker=1 ; 1/0 - DebugEnvironmentMarker entities draw their text at a world position
DebugPositionMarker=1 ; 1/0 - DebugPositionMarker entities draw axes at a world position
LoadAllZones=0 ; 1/0 - stream every zone of the level in, not only those around the player
LiveLink=1 ; 1/0 - let OpenCAGE edit the running level's scripting and call entity methods
LiveLinkPort=8765 ; the local port OpenCAGE connects to
LiveLinkCamera=1 ; 1/0 - let OpenCAGE's viewport camera drive the game camera when it asks (camera sync)OpenCAGE_Utils.log is written next to AI.exe each run: what Live Link did, and every DebugText / DebugTextStacking that started, with its text.
The game keeps a level's COMMANDS.PAK in memory exactly as it is on disk (the entity manager reads the whole file into one block), and a composite template is the file's composite record, its arrays being offsets from the start of that block. So OpenCAGE sends a composite as a one-composite PAK image (written by CathodeLib), with the positions of the offset words in it; the ASI copies it into memory it never frees, rebases those offsets onto the block (32-bit wrap-around reaches anywhere), turns the parameters into the game's variable objects as the game does when it loads the file, and points the template at the new arrays. It then brings every running instance of the composite in line with the change, working out the difference against what the game is running:
- entities (and proxies) that went are reverted and shut down, taken out of their zones and removed from the instance;
- new ones are constructed as entities or proxies by the entity manager (which builds a whole instance for a composite), added, initialised and validated, and put in the instance's zones - as the game does when it creates and initialises an instance's entities;
- entities whose parameters changed - directly, or through an alias (an override of something inside a nested instance) - have their parameter cache flushed and are live edited (put back in their initial state, repositioned, and left to react to the edit themselves), which is what CA's own live link did in the 2014 development build;
- every entity's cached parameters and links are flushed, so link changes are picked up.
Method calls are queued as triggers the way the game's own code queues them, so they run on the next frame like a link firing. Everything that touches entities runs on the entity thread, in a hook on the entity manager's per-frame processing. A composite that is not in the running level (a new one), and new models or other resources, need the level saved and loaded again.
Because a live edit puts an entity back in its initial state before the entity reacts to the edit, something that was started (a marker, a Thinker, a looping effect) stops when it is edited, unless it starts on reset: start it again.
Edits and method calls are held while the level's scripts are not running - the entity manager's script transport reads paused (1) from a level's load through its intro and opening cutscene (the game queues its own triggers then) and pause menu (2) while the game is paused, rather than running (5) - and carried out once they have run again for 5 s (a level start can let them run for a moment between its pauses); after 20 s of waiting they are turned away ("still starting" / "paused") and not carried out. OpenCAGE sends its own edits again by itself, and a method call from OpenCAGE waits for the edits made before it; anything else has to be sent again once STATUS says playing=1. Edits and calls for a level the game is not running are turned away at once. Each change of the transport is written to the log. Edits and calls pushed while a level was starting were found to be able to leave the game on a black loading screen for good. STATUS reports playing (edits are taken now), loading, and the game camera's position and facing.
Camera sync. With Live Link Camera (above OpenCAGE's 3D viewport) on "Sync viewport camera to game", OpenCAGE sends its viewport camera (CAMERA: position, forward, up and vertical field of view, in the game's world space) and the game renders from it. The ASI hooks the game's hand-over of the camera it picked to the renderer and the sound listener: it writes OpenCAGE's pose into that camera's state, lets the hand-over run, and puts the camera's own state back straight after - so the rendered frame (and what is heard) moves while the camera behaviours keep the game's camera. What the hand-over passes on stays OpenCAGE's until the next frame, though, so game logic that reads the rendered view follows the viewport too: camera viewcone triggers, the current camera FOV script value, the alien's checks for being in the player's view and camera-direction AI conditions. It works in gameplay, cutscenes, the pause menu and while a level starts: CAMERA is stored and answered at once, never held like edits. The pose applies while the OpenCAGE connection that sent it lasts and the level it was sent for is running - disconnecting gives the game its camera back; while no level or another level is running it waits, and applies again when its level is back - and a new pose is turned away for another level, when no level is running, or with LiveLinkCamera=0 (OpenCAGE sends its latest pose again every 2 s while the sync is on, so the camera picks up once the level is there). Every zone of the level streams in while it applies (see Zone loading), as the game would otherwise only stream the zones around the player. The overlay shows "Camera: following OpenCAGE", the log each change, and STATUS camera_sync=1. Cinematic Tools hooks the same hand-over (after this ASI does): the two chain, and while OpenCAGE drives the camera its pose wins (Cinematic Tools' camera shows again once OpenCAGE lets go).
On "Sync game camera to viewport" it goes the other way: OpenCAGE asks for the game's camera (CAMERA_GET) and puts its viewport camera there - position, facing and vertical field of view - taking no camera control of its own meanwhile. The same hook keeps the state of the camera it hands on every frame, before any pose of OpenCAGE's is written over it, so the answer is always the game's own camera as of its last frame (Cinematic Tools' while its camera is on): position, unit forward and up, field of view and a frame counter. It is answered at once, and turned away when no level or another level is running, with LiveLinkCamera=0, and while no frame of the running level has been drawn from a camera in the last second (a level loading).
Animation drive. In Animation Mode, OpenCAGE's CAGEAnimation editor drives the animation in the running game (ANIMATION): it holds it at the playhead while that is scrubbed, plays it from a time (at a rate, looping or not, and running its event tracks or not), and gives it back afterwards. The request is stored and answered at once, never held like edits, and the entity thread carries it out every frame, straight after the queued requests and before the game's own processing - so a keyframe edit pushed in the same frame, which binds the animation afresh, is held at the playhead before the frame is drawn. It finds the animation by the instance path, as a method call does (or in every running instance of its composite), and applies the time with the game's own function that applies an animation at a time, so every track the level binds to it behaves as it does in the level. If the game is playing the animation, only the flag that has the game advance it every frame is cleared - it is not stopped, and nothing fires. A hold never runs the event tracks, and neither a hold nor play ever reaches the animation's length, where the game would finish it and fire its finished and interrupted outputs into the level. Giving it back puts back its own times and flags (as the level's logic left them, if a script started or stopped it while it was driven), applies its own time again and lets the game advance it again if it did before - unless the level's logic started or stopped it since the drive last looked at it, in which case it is left just as that left it. An animation the game showed nothing of when it was taken (not started, not advanced, at its start - one it never applied, say) also has the entities it moves put back as they were then, since its pose at its start is not what they showed (another animation may have moved them): an entity's transform is set back exactly (one the game could not give then is left where the animation's own time puts it), and a track's number is put back under what its entity shows, which reads it the next time it reads its parameters. Giving it back happens on a release, when another animation is asked for, and when OpenCAGE disconnects, while a level that goes takes the animation with it. Nothing is applied or given back while edits must wait (a level starting, the pause menu, a cutscene or a message on screen): ANIMATION_GET then says state=waiting and why. Disabled animations, cinematics (an animation that plays a cutscene clip) and animations whose zone is not loaded are not taken, and ANIMATION_GET says so; one already taken is driven on through up to a second of its zone not being loaded, counted over the frames that look at it (with the game camera away from the player, the zone's flag drops for a frame or two at a time - the game itself applies an animation whatever its zone when a script stops or refreshes it), and only then waits for it. The log has a line for each animation taken, taken again (the level's logic changed it) and given back, with its state flags and times; the overlay notes when OpenCAGE takes an animation and gives it back, and STATUS says animation=1 while it has one taken (held, played, or waiting to be given back). ANIMATION_GET's sequence is that of the last request a frame applied, so OpenCAGE can tell when its latest one shows.
Script activity. With Activity on in a composite's flowgraph, OpenCAGE lights up the links the running game's scripts follow. It tells the game which composite instances it shows (TRACE: a composite and the instance path to it from the level's root, or every instance of the composite, up to 512 at a time) and takes what the game noted several times a second (TRACE_GET). The ASI hooks four things in the game, only to read: the one function every relay goes through, which fires an output of an entity and follows each logic link out of it (a composite instance firing one of its own pins goes through it too, as does a proxy); the game's readers of each kind of parameter value - once a reader has read, the links its entity's parameter cache holds for that parameter, read under the game's own lock on the cache, are the data links the read followed (at most 64 for one pin - a pin with more is counted in the log); the game's lookup of the entities attached to a pin, which every write of an output through its data links goes through (and which goes on, step by step, through a composite instance's pins), its links read from the cache the same way (a link from an entity to its own composite's variable is not kept there, so a write through it is not seen); and the entity manager's queueing of each method call a fired output's logic link makes (a method OpenCAGE calls itself, with CALL_METHOD, is noted too, with no caller). Each activity is kept when either end sits in a watched instance, or is one: an entity firing an output, reading a parameter through a link, sending a value out through one (both with the entity and pin at the link's other end), or having a method called through one (with the entity that called and its output), told by its id, the composite holding it, the instance path to that and, for an instance, the composite it is an instance of. They are added up between takes - one record per distinct activity, with how many times it happened and how long ago it last did - and a take hands them all over and starts afresh; at most 4096 are kept between takes, and any more are counted as dropped. Records are binary (see TRACE_GET in LIVE_LINK_SERVER.h). While nothing is traced the hooks only check a flag; while tracing, the composites of an entity's owner (and of the entity, for an instance) are checked against the watched ones (a hash set, so the check takes no longer with more watches) before anything more is read, every read of the game's memory is guarded (a fault drops that one activity, and is logged once), and the hooks, which run on several of the game's threads, share their records behind a reader/writer lock. TRACE and TRACE_GET are answered at once, never held like edits; tracing is for the level it was asked for (another level running turns them away - and a TRACE turned away for that stops the trace that was running), and belongs to the connection that asked - a disconnect stops it. The log has a line when tracing starts, and when it stops or changes, with how many of the game's calls it looked at and the time it took them; STATUS says trace=1 while this connection traces.
Messages are binary, little-endian: u32 'OCLL', u16 version (1), u16 command, u32 request id, then the command's data (see LIVE_LINK_SERVER.h). Each is answered with the same header (command | 0x8000), u8 success, a u32-length message, and any data. Commands: STATUS, CALL_METHOD, APPLY_COMPOSITE, LOAD_LEVEL, SCREENSHOT (the back buffer to a .bmp), DESCRIBE (a composite's running instances), CAMERA (a pose for the game camera to render from, or to let go of it), CAMERA_GET (the game camera's own pose, as of its last frame), ANIMATION (hold or play a CAGEAnimation, or give it back), ANIMATION_GET (what the game did with it, as of its last frame), TRACE (which composite instances to note script activity in, or stop) and TRACE_GET (the activity noted since the last take). The older JSON text message {"version":1,"load_level":"..."} still loads a level.
Build OpenCAGE_Utils.vcxproj as Release|Win32. When building from the command line pass the project directory as the solution directory, e.g. msbuild OpenCAGE_Utils.vcxproj /p:Configuration=Release /p:Platform=Win32 /p:SolutionDir=<path to this folder>\. The output is build/OpenCAGE_Utils.asi, loaded by the bundled winmm.dll ASI loader (which OpenCAGE copies into the game folder as d3d11.dll).
The game offsets used by the hooks are for the Steam retail build of AI.exe.