Tutorial Part 15: Theme Colours and Maintenance

One amazing thing about taking two years to write a simple tutorial is that it gets tested a whole lot by different developers along the way, and you get extremely reliable code by the end. [Placating tone] I know, I know… I promised lockable doors… but part of roguelike ownership is cleaning up the coils of poop in the backyard when the snow melts off in the spring, right? So let’s squash some bugs and address some questions thanks to many folks in the SadConsole discord.

Changing Theme Colours

HodgePodge asked: Where do I go to change the theme of a window? Like from blue to black. According to the ansiware tutorial it was Theme.WindowTheme.FillStyle.Background. if I remove .windowtheme it doesn’t change the blue background

That little problem has been nagging me a lot too, since SadConsole switched to the “Theme”-based colour system! You probably noticed that we’d had a puke-blue background for our Game Map and Message Log since forever. Well, let’s do something about that. Thraka’s answer to HodgePodge kindly pointed out the proper procedure for re-colouring all objects, which I am gleefully stealing for this tutorial.

Important: before you continue, make sure that you have updated your SadConsole nuget to version 8.99.x which has a new Theme/Color architecture!

I hereby deem Themes to be a UI related topic, so let’s start by modifying the UIManager class:

    public class UIManager : ContainerConsole
    {
        ...
        public SadConsole.Themes.Colors CustomColors;

    // Initializes all windows and consoles
    public void Init()
    {
        SetupCustomColors();

In the above code, CustomColors will act as a reference to the custom coloured theme we are building. Note that it is marked public, so we can access our custom colours from anywhere within our program. Now let’s build the custom coloured theme method we reference above:

// Build a new coloured theme based on SC's default theme
// and then set it as the program's default theme.
private void SetupCustomColors()
{
    // Create a set of default colours that we will modify
    CustomColors = SadConsole.Themes.Colors.CreateDefault();

    // Pick a couple of background colours that we will apply to all consoles.
    Color backgroundColor = Color.Black;

    // Set background colour for controls consoles and their controls
    CustomColors.ControlHostBack = backgroundColor;
    CustomColors.ControlBack = backgroundColor;

    // Generate background colours for dark and light themes based on
    // the default background colour.
    CustomColors.ControlBackLight = (backgroundColor * 1.3f).FillAlpha();
    CustomColors.ControlBackDark = (backgroundColor * 0.7f).FillAlpha();

    // Set a color for currently selected controls. This should always
    // be different from the background colour.
    CustomColors.ControlBackSelected = CustomColors.GrayDark;

    // Rebuild all objects' themes with the custom colours we picked above.
    CustomColors.RebuildAppearances();

    // Now set all of these colours as default for SC's default theme.
    SadConsole.Themes.Library.Default.Colors = CustomColors;
}

Now, there’s a lot going on up there, so let’s break it down into bite-sized hunks. The first line sets CustomColors to SadConsole’s default colour set. This gives us a foundation of colours to build from, rather than having to set individual colours for every type of Window, Console and Control in the program. The second step is straightforward: pick a single colour to apply to every console, control and window. While I’ve selected black, I highly recommend you to choose Red or Yellow for that relaxing and subtle Hotdog Stand experience.

The next step is a bit trickier – we’re doing light procedurally generated colouring to produce colours that are darker than and lighter than our base background colour. When we multiply backgroundColor by 1.3, we’re effectively saying “crank up the brightness of all Red, Blue and Green colours by 130%”. The FillAlpha call simply makes the colour completely opaque (non-transparent), so we don’t end up with translucent backgrounds. (But hey, if you want translucent backgrounds, this is the place to do it!)

You certainly do not need to procedurally generate your colours here. You can just set ControlsBackLight and ControlsBackDark to whatever colours you’d like instead.

The RebuildAppearances method instructs SadConsole’s Theme system to dig through every single object in CustomColors and rebuild them to use the colours we’ve selected. Finally, the last call tells the Theme system to set all of its default colours for in-game objects to CustomColors. And that’s it! Run the program, and now you are one step closer to Hot Dog Stand nirvana.

Let’s not stop there though – it’s a bit boring to have the exact same colours throughout the UI right? Let’s set some new custom colours specifically for the Message Log so it stands out a bit. Modify the MessageLogWindow class thusly:

First, remove this line if you’ve got it. The old method for changing colours just doesn’t work anymore:

public MessageLogWindow(int width, int height, string title) : base(width, height)
{
    // Ensure that the window background is the correct colour
    Theme.WindowTheme.FillStyle.Background = Color.Black;

Now add the new code:

public MessageLogWindow(int width, int height, string title) : base(width, height)
{
    // Set some custom colours for the MessageLog
    ThemeColors = GameLoop.UIManager.CustomColors.Clone();
    ThemeColors.ControlBack = Color.DarkRed;
    ThemeColors.TitleText = Color.Red;
    ThemeColors.RebuildAppearances();
...

We begin by Cloning the CustomColors defaults we’ve already set in the UIManager, so we have a new working copy to modify. We then make a few minor modifications to the colours, and then RebuildAppearances when we’re satisfied with our settings. Notice that we’re using the Window’s built-in ThemeColors variable which controls the colours the Window uses.

It’s Not a Feature, It’s a Bug

Let’s get into the weeds a bit, shall we? Freiling noticed that Entities are being displayed on the MapConsole, but are not being properly synchronized to the MultiSpatialMap that we use to store our Entity positions. Because we aren’t using Entities for much in the tutorials yet, we’ve had this bug quietly lurking in the backgound. It only became noticeable when Freiling started to use Entities on the map for things like a tooltip system that shows you the name of the Entity the mouse is hovering over.

So what was the problem exactly? Some Entities were appearing on the MapWindow (that is – you could see them onscreen), but whenever Freiling used GetEntityAt(X,Y), the MultiSpatialMap would say that nothing is there! In other words: the MultiSpatialMap called Entities had different information in it than MapWindow. That’s a big problem, because it’s a form of data corruption. Someone, somewhere, was asleep at the switch.

(me.)

So, let’s open our Map class and try to figure out what exactly caused MapWindow to have different entities shown onscreen than our MultiSpatialMap<Entity>. Scroll down to this area:

// Adds an Entity to the MultiSpatialMap
public void Add(Entity entity)
{
    // add entity to the SpatialMap
    Entities.Add(entity, entity.Position);

    // Link up the entity's Moved event to a new handler
    entity.Moved += OnEntityMoved;
}

Remember that this method adds an Entity to the MultiSpatialMap called Entities, then subscribes the new entity’s Moved event to the OnEntityMoved EventHandler. That looks totally fine, right?

Not totally fine dude! Not totally fine!

Chris3606: The MultiSpatialMap.Add function can fail to add the entity, and when it does (in GoRogue 2.0, anyway) it simply returns false. So I’m thinking what’s happening is some of your adds are failing silently. your entities are still displayed because sadconsole doesn’t care about GoRogue’s entity list, but as far as GoRogue’s list is concerned (which is the one that GetEntitiesAt uses), it was never actually there.

Failing Silently… I should rename this tutorial series. What the good doctor is saying is that sometimes Entities.Add(entity, entity.Position) does not successfully add an Entity at that Position! Sometimes it’s failing to add – I’m guessing this is because there is already an entity at that position, or something else is blocking it from being positioned there. Chris3606 assures us that this is a bug in the GoRogue v.2 implementation, so we need to work around it. What’s the solution? We need to add some logic that traps any “silent” failures. Let’s start by modifying the Add method:

// Adds an Entity to the MultiSpatialMap
public void Add(Entity entity)
{
    if (!Entities.Add(entity, entity.Position))
        throw new Exception("Failed to add entity to map");

    entity.Moved += OnEntityMoved; // Link entity Moved event to new handler
}

Learning opportunity! We’re using a new .NET keyword in the above called throw. A throw tells the compiler that we’ve encountered an “exception” or undesirable circumstance where what we wanted to happen, didn’t. Let’s also mark out a second place where GoRogue might fail to access an Entity:

// When the Entity's .Moved value changes, it triggers this event handler
// which updates the Entity's current position in the SpatialMap
private void OnEntityMoved(object sender, Entity.EntityMovedEventArgs args)
{
    if (!Entities.Move(args.Entity as Entity, args.Entity.Position))
        throw new Exception("Failed to move entity on map.");
}

And finally, we need to modify the Remove method with a similar pattern:

// Removes an Entity from the MultiSpatialMap
public void Remove(Entity entity)
{
    // remove from SpatialMap
    if (!Entities.Remove(entity))
        throw new Exception("Failed to remove entity from map");

    // De-link the entity's Moved event from the handler
    entity.Moved -= OnEntityMoved;
}

Again, this does not fix the bug exactly, but it at least acknowledges that the Entity could not be moved. So we go from failing silently to failing loudly 🙂 Again, in a future release of GoRogue v.3 this bug will be fixed!

OK this last one has been bugging me for a long while. I can’t tell if this is a regression (that is, an old bug that was fixed, then re-introduced at a later date), or simple forgetfulness in my advanced age… but did you notice that Doors don’t have names? Milhouse opened a … kind of steps off a cliff, doesn’t it? If you haven’t already fixed this, it’s an easy one! Modify the TileDoor class constructor:

//A TileDoor can be set locked/unlocked/open/closed using the constructor.
public TileDoor(bool locked, bool open) : base(Color.Gray, Color.Transparent, '+')
{
    //+ is the closed glyph
    //closed by default
    Glyph = '+';

    // Name it
    Name = "Door";

Okay, so that’s it for the current round of fixes. For sure we’ll do locks and keys next time! 🙂

Download the final source files for this tutorial here.

Published on March 12, 2021