M1c: Gleichschritt im Netz #9

Merged
steven merged 20 commits from m1c-gleichschritt-im-netz into main 2026-10-02 06:41:02 +00:00
2 changed files with 102 additions and 5 deletions
Showing only changes of commit bfd237fd5c - Show all commits
+33 -5
View File
@@ -57,6 +57,9 @@ var _save_blocked := false
var _figure_wait := 0.0 var _figure_wait := 0.0
## The address a guest is joining, for messages. ## The address a guest is joining, for messages.
var _joining := "" var _joining := ""
## The host's accepted guests when _process last looked. A new one ends a
## pause.
var _known_guests := 0
func _ready() -> void: func _ready() -> void:
@@ -233,12 +236,37 @@ func _leave(message: String) -> void:
_show_menu(message) _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: func _process(delta: float) -> void:
if link != null: if link != null:
for notice in link.pump(delta): for notice in link.pump(delta):
_on_notice(notice) _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(): if session == null or not session.is_running() or session.is_broken():
return return
_end_pause_for_new_guests()
_update_marker() _update_marker()
if Input.is_action_pressed("dig") and not clock.paused: if Input.is_action_pressed("dig") and not clock.paused:
_try_dig() _try_dig()
@@ -257,11 +285,10 @@ func _process(delta: float) -> void:
func _on_notice(notice: Dictionary) -> 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"]: match notice["type"]:
"guest_arrived":
# Pausing is for playing alone; a guest ends the pause.
if clock.paused:
_set_paused(false)
"world_received": "world_received":
if notice["resync"]: if notice["resync"]:
resyncs += 1 resyncs += 1
@@ -304,7 +331,8 @@ func _unhandled_input(event: InputEvent) -> void:
elif event.is_action_pressed("load_game"): elif event.is_action_pressed("load_game"):
_reload() _reload()
elif event.is_action_pressed("toggle_pause"): 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) _set_paused(not clock.paused)
else: else:
hud.flash("PAUSE_ONLY_ALONE") 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]) _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: func test_the_host_sees_a_guest_leave() -> void:
var host_dir := TestFiles.temporary_dir("host") var host_dir := TestFiles.temporary_dir("host")
var guest_dir := TestFiles.temporary_dir("guest") 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]) _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: func test_a_guest_that_is_turned_away_learns_why() -> void:
var host_dir := TestFiles.temporary_dir("host") var host_dir := TestFiles.temporary_dir("host")
var host := _start_host(host_dir) 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]) _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: func test_a_mistyped_address_is_reported() -> void:
var directory := TestFiles.temporary_dir("typo") var directory := TestFiles.temporary_dir("typo")
var main := _start_main(directory) var main := _start_main(directory)