1
\$\begingroup\$

So I'm trying to understand how multiplayer works in Godot and the docs are very clear when one application is the host and the other applications join the host.

Now i want to have a dedicated server and there's 2 ways of doing that: just run the executable in headless mode or export it as a dedicated server for a smaller executable that can only be run in headless mode.

I then run the executable from cmd or powershell but I don't get how I would stop the server without throwing any errors (and saving data before actually terminating)

Just closing the console results in the following errors (for a split second) and doesn't trigger the close request notification: enter image description here The result is the same if I use the provided *.console.exe Godot produces. (Tried with 4.2 and 4.2.1)

This is independent from having actual network code and can be reproduced with an empty project.

So my question is: What is the correct way to stop a headless Godot application? (+is there a notification I can use to save game state?)

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

Close the server

In theory ideal way to close a server is for it to close itself, if it is going to close at all. And it should only be possible to trigger this locally. Since there is no Window, the standard way to do this is by reading from standard input.

I'll try to do that for Godot...

We will implement a custom MainLoop. Create a script like this:

class_name CustomMainLoop
extends SceneTree

And configure it in project settings under application/run/main_loop_type, you write the class name (CustomMainLoop inb this case), for example:

MainLoopType set to CustomMainLoop

And yes, this will be the SceneTree (not the root of the SceneTree, but the SceneTree itself).

Next we will override the _initialize method, and we will detect if we are running in headless mode:

func _initialize() -> void:
    if DisplayServer.get_name() != "headless":
        return

And we will read standard input with OS.read_string_from_stdin(). However, we need to run this in another thread because otherwise we are blocking the actual game.

Thus, let us do this:

class_name CustomMainLoop
extends SceneTree


var _input_thread:Thread


func _initialize() -> void:
    if DisplayServer.get_name() != "headless":
        return

    _input_thread = Thread.new()
    _input_thread.start(_listen_to_standard_input)


func _listen_to_standard_input() -> void:
    while true:
        prints("Insert some text:")
        var line := OS.read_string_from_stdin().strip_edges()
        prints("You inserted:", line)
        match line:
            "quit":
                prints("Quitting")
                _input_thread.wait_to_finish.call_deferred()
                quit.call_deferred()
                return

By the way, you know that we usually use get_tree().quit(), well, we are running the code on the SceneTree, so we just need to call quit. But we do not want to call it from our secondary Thread. A deferred call solves that.

Similarly, we are having the main thread wait for our secondary Thread to finish, so we do not leak the Thread.

However, this is not perfect, I have run into a couple problems:

  • In PowerShell I only get one character at a time. The naive workaround made it unusable.
  • In CMD the console gets the input first. So I need to press Enter which has CMD reply with the prompt, and then what I type gets send into the game.

So, don't. Instead create a bat file, like this:

my_game.exe --headless

And use that to launch the game server instead of CMD or PowerShell.


Your Error

This seems to be a regresion of these bug:

This might be related:

If you can make a minimum reproduction project, please report at https://github.com/godotengine/godot/issues


Exit Notifications

is there a notification I can use to save game state?

I suppose you are aware of NOTIFICATION_WM_CLOSE_REQUEST, but these are Window Messages, so they won't work on headless mode.


Your Nodes will be removed from the scene tree. So their _exit_tree gets called, and they also get a NOTIFICATION_EXIT_TREE right away, followed by NOTIFICATION_UNPARENTED a little later.

If your cleanup needs to happen from an Node while inside the scene tree use _exit_tree or NOTIFICATION_EXIT_TREE in one of your Autoloads.


Futhermore, many Objects will get a NOTIFICATION_PREDELETE. Use this as your finalizer.

And those Objects also get a final undocumented notification 3 which internally has the name NOTIFICATION_PREDELETE_CLEANUP... Which is not intended for you to use.


But your static variables are not attached to an Object, let alone to a Node. Let us say you must clean them manually when you know the executable is losing.

Well, let us go back to our custom MainLoop, and override the _finalize method:

func _finalize() -> void:
    prints("_finalize")

After everything else is said and done, your _finalize will be called. And yes, you can access your static vars in _finalize. As far as I know it is your last chance.

Oh, but we would have an instance CustomMainLoop, does it also get notifications? Yes. Does it get NOTIFICATION_PREDELETE or NOTIFICATION_PREDELETE_CLEANUP? No, it does not.


Addendum: The @static_unload has come to my attention. You should be able to annotate a script with it, for example:

@static_unload()
class_name MyClassWithStaticMembers

static var _something:Resource

And this would tell Godot that it can free the static variables early.


Leaked Objects

Ah, but let me tell you something: your Scripts are Objects, and they do not get to be freed in the above process. Also you might have preload some Resources that would not have been freed yet - unless you freed them manually (and I'm not suggesting you do that). And there might be other Godot internal Objects lingering around.

If you run your executable with --verbose you will get everything that is being leaked printed after _finalize is called. Of course they get freed when the executable exits nontheless.

These do not cause the error you are seeing. The error you are seeing is caused by a bug. If you can make a minimum reproduction project, please report at https://github.com/godotengine/godot/issues

\$\endgroup\$
1
  • \$\begingroup\$ This seems to work and solves my problems. I'm now getting a different error that is already reported here \$\endgroup\$
    – LoonPie
    Feb 22 at 17:18

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .