ScoreMonitoring v2: Going Online for SMASH! 2023

Our monitor so far...

Previously, we’ve talked about how one would create a web API that can read scores from StepMania’s folder, and use it to do fancy stuff like creating a live feed for a stream. Naturally, this is best implemented in the form of a web page that gets loaded into OBS as a browser source.

Singular PC

The most simplest solution is if everything is running on one computer: next to your TV and pad, you have a desktop PC responsible for all things OBS (i.e. compositing sources, encoding, streaming, etc), playing StepMania, and perhaps on a second or third monitor, your live chat feed, Discord, and maybe something else – or if not, then at least you have some strategy to Alt+Tab back and forth.

The API is run locally, and you’d contact it via localhost. Too easy.


When it comes to the arcade, you have less freedom. No second monitor. No keyboard or mouse (older cabs lack public facing USB ports, you’d have to open the back). The biggest one by far: no internet access. Wireless offline connectivity is probably as best as you will get.

Besides the potential security overhead of having an unattended PC online, why would you ever agree to store your stream key on a public cab?

Instead, all the cab provides are just the audiovisual outputs and the score monitor for you to ping. You bring your streaming gear and internet connection.

Now, instead of using localhost to talk with the score monitor, you connect to the cabinet’s assigned network address. Note that the cabinet should be set up with everything needed to allow a connection through (reverse proxy, firewall, correct CORS headers, etc).

Here are both scenarios in a diagram. Left is a typical home singular PC setup, right is my arcade setup:

Schematic diagrams for a single-PC stream setup and an arcade situation, where the responsibilities of OBS and StepMania are on the BYO laptop and ITG cabinet, respectively
it’s practically identical save for the software being split onto two devices

Extending the monitor

Having a monitor that can process scores opens up a lot of cool shit and allows me to get pretty creative with competitions, which is necessary for ITG.

If rhythm game tournaments have shown me anything, it’s that skill gaps amongst the community are incredibly wide.[1] So, to reduce this “gap”, I often tend to lean towards co-operative tournaments, or introduce some other gaming elements into them, such as luck or strategy.

Implementing unlocks

Naturally, one of the first things one would think of with a score monitor at hand is an unlock system. So, that’s what I did with SMASH! 2023. It involved players collaboratively “exploring a tower” to unlock the songs in a setlist one by one.

StepMania has an in-built, code-based unlock system, but apparently the implementation to do this was convoluted.[2] There is another way around this, though – and that’s to manage it ourselves.

Normally, StepMania only checks for songs upon starting the game and manually reloading via the operator menu. However, with ITGmania, this capability was extended to within the song select screen.[3]

Going with this capability, the process might look something like this:

  • The current score monitor has to be asked to check for scores. Instead, I changed this to a FileSystemWatcher which listens to file modifications in the Uploads folder (scores are written when players enter the results screen after the song ends), and then does something.
  • The current score monitor also just returns the scores that it reads. However, we could very much as well add logic to this: were the score(s) a pass? Were the percentage(s) above or below a threshold? Did they play with certain modifiers? Whatever the data contains, is something we can check against. Most of SMASH! only required a pass (i.e. the grade is not Failed), but “special floors” looked at additional things in these scores.
  • That something that the file watcher will take action on is therefore (a) performing the checking logic, and if passing, (b) move my locked content songs from somewhere outside of the Songs folder, into it. To save space whilst keeping the contents of the locked songs folder intact, I don’t move folders around, but instead use symbolic links to create “shortcuts”.

All this occurs whilst the player is in the results screen. The last step to put it all together was to modify Simply Love’s game loop, such that when the player proceeds from that screen, it will always trigger the Songs refresh sequence.

And with that, we’ve implemented an unlock system that’s more customisable and easier to modify than the one that comes by default.

The score database (cabinet)

Whilst we do have an unlock system, it’s important to know that this should be a separate service from the original score monitor we’re basing this on.

The score monitor, as we have it, already does the job as required: obtain scores after a certain date. The only modifications we would probably need are to:

  • Return slightly different data. In my case, I needed the score’s GUID, name, percentage, and stage ID (derived from the substring of the folder name, which I’ve established a naming convention, e.g. [16] isophote, [07] Sweet Appetite, etc)
  • Filter out scores that don’t come from the SMASH! folder, as a prerequisite/consequence to that
Entries in the score lists contain a stage ID, which is based off the first few characters in SMASH’s song folder, as shown
spoiler alert: the stage id is not always necessarily a number

We could totally work with this as it is – except... there’s a small, but major, problem: I had a tiebreaker song.

This song’s unlock condition required knowledge of the history of every player’s score. Players obtained “accolades” for simply passing (repeatable), being the first to pass/meet the requirements of a song, and/or getting the highest percentage on them. (You can see how I’ve made the most out of the advantages of automation here.)

Only if more than one player, at the end of the day, i.e. date < {saturday 16:30}, had the same accolade count, i.e. Where(p => p.Accolades == top).Count() > 1, does this song unlock.

Given that we’ll be frequently reading those XML score files to calculate accolades, it’s more efficient to instead store the scores in a database, and have the unlocker and monitor use this, when respectively checking and saving/returning scores. For this use case, SQLite should do the trick. Though, you can of course stash it in a proper database of whatever flavour you like – be it relational, document-based, etc.

The website

