Highly Suspect Agency

Let's look at Stepmania's code

quat, Apr 02, 2023 — games stepmania

StepMania is a nearly-old-enough-to-drink piece of software by the dance game community. It's impossible to succinctly describe, it is everything and nothing at the same time - it was originally designed as a PC-based clone of Dance Dance Revolution but the code has found its way to game consoles too, and even circled its way right back into the arcade, it's open-source but also closed-source, it has been worked on by countless groups and communities and collectives and people and basically the only constant is that it's a labor of love.

Stepmania history

Some places to look:

The general dates in question (sourced mainly from Varela's archive):

And a short description of some of these spinoff projects:

All of the holdout 5.1-new/5.2 developers seem to be busy on Outfox or ITGmania. Stepmania is dead, long live Stepmania.

Ok, that's enough context. What's in this thing.

Directory tour

The src folder contains most of the game's source code in a single, flat directory structure. I'll go over the handful of subdirectories first.

arch and archutils

Simple platform abstractions. When Stepmania wants to display a LoadingWindow, for example, there are implementations using the Windows API, GTK, Mac, some older versions have Cocoa and SDL implementatons... arch_default.h contains the necessary preprocessor magic to include the right files on the right platforms. arch/ArchHooks has a bunch of one-off methods that require different implementations on each platform, like "opening a URL in the browser".

smpackage

This goes wayyyy back, at least earlier than 1.64. The original StepMania author envisioned a Stepmania-specific content package format. Quoting README-first.html in the 3.0 download:

The StepMania package format was created to make the distribution of songs and other add-ons very easy. StepMania package files have the extension '.smzip' and can be installed by double-clicking the .smzip file.

A StepMania package is 'installed' by extracting all files in the package to the StepMania program directory. This allows songs, courses, themes, and visualizations to all be installed by the Package Manager.

The file format of an .smzip file is actually the PK-Zip standard. This means you can rename any .smzip file to have the extension '.zip', and then open the file in any compression application (e.g. WinZip, WinRAR).

The StepMania Package Exporter (smpackage.exe) can create packages of your song, announcers, themes, or other add-ons. Simply launch the Package Exporter (Start Menu->Programs->StepMania->Package Exporter), click the items you would like to make into a package, then click the one of the Export buttons. "Export as One" will take all of the selected items and make one package that contains them all. "Export Individual" will create one separate package for each selected item in the list.

This didn't catch on much with the community - off-the-shelf zip programs turned out to be good enough - and smpackage is no longer a separate program, but you might find a lingering .smzip file association on your Windows computer.

Cutely, a revised version of this scheme is still listed as a "future project" on the Stepmania 5 wiki.

Horribly outdated stuff

Top-level source concepts

"Rage"

This is a light "game engine", no relation to the one from Rockstar, dating to at least earlier than Stepmania 1.64.

Input devices and joysticks, math (including matrix math), filesystem, sound playback, image loading, etc, will often be implemented in classes starting with the word Rage.

Lua bindings

Generally when something is exposed to Lua, the glue code listing the exposed fields and methods will be written at the bottom of the .cpp file. The glue is implemented in LuaBinding.h/.cpp. SM3.9 doesn't expose Lua bindings.

Ok how does this engine work

In this section when I refer to "the theme", I'm also referring to "any scripting gunk that might be contained in a modded simfile"; mod files work by placing actors and scripts on the screen just like with theming.

I will be comparing:

⚠ There's gonna be some stuff where I'm like "oh this was added in OpenITG", but it was actually added in to Stepmania "3.95", in the interim between v390release and OpenITG's fork point. I don't know exactly which CVS version of Stepmania OpenITG forked off of and I'm just clicking around looking for convenient tags to browse on Github anyway.

Actors

(TODO: cover updating/drawing cycle, at least for comparison with Screen)

What is an actor? A miserable pile of secrets. quietly-turning describes them so:

Actors are the basic building blocks used to script content for StepMania. When the player sees and interacts with something on-screen, like a menu system or a 3D model of a dancing character, that something is an actor.

There's an actor for displaying rectangles (Quad), text (BitmapText), images and video (Sprite), the groove radar used in some themes (GrooveRadar), the arrow playfield (Player), a copy of another actor (ActorProxy), and if you generalize the notion of "something you can see" you'll find actors that contain other actors (ActorFrame), actors that play sounds (ActorSound), and so on and so on and so on. Actors all the way down.

Some actors are very general-purpose (like Sprite) and are mainly configured through the theme. Other actors (like Player) mainly end up configured through the C++ engine code. A given Screen might create a few actors, or it may expect the theme to have placed down actors with specific names and types in order to function (you can imagine ScreenGameplay is not very interesting without any Player actors, right). I will talk about Screens later.

Each actor has a "tween state", which includes an x/y/z position, pitch/yaw/roll rotation, x/y/z scale ("zoom"), X and Y skew, four crop amounts (one for each edge), four fade amounts and diffuse colors (one for each corner), and a glow color. The tween system allows the theme to set any of these properties, animate them over time, and query their current value - because they can be queried, SSC also adds an "aux" variable to the mix, allowing the tween system to be used to drive an arbitrary float. (Each actor can also have "effects" applied to it, which are rudimentary animations that accomplish a similar thing to the tween system.)

Additionally, each actor has:

Basically there is "a lot" of stuff stored for each actor - enough to position and animate it anywhere you like, and a couple of mildly advanced rendering abilities.

Commands

In general, a "command" is a short program that goes into a named slot. When you call a command on an actor, it will run the program, and bubble the command downwards into each of its child actors.

Some examples of what the command system is used for:

The implementation is pretty different in SM3.9 and in OpenITG/SM5.

SM3.9

A command is a semicolon-delimited list of tween instructions. Commands are always stored in metrics.ini. Here's one from the Stepmania 3.9 default theme:

[ScreenSelectStyle]
# ...
PremiumOnCommand=addx,400;bounceend,0.5;addx,-400;glowshift;effectcolor1,1,1,1,0;effectcolor2,1,1,1,0.3

When calling a command on an actor, the engine will:

(This command would be found when executing the On command, on the actor with ID Premium, while on the screen named ScreenSelectStyle.)

SM3.9 has decently big list of instructions that can be applied to actors. ActorCommands.cpp ParseCommands parses this string into a vector<ParsedCommand>; there are six ParsedCommands in this example string, separated by semicolons. When the actor executes this command, it will move 400px to the right, set the tween mode used for further commands to bounceend over half a second, move 400 pixels back to the left, play the glowshift effect, and configure that effect's colors.

Note that the list of instructions may change per-actor type; it's a virtual function, actor implementations can override it to respond to more commands.

It is limited because while there's a lot of options, all you can do is punt around the variables on the actor.

OpenITG

OpenITG (probably actually 3.95) added two things:

Commands are now stored on the actor itself, in a map indexed by the command name. When loading an XML actor, if there's an XML attribute with a Command suffix, the prefix of the string is used as the name, and the text is parsed as a command.

Commands are always Lua functions. OpenITG added a Lua API to configure the same things the old command system could configure.

The command parser became much stranger. If the command string starts with %, it is replaced with return , and the resultant string is returned. This lets you write Lua functions as a command, like this one from the OpenITG theme metrics.ini:

ScrollerOnCommand=%function(self) self:z(-200); self:SetDrawByZPosition(true) end

If it doesn't start with a percent sign, the string is assumed to be a command in the SM3.9 format, and the engine will update it into a Lua function:

It's still possible to define commands with the metrics system - if the actor does not define a command with the given name, metrics.ini will be checked for the command. Commands can be defined using either format (list-of-tweens or a Lua function) in either location (on the actor or in metrics.ini)

SM5

Similar to OpenITG. The string munger is moved to LuaHelpers::parseCommandList, which also handles the % syntax.

SM5 also added the ability to create actors from a Lua file. When converting a Lua table to an actor, properties ending with Command may be directly set to Lua functions (no string-roundtrips or % signs required).

Messages (not in SM3.9)

Commands are per-actor; you dispatch a command on an actor, and it trickles the command dispatch down through the actor tree.

Messages, on the other hand, are global. The message manager is a global object, anything can subscribe to be notified of a message, and anything can broadcast a message.

It's possible to define "message commands" on an actor (remember that actors are only one type of message listener). There isn't a single concept of "message command"s in the game code, they are simply an interaction of the command system and the message system:

Then, when something broadcasts Blah:

Messages can't have arguments. If there's some data to be posted along with a message, the convention is to stick it in a global variable somewhere before posting the message.

The message system is how DivinEntity was able to communicate data about which arrows were being pressed to any simfile who would listen. (NotITG now broadcasts the messages from the engine.)

Worth noting that QueueCommand is a tween instruction that invokes a command on itself when being evaluated. Similarly, QueueMessage is a tween that broadcasts a message over the message manager when being evaluated.

quick diversion into input types

Annoyingly there is overlap between them - there is DANCE_BUTTON_COIN in GameInput as well as MENU_BUTTON_COIN in MenuInput. Even things like DANCE_BUTTON_MENULEFT. I dunno.

Screens

A Screen is a type of ActorFrame, which tells you a bit about what they are - a screen contains zero or more actors.

Screens update every frame, and receive a method call whenever an input event happens. There is also a notion of a "transparent" screen (where screens below it need to be drawn first - more on that with the screen manager), and a screen message system (unrelated to the OpenITG message manager). Screen messages can be posted now or take place in the future. They are generally things like "the menu timer expired", "go to the next screen", "go to the previous screen", "stop playing music" etc.

The screen manager

The screen manager maintains a stack of screens. Screens can be pushed onto the stack or popped off. The "top screen" is considered to be the one with focus. When pushing a screen onto the stack (or when replacing the screen with a new one), the SM_LoseFocus screen message is posted to the old screen, and SM_GainFocus is posted to the new screen.

The stack system is not used much. It's not that "song select opens the gameplay screen on top of it", it's used more for things like the Ok/Cancel dialog that shows up to confirm your autosync result. That's a screen.

The screen manager can also prep a screen, which constructs and initializes it (getting all its actors in place, calling the Init command on all of them, etc) without actually making it the top screen yet. The purpose of this is to prepare screens that will be used very shortly, like how ScreenGameplay will almost always be used after ScreenSelectMusic, so might as well prep it now.

There's yet another thing called a "message" - "system messages" are fortunately not too much fancy, they're for debugging toasts. As usual, in SM3.9 the actor responsible for showing the toast onscreen was hardcoded in C++, and OpenITG punts it to the theme; it sets a global variable then broadcasts SystemMessage.

XML actors and screens

SM3.9 had a pretty simple notion of screens. The C++ constuctor would initialize all the screen's actors, put them in the ActorFrame, and that was that.

OpenITG's system of loading screens from XML added much more complexity. In OpenITG, I thiiiiink the entrypoint is ActorUtil::MakeActor, which is sometimes called from ScreenManager?

LoadFromActorFile is the meat and potatoes, the rest is convenience for setting up a (not configurable...) actor - nothing you couldn't do by manually making a Sprite actor from XML. So let's look at that instead.

Or, simplifying for the common case without the funny special-cases and conveniences:

For ActorFrames, well first when you make it with XML you actually get a slightly different ActorFrameAutoDeleteChildren instance, but its LoadFromNode method will recurse into the XML structure and call ActorUtil::LoadFromActorFile again.

TODO: What I'm struggling to find is what connects Screens and Actors. The only method that looks in the screen registry is ScreenManager::MakeNewScreenInternal, which constructs the screen with the given archetype and (in the REGISTER_SCREEN_CLASS macro) calls init. I think this is the wrong place to look and the lua stuff is actually in BGAnimations with the underlay/overlay system?


← How the hell do you use the command line
The Treadmill →
Powered by Rust