Agreeably Discordant

I’ve spent the last year joining a lot of new TTRPG Discords, which has led to me playing or running a lot of online TTRPGs. Now, you can join them too. Enjoy!

Cairn https://cairnrpg.com

  • Dedicated to the rules-light dark fantasy TTRPG Cairn

Darrington Press https://darringtonpress.com

  • Dedicated to Darrington Press (Critical Role) TTRPGs like Candela Obscura and Daggerheart

Furious Eclectic People http://furiouslyeclectic.com

  • Dedicated to offbeat, rules-light, and OSR TTRPGs
  • Very open to promoting other Discords — I’ve found a ton through here!

ICRPG Community https://icrpgcommunitycontent.com/runejammer-2024

  • Dedicated to the TTRPG Index Card
  • Was at its busiest during the Runejammer 2024 online convention

Magpie Games https://magpiegames.com/pages/get-involved

  • Dedicated to Magpie Games TTRPGs such as Avatar: Legends, Masks, and Root
  • Holds semi-monthly Community Play Days

Mothership https://discord.com/servers/mothership-461670627468771329

  • Dedicated to the sci-fi horror TTRPG Mothership by Tuesday Knight Games

OmegaOm TV https://www.twitch.tv/omegaomtv/about

  • Dedicated to Alien, Mothership, and fantasy horror TTRPGs
  • All games are actual plays, streamed on Twitch

The Good Friends of Jackson Elias https://blasphemoustomes.com/a-weekend-with-good-friends/

  • Associated with the podcast of the same name
  • Dedicated to Call of Cthulhu and other horror TTRPGs
  • Hosts the yearly online convention A Weekend with Good Friends

The One Ring/LOTR RPG https://discord.com/servers/the-one-ring-lotr-rpg-348254014598545408

  • Dedicated to The One Ring and Lord of the Rings Roleplaying TTRPGs by Free League

TTRPG Pickup Con https://www.indecisionist.com/ttrpg-pickup-con

  • Monthly online conventions
  • I’ve played many new systems here, lots of fun

VaesenCon https://www.vaesencon.com

  • Primarily the site of the yearly VaesenCon online convention
  • Dedicated to the Nordic horror TTRPG Vaesen by Free League

Year Zero Worlds https://discord.com/servers/year-zero-worlds-398697411981344769

  • Dedicated to Year Zero Engine TTRPGs by Free League

Pros and Cons

If you haven’t tried one of the many online TTRPG conventions, you really should. They’re free to attend, and they’re a lot of fun. Just join the Discord hosting the con, sign up for one or more games, and show up in the designated voice channel at the right time.

There are two kinds of online cons that I’ve seen: cons that run once a year, and cons that run more often than that.

While some of the year cons are already over — see below — there are two coming up in early September where I’m running games. The same day, in fact!

If either of these look interesting to you, I would urge you to sign up for them!

  • VaesenCon, website here, where I’m running the Vaesen adventure “A Murder of Crows” on Saturday September 7 at 1:00 p.m. Eastern Time. In the town of Varberg, a.k.a. “the least appealing place in Sweden”, crows have attacked anyone entering the cemetery for over 6 months now. Is it a quirk of nature… or something more sinister? See this page for details (search for “crows”, near the bottom) and signup here. Deadline to signup August 31! Will use Discord for voice, and Roll20 for character sheets and rolls. Newbies welcome! Pre-gens provided.
  • CypherCon, signup here, where I’m running the Cypher System adventure “Seldon Crisis” on Saturday September 7 at 8 p.m. Eastern Time (page erroneously says Central). The Foundation conquered its neighbors, the Four Kingdoms, via its technocratic religion. But now, nearby Korell somehow has its own nuclear-powered warships. Your Foundation trading ship crew must investigate. Based on the Asimov books! Will use Discord for voice, and, if possible, Roll20 for character sheets and rolls. Newbies welcome! Pre-gens provided.

The following yearly cons have already happened, and were a lot of fun, too! I’ll probably try to run games at these cons next year:

  • A Weekend With Good Friends, organized by the listeners of the Good Friends of Jackson Elias podcast, con page here. I got to play Delta Green for the first time here.
  • RuneJammer, hosted by the fans of Runehammer Games, con page here. I got to play Index Card RPG for the first time here.

