Development of The Peacenet 0.1.0-i and the challenges we faced.

Development update for the final 0.1.0-i build that got shipped to itch.io

Yesterday (May 5th, 2019), we released the long-awaited The Peacenet update, version 0.1.0-indev (0.1.0-i for short). The update contained a ton of goodies and fixes, including the new Darkflare user interface theme, many fixes and improvements to the hacking system and the game’s UI, a few missions to demo the core gameplay, the Cover system and more.

The update was tagged on our GitHub and released for 64-bit Windows systems yesterday afternoon after a lot of teaser development updates showing off the new features as they were being developed. Shortly after, the official 0.1.0-i final dev update was released to our YouTube and the new version was announced.

During development, there were tons of challenges we faced and creative ways we overcame them – and I’d like to write about some of them in this post. If you enjoy reading about the technical and nerdy stuff about the development of a work-in-progress game, then… read on!

The Mission System

When adding the SILENTFLARE mission to the game, we had the challenge of demoing the core gameplay flow without making development of the mission too complicated. Ideally, we’d be able to use the same code for the demo mission in the actual game’s storyline. Things should be modular and missions should be easy to design and implement.

I wanted to make it so that adding a mission to the game didn’t require recompiling of the source code. Instead, devs could just hop into the Unreal Editor, create a new Mission Asset, and fill out the details. This was my first challenge – because I didn’t want anyone to have to program a mission.

Structure of a Mission

To solve this challenge, I came up with a simple mission structure which Unreal’s editor could easily generate a mission editor for. So, internally, this is what a mission looks like to the game.

  • Each mission is a Data Asset that gets loaded in when the save file loads. A mission is completed if a reference to its asset is in the save file’s list of “Completed Missions.”
  • The mission asset contains the name of the mission, its acquisition text, the email message that gets sent to the player when the mission is unlocked, and what missions need to be completed before the mission is unlocked.
  • The mission asset also contains a list of Mission Tasks, each one needs to be completed in order.
    • Each Mission Task contains an “Is Checkpoint” checkbox. If checked, the game will save the current game’s state to a temporary file and allow the player to retry from this point in the mission when they fail.
    • Along with this, each Task contains a Task Type object which can contain different properties based on what kind of task you select. A hack task type may ask you who the player needs to hack, while ab open program task will ask you what program to open.
  • When a mission is unlocked, the game sends you an email from the person “giving” you the mission, with the mission as an attachment. This email tells you what you’ll be doing in the mission and additional story can be sent in this email.

When a Mission is In Progress

The game does a ton of things when a mission is in progress. All of these actions are consolidated into an Actor that gets spawned into the world when the mission is active. This Mission Actor is completely controlled by the game’s C++ backend, and takes cues from the game’s World State actor as well as giving the World State actor certain instructions when things happen in the Mission. The mission actor and the world state are in a constant two-way conversation with each other during the mission.

The main things this little team of Actors accomplish during a mission are the following:

  1. Keeping track of the current Mission Task.
  2. Keeping track of the state of the game, allowing you to restore from Checkponts or retry the mission when you fail.
  3. Preventing the game from saving during a Mission – every open-world game does this and there is a technical reason behind it.
  4. Sening game events to the current Mission Task, for example “the player just started a hack,” “the player’s cover is blown,” etc.
  5. Keeping track of the current Task’s state – is it completed? Is it failed? What’s the fail reason?
  6. Advancing to the next task or ending the Mission when you complete a Task.

As well as the above, these two Actors as well as the current Task object itself are able to communicate with your Peacegate User Context every frame – and, by extension, your Desktop and any program that’s open – so the mission system constantly has an eye on everything you’re doing. It can also interact with your User Context and Desktop, which allows it to display some information about the mission’s current state on the Desktop.

But this communication is somewhat mutual – the Desktop can tell the mission actor to do certain things too – but the access is limited. Nothing in the UI can ever directly control the mission system, they can only send game events and, when the mission is failed, instruct the game to either abandon the mission, retry it, or restore from the last checkpoint (if the mission says there’s a checkpoint.)

Luckily, the Unreal Engine enforces this sort-of access control – because UFUNCTION and UPROPERTY macros in the code can control who can access what. So there wasn’t much challenge in getting all of that set up.

Checkpoints

You know how in older games, like Grand Theft Auto: San Andreas, when you fail a mission you have to restart all the way from the mission’s beginning? Isn’t that extremely annoying?

Well, newer games have a checkpoint system that stops this frustration dead in its tracks. When you reach certain points in a Mission, the game will let you retry from that point in the mission when you fail it. I wanted to implement this system in The Peacenet – since having to do a ton of mundaine actions all over again because you blew your cover can get annoying.

But how do you implement that!? Well it’s easier than it sounds.

When you reach a checkpoint…

When you reach a checkpoint, simply store the current mission task in a separate variable called LastCheckpoint. Ideally, your mission tasks are all stored in an array and the current task is stored as an index into that arary – so just store that index value in a separate variable. You can have that separate value equal to some sort of sentinel value (-1, 8675309, etc.) when a checkpoint hasn’t been reached yet.

After storing the current task in that variable, take the game’s current state and save it to disk – in some sort of temporary location that you know will still be there when the player fails. We’ll call this the Checkpoint Save.

You should also back up the game state to a different temporary save file when the mission first starts – allowing you to give the player to restart or abandon the mission. We’ll call this the Pre-mission Save.

How to tell if the player’s reached a checkpoint

If the player has reached a checkpoint, then that temporary save file will exist and the LastCheckpoint variable will not be equal to the sentinel value. So just run a check on both.

When the player fails…

When the player fails the mission, tell them why. That way, they know what not to do in the future. In that UI, present them with three options.

  • Restart the mission
  • Abandon the mission
  • Restore last checkpoint

If the user chooses to Restart the mission, simply set the Current Task to the first task in the mission and load the game state from the Pre-mission Save.

When the player chooses “Abandon,” restore the Pre-mission Save and stop the mission from running completely. Despawn any actors associated with it, etc.

If they choose Restore from Checkpoint, make sure there’s a checkpoint to restore from. If there is, set the current task to whatever’s in LastCheckpoint, and restore the Checkpoint Save.

When the mission ends

When the mission is abandoned or completed, delete the two temporary save files. If the mission is completed, update the game state to reflect that so the player can’t replay the mission. Then you can get rid of everything related to the mission state and the player is now free-roaming the game again.

And this is what you end up with.

void StartMission()
{
    SaveGameState("PreMission");
    CurrentTask = 0;
    Tasks[CurrentTask].Start();
}

void Tick(float DeltaTime)
{
    Tasks[CurrentTask].Tick(DeltaTime);

    if(Tasks[CurrentTask].Completed)
    {
        Advance();
    }
    else if(Tasks[CurrentTask].Failed)
    {
        Fail(Tasks[CurrentTask].FailedReason);
    }
}

void GameEventReceived(GameEvent Event)
{
    Tasks[CurrentTask].GameEventReceived(Event);
}

void Advance()
{
    CurrentTask++;

    if(CurrentTask >= Tasks.Count)
    {
        EndMission(true);
    }
    else
    {
        if(Tasks[CurrentTask].IsCheckpoint)
        {
             LastCheckpoint = CurrentTask;
             SaveGameState("Checkpoint");
        }

        Tasks[CurrentTask].Start();
    }
}

void EndMission(bool Completed)
{
    DeleteGameState("PreMission");
    DeleteGameState("Checkpoint");

    if(Completed)
    {
        GetGameState().CompleteMissions.Add(Mission);
        SaveGameState("Main");
    }

    PresentCompletedUI();

    Despawn();
}

void Fail()
{
    PresentFailedUI();

    when(PlayerClicksRetry)
    {
        CurrentTask = -1;
        LastCheckpoint = -1;
        RestoreGameState("PreMission");
        Advance();
    }
    when(PlayerClicksAbandon)
    {
        CurrentTask = -1;
        LastCheckpoint = -1;
        RestoreGameState("PreMission");
        EndMission(false);
    }
    when(PlayerClicksCheckpoint)
    {
        if(HasCheckpoint())
        {
            CurrentTask = LastCheckpoint - 1;
            RestoreGameState("Checkpoint");
            Advance();
        }
    }
}

bool HasCheckpoint()
{
    return (LastCheckpoint != -1) && GameStateExists("Checkpoint");
}

That’s essentially pseudocode for Peacenet’s mission state system. How do you guys like my pseudo-C? 🙂

And that code probably powers tons of games’ checkpoint-supporting mission systems. Granted, not the EXACT same code but it’s usually the same idea.

Game Events

Alrighty, so we have a mission system but… How does it know what’s happening in-game? How does it know when you run a command? When you hack someone? When your cover’s blown? This is all done through Game Events. Game Events aren’t just for the mission system either, it’s a way for different areas of the game to communicate with each other, without knowing it.

I needed to set it up in a way where I could send out a named event (like “CoverBlown”), and some data to go along with the event. Ideally the data should be completely arbitrary, decided solely by the part of the game that’s throwing the event. I don’t need to send out the name and command-line arguments used to run a Terminal Command when I’m just telling the mission system that your stealth value has updated.

I should also be able to specify any arbitrary value for any variable in the event’s data. This, and the fact that I needed to name each variable in the data and needed to allow arbitrary data to be sent, meant that C++’s typing system wasn’t really…of any help. I decided I would just send a string of text as the event name, and a Map of Strings to Strings as the event data. So the syntax for sending a game event ended up looking like this:

SendGameEvent("EventName", {
    { "Argument1", "Value1" }
    { "Argument2", "Value2" }
});

And to be quite honest with you, for C++, that’s not too bad. That event will get sent to the Peacenet world state actor, which will then propagate it to the mission actor (and thus the current Task) if a mission is active, then the rest of the game. Eventually the event will travel to a function in the game’s code that looks like this:

void HandleGameEvent(FString EventName, TMap<FString, FString> Data);

…and implementations of that function are able to first check the EventName, then the Data. They can check if the Data contains a variable with the specified name by calling Data.Contains(“argument”) and they can access the value of it with Data[“argument”].

One mildly tiny problem…

…This means that all event data is sent as text. Booleans are text. Integers are text. Floating-point numbers are text. Localized text is text. Non-localized text is text. Enums are text. Everything. Text. Text. Text. So how do you encode things like a UE4 object or a C++ structure? You don’t. Period. You are limited to only sending data that can be encoded as a string in one line of code, if you want the code to look legible.

But that’s okay. Because UE4 has a built-in event system based on Delegates which is meant for the more elaborate things. Use that instead. This event system is meant for signalling different areas of The Peacenet that the player has done something – giving a tiny amount of information about what they did, without having to know exactly where you want the event to go or having to let things have direct access to you in order to bind to the event. You should use UE4’s native event system for anything else.

Emails

Emails were another big thing in the game. I’m not going to go over every single detail about how they work – you can look at the code for that. But the just of it is, each email message is a C++ structure that looks somewhat like this:

struct MailMessage
{
    int ID; // the ID of the message.
    int[] To; // Who will receive the message? (Peacenet Entity IDs)
    int From; // Who wrote it? (Peacenet Entity ID)
    string Subject; // self explanatory.
    string Message; // The message's body.
    int[] Attachments; // Any attachments to go with the message (Attachment IDs, not-yet-implemented)
    Mission* Mission; // Optional: pointer to the mission asset that is attached to this message.
    int InReplyTo; // What message is this in reply to? (Message ID). (-1 = none)
};

And there’s a massive list of these in the save file. It is up to the Icedog Mail Viewer GUI to only show you messages that are either to or from you, based on your Entity ID, and to map email addresses to entity IDs and vice versa.

Speaking of Icedog, what about missions?

You may notice that Icedog has a “Missions” category. How does it know what to display in there? Well, that’s easy.

  • Notice the Mission pointer in the MailMessage structure? Icedog will only list emails where that pointer is NOT nullptr under the missions section.
  • It will also hide any messages where the mission is complete.
  • Remember that completed missions are also stored in a list in the save file.

So if Icedog sees no mission messages, or that all the missions sent to you are already done, they just won’t show up under Missions! Sweet.

But there’s a slight UI issue.

Since the game encodes the email’s message text and subject in the message itself, it’d be logical to set the subject text of a mission message to the mission’s name – and the message body to a message body defined by the mission and store that in the save file.

That was my logic at first, but it presented a problem. What if you change the mission’s metadata? How will that be handled in older save files? Well, it won’t. So instead, what Icedog will do, is instead of reading the subject and message body from the save file when it sees a mission, it will read directly from the mission itself.

So we have a mission system and an email system to compliment it. What’s next?

Hacking

Next, I wanted to flesh out hacking in the game. In previous builds, successfully hacking a system meant getting a bash prompt. And there was no way to really fail a hack. I set out to change that in 0.1.0.

Cover

I added the ability to analyze a specific port during hacking. This tells you information about the server running on that port, and what exploits it’s vulnerable to. But there’s a catch:

Analyzing requires a full connection to the remote computer on that port. This means that your IP address will get logged, and thus your Cover Meter will decrease.

This is the basis of the stealth system in Peacenet – that Cover Meter. The goal is to keep it at 100%. As you perform un-stealthy actions, it decreases. It will slowly rise up as you remain stealthy. If it goes below a certain value, you’ll blow your cover, and if it goes to 0%, you’ll be arrested.

As I said, analyzing gets your IP logged – which lowers the cover meter. But the challenge was coming up with other things to lower it, without compromising the game’s difficulty. I came up with these:

  • Getting your IP logged (analyzing, attacking)
  • Not deleting /var/log/system.log before disconnecting.
  • Hacking the system while someone else is using it. (not yet implemented.)

I’m sure there is more I could add but… that’s for another update.

Special Commands

In real life, in most cases, you don’t just end up getting a raw bash prompt when you hack someone. And when I was demoing what real-life hacking was like months ago, I ended up getting Meterpreter deployed onto a remote system using Metasploit. This was what I based the hacking system in the game off. Gigasploit Framework is our in-game Metasploit, but what’s our Meterpreter?

Well, Meterpreter in real-life is a remote access trojan. It gives you a reverse shell onto the remote system, and waits for you to give it commands. It doesn’t give you a bash prompt, it has its own set of commands – like turning on/off the user’s webcam, uploading/downloading files, running programs, playing audio files, etc.

In earlier Peacenet builds, we just gave you a bash prompt – not an in-game RAT. This, unfortunately, meant that there is no point in hacking a system other than to just use it remotely without permission. You can’t download files. Can’t upload. Can’t steal money. Etc. BORING!

So my solution was to completely overhaul our Shell system and have bash inherit from the base Shell and have the reverse_shell payload inherit from bash. That way, reverse_shell can allow you to run native bash commands on the remote system, as well as its special upload and download commands. So, now you can download remote files to your computer and upload files from your computer to the remote system.

Shell changes

With that said, I did a ton to the shell system in the game .

  • Shells can customize their shell prompt text.
  • Shells are in complete control of where they grab Terminal Commands from.
  • Bash will let you run Terminal Commands from files – even by specifying relative paths.
  • Shells can intercept the command-interpreting process of the base shell, seeing the requested command name and the parsed argument list – allowing for internal shell commands.
  • Shells can now be created in both C++ and Blueprint.
  • Creating a shell doesn’t require you to write any custom parsing code. It’s all done for you by the base shell.
  • Shells themselves are Terminal Commands, so anything that can be done in a Command can be done in a shell.
  • It’s up to the shell whether you’re allowed to use unix pipes or redirection.
  • As a shell, you don’t have to worry about handling unix pipes or redirection – as with parsing, this is done by the base shell.

This may seem like it doesn’t matter much for gameplay, but it truly does! It opens up so many possibilities for new payloads, system services, and even commands!

I wrote a separate article on our shell system, so you can read that if you want to know more.

Darkflare UI

This is the last thing I want to cover – the Darkflare UI. The goal of the new theme and layout changes was to accomplish the following:

  • The game shouldn’t look like a mix between 90s Mac OS and modern Linux. Pick one or the other. We went for modern Linux.
  • Windows shouldn’t awkwardly resize when their content changes. The content should respond to the size of the window.
  • The UI shouldn’t look anything like the default Unreal UI style.
  • UI elements should be consistent.
  • It should be easy to build UIs in a consistent way.

That’s a lot of goals to accomplish but… I had a plan.

Art style

I’m not an artist – but I do know what looks Linux-y and, somewhat, how to accomplish the look. But there are several different kinds of Linux-y – like… too many different kinds to count.

Linux isn’t like macOS or Windows – as a user, you basically have complete control of how your desktop looks and how it’s laid out. In fact, you’re not limited to one single desktop environment – if you don’t like the desktop that comes with Ubuntu, you can just install another one. So how do you define “a Linux-y art style”?

Go by desktop environment, UI toolkit, and theme style.

I’ve noticed that most Linux systems get their look mostly from the desktop environment, the UI toolkit it’s written in, and the style of theme applied to it.

For Peacenet, I chose XFCE4 as the desktop environment, GTK3 as the UI toolkit, and a modern, flat dark theme as my theme style. These three things are ultimately what define the game’s layout AND look.

To accomplish the nice dark look, I used textures from the old Inkblot theme. I also added my own twists to the theme.

  • I removed any form of “paint stains,” as Trey would call them – which were what Inkblot’s name came from. You don’t typically see those in a Linux desktop.
  • I used the User Color to very slightly tint the background of windows. This also makes it so the window background looks the same as the window border/titlebar, which seems to be how most of the cool kids do it nowadays.
  • I used the Ubuntu and Ubuntu Mono font families throughout the entire UI.

Building Blocks

To help with UI consistency, and easily maintaining it, I added a few building block widget blueprints to the game. These can be used anywhere in the UI and don’t care what you put inside them or whether the game’s in the main menu or the desktop.

So far, these building blocks include:

  • Inner Window Area: a panel with a dark gray background that is NOT tinted by the User Color.
  • Light Panel: A dim gray panel that’s meant to sit ontop of an Inner Window Area or the Desktop Wallpaper.
  • Peacegate Button: A dark gray button that can contain an icon and text, and remains consistent with the UI.
  • Scroll Panel: An inner window area that’s scrollable and has a dark-themed vertical scrollbar.
  • Thumbnail: A clickable picture that can contain a tool-tip that describes the picture when hovered over.

I use these building blocks heavily throughout the UI, and you can use them too. This means that I can change the look/feel of a building block just once and have the entire UI update properly.

So that’s a lot.

This article is getting long – been writing it all day. My eyes are getting quite strained, so, I’ll end it off here. But hopefully you guys found all of this interesting! 🙂

You can pick up the latest version of the game for free on itch.io at https://bitphoenixsoftware.itch.io/the-peacenet/.

Leave a Comment