Vision: An Omnified Game Overlay Platform

Vision is a game overlay platform that provides visualized data for an Omnified game; moreover, it is the culmination of the Omnified experiment as a whole. It is very much the cherry on top of all the other technologies created during this quest of ours to hack games into being near impossible to beat.

For every Omnified game hacked, new techniques and challenging gameplay systems were developed. The complexity of these gameplay systems grew to a point where it became desirable to have a way to communicate to the player (or, luck willing, a captive audience) everything that was happening behind the scenes.

This desire to be able to see the inner machinations of these Omnified game systems brought about a nascent messaging system. This more or less worked by displaying messages on stream through primitive window captures of various console windows busy monitoring the tail output of log files. Not ideal!

In order to truly provide an evocative and pleasing gameplay viewing experience, work on Vision soon began. Built on C# and WPF, it was meant to and succeeds in providing a much more visually pleasing presentation of data than what a hodgepodge of ugly console windows gives you. It also was something I wanted to have in a completed state before moving on to other projects.

And in a completed state, it is! Such a work deserves a bit of a writeup, and this article here will serve as just that.

The Hacks

Vision does not exist within a vacuum. At the highest level, there are two components to the system providing the on-screen experience to the user: 1) the Vision platform, which is a consumer of game data, and 2) the Omnified hacking framework injected into the binary game target, which is the producer of said game data.

Omnified code injected into a binary, exporting captured game data into message files that are parsed by Vision and its modules.
Omnified code injected into a binary, exporting captured game data into message files that are parsed by Vision and its modules.

The data displayed by Vision originates from data collection efforts of Omnified game-neutral systems (such as the Apocalypse and Predator systems) injected in the game’s binary. Data collection code consists of assembly instructions that copy the desired data to known symbolic addresses in memory.

The data is collected during the course of the relevant game-neutral system’s standard activities, and can be found at the appropriate system’s assembly file location in the Omnified hacking framework’s system directory. Since the only data we’re interested in visualizing is the data we’re affecting, almost all the data required for a proper Vision experience can be found in these game-changing routines.

apocalypse.asm: Here is some example data collection code found within the Apocalypse system's code.
Here is some example data collection code found within the Apocalypse system’s code.
Data needs to be communicated

Simply storing all this data in memory is not enough for Vision‘s purposes; rather, we must also organize the data into reportable chunks (i.e., messages) in a format understood by Vision. Fortunately for us, this is exactly what the Omnified hacking framework’s messaging library does.

While the injected assembly code is modifying the behavior of the game, higher level functions are being executed in a number of Lua modules that are also a part of the Omnified hacking framework. These are responsible for things such as periodically checking our known “data dump” locations and spooling them into publishable messages.

messages.lua: Here is some example messaging code found within the Omnified hacking framework.
Here is some example messaging code found within the Omnified hacking framework.

Schemas for the various message formats are defined with Lua code and found in files such as apocalypseMessages.lua and statisticMessages.lua. The message data objects are constructed and encoded into JSON by the Omnified messaging library found in messaging.lua.

Once all is said and done, we end up with data written to message files, and this data needs some damn visualization.

Omni Dab!

That’s when the Vision platform takes over.

The Platform

Vision and all of its assorted components are Omnified products that can be found within the Bad Echo technologies source repository.

It consists of a main application and a collection of plugins (referred to as Vision modules), each of which is responsible for displaying a different type of Omnified game data. The Vision application hosts these modules, laying them out on a transparent overlay which it places on top of a game.

The application runs on Windows Presentation Foundation, augmented by the Bad Echo Presentation framework. A fully transparent window is created, covering the designated screen (where the game is running) and configured to pass through all input events (we don’t want to interfere with the game!).

On this overlay window, custom layout functionality is present, which attaches detected Vision modules to particular places (referred to as anchor point locations) onto the screen. Different games consume different amounts of screen real estate, so the layout system for the modules was designed to be sufficiently configurable.

In addition to managing the physical orchestration of the visual data being presented, the host application also provides the messaging system that feeds the individual modules with the data they are to display. While the application lacks any understanding of how to parse the message data, it does know takes care of all the details in regards to the monitoring and retrieval of the data deemed relevant for display.

Anchor Point Layout