Some of them happen more often than once a year, which gives you (and me!) more chances to participate:

  • TTRPG Pickup Con, organized by Ansel Burch and others, runs monthly, main page here. I’ve run Blades in the Dark for the first time here, and I got to play Troika! and KULT: Divinity Lost here.
  • Magpie Community Play Day, organized by Magpie Games, Discord link here. I’ve gotten to both play and run Avatar Legends for the first time here.

As you can see, these cons are a great opportunity to try a new TTRPG system, and play with new players and GMs. I hope to see you there!

Help, I Need Somebody!

I’m fascinated by how all the different role-playing systems handle common tasks in different ways.

For example: the “help” action.

D&D: if you help someone, they roll with advantage. Instead of rolling the usual 1d20 plus bonuses in order to meet or beat a difficulty class (DC) or armor class (AC) number, they roll 2d20 and take the higher of the two (twice the chance of success!). The only thing you spend is time: this will use up your action in your turn, if in combat.

Dragonbane: Like D&D, help only costs your action, and they roll with advantage, which Dragonbane calls getting a boon. The difference here is that you have to roll at or below your skill level to succeed — a roll under system, not a roll over system.

Cypher System: if you don’t have an inability in it (i.e. are bad at it), you can ease the task for someone else’s d20 roll by one step — two if you’re trained or specialized in it. Each step translates to 3 points in the target number (TN), so easing the task by two steps changes it from difficulty 6 (target number 18) to difficulty 4 (target number 12). As with D&D, help only costs your action.

It’s also possible for you to perform a complementary action instead that eases someone’s else’s roll.

Avatar Legends: you can mark 1 fatigue and make the Basic move help a companion. This only improves their roll by 1 point, and so it’s only useful if they missed the roll with a 6 — where a +1 turns it into a 7, a weak hit — or got a weak hit on a 9 — where a +1 turns it into a 10, a strong hit.

Unlike other systems, it is done after the roll.

The One Ring: first, some background: TOR has aspects of roll over games and dice pool games. For a skill check, you roll a Feat die (a d12 with some special values) and a variable number of Success dice (a d6), and add them up. If you roll a 6 on any of the d6s, that is extra successes.

Now, on to the help details: you support someone by spending a Hope point (a somewhat limited resource), which gives them an extra d6. Also, you must have a rank in the skill you want to employ to help them, similar to Cypher System, where you can’t have an inability in the skill.

Star Trek Adventures: again, I’ll start with some background: Star Trek Adventures is based on the 2d20 system, which, like The One Ring, is another kind of dice pool hybrid. It uses d20s instead of d6s; for each d20 result, it uses roll under rules to see if that roll is a success — but after that, you only care about the number of successes, like a more traditional dice pool game.

Your helping someone is called teamwork, and multiple helpers are possible, if discouraged. You roll a single d20, and if it succeeds, you can add that success to the successes of the person you’re helping — but their roll only succeeds if they get at least one of their own successes.

Vaesen: in our first standard dice pool game, you generally roll your Ability + Skill number of d6s, and each 6 rolled is a success. You and up to 2 other characters can help someone with their roll, and each person helping adds a d6, up to 3d6. No cost for helping is mentioned in the rules at all.

Candela Obscura: also a d6 dice pool game, but you generally roll fewer dice than Vaesen, and there’s the concept of a mixed success on a 4–5 result. You help someone by spending 1 drive point (a limited resource) to add a d6 to their roll. You can only spend 1 drive point, and doing so opens you up to shared consequences if the roll goes poorly — since the GM never rolls, bad things only happen as a result of your rolls.

If multiple people want to help, it becomes a group roll, though it’s still only one person rolling and the others contributing. The dice pool also maxes out at 6 dice. (The subject can also spend their own drive points.)

Blades in the Dark: has the same dice rolling mechanic as Candela Obscura (which is based on it). To help, you take 1 stress, and only one person can help.

If someone is already helping, you can perform a setup action instead to help indirectly, similar to Cypher System’s complementary action.

