b0c83af092
Complete replacement of the tactical-shooter project with the netfox-cs-sample (MIT) — a CS 1.6 inspired multiplayer FPS built with Godot 4 and netfox. ## What's new - Full CS-style gameplay: teams (T/CT), rounds, economy, buy menu - 6 weapons: Knife, Glock, USP, AK-47, M4A1, AWP - Bomb plant/defuse with 2 bombsites - Flashbang & smoke grenades - Proper netfox rollback netcode at 64 tick - Network popup UI for host/join - HUD, crosshair, round timer, scoreboard - All netfox singletons registered as autoloads (works in exported builds) ## Architecture - Listen-server (host from client, no dedicated server binary) - Multiplayer-fps game lives at examples/multiplayer-fps/ - Netfox addons registered as autoloads for exported build compat - Godot 4.7 with Forward+ renderer ## Removed - Old headless-server architecture (client_main, server_main, player.gd, etc.) - Custom netfox bootstrap with ENet fallback - Old ChaffGames FPS template (2,420 lines, 844 KB) - SimulationServer GDExtension stub - Godot-jolt physics (netfox sample uses default Godot physics) - Duplicate weapon_data.gd, anti_cheat.gd, round_manager.gd, etc. - Server browser API Python venv (87 MB) - test_range map and modular assets ## Preserved - Git history - Server config at config/default_server_config.cfg - Windows export preset - Build directory (gitignored) Co-authored-by: naxIO <naxIO@users.noreply.github.com>
235 lines
8.3 KiB
GDScript
235 lines
8.3 KiB
GDScript
@tool
|
|
extends Object
|
|
class_name Vest
|
|
|
|
## Utility singleton for running tests.
|
|
##
|
|
## @tutorial(Printing custom messages): https://foxssake.github.io/vest/latest/user-guide/printing-custom-messages/
|
|
|
|
const Icons := preload("res://addons/vest/icons/vest-icons.gd")
|
|
const Timeout := preload("res://addons/vest/timeout.gd")
|
|
|
|
const ValueMeasure := preload("res://addons/vest/measures/value-measure.gd")
|
|
const SumMeasure := preload("res://addons/vest/measures/sum-measure.gd")
|
|
const AverageMeasure := preload("res://addons/vest/measures/average-measure.gd")
|
|
const MinMeasure := preload("res://addons/vest/measures/min-measure.gd")
|
|
const MaxMeasure := preload("res://addons/vest/measures/max-measure.gd")
|
|
|
|
const __ := preload("res://addons/vest/vest-internals.gd")
|
|
|
|
const NEW_TEST_MIRROR_DIR_STRUCTURE := 0
|
|
const NEW_TEST_NEXT_TO_SOURCE := 1
|
|
const NEW_TEST_IN_ROOT := 2
|
|
|
|
static var _messages: Array[String] = []
|
|
static var _scene_tree: SceneTree
|
|
static var _editor_interface_provider: Callable
|
|
|
|
## Add a custom message to the current test.
|
|
## [br][br]
|
|
## Will be included in the test report.
|
|
static func message(p_message: String):
|
|
_messages.append(p_message)
|
|
|
|
## Wait for [param condition] to be true, or until timeout.
|
|
## [br][br]
|
|
## This method will start a loop, each time waiting [param interval] seconds.
|
|
## On each iteration, [param condition] is called. If its result is true, or the
|
|
## [param duration] has been exceeded, the loop stops.
|
|
## [br][br]
|
|
## If [param duration] is 0., the loop may run infinitely, without timeout.
|
|
## [br][br]
|
|
## Returns [constant OK] if [param condition] was true.[br]
|
|
## Returns [constant ERR_TIMEOUT] if [param duration] was exceeded.[br]
|
|
## Returns [constant ERR_UNAVAILABLE] if no [SceneTree] is available.
|
|
static func until(condition: Callable, duration: float = 5., interval: float = 0.0) -> Error:
|
|
var deadline := time() + duration
|
|
|
|
if not _scene_tree:
|
|
push_warning("Missing reference to SceneTree, will return immediately!")
|
|
return ERR_UNAVAILABLE
|
|
|
|
while time() < deadline:
|
|
if condition.call():
|
|
return OK
|
|
|
|
if is_zero_approx(duration): await _scene_tree.process_frame
|
|
else: await _scene_tree.create_timer(interval).timeout
|
|
|
|
return ERR_TIMEOUT
|
|
|
|
## Create a Timeout. Timeouts can be reused for multiple wait operations. This
|
|
## is useful when a group of sequential tasks have a single shared deadline.
|
|
## [br][br]
|
|
## See [method until] for further info on the parameters.
|
|
static func timeout(duration: float = 5.0, interval: float = 0.0) -> Timeout:
|
|
return Timeout.new(duration, interval, _scene_tree)
|
|
|
|
## Wait for [param duration] seconds.
|
|
## [br][br]
|
|
## Waiting for 0 seconds will wait until the next
|
|
## [signal SceneTree.process_frame].
|
|
## [br][br]
|
|
## Returns [constant OK] on success.[br]
|
|
## Returns [constant ERR_UNAVAILABLE] if no [SceneTree] is available.
|
|
static func sleep(duration: float = 0.) -> Error:
|
|
if not _scene_tree:
|
|
push_warning("Missing reference to SceneTree, will return immediately!")
|
|
return ERR_UNAVAILABLE
|
|
|
|
if is_zero_approx(duration): await _scene_tree.process_frame
|
|
else: await _scene_tree.create_timer(duration).timeout
|
|
return OK
|
|
|
|
## Get the test runner's timeout in seconds.
|
|
##
|
|
## If the test runner doesn't finish running the tests in this time, it's
|
|
## considered stuck. This value is read from the project settings.
|
|
## [br][br]
|
|
## For more info, see the
|
|
## [url=https://foxssake.github.io/vest/latest/user-guide/project-settings/#runner-timeout]user
|
|
## guide[/url].
|
|
static func get_runner_timeout() -> float:
|
|
return ProjectSettings.get_setting("vest/runner_timeout", 8.0)
|
|
|
|
## Get the sources root for this project.
|
|
##
|
|
## This is directory is assumed root for all the implementations, and is used
|
|
## for deriving paths to test suites. This value is read from the project
|
|
## settings.
|
|
## [br][br]
|
|
## For more info, see the
|
|
## [url=https://foxssake.github.io/vest/latest/user-guide/project-settings/#sources-root]user
|
|
## guide[/url].
|
|
static func get_sources_root() -> String:
|
|
return ProjectSettings.get_setting("vest/sources_root", "res://")
|
|
|
|
## Get the test sources root for this project.
|
|
##
|
|
## This is directory is assumed root for all the test suites, and is used
|
|
## for deriving paths. This value is read from the project settings.
|
|
## [br][br]
|
|
## For more info, see the
|
|
## [url=https://foxssake.github.io/vest/latest/user-guide/project-settings/#tests-root]user
|
|
## guide[/url].
|
|
static func get_tests_root() -> String:
|
|
return ProjectSettings.get_setting("vest/tests_root", "res://tests/")
|
|
|
|
## Get the list of configured test name patterns.
|
|
##
|
|
## These are used to recognize and generate filenames for test suites. This
|
|
## value is read from the project settings.
|
|
## [br][br]
|
|
## For more info, see the
|
|
## [url=https://foxssake.github.io/vest/latest/user-guide/project-settings/#test-name-patterns]user
|
|
## guide[/url].
|
|
static func get_test_name_patterns() -> Array[FilenamePattern]:
|
|
# TODO: Memoize
|
|
var pattern_strings := ProjectSettings.get_setting("vest/test_name_patterns") as PackedStringArray
|
|
var patterns := [] as Array[FilenamePattern]
|
|
patterns.assign(Array(pattern_strings).map(func(it): return FilenamePattern.new(it)))
|
|
|
|
return patterns
|
|
|
|
## Get the preference for new test location.
|
|
##
|
|
## This setting determines what path to suggest when creating new tests.
|
|
## [br][br]
|
|
## For more info, see the
|
|
## [url=https://foxssake.github.io/vest/latest/user-guide/project-settings/#new-test-location]
|
|
## user guide[/url].
|
|
static func get_new_test_location_preference() -> int:
|
|
return ProjectSettings.get_setting("vest/new_test_location", NEW_TEST_MIRROR_DIR_STRUCTURE)
|
|
|
|
## Get the current time, in seconds.
|
|
## [br][br]
|
|
## Used for benchmarking and waiting [method until] a condition becomes true.
|
|
static func time() -> float:
|
|
return Time.get_unix_time_from_system()
|
|
|
|
## Traverse [param directory] recursively and call [param visitor] on each file.
|
|
static func traverse_directory(directory: String, visitor: Callable, max_iters: int = 131072) -> void:
|
|
var da := DirAccess.open(directory)
|
|
da.include_navigational = false
|
|
|
|
var dir_queue := [directory]
|
|
var dir_history := []
|
|
|
|
# Put an upper limit on iterations, so we can't run into a runaway loop
|
|
for i in max_iters:
|
|
if dir_queue.is_empty():
|
|
break
|
|
|
|
var dir_at := dir_queue.pop_front() as String
|
|
da.change_dir(dir_at)
|
|
dir_history.append(dir_at)
|
|
|
|
# Add directories to queue
|
|
for dir_name in da.get_directories():
|
|
var dir := da.get_current_dir().path_join(dir_name)
|
|
if not dir_history.has(dir) and not dir_queue.has(dir):
|
|
dir_queue.append(dir)
|
|
|
|
# Visit files
|
|
for file_name in da.get_files():
|
|
var file := da.get_current_dir().path_join(file_name)
|
|
visitor.call(file)
|
|
|
|
## Return all paths that match the glob [param pattern].
|
|
## The matching is done using [method String.match].
|
|
static func glob(pattern: String, max_iters: int = 131072) -> Array[String]:
|
|
if pattern.is_empty(): return []
|
|
var results: Array[String] = []
|
|
|
|
var first_glob_index := mini(
|
|
(pattern.find("?") + pattern.length()) % pattern.length(),
|
|
(pattern.find("*") + pattern.length()) % pattern.length()
|
|
)
|
|
var root := (
|
|
pattern.get_base_dir()
|
|
if first_glob_index < 0 else
|
|
pattern.substr(0, first_glob_index).get_base_dir()
|
|
)
|
|
|
|
traverse_directory(root, func(path: String):
|
|
if path.match(pattern):
|
|
results.append(path)
|
|
, max_iters)
|
|
|
|
return results
|
|
|
|
## Returns a [SceneTree]. May be used for tests requiring more complex node
|
|
## setups.
|
|
static func get_tree() -> SceneTree:
|
|
return _scene_tree
|
|
|
|
static func _clear_messages():
|
|
_messages.clear()
|
|
|
|
static func _get_messages() -> Array[String]:
|
|
return _messages.duplicate()
|
|
|
|
static func _register_scene_tree(scene_tree: SceneTree):
|
|
_scene_tree = scene_tree
|
|
|
|
# HACK: Godot script compiler fails in 4.2+ if the return type is an engine singleton
|
|
# The best we can do is return an object and assume the methods exist.
|
|
# Casting isn't possible either, also freaks out the script compiler.
|
|
static func _get_editor_interface() -> Object:
|
|
if Engine.get_version_info().hex >= 0x040200:
|
|
return Engine.get_singleton("EditorInterface")
|
|
elif _editor_interface_provider.is_valid():
|
|
return _editor_interface_provider.call()
|
|
else:
|
|
return null
|
|
|
|
static func _get_editor_scale() -> float:
|
|
var interface := _get_editor_interface()
|
|
if interface and interface.has_method("get_editor_scale"):
|
|
return interface.get_editor_scale()
|
|
else:
|
|
return 1.0
|
|
|
|
static func _register_editor_interface_provider(provider: Callable):
|
|
_editor_interface_provider = provider
|