Vision‘s main window is essentially a collection of module-derived views; these views must be laid out in a sensible fashion, lest they impede us from being able to see important “vanilla” game information. Keeping in line with this desire, the notion of anchor point locations, and the attachment of said module views to them, is introduced by Vision.

The various anchor points as they would appear on your screen that Vision attaches its modules to.
The various anchor points as they would appear on your screen that Vision attaches its modules to.

Windows Presentation Foundation comes with a number of stock layout panels; the layout pictured above may look, even to the most neophyte of WPF developers, like something easily achievable with a Grid. However, this is not actually the case, as the ability to attach multiple elements to the same anchor point location became a requirement during Vision‘s development.

This flies in the face of how a Grid generally operates, whose specific row and column composition is typically defined declaratively in code and consequently set in stone. Using a Grid for Vision’s root layout panel would require dynamic column and row definition hackery; this would be a rather brutish way to go about doing things.

A custom layout panel is required

If multiple elements need to be able to be attached to the same anchor point location, then what we really need is a custom layout panel which not only recognizes anchor point location designations, but is also able to arrange elements attached to one anchor point location independently (space willing) from elements attached to another.

New schedule!

To accomplish all of this, the AnchorPointPanel custom layout panel was created. Not having written many of my own custom layout panels before (I’m sure less than 1% of people who have developed with WPF ever have made their own), it was a bit of an undertaking; however, I was very pleased with the results, which more than sufficiently satisfied Vision‘s layout requirements.

A proper layout system is important and all, but only if you actually have something to display to the user. That’s where Vision‘s module extensibility system comes into play.

Module Extensibility System

The chief responsibility of Vision‘s main application is to provide the system for hosting the pluggable modules doing the actual visualization of a particular type of Omnified data. The underlying plugin system is powered Bad Echo’s Extensibility framework, which itself uses MEF2.

Modules are loaded during the assembly of the root Vision window’s data context. By default, the module plugin files are discovered by looking through the .\plugins directory, whose location is relative to that of the executable for Vision. This of course, can be changed via standard configuration settings exposed by Bad Echo’s Extensibility framework.

Here we see two comfy Vision modules, nestled inside a plugin directory.
Here we see two comfy Vision modules, nestled inside a plugin directory.

The framework for creating a Vision module plugin is provided by Vision‘s own Extensibility library. Modules simply reference this library and export an implementation of the IVisionModule interface and, lo and behold, they will be loaded by Vision as a plugin.

Modules are configurable

At the time of a module’s initialization, its IVisionModule export is provided with Vision‘s application configuration, found in the settings.json file located alongside Vision‘s executable. This configuration contains not only global application settings, but also module-specific ones.

An example of a module-specific configuration setting is the anchor point location for a module. Even though a module has a default anchor point location defined in its IVisionModule export, it is the configuration provided through Vision that (assuming the location setting is present) will ultimately decide which anchor point the module ends up being attached to.

Let’s look at an example configuration file:

settings.json for two modules
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
  "launchDisplay": 0,
  "leftAnchorMargin":  "0,140,0,0",
  "titleLocation": "TopLeft",
  "messageFilesDirectory": "..\\..\\hacks\\targets\\nioh2",
  "modules": {
    "BadEcho.Vision.Statistics": {
      "location": "TopLeft"
    },
    "BadEcho.Vision.Apocalypse": {
      "location": "BottomCenter",
      "maxMessages": 3,
      "effectMessageMaxWidth": 650
    }
  }
}

Module-specific configuration sections are found underneath the "modules" property, the specific module being denoted by the name of the assembly containing the module. In the above example we can see anchor point locations of "TopLeft" and "TopRight" configured for the Statistics and Apocalypse modules respectively.

Like all Bad Echo technology products, the configuration file itself is hot-pluggable, meaning that any changes made to it will be applied immediately, without having to reload the application.

Omni Suspicious...

So, we have an appropriately designed layout system and the means to load the plugins that will populate said layout. That’s still not enough for Vision to be useful, however, as there’s one critical component still missing: the means to provide exported Omnified game data to a loaded module for rendering.

Let’s take a look at the final piece of the platform: Vision‘s message file system.

Message File System

We could have the coolest looking set of Vision modules, all perfectly laid out on Vision’s overlay so that our ability to view the game’s normal UI isn’t impeded; but, without some actual exported Omnified data for these modules to render, we’ll end up with nothing but a blank overlay.