What’s a competition this complex, if players can’t even keep track of their progress? Thus, the need for a public facing site arises. Outside of front end web design, the functional features that needed to be implemented include:

  • [Public] Basic static pages for competition information
  • [Public] Score feeds, calculating accolades, and listing them in various sort orders and filters
  • [Public] Player leaderboards
  • [Public] Current tower progress
  • [Admin] Security, such as admin account capabilities and JWT tokens for the API (mostly with the help of Identity, but no idea how I managed to pull the latter off so easily)
  • [Admin] Fixing up scores (e.g. duplicating couples scores so both players get the credit, or adding names to scores players forgot about)
  • [Admin] GET/POST endpoints to modify scores to the website side’s database
  • [Admin] Other sanity checks, such as returning the dates that the website is set to filter scores by (so that earlier test scores are not displayed on the website and those past the competition period don’t count, etc)

I wasn’t quite sure how else to implement this, but the score list from the cabinet had to be duplicated on the website’s side.

The middleman

The cabinet doesn’t have internet access – only local wireless communication.

My laptop, when combined with an additional wireless dongle, does.[4]

So, I had to develop a so-called “middleman” that would be responsible for bridging the connection between the two: relaying the cab’s data, i.e. pinging the score monitor, to the website, i.e. POSTing the results.

I’d say this part of the system was the make or break of the entire project’s purpose, since without it, there’d be no way for the public (straight up) and me (without laborious manual calculations, which would be impossible to promptly resolve) to know who won.

If we were to make comparisons with the streaming setup, this middleman would be the equivalent of the browser source, the one responsible for pinging the cabinet for scores and ultimately relaying to the public-facing stream what was played. In that sense, you could also say without the browser source, the whole setup’s purpose is also defeated – except, unlike SMASH!, there’s no need to go through the trouble of persisting those scores for later retrieval.

For the middleman to upload scores consistently, it necessitated that the laptop didn’t come across any connection problems, or randomly went to sleep, etc. If for some reason it did, then we’d have to cover that gap of inactivity. This is where the “last score uploaded” endpoint on the website comes into play; it’s called when the middleman is first loaded, and determines the range of scores to get – which, as we know, we already can specify.

Putting it all together

The schematic for my SMASH! 2023 setup, extending the score monitor to allow storing the scores from the cabinet onto a server via a BYO laptop
trust me it’s actually not as complicated as it looks


The morning of Saturday at SMASH! 2022, two hours before opening, all three of Martin’s cabinets refused to boot – even though, as cliché as this is going to sound – it worked perfectly the Friday night before; after all, we were playing on them until ICC had to close. He did eventually get them up and running though, albeit by the skin of the teeth, with only minutes to spare.

CORS, a security relaxation feature acting upon the same-origin policy when dealing with requests – in our case, like submitting or retrieving scores – was my equivalent of that stressful golden hour. The key word to note here was relaxation, aka whitelisting: it won’t work unless I tell it to, not vice versa.

When I’m developing the website (or score monitor) on my laptop, I’m running it directly via dotnet, so any score submission/retrieval requests won’t proceed unless I enable the appropriate CORS headers in the app.

However! When the app lives on my server/cabinet, it’s running behind nginx. If nginx already has CORS enabled alongside my app, the browser refuses to process the request because it saw multiple allow headers, one each from those two. So, a decision must be made to determine who is responsible for handling CORS. On the cabinet, it was nginx; on the server, it was the app. Pretty confusing.

Also, not only does CORS require the existence of certain headers, they have to be correct as well. At that time during SMASH!, I was missing a specific method in a header in nginx on the cabinet’s side. When this happens 25 minutes before gates open, the pressure can make your problem solving capabilities degenerate into negative IQ territory. I was basically searching up error messages with the answer quite literally implied in it.

There’s also a quirk regarding null origins when it involves loading web pages that come from your file system instead of the internet. This must be accounted for as well, and I first came across this when developing the original score monitor.

– Allow me to remind you again, that all it takes is just one of these above misconfigurations or omissions to put a stop to everything.

Positively speaking, though, that’s one less problem I have to stress about. Next year, I’m calling it now, I bet I’ll be dealing with a whole new concept that’ll threaten to halt 2024’s tournament in its tracks.

man, cors...

[1] Here in Sydney, at least for DDR, and especially ITG, everybody mostly knows their place. It’s pretty easy to predict who will win a traditional tourney just by looking at the competitors.

[2] Official ITG did something regarding executing said unlock function with hard coded codes, and storing progress somewhere for persistence. This meant the implementation was reliant upon the theme, and I’d honestly prefer it if I didn’t have to touch that. In fact, Simply Love’s description for the unlock system states it “doesn’t support the unlock system, and I don’t know of any other theme that does”.

[3] StepMania scans the Songs folder to determine what content exists. So, if it’s not in there, it’s not visible to the game. This is how I hide the yet-to-be-unlocked songs.

ITGmania was designed for players participating in global ITG events like Stamina RPG, where players start with a base song list that they would put into the Songs folder. As they complete them, provided they are connected to the internet to contact the score servers, they can unlock new songs, and ITGmania would download this new content into the Songs folder in the background. However, players still had to manually reload the list.

[4] The ICC, of course, only provides courtesy WiFi, which I’m not aware of its limitations. The worst case scenario is that it’s limited by time. So, I brought my own mobile phone to provide guaranteed internet access for the entire day.

To access the cabinet’s hotspot (which was taken up by the laptop’s internal wireless module) and my mobile at the same time, I had to provide another wireless dongle.