Star Wars: again, I’ll provide some background first: Star Wars is a dice pool game, but the d6/d8/d12 dice have their own special positive and negative symbols on them rather than numbers, which you add up to determine the outcome. Assisting someone else is a maneuver, rather than your main action. Your assistance adds a bonus die (a d6 with positive symbols) to that person’s dice pool. Multiple people can provide assistance.

Whew! These are just the games I’m personally familiar with. I’m sure there are a lot of other variations out there.

It’s especially interesting how much or how little you can help. Sometimes, it’s doubling the chance of success. Sometimes, it’s just nudging things the tiniest bit.

And in the best examples, the way you help is inextricably linked to the unique features of that game, whether it’s a roll over/roll under game, a dice pool game, or a hybrid.

One last thing to note: in most systems, it is explicitly stated that the GM must approve the assistance attempt, and the player must explain what steps they’re taking in-game to provide the help. You can’t just say “I’m helping” — you have to say how.

Yours in Calendrical Heresy

Discussing Yoon Ha Lee’s Ninefox Gambit TTRPG.

Yoon Ha Lee’s Machineries of Empire series of novels, starting with Ninefox Gambit, depict a brutal and fascinating sci-fi world, full of strange and often unexplained technology.

The same author has put out a bunch of mini-TTRPG games on his Itch.io page, so it’s no surprise that he also published a Ninefox Gambit TTRPG last year.

The books are complex, deep, and nuanced — but the TTRPG is simple, spare, and abstract. I’ve wanted to run it since it came out last year, but finding interested players has been a challenge. Thanks to the folks at Furiously Eclectic, I’ve been able to run it twice!

It’s a D6 dice pool game, where you add to your dice pool by tagging in (using) your Edges (personality traits). There are no attributes, no skills, no gear, no tables of any kind. Almost everything you would use as scaffolding to hang your story upon is absent here, so it definitely took both me and my players some time to get used to it.

Unlike Daggerheart’s similar Experience, which should be used sparingly, in Ninefox Gambit, I wound up being extremely generous with chances to use a character’s Edge, because there’s virtually nothing else to use.

The game intentionally puts you in untenable situations, gives you impossible choices, just like the books. Your choices aren’t do the good thing or do the bad thing, they’re do the unconscionable thing or do the slightly less bad thing. I’ve tried to play my one-shots with at least some people who’ve read the novels, because otherwise such situations may seem too depressing for a roleplaying game.

Perhaps the most unusual and fun aspect of the game was the Clock mechanism. If you rolled any sixes, even if your roll otherwise succeeded, you wind up your Clock — which starts at 1, and if it goes up to 6, you are assimilated into the Hexarchate, the game’s worst fate. But, succeeding on your rolls gets easier the farther up your Clock is, because anything at or below your Clock value is a success. There’s also a Party Clock and a Hexarchate Clock that you can wind up instead, but they’re much harder to wind back down again. Figuring out what to do about your sixes is probably the most tactical aspect of the game.

The Ninefox Gambit TTRPG book has none of the professional typesetting and artwork you see in all the Kickerstartered games that have come out in the last bunch of years: it looks like it was put together in a word processor. It’s even missing character sheets, so I’ve used my meagre artistic and PDF talents to make my own:

I’ve now run two of the three one-shot scenarios that are included with the rulebook, and I’ll be running the third one this month. If the game had more meat on its bones, or if I could find interested players more easily, I might have been tempted to run a longer scenario, or even a campaign, but as it is, those sessions will satisfy my desire to explore this game. I’m glad I was able to do it!

Mission: Possible

These days, I’m leaning towards TTRPG systems where there’s a mission goal, stated up front, for every adventure.

D&D: The Road Goes Ever On

In D&D, the adventuring party often spends forever wandering around, trying to figure out what the goal even is. Especially in one-shots, this time spent can mean the adventure never finishes, because scheduling a second session with the same people turns out to be…impossible.

And, even in campaigns, it can lead to a lot of player frustration, when you just don’t know what to do. I’ve experienced both the player side of this and the Dungeon Master side of this, and neither is a particularly fun.

Sometimes, players even begin to fall into the mindset of “Let’s read the DM’s mind” and won’t do anything until they’re sure they’ve got it right. Or they’ll go round and round arguing about their course of action forever.

Mothership: Money, Money, Money