In a previous section, we saw how the Omnified hacking code injected into the target binary is responsible for collecting and dumping data of interest. It’s up to Vision‘s main application to load this exported data, provide it to the relevant Vision module, and then monitor said data for future updates.

Given the primitive nature of the code responsible for exporting Omnified data, we’re limited to primitive means of communicating it; namely, by use of the filesystem in the form of message files.

Each Vision module has its own message file, whose name is defined by the module within its exported IVisionModule implementation. Vision locates this message file, and then begins to monitor for changes, publishing said changes to the particular module interested in it.

Vision is data agnostic

Vision‘s message file system is actually completely data agnostic; it has no knowledge or understanding of the particular format that the message file is using. It’s the modules themselves that understand the data format. All the Vision platform needs to do is to be able to read the content and pass it on to the correct place.

Other than knowing where to read the content from, the only thing Vision needs to be cognizant about is the manner in which updates are being written to the message file. For some types of Omnified data, there can only exist a single instance of said data at any given point in time. For others, the data grows over time, and is more event based.

Incremental vs whole message file updates

When a message file is written to, Vision will process either the entire file, or just the content added to it since the last read. This difference in behavior is basically the only way Vision treats different message data…well, differently!

Typically, any kind of data that can be construed as some sort of historical record, or a compilation of events (if you will), probably isn’t intended to have all of its past entries processed every single time it gets a byte is appended to the file.

Vision determines which processing behavior it’ll use by looking at a module’s IVisionModule implementation, which knows best how its own data should be fed to it. More concrete examples of this will be provided in the upcoming sections on some specific Vision modules.

We’ve covered the platform, let’s take a look at it in use

The stars of the show are the Vision modules. They’re the components that actually paint the Omnified data on the screen.

And now that we’ve sufficiently covered the platform for Vision, let’s take a look at the modules that were fully implemented at the time the Omnified main experiment came to a close. These modules were successfully able to augment an Omnified game so that it looked like the following:

A full shot of Vision running on top of Omnified Nioh 2.
A full shot of Vision running on top of Omnified Nioh 2 (click to enlarge).

The Statistics Module

The first of Vision’s modules which we’ll be taking a look at, and indeed the first module made for Vision, is its Statistics module.

This module is responsible for displaying, rather beautifully on screen (I’d hope), raw game statistics exported from an Omnified game. The inspiration for this stems from one of the earliest “next-level” ideas that sprung forth from my work hacking games: the need to be able to display to the viewer some of the otherwise unavailable game data on screen.

Plus, Omnified games often deal with ridiculous numbers (of the kind that are bad for the player’s longevity when too big, typically), and I wanted those numbers visible.

All preferably in a fashion that was both easy to read and nice to look at. This was not the case initially, as the best I could manage (I was working fast!) was a window capture of a notepad-like program that would slowly refresh with the updated contents of a dumped statistics file.

With Vision, things look a whole lot better

Referencing the image provided above, here’s where the Statistics module resides:

A closeup of the Statistics module for Vision.
A closeup of the Statistics module for Vision.

Beautiful, floating, and easily readable game statistics text, made possible by a customized outlined text element control (part of the Bad Echo Presentation framework) and, of course, the Vision platform!

Not only is it nicer looking, but it is also much more functional as well. Before Vision, the on-screen display of Omnified stats would maybe take two or more seconds to reflect updated values from the game. Not so anymore, as Vision is able to show hacked game data in near real time:

The Statistics module's stamina, shown side by side with the game's own meter, accurately reflecting changes to the player's stamina.
The Statistics module’s stamina, shown side by side with the game’s own meter, accurately reflecting changes to the player’s stamina.
The custom technology required to make the above functionality possible is many and varied

I won’t be doing a deep, deep technical dive into everything that was done; the source code is available, so curious minds can find all the answers they desire by consulting that resource.

But all that aside, it definitely goes without saying that the various systems that were described earlier, like the message file system and the Omnified hacking framework itself, etc., play a big role in allowing such a smooth real-time reporting experience of hacked data.

There is a framework and design adhered to by the Statistics module itself as well, however — we’re not dealing with simple “name and value” pairs here. So, with that being said, let’s pull back the curtain a bit and take a look at what a “statistic” actually is.

Types of Statistics

As you may know, the Omnified process employs systems that are able to reshape gameplay experiences using totally game-neutral code. It should come as no surprise then, that Vision and its modules were also designed to be game-neutral.

That means, if a game is Omnified, it will completely work with Vision without requiring any kind of changes or updates to Vision at all. The (game-neutral) statistics provided by Vision for Omnified Nioh 2 would immediately be present and visualized upon the Omnification of any future game.

Game-neutral systems automatically provide game-neutral statistics

And, it’s also because the statistics themselves follow a design. This is observable by consulting the Omnified game statistic messaging schema in the hacking framework, and it is adhered to by the Statistics Vision module’s object model.

Let’s go through the different types of statistics now.

Whole Statistics

These are statistics that each concern a whole, numeric value.

The term “whole” is not in reference to the actual data type of the statistic, which may either be an integer or a non-integer. Rather, a statistic is considered “whole” if it is expressed using a single numeric value.

For example, the character’s experience level (e.g., a level 20 mage) and the number of times the player has died during the playthrough (e.g., 1230 times) are both examples of a whole statistic.

These three statistics are all considered to be "whole statistics", even though two of them can be fractional in value (a percentage value of 100% is 1, 50% is 0.5, etc.).
Three statistics that are all considered to be “whole statistics”.

Fractional Statistics

These are statistics that each concern a fractional, numeric value.

Like the term “whole”, the term “fractional” has no bearing on the data type being used. Rather, a statistic is considered “fractional” if it is expressed using values that have some sort of relationship to each other.

For example, the player’s health and stamina, as displayed in the above images, are fractional statistics. This is because each one consists of a current value, and a maximum value.

These two statistics are considered to be "fractional statistics", as each one can only be expressed by using both the current and maximum values.
These two statistics are considered to be “fractional statistics”.

The distinction between fractional and whole statistics affords us greater compactness as well as additional capabilities, such as the ability to present the statistics as a bar or gauge, as I detailed in a previous article.

Coordinate Statistics

These are statistics that each concern a coordinate triplet value.

One of the few requirements of an Omnified game is that it operates in three-dimensional space. As this requirement is satisfied by most games, it has never been an issue.

The players coordinates are typically one of the first things reversed engineered when I’m Omnifying a game; given that these values aren’t normally exposed to the player, I would’ve hoped that their display on screen was of interest for most viewers and lovers of games.

This statistic is considered to be a "coordinate statistic", since it is expressed through the use of a coordinate triplet value.
This statistic is considered to be a “coordinate statistic”.

We could have expressed the player’s coordinates using a statistic of the type we’ll be talking about next; however, having a statistic type made specifically for entity coordinates affords us some additional, specialized layout opportunities, among other things.

Statistic Groups

The final type of statistic is known as a “statistic group”. While it is indeed a statistic itself, its distinction among the other statistic types is that instead of containing a simple value, it contains other statistics.

These other statistics can be of any type: whole, fractional, coordinate, or even (I suppose) other statistic groups! The statistic group type exists for purposes of saving screen real estate, best understood by taking a look some example statistic groups.

Two "statistic groups" are pictured here containing three and four "whole statistics", respectively.
Two “statistic groups” are pictured here.

There are two statistic groups shown above, one for statistics related to damage the player has received from enemies, and one for statistics related to damage the player has inflicted on enemies.

Each child statistic belonging to these groups are themselves whole statistics. Now, instead of having multiple statistics with wastefully long names like “Max Damage Taken”, we can get away with simply “Max”.

Module Operation

The Statistics module for Vision displays the data being written to its particular message file, which by default will be found in the target directory of the Omnified game, with the name statistics.json.

As one can glean from looking at the message file’s name, Omnified statistics are encoded in JSON. A snapshot of the complete set of statistics is committed to the message file, meaning that the entire file is processed by Vision every time a write has occurred to it.

