Code Architecture: approaches for 'resuming' server code after user input?

I'm writing a game server in Rust using tokio & grpc, and one problem I have is that my game logic often needs to prompt the user to make some decision before continuing. This makes the code a lot harder to follow because I essentially need to rebuild all the context, state, and logic that lead to the decision to prompt for input before continuing (deciding whether user input is required and what kind of prompt to show is quite complex), and a given user action is often split across a sequence of 3 or 4 different input prompts.

In an entirely local game, you can just block for user input which makes everything much simpler to read in a single linear flow:

let state = build_complicated_state();
if should_prompt_for_choice_13() {
  let choice13 = stdin.read_line();
  state = apply_more_logic(state, choice1)
  let choice27 = stdin.read_line();
  write_state_to_database(state, choice27);
  let choice42 = stdin.read_line();
  // etc
}

But on the server, of course, each action is split off into a separate RPC endpoint:

fn choice_13() {
  // Rebuild all of the state, figure out why we're doing choice
  // 13 right now, etc
}

It's a little hard to demonstrate this using small code snippets, but the logic gets a lot harder to understand as its spread across N different RPC functions. I'm wondering if there are any techniques for structuring Rust code which would get me closer to the synchronous 'immediate mode' experience where game logic can be expressed inline?

You could use async and Future for this — that will let you write straight-line code in an async fn. As the foundation of this, you'd need to implement Futures that express “wait for user input”, i.e. that become ready when the user's RPC comes in.

The fact that you have write_state_to_database() in the middle of your code suggests you want to be able to resume “in the middle” in case the server restarts for any reason. In that case, that wouldn't even work by default in the local example — you'll need to add another feature to your prompt-the-user futures, the ability to record the choices so far and replay them when the server restarts, up to the last point you were actually waiting for not yet provided input. (Then you can discard that as soon as the entire async fn completes.)

This might or might not actually be a good idea. It does let you write the kind of code you want, but it might turn out that you actually need a more elaborate system which doesn't stick to a single sequence of operations. It will depend on what kind of game mechanics you're writing — especially whether it is multiplayer or not.

This is, fundamentally, the transformation that happens under the hood for async functions, and the trouble you're describing is a big part of the reason that async syntax is so widely successful: it lets programmers think about the state machine in terms of sequential instructions, rather than in terms of state tables and transitions, even though it's compiled to the latter.

If you need to be able to recover the state from stored data, then you mostly can't avoid dealing with tables & transitions somewhere, but the Future trait is general enough that you may be able to press it into service for this, provided that you also insist that each Future implement or provide access to some type that lets you serialize and reconstruct them. I've no idea how feasible that is in practice, but it'd be something I'd be interested in investigating, in your shoes.

If you can't use Future for this, you may be able to write your own state-machine transformer using the same syntax on the programmer's side, processed by a procedural macro to produce the associated types and traits. It's a much larger project, and you'd need to do some serious design work to determine what that looks like, but it may be worth the investment if being able to write code similar to your local-game example is important to you.

A lot of games don't bother, and track states manually using ad-hoc state machine code. That's fine, and works well, but also, *gestures at the speedrunning community* it has externalities.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.