Defekte Sitzung gibt Mitspieler frei, nur echte Gäste beenden die Pause

Hält der Kern mit einem internen Fehler an, schließt ein Host seine Welt
für Gäste, und ein Gast kehrt ins Menü zurück, statt dass die anderen
ohne Nachricht warten. Eine Pause endet erst, wenn der Host einen Gast
angenommen hat, nicht schon bei einer bloßen Verbindung. So kann ein
Gast nicht mehr in einer pausierten Welt festhängen, wenn der Host
zwischen Verbindung und Gruß pausiert. Eine Pause lässt sich immer
beenden. Meldungen, die nach dem Ende einer Sitzung im selben Stapel
folgen, bleiben unbeachtet. Vier neue Tests decken das ab.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Steven Wroblewski
2026-10-01 22:33:10 +02:00
parent 53ef02ae75
commit bfd237fd5c
2 changed files with 102 additions and 5 deletions
+33 -5
View File
@@ -57,6 +57,9 @@ var _save_blocked := false
var _figure_wait := 0.0
## The address a guest is joining, for messages.
var _joining := ""
## The host's accepted guests when _process last looked. A new one ends a
## pause.
var _known_guests := 0
func _ready() -> void:
@@ -233,12 +236,37 @@ func _leave(message: String) -> void:
_show_menu(message)
## A broken session runs no more ticks. Instead of leaving the others
## waiting, a host closes its world to guests and a guest leaves.
func _stop_sharing() -> void:
if not session.is_host():
_leave(tr("SIMULATION_STOPPED"))
return
link.link.close()
link = null
figures.clear()
_update_status()
## Ends a pause once the host accepted a guest: the guest joins in the next
## tick, and a paused world would keep it waiting. A connection that has not
## greeted yet is no guest.
func _end_pause_for_new_guests() -> void:
var guests := session.guest_count()
if guests > _known_guests and clock.paused:
_set_paused(false)
_known_guests = guests
func _process(delta: float) -> void:
if link != null:
for notice in link.pump(delta):
_on_notice(notice)
if session != null and session.is_broken() and link != null:
_stop_sharing()
if session == null or not session.is_running() or session.is_broken():
return
_end_pause_for_new_guests()
_update_marker()
if Input.is_action_pressed("dig") and not clock.paused:
_try_dig()
@@ -257,11 +285,10 @@ func _process(delta: float) -> void:
func _on_notice(notice: Dictionary) -> void:
# An earlier notice of the same batch may have ended the session.
if session == null:
return
match notice["type"]:
"guest_arrived":
# Pausing is for playing alone; a guest ends the pause.
if clock.paused:
_set_paused(false)
"world_received":
if notice["resync"]:
resyncs += 1
@@ -304,7 +331,8 @@ func _unhandled_input(event: InputEvent) -> void:
elif event.is_action_pressed("load_game"):
_reload()
elif event.is_action_pressed("toggle_pause"):
if session.is_host() and session.guest_count() == 0:
# Ending a pause is always allowed, starting one only alone.
if clock.paused or (session.is_host() and session.guest_count() == 0):
_set_paused(not clock.paused)
else:
hud.flash("PAUSE_ONLY_ALONE")
+69
View File
@@ -268,6 +268,34 @@ func test_only_the_host_saves_and_only_alone_one_may_pause_or_load() -> void:
_clean_up([host_dir, guest_dir])
func test_an_accepted_guest_ends_a_pause_and_a_bare_connection_does_not() -> void:
var host_dir := TestFiles.temporary_dir("host")
var guest_dir := TestFiles.temporary_dir("guest")
var host := _start_host(host_dir)
_press(host, "toggle_pause")
check(host.clock.paused, "the host pauses while alone")
# A connection that never greets is no guest.
var stranger := NetLink.new()
stranger.join("127.0.0.1", host.link.link.local_port())
var seen := func() -> bool:
stranger.poll(1.0 / 60.0)
return host.link.link._peers.size() == 1
check(await _wait_until(seen), "the host sees a bare connection")
check(host.clock.paused, "which leaves the pause alone")
var guest := _start_guest(guest_dir, host)
check(await _wait_until(_joined.bind(guest)), "a guest joins the paused world")
check(not host.clock.paused, "and ends the pause")
# Should a pause ever meet a guest, the host can still end it.
host._set_paused(true)
_press(host, "toggle_pause")
check(not host.clock.paused, "the host can always end a pause")
stranger.close()
_clean_up([host_dir, guest_dir])
func test_the_host_sees_a_guest_leave() -> void:
var host_dir := TestFiles.temporary_dir("host")
var guest_dir := TestFiles.temporary_dir("guest")
@@ -295,6 +323,36 @@ func test_a_guest_goes_back_to_the_menu_when_the_host_closes() -> void:
_clean_up([host_dir, guest_dir])
func test_a_host_that_stops_sharing_lets_its_guests_go() -> void:
var host_dir := TestFiles.temporary_dir("host")
var guest_dir := TestFiles.temporary_dir("guest")
var host := _start_host(host_dir)
var guest := _start_guest(guest_dir, host)
await _wait_until(_joined.bind(guest))
# An internal error of the core cannot be caused from here. For a broken
# session, _process calls what this test calls.
host._stop_sharing()
check(host.link == null, "the host no longer listens")
check_eq(host.hud.status_text(), "", "and no longer says that it does")
check(await _wait_until(func() -> bool: return guest.session == null), "the guest's session ends")
check_eq(guest.menu.status(), tr("HOST_LEFT"), "and the guest learns why")
_clean_up([host_dir, guest_dir])
func test_a_guest_that_stops_sharing_leaves() -> void:
var host_dir := TestFiles.temporary_dir("host")
var guest_dir := TestFiles.temporary_dir("guest")
var host := _start_host(host_dir)
var guest := _start_guest(guest_dir, host)
await _wait_until(_joined.bind(guest))
# As above, for a guest whose own core stopped.
guest._stop_sharing()
check(guest.session == null, "the guest's session ends")
check_eq(guest.menu.status(), tr("SIMULATION_STOPPED"), "the menu says why")
check(await _wait_until(func() -> bool: return host.hud.message() == tr("PLAYER_LEFT") % "Grace"), "the host sees the guest go")
_clean_up([host_dir, guest_dir])
func test_a_guest_that_is_turned_away_learns_why() -> void:
var host_dir := TestFiles.temporary_dir("host")
var host := _start_host(host_dir)
@@ -305,6 +363,17 @@ func test_a_guest_that_is_turned_away_learns_why() -> void:
_clean_up([host_dir])
func test_notices_after_the_end_of_a_session_are_ignored() -> void:
var directory := TestFiles.temporary_dir("late")
var main := _start_main(directory)
# In one batch of notices, an earlier one may have ended the session.
main._on_notice({"type": "world_received", "resync": true})
main._on_notice({"type": "protocol_error"})
check(main.session == null, "no session")
check(main.menu.visible, "the menu stays")
_clean_up([directory])
func test_a_mistyped_address_is_reported() -> void:
var directory := TestFiles.temporary_dir("typo")
var main := _start_main(directory)