Example statistics.json dump
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
[
    {
        "Type": 1,
        "Statistic": {
            "SecondaryBarColor": "#AA27D88D",
            "CurrentValue": 2714,
            "MaximumValue": 2714,
            "Name": "Health",
            "PrimaryBarColor": "#AA43BC50"
        }
    },
    {
        "Type": 1,
        "Statistic": {
            "SecondaryBarColor": "#AAB22DE5",
            "CurrentValue": 1373,
            "MaximumValue": 1373,
            "Name": "Stamina",
            "PrimaryBarColor": "#AA7515D9"
        }
    },
    {
        "Type": 0,
        "Statistic": {
            "Value": 0,
            "Name": "Amrita (XP)"
        }
    },
    {
        "Type": 0,
        "Statistic": {
            "Value": 0,
            "Name": "Enemy Health"
        }
    },
    {
        "Type": 3,
        "Statistic": {
            "Name": "Damage Taken",
            "Statistics": [
                {
                    "Type": 0,
                    "Statistic": {
                        "Value": 0,
                        "Name": "Last"
                    }
                },
                {
                    "Type": 0,
                    "Statistic": {
                        "Value": 0,
                        "IsCritical": true,
                        "Name": "Max"
                    }
                },
                {
                    "Type": 0,
                    "Statistic": {
                        "Value": 0,
                        "Name": "Total"
                    }
                }
            ]
        }
    },
    {
        "Type": 3,
        "Statistic": {
            "Name": "Damage Inflicted",
            "Statistics": [
                {
                    "Type": 0,
                    "Statistic": {
                        "Value": 0,
                        "Name": "Hits"
                    }
                },
                {
                    "Type": 0,
                    "Statistic": {
                        "Value": 0,
                        "Name": "Last"
                    }
                },
                {
                    "Type": 0,
                    "Statistic": {
                        "Value": 0,
                        "IsCritical": true,
                        "Name": "Max"
                    }
                },
                {
                    "Type": 0,
                    "Statistic": {
                        "Value": 0,
                        "Name": "Total"
                    }
                }
            ]
        }
    },
    {
        "Type": 2,
        "Statistic": {
            "Y": 2558.3046875,
            "Format": "{0:0.000}",
            "Z": 14355.823242188,
            "Name": "Coordinates",
            "X": 18031.18359375
        }
    },
    {
        "Type": 0,
        "Statistic": {
            "Value": 100,
            "IsCritical": false,
            "Name": "Player Damage",
            "Format": "{0}%"
        }
    },
    {
        "Type": 0,
        "Statistic": {
            "Value": 100,
            "IsCritical": false,
            "Name": "Player Speed",
            "Format": "{0}%"
        }
    },
    {
        "Type": 0,
        "Statistic": {
            "Value": 3244,
            "Name": "Deaths"
        }
    }
]

Of course, optimizations are in place to ensure that UI controls aren’t redrawn unless they actually need to be (i.e., the value must change).

Most statistics are per-session

When a target binary is injected with an Omnified hack, most of the statistics are initialized to default values in memory. Only one of the stock statistics has its value maintained between sessions, and that’s the statistic responsible for tracking the number of deaths.

This is achieved through special logic in the Omnified messaging library, as well as a supplementary file designed to hold the death count, as the message file itself is a volatile place.

The whole process could probably be improved (removing the dependency on a separate file for starters) and made generalized so other statistics could be maintained between sessions; however, as the Omnified experiment has concluded (for now), this probably won’t happen anytime soon.

Grabbing attention with explosions

For the most part, a viewer can expect changes in statistics to simply be reflected by the Statistics module as those changes happen; rather immediately as well, as was made clear by the previously provided picture showing Vision‘s stamina bar next to the game’s own.

Some changes are more important than others, however. An example being that of the maximum damage received by the player. Omnified games greatly amplify the player’s incoming damage, and a point of pride is definitely how much damage I’m having to tolerate while playing a game I’ve Omnified.

Normal operation of the Statistics module: in this case, an explosive update being made to the maximum damage received stat after the player gets walloped by the enemy.
Normal operation of the Statistics module: in this case, an explosive update being made to the maximum damage received stat.

Hitting a new “highest damage received” number is like hitting the jackpot in an Omnified playthrough! The effect shown above is achieved through a special little animation I have in the Statistics module, lovingly termed ExplodeTextAnimation. It’s a neat little effect that makes the update to the statistic hard to miss.

This animation plays out whenever a new, higher value is written to a critical whole statistic. A whole statistic is considered critical if the IsCritical property of the statistic is set to true. Only a few of the stock statistics are marked as critical; if one does desire more exploding text, you’ll be happy to know it’s not hard to add your own.