Outside of D&D, there are sci fi games where you’re just trying to make enough money to get by: what I like to call the “budget Alien” games. Mothership is the one I’ve been playing, but there’s also Orbital Blues, Death in Space, and many others.

The trouble is, players act differently when they just want money. In one of my games, the characters met an extraterrestrial no one had ever seen before. It should’ve been a big deal! But instead of exploring that, they just wanted to sneak past it to so they could loot its ship.

If you actually want your players to get involved in your story, they need different motivations.

What does it look like when you get the mission stated up front?

Star Trek: You Have Your Orders

Well, maybe you’re in a military organization, and the mission is literally a set of orders. The fits games like Stargate RPG, Star Trek Adventures, and Delta Green, all games that I’ve had a lot of fun running recently. (Though hierarchical organizations can cause their own problems, if players start thinking they can countermand what other players want to do. Lookin’ at you, Star Trek!)

Some of these games, in their published adventures, have an initial scene where the orders are given out. But for one-shots, I find that unnecessary. Post the orders somewhere, and you can start in medias res, already in the town/on the planet/etc.

Having orders doesn’t take all the surprise out of it, not at all! It’s always fun when something comes up that the orders don’t cover. When the team in the field has to make a difficult choice. When a walk in the park becomes a life-or-death struggle.

Vaesen: The Structure of Fairytales

In Vaesen, my current favorite of these types of games, you don’t have orders, per se, but your organization receives requests for help, and a team is sent out to provide it.

The objective always involves a Vaesen, or supernatural entity, in a world where such creatures can’t simply be shot and killed. Instead, their secret must be uncovered. The Vaesen are all different, as is their secret. So there’s variety, but there’s also structure. There’s always something new to learn.

And it’s not just playing the game that’s better. I also find writing such adventures is more fun, because there’s less ambiguity about what’s involved. You don’t have to invent a random hook and hope the players go for it. You don’t have to reinvent the wheel each time.

But that doesn’t mean there’s no creativity! While the Vaesen rulebook has lots of examples of the creatures, I’m also having fun inventing new Vaesen myself. I’ve taken from my own knowledge (yeti), from, yes, a Pirates of the Caribbean movie (Davy Jones), and even from Russian folklore (Finist the Falcon).

Less Frustration, More Fun

In my experience, players usually like knowing the goal, and only having to worry about exactly how to achieve it. The more I make that ambiguous, I’ve found, the more frustrated my players get. The clearer the mission is, the more fun they have. And that’s what it’s all about!

Death and the Vaesen

1. What’s Fun, What’s Not

Nobody likes it when their character dies in a role-playing game. It’s just not fun, and RPGs should be fun.

But the suspense of being in a situation where your character might die…well, there’s nothing else like it. That’s intoxicating.

How do you square that circle?

Most GMs, especially in D&D, lean heavily on the mechanics that make it virtually impossible to die. Even after you’ve gone to zero hit points, there’s still death saving throws, right? And, in addition to that, your enemies don’t hit you after you’re down. And, in addition to that, your friends are usually available to heal you.

And, and, and.

Those same GMs give you plenty of warning if you’re going into deadly situations, so you can know to prepare more, to be more cautious, while still allowing for a more carefree, chaotic attitude in most cases.

This works, for the most part, but it still feels like an unfortunate dynamic to me. Because it still leaves GMs scrambling when the dice do say that a character has to die.

The alternative is — well, the GM hastily concocts some sort of deus ex machina that prevents the death.

That saves you from the unexpected demise of a character, but it takes the tension away, too. You know, the thing I said was so exhilarating? You can’t both have the delicious suspense of death around every corner and the safety of knowing your character will survive.

For me, it’s a fundamental contradiction of D&D and its ilk.

2. The Horror, the Horror!

In horror RPGs, like Free League’s Alien (yes, from the Ridley Scott movie and its sequels) and Vaesen (dark fairytale 19th-century Scandinavia), death is far more overt and commonplace.

The Alien RPG’s mechanics are specifically designed to replicate the movies, where, once the action starts, tensions run ever higher, and violent ends pile up.

Marrying the RPG format (where you’re supposed to have the agency) with an Alien movie (where there’s tons of inevitable deaths) is not, for me, a great combination. The two sides aren’t complementary: they’re at war.

