Development Progress Report, week 59

Last week, there were a bunch of things that weren’t quite right. Attempts to improve font-handling were bungled, music wasn’t restoring properly when the game was reloaded (which I thought I fixed, but I wasn’t entirely correct), and image-transitions were kind of fouling up my cool game-loading effect.

This week, that’s all sorted.

There were still a few places where I was holding a copy of a bare TTF_Font pointer separately, and that was part of my problem with fonts right there. Related to that was my notion of a ‘default font’. It was a TTF_Font pointer that we’d use instead if a text-renderer ever got a NULL font pointer.

Turns out that is what led me to my problem. I’d been relying on that behaviour for so very long that I hadn’t realised that I had just plain forgotten to tell nearly all the widgets what font they should be using. So they were NULL, which got us the default font. Which could disappear without notice. Oops.

That’s all sorted.

Ditto, background music. The new fix made things right, except when you scrolled back through the narrative. Then it glitched slightly, exactly once each time scrollback commenced, and the track restarted.

Some bunny had forgotten to check if we were already playing the background track at that point. I’m not telling you which bunny, so hand me a carrot and we’ll move on.

The image-transition fix didn’t display any lingering issues, so that one was a win.

So, this week, I broke all existing saved games.

A save-file, ultimately, is a big serialised block of data. Well, maybe not that big. Around 150Kbytes or so, plus an image (the image, alas, is a BMP, running to around 7MB). All sorts of game-state is jammed in nose-to-tail in a text-file.

But no more.

Since about save-version 12, I use file-blobs similar to those used for the compiled story-file. A file-blob is a little pseudo-filesystem in a file. Prior to SV12, a save-file was a plain text-file. After SV12 it became a text-file in a file-blob.

Then there were a bunch of save-breaking changes from SV13 to SV15. Now, I think I’m past that. The new version (SV16) breaks the save data up into a bunch of little files inside the file-blob. This should make it a whole lot easier for me to add data or alter data-formats without actually breaking existing saves. Hopefully. Got my fingers crossed.

It’s also given me some ideas for improving runtime memory-management by changing the compiled story-format.

So, old saves are now invalid. Sorry about that.

It did mean that I also finally got around to adding an on-screen widget to allow old saves to be removed. It’ll need a lick of UI-polish later on, but it works exactly as intended.

It necessitated juggling the layout to the left a little to make some extra room. Probably too far, as it turns out, but that can be corrected.

A tale of three Tokenisers

So, I had a quick-and-dirty tokeniser that was in the code-base from day one.

It was simple enough:

WstringConStore tokenise(const std::wstring &source, const wchar_t delim)
{
 WstringConStore result;
 if (source.length() == 0)
  return result;
 result.reserve(16);
 std::basic_stringstream<wchar_t> source_stream(source);

 std::wstring tmp;

 while (std::getline(source_stream, tmp, delim))
  result.push_back(tmp);
 if (source[source.length() - 1] == delim)
  result.push_back(utils::empty_string);
 return result;
}

But could it be better? So I wrote a second one, and timed their performance.

std::pair<std::wstring, std::wstring> quick_split(const std::wstring& s, wchar_t delim)
{
 auto i = s.find(delim);
 if (i == std::string::npos)
  return std::make_pair(s, L""); // If there's no delimiter, just return the whole string as the first part
 return std::make_pair(s.substr(0, i), s.substr(i + 1, s.length() - 1));
}


WstringConStore alt_tokenise(const std::wstring &source, const wchar_t delim)
{
 WstringConStore result;
 if (source.length() == 0)
  return result;
 result.reserve(16);
 std::wstring s(source);
 while (s.find(delim) != std::string::npos)
 {
  auto tmp = quick_split(s, delim);
  result.push_back(tmp.first);
  s = tmp.second;
 }
 result.push_back(s);
 return result;
}

It turned out to be almost too close to call, though the second version was just fractionally better. Probably.

So, I ended up writing a third version. This looks more like one of my old first-year students would have written:

WstringConStore alt_tokenise2(const std::wstring &source, const wchar_t delim)
{
 std::wstring line;
 std::vector<std::wstring> result;
 result.reserve(16);
 bool entry = false;
 const size_t sz = source.length();
 for (size_t i = 0; i < sz; i++)
 {
 entry = true;
 if (source[i] != delim)
  line += source[i];
 else
 {
  result.push_back(line);
  line.clear();
 }
 }
 if (entry)
  result.push_back(line);
 return result;
}

And you know what? That beats the pants off the other two, in terms of performance! It’s not that the original code was slow, either. It’s just that it could be faster!

Lesson: Sometimes it is best not to try to be too fancy. Simple works. But always test and time your code, to be sure.

We use the tokeniser a lot, so this massively sped up nearly everything. Loading the whole story-file takes ~600 millisconds now, and saving/loading a game, as little as 11 millisconds (that’s on my development box, your mileage may vary).

With those operations now hugely speeded up, there’s actually cycles that can be used to improve memory-management. It’s fast enough, that I think I can just load scene meta-data initially, and package the scene bodies (performances, I call them) into their own little sub-files in the story’s file-blob, to be loaded/discarded on an as-needs basis. That’s worth trying, and I will probably look into that over Christmas.

Thanks for tuning in again this week!