Custom statistics are easy to add

Simply by Omnifying a game, all of the standard, game-neutral statistics will be wired up and able to be displayed by Vision. While that’s lovely, often there are going to be statistics completely specific to the Omnified game which we’ll want to show.

For example, if I’ve Omnified a shooter, it may become desirable to display the number of bullets in a currently equipped gun’s magazine (whose normal representation on the game’s HUD is most likely being blocked by my webcam’s image if I’m streaming the game).

Luckily, adding a custom, game-specific stat requires no modifications to be made to either the Omnified hacking framework or Vision itself. All one needs to do is create an exports.lua file in the Omnified game’s target directory, and have it contain a registerExports method that initializes the additionalStatistics variable appropriately.

Example exports.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
require("statisticMessages")
 
function registerExports()
    -- Custom statistics. 
    additionalStatistics = function()
        local bulletCount = toInt(readInteger("bulletCount"))
 
        return {
            WholeStatistic("Bullet Count", bulletCount)
        }
    end
end
 
function unregisterExports()
 
end

This, of course, assumes that the appropriate data collection code has been written somewhere in the target binary’s hooks.asm, properly dumping live values of the bullet count to the address of bulletCount. My game-neutral code is general purpose, not omniscient.

That aside, it’s quite easy to add as many custom statistics as we please to an Omnified offering.

The interface can be hidden if it gets in the way

With potentially all of these custom statistics in addition to the stock statistics, there may be certain screens in the game containing elements that Vision ends up obstructing. A proper anchor point location only gets us so far.

Luckily (and this applies to the entirety of Vision, not just the Statistics module), the overlay can be hidden and revealed at a later point at the press of a hotkey.

If Vision actually does get in the way, a simple button press gets it out of sight, out of mind -- until we want it back.
If Vision actually does get in the way, a simple button press gets it out of sight, out of mind — until we want it back.
And that’s the first of the modules we’ll be discussing

I’m quite proud of the Statistics module, and there’s much more that could be said about it. If I ever find myself returning to do further Omnified development, there’s much more I will be able to do with it.

Whether or not that happens, I’m pleased with it in its current state. To see more of it in action, you can check out my stream’s VODs.

But, before you do that, let’s discuss the second module of Vision, whose completion essentially marked the wrapping up of the Omnified endeavor.

The Apocalypse Module

When I decided to refocus my efforts from livestreaming hacked-to-insanity games to developing my own game, I did so with a condition in place requiring me to complete the Apocalypse module for Vision first. This condition has since then been satisfied.

Arguably more invaluable than even the Statistics module in helping a casual observer understand the Omnified experience: the Apocalypse module grants insight into the mysteries of the Apocalypse system itself by providing a display of all the Apocalypse-related events occurring in the game.

Check out the dedicated article for information on the Apocalypse system

A complete overview of the Apocalypse system is not possible here, and I’d recommend checking out the linked article dedicated to it. It’s more or less up to date, and I’ll probably do one last revision of it in the future for the sake of accuracy, if needed.

To summarize: the Apocalypse is one of my Omnified game-neutral systems, with the very specific purpose of overhauling a game’s built-in damage system. Instead of just receiving damage, the Apocalypse system will “roll some dice” and apply a random effect based on that damage.

This typically results in many, horrific deaths for the player. That’s a rather significant augmentation of an important game system. It’s something that can easily turn a love tap into a deathblow.

Making a big change a little less confusing

With such a drastic change to gameplay, it becomes critical that we be able to see why exactly a normally light amount of damage happened to just kill us. Luckily for you and me, the Apocalypse module of Vision does just that, and it does it by elegantly displaying all of the various “decisions” the Apocalypse system is making behind the scenes.

Similar to how Omnified statistics used to be displayed to viewers, Apocalypse events were previously shown using a primitive and ugly window capture; although, in Apocalypse’s case, the window being captured was a console window that was basically running a tail command on a log file.

The window and its text had to be quite small, as the transparency of the window could only be increased so much before the text was rendered invisible. That means there had to be a semi-transparent black box with text on the bottom or top of the game screen. A veritable eye sore.

Fortunately, with Vision, things once again look a whole lot better

Referencing the original full shot of Vision provided above, here’s where the Apocalypse module resides:

A closeup of the Apocalypse module for Vision.
A closeup of the Apocalypse module for Vision.

There’s a lot of important information to communicate when discussing an Apocalypse event, and I think the Apocalypse module, as evidenced by the above image, does it rather well. With a proper anchor point location designated, it can do its difficult job while still allowing us to see the game we’re playing.

Visualizing Apocalypse Events

I always envisioned the Apocalypse system in the back of my mind as an evil Dungeon Master rolling a many-sided die in order to determine our fates. Indeed, there are essentially numerous die rolls occurring within the Apocalypse assembly code, and it’s these rolls that the old display logic would, rather verbosely, report on.

With Vision, we can elect to describe these rolls using imagery as opposed to words. This was achieved by implementing each type of die as a WPF view containing declarative DrawingImage XAML code (converted from an SVG format) that described the die’s shape.

The die view used for primary Apocalypse rolls, as seen in the XAML designer, bound to an event with a die roll of 5.
The die view used for primary Apocalypse rolls.

These die UI controls are essentially vector graphics, so the drawings are scaled to be as large as the space I allow for them; the ones being pictured here having been given much more room than what they’d normally get when hosted by an Apocalypse event view.

The majority of Apocalypse events, particularly the ones that target the player, feature a primary die roll. The results of the roll are displayed by the Apocalypse Vision module using the die view pictured above.

These aren’t the only kinds of rolls the Apocalypse system engages in, however. Should the primary die roll yield a 7, 8, or 9, then a supplementary risk of murder roll is triggered. If that roll ends up on the wrong number: we’re dead.

The die view used for "risk of murder" Apocalypse rolls, as seen in the XAML designer, bound to an event with a die roll of 5. This die will kill ya!
The die view used for “risk of murder” Apocalypse rolls.

Spiky and deadly! Now, instead of a bunch of words talking about all these rolls we can just display the dice themselves on the screen with just one or two (in the case of a murder roll) columns worth of space being occupied.

Effect text, and effect sound

Other than the die visuals, Vision also lets me slap a bunch of nicely outlined and laid out text on the screen. That’s really our main prerogative: textually describing the enforcement of an Omnified rule.

We need to do a little more than that, however. Sometimes, an event’s appearance is also accompanied by a lovely and rather jarring sound, as anyone who has watched one of my broadcasts would know (hint, hint: think Duke Nukem, Tom Petty, etc.).

Previously, the Omnified hacking framework itself took care of playing the sound effects, which made sense as it also used to contain the actual (primitive) display logic for the events. With the move to Vision, the Apocalypse module takes care of all the sound effect randomization and playback instead.

Module Operation

The Apocalypse module for Vision displays the events being logged to its particular message file, which by default will be found in the target directory of the Omnified game, with the name apocalypse.jsonl.

This message file and its format are a bit different from what the Statistics module uses — and its format might be one you haven’t encountered before: JSON Lines. This format allows us to store structured, JSON-encoded data that can be parsed one record at a time as each record gets added to the message file.

Example apocalypse.jsonl dump
1
2
3
4
5
6
7
{"Event":{"Damage":551,"HealthAfter":1669,"ZDisplacement":339.30004882813,"IsFreeFalling":true,"DieRoll":6,"YDisplacement":1427.7600097656,"Timestamp":"2022-01-08T03:42:10Z","XDisplacement":718.55993652344},"Type":2}
{"Event":{"DieRoll":1,"ExtraDamageMultiplier":2.0,"Damage":872,"Timestamp":"2022-01-09T06:08:50Z","HealthAfter":1827},"Type":1}
{"Event":{"DieRoll":10,"HealthHealed":872,"Damage":872,"Timestamp":"2022-01-09T06:08:52Z","HealthAfter":2699},"Type":5}
{"Event":{"DieRoll":1,"ExtraDamageMultiplier":2.0,"Damage":1830,"Timestamp":"2022-01-09T06:08:52Z","HealthAfter":869},"Type":1}
{"Event":{"AdditionalDamage":67,"BonusDamageType":0,"IsExtreme":false,"Timestamp":"2022-01-09T06:09:52Z","BonusMultiplier":3.3},"Type":0}
{"Event":{"DieRoll":8,"FatalisAfflicted":false,"Timestamp":"2022-01-09T06:10:53Z","Damage":800,"MurderRoll":1,"HealthAfter":69},"Type":3}
{"Event":{"Damage":76521,"HealthAfter":0,"MurderRoll":5,"DieRoll":8,"Timestamp":"2022-01-09T06:11:53Z","MurderMultiplier":69.0},"Type":4}