I want to feel like I have at least some say in whether I live or die, and how. And I didn’t feel like I had that when I played the Alien RPG.

OK…but if that’s true, then why do I think Vaesen, published by the same company, with many of the same rules and even worse player character fragility, works so much better?

Vaesen adventures don’t feel like movies to me. They feel like short stories.

In short stories, there may be a violent confrontation, where people die, but there’s generally only one of them. And every character gets to have their moment in the sun. They’re not just sacrifices on the way to the Final Girl.

You’re more careful under these circumstances, where real death is always right around the corner, but there’s also heightened terror and wonder as well. Personally, I like this setup more than the implied-but-often-not-really danger of D&D games.

And what’s true in D&D is true here as well: if your GM intervenes to save you, the tension goes out the window. So I prefer GMs who will follow through on a dangerous situation. Which isn’t all of them!

3. The Story’s the Thing

The trick is to make the character’s sacrifice matter. And here’s where it evolves from just die rolls and numbers to how both the GM and the player handle the story, the narrative elements.

I’m not a fan of games where it’s all narrative elements, where it’s just players improvising with no rules or rolls — I want the rules and rolls! But, on top of that, you have to weave a tale, something that ties together the randomness of the dice and the structure of the system. You have to make it mean something.

In D&D, the story is more like a novel — and beyond that, a long-running novel series, like Terry Pratchett’s Discworld. The characters always come back for more adventures.

But as I said above, I think of Vaesen at a short story. You craft a beginning for your character, and — partially as the dice dictate — you also craft an end. There’s danger, but you have a choice how to go, how to tell your own saga. Unlike in real life, you get to narrate your own death, to your own satisfaction.

And that’s how I square that circle.

Gated by the Clock

I like a lot about the Stargate RPG — based on a beloved show, a gorgeously illustrated rulebook — but there’s also a lot I don’t like.

Topping the list is their insistence that a single “mission” should only take 3 hours. It’s extremely difficult to deliver a new, wondrous, suspenseful roleplaying experience to your group in that time.

In Stargate—and other mission-based RPGs like Star Trek—you receive your goals up front, which helps. But keeping the action dense and well-flowing still takes a ton of effort.

Each homebrew “mission” I write for my Stargate campaign is like a TV script. I even divide it up into three acts: the Introduction to the problem, the Investigation of the problem, and finally the Twist a.k.a. the Reveal.

There’s usually something the PCs need to figure out, something they need to know, to truly understand the mystery or deal with the antagonist successfully. So there should be clues, but it should still be a bit confusing, right up until it all falls into place (or possibly starts the climactic battle).

Managing the pace of reveals is…really hard. You want to feed them clues, but not railroad them. You want the answers to feel like they come from their actions, but you also don’t want them to be frittering away the minutes on false leads. You can’t reveal everything all at once, but you also can’t require too many torturous steps (any one of which they might get wrong) to get to the prize.

I think I’m getting better at writing it, and running it.

In our most recent adventure, one group of locals, the putative allies of the team, wanted to keep a Naquadah mine open and running, while another group had seized it and wanted it shut down, but wouldn’t say why.

Immediately after coming through the gate, the party was thrown into a banquet involving the leaders of the two groups, and had to both (a) navigate the two sides’ accusations, and (b) ascertain as much as they could about what was going on. That was Act I, and afterwards the players said they enjoyed the give and take of it, the gradual pace of revelations.

The party impressed the second local group enough that they were invited on a tour of the mine they had captured. I planned Act II to be an inspection of at least some of the mine, while Act III would be figuring out the second group’s perspective and dealing with it.

But it turns out, the team had gotten far enough with their discussions with the second group that they were able to figure out the why at the very beginning of the mine outing. (The mine, in addition to Naquadah, also had mercury in it — a twist!)

So the remainder of the mission was just exploring the rest of the mine to find the mercury leak. The challenge was making the final hour still feel like a climax, even though most of the work had already been done.

I sprinkled in some action by having a build-up of carbon dioxide, which caused hallucinations in one character of a Goa’uld firing at them. Extremely simple on paper, though it did cause some excitement. But it was also nice to not have real fighting, since the Stargate RPG is based on D&D 5e, where combat can be cumbersome and time-consuming.

