Files
claude c4beeae38f Initial commit: Transcience MVP
Top-down twin-stick bullet-hell, Godot 4.7, server-authoritative dedicated
server with client-side prediction. Clients send input only; the server
resolves every hit for both players and enemies (no PvP).

- SimWorld: whole simulation as plain RefCounted objects (no nodes, no
  physics server), ~0.24ms/tick at peak load -- runs headless for free and
  drives 78 tests in under a second
- BulletPool: struct-of-arrays bullet storage, replicated as spawn/despawn
  events rather than per-tick state
- Emitter framework (Ring/AimedSpread/WallGap/ArcSweep) shared by trash
  enemies and bosses -- a new boss is data in src/content/content.gd, no
  simulation changes
- The Warden of the Fold: stationary 4-phase boss built entirely on that
  format
- Lobby hub with a portal into on-demand dungeon instances; one process
  hosts the hub plus every concurrent dungeon
- Emergency escape: 3s server-owned channel, cancelled by damage
- tools/check.sh, test.sh (GUT), smoke.sh (real server + bot clients over
  ENet), bench.gd; git hooks wired to the same scripts
- docs/ARCHITECTURE.md, NETCODE.md, WORKFLOW.md, ROADMAP.md
2026-09-03 16:03:57 +02:00

202 lines
5.5 KiB
GDScript

extends Node
class GutAwaiterLogger:
var _time_waited = 0.0
var logger = GutUtils.get_logger()
var waiting_on = "nothing"
var logged_initial_message = false
var wait_log_delay := 1.0
var disabled = false
func waited(x):
_time_waited += x
if(!logged_initial_message and _time_waited >= wait_log_delay):
log_it()
logged_initial_message = true
func reset():
_time_waited = 0.0
logged_initial_message = false
func log_it():
if(!disabled):
var msg = str("--- Awaiting ", waiting_on, " ---")
logger.wait_msg(msg)
signal timeout
signal wait_started
var await_logger = GutAwaiterLogger.new()
var _wait_time := 0.0
var _wait_process_frames := 0
var _wait_physics_frames := 0
var _signal_to_wait_on = null
var _predicate_method = null
var _waiting_for_predicate_to_be = null
var _predicate_time_between := 0.0
var _predicate_time_between_elpased := 0.0
var _elapsed_time := 0.0
var _elapsed_frames := 0
var _did_last_wait_timeout = false
var did_last_wait_timeout = false :
get: return _did_last_wait_timeout
set(val): push_error("Cannot set did_last_wait_timeout")
func _ready() -> void:
get_tree().process_frame.connect(_on_tree_process_frame)
get_tree().physics_frame.connect(_on_tree_physics_frame)
func _on_tree_process_frame():
# Count frames here instead of in _process so that tree order never
# makes a difference and the count/signaling happens outside of
# _process being called.
if(_wait_process_frames > 0):
_elapsed_frames += 1
if(_elapsed_frames > _wait_process_frames):
_end_wait()
func _on_tree_physics_frame():
# Count frames here instead of in _physics_process so that tree order never
# makes a difference and the count/signaling happens outside of
# _physics_process being called.
if(_wait_physics_frames != 0):
_elapsed_frames += 1
if(_elapsed_frames > _wait_physics_frames):
_end_wait()
func _physics_process(delta):
if(is_waiting()):
await_logger.waited(delta)
if(_wait_time != 0.0):
_elapsed_time += delta
if(_elapsed_time >= _wait_time):
_end_wait()
if(_predicate_method != null):
_predicate_time_between_elpased += delta
if(_predicate_time_between_elpased >= _predicate_time_between):
_predicate_time_between_elpased = 0.0
var result = _predicate_method.call()
if(_waiting_for_predicate_to_be == false):
if(typeof(result) != TYPE_BOOL or result != true):
_end_wait()
else:
if(typeof(result) == TYPE_BOOL and result == _waiting_for_predicate_to_be):
_end_wait()
func _end_wait():
await_logger.reset()
# Check for time before checking for frames so that the extra frames added
# when waiting on a signal do not cause a false negative for timing out.
if(_wait_time > 0):
_did_last_wait_timeout = _elapsed_time >= _wait_time
elif(_wait_physics_frames > 0):
_did_last_wait_timeout = _elapsed_frames >= _wait_physics_frames
elif(_wait_process_frames > 0):
_did_last_wait_timeout = _elapsed_frames >= _wait_process_frames
if(_signal_to_wait_on != null and \
is_instance_valid(_signal_to_wait_on.get_object()) and \
_signal_to_wait_on.is_connected(_signal_callback)):
_signal_to_wait_on.disconnect(_signal_callback)
_wait_process_frames = 0
_wait_time = 0.0
_wait_physics_frames = 0
_signal_to_wait_on = null
_predicate_method = null
_elapsed_time = 0.0
_elapsed_frames = 0
timeout.emit()
const ARG_NOT_SET = '_*_argument_*_is_*_not_set_*_'
func _signal_callback(
_arg1=ARG_NOT_SET, _arg2=ARG_NOT_SET, _arg3=ARG_NOT_SET,
_arg4=ARG_NOT_SET, _arg5=ARG_NOT_SET, _arg6=ARG_NOT_SET,
_arg7=ARG_NOT_SET, _arg8=ARG_NOT_SET, _arg9=ARG_NOT_SET):
_signal_to_wait_on.disconnect(_signal_callback)
# DO NOT _end_wait here. For other parts of the test to get the signal that
# was waited on, we have to wait for another frames. For example, the
# signal_watcher doesn't get the signal in time if we don't do this.
_wait_process_frames = 1
func wait_seconds(x, msg=''):
await_logger.waiting_on = str(x, " seconds ", msg)
_did_last_wait_timeout = false
_wait_time = x
wait_started.emit()
func wait_process_frames(x, msg=''):
await_logger.waiting_on = str(x, " idle frames ", msg)
_did_last_wait_timeout = false
_wait_process_frames = x
wait_started.emit()
func wait_physics_frames(x, msg=''):
await_logger.waiting_on = str(x, " physics frames ", msg)
_did_last_wait_timeout = false
_wait_physics_frames = x
wait_started.emit()
func wait_for_signal(the_signal : Signal, max_time, msg=''):
await_logger.waiting_on = str("signal ", the_signal.get_name(), " or ", max_time, "s ", msg)
_did_last_wait_timeout = false
the_signal.connect(_signal_callback)
_signal_to_wait_on = the_signal
_wait_time = max_time
wait_started.emit()
func wait_until(predicate_function: Callable, max_time, time_between_calls:=0.0, msg=''):
await_logger.waiting_on = str("callable to return TRUE or ", max_time, "s. ", msg)
_predicate_time_between = time_between_calls
_predicate_method = predicate_function
_wait_time = max_time
_waiting_for_predicate_to_be = true
_predicate_time_between_elpased = 0.0
_did_last_wait_timeout = false
wait_started.emit()
func wait_while(predicate_function: Callable, max_time, time_between_calls:=0.0, msg=''):
await_logger.waiting_on = str("callable to return FALSE or ", max_time, "s. ", msg)
_predicate_time_between = time_between_calls
_predicate_method = predicate_function
_wait_time = max_time
_waiting_for_predicate_to_be = false
_predicate_time_between_elpased = 0.0
_did_last_wait_timeout = false
wait_started.emit()
func is_waiting():
return _wait_time != 0.0 or \
_wait_physics_frames != 0 or \
_wait_process_frames != 0