One can safely assume that the example provided above, due to the low number of events present, is either only a partial dump, or one for a brand-new game.

Apocalypse messages are processed incrementally

The Apocalypse module uses the alternative mode made available for processing message files: when its message file is written to, only the content that was just added to it since the last read is processed.

This is because we really only care about events that are currently happening. The message file for Statistics will always contain the same number of statistics (barring us just happening to add a new, custom stat midsession), whereas the Apocalypse message file is going to keep on growing and growing.

The events tell a story, and we need only worry about the most recent of developments.

Explaining murder

Of all the game-neutral Omnified systems, the Apocalypse system requires the most transparency, as it has a direct impact on one of the more crucial aspects of gameplay: whether you’re alive or dead.

Normal operation of the Apocalypse module: in this case, the player rolling bad on a "risk of murder" roll and subsequently being...murdered!
Normal operation of the Apocalypse module: in this case, the player rolling bad on a “risk of murder” roll and subsequently being…murdered!

When a new Apocalypse event is detected, its visualized form bounces down from the heavens onto your existing events (or up underneath them, depending on whether the module is anchored to the top or bottom of the screen), it then proceeds to shimmer a bit to grab attention, and then “melts” into the game.

All events are supported

The Apocalypse system has quite a few events that cause a variety of effects: increased damage to the player or enemy, restoration of the player’s health, a sudden shift in the player’s location, and even timed debuff afflictions of doom like Fatalis.

With the Apocalypse module for Vision, we have nice, concise visualizations for all of these events.

There are quite a few different events that can occur with the Apocalypse system; its Vision module tries to communicate these events to us in the best way possible.
There are quite a few different events that can occur with the Apocalypse system; its Vision module tries to communicate these events to us in the best way possible.

Not all possible Apocalypse events are shown above, but a number of them are. You get the picture.

Configurable screen clutter

Like the Statistics module, there’s a lot of room for configuration as far as the Apocalypse module’s behavior goes. I haven’t been able to fully exploit all of its potential, as I considered it to be a “finished product” at the moment, but there’s a lot here that can be taken advantage of.

The maximum number of events displayed on the screen is essentially controlled by the maxMessages setting for the module in Vision’s settings.json configuration.

In addition to that, the underlying collection view engine (provided by the Bad Echo Presentation framework), allows for delayed capacity enforcements, meaning we can allow for the events to pile up a little bit before we trim them down. This is useful for games where lots of events might occur at once.

Obviously, such a mechanism might result in a complete screen takeover if a ton of events are happening. That won’t happen here, however, as there is a limit on the number of items allowed to exceed the collection view’s capacity before an immediate capacity enforcement is triggered.

By default, the capacity enforcement delay limit is set to twice the value that maxMessages is set to. At the time of writing, there is no configurable endpoint to set the capacity enforcement delay itself — both of these settings most certainly deserve to be exposed in configuration, however I never got around to it.

Once again, for more information on the Apocalypse system, check out its article

There’s lots to talk about concerning the Apocalypse system, but for the most part, all information regarding it can be found in its dedicated article linked above.

Finishing the Apocalypse module for Vision was a goal of mine, and I’m proud of the result. It is the best (within reason of course) visual representation I can think of for this insane game hacking system that I created, and it’s a perfect sendoff to the Omnified experiment.

The End?

Writing this very article was the last of the self-imposed conditions that I needed to satisfy before I could “move on”.

The Omnified endeavor was one of the craziest and most difficult challenges I have ever undertook, and with Vision as its culmination (along with everything developed in between), I kicked its butt.

I’d love to keep doing it, however it’s a lot of work and it didn’t really pan out in front of a general viewing audience.

With this writeup on Vision complete, it’s time to move on to new things! I’ll be developing a game with my wife, and lots of fun technologies along the way.

Of course, since Omnified gaming and Vision are my babies, I may choose to continue to work on them whenever I wish to. And probably will every now and then.

Hope everyone had a fun ride; as for myself, I’m looking forward to hitting up the next one.