I’m going to get a lot more practice, because we’re only about halfway through the “season”. And while this is a campaign, the techniques I’m employing here are exceptionally well-suited to the one-shots I’m also running these days, in a wide variety of systems. Third-party modules tend to have too many scenes to really be played in a single session, but for the adventures I write myself, I can avoid that mistake.

It’s gonna be fun!

Scary Sharks and Custom CodingKeys

I had to handle a fun little challenge with Codable and unorthodox JSON recently (as you do).

Apple’s Codable API have been around for a while, and it’s an example of the best kind of API: it makes the easy things easy, and the hard things possible.

[
    {
	"type": "Great White Shark",
	"movie": "Jaws",
        "length": 15
    },
    {
        "type": "Megalodon",
        "movie": "The Meg",
        "length": 50
    },
    {
        "type": "Mako Shark",
        "movie": "Deep Blue Sea",
        "length": 14
    }
]

For example, let’s say I’d like to load in some JSON data about sharks in movies:

struct MovieShark: Codable {
    let type: String
    let movie: String
    let length: Float
}

let sharks = try JSONDecoder().decode([MovieShark].self, from: data)

Make a struct with the same fields, let loose your decoder, and you’re done!

(Note: there are a few more steps to get running code, but you get the gist.)

If you need to fiddle with the keys a bit, say, because your data uses shark_type instead of just type, you add a CodingKeys entry:

    enum CodingKeys: String, CodingKey {
        case type = "shark_type"
        case movie
        case length
    }

I’ve tried (admittedly not that hard) to figure how how CodingKeys works.

  • How can defining a nested enum of a particular name cause this behavior?
  • How does the Swift enum type automatically conform to CodingKey?

Googling didn’t turn up any details on this. And, I mean, it doesn’t matter, right?

Well, sometimes it does.

The challenge I was facing was that my data wasn’t in the format shown above, but rather in this format:

[
    {
        "Great White Shark" : {
            "movie": "Jaws",
            "length": 15
	}
    },
    {
        "Megalodon": {
            "movie": "The Meg",
            "length": 50
        }
    },
    {
        "Mako Shark": {
            "movie": "Deep Blue Sea",
            "length": 14
        }
    }
]

The simple Codable approach can’t handle top-level dynamic keys like this. My question was, can I use Codable to do this at all?

And the answer is yes.

Turns out, CodingKeys doesn’t have to be an enum.

You can implement this protocol with a struct that has all its requirements: a string property and initializer, and an integer property and initializer.

Once you’ve done that, of course, you don’t have the static keys you still need, so I stashed those in a separate, unrelated enum called OtherCodingKeys.

struct MovieShark {
    let type: String
    let movie: String
    let length: Float
    
    struct CodingKeys: CodingKey {
        var stringValue: String
        init(stringValue: String) {
            self.stringValue = stringValue
        }

        var intValue: Int? { return nil }
        init?(intValue: Int) { return nil }
    }

    enum OtherCodingKeys {
        case movie
        case length
    }
}

You’ll notice MovieShark no longer declares itself as implementing Codable here. That’s because I need to implement custom versions of Encodable and Decodable separately.

First, Encodable:

private struct MovieSharkContents: Codable {
    let movie: String
    let length: Float
}

extension MovieShark: Encodable {
    func encode(to coder: Encoder) throws {
        var container = coder.container(keyedBy: CodingKeys.self)
        try container.encode(MovieSharkContents(movie: movie, length: length), forKey: CodingKeys(stringValue: type))
    }
}

There are two steps:

  • Get the top-level container of the encoder with coder.container(keyedBy: CodingKeys.self). This is the standard way to start a custom encoding.
  • Specify a dynamic key by using the CodingKeys string initializer, CodingKeys(stringValue: type). You can’t just specify a random string, because that won’t be of the correct type.

Note, because the values of the subsequent dictionary are heterogenous, and Swift can’t serialize a [String: Any] type, I had to make an intermediate type, MovieSharkContents, to represent it.

Next, Decodable:

enum MovieSharkError: Error {
    case unableToDecode
}

extension MovieShark: Decodable {
     init(from coder: Decoder) throws {
         let container = try coder.container(keyedBy: CodingKeys.self)
         for key in container.allKeys {
             type = key.stringValue
             let contents = try container.decode(MovieSharkContents.self, forKey: key)
             movie = contents.movie
             length = contents.length
             return
         }
         throw MovieSharkError.unableToDecode
     }
}

Here, since the top-level key has an unknown name, I iterate through all the top-level keys, and pick the first.

I transfer that top-level key to the type property, and the dictionary values inside it to the other properties of MovieShark. If I don’t find any top-level key at all, I throw an exception.

In this way, I can keep a straightforward MovieShark struct with all the properties I expect, but also handle both loading and saving its custom JSON.

I figured out how to do this, by the way, from the very helpful Flight School Guide to Swift Codable. I still don’t get all the Swift magic behind CodingKeys, but I know a little more about how to use it!

Adventures in Tera

As promised, the Stargate SG-Fun podcast website is up and running.

In related news: I don’t always make websites, but when I do, I prefer static site generators: systems that, rather than constructing website pages right when you visit them using databases and complicated server software, instead run their potentially complicated creation logic ahead of time. That way, the site, after each behind-the-scenes update, always exhibits the same, inert, safe pages to the world.

When my Edge Cases cohost Wolf Rentzsch set up our Edge Cases podcast website (now back from the dead!), he used Jekyll.

When I, Michael Helmbrecht, Mike Critz, and Samuel Giddins set up the updated NSCoderNight website, we used Middleman. In that case, the system bitrotted so thoroughly even within four years that I had to abandon it completely!

So when I went looking around for an SSG to use for SG-Fun, I knew I wouldn’t be using that. And when I did pick one, I picked Zola, an SSG that prided itself on having “no dependencies”. This is, of course, lie — it requires a ton of other components in order to be installed correctly. But I guess, in theory, because it’s a compiled Rust binary, it’s in less danger of being at the mercy of updated or abandoned thirty-party libraries over time, like Middleman was.

Installation

There are two ways to install Zola: brew and MacPorts.

Attempting to install via brew hung on my computer, and all my attempts to update brew — even to try to uninstall and reinstall brew — were unsuccessful. A great start! (Edit: this Mastodon post might help, but I haven’t tried it.)

MacPorts worked better, probably because I could install/update MacPorts by downloading a macOS package file and running that.

Once I had MacPorts working, I could use that to install Zola as described in the Zola docs.

Zola Basics

Zola uses the Tera template engine, created by Netlify, a well-funded startup aimed at enterprise websites. This allows Zola, a one-person open source project, to have a much more capable templating engine that it would be able to create for itself otherwise. Many SSGs adopt external systems for precisely this reason. Tera has loops, conditionals, string manipulation, math operators, macros, all the logic I needed for my relatively simple website.

The downside of such an approach is that, now, you have to look in two sets of documentation to figure out how to do things, instead of just one.

And even then, it was often difficult! I recommend doing what I did, which is download a couple of the theme example sites that are listed in Zola, to see how they put things together.

Zola, like most simple SSGs, has a concept of “pages” (think: blog posts) driven by Markdown data files. These are then organized in “sections”. So you could, say, have a main section for your blog posts (or podcast episodes), and a separate section for an About page.

They also have pagination functionality. If you have two hundred blog posts, it can, with only a little bit of effort (check out those theme sites!), split up your posts into individual pages with the number of posts you specify, such as ten or twenty.

Single Source of Truth

I don’t like retyping the same information into more than one place, because that allows those multiple sources of truth to diverge. Programmers call it D.R.Y.: don’t repeat yourself.

Zola helps with this, but it isn’t perfect.

  • Zola can extract the date of a post from the filename, so I only had to put it there, not both there and in the file’s contents. Well…in the post filename and also the audio filename.
  • I could use macros to take the length of the podcast audio file in seconds, an integer, and convert it into hours + colon + minutes + colon + seconds I used on the web page, while still being able to use it as an integer in the RSS feed.
  • I did have to specify the podcast title in two formats: once in slug format in the post filename (and separately in the audio filename), and once as the full human-readable title. E.g. thanks-send-more and Thanks, Send More.

It all wound up being doable, but I wrote more find/replace logic than I was expecting. Luckily, the Tera programming language, while simple, was up for the task.

One interesting wrinkle is that the “post” file (i.e. podcast episode file), which is an .md file, could not itself contain conditional logic, just plain old data. So when I wanted to use a title in quotes, like Unjustly Maligned Episode #1 “Stargate SG-1” with Jason Snell, I had to put the logic to convert the smart quotes into the appropriate format somewhere else. For HTML pages? “ and ”. But for the podcast RSS file, which would only accept Unicode characters? The vastly less human-readable x201C; and x201D;.

Another example: each podcast episode’s content section contains HTML tags like <p>, for display in web pages and in the <content:encoded> section of the RSS feed. But for the OpenGraph og:description section, that same content had to simply be divided by returns, not HTML tags. Luckily, there are Tera built-in functions to strip HTML tags, and to perform find/replace actions like “find every return and replace it with two returns”.

Sassy

In addition to Tera, Zola includes Sass, a CSS preprocessor. Sassy was absolutely necessary to follow my policy of a single source of truth in the site’s CSS file.

I found myself wanting to, say, use a single value to set the left and right margins of a page, so I could iterate through different values without having to do a bunch of tedious copy and pasting throughout the CSS file.

Sass lets you use variables in your CSS file that are set to one value, but then used in a bunch of places. During site generation, it “compiles” your .scss file by turning all those variables into their concrete values, resulting in a regular CSS file.

It even lets you do simple math on these values.

I only use eight variables total in my .scss file, but it still felt like my visual adjustment/tweaking process became much easier.

One Last Thing

There wasn’t much that Zola wound up not being able to do for me, but there was one thing.

Zola won’t let you generate arbitrary individual files at the top level. Instead, you can only generate files of the pattern NameOfSection/index.html.

This is fine for “post” files (podcast episode files), but I wanted to use the power of the templating engine to automatically generate an .htaccess file for my site.

The best I could do was generate an htaccess/index.html file, and then write my own local script to move it to the right place.

Edge of Tomorrow

In this post I said, “As of now, I probably won’t resubmit [Edge Cases] to Apple Podcasts”.

I wasn’t going to.

But now I have.

And the details might be mildly interesting to someone.

I needed to log in to my Apple Podcasts Connect account anyway, for the sake of the podcast I’m on that we’re moving from the Incomparable.

Once I logged in, I saw the deactivated Edge Cases podcast. What the heck, I said to myself. Let’s try submitting it now that the files are back up, and see if it works.

Nope!

But the only error was that we needed a larger artwork file.

Here’s the original (shrunk down so it doesn’t disrupt the flow of this article):

Image

It’s just the title “edge cases” in white text on black, 512 by 512 pixels.

Apple’s error message said it needed to be 3,000 by 3,000 pixels.

I could have just taken the existing file and upscaled it, but I wanted to do better than that.

Whatever original art file Wolf used to generate this is lost in the seas of time. So to make a bigger version, I’d have to recreate it.

First problem: the font is unusual. I didn’t know what it was, and it didn’t appear to exist on my computer.

The Internet to the rescue! There are apparently websites out there which take an image, and spit back out the names of the fonts that are used in it.

I’m going to try not to think about how free websites like this make their money, probably by taking the image and uploading it to evil ChatGPT artwork generators, but hey, free service.

Here’s the one I used: https://www.fontsquirrel.com/matcherator

It told me that the font was “Gara”. Yeah, I definitely didn’t have that one already. Another Internet search told me that I could indeed download the Gara font for free from a variety of websites. The one I chose was FontZillion: https://www.fontzillion.com/fonts/iaki-marqunez/gara

(It apparently was added to FontZillion a quarter century ago.)

So, first of all, I downloaded the font files and added them to my Mac via the Font Book application.

Then I went about recreating the logo, in Acorn. I upscaled the old image into a new 3,000 x 3,000 Acorn image file, and typed in, resized, and arranged the new text until it matched as much as it was going to. If you squint at this animated gif long enough, you’ll see the minute changes that occur when I switch from the old file to the new one:

Image

I uploaded the new file to the https://edgecases.com website with the same name as the old one, waited a bit for that change to propagate, and bam, the next time I submitted the podcast to Apple, it was accepted.

Case closed!