22 Commits
Author SHA1 Message Date
Sansan 10e9e7b2a4 New app icon, packaged with a menu entry for Linux
Build / test (pull_request) Successful in 22s
Build / build (pull_request) Skipped
The icon is a plate wearing headphones with a red Nordic house cookie
on it. It replaces the default Godot icon, so the game window, Windows
and macOS builds use it.

Linux programs can't carry their own icon, so the build now also makes
MusicPlayer_<version>_linux.tar.gz with the game, the icon as a PNG, a
.desktop menu entry and install.sh, which installs them for the current
user. The bare Linux binary is still published.
2026-10-06 08:32:56 +02:00
Sansan 29b11560b5 Show where each item starts in the inspiration and sync lists
Items in the inspiration list and the sync result columns now each have
their own background box, alternating between two shades, with a small
gap between them. When a long name wraps over several lines it is
still clear which lines belong together. The sync columns keep their
alternating white and orange text.
2026-10-06 08:32:56 +02:00
Sansan 2a332c2d19 Make the sync result look right with any number of columns
- Visible columns share the width equally, whether one or five are shown
- Long soundtrack names and errors wrap inside their column instead of
  widening the window, which pushed the title and Restart button out of
  view and cut off text
- Separators only go between columns; the extra one before the first
  column is gone
- Wider, centred panel with inner margins, and spacing above the
  Restart button
- The Re-added header no longer reads ReAdded
2026-10-06 08:32:56 +02:00
Sansan 6cbbfee6a1 Tidy up popup layouts
- Settings: labels in one column and switches and number buttons lined
  up in another, with margins inside the panel
- Log: title and margins; text no longer touches the panel edge
- Sync result: error list centred under its header, which now reads
  Errors instead of CatchedErrors
- About: text wraps inside the panel instead of running under the
  scrollbar
- Character select: wider panel, so the right column of characters is
  no longer right next to the scrollbar
2026-10-06 08:32:56 +02:00
Sansan c7d49bc5cc Rework popups and fix blurry text and player list layout
Popups:
- All nine popups now extend a shared PopupWindow class with open(),
  close() and toggle(), instead of each copying its own close-on-click
  code. Popups that need cleanup override close().
- A click outside a popup closed it and also pressed whatever was
  underneath, e.g. Reset playlist. The closing click is now consumed.
- Popups used Godot's see-through default panel, so the song list and
  titles showed through. They now use an opaque popup theme, and a
  backdrop dims the window behind them.
- The main window root now fills the screen, so the backdrop covers it.

Text and images:
- Many labels and buttons forced nearest filtering, which made text and
  icons jagged when the window is not 1920x1080. They now use the
  default smooth filtering. Only the pixel-art "no character" picture
  keeps nearest filtering when the winner window enlarges it.

Player list:
- A long player name pushed that row's character and buttons to the
  right. Names now take the free space and shorten with an ellipsis, so
  all rows line up and use the full width of the panel.
2026-10-06 08:32:56 +02:00
Sansan 6288739de0 Use the Compatibility renderer
The game is a 2D interface without 3D, shaders or lights, so it does
not need Forward+. With Compatibility it uses about 25% less CPU and
30 MB less memory while running, with no visible difference.
2026-10-06 08:32:56 +02:00
Sansan d79f5f92c5 Remove unused code
- The old character select scene, which was never shown but loaded
  every character image at startup
- Unused SyncResponse and ProgressResponse classes
- Playlist.set_last_played_answered and select/local-loading code in
  the new character select, none of which were called
- Settings.delete_player_children, identical to delete_children
- An unused variable holding a local file path
2026-10-06 08:32:56 +02:00
Sansan 14c166614d Add one log row at a time and pad log times
The log rebuilt every row each time a point was given or taken. It now
adds just the new row, and shows times as 09:05:03 instead of 9:5:3.
2026-10-06 08:32:56 +02:00
Sansan 428decf73a Turn statistics back on for a new game and fix low winning scores
Winning turned statistics off and nothing turned them back on, while
the Settings checkbox still showed them as on. Resetting points now
turns them back on if the win turned them off, and the checkbox shows
the current state when Settings is opened.

A winning score of 1 could never be won and with 2 the first point was
not announced as match point, because the first-point check ran first.
The winning score also could not be kept from going below 1.
2026-10-06 08:32:56 +02:00
Sansan 739d0f7af6 Preload songs only after the song list has loaded
At startup and after resetting the playlist, loading the song list and
preloading the first song started at the same time. When the list
answered last, it cleared the playlist and dropped the preloaded song.
Preloading now starts once the list has loaded, and loading the list is
retried if it fails.
2026-10-06 08:32:56 +02:00
Sansan ed292877ef Ignore Show answer when no song is playing
Before the first song played, currently_playing_song is -1, so pressing
Show answer marked the last song in the list as answered. That is the
preloaded song, so its title was shown as soon as it started playing,
and played?song=-1 was sent to the server. With an empty playlist it
crashed.
2026-10-06 08:32:56 +02:00
Sansan 5a979fca48 Time out requests the server never answers
HTTPRequest has no timeout by default, so a server that accepts the
connection but never responds left song downloads waiting forever.
Requests now fail after 120 seconds, which triggers the normal retry.
2026-10-06 08:32:56 +02:00
Sansan 1acc757981 Fill the song cache when the settings menu is closed
Raising the cache size used to have no effect until the next song was
played. Downloads now start when the settings window closes, so clicking
the number up and down while it is open does not start any downloads.
2026-10-06 08:32:56 +02:00
Sansan 16724922ed Test that the song cache fills up to the cache size setting
Covers a cache size above 1: songs are preloaded until the setting is
reached, playing a song tops the cache up again, and the cache keeps
filling after a failed download once the server answers again.
2026-10-06 08:32:56 +02:00
Sansan c1fc24adf9 Release from the version in project.godot
The version now lives in project.godot (application/config/version)
and Settings.version reads it from there. The Build workflow tests and
exports on every push, and on main creates the tag, uploads the builds
and creates the Gitea release when that version has no tag yet. This
replaces gitea-release.yaml, which released when a branch named after
the version was pushed.

Godot setup moves into a local .gitea/actions/setup-godot action shared
by the test and build jobs, and Godot is updated to 4.7.2 to match the
editor the project is made with.
2026-10-06 08:32:56 +02:00
Sansan f67dccbf52 Retry failed song downloads instead of getting stuck
MusicServerClient functions now take an optional on_failed callback,
called when a request fails or returns invalid JSON. Before, a failed
request was silently dropped, so a failure while preloading a song left
Playlist.is_song_downloading set forever and no more songs were fetched.

Song fetching retries the step that failed (download, queue or info)
every song_fetch_retry_seconds, so the server and the game keep the same
playlist.
2026-10-06 08:32:56 +02:00
Sansan c985a6ce5b Replace the broken test suite with working GdUnit4 tests
The previous tests never compiled: they used GUT's assert functions,
Godot 3 APIs and reassigned autoload functions, so no test ran.

- 90 tests: unit tests for the playlist, songs, player scoring, time
  formatting, search and the server client, plus integration tests
  that run MainWindow and SyncWindow against a fake server
- Keybind tests check that Alt+<number> and Ctrl+Alt+<number> keep
  following the player rows while players are added and removed
- run_tests.sh finds Godot and runs the suite headless (~10 seconds)
- CI runs the tests before the build job in gitea-build.yaml; the
  separate test.yml used a GitHub action that does not exist
- GdUnit4 is committed instead of ignored, since the project enables
  it, and .gitea/ is no longer ignored
- Exported builds leave out tests/ and addons/gdUnit4/
2026-10-06 08:32:56 +02:00
Sansan 56e9fe66d3 Move server calls to MusicServerClient and give players stable ids
Server requests move out of Settings into a new MusicServerClient
autoload with one function per endpoint. It sends requests through a
swappable HttpTransport so tests can fake the server, and it frees its
HTTPRequest nodes after use.

Players now have a UUID that never changes. Their position in
Settings.player_array is only used for the Alt+<number> keybinds.
Previously the position doubled as the id, so removing a player made
points and wins land on the wrong player, and could delete another
player's point from a song.

Also fixes:
- format_time showed 59.6 seconds as "00:60"
- searches containing regex characters such as "Q*bert" broke search
- SongObject and PlayerObject were Nodes that were never freed; they
  are now RefCounted
- search matching is split out of the UI code into find_matches()
2026-10-06 08:32:55 +02:00
Sansan ab338b0696 Add GdUnit4 testing framework with comprehensive test coverage
- Add GdUnit4 v6.2.1 framework in addons/gdUnit4/
- Create test directory structure (tests/unit/, tests/integration/, tests/mocks/)
- Add unit tests for MusicPlayer.gd (8 tests)
- Add unit tests for MainWindow.gd (15+ tests)
- Add unit tests for Settings.gd (5 tests)
- Add unit tests for Playlist.gd (11 tests)
- Add integration tests for full flow (10+ tests)
- Add MockHTTPRequest for HTTP request mocking
- Add TestHelpers with utility functions
- Add Gitea Actions CI workflow (.gitea/workflows/test.yml)
- Add .runsettings for GdUnit4 CLI configuration
- Add run_tests.sh for convenient test execution
- Add TESTING.md with comprehensive documentation
- Add TESTING_PLAN.md with implementation details
- Update .gitignore for test artifacts
- Enable GdUnit4 plugin in project.godot

Features:
- Test button functionality (play, pause, search, sync, etc.)
- Mock REST API requests to server
- Test complete application flows
- Run tests in CI (headless mode)
- Run tests in GUI for visual feedback
- JUnit XML and HTML test reports

Generated by OpenCode AI assistant.
2026-10-06 08:32:55 +02:00
Sansan 9a91d969bd Merge pull request 'Made improvements to the Search window, Made small changes to the inspiration list scrolling, Made small changes to the player list, Changed the term game to soundtrack, #28: Made changes to the sync window' (#37) from develop into main
Build / build (push) Successful in 1m6s
Reviewed-on: #37
2026-10-05 22:53:08 +02:00
Sansan 8da6fc442f Made improvements to the Search window, Made small changes to the inspiration list scrolling, Made small changes to the player list, Changed the term game to soundtrack, #28: Made changes to the sync window
Build / build (push) Successful in 1m14s
Publish / build (push) Successful in 1m10s
2026-09-26 16:21:02 +02:00
Sansan 235bdcbcb5 #34: Fixed bug in Character Select when clicking or scrolling
Build / build (push) Successful in 1m6s
2026-05-17 12:14:07 +02:00
618 changed files with 34641 additions and 1010 deletions

No files matched your search

+38
View File
@@ -0,0 +1,38 @@
name: "Setup Godot"
description: "Downloads Godot for Linux, puts it on PATH as `godot` and optionally installs the export templates"
inputs:
version:
description: "Godot version, e.g. 4.7.2"
required: true
export-templates:
description: "Also install the export templates ('true' or 'false')"
required: false
default: "false"
runs:
using: "composite"
steps:
- name: Download Godot
shell: bash
run: |
URL="https://github.com/godotengine/godot-builds/releases/download/${{ inputs.version }}-stable"
mkdir -p "$HOME/.godot-bin"
curl --fail -L -o godot.zip "$URL/Godot_v${{ inputs.version }}-stable_linux.x86_64.zip"
unzip -q godot.zip
mv "Godot_v${{ inputs.version }}-stable_linux.x86_64" "$HOME/.godot-bin/godot"
chmod +x "$HOME/.godot-bin/godot"
rm godot.zip
echo "$HOME/.godot-bin" >> "$GITHUB_PATH"
- name: Install Export Templates
if: inputs.export-templates == 'true'
shell: bash
run: |
URL="https://github.com/godotengine/godot-builds/releases/download/${{ inputs.version }}-stable"
TEMPLATES_DIR="$HOME/.local/share/godot/export_templates/${{ inputs.version }}.stable"
curl --fail -L -o export_templates.tpz "$URL/Godot_v${{ inputs.version }}-stable_export_templates.tpz"
unzip -q export_templates.tpz
mkdir -p "$TEMPLATES_DIR"
mv templates/* "$TEMPLATES_DIR"
rm -r templates export_templates.tpz
+90 -36
View File
@@ -1,59 +1,113 @@
# #
# .gitea/workflows/gitea-build.yaml # .gitea/workflows/gitea-build.yaml
# #
# Tests and exports MusicPlayer on every push and pull request to main and develop,
# and releases it from main.
#
# Releases follow the version in project.godot (application/config/version). Bump it,
# push to main, and once the tests and exports pass the builds are uploaded to the
# package registry and the tag and Gitea release are created. If that version already
# has a tag, nothing is released. See RELEASING.md.
#
# All jobs run in this one workflow, one after another: separate workflows started by
# the same push run at the same time on the single runner and can break each other's
# copy of a shared action.
# #
name: "Build" name: "Build"
on: on:
push: push:
branches: [main, develop]
pull_request:
branches: [main, develop] branches: [main, develop]
env: env:
GODOT_VERSION: 4.6 GODOT_VERSION: 4.7.2
PROJECT_PATH: . PROJECT_NAME: MusicPlayer
EXPORT_NAME: MusicPlayer
jobs: jobs:
build: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Download Godot
- name: Setup Godot
uses: ./.gitea/actions/setup-godot
with:
version: ${{ env.GODOT_VERSION }}
- name: Run Tests
run: ./run_tests.sh
build:
needs: test
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: Read Version
id: version
run: | run: |
#echo "https://github.com/godotengine/godot-builds/releases/download/$GODOT_VERSION-stable/Godot_v$GODOT_VERSION-stable_linux.x86_64.zip" GAME_VERSION="$(sed -n 's/^config\/version="\(.*\)"$/\1/p' project.godot)"
curl -L -o godot.zip https://github.com/godotengine/godot-builds/releases/download/$GODOT_VERSION-stable/Godot_v$GODOT_VERSION-stable_linux.x86_64.zip if [ -z "$GAME_VERSION" ]; then
unzip godot.zip echo "No application/config/version in project.godot"
mv Godot_v$GODOT_VERSION-stable_linux.x86_64 godot exit 1
chmod +x godot fi
echo "GAME_VERSION=$GAME_VERSION" >> "$GITHUB_ENV"
if [ "$GITHUB_REF" != "refs/heads/main" ]; then
echo "Not on main, building without releasing"
echo "release=false" >> "$GITHUB_OUTPUT"
elif git rev-parse -q --verify "refs/tags/$GAME_VERSION" > /dev/null; then
echo "$GAME_VERSION is already released, building without releasing"
echo "release=false" >> "$GITHUB_OUTPUT"
else
echo "Releasing $GAME_VERSION"
echo "release=true" >> "$GITHUB_OUTPUT"
fi
- name: Download Godot Export Templates - name: Setup Godot
uses: ./.gitea/actions/setup-godot
with:
version: ${{ env.GODOT_VERSION }}
export-templates: "true"
- name: Export
run: | run: |
#echo "https://github.com/godotengine/godot-builds/releases/download/$GODOT_VERSION-stable/Godot_v$GODOT_VERSION-stable_export_templates.tpz" mkdir -p build
curl -L -o export_templates.zip https://github.com/godotengine/godot-builds/releases/download/$GODOT_VERSION-stable/Godot_v$GODOT_VERSION-stable_export_templates.tpz godot --headless --path . --import
unzip export_templates.zip godot --headless --path . --export-release "Windows Desktop" "build/${PROJECT_NAME}_${GAME_VERSION}.exe"
godot --headless --path . --export-release "Linux" "build/${PROJECT_NAME}_${GAME_VERSION}.x86_64"
- name: Install Export Templates # Linux binaries can't hold an icon; the package adds it with a menu entry and installer
- name: Package Linux
run: | run: |
mkdir -p ~/.local/share/godot/export_templates/$GODOT_VERSION.stable linux/package.sh "build/${PROJECT_NAME}_${GAME_VERSION}.x86_64" "$GAME_VERSION" build
mv ./templates/* ~/.local/share/godot/export_templates/$GODOT_VERSION.stable ls -la build
- name: Create build folder - name: Upload Artifacts
if: steps.version.outputs.release == 'true'
run: | run: |
mkdir -v -p build for FILE in build/${PROJECT_NAME}_${GAME_VERSION}*; do
NAME="$(basename "$FILE")"
- name: Windows Build echo "Uploading $NAME"
run: | curl --fail --user ${{ github.repository_owner }}:${{ secrets.TOKEN }} \
cd $PROJECT_PATH --upload-file "$FILE" \
./godot --headless --verbose --export-release "Windows Desktop" "build/$EXPORT_NAME.exe" https://gitea.sanplex.xyz/api/packages/sansan/generic/$PROJECT_NAME/$GAME_VERSION/$NAME
done
- name: Linux Build
run: |
cd $PROJECT_PATH
./godot --headless --verbose --export-release "Linux" "build/$EXPORT_NAME.x86_64"
# - name: MacOS Build
# run: |
# cd $PROJECT_PATH
# ./godot --headless --verbose --export-release "macOS" "build/$EXPORT_NAME.zip"
- name: Create Release
if: steps.version.outputs.release == 'true'
uses: akkuman/gitea-release-action@v1
env:
NODE_OPTIONS: '--experimental-fetch' # if nodejs < 18
with:
tag_name: ${{ env.GAME_VERSION }}
target_commitish: ${{ github.sha }}
files: |-
build/${{ env.PROJECT_NAME }}_*
-90
View File
@@ -1,90 +0,0 @@
#
# .gitea/workflows/gitea-release.yaml
#
#
name: "Publish"
on:
push:
branches:
- '*.*'
env:
GODOT_VERSION: 4.6
PROJECT_PATH: .
PROJECT_NAME: MusicPlayer
GAME_VERSION: ${{gitea.ref_name}}
EXPORT_NAME: MusicPlayer_${{gitea.ref_name}}
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Download Godot
run: |
#echo "https://github.com/godotengine/godot-builds/releases/download/$GODOT_VERSION-stable/Godot_v$GODOT_VERSION-stable_linux.x86_64.zip"
curl -L -o godot.zip https://github.com/godotengine/godot-builds/releases/download/$GODOT_VERSION-stable/Godot_v$GODOT_VERSION-stable_linux.x86_64.zip
unzip godot.zip
mv Godot_v$GODOT_VERSION-stable_linux.x86_64 godot
chmod +x godot
- name: Download Godot Export Templates
run: |
#echo "https://github.com/godotengine/godot-builds/releases/download/$GODOT_VERSION-stable/Godot_v$GODOT_VERSION-stable_export_templates.tpz"
curl -L -o export_templates.zip https://github.com/godotengine/godot-builds/releases/download/$GODOT_VERSION-stable/Godot_v$GODOT_VERSION-stable_export_templates.tpz
unzip export_templates.zip
- name: Install Export Templates
run: |
mkdir -p ~/.local/share/godot/export_templates/$GODOT_VERSION.stable
mv ./templates/* ~/.local/share/godot/export_templates/$GODOT_VERSION.stable
- name: Create build folder
run: |
mkdir -v -p build
- name: Windows Build
run: |
cd $PROJECT_PATH
./godot --headless --verbose --export-release "Windows Desktop" "build/$EXPORT_NAME.exe"
- name: Linux Build
run: |
cd $PROJECT_PATH
./godot --headless --verbose --export-release "Linux" "build/$EXPORT_NAME.x86_64"
# - name: MacOS Build
# run: |
# cd $PROJECT_PATH
# ./godot --headless --verbose --export-release "macOS" "build/$EXPORT_NAME.zip"
- name: Upload Artifact Windows
run: |
#ls -la build
curl --user ${{ github.repository_owner }}:${{ secrets.TOKEN }} \
--upload-file "build/$EXPORT_NAME.exe" \
https://gitea.sanplex.xyz/api/packages/sansan/generic/$PROJECT_NAME/$GAME_VERSION/$EXPORT_NAME.exe
- name: Upload Artifact Linux
run: |
ls -la build
curl --user ${{ github.repository_owner }}:${{ secrets.TOKEN }} \
--upload-file "build/$EXPORT_NAME.x86_64" \
https://gitea.sanplex.xyz/api/packages/sansan/generic/$PROJECT_NAME/$GAME_VERSION/$EXPORT_NAME.x86_64
# - name: Upload Artifact MacOS
# run: |
# ls -la build
# curl --user ${{ github.repository_owner }}:${{ secrets.TOKEN }} \
# --upload-file "build/$EXPORT_NAME.zip" \
# https://gitea.sanplex.xyz/api/packages/sansan/generic/$PROJECT_NAME/$GAME_VERSION/$EXPORT_NAME.zip
- name: Create release
uses: akkuman/gitea-release-action@v1
env:
NODE_OPTIONS: '--experimental-fetch' # if nodejs < 18
with:
files: |-
build/**
+3
View File
@@ -6,3 +6,6 @@
.idea .idea
.vscode .vscode
.DS_Store .DS_Store
# GdUnit4 test reports
reports/
+9 -15
View File
@@ -1,4 +1,4 @@
extends Control extends PopupWindow
@onready @onready
var about_panel: Panel = %AboutPanel var about_panel: Panel = %AboutPanel
@@ -13,6 +13,12 @@ var whats_new_label: Label = %WhatsNewLabel
# Changelog as Dictionary with version as key and description as value # Changelog as Dictionary with version as key and description as value
const WHATS_NEW: Dictionary = { const WHATS_NEW: Dictionary = {
"1.9.0": "Made improvements to the Search window
Made small changes to the inspiration list scrolling
Made small changes to the player list
Changed the term game to soundtrack
#28: Made changes to the sync window
#34: Fixed bug in Character Select when clicking or scrolling",
"1.8.5": "#9: New winner screen with positions of all players "1.8.5": "#9: New winner screen with positions of all players
#32: New setting for how long to hide the beginning #32: New setting for how long to hide the beginning
#33: Fixed bug with hiding beginning when song restarts", #33: Fixed bug with hiding beginning when song restarts",
@@ -45,16 +51,8 @@ const WHATS_NEW: Dictionary = {
} }
func _input(event: InputEvent) -> void: func open() -> void:
if self.visible == true: super()
if event is InputEventMouseButton && event.is_pressed():
var evLocal: InputEvent = make_input_local(event)
if !Rect2(Vector2(0, 0), about_panel.size).has_point(evLocal.position):
self.visible = false
func show_window() -> void:
visible = true
version_label.text = Settings.version version_label.text = Settings.version
# Build changelog text with version headers # Build changelog text with version headers
@@ -64,7 +62,3 @@ func show_window() -> void:
changelog_text += WHATS_NEW[version] + "\n" changelog_text += WHATS_NEW[version] + "\n"
whats_new_label.text = changelog_text whats_new_label.text = changelog_text
func hide_window() -> void:
visible = false
+7 -9
View File
@@ -1,8 +1,10 @@
[gd_scene format=3 uid="uid://cp54idaudb8dp"] [gd_scene format=3 uid="uid://cp54idaudb8dp"]
[ext_resource type="Script" uid="uid://b7bh0of7olbcb" path="res://AboutWindow.gd" id="1_about_script"] [ext_resource type="Script" uid="uid://b7bh0of7olbcb" path="res://AboutWindow.gd" id="1_about_script"]
[ext_resource type="Theme" path="res://popup_theme.tres" id="popup_theme"]
[node name="AboutControl" type="Control" unique_id=1626010355] [node name="AboutControl" type="Control" unique_id=1626010355 node_paths=PackedStringArray("panel")]
theme = ExtResource("popup_theme")
layout_mode = 3 layout_mode = 3
anchors_preset = 15 anchors_preset = 15
anchor_right = 1.0 anchor_right = 1.0
@@ -10,6 +12,7 @@ anchor_bottom = 1.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
script = ExtResource("1_about_script") script = ExtResource("1_about_script")
panel = NodePath("AboutPanel")
[node name="AboutPanel" type="Panel" parent="." unique_id=1907299275] [node name="AboutPanel" type="Panel" parent="." unique_id=1907299275]
unique_name_in_owner = true unique_name_in_owner = true
@@ -18,7 +21,6 @@ offset_right = 600.0
offset_bottom = 640.0 offset_bottom = 640.0
[node name="TitleLabel" type="Label" parent="AboutPanel" unique_id=200295721] [node name="TitleLabel" type="Label" parent="AboutPanel" unique_id=200295721]
texture_filter = 1
layout_mode = 0 layout_mode = 0
offset_right = 600.0 offset_right = 600.0
offset_bottom = 50.0 offset_bottom = 50.0
@@ -28,10 +30,11 @@ vertical_alignment = 1
[node name="ScrollContainer" type="ScrollContainer" parent="AboutPanel" unique_id=417677913] [node name="ScrollContainer" type="ScrollContainer" parent="AboutPanel" unique_id=417677913]
layout_mode = 0 layout_mode = 0
offset_left = 10.0 offset_left = 20.0
offset_top = 50.0 offset_top = 50.0
offset_right = 590.0 offset_right = 580.0
offset_bottom = 630.0 offset_bottom = 630.0
horizontal_scroll_mode = 0
[node name="ContentVBox" type="VBoxContainer" parent="AboutPanel/ScrollContainer" unique_id=1746337912] [node name="ContentVBox" type="VBoxContainer" parent="AboutPanel/ScrollContainer" unique_id=1746337912]
layout_mode = 2 layout_mode = 2
@@ -40,14 +43,11 @@ size_flags_vertical = 3
[node name="VersionLabel" type="Label" parent="AboutPanel/ScrollContainer/ContentVBox" unique_id=352884126] [node name="VersionLabel" type="Label" parent="AboutPanel/ScrollContainer/ContentVBox" unique_id=352884126]
unique_name_in_owner = true unique_name_in_owner = true
texture_filter = 1
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
horizontal_alignment = 1 horizontal_alignment = 1
[node name="DescriptionLabel" type="Label" parent="AboutPanel/ScrollContainer/ContentVBox" unique_id=543094263] [node name="DescriptionLabel" type="Label" parent="AboutPanel/ScrollContainer/ContentVBox" unique_id=543094263]
texture_filter = 1
custom_minimum_size = Vector2(560, 0)
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
text = "Try your video game music knowledge with this VGM randomizer, invite your friends and see who is the best." text = "Try your video game music knowledge with this VGM randomizer, invite your friends and see who is the best."
@@ -56,8 +56,6 @@ autowrap_mode = 2
[node name="WhatsNewLabel" type="Label" parent="AboutPanel/ScrollContainer/ContentVBox" unique_id=1363040291] [node name="WhatsNewLabel" type="Label" parent="AboutPanel/ScrollContainer/ContentVBox" unique_id=1363040291]
unique_name_in_owner = true unique_name_in_owner = true
texture_filter = 1
custom_minimum_size = Vector2(560, 0)
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
autowrap_mode = 2 autowrap_mode = 2
-40
View File
@@ -1,40 +0,0 @@
extends Control
@onready
var character_grid_container := $ScrollContainer2
@onready
var character_grid := $ScrollContainer2/GridContainer
signal character_selected(file_name)
# Called when the node enters the scene tree for the first time.
func _ready():
var characters := DirAccess.open("res://characters/")
if characters:
characters.list_dir_begin()
var file_name = characters.get_next()
while file_name != "":
file_name = file_name.replace('.import', '') # <--- remove the .import
if file_name.ends_with(".png"):
var texture = load("res://characters/" + file_name)
var texture_btn := TextureButton.new()
character_grid.add_child(texture_btn)
texture_btn.custom_minimum_size = Vector2(80, 40)
texture_btn.ignore_texture_size = true
texture_btn.stretch_mode = TextureButton.STRETCH_KEEP_ASPECT
texture_btn.texture_normal = texture
texture_btn.pressed.connect(select_character.bind(file_name))
file_name = characters.get_next()
func show_grid():
character_grid_container.visible = true
func select_character(file_name: String):
print("select_character")
character_selected.emit(file_name)
-1
View File
@@ -1 +0,0 @@
uid://cmxl2w45vvmx6
-25
View File
@@ -1,25 +0,0 @@
[gd_scene load_steps=2 format=3 uid="uid://b16on0oscg1bv"]
[ext_resource type="Script" uid="uid://cmxl2w45vvmx6" path="res://CharacterSelect.gd" id="1_4bba6"]
[node name="CharacterSelect" type="Control"]
layout_mode = 3
anchors_preset = 0
offset_right = 40.0
offset_bottom = 40.0
script = ExtResource("1_4bba6")
[node name="ScrollContainer2" type="ScrollContainer" parent="."]
clip_contents = false
custom_minimum_size = Vector2(100, 100)
layout_mode = 0
offset_left = 194.0
offset_top = 152.0
offset_right = 670.0
offset_bottom = 375.0
[node name="GridContainer" type="GridContainer" parent="ScrollContainer2"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
columns = 3
+28 -24
View File
@@ -1,34 +1,38 @@
extends ScrollContainer extends ScrollContainer
var SCROLL: float = 0 # Scroll direction: 1 = down, -1 = up
var delay: float = 0.02 #seconds var scroll_direction: int = 1
var wait: float = 0
# Called when the node enters the scene tree for the first time. # Called when the node enters the scene tree for the first time.
func _ready() -> void: func _ready() -> void:
wait = delay scroll_direction = 1
# Called every frame. 'delta' is the elapsed time since the previous frame. # Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void: func _process(delta: float) -> void:
wait -= delta var speed_pixels_per_second: float = float(Settings.inspiration_list_speed) * 60.0
if wait < 0: var scroll_amount: float = speed_pixels_per_second * delta * float(scroll_direction)
wait = delay
#SCROLL DOWN var scroll_bar = get_v_scroll_bar()
if SCROLL == 1:
if (scroll_vertical + get_v_scroll_bar().page) < get_v_scroll_bar().max_value: # Don't scroll if list is empty or too short
scroll_vertical += Settings.inspiration_list_speed if scroll_bar.max_value <= 0:
else: return
scroll_back_up()
#SCROLL UP
elif SCROLL == -1:
if scroll_vertical != 0:
scroll_vertical -= Settings.inspiration_list_speed
else:
scroll_to_bottom()
func scroll_back_up() -> void: var current_pos = scroll_bar.value
SCROLL = -1
func scroll_to_bottom() -> void: if scroll_direction == 1:
scroll_vertical = 0 #Reset to top first. # Scrolling down
SCROLL = 1 if current_pos + scroll_bar.page < scroll_bar.max_value:
scroll_bar.value = min(current_pos + scroll_amount, scroll_bar.max_value - scroll_bar.page)
else:
# Reached bottom, reverse direction
scroll_direction = -1
scroll_bar.value = scroll_bar.max_value - scroll_bar.page
else:
# Scrolling up
if current_pos > 0:
scroll_bar.value = max(current_pos + scroll_amount, 0)
else:
# Reached top, reverse direction
scroll_direction = 1
scroll_bar.value = 0
+21 -7
View File
@@ -1,8 +1,10 @@
[gd_scene load_steps=2 format=3 uid="uid://bijh5h5yrivm3"] [gd_scene load_steps=2 format=3 uid="uid://bijh5h5yrivm3"]
[ext_resource type="Script" uid="uid://cw41y87l64qo7" path="res://log.gd" id="1_q72ua"] [ext_resource type="Script" uid="uid://cw41y87l64qo7" path="res://log.gd" id="1_q72ua"]
[ext_resource type="Theme" path="res://popup_theme.tres" id="popup_theme"]
[node name="Control" type="Control"] [node name="Control" type="Control" node_paths=PackedStringArray("panel")]
theme = ExtResource("popup_theme")
custom_minimum_size = Vector2(700, 700) custom_minimum_size = Vector2(700, 700)
layout_mode = 3 layout_mode = 3
anchors_preset = 15 anchors_preset = 15
@@ -11,23 +13,35 @@ anchor_bottom = 1.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
script = ExtResource("1_q72ua") script = ExtResource("1_q72ua")
panel = NodePath("LogPanel")
[node name="LogPanel" type="Panel" parent="."] [node name="LogPanel" type="Panel" parent="."]
unique_name_in_owner = true unique_name_in_owner = true
visibility_layer = 513 visibility_layer = 513
custom_minimum_size = Vector2(700, 700) custom_minimum_size = Vector2(700, 700)
layout_mode = 0 layout_mode = 0
offset_right = 40.0 offset_right = 700.0
offset_bottom = 40.0 offset_bottom = 700.0
[node name="TitleLabel" type="Label" parent="LogPanel"]
layout_mode = 0
offset_top = 8.0
offset_right = 700.0
offset_bottom = 48.0
theme_override_font_sizes/font_size = 25
text = "Log"
horizontal_alignment = 1
[node name="LogScrollContainer" type="ScrollContainer" parent="LogPanel"] [node name="LogScrollContainer" type="ScrollContainer" parent="LogPanel"]
custom_minimum_size = Vector2(700, 700)
layout_mode = 0 layout_mode = 0
offset_right = 40.0 offset_left = 20.0
offset_bottom = 40.0 offset_top = 56.0
offset_right = 680.0
offset_bottom = 684.0
horizontal_scroll_mode = 0
[node name="LogVBoxContainer" type="VBoxContainer" parent="LogPanel/LogScrollContainer"] [node name="LogVBoxContainer" type="VBoxContainer" parent="LogPanel/LogScrollContainer"]
unique_name_in_owner = true unique_name_in_owner = true
custom_minimum_size = Vector2(700, 700)
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 1 theme_override_constants/separation = 1
+141 -107
View File
@@ -23,7 +23,7 @@ var inspiration_scroll: ScrollContainer = $ScrollContainer
var inspiration_list: VBoxContainer = $ScrollContainer/VBoxContainer var inspiration_list: VBoxContainer = $ScrollContainer/VBoxContainer
@onready @onready
var game_label: Label = $VBoxContainer/GameLabel var soundtrack_label: Label = $VBoxContainer/SoundtrackLabel
@onready @onready
var song_label: Label = $VBoxContainer/SongLabel var song_label: Label = $VBoxContainer/SongLabel
@@ -107,7 +107,7 @@ var music_list: VBoxContainer = $MusicListPanel/ScrollContainer/MusicList
var player_container: VBoxContainer = $Players/VBoxContainer var player_container: VBoxContainer = $Players/VBoxContainer
@onready @onready
var character_select: Control = $CharacterSelect var players_list_container: VBoxContainer = $Players/VBoxContainer/ScrollContainer/PlayersListVBoxContainer
@onready @onready
var new_character_select: Control = %NewCharacterSelect var new_character_select: Control = %NewCharacterSelect
@@ -134,11 +134,21 @@ var player: PackedScene = preload("res://Player.tscn")
var song_list_object: PackedScene = preload("res://song_list_object.tscn") var song_list_object: PackedScene = preload("res://song_list_object.tscn")
var songs: Array= [] var songs: Array= []
var games: Array = [] var soundtracks: Array = []
var current_player: Node var current_player: Node
## True while statistics are off because a player won, until points are reset.
var stats_paused_by_win: bool = false
## Dims the window behind an open popup and keeps clicks from reaching the buttons under it.
var popup_backdrop: ColorRect
## Seconds to wait before retrying a song download step that failed.
var song_fetch_retry_seconds: float = 5.0
# Called when the node enters the scene tree for the first time. # Called when the node enters the scene tree for the first time.
func _ready() -> void: func _ready() -> void:
_setup_popup_backdrop()
var debug: bool = OS.has_feature("debug") var debug: bool = OS.has_feature("debug")
var is_mac: bool = OS.has_feature("macos") var is_mac: bool = OS.has_feature("macos")
if debug: if debug:
@@ -152,7 +162,7 @@ func _ready() -> void:
next_button.pressed.connect(play_next_song) next_button.pressed.connect(play_next_song)
show_answer_button.pressed.connect(show_answer_pressed) show_answer_button.pressed.connect(show_answer_pressed)
search_button.pressed.connect(show_search) search_button.pressed.connect(show_search)
sync_button.pressed.connect(sync_games) sync_button.pressed.connect(sync_soundtracks)
sound_test_button.pressed.connect(music_player_container.get_sound_test_song) sound_test_button.pressed.connect(music_player_container.get_sound_test_song)
statistics_button.pressed.connect(get_statistics) statistics_button.pressed.connect(get_statistics)
shortcuts_button.pressed.connect(show_shortcuts) shortcuts_button.pressed.connect(show_shortcuts)
@@ -162,12 +172,12 @@ func _ready() -> void:
reset_playlist_button.pressed.connect(reset_playlist) reset_playlist_button.pressed.connect(reset_playlist)
reset_points_button.pressed.connect(reset_points) reset_points_button.pressed.connect(reset_points)
character_select.connect("character_selected", _on_character_selected)
new_character_select.connect("new_new_character_selected", _on_new_character_selected) new_character_select.connect("new_new_character_selected", _on_new_character_selected)
new_player_name_field.connect("enter_key_pressed", add_player) new_player_name_field.connect("enter_key_pressed", add_player)
music_player_container.connect("play_next_song", play_next_song) music_player_container.connect("play_next_song", play_next_song)
music_player_container.connect("update_song_list", update_song_list) music_player_container.connect("update_song_list", update_song_list)
settings_window.connect("server_changed", server_updated) settings_window.connect("server_changed", server_updated)
settings_window.visibility_changed.connect(_on_settings_window_visibility_changed)
sync_window.connect("sync_started", _on_sync_started) sync_window.connect("sync_started", _on_sync_started)
sync_window.connect("sync_finished", _on_sync_finished) sync_window.connect("sync_finished", _on_sync_finished)
@@ -179,8 +189,7 @@ func _ready() -> void:
get_suggestion_list() get_suggestion_list()
fetch_song_list_at_start() fetch_song_list_at_start()
fetch_song() search_view.get_list_of_soundtracks()
search_view.get_list_of_games()
new_character_select.load_characters() new_character_select.load_characters()
func _input(event: InputEvent) -> void: func _input(event: InputEvent) -> void:
@@ -209,9 +218,9 @@ func _input(event: InputEvent) -> void:
if Settings.fullscreen == false: if Settings.fullscreen == false:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED) DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
if event.alt_pressed && event.keycode == KEY_K: if event.alt_pressed && event.keycode == KEY_K:
shortcuts_window.visible = !shortcuts_window.visible shortcuts_window.toggle()
if event.alt_pressed && event.keycode == KEY_L: if event.alt_pressed && event.keycode == KEY_L:
log_window.visible = !log_window.visible log_window.toggle()
func server_updated() -> void: func server_updated() -> void:
print("server_updated") print("server_updated")
@@ -220,7 +229,7 @@ func server_updated() -> void:
func update_from_server() -> void: func update_from_server() -> void:
print("update_from_server") print("update_from_server")
search_view.get_list_of_games() search_view.get_list_of_soundtracks()
get_suggestion_list() get_suggestion_list()
reset_playlist() reset_playlist()
new_character_select.load_characters() new_character_select.load_characters()
@@ -233,28 +242,29 @@ func show_answer_pressed() -> void:
Playlist.song_is_answered() Playlist.song_is_answered()
update_song_list() update_song_list()
# Loads the songs already in the server playlist, then starts preloading new ones.
# Preloading waits for the list, because loading the list clears the playlist.
func fetch_song_list_at_start() -> void: func fetch_song_list_at_start() -> void:
print("fetch_song_list_at_start") print("fetch_song_list_at_start")
var fetch_song_list_at_start_done: Callable = func fetch_songs_at_start_done(data) -> void: var fetch_song_list_at_start_done: Callable = func fetch_songs_at_start_done(data) -> void:
Playlist.clear_song_list() Playlist.clear_song_list()
if data == null:
Playlist.update_last_played_song()
update_song_list()
return
if typeof(data) == TYPE_ARRAY: if typeof(data) == TYPE_ARRAY:
for d: Dictionary in data: for d: Dictionary in data:
var new_song_object: SongObject = SongObject.new() var new_song_object: SongObject = SongObject.new()
new_song_object.song_number = d.SongNo new_song_object.song_number = d.SongNo
new_song_object.game_title = d.Game new_song_object.soundtrack_title = d.Soundtrack
new_song_object.song_title = d.Song new_song_object.song_title = d.Song
new_song_object.is_answered = true new_song_object.is_answered = true
new_song_object.has_played = true new_song_object.has_played = true
Playlist.add_to_playlist(new_song_object) Playlist.add_to_playlist(new_song_object)
else: elif data != null:
print("Unexpected data") print("Unexpected data")
Playlist.update_last_played_song() Playlist.update_last_played_song()
update_song_list() update_song_list()
Settings.make_request2("/music/list", fetch_song_list_at_start_done, true) if !Playlist.is_song_downloading:
fetch_song()
MusicServerClient.fetch_song_list(fetch_song_list_at_start_done,
_retry_later.bind(fetch_song_list_at_start))
func play_next_song() -> void: func play_next_song() -> void:
print("play_next_song ", Playlist.latest_played_song) print("play_next_song ", Playlist.latest_played_song)
@@ -271,48 +281,81 @@ func play_next_song() -> void:
#"/music/rand" #"/music/rand"
#"/music/addQue" #"/music/addQue"
#"/music/info" #"/music/info"
func _popups() -> Array[PopupWindow]:
return [search_view, sync_window, settings_window, qr_window, new_character_select,
shortcuts_window, about_window, winner_window, log_window]
# The backdrop sits above the main window and below every popup. Popups close
# themselves when clicked outside, and the backdrop keeps that click from reaching
# the buttons behind them.
func _setup_popup_backdrop() -> void:
popup_backdrop = ColorRect.new()
popup_backdrop.name = "PopupBackdrop"
popup_backdrop.color = Color(0, 0, 0, 0.55)
popup_backdrop.mouse_filter = Control.MOUSE_FILTER_STOP
add_child(popup_backdrop)
popup_backdrop.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
for popup: PopupWindow in _popups():
move_child(popup, -1)
popup.visibility_changed.connect(_update_popup_backdrop)
_update_popup_backdrop()
func _update_popup_backdrop() -> void:
popup_backdrop.visible = _popups().any(func(popup: PopupWindow) -> bool: return popup.visible)
# The cache size can be changed in the settings. Waiting until the settings are closed
# avoids starting downloads while the user is still clicking the number up and down.
func _on_settings_window_visibility_changed() -> void:
if !settings_window.visible && !Playlist.is_song_downloading && Playlist.should_preload_more_songs():
fetch_song()
func fetch_song() -> void: func fetch_song() -> void:
print("fetch_song") print("fetch_song")
Playlist.is_song_downloading = true Playlist.is_song_downloading = true
var url: String = "" _download_random_song(SongObject.new())
if Settings.use_low_played_mode:
url = "/music/rand/low" # Fetching a song takes three requests. If one fails, only that step is retried, so
else: # the server and the game keep the same songs in their playlists.
url = "/music/rand" func _download_random_song(new_song_object: SongObject) -> void:
var new_song_object: SongObject = SongObject.new() var song_downloaded: Callable = func song_downloaded(body: PackedByteArray) -> void:
var fetch_song_done: Callable = func fetch_song_done(body: PackedByteArray) -> void: print("song_downloaded")
print("fetch_song_done")
var sound: AudioStream = AudioStreamMP3.new() var sound: AudioStream = AudioStreamMP3.new()
sound.data = body sound.data = body
new_song_object.song = sound new_song_object.song = sound
_add_song_to_queue(new_song_object)
var added_to_que_done: Callable = func added_to_que_done() -> void: MusicServerClient.fetch_random_song(Settings.use_low_played_mode, song_downloaded,
print("added_to_que_done") _retry_later.bind(_download_random_song.bind(new_song_object)))
var fetch_song_info_done: Callable = func fetch_song_info_done(data_received: Dictionary) -> void:
print("fetch_song_info_done")
new_song_object.song_number = data_received.SongNo
new_song_object.game_title = data_received.Game
new_song_object.song_title = data_received.Song
print("new_song_object: ", new_song_object.to_string())
Playlist.add_to_playlist(new_song_object)
next_button.disabled = false
if Playlist.should_preload_more_songs():
fetch_song()
else:
Playlist.is_song_downloading = false
if Settings.is_debug:
update_song_list()
Settings.make_request2("/music/info", fetch_song_info_done, true)
Settings.make_request2("/music/addQue", added_to_que_done, false) func _add_song_to_queue(new_song_object: SongObject) -> void:
MusicServerClient.add_to_queue(_fetch_song_info.bind(new_song_object),
_retry_later.bind(_add_song_to_queue.bind(new_song_object)))
func _fetch_song_info(new_song_object: SongObject) -> void:
var song_info_fetched: Callable = func song_info_fetched(data_received: Dictionary) -> void:
print("song_info_fetched")
new_song_object.song_number = data_received.SongNo
new_song_object.soundtrack_title = data_received.Soundtrack
new_song_object.song_title = data_received.Song
print("new_song_object: ", new_song_object.to_string())
Playlist.add_to_playlist(new_song_object)
next_button.disabled = false
if Playlist.should_preload_more_songs():
fetch_song()
else:
Playlist.is_song_downloading = false
if Settings.is_debug:
update_song_list()
MusicServerClient.fetch_song_info(song_info_fetched,
_retry_later.bind(_fetch_song_info.bind(new_song_object)))
func _retry_later(step: Callable) -> void:
print("Song download failed, retrying in %s seconds" % song_fetch_retry_seconds)
get_tree().create_timer(song_fetch_retry_seconds).timeout.connect(step)
Settings.make_request2(url, fetch_song_done, true)
func update_song_list() -> void: func update_song_list() -> void:
print("update_song_list") print("update_song_list")
if Playlist.currently_playing_song >= 0: if Playlist.currently_playing_song >= 0:
game_label.text = Playlist.get_current_game_title() soundtrack_label.text = Playlist.get_current_soundtrack_title()
song_label.text = Playlist.get_current_song_title() song_label.text = Playlist.get_current_song_title()
Settings.delete_children(music_list) Settings.delete_children(music_list)
for s: SongObject in Playlist.get_playlist(): for s: SongObject in Playlist.get_playlist():
@@ -332,7 +375,7 @@ func song_object_clicked(event: InputEvent, song_no: int) -> void:
sound.data = body sound.data = body
Playlist.add_song_to_song_object(song_no, sound) Playlist.add_song_to_song_object(song_no, sound)
music_player_container.play_song_object(song_no) music_player_container.play_song_object(song_no)
Settings.make_request2("/music?song=" + str(song_no), song_downloaded, true) MusicServerClient.fetch_song(song_no, song_downloaded)
else: else:
print("song already downloaded") print("song already downloaded")
music_player_container.play_song_object(song_no) music_player_container.play_song_object(song_no)
@@ -342,39 +385,42 @@ func reset_playlist() -> void:
var playlist_reset_done: Callable = func playlist_reset_done() -> void: var playlist_reset_done: Callable = func playlist_reset_done() -> void:
Playlist.reset_playlist() Playlist.reset_playlist()
fetch_song_list_at_start() fetch_song_list_at_start()
fetch_song()
Settings.make_request2("/music/reset", playlist_reset_done, false) MusicServerClient.reset_playlist(playlist_reset_done)
func reset_points() -> void: func reset_points() -> void:
print("reset_points") print("reset_points")
for player_objects: PlayerObject in Settings.player_array: for player_objects: PlayerObject in Settings.player_array:
player_objects.reset_points() player_objects.reset_points()
load_players() load_players()
# A new game starts, so turn statistics back on if winning turned them off
if stats_paused_by_win:
Settings.add_to_stats = true
stats_paused_by_win = false
func show_about() -> void: func show_about() -> void:
print("show_about") print("show_about")
about_window.show_window() about_window.open()
func show_shortcuts() -> void: func show_shortcuts() -> void:
print("show_shortcuts") print("show_shortcuts")
shortcuts_window.visible = true shortcuts_window.open()
func show_settings() -> void: func show_settings() -> void:
print("show_settings") print("show_settings")
settings_window.visible = true settings_window.open()
func show_qr() -> void: func show_qr() -> void:
print("show_qr") print("show_qr")
qr_window.show_window() qr_window.open()
func get_statistics() -> void: func get_statistics() -> void:
print("get_statistics") print("get_statistics")
statistic_popup.visible = true statistic_popup.visible = true
statistic_label.text = "Total amount of games in the playlist: " + str(games.size()) statistic_label.text = "Total amount of soundtracks in the playlist: " + str(soundtracks.size())
func sync_games() -> void: func sync_soundtracks() -> void:
print("sync_games") print("sync_soundtracks")
sync_window.start_sync() sync_window.start_sync()
func _on_sync_started() -> void: func _on_sync_started() -> void:
@@ -397,20 +443,14 @@ func get_suggestion_list() -> void:
print("get_suggestion_list") print("get_suggestion_list")
var populate_list: Callable = func populate_list(array) -> void: var populate_list: Callable = func populate_list(array) -> void:
if typeof(array) == TYPE_ARRAY: if typeof(array) == TYPE_ARRAY:
games = [] soundtracks = []
Settings.delete_children(inspiration_list) Settings.delete_children(inspiration_list)
games.append_array(array) soundtracks.append_array(array)
for game: String in games: for i: int in soundtracks.size():
var inspiration_label: Label = Label.new() inspiration_list.add_child(ListRow.create(soundtracks[i], i, 20))
inspiration_label.set_texture_filter(TextureFilter.TEXTURE_FILTER_NEAREST)
inspiration_label.add_theme_font_size_override("font_size", 20)
inspiration_label.text = game
inspiration_label.autowrap_mode = TextServer.AUTOWRAP_WORD
inspiration_list.add_child(inspiration_label)
inspiration_scroll.scroll_to_bottom()
else: else:
print("Unexpected data") print("Unexpected data")
Settings.make_request2("/music/all", populate_list, true) MusicServerClient.fetch_all_soundtracks(populate_list)
func add_players() -> void: func add_players() -> void:
print("add_players") print("add_players")
@@ -421,62 +461,66 @@ func add_players() -> void:
func add_player() -> void: func add_player() -> void:
print("add_player") print("add_player")
if new_player_name_field.text != "" && Settings.player_array.size() < Settings.MAX_NUMBER_OF_PLAYERS: if new_player_name_field.text != "" && Settings.player_array.size() < Settings.MAX_NUMBER_OF_PLAYERS:
var new_player_object: PlayerObject = PlayerObject.new(new_player_name_field.text, Settings.player_array.size()) var new_player_object: PlayerObject = PlayerObject.new(new_player_name_field.text)
new_player_name_field.text = "" new_player_name_field.text = ""
Settings.player_array.append(new_player_object) Settings.player_array.append(new_player_object)
new_player_object.connect("first_point_triggerd", music_player_container._on_point_triggered.bind("first")) new_player_object.connect("first_point_triggerd", music_player_container._on_point_triggered.bind("first"))
new_player_object.connect("match_point_triggerd", music_player_container._on_point_triggered.bind("match")) new_player_object.connect("match_point_triggerd", music_player_container._on_point_triggered.bind("match"))
new_player_object.connect("winner_triggerd", _on_player_won.bind(new_player_object.id)) new_player_object.connect("winner_triggerd", _on_player_won.bind(new_player_object))
new_player_object.connect("point_given_sound", _on_make_point_given_sound) new_player_object.connect("point_given_sound", _on_make_point_given_sound)
new_player_object.connect("player_point_given", _on_point_given.bind(new_player_object.id)) new_player_object.connect("player_point_given", _on_point_given.bind(new_player_object))
new_player_object.connect("player_point_taken", _on_point_taken.bind(new_player_object.id)) new_player_object.connect("player_point_taken", _on_point_taken.bind(new_player_object))
load_players() load_players()
func load_players() -> void: func load_players() -> void:
print("load_players") print("load_players")
Settings.delete_player_children(player_container) Settings.delete_children(players_list_container)
var counter: int = 0 var counter: int = 0
for player_object: PlayerObject in Settings.player_array: for player_object: PlayerObject in Settings.player_array:
print(player_object.player_name) print(player_object.player_name)
var new_player: Player = player.instantiate() var new_player: Player = player.instantiate()
new_player.id = counter new_player.player_object = player_object
new_player.list_index = counter
new_player.connect("change_character_clicked", _on_player_change_character_clicked.bind(new_player, player_object.player_name))
new_player.connect("player_removed", _on_player_removed.bind(counter))
player_container.add_child(new_player) new_player.connect("change_character_clicked", _on_player_change_character_clicked.bind(new_player, player_object.player_name))
new_player.connect("player_removed", _on_player_removed.bind(player_object))
players_list_container.add_child(new_player)
counter += 1 counter += 1
func _on_point_given(player_given_point: int) -> void: func _on_point_given(player_object: PlayerObject) -> void:
print("_on_point_given") print("_on_point_given")
log_window.add_log_row(Settings.player_array[player_given_point].player_name + " got a point") log_window.add_log_row(player_object.player_name + " got a point")
if Playlist.currently_playing_song >= 0: if Playlist.currently_playing_song >= 0:
Playlist.add_point(player_given_point) Playlist.add_point(player_object.id)
update_song_list() update_song_list()
func _on_make_point_given_sound() -> void: func _on_make_point_given_sound() -> void:
music_player_container.play_sound_effect(preload("res://sounds/itemopen.wav")) music_player_container.play_sound_effect(preload("res://sounds/itemopen.wav"))
func _on_point_taken(player_taken_point: int) -> void: func _on_point_taken(player_object: PlayerObject) -> void:
print("_on_point_taken") print("_on_point_taken")
log_window.add_log_row(Settings.player_array[player_taken_point].player_name + " lost a point") log_window.add_log_row(player_object.player_name + " lost a point")
music_player_container.play_sound_effect(preload("res://sounds/itemequip.wav")) music_player_container.play_sound_effect(preload("res://sounds/itemequip.wav"))
if Playlist.currently_playing_song >= 0: if Playlist.currently_playing_song >= 0:
Playlist.remove_point(player_taken_point) Playlist.remove_point(player_object.id)
update_song_list() update_song_list()
func _on_player_won(winning_player_id: int) -> void: func _on_player_won(player_object: PlayerObject) -> void:
print("_on_player_won") print("_on_player_won")
winner_window.show_winner(winning_player_id) winner_window.show_winner(player_object)
music_player_container.play_song(preload("res://sounds/winning.mp3")) music_player_container.play_song(preload("res://sounds/winning.mp3"))
Settings.add_to_stats = false # Songs played after the game is won don't count towards the statistics
if Settings.add_to_stats:
Settings.add_to_stats = false
stats_paused_by_win = true
func _on_player_removed(player_to_remove: int) -> void: func _on_player_removed(player_object: PlayerObject) -> void:
print("_on_player_removed ", player_to_remove) print("_on_player_removed ", player_object.player_name)
Settings.player_array.remove_at(player_to_remove) Settings.player_array.erase(player_object)
load_players() load_players()
Playlist.remove_player_from_list(player_to_remove) Playlist.remove_player_from_list(player_object.id)
update_song_list() update_song_list()
func _on_player_change_character_clicked(new_player: Player, player_name: String) -> void: func _on_player_change_character_clicked(new_player: Player, player_name: String) -> void:
@@ -485,21 +529,13 @@ func _on_player_change_character_clicked(new_player: Player, player_name: String
print("Change character for: " + player_name) print("Change character for: " + player_name)
new_character_select.show_grid(player_name) new_character_select.show_grid(player_name)
func _on_character_selected(file_name: String) -> void:
print("_on_character_selected")
character_select.visible = false
current_player.character_selected(file_name)
func _on_new_character_selected(character_texture: ImageTexture) -> void: func _on_new_character_selected(character_texture: ImageTexture) -> void:
print("_on_new_character_selected") print("_on_new_character_selected")
current_player.new_character_selected(character_texture) current_player.new_character_selected(character_texture)
func show_search() -> void: func show_search() -> void:
print("show_search") print("show_search")
if search_view.visible == false: search_view.toggle()
search_view.visible = true
else:
search_view.visible = false
###Local ###Local
@@ -519,11 +555,11 @@ func dir_contents(path: String) -> void:
dir.list_dir_begin() dir.list_dir_begin()
var file_name: String = dir.get_next() var file_name: String = dir.get_next()
songs.clear() songs.clear()
games = [] soundtracks = []
while file_name != "": while file_name != "":
if dir.current_is_dir(): if dir.current_is_dir():
#print("Found directory: " + file_name) #print("Found directory: " + file_name)
games.append(file_name) soundtracks.append(file_name)
else: else:
#print("Found file: " + file_name) #print("Found file: " + file_name)
if file_name.ends_with(".mp3"): if file_name.ends_with(".mp3"):
@@ -532,7 +568,5 @@ func dir_contents(path: String) -> void:
else: else:
print("An error occurred when trying to access the path.") print("An error occurred when trying to access the path.")
for game: String in games: for i: int in soundtracks.size():
var local_inspiration_label: Label = Label.new() inspiration_list.add_child(ListRow.create(soundtracks[i], i, 20))
local_inspiration_label.text = game
inspiration_list.add_child(local_inspiration_label)
+40 -57
View File
@@ -1,7 +1,6 @@
[gd_scene format=3 uid="uid://xwq863o6uvsu"] [gd_scene format=3 uid="uid://xwq863o6uvsu"]
[ext_resource type="Script" uid="uid://b8f4g15cas2j2" path="res://MainWindow.gd" id="1_eu0t5"] [ext_resource type="Script" uid="uid://b8f4g15cas2j2" path="res://MainWindow.gd" id="1_eu0t5"]
[ext_resource type="PackedScene" uid="uid://b16on0oscg1bv" path="res://CharacterSelect.tscn" id="2_76kf4"]
[ext_resource type="Script" uid="uid://elgn7gkxgagq" path="res://InsperationScrollContainer.gd" id="2_gxtxm"] [ext_resource type="Script" uid="uid://elgn7gkxgagq" path="res://InsperationScrollContainer.gd" id="2_gxtxm"]
[ext_resource type="Texture2D" uid="uid://bcfmpd7h512ef" path="res://icons/person_add_light.svg" id="5_31tjv"] [ext_resource type="Texture2D" uid="uid://bcfmpd7h512ef" path="res://icons/person_add_light.svg" id="5_31tjv"]
[ext_resource type="PackedScene" uid="uid://ds15cgsf8vpvc" path="res://MusicPlayer.tscn" id="5_emn36"] [ext_resource type="PackedScene" uid="uid://ds15cgsf8vpvc" path="res://MusicPlayer.tscn" id="5_emn36"]
@@ -17,6 +16,7 @@
[ext_resource type="PackedScene" uid="uid://btupbowehiyyu" path="res://QRWindow.tscn" id="15_qr_window"] [ext_resource type="PackedScene" uid="uid://btupbowehiyyu" path="res://QRWindow.tscn" id="15_qr_window"]
[ext_resource type="PackedScene" uid="uid://cp54idaudb8dp" path="res://AboutWindow.tscn" id="16_about_window"] [ext_resource type="PackedScene" uid="uid://cp54idaudb8dp" path="res://AboutWindow.tscn" id="16_about_window"]
[ext_resource type="PackedScene" uid="uid://cxlsmxnrp0agh" path="res://WinnerWindow.tscn" id="17_winner_window"] [ext_resource type="PackedScene" uid="uid://cxlsmxnrp0agh" path="res://WinnerWindow.tscn" id="17_winner_window"]
[ext_resource type="Theme" path="res://popup_theme.tres" id="popup_theme"]
[sub_resource type="LabelSettings" id="LabelSettings_ychxr"] [sub_resource type="LabelSettings" id="LabelSettings_ychxr"]
font_size = 25 font_size = 25
@@ -70,7 +70,11 @@ events = [SubResource("InputEventKey_ujjlu")]
[node name="Control" type="Control" unique_id=407149696] [node name="Control" type="Control" unique_id=407149696]
layout_mode = 3 layout_mode = 3
anchors_preset = 0 anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_eu0t5") script = ExtResource("1_eu0t5")
[node name="TopPanelContainer" type="PanelContainer" parent="." unique_id=663404711] [node name="TopPanelContainer" type="PanelContainer" parent="." unique_id=663404711]
@@ -79,7 +83,6 @@ offset_right = 1920.0
offset_bottom = 56.0 offset_bottom = 56.0
[node name="Title" type="Label" parent="." unique_id=1187907861] [node name="Title" type="Label" parent="." unique_id=1187907861]
texture_filter = 1
layout_mode = 0 layout_mode = 0
offset_left = 64.0 offset_left = 64.0
offset_top = 12.0 offset_top = 12.0
@@ -121,14 +124,12 @@ follow_focus = true
script = ExtResource("7_dj026") script = ExtResource("7_dj026")
[node name="MusicList" type="VBoxContainer" parent="MusicListPanel/ScrollContainer" unique_id=373398568] [node name="MusicList" type="VBoxContainer" parent="MusicListPanel/ScrollContainer" unique_id=373398568]
texture_filter = 1
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
size_flags_vertical = 3 size_flags_vertical = 3
theme_override_constants/separation = 10 theme_override_constants/separation = 10
[node name="ScrollContainer" type="ScrollContainer" parent="." unique_id=498283761] [node name="ScrollContainer" type="ScrollContainer" parent="." unique_id=498283761]
texture_filter = 1
layout_mode = 0 layout_mode = 0
offset_left = 1216.0 offset_left = 1216.0
offset_top = 128.0 offset_top = 128.0
@@ -139,7 +140,6 @@ vertical_scroll_mode = 3
script = ExtResource("2_gxtxm") script = ExtResource("2_gxtxm")
[node name="VBoxContainer" type="VBoxContainer" parent="ScrollContainer" unique_id=1215162949] [node name="VBoxContainer" type="VBoxContainer" parent="ScrollContainer" unique_id=1215162949]
texture_filter = 1
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
size_flags_vertical = 3 size_flags_vertical = 3
@@ -151,8 +151,7 @@ offset_top = 64.0
offset_right = 1152.0 offset_right = 1152.0
offset_bottom = 384.0 offset_bottom = 384.0
[node name="GameLabel" type="Label" parent="VBoxContainer" unique_id=173147793] [node name="SoundtrackLabel" type="Label" parent="VBoxContainer" unique_id=173147793]
texture_filter = 1
custom_minimum_size = Vector2(1, 1) custom_minimum_size = Vector2(1, 1)
layout_mode = 2 layout_mode = 2
label_settings = SubResource("LabelSettings_qspbu") label_settings = SubResource("LabelSettings_qspbu")
@@ -160,22 +159,12 @@ horizontal_alignment = 1
autowrap_mode = 2 autowrap_mode = 2
[node name="SongLabel" type="Label" parent="VBoxContainer" unique_id=1687490027] [node name="SongLabel" type="Label" parent="VBoxContainer" unique_id=1687490027]
texture_filter = 1
custom_minimum_size = Vector2(1, 1) custom_minimum_size = Vector2(1, 1)
layout_mode = 2 layout_mode = 2
label_settings = SubResource("LabelSettings_3m52w") label_settings = SubResource("LabelSettings_3m52w")
horizontal_alignment = 1 horizontal_alignment = 1
autowrap_mode = 2 autowrap_mode = 2
[node name="CharacterSelect" parent="." unique_id=2079506802 instance=ExtResource("2_76kf4")]
visible = false
top_level = true
layout_mode = 0
offset_left = 616.0
offset_top = 264.0
offset_right = 776.0
offset_bottom = 424.0
[node name="MusicPlayer" parent="." unique_id=1990459832 instance=ExtResource("5_emn36")] [node name="MusicPlayer" parent="." unique_id=1990459832 instance=ExtResource("5_emn36")]
layout_mode = 0 layout_mode = 0
anchors_preset = 0 anchors_preset = 0
@@ -205,7 +194,6 @@ custom_minimum_size = Vector2(2.08165e-12, 40)
layout_mode = 2 layout_mode = 2
[node name="Label" type="Label" parent="Players/VBoxContainer/HBoxContainer" unique_id=1331357386] [node name="Label" type="Label" parent="Players/VBoxContainer/HBoxContainer" unique_id=1331357386]
texture_filter = 1
custom_minimum_size = Vector2(2.08165e-12, 40) custom_minimum_size = Vector2(2.08165e-12, 40)
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
@@ -215,7 +203,6 @@ horizontal_alignment = 1
vertical_alignment = 1 vertical_alignment = 1
[node name="AddPlayersButton" type="TextureButton" parent="Players/VBoxContainer/HBoxContainer" unique_id=634410148] [node name="AddPlayersButton" type="TextureButton" parent="Players/VBoxContainer/HBoxContainer" unique_id=634410148]
texture_filter = 1
custom_minimum_size = Vector2(2.08165e-12, 40) custom_minimum_size = Vector2(2.08165e-12, 40)
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 8 size_flags_horizontal = 8
@@ -242,7 +229,6 @@ placeholder_text = "Player name"
script = ExtResource("7_qsdfy") script = ExtResource("7_qsdfy")
[node name="AddPlayerButton" type="Button" parent="Players/VBoxContainer/AddPlayerContainer" unique_id=245001313] [node name="AddPlayerButton" type="Button" parent="Players/VBoxContainer/AddPlayerContainer" unique_id=245001313]
texture_filter = 1
custom_minimum_size = Vector2(2.08165e-12, 40) custom_minimum_size = Vector2(2.08165e-12, 40)
layout_mode = 2 layout_mode = 2
text = "Add" text = "Add"
@@ -253,52 +239,57 @@ layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
theme_override_styles/panel = SubResource("StyleBoxFlat_k1ygi") theme_override_styles/panel = SubResource("StyleBoxFlat_k1ygi")
[node name="ScrollContainer" type="ScrollContainer" parent="Players/VBoxContainer" unique_id=1000000001]
layout_mode = 2
size_flags_vertical = 15
horizontal_scroll_mode = 0
[node name="PlayersListVBoxContainer" type="VBoxContainer" parent="Players/VBoxContainer/ScrollContainer" unique_id=1000000002]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 15
[node name="ResetPlaylistButton" type="Button" parent="." unique_id=1754155620] [node name="ResetPlaylistButton" type="Button" parent="." unique_id=1754155620]
texture_filter = 1
layout_mode = 0 layout_mode = 0
offset_left = 1359.0 offset_left = 1360.0
offset_top = 66.0 offset_top = 64.0
offset_right = 1470.0 offset_right = 1471.0
offset_bottom = 97.0 offset_bottom = 95.0
focus_mode = 0 focus_mode = 0
action_mode = 0 action_mode = 0
text = "Reset playlist" text = "Reset playlist"
[node name="ResetPointsButton" type="Button" parent="." unique_id=942880776] [node name="ResetPointsButton" type="Button" parent="." unique_id=942880776]
texture_filter = 1
layout_mode = 0 layout_mode = 0
offset_left = 1487.0 offset_left = 1488.0
offset_top = 66.0 offset_top = 64.0
offset_right = 1598.0 offset_right = 1599.0
offset_bottom = 97.0 offset_bottom = 95.0
focus_mode = 0 focus_mode = 0
action_mode = 0 action_mode = 0
text = "Reset points" text = "Reset points"
[node name="SyncButton" type="Button" parent="." unique_id=1228313556] [node name="SyncButton" type="Button" parent="." unique_id=1228313556]
texture_filter = 1
layout_mode = 0 layout_mode = 0
offset_left = 1615.0 offset_left = 1616.0
offset_top = 66.0 offset_top = 64.0
offset_right = 1726.0 offset_right = 1759.0
offset_bottom = 97.0 offset_bottom = 95.0
focus_mode = 0 focus_mode = 0
action_mode = 0 action_mode = 0
text = "Sync games" text = "Sync soundtracks"
[node name="SoundTestButton" type="Button" parent="." unique_id=1332971879] [node name="SoundTestButton" type="Button" parent="." unique_id=1332971879]
texture_filter = 1
layout_mode = 0 layout_mode = 0
offset_left = 1743.0 offset_left = 1776.0
offset_top = 66.0 offset_top = 64.0
offset_right = 1854.0 offset_right = 1887.0
offset_bottom = 97.0 offset_bottom = 95.0
focus_mode = 0 focus_mode = 0
action_mode = 0 action_mode = 0
text = "Sound test" text = "Sound test"
[node name="ShowAnswerButton" type="Button" parent="." unique_id=415388039] [node name="ShowAnswerButton" type="Button" parent="." unique_id=415388039]
texture_filter = 1
layout_mode = 0 layout_mode = 0
offset_left = 65.0 offset_left = 65.0
offset_top = 944.0 offset_top = 944.0
@@ -310,7 +301,6 @@ shortcut = SubResource("Shortcut_a7fvb")
text = "Show answer" text = "Show answer"
[node name="NextButton" type="Button" parent="." unique_id=1744703488] [node name="NextButton" type="Button" parent="." unique_id=1744703488]
texture_filter = 1
layout_mode = 0 layout_mode = 0
offset_left = 193.0 offset_left = 193.0
offset_top = 944.0 offset_top = 944.0
@@ -323,7 +313,6 @@ shortcut = SubResource("Shortcut_d6fml")
text = "Randomize new track" text = "Randomize new track"
[node name="SearchButton" type="Button" parent="." unique_id=1782453581] [node name="SearchButton" type="Button" parent="." unique_id=1782453581]
texture_filter = 1
layout_mode = 0 layout_mode = 0
offset_left = 1354.0 offset_left = 1354.0
offset_top = 8.0 offset_top = 8.0
@@ -335,7 +324,6 @@ shortcut = SubResource("Shortcut_fbju4")
text = "Search" text = "Search"
[node name="SettingsButton" type="Button" parent="." unique_id=1193400323] [node name="SettingsButton" type="Button" parent="." unique_id=1193400323]
texture_filter = 1
layout_mode = 0 layout_mode = 0
offset_left = 1442.0 offset_left = 1442.0
offset_top = 8.0 offset_top = 8.0
@@ -346,7 +334,6 @@ action_mode = 0
text = "Settings" text = "Settings"
[node name="StatisticsButton" type="Button" parent="." unique_id=2045021269] [node name="StatisticsButton" type="Button" parent="." unique_id=2045021269]
texture_filter = 1
layout_mode = 0 layout_mode = 0
offset_left = 1530.0 offset_left = 1530.0
offset_top = 8.0 offset_top = 8.0
@@ -357,7 +344,6 @@ action_mode = 0
text = "Statistics" text = "Statistics"
[node name="ShortcutsButton" type="Button" parent="." unique_id=2066498226] [node name="ShortcutsButton" type="Button" parent="." unique_id=2066498226]
texture_filter = 1
layout_mode = 0 layout_mode = 0
offset_left = 1626.0 offset_left = 1626.0
offset_top = 8.0 offset_top = 8.0
@@ -368,7 +354,6 @@ action_mode = 0
text = "Shortcuts" text = "Shortcuts"
[node name="AboutButton" type="Button" parent="." unique_id=1057035253] [node name="AboutButton" type="Button" parent="." unique_id=1057035253]
texture_filter = 1
layout_mode = 0 layout_mode = 0
offset_left = 1722.0 offset_left = 1722.0
offset_top = 8.0 offset_top = 8.0
@@ -379,7 +364,6 @@ action_mode = 0
text = "About" text = "About"
[node name="QRButton" type="Button" parent="." unique_id=2021908559] [node name="QRButton" type="Button" parent="." unique_id=2021908559]
texture_filter = 1
layout_mode = 0 layout_mode = 0
offset_left = 1802.0 offset_left = 1802.0
offset_top = 8.0 offset_top = 8.0
@@ -401,9 +385,9 @@ offset_bottom = 96.0
unique_name_in_owner = true unique_name_in_owner = true
visible = false visible = false
layout_mode = 1 layout_mode = 1
offset_left = 401.0 offset_left = 210.0
offset_top = 153.0 offset_top = 153.0
offset_right = 401.0 offset_right = 210.0
offset_bottom = 153.0 offset_bottom = 153.0
[node name="SettingsWindow" parent="." unique_id=1323246978 instance=ExtResource("11_k62u5")] [node name="SettingsWindow" parent="." unique_id=1323246978 instance=ExtResource("11_k62u5")]
@@ -427,9 +411,9 @@ offset_bottom = 153.0
unique_name_in_owner = true unique_name_in_owner = true
visible = false visible = false
layout_mode = 1 layout_mode = 1
offset_left = 420.0 offset_left = 405.0
offset_top = 165.0 offset_top = 165.0
offset_right = 1572.0 offset_right = 1557.0
offset_bottom = 805.0 offset_bottom = 805.0
[node name="ShortcutsWindow" parent="." unique_id=1513416242 instance=ExtResource("13_8eebo")] [node name="ShortcutsWindow" parent="." unique_id=1513416242 instance=ExtResource("13_8eebo")]
@@ -442,12 +426,12 @@ offset_right = 1262.0
offset_bottom = 860.0 offset_bottom = 860.0
[node name="StatisticsPopupPanel" type="PopupPanel" parent="." unique_id=298311407] [node name="StatisticsPopupPanel" type="PopupPanel" parent="." unique_id=298311407]
theme = ExtResource("popup_theme")
oversampling_override = 1.0 oversampling_override = 1.0
initial_position = 2 initial_position = 2
size = Vector2i(450, 100) size = Vector2i(450, 100)
[node name="Label" type="Label" parent="StatisticsPopupPanel" unique_id=548316493] [node name="Label" type="Label" parent="StatisticsPopupPanel" unique_id=548316493]
texture_filter = 1
offset_left = 4.0 offset_left = 4.0
offset_top = 4.0 offset_top = 4.0
offset_right = 446.0 offset_right = 446.0
@@ -456,12 +440,11 @@ text = "Statistics"
horizontal_alignment = 1 horizontal_alignment = 1
[node name="StatisticsLabel" type="Label" parent="StatisticsPopupPanel" unique_id=58226477] [node name="StatisticsLabel" type="Label" parent="StatisticsPopupPanel" unique_id=58226477]
texture_filter = 1
offset_left = 4.0 offset_left = 4.0
offset_top = 4.0 offset_top = 4.0
offset_right = 446.0 offset_right = 446.0
offset_bottom = 96.0 offset_bottom = 96.0
text = "Total amount of games in the playlist: 9000" text = "Total amount of soundtracks in the playlist: 9000"
horizontal_alignment = 1 horizontal_alignment = 1
vertical_alignment = 1 vertical_alignment = 1
+5 -8
View File
@@ -28,9 +28,6 @@ var volume_slider: HSlider = $MusicPlayerContainer/VolumeSlider
@onready @onready
var music_time_label: Label = $MusicPlayerContainer/MusicTimeLabel var music_time_label: Label = $MusicPlayerContainer/MusicTimeLabel
@onready
var path: String = '/Users/sebastian/ResilioSync/Sorterat_test/Metal Gear Solid 4 - Guns of the Patriots/2-16 Metal Gear Saga.mp3'
var play_icon: Texture = preload("res://icons/play_icon_light.svg") var play_icon: Texture = preload("res://icons/play_icon_light.svg")
var pause_icon: Texture = preload("res://icons/pause_icon_light.svg") var pause_icon: Texture = preload("res://icons/pause_icon_light.svg")
@@ -63,12 +60,12 @@ func _process(_delta: float) -> void:
music_time_label.text = format_text(progress_slider.value, 0.0) music_time_label.text = format_text(progress_slider.value, 0.0)
func format_time(time: float) -> String: func format_time(time: float) -> String:
var mins: String = "%02d" % floor(time / 60) var total_seconds: int = roundi(time)
var sec: String = "%02d" % round(fmod(time, 60)) @warning_ignore("integer_division")
return mins + ":" + sec return "%02d:%02d" % [total_seconds / 60, total_seconds % 60]
func format_text(part: float, total: float) -> String: func format_text(part: float, total: float) -> String:
if (Settings.hide_beginning && part <= Settings.hide_for_seconds) && Settings.hide_length: if (Settings.hide_beginning && part < Settings.hide_for_seconds) && Settings.hide_length:
return "??:?? / ??:??" return "??:?? / ??:??"
elif Settings.hide_length: elif Settings.hide_length:
return format_time(part) + " / ??:??" return format_time(part) + " / ??:??"
@@ -209,7 +206,7 @@ func get_sound_test_song() -> void:
var sound: AudioStream = AudioStreamMP3.new() var sound: AudioStream = AudioStreamMP3.new()
sound.data = body sound.data = body
play_song(sound) play_song(sound)
Settings.make_request2("/music/soundTest", test_sound, true) MusicServerClient.fetch_sound_test_song(test_sound)
##### LOCAL ##### LOCAL
var local_path: String = '/Users/sebastian/ResilioSync/Sorterat_test/Metal Gear Solid 4 - Guns of the Patriots/2-16 Metal Gear Saga.mp3' var local_path: String = '/Users/sebastian/ResilioSync/Sorterat_test/Metal Gear Solid 4 - Guns of the Patriots/2-16 Metal Gear Saga.mp3'
+16 -17
View File
@@ -19,15 +19,19 @@ var remove_player := $HBoxContainer/RemovePlayer
signal change_character_clicked signal change_character_clicked
signal player_removed signal player_removed
@export ## The player shown in this row.
var id: int var player_object: PlayerObject
## Position in Settings.player_array. Only used for the keybinds: Alt+<list_index + 1>
## gives this player a point and Ctrl+Alt+<list_index + 1> takes one away.
var list_index: int
# Called when the node enters the scene tree for the first time. # Called when the node enters the scene tree for the first time.
func _ready() -> void: func _ready() -> void:
add_button.pressed.connect(add_point) add_button.pressed.connect(add_point)
add_button.set_shortcut(create_button_shortcut(id, false)) add_button.set_shortcut(create_button_shortcut(list_index, false))
minus_button.pressed.connect(minus_point) minus_button.pressed.connect(minus_point)
minus_button.set_shortcut(create_button_shortcut(id, true)) minus_button.set_shortcut(create_button_shortcut(list_index, true))
character.pressed.connect(change_character) character.pressed.connect(change_character)
remove_player.pressed.connect(func() -> void: player_removed.emit()) remove_player.pressed.connect(func() -> void: player_removed.emit())
update_score() update_score()
@@ -36,44 +40,39 @@ func _ready() -> void:
func _process(delta: float) -> void: func _process(delta: float) -> void:
remove_player.visible = Settings.edit_players remove_player.visible = Settings.edit_players
func create_button_shortcut(scancode : int, ctrl_pressed: bool) -> Shortcut: func create_button_shortcut(index: int, ctrl_pressed: bool) -> Shortcut:
var button_shortcut: Shortcut = Shortcut.new() var button_shortcut: Shortcut = Shortcut.new()
var button_event : InputEventKey = InputEventKey.new() var button_event : InputEventKey = InputEventKey.new()
button_event.keycode = scancode + 49 button_event.keycode = KEY_1 + index
button_event.alt_pressed = true button_event.alt_pressed = true
button_event.ctrl_pressed = ctrl_pressed button_event.ctrl_pressed = ctrl_pressed
button_shortcut.events.append(button_event) button_shortcut.events.append(button_event)
return button_shortcut return button_shortcut
func add_point() -> void: func add_point() -> void:
Settings.player_array[id].add_point() player_object.add_point()
update_score() update_score()
func minus_point() -> void: func minus_point() -> void:
Settings.player_array[id].minus_point() player_object.minus_point()
update_score() update_score()
func change_character() -> void: func change_character() -> void:
change_character_clicked.emit() change_character_clicked.emit()
func character_selected(file_name: String) -> void:
print("Back in player list with: " + file_name)
Settings.player_array[id].character = load("res://characters/" + file_name)
set_player_character()
func new_character_selected(character_texture: ImageTexture) -> void: func new_character_selected(character_texture: ImageTexture) -> void:
print("Back in player list with new") print("Back in player list with new")
Settings.player_array[id].character = character_texture player_object.character = character_texture
set_player_character() set_player_character()
func set_player_character() -> void: func set_player_character() -> void:
character.custom_minimum_size = Vector2(80, 40) character.custom_minimum_size = Vector2(80, 40)
character.ignore_texture_size = true character.ignore_texture_size = true
character.stretch_mode = TextureButton.STRETCH_KEEP_ASPECT_CENTERED character.stretch_mode = TextureButton.STRETCH_KEEP_ASPECT_CENTERED
character.texture_normal = Settings.player_array[id].character character.texture_normal = player_object.character
func update_score() -> void: func update_score() -> void:
if OS.has_feature("debug"): if OS.has_feature("debug"):
player_name_field.text = str(Settings.player_array[id].id) + " " + Settings.player_array[id].player_name + ": " + str(Settings.player_array[id].player_score) player_name_field.text = str(list_index + 1) + " " + player_object.player_name + ": " + str(player_object.player_score)
else: else:
player_name_field.text = Settings.player_array[id].player_name + ": " + str(Settings.player_array[id].player_score) player_name_field.text = player_object.player_name + ": " + str(player_object.player_score)
+10 -5
View File
@@ -29,9 +29,12 @@ script = ExtResource("1_if4kc")
[node name="HBoxContainer" type="HBoxContainer" parent="."] [node name="HBoxContainer" type="HBoxContainer" parent="."]
custom_minimum_size = Vector2(2.08165e-12, 40) custom_minimum_size = Vector2(2.08165e-12, 40)
layout_mode = 0 layout_mode = 1
offset_right = 40.0 anchors_preset = 15
offset_bottom = 40.0 anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="Panel4" type="Panel" parent="HBoxContainer"] [node name="Panel4" type="Panel" parent="HBoxContainer"]
custom_minimum_size = Vector2(5, 40) custom_minimum_size = Vector2(5, 40)
@@ -41,12 +44,15 @@ theme_override_styles/panel = SubResource("StyleBoxFlat_821k2")
[node name="Name" type="Label" parent="HBoxContainer"] [node name="Name" type="Label" parent="HBoxContainer"]
custom_minimum_size = Vector2(130, 40) custom_minimum_size = Vector2(130, 40)
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 1 size_flags_vertical = 1
text = ": 0" text = ": 0"
vertical_alignment = 1 vertical_alignment = 1
clip_text = true
text_overrun_behavior = 3
[node name="Panel3" type="Panel" parent="HBoxContainer"] [node name="Panel3" type="Panel" parent="HBoxContainer"]
custom_minimum_size = Vector2(80, 2.08165e-12) custom_minimum_size = Vector2(16, 2.08165e-12)
layout_mode = 2 layout_mode = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_w10e8") theme_override_styles/panel = SubResource("StyleBoxFlat_w10e8")
@@ -91,7 +97,6 @@ theme_override_styles/panel = SubResource("StyleBoxFlat_b2crt")
[node name="RemovePlayer" type="TextureButton" parent="HBoxContainer"] [node name="RemovePlayer" type="TextureButton" parent="HBoxContainer"]
custom_minimum_size = Vector2(40, 40) custom_minimum_size = Vector2(40, 40)
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3
focus_mode = 0 focus_mode = 0
action_mode = 0 action_mode = 0
texture_normal = ExtResource("3_j3uxe") texture_normal = ExtResource("3_j3uxe")
+24 -10
View File
@@ -1,4 +1,4 @@
extends Node extends RefCounted
class_name PlayerObject class_name PlayerObject
signal first_point_triggerd signal first_point_triggerd
@@ -8,8 +8,8 @@ signal point_given_sound
signal player_point_given signal player_point_given
signal player_point_taken signal player_point_taken
@export ## Unique id that never changes, unlike the player's position in Settings.player_array.
var id: int var id: String = _generate_uuid()
@export @export
var player_name: String var player_name: String
@@ -19,24 +19,38 @@ var player_score: int
var is_first_point: bool = true var is_first_point: bool = true
var character: Texture = load("res://noCharacter.png") ## Picture shown until the player picks a character.
const NO_CHARACTER_PATH: String = "res://noCharacter.png"
func _init(new_name: String, new_id: int) -> void: var character: Texture = load(NO_CHARACTER_PATH)
func _init(new_name: String) -> void:
player_name = new_name player_name = new_name
id = new_id
player_score = 0 player_score = 0
## Returns a random version 4 UUID, e.g. "3f2b8c1e-9a4d-4e7f-b2c6-5d8e1f0a7b93".
static func _generate_uuid() -> String:
var bytes: PackedByteArray = Crypto.new().generate_random_bytes(16)
bytes[6] = (bytes[6] & 0x0f) | 0x40
bytes[8] = (bytes[8] & 0x3f) | 0x80
var hex: String = bytes.hex_encode()
return "%s-%s-%s-%s-%s" % [hex.substr(0, 8), hex.substr(8, 4), hex.substr(12, 4), hex.substr(16, 4), hex.substr(20, 12)]
func add_point() -> void: func add_point() -> void:
if player_score < Settings.winning_score: if player_score < Settings.winning_score:
player_score += 1 player_score += 1
player_point_given.emit() player_point_given.emit()
if player_score == 1 && is_first_point: var first_point: bool = player_score == 1 && is_first_point
if player_score == 1:
is_first_point = false is_first_point = false
first_point_triggerd.emit() # With a low winning score the first point can also be match point or the win,
# so the more important event is checked first
if player_score == Settings.winning_score:
winner_triggerd.emit()
elif player_score == Settings.winning_score - 1: elif player_score == Settings.winning_score - 1:
match_point_triggerd.emit() match_point_triggerd.emit()
elif player_score == Settings.winning_score: elif first_point:
winner_triggerd.emit() first_point_triggerd.emit()
else: else:
point_given_sound.emit() point_given_sound.emit()
+3 -14
View File
@@ -1,4 +1,4 @@
extends Control extends PopupWindow
@onready @onready
var qr_panel: Panel = $QRPanel var qr_panel: Panel = $QRPanel
@@ -21,21 +21,10 @@ func _ready() -> void:
url_label.text = qr_code_rect.data url_label.text = qr_code_rect.data
print("QR code generated for: ", url) print("QR code generated for: ", url)
func show_window() -> void: func open() -> void:
visible = true super()
# Regenerate QR code in case settings changed # Regenerate QR code in case settings changed
var url: String = Settings.default_path + "/search" var url: String = Settings.default_path + "/search"
qr_code_rect.data = url qr_code_rect.data = url
url_label.text = qr_code_rect.data url_label.text = qr_code_rect.data
func _input(event: InputEvent) -> void:
if self.visible == true:
if event is InputEventMouseButton && event.is_pressed():
var evLocal: InputEvent = make_input_local(event)
if !Rect2(Vector2(0, 0), qr_panel.size).has_point(evLocal.position):
self.visible = false
func hide_window() -> void:
visible = false
+4 -1
View File
@@ -2,8 +2,10 @@
[ext_resource type="Script" uid="uid://bsa6xg5xos6h7" path="res://QRWindow.gd" id="1_qr_script"] [ext_resource type="Script" uid="uid://bsa6xg5xos6h7" path="res://QRWindow.gd" id="1_qr_script"]
[ext_resource type="Script" uid="uid://dynulhnyq5hpq" path="res://addons/kenyoni/qr_code/qr_code_rect.gd" id="2_jpsxt"] [ext_resource type="Script" uid="uid://dynulhnyq5hpq" path="res://addons/kenyoni/qr_code/qr_code_rect.gd" id="2_jpsxt"]
[ext_resource type="Theme" path="res://popup_theme.tres" id="popup_theme"]
[node name="QRControl" type="Control" unique_id=1996957585] [node name="QRControl" type="Control" unique_id=1996957585 node_paths=PackedStringArray("panel")]
theme = ExtResource("popup_theme")
layout_mode = 3 layout_mode = 3
anchors_preset = 15 anchors_preset = 15
anchor_right = 1.0 anchor_right = 1.0
@@ -11,6 +13,7 @@ anchor_bottom = 1.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
script = ExtResource("1_qr_script") script = ExtResource("1_qr_script")
panel = NodePath("QRPanel")
[node name="QRPanel" type="Panel" parent="." unique_id=1021365843] [node name="QRPanel" type="Panel" parent="." unique_id=1021365843]
unique_name_in_owner = true unique_name_in_owner = true
+29
View File
@@ -0,0 +1,29 @@
# Releasing
Releases are made from the version in `project.godot`
(**Project > Project Settings > Application > Config > Version**). The game shows the
same version in the About window.
1. Bump the version, e.g. from `1.9.0` to `1.10.0`, and add a `WHATS_NEW` entry in
`AboutWindow.gd`.
2. Merge to `main` and push.
The `Build` workflow (`.gitea/workflows/gitea-build.yaml`) then:
1. runs the tests,
2. exports the Windows and Linux builds, and packages Linux as
`MusicPlayer_<version>_linux.tar.gz` with the icon, an app menu entry and `install.sh`
(Linux programs can't carry their own icon the way a Windows .exe does),
3. if no tag exists for the version yet: uploads the builds to the package registry
under `MusicPlayer/<version>/`, and creates the `<version>` tag and Gitea release
with the builds attached.
Pushes that don't change the version, and pushes to `develop`, only test and export.
Don't create tags or `1.x.y` branches by hand; the workflow makes the tag.
## Installing on Linux
Unpack `MusicPlayer_<version>_linux.tar.gz` and run `./install.sh` inside the
`MusicPlayer` folder. It installs the game to `~/.local/bin` and adds it, with its icon,
to the app menu. The bare `MusicPlayer_<version>.x86_64` is still published for anyone
who just wants to run the file.
+143 -136
View File
@@ -1,4 +1,4 @@
extends Control extends PopupWindow
@onready @onready
var search_list: VBoxContainer = $ScrollContainer/VBoxContainer var search_list: VBoxContainer = $ScrollContainer/VBoxContainer
@@ -9,24 +9,26 @@ var search_panel: Panel = %SearchPanel
@onready @onready
var clear_button: Button = $ClearButton var clear_button: Button = $ClearButton
@onready
var close_button: Button = $CloseButton
@onready @onready
var search_bar: TextEdit = $Searchbar var search_bar: TextEdit = $Searchbar
var games: Array = [] var soundtracks: Array = []
var regex: RegEx
# Precomputed soundtrack data: array of dicts with original, lowercase, and cleaned fields
var precomputed_soundtracks: Array = []
# Regex cache: maps cleaned search terms to compiled RegEx objects
var regex_cache: Dictionary = {}
var label_pool: Array[Label] = []
# Called when the node enters the scene tree for the first time. # Called when the node enters the scene tree for the first time.
func _ready() -> void: func _ready() -> void:
clear_button.pressed.connect(clear) clear_button.pressed.connect(clear)
close_button.pressed.connect(close)
search_bar.grab_focus() search_bar.grab_focus()
search_bar.text_changed.connect(search) search_bar.text_changed.connect(search)
visibility_changed.connect(focus) visibility_changed.connect(focus)
regex = RegEx.new()
func focus() -> void: func focus() -> void:
if self.visible == true: if self.visible == true:
search_bar.grab_focus() search_bar.grab_focus()
@@ -34,145 +36,150 @@ func focus() -> void:
func close() -> void: func close() -> void:
clear() clear()
self.visible = false super()
func search() -> void: func search() -> void:
print(search_bar.text) print(search_bar.text)
Settings.delete_children(search_list) var search_text: String = search_bar.text
var search_text: String = search_bar.text.to_lower()
for game: String in games:
if is_match_exact(search_text, game):
add_game(game)
var clean_search_text: String = clean_search_term(search_text)
for game: String in games:
if is_match_contains(clean_search_text, clean_game(game)):
add_game(game)
for game: String in games:
if is_match_regex(clean_search_text, clean_game(game)):
add_game(game)
func clean_search_term(term: String) -> String:
return term.replace(" ", "").replace("é", "e").replace("+", "plus").replace("&", "and").replace("'n", "and")
func clean_game(term: String) -> String:
return term.replace(" ", "").replace("é", "e").replace("+", "plus").replace("&", "and").replace("'n", "and").to_lower()
func is_match_exact(search_term: String, game_name: String) -> bool:
game_name = game_name.to_lower()
if search_term == "": if search_text == "":
return true show_all_soundtracks()
elif game_name.contains(search_term): return
return true
else: perform_search(search_text)
return false
func is_match_contains(search_term: String, game_name: String) -> bool: func show_all_soundtracks() -> void:
if search_term == "": # Hide all visible labels and add to pool
return true
elif game_name.contains(search_term):
return true
else:
return false
func is_match_regex(search_term: String, game_name: String) -> bool:
if search_term == "":
return true
elif compile_regex(search_term).search(game_name):
return true
else:
return false
func add_game(game: String) -> void:
var label: Label = Label.new()
label.text = game
print("game: " + game)
label.autowrap_mode = TextServer.AUTOWRAP_WORD
if !check_if_game_exists(game):
search_list.add_child(label)
func check_if_game_exists(game: String) -> bool:
var game_exists: bool = false
for child: Label in search_list.get_children(): for child: Label in search_list.get_children():
if child.text == game: if child.visible:
game_exists = true child.visible = false
return game_exists label_pool.append(child)
func compile_regex(search_term: String) -> RegEx:
var regText: String = ".*"
for letter: String in search_term:
regText += letter + ".*"
regex.compile(regText)
return regex
func get_list_of_games() -> void:
print("get_list_of_games")
var handle_games: Callable = func handle_games(array: Array) -> void:
if typeof(array) == TYPE_ARRAY:
games = []
Settings.delete_children(search_list)
games.append_array(array)
for game: String in games:
var label: Label = Label.new()
label.text = game
label.autowrap_mode = TextServer.AUTOWRAP_WORD
search_list.add_child(label)
else:
print("Unexpected data")
Settings.make_request2("/music/all/order", handle_games, true)
func _input(event: InputEvent) -> void:
if self.visible == true:
if event is InputEventMouseButton && event.is_pressed():
var evLocal: InputEvent = make_input_local(event)
if !Rect2(Vector2(0, 0), search_panel.size).has_point(evLocal.position):
self.visible = false
func clear() -> void:
search_bar.text = ""
search()
search_bar.grab_focus()
func old_search() -> void: # Show all soundtracks in order
print(search_bar.text) for soundtrack: String in soundtracks:
Settings.delete_children(search_list) add_soundtrack_reuse(soundtrack)
for game: String in games:
if old_is_match_exact(search_bar.text, game):
add_game(game)
for game: String in games:
if is_match_contains(clean_term(search_bar.text), clean_term(game)):
add_game(game)
for game: String in games:
if old_is_match_regex(clean_term(search_bar.text), clean_term(game)):
add_game(game)
func perform_search(search_text: String) -> void:
# Hide all visible labels and add to pool
for child: Label in search_list.get_children():
if child.visible:
child.visible = false
label_pool.append(child)
for soundtrack: String in find_matches(search_text):
add_soundtrack_reuse(soundtrack)
## Returns the soundtracks matching [param search_text], best matches first: plain
## case-insensitive matches, then matches ignoring spaces and special characters,
## then matches with the letters in order but not next to each other.
func find_matches(search_text: String) -> Array[String]:
var seen: Dictionary = {}
var search_text_lower: String = search_text.to_lower()
var clean_search_text: String = clean_term(search_text)
var matches: Array[String] = []
# Pass 1: Exact case-insensitive contains match on lowercase versions
for i in range(precomputed_soundtracks.size()):
var st_data = precomputed_soundtracks[i]
if st_data["lowercase"].contains(search_text_lower):
if not st_data["original"] in seen:
seen[st_data["original"]] = true
matches.append(st_data["original"])
# Pass 2: Contains match on cleaned terms
for i in range(precomputed_soundtracks.size()):
var st_data = precomputed_soundtracks[i]
if st_data["cleaned"].contains(clean_search_text):
if not st_data["original"] in seen:
seen[st_data["original"]] = true
matches.append(st_data["original"])
# Pass 3: Regex match on cleaned terms
var regex = compile_regex_cached(clean_search_text)
for i in range(precomputed_soundtracks.size()):
var st_data = precomputed_soundtracks[i]
if regex.search(st_data["cleaned"]):
if not st_data["original"] in seen:
seen[st_data["original"]] = true
matches.append(st_data["original"])
return matches
# Cleans a term by removing spaces and replacing special characters.
# Matches MusicServer's clean_term() function.
func clean_term(term: String) -> String: func clean_term(term: String) -> String:
return term.replace(" ", "").replace("é", "e").replace("+", "plus").replace("&", "and").replace("'n", "and").to_lower() return term.replace(" ", "").replace("é", "e").replace("+", "plus").replace("&", "and").replace("'n", "and").to_lower()
# Compiles a regex pattern with caching to avoid recompiling the same pattern multiple times.
func compile_regex_cached(search_term: String) -> RegEx:
if not search_term in regex_cache:
var regex = RegEx.new()
var regText: String = ".*"
for letter: String in search_term:
# Escape regex metacharacters so searches like "Q*bert" or "(Remix)" work
if "\\^$.|?*+()[]{}".contains(letter):
regText += "\\"
regText += letter + ".*"
regex.compile(regText)
regex_cache[search_term] = regex
return regex_cache[search_term]
func old_is_match_exact(search_term: String, game_name: String) -> bool: func add_soundtrack(soundtrack: String) -> void:
search_term = search_term.to_lower() # Backward compatibility - delegates to reuse version
game_name = game_name.to_lower() add_soundtrack_reuse(soundtrack)
if search_term == "": # Optimized version that reuses label nodes from pool instead of creating new ones
return true func add_soundtrack_reuse(soundtrack: String) -> void:
elif game_name.contains(search_term): var label: Label
return true if label_pool.size() > 0:
label = label_pool.pop_front()
label.text = soundtrack
label.visible = true
# Move to end to maintain display order
search_list.move_child(label, search_list.get_child_count() - 1)
else: else:
return false label = Label.new()
label.text = soundtrack
label.autowrap_mode = TextServer.AUTOWRAP_WORD
search_list.add_child(label)
label.visible = true
func old_is_match_regex(search_term: String, game_name: String) -> bool: func check_if_soundtrack_exists(soundtrack: String) -> bool:
if search_term == "": var soundtrack_exists: bool = false
return true for child: Label in search_list.get_children():
elif compile_regex(search_term).search(game_name): if child.text == soundtrack and child.visible:
return true soundtrack_exists = true
else: return soundtrack_exists
return false
func old_compile_regex(search_term: String) -> RegEx: func get_list_of_soundtracks() -> void:
var regex = RegEx.new() print("get_list_of_soundtracks")
var regText: String = ".*" var handle_soundtracks: Callable = func handle_soundtracks(array) -> void:
for letter in search_term: if typeof(array) == TYPE_ARRAY:
regText += letter + ".*" # Reuse existing labels: hide all and add to pool
regex.compile(regText) for child: Label in search_list.get_children():
return regex child.visible = false
label_pool.append(child)
set_soundtracks(array)
for soundtrack: String in soundtracks:
add_soundtrack_reuse(soundtrack)
else:
print("Unexpected data")
MusicServerClient.fetch_all_soundtracks_ordered(handle_soundtracks)
## Replaces the searchable soundtracks and precomputes their search forms.
func set_soundtracks(list: Array) -> void:
soundtracks = []
precomputed_soundtracks = []
soundtracks.append_array(list)
for soundtrack: String in soundtracks:
precomputed_soundtracks.append({
"original": soundtrack,
"lowercase": soundtrack.to_lower(),
"cleaned": clean_term(soundtrack)
})
func clear() -> void:
search_bar.text = ""
show_all_soundtracks()
search_bar.grab_focus()
+18 -29
View File
@@ -1,8 +1,10 @@
[gd_scene load_steps=2 format=3 uid="uid://bxydgil1yifps"] [gd_scene format=3 uid="uid://bxydgil1yifps"]
[ext_resource type="Script" uid="uid://bg5yo08638600" path="res://SearchWindow.gd" id="1_m7bij"] [ext_resource type="Script" uid="uid://bg5yo08638600" path="res://SearchWindow.gd" id="1_m7bij"]
[ext_resource type="Theme" path="res://popup_theme.tres" id="popup_theme"]
[node name="Control" type="Control"] [node name="Control" type="Control" unique_id=616238617 node_paths=PackedStringArray("panel")]
theme = ExtResource("popup_theme")
layout_mode = 3 layout_mode = 3
anchors_preset = 15 anchors_preset = 15
anchor_right = 1.0 anchor_right = 1.0
@@ -10,54 +12,41 @@ anchor_bottom = 1.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
script = ExtResource("1_m7bij") script = ExtResource("1_m7bij")
panel = NodePath("SearchPanel")
[node name="SearchPanel" type="Panel" parent="."] [node name="SearchPanel" type="Panel" parent="." unique_id=1262945994]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 0 layout_mode = 0
offset_right = 1152.0 offset_right = 1152.0
offset_bottom = 640.0 offset_bottom = 640.0
[node name="ClearButton" type="Button" parent="."] [node name="ClearButton" type="Button" parent="." unique_id=1549439398]
texture_filter = 1
layout_mode = 0 layout_mode = 0
offset_left = 880.0 offset_left = 936.0
offset_top = 24.0 offset_top = 16.0
offset_right = 944.0 offset_right = 1027.0
offset_bottom = 64.0 offset_bottom = 56.0
scale = Vector2(2, 2) scale = Vector2(2, 2)
focus_mode = 0 focus_mode = 0
action_mode = 0 action_mode = 0
text = "Clear" text = "Clear"
[node name="CloseButton" type="Button" parent="."] [node name="Searchbar" type="TextEdit" parent="." unique_id=2112669372]
texture_filter = 1
layout_mode = 0 layout_mode = 0
offset_left = 1032.0 offset_left = 32.0
offset_top = 24.0 offset_top = 16.0
offset_right = 1082.0 offset_right = 468.0
offset_bottom = 64.0 offset_bottom = 56.0
scale = Vector2(2, 2)
focus_mode = 0
action_mode = 0
text = "Close"
[node name="Searchbar" type="TextEdit" parent="."]
texture_filter = 1
layout_mode = 0
offset_left = 64.0
offset_top = 24.0
offset_right = 448.0
offset_bottom = 64.0
scale = Vector2(2, 2) scale = Vector2(2, 2)
placeholder_text = "Search" placeholder_text = "Search"
[node name="ScrollContainer" type="ScrollContainer" parent="."] [node name="ScrollContainer" type="ScrollContainer" parent="." unique_id=213733486]
layout_mode = 0 layout_mode = 0
offset_top = 128.0 offset_top = 128.0
offset_right = 576.0 offset_right = 576.0
offset_bottom = 384.0 offset_bottom = 384.0
scale = Vector2(2, 2) scale = Vector2(2, 2)
[node name="VBoxContainer" type="VBoxContainer" parent="ScrollContainer"] [node name="VBoxContainer" type="VBoxContainer" parent="ScrollContainer" unique_id=1567403144]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
+3 -73
View File
@@ -28,81 +28,11 @@ var edit_players: bool = false
var currently_syncing: bool = false var currently_syncing: bool = false
var character_select_open: bool = false var character_select_open: bool = false
var version: String = "1.8.5" ## The game version, set in Project Settings > Application > Config > Version.
## Releases are made from it, see RELEASING.md.
var version: String = ProjectSettings.get_setting("application/config/version", "")
func make_request2(address: String, func_name: Callable, expect_data: bool) -> void:
var error_handling: Callable = func(_result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
print("request done to address: ", default_path + address)
if response_code == 200:
print("func_name: ", func_name.get_method())
if !expect_data:
func_name.call()
else:
var json: JSON = JSON.new()
var error: int = json.parse(body.get_string_from_utf8())
if error == OK:
var data_received = json.get_data()
print("data_received type: ", type_string(typeof(data_received)))
if typeof(data_received) == TYPE_ARRAY:
func_name.call(data_received)
elif func_name != null:
func_name.call(data_received)
else:
print("data_received type: ", type_string(typeof(body)))
func_name.call(body)
var http_request: HTTPRequest = HTTPRequest.new()
add_child(http_request)
http_request.request_completed.connect(error_handling)
# Perform a GET request. The URL below returns JSON as of writing.
print("address: ", default_path + address)
var request_error: int = http_request.request(default_path + address)
if request_error != OK:
push_error("An error occurred in the HTTP request.")
func make_request3(address: String) -> void:
var error_handling: Callable = func(_result: int, response_code: int, _headers: PackedStringArray, _body: PackedByteArray) -> void:
print("request done to address: ", default_path + address)
if response_code != 200:
print("Error: " + str(response_code))
var http_request: HTTPRequest = HTTPRequest.new()
add_child(http_request)
http_request.request_completed.connect(error_handling)
# Perform a GET request. The URL below returns JSON as of writing.
var request_error: int = http_request.request(default_path + address)
if request_error != OK:
push_error("An error occurred in the HTTP request.")
func make_put_request(address: String) -> void:
var error_handling: Callable = func(_result: int, response_code: int, _headers: PackedStringArray, _body: PackedByteArray) -> void:
print("request done to address: ", default_path + address)
if response_code != 200:
print("Error: " + str(response_code))
var http_request: HTTPRequest = HTTPRequest.new()
add_child(http_request)
http_request.request_completed.connect(error_handling)
# Perform a GET request. The URL below returns JSON as of writing.
var headers: PackedStringArray = ["Content-Type: application/json"]
var request_error: int = http_request.request(default_path + address, headers, HTTPClient.METHOD_PUT)
if request_error != OK:
push_error("An error occurred in the HTTP request.")
func delete_children(node: Node) -> void: func delete_children(node: Node) -> void:
for n: Node in node.get_children(): for n: Node in node.get_children():
node.remove_child(n) node.remove_child(n)
n.queue_free() n.queue_free()
func delete_player_children(node: Node) -> void:
for n: Node in node.get_children():
print(n)
print(n.name)
if n.name == "HBoxContainer":
pass
elif n.name == "AddPlayerContainer":
pass
else:
node.remove_child(n)
n.queue_free()
+7 -8
View File
@@ -1,4 +1,4 @@
extends Control extends PopupWindow
@onready @onready
var settings_panel: Panel = %SettingsPanel var settings_panel: Panel = %SettingsPanel
@@ -109,13 +109,12 @@ func _ready() -> void:
update_hide_for_seconds_enabled() update_hide_for_seconds_enabled()
select_server_button.select(Settings.selected_server) select_server_button.select(Settings.selected_server)
select_server_button.item_selected.connect(select_server) select_server_button.item_selected.connect(select_server)
visibility_changed.connect(_on_visibility_changed)
func _input(event: InputEvent) -> void: # Settings can change outside this window, e.g. winning turns statistics off
if self.visible == true: func _on_visibility_changed() -> void:
if event is InputEventMouseButton && event.is_pressed(): if visible:
var evLocal: InputEvent = make_input_local(event) add_to_database_button.button_pressed = Settings.add_to_stats
if !Rect2(Vector2(0, 0), settings_panel.size).has_point(evLocal.position):
self.visible = false
func fullscreen() -> void: func fullscreen() -> void:
Settings.fullscreen = !Settings.fullscreen Settings.fullscreen = !Settings.fullscreen
@@ -154,7 +153,7 @@ func hide_ticks() -> void:
Settings.hide_ticks = !Settings.hide_ticks Settings.hide_ticks = !Settings.hide_ticks
func lower_winning_score() -> void: func lower_winning_score() -> void:
Settings.winning_score -= 1 Settings.winning_score = max(Settings.winning_score - 1, 1)
score_label.text = str(Settings.winning_score) score_label.text = str(Settings.winning_score)
func increase_winning_score() -> void: func increase_winning_score() -> void:
+31 -13
View File
@@ -1,11 +1,13 @@
[gd_scene format=3 uid="uid://dldpeo5y3l5hq"] [gd_scene format=3 uid="uid://dldpeo5y3l5hq"]
[ext_resource type="Script" uid="uid://dukqyjoduf1af" path="res://SettingsWindow.gd" id="1_bt55j"] [ext_resource type="Script" uid="uid://dukqyjoduf1af" path="res://SettingsWindow.gd" id="1_bt55j"]
[ext_resource type="Theme" path="res://popup_theme.tres" id="popup_theme"]
[sub_resource type="LabelSettings" id="LabelSettings_3xrlm"] [sub_resource type="LabelSettings" id="LabelSettings_3xrlm"]
font_size = 25 font_size = 25
[node name="SettingsControl" type="Control" unique_id=1298840328] [node name="SettingsControl" type="Control" unique_id=1298840328 node_paths=PackedStringArray("panel")]
theme = ExtResource("popup_theme")
layout_mode = 3 layout_mode = 3
anchors_preset = 15 anchors_preset = 15
anchor_right = 1.0 anchor_right = 1.0
@@ -13,6 +15,7 @@ anchor_bottom = 1.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
script = ExtResource("1_bt55j") script = ExtResource("1_bt55j")
panel = NodePath("SettingsPanel")
[node name="SettingsPanel" type="Panel" parent="." unique_id=45537159] [node name="SettingsPanel" type="Panel" parent="." unique_id=45537159]
unique_name_in_owner = true unique_name_in_owner = true
@@ -22,8 +25,11 @@ offset_bottom = 640.0
[node name="ScrollContainer" type="ScrollContainer" parent="SettingsPanel" unique_id=1325795358] [node name="ScrollContainer" type="ScrollContainer" parent="SettingsPanel" unique_id=1325795358]
layout_mode = 0 layout_mode = 0
offset_right = 500.0 offset_left = 20.0
offset_bottom = 640.0 offset_top = 8.0
offset_right = 480.0
offset_bottom = 632.0
horizontal_scroll_mode = 0
[node name="VBoxContainer" type="VBoxContainer" parent="SettingsPanel/ScrollContainer" unique_id=1796635163] [node name="VBoxContainer" type="VBoxContainer" parent="SettingsPanel/ScrollContainer" unique_id=1796635163]
clip_contents = true clip_contents = true
@@ -40,7 +46,7 @@ horizontal_alignment = 1
[node name="StopAfterCurrentCheckButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=508305575] [node name="StopAfterCurrentCheckButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=508305575]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 0 size_flags_horizontal = 3
button_pressed = true button_pressed = true
action_mode = 0 action_mode = 0
text = "Stop after current" text = "Stop after current"
@@ -48,7 +54,7 @@ text = "Stop after current"
[node name="HideNextTrackCheckButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=1568244599] [node name="HideNextTrackCheckButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=1568244599]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 0 size_flags_horizontal = 3
button_pressed = true button_pressed = true
action_mode = 0 action_mode = 0
text = "Hide next track" text = "Hide next track"
@@ -56,21 +62,21 @@ text = "Hide next track"
[node name="AddToDatabaseCheckButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=1673750542] [node name="AddToDatabaseCheckButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=1673750542]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 0 size_flags_horizontal = 3
action_mode = 0 action_mode = 0
text = "Turn on played to database" text = "Turn on played to database"
[node name="LowPlayedCheckButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=1672657048] [node name="LowPlayedCheckButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=1672657048]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 0 size_flags_horizontal = 3
action_mode = 0 action_mode = 0
text = "Use low played mode" text = "Use low played mode"
[node name="HideBeginningCheckButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=1370617867] [node name="HideBeginningCheckButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=1370617867]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 0 size_flags_horizontal = 3
action_mode = 0 action_mode = 0
text = "Hide beginning of songs" text = "Hide beginning of songs"
@@ -81,12 +87,15 @@ layout_mode = 2
[node name="HideForSecondsTitle" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/HideForSecondsHBoxContainer" unique_id=1822369383] [node name="HideForSecondsTitle" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/HideForSecondsHBoxContainer" unique_id=1822369383]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3
text = "Hide Start Seconds: " text = "Hide Start Seconds: "
[node name="HideForSecondsLabel" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/HideForSecondsHBoxContainer" unique_id=1580108945] [node name="HideForSecondsLabel" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/HideForSecondsHBoxContainer" unique_id=1580108945]
unique_name_in_owner = true unique_name_in_owner = true
custom_minimum_size = Vector2(48, 0)
layout_mode = 2 layout_mode = 2
text = "5.0" text = "5.0"
horizontal_alignment = 1
[node name="LowerHideForSecondsButton" type="Button" parent="SettingsPanel/ScrollContainer/VBoxContainer/HideForSecondsHBoxContainer" unique_id=180643442] [node name="LowerHideForSecondsButton" type="Button" parent="SettingsPanel/ScrollContainer/VBoxContainer/HideForSecondsHBoxContainer" unique_id=180643442]
unique_name_in_owner = true unique_name_in_owner = true
@@ -107,14 +116,14 @@ text = "+1"
[node name="HideLengthCheckButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=67806372] [node name="HideLengthCheckButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=67806372]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 0 size_flags_horizontal = 3
action_mode = 0 action_mode = 0
text = "Hide length of songs" text = "Hide length of songs"
[node name="HideTicksCheckButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=706322752] [node name="HideTicksCheckButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=706322752]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 0 size_flags_horizontal = 3
action_mode = 0 action_mode = 0
text = "Hide ticks in playbar" text = "Hide ticks in playbar"
@@ -123,12 +132,15 @@ layout_mode = 2
[node name="Label2" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/HBoxContainer" unique_id=1892907487] [node name="Label2" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/HBoxContainer" unique_id=1892907487]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3
text = "Winning Score: " text = "Winning Score: "
[node name="ScoreLabel" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/HBoxContainer" unique_id=444542662] [node name="ScoreLabel" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/HBoxContainer" unique_id=444542662]
unique_name_in_owner = true unique_name_in_owner = true
custom_minimum_size = Vector2(48, 0)
layout_mode = 2 layout_mode = 2
text = "20" text = "20"
horizontal_alignment = 1
[node name="LowerButton" type="Button" parent="SettingsPanel/ScrollContainer/VBoxContainer/HBoxContainer" unique_id=614806355] [node name="LowerButton" type="Button" parent="SettingsPanel/ScrollContainer/VBoxContainer/HBoxContainer" unique_id=614806355]
unique_name_in_owner = true unique_name_in_owner = true
@@ -151,12 +163,15 @@ layout_mode = 2
[node name="CacheTitleLabel" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/CacheHBoxContainer" unique_id=365012193] [node name="CacheTitleLabel" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/CacheHBoxContainer" unique_id=365012193]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3
text = "Predownload songs: " text = "Predownload songs: "
[node name="CacheLabel" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/CacheHBoxContainer" unique_id=1558370172] [node name="CacheLabel" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/CacheHBoxContainer" unique_id=1558370172]
unique_name_in_owner = true unique_name_in_owner = true
custom_minimum_size = Vector2(48, 0)
layout_mode = 2 layout_mode = 2
text = "1" text = "1"
horizontal_alignment = 1
[node name="LowerCacheButton" type="Button" parent="SettingsPanel/ScrollContainer/VBoxContainer/CacheHBoxContainer" unique_id=1990385038] [node name="LowerCacheButton" type="Button" parent="SettingsPanel/ScrollContainer/VBoxContainer/CacheHBoxContainer" unique_id=1990385038]
unique_name_in_owner = true unique_name_in_owner = true
@@ -179,12 +194,15 @@ layout_mode = 2
[node name="InspirationListSpeedTitle" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/InspirationHBoxContainer" unique_id=1397861506] [node name="InspirationListSpeedTitle" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/InspirationHBoxContainer" unique_id=1397861506]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3
text = "Inspiration List Speed: " text = "Inspiration List Speed: "
[node name="InspirationListSpeedLabel" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/InspirationHBoxContainer" unique_id=867387668] [node name="InspirationListSpeedLabel" type="Label" parent="SettingsPanel/ScrollContainer/VBoxContainer/InspirationHBoxContainer" unique_id=867387668]
unique_name_in_owner = true unique_name_in_owner = true
custom_minimum_size = Vector2(48, 0)
layout_mode = 2 layout_mode = 2
text = "1" text = "1"
horizontal_alignment = 1
[node name="LowerInspirationSpeedButton" type="Button" parent="SettingsPanel/ScrollContainer/VBoxContainer/InspirationHBoxContainer" unique_id=845209534] [node name="LowerInspirationSpeedButton" type="Button" parent="SettingsPanel/ScrollContainer/VBoxContainer/InspirationHBoxContainer" unique_id=845209534]
unique_name_in_owner = true unique_name_in_owner = true
@@ -221,13 +239,13 @@ popup/item_4/id = 5
[node name="FullscreenButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=400304684] [node name="FullscreenButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=400304684]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 0 size_flags_horizontal = 3
text = "Fullscreen" text = "Fullscreen"
[node name="QuickSyncButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=49497448] [node name="QuickSyncButton" type="CheckButton" parent="SettingsPanel/ScrollContainer/VBoxContainer" unique_id=49497448]
visible = false visible = false
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 0 size_flags_horizontal = 3
button_pressed = true button_pressed = true
action_mode = 0 action_mode = 0
text = "Quick sync" text = "Quick sync"
@@ -236,6 +254,6 @@ text = "Quick sync"
unique_name_in_owner = true unique_name_in_owner = true
visible = false visible = false
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 0 size_flags_horizontal = 3
disabled = true disabled = true
text = "Local" text = "Local"
+15 -16
View File
@@ -1,4 +1,4 @@
extends Node extends RefCounted
class_name SongObject class_name SongObject
@@ -6,9 +6,10 @@ var song_number: int
var song_title: String var song_title: String
var game_title: String var soundtrack_title: String
var players_given_point: Array[int] = [] ## Ids of the players who got a point for this song.
var players_given_point: Array[String] = []
var is_answered: bool = false var is_answered: bool = false
@@ -18,9 +19,9 @@ var is_playing: bool = false
var song: AudioStream var song: AudioStream
func get_game_title() -> String: func get_soundtrack_title() -> String:
if is_answered: if is_answered:
return game_title return soundtrack_title
else: else:
return "????????" return "????????"
@@ -33,24 +34,22 @@ func get_song_title() -> String:
func get_song_info() -> String: func get_song_info() -> String:
if Settings.is_debug: if Settings.is_debug:
var format_string: String = "%d. %s - %s | played: %s | answered: %s" var format_string: String = "%d. %s - %s | played: %s | answered: %s"
return format_string % [(song_number + 1), game_title, song_title, has_played, is_answered] return format_string % [(song_number + 1), soundtrack_title, song_title, has_played, is_answered]
elif is_answered: elif is_answered:
var format_string: String = "%d. %s - %s" var format_string: String = "%d. %s - %s"
return format_string % [(song_number + 1), game_title, song_title] return format_string % [(song_number + 1), soundtrack_title, song_title]
else: else:
return "??? - ???" return "??? - ???"
func add_point(id: int) -> void: func add_point(player_id: String) -> void:
print("add_point") print("add_point")
players_given_point.append(id) if !players_given_point.has(player_id):
players_given_point.sort() players_given_point.append(player_id)
func remove_point(id: int) -> void: func remove_point(player_id: String) -> void:
print("remove_point") print("remove_point")
if players_given_point.has(id): players_given_point.erase(player_id)
players_given_point.remove_at(players_given_point.find(id))
players_given_point.sort()
func _to_string() -> String: func _to_string() -> String:
var format_string: String = "song_number: %d | game_title: %s | song_title: %s | is_answered: %s | has_played: %s | is_playing: %s" var format_string: String = "song_number: %d | soundtrack_title: %s | song_title: %s | is_answered: %s | has_played: %s | is_playing: %s"
return format_string % [(song_number), game_title, song_title, is_answered, has_played, is_playing] return format_string % [(song_number), soundtrack_title, song_title, is_answered, has_played, is_playing]
+82 -64
View File
@@ -1,11 +1,13 @@
[gd_scene load_steps=3 format=3 uid="uid://dpdem7pdxweb5"] [gd_scene format=3 uid="uid://dpdem7pdxweb5"]
[ext_resource type="Script" uid="uid://c3ejukwth512a" path="res://sync_window.gd" id="1_8ba47"] [ext_resource type="Script" uid="uid://c3ejukwth512a" path="res://sync_window.gd" id="1_8ba47"]
[ext_resource type="Theme" path="res://popup_theme.tres" id="popup_theme"]
[sub_resource type="LabelSettings" id="LabelSettings_anbfi"] [sub_resource type="LabelSettings" id="LabelSettings_anbfi"]
font_size = 25 font_size = 25
[node name="Control" type="Control"] [node name="Control2" type="Control" unique_id=1302914356 node_paths=PackedStringArray("panel")]
theme = ExtResource("popup_theme")
layout_mode = 3 layout_mode = 3
anchors_preset = 15 anchors_preset = 15
anchor_right = 1.0 anchor_right = 1.0
@@ -13,23 +15,28 @@ anchor_bottom = 1.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
script = ExtResource("1_8ba47") script = ExtResource("1_8ba47")
panel = NodePath("SyncPanel")
[node name="SyncPanel" type="Panel" parent="."] [node name="SyncPanel" type="Panel" parent="." unique_id=1714634204]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 0 layout_mode = 0
offset_right = 1152.0 offset_right = 1500.0
offset_bottom = 640.0 offset_bottom = 640.0
[node name="ScrollContainer" type="ScrollContainer" parent="SyncPanel"] [node name="ScrollContainer" type="ScrollContainer" parent="SyncPanel" unique_id=1289116349]
layout_mode = 0 layout_mode = 0
offset_right = 1152.0 offset_left = 20.0
offset_bottom = 640.0 offset_top = 8.0
offset_right = 1480.0
offset_bottom = 632.0
horizontal_scroll_mode = 0
[node name="SyncVBoxContainer" type="VBoxContainer" parent="SyncPanel/ScrollContainer"] [node name="SyncVBoxContainer" type="VBoxContainer" parent="SyncPanel/ScrollContainer" unique_id=1594735567]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
theme_override_constants/separation = 12
[node name="SyncTitleLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer"] [node name="SyncTitleLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer" unique_id=2125198054]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
size_flags_vertical = 0 size_flags_vertical = 0
@@ -37,7 +44,13 @@ text = "Sync"
label_settings = SubResource("LabelSettings_anbfi") label_settings = SubResource("LabelSettings_anbfi")
horizontal_alignment = 1 horizontal_alignment = 1
[node name="NoChangesLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer"] [node name="SyncTime" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer" unique_id=1344333200]
unique_name_in_owner = true
layout_mode = 2
text = "Sync time: "
horizontal_alignment = 1
[node name="NoChangesLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer" unique_id=1059308183]
unique_name_in_owner = true unique_name_in_owner = true
visible = false visible = false
layout_mode = 2 layout_mode = 2
@@ -46,105 +59,110 @@ text = "No changes since last sync"
label_settings = SubResource("LabelSettings_anbfi") label_settings = SubResource("LabelSettings_anbfi")
horizontal_alignment = 1 horizontal_alignment = 1
[node name="ProgressHBoxContainer" type="HBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer"] [node name="ProgressHBoxContainer" type="HBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer" unique_id=196414805]
unique_name_in_owner = true unique_name_in_owner = true
visible = false visible = false
layout_mode = 2 layout_mode = 2
[node name="SyncProgressBar" type="ProgressBar" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/ProgressHBoxContainer"] [node name="SyncProgressBar" type="ProgressBar" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/ProgressHBoxContainer" unique_id=719135231]
unique_name_in_owner = true unique_name_in_owner = true
custom_minimum_size = Vector2(1000, 0) custom_minimum_size = Vector2(1300, 0)
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 6 size_flags_horizontal = 6
step = 1.0 step = 1.0
rounded = true rounded = true
[node name="SyncHBoxContainer" type="HBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer"] [node name="SyncHBoxContainer" type="HBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer" unique_id=1156646398]
unique_name_in_owner = true unique_name_in_owner = true
visible = false visible = false
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 4 size_flags_horizontal = 3
theme_override_constants/separation = 30 theme_override_constants/separation = 16
[node name="VSeparator6" type="VSeparator" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer"] [node name="SoundtracksAddedVBox" type="VBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer" unique_id=897546696]
layout_mode = 2
[node name="GamesAddedVBox" type="VBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer"]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3
[node name="GamesAddedLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer/GamesAddedVBox"] [node name="SoundtracksAddedLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer/SoundtracksAddedVBox" unique_id=535797768]
layout_mode = 2 layout_mode = 2
text = "GamesAdded" text = "SoundtracksAdded"
horizontal_alignment = 1 horizontal_alignment = 1
[node name="GamesAddedSeparator" type="VSeparator" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer"] [node name="SoundtracksAddedSeparator" type="VSeparator" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer" unique_id=853642947]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
[node name="GamesReAddedVBox" type="VBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer"] [node name="SoundtracksReAddedVBox" type="VBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer" unique_id=995736806]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
[node name="SoundtracksReAddedLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer/SoundtracksReAddedVBox" unique_id=1142636525]
layout_mode = 2
text = "SoundtracksReAdded"
[node name="SoundtracksReAddedSeparator" type="VSeparator" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer" unique_id=1872445446]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
[node name="GamesReAddedLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer/GamesReAddedVBox"] [node name="SoundtracksChangedTitleVBox" type="VBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer" unique_id=294212033]
unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
text = "GamesReAdded" size_flags_horizontal = 3
[node name="GamesReAddedSeparator" type="VSeparator" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer"] [node name="SoundtracksChangedTitleLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer/SoundtracksChangedTitleVBox" unique_id=696960827]
layout_mode = 2
text = "SoundtracksChangedTitle"
[node name="SoundtracksChangedTitleSeparator" type="VSeparator" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer" unique_id=1730403036]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
[node name="GamesChangedTitleVBox" type="VBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer"] [node name="SoundtracksChangedContentVBox" type="VBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer" unique_id=1470922281]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
[node name="SoundtracksChangedContentLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer/SoundtracksChangedContentVBox" unique_id=1209639099]
layout_mode = 2
text = "SoundtracksChangedContent"
[node name="SoundtracksChangedContentSeparator" type="VSeparator" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer" unique_id=626592535]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
[node name="GamesChangedTitleLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer/GamesChangedTitleVBox"] [node name="SoundtracksRemovedVBox" type="VBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer" unique_id=1174594338]
unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
text = "GamesChangedTitle" size_flags_horizontal = 3
[node name="GamesChangedTitleSeparator" type="VSeparator" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer"] [node name="SoundtracksRemovedLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer/SoundtracksRemovedVBox" unique_id=877713862]
layout_mode = 2
text = "SoundtracksRemoved"
[node name="SoundtracksRemovedSeparator" type="VSeparator" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer" unique_id=839653516]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
[node name="GamesChangedContentVBox" type="VBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer"] [node name="CatchedErrorsVBox" type="VBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer" unique_id=63733711]
unique_name_in_owner = true
layout_mode = 2
[node name="GamesChangedContentLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer/GamesChangedContentVBox"]
layout_mode = 2
text = "GamesChangedContent"
[node name="GamesChangedContentSeparator" type="VSeparator" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
[node name="GamesRemovedVBox" type="VBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
[node name="GamesRemovedLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer/GamesRemovedVBox"]
layout_mode = 2
text = "GamesRemoved"
[node name="GamesRemovedSeparator" type="VSeparator" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/SyncHBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
[node name="SyncTime" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Sync time: "
horizontal_alignment = 1
[node name="CatchedErrorsVBox" type="VBoxContainer" parent="SyncPanel/ScrollContainer/SyncVBoxContainer"]
unique_name_in_owner = true unique_name_in_owner = true
visible = false visible = false
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3
[node name="CatchedErrorsLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/CatchedErrorsVBox"] [node name="CatchedErrorsLabel" type="Label" parent="SyncPanel/ScrollContainer/SyncVBoxContainer/CatchedErrorsVBox" unique_id=1784389644]
layout_mode = 2 layout_mode = 2
text = "CatchedErrors" text = "CatchedErrors"
horizontal_alignment = 1 horizontal_alignment = 1
[node name="SyncTimer" type="Timer" parent="."] [node name="RestartSyncButton" type="Button" parent="SyncPanel/ScrollContainer/SyncVBoxContainer" unique_id=372442237]
unique_name_in_owner = true
visible = false
custom_minimum_size = Vector2(200, 80)
layout_mode = 2
size_flags_horizontal = 4
text = "Restart Sync"
[node name="SyncTimer" type="Timer" parent="." unique_id=1955169547]
unique_name_in_owner = true unique_name_in_owner = true
+63
View File
@@ -0,0 +1,63 @@
# Testing
Tests use [GdUnit4](https://github.com/godot-gdunit-labs/gdUnit4) 6.2.1, committed in
`addons/gdUnit4/`. The whole suite runs headless in about 10 seconds.
## Running the tests
```bash
./run_tests.sh # everything
./run_tests.sh res://tests/unit # only unit tests
./run_tests.sh res://tests/unit/test_search.gd # one suite
```
The script finds Godot through `$GODOT`, then `godot` on your PATH, then
`/Applications/Godot.app` or `/Applications/Godot_mono.app`. Reports are written to
`reports/` (ignored by git).
In the editor, open the GdUnit4 panel or right-click the `tests` folder and choose
**Run Tests**.
CI runs the suite in the `test` job of `.gitea/workflows/gitea-build.yaml` on every push
and pull request to `main` and `develop`. The `build` job only starts once the tests
pass.
## Layout
| Folder | Contents |
|---|---|
| `tests/unit/` | Logic without scenes: playlist, songs, player scoring, time formatting, search, the server client |
| `tests/integration/` | Real scenes (`MainWindow.tscn`, `SyncWindow.tscn`) run against a fake server, including the point keybinds |
| `tests/fakes/` | `FakeHttpTransport`, `AutoloadSnapshot` and the `MainWindowTestSuite` base class |
| `tests/data/` | Files the fake server sends, e.g. a short MP3 |
## Faking the MusicServer
All server calls go through the `MusicServerClient` autoload, which sends requests with
a swappable `transport`. Tests install a `FakeHttpTransport` that answers from canned
responses and records what was requested:
```gdscript
var server: FakeHttpTransport = FakeHttpTransport.new().install()
server.respond_json("/music/list", [{"SongNo": 0, "Soundtrack": "Chrono Trigger", "Song": "Frog's Theme"}])
server.respond_bytes("/music/rand", mp3_bytes)
# ... run the code under test ...
assert_int(server.count("/music/rand")).is_equal(1)
server.uninstall()
```
Paths without a response get a 404, so the code under test sees a failed request.
## Writing tests
- Settings and Playlist are global. Tests that change them restore them afterwards, either
by hand or with `AutoloadSnapshot.take([Settings, Playlist])` and `restore()`.
- Main window tests extend `MainWindowTestSuite`, which starts the window against the fake
server and has helpers such as `_add_player()`, `_remove_player()` and
`_give_point_with_keybind()`.
- Player ids are UUID strings that never change. A player's position in
`Settings.player_array` only decides their keybind (Alt+1 for the first row and so on),
so tests check both: that points land on the right player id, and that keybinds follow
the rows when players are added or removed.
+4 -2
View File
@@ -2,11 +2,13 @@
[ext_resource type="Script" uid="uid://dxpb22xbwf3t1" path="res://winner_window.gd" id="1_winner_script"] [ext_resource type="Script" uid="uid://dxpb22xbwf3t1" path="res://winner_window.gd" id="1_winner_script"]
[ext_resource type="Texture2D" uid="uid://r4as0nmtoa7p" path="res://noCharacter.png" id="2_no_character"] [ext_resource type="Texture2D" uid="uid://r4as0nmtoa7p" path="res://noCharacter.png" id="2_no_character"]
[ext_resource type="Theme" path="res://popup_theme.tres" id="popup_theme"]
[sub_resource type="LabelSettings" id="LabelSettings_hr75l"] [sub_resource type="LabelSettings" id="LabelSettings_hr75l"]
font_size = 35 font_size = 35
[node name="WinnerControl" type="Control" unique_id=1111111111] [node name="WinnerControl" type="Control" unique_id=1111111111 node_paths=PackedStringArray("panel")]
theme = ExtResource("popup_theme")
layout_mode = 3 layout_mode = 3
anchors_preset = 15 anchors_preset = 15
anchor_right = 1.0 anchor_right = 1.0
@@ -14,6 +16,7 @@ anchor_bottom = 1.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
script = ExtResource("1_winner_script") script = ExtResource("1_winner_script")
panel = NodePath("WinnerPanel")
[node name="WinnerPanel" type="Panel" parent="." unique_id=89212869] [node name="WinnerPanel" type="Panel" parent="." unique_id=89212869]
unique_name_in_owner = true unique_name_in_owner = true
@@ -40,7 +43,6 @@ stretch_mode = 5
[node name="WinnerLabel" type="Label" parent="WinnerPanel/VBoxContainer" unique_id=1437597286] [node name="WinnerLabel" type="Label" parent="WinnerPanel/VBoxContainer" unique_id=1437597286]
unique_name_in_owner = true unique_name_in_owner = true
texture_filter = 1
layout_mode = 2 layout_mode = 2
text = "Winner!" text = "Winner!"
label_settings = SubResource("LabelSettings_hr75l") label_settings = SubResource("LabelSettings_hr75l")
+4
View File
@@ -0,0 +1,4 @@
# GdUnit4 specific ignores
*.gdunit
*.gdunit.*
/test_results.*
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Mike Schulze
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env -S godot -s
extends SceneTree
var _cli_runner: GdUnitTestCIRunner
func _initialize() -> void:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_MINIMIZED)
_cli_runner = GdUnitTestCIRunner.new()
root.add_child(_cli_runner)
# do not use print statements on _finalize it results in random crashes
func _finalize() -> void:
queue_delete(_cli_runner)
if OS.is_stdout_verbose():
prints("Finallize ..")
prints("-Orphan nodes report-----------------------")
Window.print_orphan_nodes()
prints("Finallize .. done")
+1
View File
@@ -0,0 +1 @@
uid://bipm0dv6p07il
+167
View File
@@ -0,0 +1,167 @@
#!/usr/bin/env -S godot -s
extends MainLoop
const GdUnitTools := preload("res://addons/gdUnit4/src/core/GdUnitTools.gd")
# gdlint: disable=max-line-length
const LOG_FRAME_TEMPLATE = """
<!DOCTYPE html>
<html style="display: inline-grid;">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Godot Logging</title>
<link rel="stylesheet" href="css/styles.css">
</head>
<body style="background-color: #eee;">
<div class="godot-report-frame"">
${content}
</div>
</body>
</html>
"""
const NO_LOG_MESSAGE = """
<h3>No logging available!</h3>
</br>
<p>In order for logging to take place, you must activate the Activate file logging option in the project settings.</p>
<p>You can enable the logging under:
<b>Project Settings</b> > <b>Debug</b> > <b>File Logging</b> > <b>Enable File Logging</b> in the project settings.</p>
"""
#warning-ignore-all:return_value_discarded
var _cmd_options := CmdOptions.new([
CmdOption.new(
"-rd, --report-directory",
"-rd <directory>",
"Specifies the output directory in which the reports are to be written. The default is res://reports/.",
TYPE_STRING,
true
)
])
var _report_root_path: String
var _current_report_path: String
var _debug_cmd_args := PackedStringArray()
func _init() -> void:
set_report_directory(GdUnitFileAccess.current_dir() + "reports")
set_current_report_path()
func _process(_delta: float) -> bool:
# check if reports exists
if not reports_available():
prints("no reports found")
return true
# only process if godot logging is enabled
if not GdUnitSettings.is_log_enabled():
write_report(NO_LOG_MESSAGE, "")
return true
# parse possible custom report path,
var cmd_parser := CmdArgumentParser.new(_cmd_options, "GdUnitCmdTool.gd")
# ignore erros and exit quitly
if cmd_parser.parse(get_cmdline_args(), true).is_error():
return true
CmdCommandHandler.new(_cmd_options).register_cb("-rd", set_report_directory)
var godot_log_file := scan_latest_godot_log()
var result := read_log_file_content(godot_log_file)
if result.is_error():
write_report(result.error_message(), godot_log_file)
return true
write_report(result.value_as_string(), godot_log_file)
return true
func set_current_report_path() -> void:
# scan for latest report directory
var iteration := GdUnitFileAccess.find_last_path_index(
_report_root_path, GdUnitConstants.REPORT_DIR_PREFIX
)
_current_report_path = "%s/%s%d" % [_report_root_path, GdUnitConstants.REPORT_DIR_PREFIX, iteration]
func set_report_directory(path: String) -> void:
_report_root_path = path
func get_log_report_html() -> String:
return _current_report_path + "/godot_report_log.html"
func reports_available() -> bool:
return DirAccess.dir_exists_absolute(_report_root_path)
func scan_latest_godot_log() -> String:
var path := GdUnitSettings.get_log_path().get_base_dir()
var files_sorted := Array()
for file in GdUnitFileAccess.scan_dir(path):
var file_name := "%s/%s" % [path, file]
files_sorted.append(file_name)
# sort by name, the name contains the timestamp so we sort at the end by timestamp
files_sorted.sort()
return files_sorted.back()
func read_log_file_content(log_file: String) -> GdUnitResult:
var file := FileAccess.open(log_file, FileAccess.READ)
if file == null:
return GdUnitResult.error(
"Can't find log file '%s'. Error: %s"
% [log_file, error_string(FileAccess.get_open_error())]
)
var content := "<pre>" + file.get_as_text()
# patch out console format codes
for color_index in range(0, 256):
var to_replace := "[38;5;%dm" % color_index
content = content.replace(to_replace, "")
content += "</pre>"
content = content\
.replace("", "")\
.replace(GdUnitCSIMessageWriter.CSI_BOLD, "")\
.replace(GdUnitCSIMessageWriter.CSI_ITALIC, "")\
.replace(GdUnitCSIMessageWriter.CSI_UNDERLINE, "")
return GdUnitResult.success(content)
func write_report(content: String, godot_log_file: String) -> GdUnitResult:
var file := FileAccess.open(get_log_report_html(), FileAccess.WRITE)
if file == null:
return GdUnitResult.error(
"Can't open to write '%s'. Error: %s"
% [get_log_report_html(), error_string(FileAccess.get_open_error())]
)
var report_html := LOG_FRAME_TEMPLATE.replace("${content}", content)
file.store_string(report_html)
_update_index_html(godot_log_file)
return GdUnitResult.success(file)
func _update_index_html(godot_log_file: String) -> void:
var index_path := "%s/index.html" % _current_report_path
var index_file := FileAccess.open(index_path, FileAccess.READ_WRITE)
if index_file == null:
push_error(
"Can't add log path '%s' to `%s`. Error: %s"
% [godot_log_file, index_path, error_string(FileAccess.get_open_error())]
)
return
var content := index_file.get_as_text()\
.replace("${log_report}", get_log_report_html())\
.replace("${godot_log_file}", godot_log_file)
# overide it
index_file.seek(0)
index_file.store_string(content)
func get_cmdline_args() -> PackedStringArray:
if _debug_cmd_args.is_empty():
return OS.get_cmdline_args()
return _debug_cmd_args
+1
View File
@@ -0,0 +1 @@
uid://cyft2xqn77ffl
+7
View File
@@ -0,0 +1,7 @@
[plugin]
name="gdUnit4"
description="Unit Testing Framework for Godot Scripts"
author="Mike Schulze"
version="6.2.1"
script="plugin.gd"
+90
View File
@@ -0,0 +1,90 @@
@tool
extends EditorPlugin
var _gd_inspector: Control
var _gd_console: Control
var _filesystem_context_menu: EditorContextMenuPlugin
var _editor_context_menu: EditorContextMenuPlugin
var _editor_code_context_menu: EditorContextMenuPlugin
func _enter_tree() -> void:
var inferred_declaration := GdUnitSettings.validate_is_inferred_declaration_enabled()
if inferred_declaration.is_error():
printerr(inferred_declaration.error_message())
printerr("Loading GdUnit4 Plugin failed.")
return
if check_running_in_test_env():
@warning_ignore("return_value_discarded")
GdUnitCSIMessageWriter.new().prints_warning("It was recognized that GdUnit4 is running in a test environment, therefore the GdUnit4 plugin will not be executed!")
return
if Engine.get_version_info().hex < 0x40500:
prints("This GdUnit4 plugin version '%s' requires Godot version '4.5' or higher to run." % GdUnit4Version.current())
return
GdUnitSettings.setup()
GdUnitEditorColorTheme.setup()
# Install the GdUnit Inspector
_gd_inspector = (load("res://addons/gdUnit4/src/ui/GdUnitInspector.tscn") as PackedScene).instantiate()
_add_context_menus()
add_control_to_dock(EditorPlugin.DOCK_SLOT_LEFT_UR, _gd_inspector)
# Install the GdUnit Console
_gd_console = (load("res://addons/gdUnit4/src/ui/GdUnitConsole.tscn") as PackedScene).instantiate()
var control: Control = add_control_to_bottom_panel(_gd_console, "gdUnitConsole")
@warning_ignore("unsafe_method_access")
await _gd_console.setup_update_notification(control)
if GdUnit4CSharpApiLoader.is_api_loaded():
prints("GdUnit4Net version '%s' loaded." % GdUnit4CSharpApiLoader.version())
else:
prints("No GdUnit4Net found.")
# Connect to be notified for script changes to be able to discover new tests
GdUnitTestDiscoverGuard.instance()
@warning_ignore("return_value_discarded")
resource_saved.connect(_on_resource_saved)
prints("Loading GdUnit4 Plugin success")
func _exit_tree() -> void:
if check_running_in_test_env():
return
if is_instance_valid(_gd_inspector):
remove_control_from_docks(_gd_inspector)
_gd_inspector.free()
_remove_context_menus()
if is_instance_valid(_gd_console):
remove_control_from_bottom_panel(_gd_console)
_gd_console.free()
var gdUnitTools: GDScript = load("res://addons/gdUnit4/src/core/GdUnitTools.gd")
@warning_ignore("unsafe_method_access")
gdUnitTools.dispose_all(true)
prints("Unload GdUnit4 Plugin success")
func check_running_in_test_env() -> bool:
var args: PackedStringArray = OS.get_cmdline_args()
args.append_array(OS.get_cmdline_user_args())
return DisplayServer.get_name() == "headless" or args.has("--selftest") or args.has("--add") or args.has("-a") or args.has("--quit-after") or args.has("--import")
func _add_context_menus() -> void:
_filesystem_context_menu = preload("res://addons/gdUnit4/src/ui/menu/GdUnitEditorFileSystemContextMenuHandler.gd").new()
_editor_context_menu = preload("res://addons/gdUnit4/src/ui/menu/GdUnitScriptEditorContextMenuHandler.gd").new()
_editor_code_context_menu = preload("res://addons/gdUnit4/src/ui/menu/GdUnitScriptEditorContextMenuHandler.gd").new()
add_context_menu_plugin(EditorContextMenuPlugin.CONTEXT_SLOT_FILESYSTEM, _filesystem_context_menu)
add_context_menu_plugin(EditorContextMenuPlugin.CONTEXT_SLOT_SCRIPT_EDITOR, _editor_context_menu)
add_context_menu_plugin(EditorContextMenuPlugin.CONTEXT_SLOT_SCRIPT_EDITOR_CODE, _editor_code_context_menu)
func _remove_context_menus() -> void:
if is_instance_valid(_filesystem_context_menu):
remove_context_menu_plugin(_filesystem_context_menu)
if is_instance_valid(_editor_context_menu):
remove_context_menu_plugin(_editor_context_menu)
if is_instance_valid(_editor_code_context_menu):
remove_context_menu_plugin(_editor_code_context_menu)
func _on_resource_saved(resource: Resource) -> void:
if resource is Script:
await GdUnitTestDiscoverGuard.instance().discover(resource as Script)
+1
View File
@@ -0,0 +1 @@
uid://qmkf30a5bmap
+65
View File
@@ -0,0 +1,65 @@
@echo off
setlocal enabledelayedexpansion
:: Initialize variables
set "godot_binary="
set "filtered_args="
:: Process all arguments
set "i=0"
:parse_args
if "%~1"=="" goto end_parse_args
if "%~1"=="--godot_binary" (
set "godot_binary=%~2"
shift
shift
) else (
set "filtered_args=!filtered_args! %~1"
shift
)
goto parse_args
:end_parse_args
:: If --godot_binary wasn't provided, fallback to environment variable
if "!godot_binary!"=="" (
set "godot_binary=%GODOT_BIN%"
)
:: Check if we have a godot_binary value from any source
if "!godot_binary!"=="" (
echo Godot binary path is not specified.
echo Please either:
echo - Set the environment variable: set GODOT_BIN=C:\path\to\godot.exe
echo - Or use the --godot_binary argument: --godot_binary C:\path\to\godot.exe
exit /b 1
)
:: Check if the Godot binary exists
if not exist "!godot_binary!" (
echo Error: The specified Godot binary '!godot_binary!' does not exist.
exit /b 1
)
:: Get Godot version and check if it's a mono build
for /f "tokens=*" %%i in ('"!godot_binary!" --version') do set GODOT_VERSION=%%i
echo !GODOT_VERSION! | findstr /I "mono" >nul
if !errorlevel! equ 0 (
echo Godot .NET detected
echo Compiling c# classes ... Please Wait
dotnet build --debug
echo done !errorlevel!
)
:: Run the tests with the filtered arguments.
:: --remote-debug tcp://127.0.0.1:0 prevents Godot from activating its local interactive
:: CLI debugger, which would cause an endless 'debug>' loop on script parse errors.
:: Port 0 is used intentionally as it is never bound, so the connection is always refused.
"!godot_binary!" --path . -s -d --remote-debug tcp://127.0.0.1:0 res://addons/gdUnit4/bin/GdUnitCmdTool.gd !filtered_args!
set exit_code=%ERRORLEVEL%
echo Run tests ends with %exit_code%
:: Run the copy log command
"!godot_binary!" --headless --path . --quiet -s res://addons/gdUnit4/bin/GdUnitCopyLog.gd !filtered_args! > nul
set exit_code2=%ERRORLEVEL%
exit /b %exit_code%
+65
View File
@@ -0,0 +1,65 @@
#!/bin/bash
# Check for command-line argument
godot_binary=""
filtered_args=""
# Process all arguments with a more compatible approach
while [ $# -gt 0 ]; do
if [ "$1" = "--godot_binary" ] && [ $# -gt 1 ]; then
# Get the next argument as the value
godot_binary="$2"
shift 2
else
# Keep non-godot_binary arguments for passing to Godot
filtered_args="$filtered_args $1"
shift
fi
done
# If --godot_binary wasn't provided, fallback to environment variable
if [ -z "$godot_binary" ]; then
godot_binary="$GODOT_BIN"
fi
# Check if we have a godot_binary value from any source
if [ -z "$godot_binary" ]; then
echo "Godot binary path is not specified."
echo "Please either:"
echo " - Set the environment variable: export GODOT_BIN=/path/to/godot"
echo " - Or use the --godot_binary argument: --godot_binary /path/to/godot"
exit 1
fi
# Check if the Godot binary exists and is executable
if [ ! -f "$godot_binary" ]; then
echo "Error: The specified Godot binary '$godot_binary' does not exist."
exit 1
fi
if [ ! -x "$godot_binary" ]; then
echo "Error: The specified Godot binary '$godot_binary' is not executable."
exit 1
fi
# Get Godot version and check if it's a .NET build
GODOT_VERSION=$("$godot_binary" --version)
if echo "$GODOT_VERSION" | grep -i "mono" > /dev/null; then
echo "Godot .NET detected"
echo "Compiling c# classes ... Please Wait"
dotnet build --debug
echo "done $?"
fi
# Run the tests with the filtered arguments.
# --remote-debug tcp://127.0.0.1:0 prevents Godot from activating its local interactive
# CLI debugger, which would cause an endless 'debug>' loop on script parse errors.
# Port 0 is used intentionally as it is never bound, so the connection is always refused.
"$godot_binary" --path . -s -d --remote-debug tcp://127.0.0.1:0 res://addons/gdUnit4/bin/GdUnitCmdTool.gd $filtered_args
exit_code=$?
echo "Run tests ends with $exit_code"
# Run the copy log command
"$godot_binary" --headless --path . --quiet -s res://addons/gdUnit4/bin/GdUnitCopyLog.gd $filtered_args > /dev/null
exit_code2=$?
exit $exit_code
+12
View File
@@ -0,0 +1,12 @@
class_name Comparator
extends Resource
enum {
EQUAL,
LESS_THAN,
LESS_EQUAL,
GREATER_THAN,
GREATER_EQUAL,
BETWEEN_EQUAL,
NOT_BETWEEN_EQUAL,
}
+1
View File
@@ -0,0 +1 @@
uid://dapcyf8uxkfwt
+80
View File
@@ -0,0 +1,80 @@
## Factory class providing convenient static methods to create various fuzzer instances.[br]
##
## Fuzzers is a utility class that simplifies the creation of different fuzzer types
## for testing purposes. It provides static factory methods that create pre-configured
## fuzzers with sensible defaults, making it easier to set up fuzz testing in your
## test suites without manually instantiating each fuzzer type.[br]
##
## This class acts as a central access point for all fuzzer types, improving code
## readability and reducing boilerplate in test cases.[br]
##
## @tutorial(Fuzzing Testing): https://en.wikipedia.org/wiki/Fuzzing
class_name Fuzzers
extends Resource
## Generates random strings with length between [param min_length] and
## [param max_length] (inclusive), using characters from [param charset].
## See [StringFuzzer] for detailed documentation and examples.
static func rand_str(min_length: int, max_length: int, charset := StringFuzzer.DEFAULT_CHARSET) -> StringFuzzer:
return StringFuzzer.new(min_length, max_length, charset)
## Creates a [BoolFuzzer] for generating random boolean values.[br]
##
## See [BoolFuzzer] for detailed documentation and examples.
static func boolean() -> BoolFuzzer:
return BoolFuzzer.new()
## Creates an [IntFuzzer] for generating random integers within a range.[br]
##
## Generates random integers between [param from] and [param to] (inclusive)
## using [constant IntFuzzer.NORMAL] mode.
## See [IntFuzzer] for detailed documentation and examples.
static func rangei(from: int, to: int) -> IntFuzzer:
return IntFuzzer.new(from, to)
## Creates a [FloatFuzzer] for generating random floats within a range.[br]
##
## Generates random float values between [param from] and [param to] (inclusive).
## See [FloatFuzzer] for detailed documentation and examples.
static func rangef(from: float, to: float) -> FloatFuzzer:
return FloatFuzzer.new(from, to)
## Creates a [Vector2Fuzzer] for generating random 2D vectors within a range.[br]
##
## Generates random Vector2 values where each component is bounded by
## [param from] and [param to] (inclusive).
## See [Vector2Fuzzer] for detailed documentation and examples.
static func rangev2(from: Vector2, to: Vector2) -> Vector2Fuzzer:
return Vector2Fuzzer.new(from, to)
## Creates a [Vector3Fuzzer] for generating random 3D vectors within a range.[br]
##
## Generates random Vector3 values where each component is bounded by
## [param from] and [param to] (inclusive).
## See [Vector3Fuzzer] for detailed documentation and examples.
static func rangev3(from: Vector3, to: Vector3) -> Vector3Fuzzer:
return Vector3Fuzzer.new(from, to)
## Creates an [IntFuzzer] that generates only even integers.[br]
##
## Generates random even integers between [param from] and [param to] (inclusive)
## using [constant IntFuzzer.EVEN] mode.
## See [IntFuzzer] for detailed documentation about even number generation.
static func eveni(from: int, to: int) -> IntFuzzer:
return IntFuzzer.new(from, to, IntFuzzer.EVEN)
## Creates an [IntFuzzer] that generates only odd integers.[br]
##
## Generates random odd integers between [param from] and [param to] (inclusive)
## using [constant IntFuzzer.ODD] mode.
## See [IntFuzzer] for detailed documentation about odd number generation.
static func oddi(from: int, to: int) -> IntFuzzer:
return IntFuzzer.new(from, to, IntFuzzer.ODD)
+1
View File
@@ -0,0 +1 @@
uid://b1ctmh8qmi5d0
+122
View File
@@ -0,0 +1,122 @@
## An Assertion Tool to verify array values
@abstract class_name GdUnitArrayAssert
extends GdUnitAssert
## Verifies that the current value is null.
@abstract func is_null() -> GdUnitArrayAssert
## Verifies that the current value is not null.
@abstract func is_not_null() -> GdUnitArrayAssert
## Verifies that the current Array is equal to the given one.
@abstract func is_equal(...expected: Array) -> GdUnitArrayAssert
## Verifies that the current Array is equal to the given one, ignoring case considerations.
@abstract func is_equal_ignoring_case(...expected: Array) -> GdUnitArrayAssert
## Verifies that the current Array is not equal to the given one.
@abstract func is_not_equal(...expected: Array) -> GdUnitArrayAssert
## Verifies that the current Array is not equal to the given one, ignoring case considerations.
@abstract func is_not_equal_ignoring_case(...expected: Array) -> GdUnitArrayAssert
## Overrides the default failure message by given custom message.
@abstract func override_failure_message(message: String) -> GdUnitArrayAssert
## Appends a custom message to the failure message.
@abstract func append_failure_message(message: String) -> GdUnitArrayAssert
## Verifies that the current Array is empty, it has a size of 0.
@abstract func is_empty() -> GdUnitArrayAssert
## Verifies that the current Array is not empty, it has a size of minimum 1.
@abstract func is_not_empty() -> GdUnitArrayAssert
## Verifies that the current Array is the same. [br]
## Compares the current by object reference equals
@abstract func is_same(expected: Variant) -> GdUnitArrayAssert
## Verifies that the current Array is NOT the same. [br]
## Compares the current by object reference equals
@abstract func is_not_same(expected: Variant) -> GdUnitArrayAssert
## Verifies that the current Array has a size of given value.
@abstract func has_size(expectd: int) -> GdUnitArrayAssert
## Verifies that the current Array contains the given values, in any order.[br]
## The values are compared by deep parameter comparision, for object reference compare you have to use [method contains_same]
@abstract func contains(...expected: Array) -> GdUnitArrayAssert
## Verifies that the current Array contains exactly only the given values and nothing else, in same order.[br]
## The values are compared by deep parameter comparision, for object reference compare you have to use [method contains_same_exactly]
@abstract func contains_exactly(...expected: Array) -> GdUnitArrayAssert
## Verifies that the current Array contains exactly only the given values and nothing else, in any order.[br]
## The values are compared by deep parameter comparision, for object reference compare you have to use [method contains_same_exactly_in_any_order]
@abstract func contains_exactly_in_any_order(...expected: Array) -> GdUnitArrayAssert
## Verifies that the current Array contains the given values, in any order.[br]
## The values are compared by object reference, for deep parameter comparision use [method contains]
@abstract func contains_same(...expected: Array) -> GdUnitArrayAssert
## Verifies that the current Array contains exactly only the given values and nothing else, in same order.[br]
## The values are compared by object reference, for deep parameter comparision use [method contains_exactly]
@abstract func contains_same_exactly(...expected: Array) -> GdUnitArrayAssert
## Verifies that the current Array contains exactly only the given values and nothing else, in any order.[br]
## The values are compared by object reference, for deep parameter comparision use [method contains_exactly_in_any_order]
@abstract func contains_same_exactly_in_any_order(...expected: Array) -> GdUnitArrayAssert
## Verifies that the current Array do NOT contains the given values, in any order.[br]
## The values are compared by deep parameter comparision, for object reference compare you have to use [method not_contains_same]
## [b]Example:[/b]
## [codeblock]
## # will succeed
## assert_array([1, 2, 3, 4, 5]).not_contains(6)
## # will fail
## assert_array([1, 2, 3, 4, 5]).not_contains(2, 6)
## [/codeblock]
@abstract func not_contains(...expected: Array) -> GdUnitArrayAssert
## Verifies that the current Array do NOT contains the given values, in any order.[br]
## The values are compared by object reference, for deep parameter comparision use [method not_contains]
## [b]Example:[/b]
## [codeblock]
## # will succeed
## assert_array([1, 2, 3, 4, 5]).not_contains(6)
## # will fail
## assert_array([1, 2, 3, 4, 5]).not_contains(2, 6)
## [/codeblock]
@abstract func not_contains_same(...expected: Array) -> GdUnitArrayAssert
## Extracts all values by given function name and optional arguments into a new ArrayAssert.
## If the elements not accessible by `func_name` the value is converted to `"n.a"`, expecting null values
@abstract func extract(func_name: String, ...func_args: Array) -> GdUnitArrayAssert
## Extracts all values by given extractor's into a new ArrayAssert.
## If the elements not extractable than the value is converted to `"n.a"`, expecting null values
## -- The argument type is Array[GdUnitValueExtractor]
@abstract func extractv(...extractors: Array) -> GdUnitArrayAssert
@@ -0,0 +1 @@
uid://cdmo2fwnaxtpe
+47
View File
@@ -0,0 +1,47 @@
## Base interface of all GdUnit asserts
@abstract class_name GdUnitAssert
extends RefCounted
## Verifies that the current value is null.
@abstract func is_null() -> GdUnitAssert
## Verifies that the current value is not null.
@abstract func is_not_null() -> GdUnitAssert
## Verifies that the current value is equal to expected one.
@abstract func is_equal(expected: Variant) -> GdUnitAssert
## Verifies that the current value is not equal to expected one.
@abstract func is_not_equal(expected: Variant) -> GdUnitAssert
## Overrides the default failure message by given custom message.[br]
## This function allows you to replace the automatically generated failure message with a more specific
## or user-friendly message that better describes the test failure context.[br]
## Usage:
## [codeblock]
## # Override with custom context-specific message
## func test_player_inventory():
## assert_that(player.get_item_count("sword"))\
## .override_failure_message("Player should have exactly one sword")\
## .is_equal(1)
## [/codeblock]
@abstract func override_failure_message(message: String) -> GdUnitAssert
## Appends a custom message to the failure message.[br]
## This can be used to add additional information to the generated failure message
## while keeping the original assertion details for better debugging context.[br]
## Usage:
## [codeblock]
## # Add context to existing failure message
## func test_player_health():
## assert_that(player.health)\
## .append_failure_message("Player was damaged by: %s" % last_damage_source)\
## .is_greater(0)
## [/codeblock]
@abstract func append_failure_message(message: String) -> GdUnitAssert
+1
View File
@@ -0,0 +1 @@
uid://doph0d61vwg81
+73
View File
@@ -0,0 +1,73 @@
class_name GdUnitAwaiter
extends RefCounted
# Waits for a specified signal in an interval of 50ms sent from the <source>, and terminates with an error after the specified timeout has elapsed.
# source: the object from which the signal is emitted
# signal_name: signal name
# args: the expected signal arguments as an array
# timeout: the timeout in ms, default is set to 2000ms
func await_signal_on(source :Object, signal_name :String, args :Array = [], timeout_millis :int = 2000) -> Variant:
# fail fast if the given source instance invalid
var assert_that := GdUnitAssertImpl.new(signal_name)
var stack_trace := GdUnitStackTrace.new()
var line_number := stack_trace.get_line_number()
if not is_instance_valid(source):
@warning_ignore("return_value_discarded")
assert_that.report_error(GdAssertMessages.error_await_signal_on_invalid_instance(source, signal_name, args), line_number)
return await (Engine.get_main_loop() as SceneTree).process_frame
# fail fast if the given source instance invalid
if not is_instance_valid(source):
@warning_ignore("return_value_discarded")
assert_that.report_error(GdAssertMessages.error_await_signal_on_invalid_instance(source, signal_name, args), line_number)
return await await_idle_frame()
var awaiter := GdUnitSignalAwaiter.new(timeout_millis)
var value :Variant = await awaiter.on_signal(source, signal_name, args)
if awaiter.is_interrupted():
var failure := "await_signal_on(%s, %s) timed out after %sms" % [signal_name, args, timeout_millis]
@warning_ignore("return_value_discarded")
assert_that.report_error(failure, line_number)
return value
# Waits for a specified signal sent from the <source> between idle frames and aborts with an error after the specified timeout has elapsed
# source: the object from which the signal is emitted
# signal_name: signal name
# args: the expected signal arguments as an array
# timeout: the timeout in ms, default is set to 2000ms
func await_signal_idle_frames(source :Object, signal_name :String, args :Array = [], timeout_millis :int = 2000) -> Variant:
var line_number := GdUnitStackTrace.new().get_line_number()
# fail fast if the given source instance invalid
if not is_instance_valid(source):
@warning_ignore("return_value_discarded")
GdUnitAssertImpl.new(signal_name)\
.report_error(GdAssertMessages.error_await_signal_on_invalid_instance(source, signal_name, args), line_number)
return await await_idle_frame()
var awaiter := GdUnitSignalAwaiter.new(timeout_millis, true)
var value :Variant = await awaiter.on_signal(source, signal_name, args)
if awaiter.is_interrupted():
var failure := "await_signal_idle_frames(%s, %s) timed out after %sms" % [signal_name, args, timeout_millis]
@warning_ignore("return_value_discarded")
GdUnitAssertImpl.new(signal_name).report_error(failure, line_number)
return value
# Waits for for a given amount of milliseconds
# example:
# # waits for 100ms
# await GdUnitAwaiter.await_millis(myNode, 100).completed
# use this waiter and not `await get_tree().create_timer().timeout to prevent errors when a test case is timed out
func await_millis(milliSec :int) -> void:
var timer :Timer = Timer.new()
timer.set_name("gdunit_await_millis_timer_%d" % timer.get_instance_id())
(Engine.get_main_loop() as SceneTree).root.add_child(timer)
timer.add_to_group("GdUnitTimers")
timer.set_one_shot(true)
timer.start(milliSec / 1000.0)
await timer.timeout
timer.queue_free()
# Waits until the next idle frame
func await_idle_frame() -> void:
await (Engine.get_main_loop() as SceneTree).process_frame
+1
View File
@@ -0,0 +1 @@
uid://do0d4vult83qq
+35
View File
@@ -0,0 +1,35 @@
## An Assertion Tool to verify boolean values
@abstract class_name GdUnitBoolAssert
extends GdUnitAssert
## Verifies that the current value is null.
@abstract func is_null() -> GdUnitBoolAssert
## Verifies that the current value is not null.
@abstract func is_not_null() -> GdUnitBoolAssert
## Verifies that the current value is equal to the given one.
@abstract func is_equal(expected: Variant) -> GdUnitBoolAssert
## Verifies that the current value is not equal to the given one.
@abstract func is_not_equal(expected: Variant) -> GdUnitBoolAssert
## Overrides the default failure message by given custom message.
@abstract func override_failure_message(message: String) -> GdUnitBoolAssert
## Appends a custom message to the failure message.
@abstract func append_failure_message(message: String) -> GdUnitBoolAssert
## Verifies that the current value is true.
@abstract func is_true() -> GdUnitBoolAssert
## Verifies that the current value is false.
@abstract func is_false() -> GdUnitBoolAssert
@@ -0,0 +1 @@
uid://yucqgte8f3lr
+10
View File
@@ -0,0 +1,10 @@
class_name GdUnitConstants
extends RefCounted
const NO_ARG :Variant = "<--null-->"
const EXPECT_ASSERT_REPORT_FAILURES := "expect_assert_report_failures"
## The maximum number of report history files to store
const DEFAULT_REPORT_HISTORY_COUNT = 20
const REPORT_DIR_PREFIX = "report_"
@@ -0,0 +1 @@
uid://b8wk88u0ki4n7
@@ -0,0 +1,79 @@
## An Assertion Tool to verify dictionary
@abstract class_name GdUnitDictionaryAssert
extends GdUnitAssert
## Verifies that the current value is null.
@abstract func is_null() -> GdUnitDictionaryAssert
## Verifies that the current value is not null.
@abstract func is_not_null() -> GdUnitDictionaryAssert
## Verifies that the current dictionary is equal to the given one, ignoring order.
@abstract func is_equal(expected: Variant) -> GdUnitDictionaryAssert
## Verifies that the current dictionary is not equal to the given one, ignoring order.
@abstract func is_not_equal(expected: Variant) -> GdUnitDictionaryAssert
## Overrides the default failure message by given custom message.
@abstract func override_failure_message(message: String) -> GdUnitDictionaryAssert
## Appends a custom message to the failure message.
@abstract func append_failure_message(message: String) -> GdUnitDictionaryAssert
## Verifies that the current dictionary is empty, it has a size of 0.
@abstract func is_empty() -> GdUnitDictionaryAssert
## Verifies that the current dictionary is not empty, it has a size of minimum 1.
@abstract func is_not_empty() -> GdUnitDictionaryAssert
## Verifies that the current dictionary is the same. [br]
## Compares the current by object reference equals
@abstract func is_same(expected: Variant) -> GdUnitDictionaryAssert
## Verifies that the current dictionary is NOT the same. [br]
## Compares the current by object reference equals
@abstract func is_not_same(expected: Variant) -> GdUnitDictionaryAssert
## Verifies that the current dictionary has a size of given value.
@abstract func has_size(expected: int) -> GdUnitDictionaryAssert
## Verifies that the current dictionary contains the given key(s).[br]
## The keys are compared by deep parameter comparision, for object reference compare you have to use [method contains_same_keys]
@abstract func contains_keys(...expected: Array) -> GdUnitDictionaryAssert
## Verifies that the current dictionary contains the given key and value.[br]
## The key and value are compared by deep parameter comparision, for object reference compare you have to use [method contains_same_key_value]
@abstract func contains_key_value(key: Variant, value: Variant) -> GdUnitDictionaryAssert
## Verifies that the current dictionary not contains the given key(s).[br]
## The keys are compared by deep parameter comparision, for object reference compare you have to use [method not_contains_same_keys]
@abstract func not_contains_keys(...expected: Array) -> GdUnitDictionaryAssert
## Verifies that the current dictionary contains the given key(s).[br]
## The keys are compared by object reference, for deep parameter comparision use [method contains_keys]
@abstract func contains_same_keys(expected: Array) -> GdUnitDictionaryAssert
## Verifies that the current dictionary contains the given key and value.[br]
## The key and value are compared by object reference, for deep parameter comparision use [method contains_key_value]
@abstract func contains_same_key_value(key: Variant, value: Variant) -> GdUnitDictionaryAssert
## Verifies that the current dictionary not contains the given key(s).
## The keys are compared by object reference, for deep parameter comparision use [method not_contains_keys]
@abstract func not_contains_same_keys(...expected: Array) -> GdUnitDictionaryAssert
@@ -0,0 +1 @@
uid://c52l1pf0xf6kd
+56
View File
@@ -0,0 +1,56 @@
## An assertion tool to verify GDUnit asserts.
## This assert is for internal use only, to verify that failed asserts work as expected.
@abstract class_name GdUnitFailureAssert
extends GdUnitAssert
## Verifies that the current value is null.
@abstract func is_null() -> GdUnitFailureAssert
## Verifies that the current value is not null.
@abstract func is_not_null() -> GdUnitFailureAssert
## Verifies that the current value is equal to the given one.
@abstract func is_equal(expected: Variant) -> GdUnitFailureAssert
## Verifies that the current value is not equal to expected one.
@abstract func is_not_equal(expected: Variant) -> GdUnitFailureAssert
## Overrides the default failure message by given custom message.
@abstract func override_failure_message(message: String) -> GdUnitFailureAssert
## Appends a custom message to the failure message.
@abstract func append_failure_message(message: String) -> GdUnitFailureAssert
## Verifies if the executed assert was successful
@abstract func is_success() -> GdUnitFailureAssert
## Verifies if the executed assert has failed
@abstract func is_failed() -> GdUnitFailureAssert
## Verifies the failure line is equal to expected one.
@abstract func has_line(expected: int) -> GdUnitFailureAssert
## Verifies the failure stack trace is equal to expected one.
@abstract func has_stack_trace(stack_trace: Array[GdUnitStackTraceElement]) -> GdUnitFailureAssert
## Verifies the failure message is equal to expected one.
@abstract func has_message(expected: String) -> GdUnitFailureAssert
## Verifies that the failure message starts with the expected message.
@abstract func starts_with_message(expected: String) -> GdUnitFailureAssert
## Verifies that the failure message contains the expected message.
@abstract func contains_message(expected: String) -> GdUnitFailureAssert
@@ -0,0 +1 @@
uid://b7u30qhxenurx
+38
View File
@@ -0,0 +1,38 @@
@abstract class_name GdUnitFileAssert
extends GdUnitAssert
## Verifies that the current value is null.
@abstract func is_null() -> GdUnitFileAssert
## Verifies that the current value is not null.
@abstract func is_not_null() -> GdUnitFileAssert
## Verifies that the current value is equal to the given one.
@abstract func is_equal(expected: Variant) -> GdUnitFileAssert
## Verifies that the current value is not equal to expected one.
@abstract func is_not_equal(expected: Variant) -> GdUnitFileAssert
## Overrides the default failure message by given custom message.
@abstract func override_failure_message(message: String) -> GdUnitFileAssert
## Appends a custom message to the failure message.
@abstract func append_failure_message(message: String) -> GdUnitFileAssert
@abstract func is_file() -> GdUnitFileAssert
@abstract func exists() -> GdUnitFileAssert
@abstract func is_script() -> GdUnitFileAssert
@abstract func contains_exactly(expected_rows :Array) -> GdUnitFileAssert
@@ -0,0 +1 @@
uid://oiy1n0qmt45d
+75
View File
@@ -0,0 +1,75 @@
## An Assertion Tool to verify float values
@abstract class_name GdUnitFloatAssert
extends GdUnitAssert
## Verifies that the current value is null.
@abstract func is_null() -> GdUnitFloatAssert
## Verifies that the current value is not null.
@abstract func is_not_null() -> GdUnitFloatAssert
## Verifies that the current value is equal to the given one.
@abstract func is_equal(expected: Variant) -> GdUnitFloatAssert
## Verifies that the current value is not equal to expected one.
@abstract func is_not_equal(expected: Variant) -> GdUnitFloatAssert
## Verifies that the current and expected value are approximately equal.
@abstract func is_equal_approx(expected: float, approx: float) -> GdUnitFloatAssert
## Overrides the default failure message by given custom message.
@abstract func override_failure_message(message: String) -> GdUnitFloatAssert
## Appends a custom message to the failure message.
@abstract func append_failure_message(message: String) -> GdUnitFloatAssert
## Verifies that the current value is less than the given one.
@abstract func is_less(expected: float) -> GdUnitFloatAssert
## Verifies that the current value is less than or equal the given one.
@abstract func is_less_equal(expected: float) -> GdUnitFloatAssert
## Verifies that the current value is greater than the given one.
@abstract func is_greater(expected: float) -> GdUnitFloatAssert
## Verifies that the current value is greater than or equal the given one.
@abstract func is_greater_equal(expected: float) -> GdUnitFloatAssert
## Verifies that the current value is negative.
@abstract func is_negative() -> GdUnitFloatAssert
## Verifies that the current value is not negative.
@abstract func is_not_negative() -> GdUnitFloatAssert
## Verifies that the current value is equal to zero.
@abstract func is_zero() -> GdUnitFloatAssert
## Verifies that the current value is not equal to zero.
@abstract func is_not_zero() -> GdUnitFloatAssert
## Verifies that the current value is in the given set of values.
@abstract func is_in(expected: Array) -> GdUnitFloatAssert
## Verifies that the current value is not in the given set of values.
@abstract func is_not_in(expected: Array) -> GdUnitFloatAssert
## Verifies that the current value is between the given boundaries (inclusive).
@abstract func is_between(from: float, to: float) -> GdUnitFloatAssert
@@ -0,0 +1 @@
uid://c82pxwpofcaa3
+42
View File
@@ -0,0 +1,42 @@
## An Assertion Tool to verify function callback values
@abstract class_name GdUnitFuncAssert
extends GdUnitAssert
## Verifies that the current value is null.
@abstract func is_null() -> GdUnitFuncAssert
## Verifies that the current value is not null.
@abstract func is_not_null() -> GdUnitFuncAssert
## Verifies that the current value is equal to the given one.
@abstract func is_equal(expected: Variant) -> GdUnitFuncAssert
## Verifies that the current value is not equal to expected one.
@abstract func is_not_equal(expected: Variant) -> GdUnitFuncAssert
## Overrides the default failure message by given custom message.
@abstract func override_failure_message(message: String) -> GdUnitFuncAssert
## Appends a custom message to the failure message.
@abstract func append_failure_message(message: String) -> GdUnitFuncAssert
## Verifies that the current value is true.
@abstract func is_true() -> GdUnitFuncAssert
## Verifies that the current value is false.
@abstract func is_false() -> GdUnitFuncAssert
## Sets the timeout in ms to wait the function returnd the expected value, if the time over a failure is emitted.[br]
## e.g.[br]
## do wait until 5s the function `is_state` is returns 10 [br]
## [code]assert_func(instance, "is_state").wait_until(5000).is_equal(10)[/code]
@abstract func wait_until(timeout: int) -> GdUnitFuncAssert
@@ -0,0 +1 @@
uid://bf4ugqv5d4oy1
@@ -0,0 +1,59 @@
## An assertion tool to verify for Godot runtime errors like assert() and push notifications like push_error().
@abstract class_name GdUnitGodotErrorAssert
extends GdUnitAssert
## Verifies that the current value is null.
@abstract func is_null() -> GdUnitGodotErrorAssert
## Verifies that the current value is not null.
@abstract func is_not_null() -> GdUnitGodotErrorAssert
## Verifies that the current value is equal to the given one.
@abstract func is_equal(expected: Variant) -> GdUnitGodotErrorAssert
## Verifies that the current value is not equal to expected one.
@abstract func is_not_equal(expected: Variant) -> GdUnitGodotErrorAssert
## Overrides the default failure message by given custom message.
@abstract func override_failure_message(message: String) -> GdUnitGodotErrorAssert
## Appends a custom message to the failure message.
@abstract func append_failure_message(message: String) -> GdUnitGodotErrorAssert
## Verifies if the executed code runs without any runtime errors
## Usage:
## [codeblock]
## await assert_error(<callable>).is_success()
## [/codeblock]
@abstract func is_success() -> GdUnitGodotErrorAssert
## Verifies if the executed code runs into a runtime error
## Usage:
## [codeblock]
## await assert_error(<callable>).is_runtime_error(<expected error message>)
## [/codeblock]
@abstract func is_runtime_error(expected_error: Variant) -> GdUnitGodotErrorAssert
## Verifies if the executed code has a push_warning() used
## Usage:
## [codeblock]
## await assert_error(<callable>).is_push_warning(<expected push warning message>)
## [/codeblock]
@abstract func is_push_warning(expected_warning: Variant) -> GdUnitGodotErrorAssert
## Verifies if the executed code has a push_error() used
## Usage:
## [codeblock]
## await assert_error(<callable>).is_push_error(<expected push error message>)
## [/codeblock]
@abstract func is_push_error(expected_error: Variant) -> GdUnitGodotErrorAssert
@@ -0,0 +1 @@
uid://dkub4b1kkhi86
+79
View File
@@ -0,0 +1,79 @@
## An Assertion Tool to verify integer values
@abstract class_name GdUnitIntAssert
extends GdUnitAssert
## Verifies that the current value is null.
@abstract func is_null() -> GdUnitIntAssert
## Verifies that the current value is not null.
@abstract func is_not_null() -> GdUnitIntAssert
## Verifies that the current value is equal to the given one.
@abstract func is_equal(expected: Variant) -> GdUnitIntAssert
## Verifies that the current value is not equal to expected one.
@abstract func is_not_equal(expected: Variant) -> GdUnitIntAssert
## Overrides the default failure message by given custom message.
@abstract func override_failure_message(message: String) -> GdUnitIntAssert
## Appends a custom message to the failure message.
@abstract func append_failure_message(message: String) -> GdUnitIntAssert
## Verifies that the current value is less than the given one.
@abstract func is_less(expected: int) -> GdUnitIntAssert
## Verifies that the current value is less than or equal the given one.
@abstract func is_less_equal(expected: int) -> GdUnitIntAssert
## Verifies that the current value is greater than the given one.
@abstract func is_greater(expected: int) -> GdUnitIntAssert
## Verifies that the current value is greater than or equal the given one.
@abstract func is_greater_equal(expected: int) -> GdUnitIntAssert
## Verifies that the current value is even.
@abstract func is_even() -> GdUnitIntAssert
## Verifies that the current value is odd.
@abstract func is_odd() -> GdUnitIntAssert
## Verifies that the current value is negative.
@abstract func is_negative() -> GdUnitIntAssert
## Verifies that the current value is not negative.
@abstract func is_not_negative() -> GdUnitIntAssert
## Verifies that the current value is equal to zero.
@abstract func is_zero() -> GdUnitIntAssert
## Verifies that the current value is not equal to zero.
@abstract func is_not_zero() -> GdUnitIntAssert
## Verifies that the current value is in the given set of values.
@abstract func is_in(expected: Array) -> GdUnitIntAssert
## Verifies that the current value is not in the given set of values.
@abstract func is_not_in(expected: Array) -> GdUnitIntAssert
## Verifies that the current value is between the given boundaries (inclusive).
@abstract func is_between(from: int, to: int) -> GdUnitIntAssert
@@ -0,0 +1 @@
uid://cb8iji5xfd6mw
+61
View File
@@ -0,0 +1,61 @@
## An Assertion Tool to verify Object values
@abstract class_name GdUnitObjectAssert
extends GdUnitAssert
## Verifies that the current value is null.
@abstract func is_null() -> GdUnitObjectAssert
## Verifies that the current value is not null.
@abstract func is_not_null() -> GdUnitObjectAssert
## Verifies that the current value is equal to the given one.
@abstract func is_equal(expected: Variant) -> GdUnitObjectAssert
## Verifies that the current value is not equal to expected one.
@abstract func is_not_equal(expected: Variant) -> GdUnitObjectAssert
## Overrides the default failure message by given custom message.
@abstract func override_failure_message(message: String) -> GdUnitObjectAssert
## Appends a custom message to the failure message.
@abstract func append_failure_message(message: String) -> GdUnitObjectAssert
## Verifies that the current object is the same as the given one.
@abstract func is_same(expected: Variant) -> GdUnitObjectAssert
## Verifies that the current object is not the same as the given one.
@abstract func is_not_same(expected: Variant) -> GdUnitObjectAssert
## Verifies that the current object is an instance of the given type.
@abstract func is_instanceof(type: Variant) -> GdUnitObjectAssert
## Verifies the current object is a valid instance (e.g. has not been freed from memory).[br]
## [codeblock]
## var node := Node.new()
## assert_object(node).is_valid() # passes — node is alive
## node.free()
## assert_object(node).is_valid() # fails — node has been freed
## [/codeblock]
@abstract func is_valid() -> GdUnitObjectAssert
## Verifies that the current object is not an instance of the given type.
@abstract func is_not_instanceof(type: Variant) -> GdUnitObjectAssert
## Checks whether the current object inherits from the specified type.
@abstract func is_inheriting(type: Variant) -> GdUnitObjectAssert
## Checks whether the current object does NOT inherit from the specified type.
@abstract func is_not_inheriting(type: Variant) -> GdUnitObjectAssert
@@ -0,0 +1 @@
uid://dvu6dibmft8cm
+51
View File
@@ -0,0 +1,51 @@
## An Assertion Tool to verify Results
@abstract class_name GdUnitResultAssert
extends GdUnitAssert
## Verifies that the current value is null.
@abstract func is_null() -> GdUnitResultAssert
## Verifies that the current value is not null.
@abstract func is_not_null() -> GdUnitResultAssert
## Verifies that the current value is equal to the given one.
@abstract func is_equal(expected: Variant) -> GdUnitResultAssert
## Verifies that the current value is not equal to expected one.
@abstract func is_not_equal(expected: Variant) -> GdUnitResultAssert
## Overrides the default failure message by given custom message.
@abstract func override_failure_message(message: String) -> GdUnitResultAssert
## Appends a custom message to the failure message.
@abstract func append_failure_message(message: String) -> GdUnitResultAssert
## Verifies that the result is ends up with empty
@abstract func is_empty() -> GdUnitResultAssert
## Verifies that the result is ends up with success
@abstract func is_success() -> GdUnitResultAssert
## Verifies that the result is ends up with warning
@abstract func is_warning() -> GdUnitResultAssert
## Verifies that the result is ends up with error
@abstract func is_error() -> GdUnitResultAssert
## Verifies that the result contains the given message
@abstract func contains_message(expected: String) -> GdUnitResultAssert
## Verifies that the result contains the given value
@abstract func is_value(expected: Variant) -> GdUnitResultAssert
@@ -0,0 +1 @@
uid://djbyj2882ht2s
+350
View File
@@ -0,0 +1,350 @@
## The Scene Runner is a tool used for simulating interactions on a scene.
## With this tool, you can simulate input events such as keyboard or mouse input and/or simulate scene processing over a certain number of frames.
## This tool is typically used for integration testing a scene.
@abstract class_name GdUnitSceneRunner
extends RefCounted
## Simulates that an action has been pressed.[br]
## [member action] : the action e.g. [code]"ui_up"[/code][br]
## [member event_index] : [url=https://docs.godotengine.org/en/4.4/classes/class_inputeventaction.html#class-inputeventaction-property-event-index]default=-1[/url][br]
@abstract func simulate_action_pressed(action: String, event_index := -1) -> GdUnitSceneRunner
## Simulates that an action is pressed.[br]
## [member action] : the action e.g. [code]"ui_up"[/code][br]
## [member event_index] : [url=https://docs.godotengine.org/en/4.4/classes/class_inputeventaction.html#class-inputeventaction-property-event-index]default=-1[/url][br]
@abstract func simulate_action_press(action: String, event_index := -1) -> GdUnitSceneRunner
## Simulates that an action has been released.[br]
## [member action] : the action e.g. [code]"ui_up"[/code][br]
## [member event_index] : [url=https://docs.godotengine.org/en/4.4/classes/class_inputeventaction.html#class-inputeventaction-property-event-index]default=-1[/url][br]
@abstract func simulate_action_release(action: String, event_index := -1) -> GdUnitSceneRunner
## Simulates that a key has been pressed.[br]
## @deprecated: the modifier [b]shift_pressed[/b] and [b]ctrl_pressed[/b] will be removed in v7.0
## [member key_code] : the key code e.g. [constant KEY_ENTER][br]
## [codeblock]
## func test_key_presssed():
## var runner = scene_runner("res://scenes/simple_scene.tscn")
## await runner.simulate_key_pressed(KEY_SPACE)
## [/codeblock]
@abstract func simulate_key_pressed(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner
## Simulates that a key is pressing.[br]
## @deprecated: the modifier [b]shift_pressed[/b] and [b]ctrl_pressed[/b] will be removed in v7.0[br]See `test_key_shift_and_A_presssing` for example using key combinations
## [member key_code] : the key code e.g. [constant KEY_ENTER][br]
## [codeblock]
## # Do simulate key pressing A
## func test_key_A_presssing():
## var runner = scene_runner("res://scenes/simple_scene.tscn")
## await runner.simulate_key_press(KEY_A)
##
##
## # Do simulate keycombination pressing shift+A
## func test_key_shift_and_A_presssing():
## var runner = scene_runner("res://scenes/simple_scene.tscn")
## runner.simulate_key_press(KEY_SHIFT)
## runner.simulate_key_press(KEY_A)
## await _runner.await_input_processed()
## [/codeblock]
@abstract func simulate_key_press(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner
## Simulates that a key has been released.[br]
## [member key_code] : the key code e.g. [constant KEY_ENTER][br]
## [codeblock]
## # Do simulate releasing key A
## func test_key_A_releasing():
## var runner = scene_runner("res://scenes/simple_scene.tscn")
## await runner.simulate_key_release(KEY_A)
##
##
## # Do simulate keycombination pressing shift+A
## func test_key_shift_and_A_releasing(():
## var runner = scene_runner("res://scenes/simple_scene.tscn")
## runner.simulate_key_release(KEY_SHIFT)
## runner.simulate_key_release(KEY_A)
## await _runner.await_input_processed()
## [/codeblock]
## @deprecated: the modifier [b]shift_pressed[/b] and [b]ctrl_pressed[/b] will be removed in v7.0[br]See `test_key_shift_and_A_releasing` for example using key combinations
@abstract func simulate_key_release(key_code: int, shift_pressed := false, ctrl_pressed := false) -> GdUnitSceneRunner
## Sets the mouse position to the specified vector, provided in pixels and relative to an origin at the upper left corner of the currently focused Window Manager game window.[br]
## [member position] : The absolute position in pixels as Vector2
@abstract func set_mouse_position(position: Vector2) -> GdUnitSceneRunner
## Returns the mouse's position in this Viewport using the coordinate system of this Viewport.
@abstract func get_mouse_position() -> Vector2
## Gets the current global mouse position of the current window
@abstract func get_global_mouse_position() -> Vector2
## Simulates a mouse moved to final position.[br]
## [member position] : The final mouse position
@abstract func simulate_mouse_move(position: Vector2) -> GdUnitSceneRunner
## Simulates a mouse move to the relative coordinates (offset).[br]
## [color=yellow]You must use [b]await[/b] to wait until the simulated mouse movement is complete.[/color][br]
## [br]
## [member relative] : The relative position, indicating the mouse position offset.[br]
## [member time] : The time to move the mouse by the relative position in seconds (default is 1 second).[br]
## [member trans_type] : Sets the type of transition used (default is TRANS_LINEAR).[br]
## [codeblock]
## func test_move_mouse():
## var runner = scene_runner("res://scenes/simple_scene.tscn")
## await runner.simulate_mouse_move_relative(Vector2(100,100))
## [/codeblock]
@abstract func simulate_mouse_move_relative(relative: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner
## Simulates a mouse move to the absolute coordinates.[br]
## [color=yellow]You must use [b]await[/b] to wait until the simulated mouse movement is complete.[/color][br]
## [br]
## [member position] : The final position of the mouse.[br]
## [member time] : The time to move the mouse to the final position in seconds (default is 1 second).[br]
## [member trans_type] : Sets the type of transition used (default is TRANS_LINEAR).[br]
## [codeblock]
## func test_move_mouse():
## var runner = scene_runner("res://scenes/simple_scene.tscn")
## await runner.simulate_mouse_move_absolute(Vector2(100,100))
## [/codeblock]
@abstract func simulate_mouse_move_absolute(position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner
## Simulates a mouse button pressed.[br]
## [member button_index] : The mouse button identifier, one of the [enum MouseButton] or button wheel constants.
## [member double_click] : Set to true to simulate a double-click
@abstract func simulate_mouse_button_pressed(button_index: MouseButton, double_click := false) -> GdUnitSceneRunner
## Simulates a mouse button press (holding)[br]
## [member button_index] : The mouse button identifier, one of the [enum MouseButton] or button wheel constants.
## [member double_click] : Set to true to simulate a double-click
@abstract func simulate_mouse_button_press(button_index: MouseButton, double_click := false) -> GdUnitSceneRunner
## Simulates a mouse button released.[br]
## [member button_index] : The mouse button identifier, one of the [enum MouseButton] or button wheel constants.
@abstract func simulate_mouse_button_release(button_index: MouseButton) -> GdUnitSceneRunner
## Simulates a screen touch is pressed.[br]
## [member index] : The touch index in the case of a multi-touch event.[br]
## [member position] : The position to touch the screen.[br]
## [member double_tap] : If true, the touch's state is a double tab.
@abstract func simulate_screen_touch_pressed(index: int, position: Vector2, double_tap := false) -> GdUnitSceneRunner
## Simulates a screen touch press without releasing it immediately, effectively simulating a "hold" action.[br]
## [member index] : The touch index in the case of a multi-touch event.[br]
## [member position] : The position to touch the screen.[br]
## [member double_tap] : If true, the touch's state is a double tab.
@abstract func simulate_screen_touch_press(index: int, position: Vector2, double_tap := false) -> GdUnitSceneRunner
## Simulates a screen touch is released.[br]
## [member index] : The touch index in the case of a multi-touch event.[br]
## [member double_tap] : If true, the touch's state is a double tab.
@abstract func simulate_screen_touch_release(index: int, double_tap := false) -> GdUnitSceneRunner
## Simulates a touch drag and drop event to a relative position.[br]
## [color=yellow]You must use [b]await[/b] to wait until the simulated drag&drop is complete.[/color][br]
## [br]
## [member index] : The touch index in the case of a multi-touch event.[br]
## [member relative] : The relative position, indicating the drag&drop position offset.[br]
## [member time] : The time to move to the relative position in seconds (default is 1 second).[br]
## [member trans_type] : Sets the type of transition used (default is TRANS_LINEAR).[br]
## [codeblock]
## func test_touch_drag_drop():
## var runner = scene_runner("res://scenes/simple_scene.tscn")
## # start drag at position 50,50
## runner.simulate_screen_touch_drag_begin(1, Vector2(50, 50))
## # and drop it at final at 150,50 relative (50,50 + 100,0)
## await runner.simulate_screen_touch_drag_relative(1, Vector2(100,0))
## [/codeblock]
@abstract func simulate_screen_touch_drag_relative(index: int, relative: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner
## Simulates a touch screen drop to the absolute coordinates (offset).[br]
## [color=yellow]You must use [b]await[/b] to wait until the simulated drop is complete.[/color][br]
## [br]
## [member index] : The touch index in the case of a multi-touch event.[br]
## [member position] : The final position, indicating the drop position.[br]
## [member time] : The time to move to the final position in seconds (default is 1 second).[br]
## [member trans_type] : Sets the type of transition used (default is TRANS_LINEAR).[br]
## [codeblock]
## func test_touch_drag_drop():
## var runner = scene_runner("res://scenes/simple_scene.tscn")
## # start drag at position 50,50
## runner.simulate_screen_touch_drag_begin(1, Vector2(50, 50))
## # and drop it at 100,50
## await runner.simulate_screen_touch_drag_absolute(1, Vector2(100,50))
## [/codeblock]
@abstract func simulate_screen_touch_drag_absolute(index: int, position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner
## Simulates a complete drag and drop event from one position to another.[br]
## This is ideal for testing complex drag-and-drop scenarios that require a specific start and end position.[br]
## [color=yellow]You must use [b]await[/b] to wait until the simulated drop is complete.[/color][br]
## [br]
## [member index] : The touch index in the case of a multi-touch event.[br]
## [member position] : The drag start position, indicating the drag position.[br]
## [member drop_position] : The drop position, indicating the drop position.[br]
## [member time] : The time to move to the final position in seconds (default is 1 second).[br]
## [member trans_type] : Sets the type of transition used (default is TRANS_LINEAR).[br]
## [codeblock]
## func test_touch_drag_drop():
## var runner = scene_runner("res://scenes/simple_scene.tscn")
## # start drag at position 50,50 and drop it at 100,50
## await runner.simulate_screen_touch_drag_drop(1, Vector2(50, 50), Vector2(100,50))
## [/codeblock]
@abstract func simulate_screen_touch_drag_drop(index: int, position: Vector2, drop_position: Vector2, time: float = 1.0, trans_type: Tween.TransitionType = Tween.TRANS_LINEAR) -> GdUnitSceneRunner
## Simulates a touch screen drag event to given position.[br]
## [member index] : The touch index in the case of a multi-touch event.[br]
## [member position] : The drag start position, indicating the drag position.[br]
@abstract func simulate_screen_touch_drag(index: int, position: Vector2) -> GdUnitSceneRunner
## Returns the actual position of the touchscreen drag position by given index.
## [member index] : The touch index in the case of a multi-touch event.[br]
@abstract func get_screen_touch_drag_position(index: int) -> Vector2
## Sets how fast or slow the scene simulation is processed (clock ticks versus the real).[br]
## It defaults to 1.0. A value of 2.0 means the game moves twice as fast as real life,
## whilst a value of 0.5 means the game moves at half the regular speed.
## [member time_factor] : A float representing the simulation speed.[br]
## - Default is 1.0, meaning the simulation runs at normal speed.[br]
## - A value of 2.0 means the simulation runs twice as fast as real time.[br]
## - A value of 0.5 means the simulation runs at half the regular speed.[br]
@abstract func set_time_factor(time_factor: float = 1.0) -> GdUnitSceneRunner
## Simulates scene processing for a certain number of frames.[br]
## [member frames] : amount of frames to process[br]
## [member delta_milli] : the time delta between a frame in milliseconds
@abstract func simulate_frames(frames: int, delta_milli: int = -1) -> GdUnitSceneRunner
## Simulates scene processing until the given signal is emitted by the scene.[br]
## [member signal_name] : the signal to stop the simulation[br]
## [member args] : optional signal arguments to be matched for stop[br]
@abstract func simulate_until_signal(signal_name: String, ...args: Array) -> GdUnitSceneRunner
## Simulates scene processing until the given signal is emitted by the given object.[br]
## [member source] : the object that should emit the signal[br]
## [member signal_name] : the signal to stop the simulation[br]
## [member args] : optional signal arguments to be matched for stop
@abstract func simulate_until_object_signal(source: Object, signal_name: String, ...args: Array) -> GdUnitSceneRunner
## Waits for all input events to be processed by flushing any buffered input events
## and then awaiting a full cycle of both the process and physics frames.[br]
## [br]
## This is typically used to ensure that any simulated or queued inputs are fully
## processed before proceeding with the next steps in the scene.[br]
## It's essential for reliable input simulation or when synchronizing logic based
## on inputs.[br]
##
## Usage Example:
## [codeblock]
## await await_input_processed() # Ensure all inputs are processed before continuing
## [/codeblock]
@abstract func await_input_processed() -> void
## The await_func function pauses execution until a specified function in the scene returns a value.[br]
## It returns a [GdUnitFuncAssert], which provides a suite of assertion methods to verify the returned value.[br]
## [member func_name] : The name of the function to wait for.[br]
## [member args] : Optional function arguments
## [br]
## Usage Example:
## [codeblock]
## # Waits for 'calculate_score' function and verifies the result is equal to 100.
## await_func("calculate_score").is_equal(100)
## [/codeblock]
@abstract func await_func(func_name: String, ...args: Array) -> GdUnitFuncAssert
## The await_func_on function extends the functionality of await_func by allowing you to specify a source node within the scene.[br]
## It waits for a specified function on that node to return a value and returns a [GdUnitFuncAssert] object for assertions.[br]
## [member source] : The object where implements the function.[br]
## [member func_name] : The name of the function to wait for.[br]
## [member args] : optional function arguments
## [br]
## Usage Example:
## [codeblock]
## # Waits for 'calculate_score' function and verifies the result is equal to 100.
## var my_instance := ScoreCalculator.new()
## await_func(my_instance, "calculate_score").is_equal(100)
## [/codeblock]
@abstract func await_func_on(source: Object, func_name: String, ...args: Array) -> GdUnitFuncAssert
## Waits for the specified signal to be emitted by the scene. If the signal is not emitted within the given timeout, the operation fails.[br]
## [member signal_name] : The name of the signal to wait for[br]
## [member args] : The signal arguments as an array[br]
## [member timeout] : The maximum duration (in milliseconds) to wait for the signal to be emitted before failing
@abstract func await_signal(signal_name: String, args := [], timeout := 2000 ) -> void
## Waits for the specified signal to be emitted by a particular source node. If the signal is not emitted within the given timeout, the operation fails.[br]
## [member source] : the object from which the signal is emitted[br]
## [member signal_name] : The name of the signal to wait for[br]
## [member args] : The signal arguments as an array[br]
## [member timeout] : tThe maximum duration (in milliseconds) to wait for the signal to be emitted before failing
@abstract func await_signal_on(source: Object, signal_name: String, args := [], timeout := 2000 ) -> void
## Restores the scene window to a windowed mode and brings it to the foreground.[br]
## This ensures that the scene is visible and active during testing, making it easier to observe and interact with.
@abstract func move_window_to_foreground() -> GdUnitSceneRunner
## Minimizes the scene window to a windowed mode and brings it to the background.[br]
## This ensures that the scene is hidden during testing.
@abstract func move_window_to_background() -> GdUnitSceneRunner
## Return the current value of the property with the name <name>.[br]
## [member name] : name of property[br]
## [member return] : the value of the property
@abstract func get_property(name: String) -> Variant
## Set the value <value> of the property with the name <name>.[br]
## [member name] : name of property[br]
## [member value] : value of property[br]
## [member return] : true|false depending on valid property name.
@abstract func set_property(name: String, value: Variant) -> bool
## executes the function specified by <name> in the scene and returns the result.[br]
## [member name] : the name of the function to execute[br]
## [member args] : optional function arguments[br]
## [member return] : the function result
@abstract func invoke(name: String, ...args: Array) -> Variant
## Searches for the specified node with the name in the current scene and returns it, otherwise null.[br]
## [member name] : the name of the node to find[br]
## [member recursive] : enables/disables seraching recursive[br]
## [member return] : the node if find otherwise null
@abstract func find_child(name: String, recursive: bool = true, owned: bool = false) -> Node
## Access to current running scene
@abstract func scene() -> Node
@@ -0,0 +1 @@
uid://cd83xyqmgeb3n
+158
View File
@@ -0,0 +1,158 @@
## An Assertion Tool to verify for emitted signals until a waiting time
@abstract class_name GdUnitSignalAssert
extends GdUnitAssert
## Verifies that the current value is null.
@abstract func is_null() -> GdUnitSignalAssert
## Verifies that the current value is not null.
@abstract func is_not_null() -> GdUnitSignalAssert
## Verifies that the current value is equal to the given one.
@abstract func is_equal(expected: Variant) -> GdUnitSignalAssert
## Verifies that the current value is not equal to expected one.
@abstract func is_not_equal(expected: Variant) -> GdUnitSignalAssert
## Overrides the default failure message by given custom message.
@abstract func override_failure_message(message: String) -> GdUnitSignalAssert
## Appends a custom message to the failure message.
@abstract func append_failure_message(message: String) -> GdUnitSignalAssert
## Verifies that the specified signal is emitted with the expected arguments.[br]
##
## This assertion waits for a signal to be emitted from the object under test and
## validates that it was emitted with the correct arguments. The function supports
## both typed signals (Signal type) and string-based signal names for flexibility
## in different testing scenarios.[br]
## [br]
## [b]Parameters:[/b][br]
## [param signal_name]: The signal to monitor. Can be either:[br]
## • A [Signal] reference (recommended for type safety)[br]
## • A [String] with the signal name
## [param signal_args]: Optional expected signal arguments.[br]
## When provided, verifies the signal was emitted with exactly these values.[br]
## [br]
## [b]Returns:[/b][br]
## [GdUnitSignalAssert] - Returns self for method chaining.[br]
## [br]
## [b]Examples:[/b]
## [codeblock]
## signal signal_a(value: int)
## signal signal_b(name: String, count: int)
##
## # Wait for signal emission without checking arguments
## # Using Signal reference (type-safe)
## await assert_signal(instance).is_emitted(signal_a)
## # Using string name (dynamic)
## await assert_signal(instance).is_emitted("signal_a")
##
## # Wait for signal emission with specific argument
## await assert_signal(instance).is_emitted(signal_a, 10)
##
## # Wait for signal with multiple arguments
## await assert_signal(instance).is_emitted(signal_b, "test", 42)
##
## # Wait max 500ms for signal with argument 10
## await assert_signal(instance).wait_until(500).is_emitted(signal_a, 10)
## [/codeblock]
## [br]
## [b]Note:[/b] This is an async operation - use [code]await[/code] when calling.[br]
## The assertion fails if the signal is not emitted within the timeout period.
@abstract func is_emitted(signal_name: Variant, ...signal_args: Array) -> GdUnitSignalAssert
## Verifies that the specified signal is NOT emitted with the expected arguments.[br]
##
## This assertion waits for a specified time period and validates that a signal
## was not emitted with the given arguments. Useful for ensuring certain conditions
## don't trigger unwanted signals or for verifying signal filtering logic.[br]
## [br]
## [b]Parameters:[/b][br]
## [param signal_name]: The signal to monitor. Can be either:[br]
## • A [Signal] reference (recommended for type safety)[br]
## • A [String] with the signal name
## [param signal_args]: Optional expected signal arguments.[br]
## When provided, verifies the signal was not emitted with these specific values.[br]
## If omitted, verifies the signal was not emitted at all.[br]
## [br]
## [b]Returns:[/b][br]
## [GdUnitSignalAssert] - Returns self for method chaining.[br]
## [br]
## [b]Examples:[/b]
## [codeblock]
## signal signal_a(value: int)
## signal signal_b(name: String, count: int)
##
## # Verify signal is not emitted at all (without checking arguments)
## await assert_signal(instance).wait_until(500).is_not_emitted(signal_a)
## await assert_signal(instance).wait_until(500).is_not_emitted("signal_a")
##
## # Verify signal is not emitted with specific argument
## await assert_signal(instance).wait_until(500).is_not_emitted(signal_a, 10)
##
## # Verify signal is not emitted with multiple arguments
## await assert_signal(instance).wait_until(500).is_not_emitted(signal_b, "test", 42)
##
## # Can be emitted with different arguments (this passes)
## instance.emit_signal("signal_a", 20) # Emits with 20, not 10
## await assert_signal(instance).wait_until(500).is_not_emitted(signal_a, 10)
## [/codeblock]
## [br]
## [b]Note:[/b] This is an async operation - use [code]await[/code] when calling.[br]
## The assertion fails if the signal IS emitted with the specified arguments within the timeout period.
@abstract func is_not_emitted(signal_name: Variant, ...signal_args: Array) -> GdUnitSignalAssert
## Verifies that the specified signal exists on the emitter object.[br]
##
## This assertion checks if a signal is defined on the object under test,
## regardless of whether it has been emitted. Useful for validating that
## objects have the expected signals before testing their emission.[br]
## [br]
## [b]Parameters:[/b][br]
## [param signal_name]: The signal to check. Can be either:[br]
## • A [Signal] reference (recommended for type safety)[br]
## • A [String] with the signal name
## [br]
## [b]Returns:[/b][br]
## [GdUnitSignalAssert] - Returns self for method chaining.[br]
## [br]
## [b]Examples:[/b]
## [codeblock]
## signal my_signal(value: int)
## signal another_signal()
##
## # Verify signal exists using Signal reference
## assert_signal(instance).is_signal_exists(my_signal)
##
## # Verify signal exists using string name
## assert_signal(instance).is_signal_exists("my_signal")
##
## # Chain with other assertions
## assert_signal(instance) \
## .is_signal_exists(my_signal) \
## .is_emitted(my_signal, 42)
##
## [/codeblock]
## [br]
## [b]Note:[/b] This only checks signal definition, not emission.[br]
## The assertion fails if the signal is not defined on the object.
@abstract func is_signal_exists(signal_name: Variant) -> GdUnitSignalAssert
## Sets the assert signal timeout in ms, if the time over a failure is reported.[br]
## Example:
## [codeblock]
## do wait until 5s the instance has emitted the signal `signal_a`[br]
## assert_signal(instance).wait_until(5000).is_emitted("signal_a")
## [/codeblock]
@abstract func wait_until(timeout: int) -> GdUnitSignalAssert
@@ -0,0 +1 @@
uid://dnno7sv5ruwws
+71
View File
@@ -0,0 +1,71 @@
## An Assertion Tool to verify String values
@abstract class_name GdUnitStringAssert
extends GdUnitAssert
## Verifies that the current value is null.
@abstract func is_null() -> GdUnitStringAssert
## Verifies that the current value is not null.
@abstract func is_not_null() -> GdUnitStringAssert
## Verifies that the current value is equal to the given one.
@abstract func is_equal(expected: Variant) -> GdUnitStringAssert
## Verifies that the current String is equal to the given one, ignoring case considerations.
@abstract func is_equal_ignoring_case(expected: Variant) -> GdUnitStringAssert
## Verifies that the current value is not equal to expected one.
@abstract func is_not_equal(expected: Variant) -> GdUnitStringAssert
## Verifies that the current String is not equal to the given one, ignoring case considerations.
@abstract func is_not_equal_ignoring_case(expected: Variant) -> GdUnitStringAssert
## Overrides the default failure message by given custom message.
@abstract func override_failure_message(message: String) -> GdUnitStringAssert
## Appends a custom message to the failure message.
@abstract func append_failure_message(message: String) -> GdUnitStringAssert
## Verifies that the current String is empty, it has a length of 0.
@abstract func is_empty() -> GdUnitStringAssert
## Verifies that the current String is not empty, it has a length of minimum 1.
@abstract func is_not_empty() -> GdUnitStringAssert
## Verifies that the current String contains the given String.
@abstract func contains(expected: String) -> GdUnitStringAssert
## Verifies that the current String does not contain the given String.
@abstract func not_contains(expected: String) -> GdUnitStringAssert
## Verifies that the current String does not contain the given String, ignoring case considerations.
@abstract func contains_ignoring_case(expected: String) -> GdUnitStringAssert
## Verifies that the current String does not contain the given String, ignoring case considerations.
@abstract func not_contains_ignoring_case(expected: String) -> GdUnitStringAssert
## Verifies that the current String starts with the given prefix.
@abstract func starts_with(expected: String) -> GdUnitStringAssert
## Verifies that the current String ends with the given suffix.
@abstract func ends_with(expected: String) -> GdUnitStringAssert
## Verifies that the current String has the expected length by used comparator.
@abstract func has_length(length: int, comparator: int = Comparator.EQUAL) -> GdUnitStringAssert
@@ -0,0 +1 @@
uid://c0l4juv45771a
+774
View File
@@ -0,0 +1,774 @@
## The main class for all GdUnit test suites[br]
## This class is the main class to implement your unit tests[br]
## You have to extend and implement your test cases as described[br]
## e.g MyTests.gd [br]
## [codeblock]
## extends GdUnitTestSuite
## # testcase
## func test_case_a():
## assert_that("value").is_equal("value")
## [/codeblock]
## @tutorial: https://mikeschulze.github.io/gdUnit4/faq/test-suite/
@icon("res://addons/gdUnit4/src/ui/settings/logo.png")
class_name GdUnitTestSuite
extends Node
### internal runtime variables that must not be overwritten!!!
@warning_ignore("unused_private_class_variable")
var __is_skipped := false
@warning_ignore("unused_private_class_variable")
var __skip_reason := "Unknow."
var __active_test_case: String
var __awaiter := __gdunit_awaiter()
### We now load all used asserts and tool scripts into the cache according to the principle of "lazy loading"
### in order to noticeably reduce the loading time of the test suite.
# We go this hard way to increase the loading performance to avoid reparsing all the used scripts
# for more detailed info -> https://github.com/godotengine/godot/issues/67400
static func __lazy_load(script_path: String) -> GDScript:
return GdUnitAssertions.__lazy_load(script_path)
func __gdunit_assert() -> GDScript:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd")
func __gdunit_tools() -> GDScript:
return __lazy_load("res://addons/gdUnit4/src/core/GdUnitTools.gd")
func __gdunit_file_access() -> GDScript:
return __lazy_load("res://addons/gdUnit4/src/core/GdUnitFileAccess.gd")
func __gdunit_awaiter() -> Object:
return __lazy_load("res://addons/gdUnit4/src/GdUnitAwaiter.gd").new()
func __gdunit_argument_matchers() -> GDScript:
return __lazy_load("res://addons/gdUnit4/src/matchers/GdUnitArgumentMatchers.gd")
func __gdunit_object_interactions() -> GDScript:
return __lazy_load("res://addons/gdUnit4/src/doubler/GdUnitObjectInteractions.gd")
## This function is called before a test suite starts[br]
## You can overwrite to prepare test data or initalizize necessary variables
func before() -> void:
pass
## This function is called at least when a test suite is finished[br]
## You can overwrite to cleanup data created during test running
func after() -> void:
pass
## This function is called before a test case starts[br]
## You can overwrite to prepare test case specific data
func before_test() -> void:
pass
## This function is called after the test case is finished[br]
## You can overwrite to cleanup your test case specific data
func after_test() -> void:
pass
func is_failure() -> bool:
return Engine.get_meta("GD_TEST_FAILURE") if Engine.has_meta("GD_TEST_FAILURE") else false
func set_active_test_case(test_case: String) -> void:
__active_test_case = test_case
# === Tools ====================================================================
# Mapps Godot error number to a readable error message. See at ERROR
# https://docs.godotengine.org/de/stable/classes/class_@globalscope.html#enum-globalscope-error
func error_as_string(error_number: int) -> String:
return error_string(error_number)
## A litle helper to auto freeing your created objects after test execution
func auto_free(obj: Variant) -> Variant:
var execution_context := GdUnitThreadManager.get_current_context().get_execution_context()
# If the execution context is null, we are not in the execution call stack, so we mark the object as “queued free.”
if execution_context == null:
if obj is Node:
@warning_ignore("unsafe_method_access")
obj.queue_free()
return obj
return execution_context.register_auto_free(obj)
## Saves the current project settings into the active execution context.[br]
## Useful when the "Auto Save Project Settings" setting is disabled and a test needs settings isolation on demand.[br]
## [br]
## Call this in [code]before()[/code]/[code]before_test()[/code] to snapshot the settings a test
## is about to change, to restore see [method restore_project_settings].
func save_project_settings() -> void:
var execution_context := GdUnitThreadManager.get_current_context().get_execution_context()
if execution_context == null:
return
execution_context.save_project_settings()
## Restores the project settings previously captured by [method save_project_settings].[br]
## [br]
## Call this in [code]after()[/code]/[code]after_test()[/code] to undo any settings a test changed.
func restore_project_settings() -> void:
var execution_context := GdUnitThreadManager.get_current_context().get_execution_context()
if execution_context == null:
return
execution_context.restore_project_settings()
@warning_ignore("native_method_override")
func add_child(node: Node, force_readable_name := false, internal := Node.INTERNAL_MODE_DISABLED) -> void:
super.add_child(node, force_readable_name, internal)
var execution_context := GdUnitThreadManager.get_current_context().get_execution_context()
if execution_context != null:
execution_context.orphan_monitor_start()
## Discard the error message triggered by a timeout (interruption).[br]
## By default, an interrupted test is reported as an error.[br]
## This function allows you to change the message to Success when an interrupted error is reported.
func discard_error_interupted_by_timeout() -> void:
@warning_ignore("unsafe_method_access")
__gdunit_tools().register_expect_interupted_by_timeout(self, __active_test_case)
## Creates a new directory under the temporary directory *user://tmp*[br]
## Useful for storing data during test execution. [br]
## The directory is automatically deleted after test suite execution
func create_temp_dir(relative_path: String) -> String:
@warning_ignore("unsafe_method_access")
return __gdunit_file_access().create_temp_dir(relative_path)
## Deletes the temporary base directory[br]
## Is called automatically after each execution of the test suite
func clean_temp_dir() -> void:
@warning_ignore("unsafe_method_access")
__gdunit_file_access().clear_tmp()
## Creates a new file under the temporary directory *user://tmp* + <relative_path>[br]
## with given name <file_name> and given file <mode> (default = File.WRITE)[br]
## If success the returned File is automatically closed after the execution of the test suite
func create_temp_file(relative_path: String, file_name: String, mode := FileAccess.WRITE) -> FileAccess:
@warning_ignore("unsafe_method_access")
return __gdunit_file_access().create_temp_file(relative_path, file_name, mode)
## Reads a resource by given path <resource_path> into a PackedStringArray.
func resource_as_array(resource_path: String) -> PackedStringArray:
@warning_ignore("unsafe_method_access")
return __gdunit_file_access().resource_as_array(resource_path)
## Reads a resource by given path <resource_path> and returned the content as String.
func resource_as_string(resource_path: String) -> String:
@warning_ignore("unsafe_method_access")
return __gdunit_file_access().resource_as_string(resource_path)
## Reads a resource by given path <resource_path> and return Variand translated by str_to_var
func resource_as_var(resource_path: String) -> Variant:
@warning_ignore("unsafe_method_access", "unsafe_cast")
return str_to_var(__gdunit_file_access().resource_as_string(resource_path) as String)
## Waits for given signal to be emitted by <source> until a specified timeout to fail[br]
## source: the object from which the signal is emitted[br]
## signal_name: signal name[br]
## args: the expected signal arguments as an array[br]
## timeout: the timeout in ms, default is set to 2000ms
func await_signal_on(source: Object, signal_name: String, args: Array = [], timeout: int = 2000) -> Variant:
@warning_ignore("unsafe_method_access")
return await __awaiter.await_signal_on(source, signal_name, args, timeout)
## Waits until the next idle frame
func await_idle_frame() -> void:
@warning_ignore("unsafe_method_access")
await __awaiter.await_idle_frame()
## Waits for a given amount of milliseconds[br]
## example:[br]
## [codeblock]
## # waits for 100ms
## await await_millis(myNode, 100).completed
## [/codeblock][br]
## use this waiter and not `await get_tree().create_timer().timeout to prevent errors when a test case is timed out
func await_millis(timeout: int) -> void:
@warning_ignore("unsafe_method_access")
await __awaiter.await_millis(timeout)
## Collects detailed information about orphaned nodes for debugging purposes.[br]
##
## This function gathers comprehensive details about nodes that remain in memory
## after test execution (orphans). It provides debugging information to help
## identify the source of memory leaks in tests. Must be manually called in
## tests when orphan nodes are detected.[br]
## [br]
## [b]When to Use:[/b][br]
## - When GdUnit4 reports orphan nodes after test execution[br]
## - For debugging memory leaks in test scenarios[br]
## - To get detailed information about unreleased nodes[br]
## [br]
## [b]Usage Pattern:[/b][br]
## Add this call at the end of tests that are suspected to create orphans,
## or when the test runner reports orphan detection.[br]
## [br]
## [b]Examples:[/b]
## [codeblock]
## func test_scene_management():
## # Test code that might create orphan nodes
## var scene = preload("res://TestScene.tscn").instantiate()
## add_child(scene)
##
## # Do test operations
## scene.some_method()
##
## # Clean up (but might miss some nodes)
## scene.queue_free()
##
## # Collect orphan details if any are detected
## collect_orphan_node_details()
## [/codeblock]
## [br]
## [b]Note:[/b] This is a debugging utility function that should be removed
## or commented out once orphan issues are resolved.
func collect_orphan_node_details() -> void:
GdUnitThreadManager.get_current_context().get_execution_context().orphan_monitor_collect()
## Creates a new scene runner to allow simulate interactions checked a scene.[br]
## The runner will manage the scene instance and release after the runner is released[br]
## example:[br]
## [codeblock]
## # creates a runner by using a instanciated scene
## var scene = load("res://foo/my_scne.tscn").instantiate()
## var runner := scene_runner(scene)
##
## # or simply creates a runner by using the scene resource path
## var runner := scene_runner("res://foo/my_scne.tscn")
## [/codeblock]
func scene_runner(scene: Variant, verbose := false) -> GdUnitSceneRunner:
return auto_free(__lazy_load("res://addons/gdUnit4/src/core/GdUnitSceneRunnerImpl.gd").new(scene, verbose))
# === Mocking & Spy ===========================================================
## do return a default value for primitive types or null
const RETURN_DEFAULTS = GdUnitMock.RETURN_DEFAULTS
## do call the real implementation
const CALL_REAL_FUNC = GdUnitMock.CALL_REAL_FUNC
## do return a default value for primitive types and a fully mocked value for Object types
## builds full deep mocked object
const RETURN_DEEP_STUB = GdUnitMock.RETURN_DEEP_STUB
## Creates a mock for given class name
func mock(clazz: Variant, mock_mode := RETURN_DEFAULTS) -> Variant:
@warning_ignore("unsafe_method_access")
return __lazy_load("res://addons/gdUnit4/src/mocking/GdUnitMockBuilder.gd").build(clazz, mock_mode)
## Creates a spy checked given object instance
func spy(instance: Variant) -> Variant:
@warning_ignore("unsafe_method_access")
return __lazy_load("res://addons/gdUnit4/src/spy/GdUnitSpyBuilder.gd").build(instance)
## Configures a return value for the specified function and used arguments.[br]
## [b]Example:
## [codeblock]
## # overrides the return value of myMock.is_selected() to false
## do_return(false).on(myMock).is_selected()
## [/codeblock]
func do_return(value: Variant) -> GdUnitMock:
return GdUnitMock.new(value)
## Verifies certain behavior happened at least once or exact number of times
func verify(obj: Variant, times := 1) -> Variant:
@warning_ignore("unsafe_method_access")
return __gdunit_object_interactions().verify(obj, times)
## Verifies no interactions is happen checked this mock or spy
func verify_no_interactions(obj: Variant) -> GdUnitAssert:
@warning_ignore("unsafe_method_access")
return __gdunit_object_interactions().verify_no_interactions(obj)
## Verifies the given mock or spy has any unverified interaction.
func verify_no_more_interactions(obj: Variant) -> GdUnitAssert:
@warning_ignore("unsafe_method_access")
return __gdunit_object_interactions().verify_no_more_interactions(obj)
## Resets the saved function call counters checked a mock or spy
func reset(obj: Variant) -> void:
@warning_ignore("unsafe_method_access")
__gdunit_object_interactions().reset(obj)
## Starts monitoring the specified source to collect all transmitted signals.[br]
## The collected signals can then be checked with 'assert_signal'.[br]
## By default, the specified source is automatically released when the test ends.
## You can control this behavior by setting auto_free to false if you do not want the source to be automatically freed.[br]
## Usage:
## [codeblock]
## var emitter := monitor_signals(MyEmitter.new())
## # call the function to send the signal
## emitter.do_it()
## # verify the signial is emitted
## await assert_signal(emitter).is_emitted('my_signal')
## [/codeblock]
func monitor_signals(source: Object, _auto_free := true) -> Object:
@warning_ignore("unsafe_method_access")
__lazy_load("res://addons/gdUnit4/src/core/thread/GdUnitThreadManager.gd")\
.get_current_context()\
.get_signal_collector()\
.register_emitter(source, true) # force recreate to start with a fresh monitoring
return auto_free(source) if _auto_free else source
# === Argument matchers ========================================================
## Argument matcher to match any argument
func any() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().any()
## Argument matcher to match any boolean value
func any_bool() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_BOOL)
## Argument matcher to match any integer value
func any_int() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_INT)
## Argument matcher to match any float value
func any_float() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_FLOAT)
## Argument matcher to match any String value
func any_string() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_STRING)
## Argument matcher to match any Color value
func any_color() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_COLOR)
## Argument matcher to match any Vector typed value
func any_vector() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_types([
TYPE_VECTOR2,
TYPE_VECTOR2I,
TYPE_VECTOR3,
TYPE_VECTOR3I,
TYPE_VECTOR4,
TYPE_VECTOR4I,
])
## Argument matcher to match any Vector2 value
func any_vector2() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_VECTOR2)
## Argument matcher to match any Vector2i value
func any_vector2i() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_VECTOR2I)
## Argument matcher to match any Vector3 value
func any_vector3() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_VECTOR3)
## Argument matcher to match any Vector3i value
func any_vector3i() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_VECTOR3I)
## Argument matcher to match any Vector4 value
func any_vector4() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_VECTOR4)
## Argument matcher to match any Vector4i value
func any_vector4i() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_VECTOR4I)
## Argument matcher to match any Rect2 value
func any_rect2() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_RECT2)
## Argument matcher to match any Plane value
func any_plane() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_PLANE)
## Argument matcher to match any Quaternion value
func any_quat() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_QUATERNION)
## Argument matcher to match any AABB value
func any_aabb() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_AABB)
## Argument matcher to match any Basis value
func any_basis() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_BASIS)
## Argument matcher to match any Transform2D value
func any_transform_2d() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_TRANSFORM2D)
## Argument matcher to match any Transform3D value
func any_transform_3d() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_TRANSFORM3D)
## Argument matcher to match any NodePath value
func any_node_path() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_NODE_PATH)
## Argument matcher to match any RID value
func any_rid() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_RID)
## Argument matcher to match any Object value
func any_object() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_OBJECT)
## Argument matcher to match any Dictionary value
func any_dictionary() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_DICTIONARY)
## Argument matcher to match any Array value
func any_array() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_ARRAY)
## Argument matcher to match any PackedByteArray value
func any_packed_byte_array() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_PACKED_BYTE_ARRAY)
## Argument matcher to match any PackedInt32Array value
func any_packed_int32_array() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_PACKED_INT32_ARRAY)
## Argument matcher to match any PackedInt64Array value
func any_packed_int64_array() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_PACKED_INT64_ARRAY)
## Argument matcher to match any PackedFloat32Array value
func any_packed_float32_array() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_PACKED_FLOAT32_ARRAY)
## Argument matcher to match any PackedFloat64Array value
func any_packed_float64_array() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_PACKED_FLOAT64_ARRAY)
## Argument matcher to match any PackedStringArray value
func any_packed_string_array() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_PACKED_STRING_ARRAY)
## Argument matcher to match any PackedVector2Array value
func any_packed_vector2_array() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_PACKED_VECTOR2_ARRAY)
## Argument matcher to match any PackedVector3Array value
func any_packed_vector3_array() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_PACKED_VECTOR3_ARRAY)
## Argument matcher to match any PackedColorArray value
func any_packed_color_array() -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().by_type(TYPE_PACKED_COLOR_ARRAY)
## Argument matcher to match any instance of given class
func any_class(clazz :Object) -> GdUnitArgumentMatcher:
@warning_ignore("unsafe_method_access")
return __gdunit_argument_matchers().any_class(clazz)
# === value extract utils ======================================================
## Builds an extractor by given function name and optional arguments
func extr(func_name: String, args := Array()) -> GdUnitValueExtractor:
return __lazy_load("res://addons/gdUnit4/src/extractors/GdUnitFuncValueExtractor.gd").new(func_name, args)
## Creates a GdUnitTuple from the provided arguments for use in test assertions.
## [br]
## This is the primary helper function for creating tuples in GdUnit4 tests.
## It provides a convenient way to group multiple expected values when using
## [method extractv] assertions. The function enforces that tuples must contain
## at least two values, as single-value extractions don't require tuple grouping.
## [br]
## [b]Parameters:[/b] [br]
## - [code]...args[/code]: Variable number of arguments (minimum 2) to group into a tuple.
## Each argument represents a value to be compared in assertions.
## [br]
## [b]Returns:[/b] [br]
## A [GdUnitTuple] containing the provided values, or an empty tuple if fewer than
## 2 arguments are provided (with an error message).
## [br]
## [b]Error Handling:[/b] [br]
## [codeblock]
## # This will push an error and return empty tuple
## var invalid = tuple("single_value") # Error: requires at least 2 arguments
## [br]
## # Correct usage - minimum 2 arguments
## var valid = tuple("name", "value")
## var valid_multi = tuple(1, 2, 3, 4, 5) # Can have many values
## [/codeblock]
func tuple(...args: Array) -> GdUnitTuple:
if args.size() < 2:
push_error("Tuple requires at least two arguments.")
return GdUnitTuple.new()
return GdUnitTuple.new.callv(args)
# === Asserts ==================================================================
## The common assertion tool to verify values.
## It checks the given value by type to fit to the best assert
func assert_that(current: Variant) -> GdUnitAssert:
match typeof(current):
TYPE_BOOL:
return assert_bool(current)
TYPE_INT:
return assert_int(current)
TYPE_FLOAT:
return assert_float(current)
TYPE_STRING:
return assert_str(current)
TYPE_VECTOR2, TYPE_VECTOR2I, TYPE_VECTOR3, TYPE_VECTOR3I, TYPE_VECTOR4, TYPE_VECTOR4I:
return assert_vector(current, false)
TYPE_DICTIONARY:
return assert_dict(current)
TYPE_ARRAY, TYPE_PACKED_BYTE_ARRAY, TYPE_PACKED_INT32_ARRAY, TYPE_PACKED_INT64_ARRAY,\
TYPE_PACKED_FLOAT32_ARRAY, TYPE_PACKED_FLOAT64_ARRAY, TYPE_PACKED_STRING_ARRAY,\
TYPE_PACKED_VECTOR2_ARRAY, TYPE_PACKED_VECTOR3_ARRAY, TYPE_PACKED_COLOR_ARRAY:
return assert_array(current, false)
TYPE_OBJECT, TYPE_NIL:
return assert_object(current)
_:
return __gdunit_assert().new(current)
## An assertion tool to verify boolean values.
static func assert_bool(current: Variant) -> GdUnitBoolAssert:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd").new(current)
## An assertion tool to verify String values.
func assert_str(current: Variant) -> GdUnitStringAssert:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd").new(current)
## An assertion tool to verify integer values.
func assert_int(current: Variant) -> GdUnitIntAssert:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd").new(current)
## An assertion tool to verify float values.
func assert_float(current: Variant) -> GdUnitFloatAssert:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd").new(current)
## An assertion tool to verify Vector values.[br]
## This assertion supports all vector types.[br]
## Usage:
## [codeblock]
## assert_vector(Vector2(1.2, 1.000001)).is_equal(Vector2(1.2, 1.000001))
## [/codeblock]
func assert_vector(current: Variant, type_check := true) -> GdUnitVectorAssert:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd").new(current, type_check)
## An assertion tool to verify arrays.
func assert_array(current: Variant, type_check := true) -> GdUnitArrayAssert:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd").new(current, type_check)
## An assertion tool to verify dictionaries.
func assert_dict(current: Variant) -> GdUnitDictionaryAssert:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd").new(current)
## An assertion tool to verify FileAccess.
func assert_file(current: Variant) -> GdUnitFileAssert:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd").new(current)
## An assertion tool to verify Objects.
func assert_object(current: Variant) -> GdUnitObjectAssert:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd").new(current)
func assert_result(current: Variant) -> GdUnitResultAssert:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd").new(current)
## An assertion tool that waits until a certain time for an expected function return value
func assert_func(instance: Object, func_name: String, args := Array()) -> GdUnitFuncAssert:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd").new(instance, func_name, args)
## An assertion tool to verify for emitted signals until a certain time.
func assert_signal(instance: Object) -> GdUnitSignalAssert:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd").new(instance)
## An assertion tool to test for failing assertions.[br]
## This assert is only designed for internal use to verify failing asserts working as expected.[br]
## Usage:
## [codeblock]
## assert_failure(func(): assert_bool(true).is_not_equal(true)) \
## .has_message("Expecting:\n 'true'\n not equal to\n 'true'")
## [/codeblock]
func assert_failure(assertion: Callable) -> GdUnitFailureAssert:
@warning_ignore("unsafe_method_access")
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd").new().execute(assertion)
## An assertion tool to test for failing assertions.[br]
## This assert is only designed for internal use to verify failing asserts working as expected.[br]
## Usage:
## [codeblock]
## await assert_failure_await(func(): assert_bool(true).is_not_equal(true)) \
## .has_message("Expecting:\n 'true'\n not equal to\n 'true'")
## [/codeblock]
func assert_failure_await(assertion: Callable) -> GdUnitFailureAssert:
@warning_ignore("unsafe_method_access")
return await __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd").new().execute_and_await(assertion)
## An assertion tool to verify Godot errors.[br]
## You can use to verify certain Godot errors like failing assertions, push_error, push_warn.[br]
## Usage:
## [codeblock]
## # tests no error occurred during execution of the code
## await assert_error(func (): return 0 )\
## .is_success()
##
## # tests a push_error('test error') occured during execution of the code
## await assert_error(func (): push_error('test error') )\
## .is_push_error('test error')
## [/codeblock]
func assert_error(current: Callable) -> GdUnitGodotErrorAssert:
return __lazy_load("res://addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd").new(current)
## Explicitly fails the current test indicating that the feature is not yet implemented.[br]
## This function is useful during development when you want to write test cases before implementing the actual functionality.[br]
## It provides a clear indication that the test failure is expected because the feature is still under development.[br]
## Usage:
## [codeblock]
## # Test for a feature that will be implemented later
## func test_advanced_ai_behavior():
## assert_not_yet_implemented()
##
## [/codeblock]
func assert_not_yet_implemented() -> void:
@warning_ignore("unsafe_method_access")
__gdunit_assert().new(null).do_fail_not_implemented()
## Explicitly fails the current test with a custom error message.[br]
## This function reports an error but does not terminate test execution automatically.[br]
## You must use 'return' after calling fail() to stop the test since GDScript has no exception support.[br]
## Useful for complex conditional testing scenarios where standard assertions are insufficient.[br]
## Usage:
## [codeblock]
## # Fail test when conditions are not met
## if !custom_check(player):
## fail("Player should be alive but has %d health" % player.health)
## return
##
## # Continue with test if conditions pass
## assert_that(player.health).is_greater(0)
## [/codeblock]
func fail(message: String) -> void:
@warning_ignore("unsafe_method_access")
__gdunit_assert().new(null).do_fail_with_message(message)
# --- internal stuff do not override!!!
func ResourcePath() -> String:
return get_script().resource_path
@@ -0,0 +1 @@
uid://bqh4y8rpdssrx
+86
View File
@@ -0,0 +1,86 @@
## A tuple implementation for GdUnit4 test assertions and value extraction.
## @tutorial(GdUnit4 Array Assertions): https://mikeschulze.github.io/gdUnit4/latest/testing/assert-array/#extractv
## @tutorial(GdUnit4 Testing Framework): https://mikeschulze.github.io/gdUnit4/
## [br]
## The GdUnitTuple class is a utility container designed specifically for the GdUnit4
## testing framework. It enables advanced assertion operations, particularly when
## extracting and comparing multiple values from complex test results.
## [br]
## [b]Primary Use Cases in Testing:[/b] [br]
## - Extracting multiple properties from test objects with [method extractv]## [br]
## - Grouping related assertion values for comparison## [br]
## - Returning multiple values from test helper methods## [br]
## - Organizing expected vs actual value pairs in assertions## [br]
## [br]
## [b]Example Usage in Tests:[/b]
## [codeblock]
## func test_player_stats_after_level_up():
## var player = Player.new()
## player.level_up()
##
## # Extract multiple properties using tuple
## assert_array([player]) \
## .extractv(extr("name"), extr("level"), extr("hp")) \
## .contains(tuple("Hero", 2, 150))
##
## func test_enemy_spawn_positions():
## var enemies: Array = spawn_enemies(3)
##
## # Verify multiple enemies have correct position data
## assert_array(enemies) \
## .extractv(extr("position.x"), extr("position.y")) \
## .contains_exactly([
## tuple(100, 200),
## tuple(150, 200),
## tuple(200, 200)
## ])
## [/codeblock]
## [br]
## [b]Integration with GdUnit4 Assertions:[/b] [br]
## Tuples work seamlessly with array assertion methods like: [br]
## - [code]contains()[/code] - Check if extracted values contain specific tuples [br]
## - [code]contains_exactly()[/code] - Verify exact tuple matches [br]
## - [code]is_equal()[/code] - Compare tuple equality [br]
## [br]
## [b]Note:[/b] This class is part of the GdUnit4 testing framework's internal
## utilities and is primarily intended for use within test assertions rather
## than production code.
class_name GdUnitTuple
extends RefCounted
var _values: Array = []
## Initializes a new GdUnitTuple with test values.
## [br]
## Creates a tuple to hold multiple values extracted from test objects
## or expected values for assertions. Commonly used with the [code]tuple()[/code]
## helper function in GdUnit4 tests.
## [br]
## [b]Parameters:[/b]
## - [code]...args[/code]: Variable number of values to store.
func _init(...args: Array) -> void:
_values = args
## Returns the tuple's values as an array for assertion comparisons.
## [br]
## Provides access to the stored test values. Used internally by GdUnit4's
## assertion system when comparing tuples in test validations.
## [br]
## [b]Returns:[/b]
## An [Array] containing all values stored in the tuple.
func values() -> Array:
return _values
## Returns a string representation for test output and debugging.
## [br]
## Formats the tuple for display in test results, error messages, and debug logs.
## This method is automatically called by GdUnit4 when displaying assertion
## failures involving tuples.
## [br]
## [b]Returns:[/b]
## A [String] in the format "tuple([value1, value2, ...])"
func _to_string() -> String:
return "tuple(%s)" % str(_values)
+1
View File
@@ -0,0 +1 @@
uid://btxdkihimm6t2
@@ -0,0 +1,9 @@
## This is the base interface for value extraction
class_name GdUnitValueExtractor
extends RefCounted
## Extracts a value by given implementation
func extract_value(value :Variant) -> Variant:
push_error("Uninplemented func 'extract_value'")
return value
@@ -0,0 +1 @@
uid://be6abg0ghp37l
+55
View File
@@ -0,0 +1,55 @@
## An Assertion Tool to verify Vector values
@abstract class_name GdUnitVectorAssert
extends GdUnitAssert
## Verifies that the current value is null.
@abstract func is_null() -> GdUnitVectorAssert
## Verifies that the current value is not null.
@abstract func is_not_null() -> GdUnitVectorAssert
## Verifies that the current value is equal to the given one.
@abstract func is_equal(expected: Variant) -> GdUnitVectorAssert
## Verifies that the current value is not equal to expected one.
@abstract func is_not_equal(expected: Variant) -> GdUnitVectorAssert
## Verifies that the current and expected value are approximately equal.
@abstract func is_equal_approx(expected: Variant, approx: Variant) -> GdUnitVectorAssert
## Overrides the default failure message by given custom message.
@abstract func override_failure_message(message: String) -> GdUnitVectorAssert
## Appends a custom message to the failure message.
@abstract func append_failure_message(message: String) -> GdUnitVectorAssert
## Verifies that the current value is less than the given one.
@abstract func is_less(expected: Variant) -> GdUnitVectorAssert
## Verifies that the current value is less than or equal the given one.
@abstract func is_less_equal(expected: Variant) -> GdUnitVectorAssert
## Verifies that the current value is greater than the given one.
@abstract func is_greater(expected: Variant) -> GdUnitVectorAssert
## Verifies that the current value is greater than or equal the given one.
@abstract func is_greater_equal(expected: Variant) -> GdUnitVectorAssert
## Verifies that the current value is between the given boundaries (inclusive).
@abstract func is_between(from: Variant, to: Variant) -> GdUnitVectorAssert
## Verifies that the current value is not between the given boundaries (inclusive).
@abstract func is_not_between(from: Variant, to: Variant) -> GdUnitVectorAssert
@@ -0,0 +1 @@
uid://bchhqrm7gxygm
@@ -0,0 +1,25 @@
# a value provider unsing a callback to get `next` value from a certain function
class_name CallBackValueProvider
extends ValueProvider
var _cb :Callable
var _args :Array
func _init(instance :Object, func_name :String, args :Array = Array(), force_error := true) -> void:
_cb = Callable(instance, func_name);
_args = args
if force_error and not _cb.is_valid():
push_error("Can't find function '%s' checked instance %s" % [func_name, instance])
func get_value() -> Variant:
if not _cb.is_valid():
return null
if _args.is_empty():
return await _cb.call()
return await _cb.callv(_args)
func dispose() -> void:
_cb = Callable()
@@ -0,0 +1 @@
uid://bauwyi0k0fetp
@@ -0,0 +1,13 @@
# default value provider, simple returns the initial value
class_name DefaultValueProvider
extends ValueProvider
var _value: Variant
func _init(value: Variant) -> void:
_value = value
func get_value() -> Variant:
return _value
@@ -0,0 +1 @@
uid://b24r327dm6o6m
@@ -0,0 +1,757 @@
class_name GdAssertMessages
extends Resource
const SUB_COLOR := Color(1, 0, 0, .15)
const ADD_COLOR := Color(0, 1, 0, .15)
const NO_ORPHAN_DETAILS := "⚠️No details available. Run tests in debug mode to collect details."
# Dictionary of control characters and their readable representations
const CONTROL_CHARS = {
"\n": "<LF>", # Line Feed
"\r": "<CR>", # Carriage Return
"\t": "<TAB>", # Tab
"\b": "<BS>", # Backspace
"\f": "<FF>", # Form Feed
"\v": "<VT>", # Vertical Tab
"\a": "<BEL>", # Bell
"": "<ESC>" # Escape
}
static var _warn_color := GdUnitEditorColorTheme.state_warning.to_html()
static var _error_color := GdUnitEditorColorTheme.state_failure.to_html()
static var _value_color := GdUnitEditorColorTheme.value_color.to_html()
static func format_dict(value :Variant) -> String:
if not value is Dictionary:
return str(value)
var dict_value: Dictionary = value
if dict_value.is_empty():
return "{ }"
var as_rows := var_to_str(value).split("\n")
for index in range( 1, as_rows.size()-1):
as_rows[index] = " " + as_rows[index]
as_rows[-1] = " " + as_rows[-1]
return "\n".join(as_rows)
# improved version of InputEvent as text
static func input_event_as_text(event :InputEvent) -> String:
var text := ""
if event is InputEventKey:
var key_event := event as InputEventKey
text += """
InputEventKey : keycode=%s (%s) pressed: %s
physical_keycode: %s
location: %s
echo: %s""" % [
key_event.keycode,
key_event.as_text_keycode(),
key_event.pressed,
key_event.physical_keycode,
key_event.location,
key_event.echo]
else:
text += event.as_text()
if event is InputEventMouse:
var mouse_event := event as InputEventMouse
text += """
global_position: %s""" % mouse_event.global_position
if event is InputEventWithModifiers:
var mouse_event := event as InputEventWithModifiers
text += """
--------
mods: %s
shift: %s
alt: %s
control: %s
meta: %s
command: %s""" % [
mouse_event.get_modifiers_mask(),
mouse_event.shift_pressed,
mouse_event.alt_pressed,
mouse_event.ctrl_pressed,
mouse_event.meta_pressed,
mouse_event.command_or_control_autoremap]
return text.dedent()
static func _colored_string_div(characters: String) -> String:
return colored_array_div(characters.to_utf32_buffer().to_int32_array())
static func colored_array_div(characters: PackedInt32Array) -> String:
if characters.is_empty():
return "<empty>"
var result := PackedInt32Array()
var index := 0
var missing_chars := PackedInt32Array()
var additional_chars := PackedInt32Array()
while index < characters.size():
var character := characters[index]
match character:
GdDiffTool.DIV_ADD:
index += 1
@warning_ignore("return_value_discarded")
additional_chars.append(characters[index])
GdDiffTool.DIV_SUB:
index += 1
@warning_ignore("return_value_discarded")
missing_chars.append(characters[index])
_:
if not missing_chars.is_empty():
result.append_array(format_chars(missing_chars, SUB_COLOR))
missing_chars = PackedInt32Array()
if not additional_chars.is_empty():
result.append_array(format_chars(additional_chars, ADD_COLOR))
additional_chars = PackedInt32Array()
@warning_ignore("return_value_discarded")
result.append(character)
index += 1
result.append_array(format_chars(missing_chars, SUB_COLOR))
result.append_array(format_chars(additional_chars, ADD_COLOR))
return result.to_byte_array().get_string_from_utf32()
static func _typed_value(value :Variant) -> String:
return GdDefaultValueDecoder.decode(value)
static func _warning(error :String) -> String:
return "[color=%s]%s[/color]" % [_warn_color, error]
static func _error(error :String) -> String:
return "[color=%s]%s[/color]" % [_error_color, error]
static func _nerror(number :Variant) -> String:
match typeof(number):
TYPE_INT:
return "[color=%s]%d[/color]" % [_error_color, number]
TYPE_FLOAT:
return "[color=%s]%f[/color]" % [_error_color, number]
_:
return "[color=%s]%s[/color]" % [_error_color, str(number)]
static func _colored(value: Variant, color: Color) -> String:
return "[color=%s]%s[/color]" % [color.to_html(), value]
static func _colored_value(value :Variant) -> String:
match typeof(value):
TYPE_STRING, TYPE_STRING_NAME:
return "'[color=%s]%s[/color]'" % [_value_color, _colored_string_div(str(value))]
TYPE_INT:
return "[color=%s]%d[/color]" % [_value_color, value]
TYPE_FLOAT:
return "[color=%s]%s[/color]" % [_value_color, _typed_value(value)]
TYPE_COLOR:
return "'[color=%s]%s[/color]'" % [_value_color, _typed_value(value)]
TYPE_OBJECT:
if value == null:
return "'[color=%s]<null>[/color]'" % [_value_color]
if value is InputEvent:
var ie: InputEvent = value
return "[color=%s]<%s>[/color]" % [_value_color, input_event_as_text(ie)]
var obj_value: Object = value
if obj_value.has_method("_to_string"):
return "[color=%s]<%s>[/color]" % [_value_color, str(value)]
return "[color=%s]<%s>[/color]" % [_value_color, obj_value.get_class()]
TYPE_DICTIONARY:
return "'[color=%s]%s[/color]'" % [_value_color, format_dict(value)]
_:
if GdArrayTools.is_array_type(value):
return "'[color=%s]%s[/color]'" % [_value_color, _typed_value(value)]
return "'[color=%s]%s[/color]'" % [_value_color, value]
static func _index_report_as_table(index_reports :Array) -> String:
var table := "[table=3]$cells[/table]"
var header := "[cell][right][b]$text[/b][/right]\t[/cell]"
var cell := "[cell][right]$text[/right]\t[/cell]"
var cells := header.replace("$text", "Index") + header.replace("$text", "Current") + header.replace("$text", "Expected")
for report :Variant in index_reports:
var index :String = str(report["index"])
var current :String = str(report["current"])
var expected :String = str(report["expected"])
cells += cell.replace("$text", index) + cell.replace("$text", current) + cell.replace("$text", expected)
return table.replace("$cells", cells)
static func orphan_warning(orphans_count: int, info: String) -> String:
if EngineDebugger.is_active():
return """
%s Detected %s possible orphan nodes.
To capture detailed information insert
[code]%s[/code]
""".dedent().trim_prefix("\n").trim_suffix("\n") % [
_warning("WARNING:"),
_colored_value(orphans_count),
_colored(info, Color.STEEL_BLUE)
]
return """
%s Detected %s possible orphan nodes.
%s
""".dedent().trim_prefix("\n").trim_suffix("\n") % [
_warning("WARNING:"),
_colored_value(orphans_count),
_warning(NO_ORPHAN_DETAILS)
]
static func orphan_detected(orphan_count: int) -> String:
if EngineDebugger.is_active():
return "%s Detected %s orphan nodes." % [
_warning("WARNING:"),
_colored_value(orphan_count)
]
return """
%s Detected %s orphan nodes.
%s""".dedent().trim_prefix("\n") % [
_warning("WARNING:"),
_colored_value(orphan_count),
_warning(NO_ORPHAN_DETAILS)
]
static func orphan_node_info(orphan_info: GdUnitOrphanNodeInfo) -> String:
var message := "<%s> Id:%s" % [
_colored(orphan_info._type, GdUnitEditorColorTheme.engine_type_color),
_colored(orphan_info._id, GdUnitEditorColorTheme.engine_type_color)
]
if orphan_info._stack_element == null:
message += _warning("\n\tNo source info available")
return message
static func fuzzer_interuped(iterations: int, error: String) -> String:
return "%s %s %s\n %s" % [
_error("Found an error after"),
_colored_value(iterations + 1),
_error("test iterations"),
error]
static func test_timeout(timeout :int) -> String:
return "%s\n %s" % [_error("Timeout !"), _colored_value("Test timed out after %s" % LocalTime.elapsed(timeout))]
static func test_session_terminated() -> String:
return "%s" % _error("Test Session Terminated")
# gdlint:disable = mixed-tabs-and-spaces
static func test_suite_skipped(hint :String, skip_count :int) -> String:
return """
%s
Skipped %s tests
Reason: %s
""".dedent().trim_prefix("\n")\
% [_error("The Entire test-suite is skipped!"), _colored_value(skip_count), _colored_value(hint)]
static func test_skipped(hint :String) -> String:
return """
%s
Reason: %s
""".dedent().trim_prefix("\n")\
% [_error("This test is skipped!"), _colored_value(hint)]
static func error_not_implemented() -> String:
return _error("Test not implemented!")
static func error_with_message(message: String) -> String:
return _error(message)
static func error_is_null(current :Variant) -> String:
return "%s %s but was %s" % [_error("Expecting:"), _colored_value(null), _colored_value(current)]
static func error_is_not_null() -> String:
return "%s %s" % [_error("Expecting: not to be"), _colored_value(null)]
static func error_equal(current :Variant, expected :Variant, index_reports :Array = []) -> String:
var report := """
%s
%s
but was
%s""".dedent().trim_prefix("\n") % [_error("Expecting:"), _colored_value(expected), _colored_value(current)]
if not index_reports.is_empty():
report += "\n\n%s\n%s" % [_error("Differences found:"), _index_report_as_table(index_reports)]
return report
static func error_not_equal(current :Variant, expected :Variant) -> String:
return "%s\n %s\n not equal to\n %s" % [_error("Expecting:"), _colored_value(expected), _colored_value(current)]
static func error_not_equal_case_insensetiv(current :Variant, expected :Variant) -> String:
return "%s\n %s\n not equal to (case insensitiv)\n %s" % [
_error("Expecting:"), _colored_value(expected), _colored_value(current)]
static func error_is_empty(current :Variant) -> String:
return "%s\n must be empty but was\n %s" % [_error("Expecting:"), _colored_value(current)]
static func error_is_not_empty() -> String:
return "%s\n must not be empty" % [_error("Expecting:")]
static func error_is_same(current :Variant, expected :Variant) -> String:
return "%s\n %s\n to refer to the same object\n %s" % [_error("Expecting:"), _colored_value(expected), _colored_value(current)]
@warning_ignore("unused_parameter")
static func error_not_same(_current :Variant, expected :Variant) -> String:
return "%s\n %s" % [_error("Expecting not same:"), _colored_value(expected)]
static func error_not_same_error(current :Variant, expected :Variant) -> String:
return "%s\n %s\n but was\n %s" % [_error("Expecting error message:"), _colored_value(expected), _colored_value(current)]
static func error_is_instanceof(current: GdUnitResult, expected :GdUnitResult) -> String:
return "%s\n %s\n But it was %s" % [_error("Expected instance of:"),\
_colored_value(expected.or_else(null)), _colored_value(current.or_else(null))]
# -- Boolean Assert specific messages -----------------------------------------------------
static func error_is_true(current :Variant) -> String:
return "%s %s but is %s" % [_error("Expecting:"), _colored_value(true), _colored_value(current)]
static func error_is_false(current :Variant) -> String:
return "%s %s but is %s" % [_error("Expecting:"), _colored_value(false), _colored_value(current)]
# - Integer/Float Assert specific messages -----------------------------------------------------
static func error_is_even(current :Variant) -> String:
return "%s\n %s must be even" % [_error("Expecting:"), _colored_value(current)]
static func error_is_odd(current :Variant) -> String:
return "%s\n %s must be odd" % [_error("Expecting:"), _colored_value(current)]
static func error_is_negative(current :Variant) -> String:
return "%s\n %s be negative" % [_error("Expecting:"), _colored_value(current)]
static func error_is_not_negative(current :Variant) -> String:
return "%s\n %s be not negative" % [_error("Expecting:"), _colored_value(current)]
static func error_is_zero(current :Variant) -> String:
return "%s\n equal to 0 but is %s" % [_error("Expecting:"), _colored_value(current)]
static func error_is_not_zero() -> String:
return "%s\n not equal to 0" % [_error("Expecting:")]
static func error_is_wrong_type(current_type :Variant.Type, expected_type :Variant.Type) -> String:
return "%s\n Expecting type %s but is %s" % [
_error("Unexpected type comparison:"),
_colored_value(GdObjects.type_as_string(current_type)),
_colored_value(GdObjects.type_as_string(expected_type))]
static func error_is_value(operation :int, current :Variant, expected :Variant, expected2 :Variant = null) -> String:
match operation:
Comparator.EQUAL:
return "%s\n %s but was %s" % [_error("Expecting:"), _colored_value(expected), _colored_value(current)]
Comparator.LESS_THAN:
return "%s\n %s but was %s" % [_error("Expecting to be less than:"), _colored_value(expected), _colored_value(current)]
Comparator.LESS_EQUAL:
return "%s\n %s but was %s" % [_error("Expecting to be less than or equal:"), _colored_value(expected), _colored_value(current)]
Comparator.GREATER_THAN:
return "%s\n %s but was %s" % [_error("Expecting to be greater than:"), _colored_value(expected), _colored_value(current)]
Comparator.GREATER_EQUAL:
return "%s\n %s but was %s" % [_error("Expecting to be greater than or equal:"), _colored_value(expected), _colored_value(current)]
Comparator.BETWEEN_EQUAL:
return "%s\n %s\n in range between\n %s <> %s" % [
_error("Expecting:"), _colored_value(current), _colored_value(expected), _colored_value(expected2)]
Comparator.NOT_BETWEEN_EQUAL:
return "%s\n %s\n not in range between\n %s <> %s" % [
_error("Expecting:"), _colored_value(current), _colored_value(expected), _colored_value(expected2)]
return "TODO create expected message"
static func error_is_in(current :Variant, expected :Array) -> String:
return "%s\n %s\n is in\n %s" % [_error("Expecting:"), _colored_value(current), _colored_value(str(expected))]
static func error_is_not_in(current :Variant, expected :Array) -> String:
return "%s\n %s\n is not in\n %s" % [_error("Expecting:"), _colored_value(current), _colored_value(str(expected))]
# - StringAssert ---------------------------------------------------------------------------------
static func error_equal_ignoring_case(current :Variant, expected :Variant) -> String:
return "%s\n %s\n but was\n %s (ignoring case)" % [_error("Expecting:"), _colored_value(expected), _colored_value(current)]
static func error_contains(current :Variant, expected :Variant) -> String:
return "%s\n %s\n do contains\n %s" % [_error("Expecting:"), _colored_value(current), _colored_value(expected)]
static func error_not_contains(current :Variant, expected :Variant) -> String:
return "%s\n %s\n not do contain\n %s" % [_error("Expecting:"), _colored_value(current), _colored_value(expected)]
static func error_contains_ignoring_case(current :Variant, expected :Variant) -> String:
return "%s\n %s\n contains\n %s\n (ignoring case)" % [_error("Expecting:"), _colored_value(current), _colored_value(expected)]
static func error_not_contains_ignoring_case(current :Variant, expected :Variant) -> String:
return "%s\n %s\n not do contains\n %s\n (ignoring case)" % [_error("Expecting:"), _colored_value(current), _colored_value(expected)]
static func error_starts_with(current :Variant, expected :Variant) -> String:
return "%s\n %s\n to start with\n %s" % [_error("Expecting:"), _colored_value(current), _colored_value(expected)]
static func error_ends_with(current :Variant, expected :Variant) -> String:
return "%s\n %s\n to end with\n %s" % [_error("Expecting:"), _colored_value(current), _colored_value(expected)]
static func error_has_length(current :Variant, expected: int, compare_operator :int) -> String:
@warning_ignore("unsafe_method_access")
var current_length :Variant = current.length() if current != null else null
match compare_operator:
Comparator.EQUAL:
return "%s\n %s but was %s in\n %s" % [
_error("Expecting size:"), _colored_value(expected), _nerror(current_length), _colored_value(current)]
Comparator.LESS_THAN:
return "%s\n %s but was %s in\n %s" % [
_error("Expecting size to be less than:"), _colored_value(expected), _nerror(current_length), _colored_value(current)]
Comparator.LESS_EQUAL:
return "%s\n %s but was %s in\n %s" % [
_error("Expecting size to be less than or equal:"), _colored_value(expected),
_nerror(current_length), _colored_value(current)]
Comparator.GREATER_THAN:
return "%s\n %s but was %s in\n %s" % [
_error("Expecting size to be greater than:"), _colored_value(expected),
_nerror(current_length), _colored_value(current)]
Comparator.GREATER_EQUAL:
return "%s\n %s but was %s in\n %s" % [
_error("Expecting size to be greater than or equal:"), _colored_value(expected),
_nerror(current_length), _colored_value(current)]
return "TODO create expected message"
# - ArrayAssert specific messgaes ---------------------------------------------------
static func error_arr_contains(current: Variant, expected: Variant, not_expect: Variant, not_found: Variant, by_reference: bool) -> String:
var failure_message := "Expecting contains SAME elements:" if by_reference else "Expecting contains elements:"
var error := "%s\n %s\n do contains (in any order)\n %s" % [
_error(failure_message), _colored_value(current), _colored_value(expected)]
if not is_empty(not_expect):
error += "\nbut some elements where not expected:\n %s" % _colored_value(not_expect)
if not is_empty(not_found):
var prefix := "but" if is_empty(not_expect) else "and"
error += "\n%s could not find elements:\n %s" % [prefix, _colored_value(not_found)]
return error
static func error_arr_contains_exactly(
current: Variant,
expected: Variant,
not_expect: Variant,
not_found: Variant, compare_mode: GdObjects.COMPARE_MODE) -> String:
var failure_message := (
"Expecting contains exactly elements:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST
else "Expecting contains SAME exactly elements:"
)
if is_empty(not_expect) and is_empty(not_found):
var arr_current: Array = current
var arr_expected: Array = expected
var diff := _find_first_diff(arr_current, arr_expected)
return "%s\n %s\n do contains (in same order)\n %s\n but has different order %s" % [
_error(failure_message), _colored_value(current), _colored_value(expected), diff]
var error := "%s\n %s\n do contains (in same order)\n %s" % [
_error(failure_message), _colored_value(current), _colored_value(expected)]
if not is_empty(not_expect):
error += "\nbut some elements where not expected:\n %s" % _colored_value(not_expect)
if not is_empty(not_found):
var prefix := "but" if is_empty(not_expect) else "and"
error += "\n%s could not find elements:\n %s" % [prefix, _colored_value(not_found)]
return error
static func error_arr_contains_exactly_in_any_order(
current: Variant,
expected: Variant,
not_expect: Variant,
not_found: Variant,
compare_mode: GdObjects.COMPARE_MODE) -> String:
var failure_message := (
"Expecting contains exactly elements:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST
else "Expecting contains SAME exactly elements:"
)
var error := "%s\n %s\n do contains exactly (in any order)\n %s" % [
_error(failure_message), _colored_value(current), _colored_value(expected)]
if not is_empty(not_expect):
error += "\nbut some elements where not expected:\n %s" % _colored_value(not_expect)
if not is_empty(not_found):
var prefix := "but" if is_empty(not_expect) else "and"
error += "\n%s could not find elements:\n %s" % [prefix, _colored_value(not_found)]
return error
static func error_arr_not_contains(current: Variant, expected: Variant, found: Variant, compare_mode: GdObjects.COMPARE_MODE) -> String:
var failure_message := "Expecting:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST else "Expecting SAME:"
var error := "%s\n %s\n do not contains\n %s" % [
_error(failure_message), _colored_value(current), _colored_value(expected)]
if not is_empty(found):
error += "\n but found elements:\n %s" % _colored_value(found)
return error
# - DictionaryAssert specific messages ----------------------------------------------
static func error_contains_keys(current :Array, expected :Array, keys_not_found :Array, compare_mode :GdObjects.COMPARE_MODE) -> String:
var failure := (
"Expecting contains keys:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST
else "Expecting contains SAME keys:"
)
return "%s\n %s\n to contains:\n %s\n but can't find key's:\n %s" % [
_error(failure), _colored_value(current), _colored_value(expected), _colored_value(keys_not_found)]
static func error_not_contains_keys(current :Array, expected :Array, keys_not_found :Array, compare_mode :GdObjects.COMPARE_MODE) -> String:
var failure := (
"Expecting NOT contains keys:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST
else "Expecting NOT contains SAME keys"
)
return "%s\n %s\n do not contains:\n %s\n but contains key's:\n %s" % [
_error(failure), _colored_value(current), _colored_value(expected), _colored_value(keys_not_found)]
static func error_contains_key_value(key :Variant, value :Variant, current_value :Variant, compare_mode :GdObjects.COMPARE_MODE) -> String:
var failure := (
"Expecting contains key and value:" if compare_mode == GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST
else "Expecting contains SAME key and value:"
)
return "%s\n %s : %s\n but contains\n %s : %s" % [
_error(failure), _colored_value(key), _colored_value(value), _colored_value(key), _colored_value(current_value)]
# - ResultAssert specific errors ----------------------------------------------------
static func error_result_is_empty(current :GdUnitResult) -> String:
return _result_error_message(current, GdUnitResult.EMPTY)
static func error_result_is_success(current :GdUnitResult) -> String:
return _result_error_message(current, GdUnitResult.SUCCESS)
static func error_result_is_warning(current :GdUnitResult) -> String:
return _result_error_message(current, GdUnitResult.WARN)
static func error_result_is_error(current :GdUnitResult) -> String:
return _result_error_message(current, GdUnitResult.ERROR)
static func error_result_has_message(current :String, expected :String) -> String:
return "%s\n %s\n but was\n %s." % [_error("Expecting:"), _colored_value(expected), _colored_value(current)]
static func error_result_has_message_on_success(expected :String) -> String:
return "%s\n %s\n but the GdUnitResult is a success." % [_error("Expecting:"), _colored_value(expected)]
static func error_result_is_value(current :Variant, expected :Variant) -> String:
return "%s\n %s\n but was\n %s." % [_error("Expecting to contain same value:"), _colored_value(expected), _colored_value(current)]
static func _result_error_message(current :GdUnitResult, expected_type :int) -> String:
if current == null:
return _error("Expecting the result must be a %s but was <null>." % result_type(expected_type))
if current.is_success():
return _error("Expecting the result must be a %s but was SUCCESS." % result_type(expected_type))
var error := "Expecting the result must be a %s but was %s:" % [result_type(expected_type), result_type(current._state)]
return "%s\n %s" % [_error(error), _colored_value(result_message(current))]
static func error_interrupted(func_name :String, expected :Variant, elapsed :String) -> String:
func_name = humanized(func_name)
if expected == null:
return "%s %s but timed out after %s" % [_error("Expected:"), func_name, elapsed]
return "%s %s %s but timed out after %s" % [_error("Expected:"), func_name, _colored_value(expected), elapsed]
static func error_wait_signal(signal_name :String, args :Array, elapsed :String) -> String:
if args.is_empty():
return "%s %s but timed out after %s" % [
_error("Expecting emit signal:"), _colored_value(signal_name + "()"), elapsed]
return "%s %s but timed out after %s" % [
_error("Expecting emit signal:"), _colored_value(signal_name + "(" + str(args) + ")"), elapsed]
static func error_signal_emitted(signal_name :String, args :Array, elapsed :String) -> String:
if args.is_empty():
return "%s %s but is emitted after %s" % [
_error("Expecting do not emit signal:"), _colored_value(signal_name + "()"), elapsed]
return "%s %s but is emitted after %s" % [
_error("Expecting do not emit signal:"), _colored_value(signal_name + "(" + str(args) + ")"), elapsed]
static func error_await_signal_on_invalid_instance(source :Variant, signal_name :String, args :Array) -> String:
return "%s\n await_signal_on(%s, %s, %s)" % [
_error("Invalid source! Can't await on signal:"), _colored_value(source), signal_name, args]
static func result_type(type :int) -> String:
match type:
GdUnitResult.SUCCESS: return "SUCCESS"
GdUnitResult.WARN: return "WARNING"
GdUnitResult.ERROR: return "ERROR"
GdUnitResult.EMPTY: return "EMPTY"
return "UNKNOWN"
static func result_message(result :GdUnitResult) -> String:
match result._state:
GdUnitResult.SUCCESS: return ""
GdUnitResult.WARN: return result.warn_message()
GdUnitResult.ERROR: return result.error_message()
GdUnitResult.EMPTY: return ""
return "UNKNOWN"
# -----------------------------------------------------------------------------------
# - Spy|Mock specific errors ----------------------------------------------------
static func error_no_more_interactions(summary :Dictionary) -> String:
var interactions := PackedStringArray()
for args :Array in summary.keys():
var times :int = summary[args]
@warning_ignore("return_value_discarded")
interactions.append(_format_arguments(args, times))
return "%s\n%s\n%s" % [_error("Expecting no more interactions!"), _error("But found interactions on:"), "\n".join(interactions)]
static func error_validate_interactions(current_interactions: Dictionary, expected_interactions: Dictionary) -> String:
var collected_interactions := PackedStringArray()
for args: Array in current_interactions.keys():
var times: int = current_interactions[args]
@warning_ignore("return_value_discarded")
collected_interactions.append(_format_arguments(args, times))
var arguments: Array = expected_interactions.keys()[0]
var interactions: int = expected_interactions.values()[0]
var expected_interaction := _format_arguments(arguments, interactions)
return "%s\n%s\n%s\n%s" % [
_error("Expecting interaction on:"), expected_interaction, _error("But found interactions on:"), "\n".join(collected_interactions)]
static func _format_arguments(args :Array, times :int) -> String:
var fname :String = args[0]
var fargs := args.slice(1) as Array
var typed_args := _to_typed_args(fargs)
var fsignature := _colored_value("%s(%s)" % [fname, ", ".join(typed_args)])
return " %s %d time's" % [fsignature, times]
static func _to_typed_args(args :Array) -> PackedStringArray:
var typed := PackedStringArray()
for arg :Variant in args:
@warning_ignore("return_value_discarded")
typed.append(_format_arg(arg) + " :" + GdObjects.type_as_string(typeof(arg)))
return typed
static func _format_arg(arg :Variant) -> String:
if arg is InputEvent:
var ie: InputEvent = arg
return input_event_as_text(ie)
return str(arg)
static func _find_first_diff(left :Array, right :Array) -> String:
for index in left.size():
var l :Variant = left[index]
var r :Variant = "<no entry>" if index >= right.size() else right[index]
if not GdObjects.equals(l, r):
return "at position %s\n '%s' vs '%s'" % [_colored_value(index), _typed_value(l), _typed_value(r)]
return ""
static func error_has_size(current :Variant, expected: int) -> String:
@warning_ignore("unsafe_method_access")
var current_size :Variant = null if current == null else current.size()
return "%s\n %s\n but was\n %s" % [_error("Expecting size:"), _colored_value(expected), _colored_value(current_size)]
static func error_contains_exactly(current: Array, expected: Array) -> String:
return "%s\n %s\n but was\n %s" % [_error("Expecting exactly equal:"), _colored_value(expected), _colored_value(current)]
static func format_chars(characters: PackedInt32Array, type: Color) -> PackedInt32Array:
if characters.size() == 0:# or characters[0] == 10:
return characters
# Replace each control character with its readable form
var formatted_text := characters.to_byte_array().get_string_from_utf32()
for control_char: String in CONTROL_CHARS:
var replace_text: String = CONTROL_CHARS[control_char]
formatted_text = formatted_text.replace(control_char, replace_text)
# Handle special ASCII control characters (0x00-0x1F, 0x7F)
var ascii_text := ""
for i in formatted_text.length():
var character := formatted_text[i]
var code := character.unicode_at(0)
if code < 0x20 and not CONTROL_CHARS.has(character): # Control characters not handled above
ascii_text += "<0x%02X>" % code
elif code == 0x7F: # DEL character
ascii_text += "<DEL>"
else:
ascii_text += character
var message := "[bgcolor=#%s][color=white]%s[/color][/bgcolor]" % [
type.to_html(),
ascii_text
]
var result := PackedInt32Array()
result.append_array(message.to_utf32_buffer().to_int32_array())
return result
static func format_invalid(value :String) -> String:
return "[bgcolor=#%s][color=with]%s[/color][/bgcolor]" % [SUB_COLOR.to_html(), value]
static func humanized(value :String) -> String:
return value.replace("_", " ")
static func build_failure_message(failure :String, additional_failure_message: String, custom_failure_message: String) -> String:
var message := failure if custom_failure_message.is_empty() else custom_failure_message
if additional_failure_message.is_empty():
return message
return """
%s
[color=LIME_GREEN][b]Additional info:[/b][/color]
%s""".dedent().trim_prefix("\n") % [message, additional_failure_message]
static func is_empty(value: Variant) -> bool:
var arry_value: Array = value
return arry_value != null and arry_value.is_empty()
@@ -0,0 +1 @@
uid://cdmds6thunddc
@@ -0,0 +1,57 @@
class_name GdAssertReports
extends RefCounted
const LAST_ERROR = "last_assert_error_message"
const LAST_ERROR_LINE = "last_assert_error_line"
static func report_success() -> void:
GdUnitSignals.instance().gdunit_set_test_failed.emit(false)
GdAssertReports.set_last_error_line_number(-1)
Engine.remove_meta(LAST_ERROR)
static func report_warning(message :String, line_number :int) -> void:
GdUnitSignals.instance().gdunit_set_test_failed.emit(false)
send_report(GdUnitReport.new().create(GdUnitReport.WARN, line_number, message))
static func report_error(error: GdUnitError) -> void:
GdUnitSignals.instance().gdunit_set_test_failed.emit(true)
GdAssertReports.set_last_error_line_number(error._line_number)
Engine.set_meta(LAST_ERROR, error._message)
Engine.set_meta("GD_TEST_FAILURE", true)
GdUnitThreadManager.get_current_context().get_execution_context().set_error(error)
# if we expect to fail we handle as success test
if _do_expect_assert_failing():
return
send_report(GdUnitReport.new().from_error(GdUnitReport.FAILURE, error))
static func reset_last_error_line_number() -> void:
Engine.remove_meta(LAST_ERROR_LINE)
static func set_last_error_line_number(line_number :int) -> void:
Engine.set_meta(LAST_ERROR_LINE, line_number)
static func get_last_error_line_number() -> int:
if Engine.has_meta(LAST_ERROR_LINE):
return Engine.get_meta(LAST_ERROR_LINE)
return -1
static func _do_expect_assert_failing() -> bool:
if Engine.has_meta(GdUnitConstants.EXPECT_ASSERT_REPORT_FAILURES):
return Engine.get_meta(GdUnitConstants.EXPECT_ASSERT_REPORT_FAILURES)
return false
static func current_failure() -> String:
return Engine.get_meta(LAST_ERROR)
static func send_report(report :GdUnitReport) -> void:
GdUnitThreadManager.get_current_context().get_execution_context().add_report(report)
@@ -0,0 +1 @@
uid://d2yux5f864g6e
@@ -0,0 +1,420 @@
class_name GdUnitArrayAssertImpl
extends GdUnitArrayAssert
var _base: GdUnitAssertImpl
var _current_value_provider: ValueProvider
var _type_check: bool
func _init(current: Variant, type_check := true) -> void:
_type_check = type_check
_current_value_provider = DefaultValueProvider.new(current)
_base = GdUnitAssertImpl.new(current)
# save the actual assert instance on the current thread context
GdUnitThreadManager.get_current_context().set_assert(self)
if not _validate_value_type(current):
@warning_ignore("return_value_discarded")
report_error("GdUnitArrayAssert inital error, unexpected type <%s>" % GdObjects.typeof_as_string(current))
func _notification(event: int) -> void:
if event == NOTIFICATION_PREDELETE:
if _base != null:
_base.notification(event)
_base = null
func report_success() -> GdUnitArrayAssert:
@warning_ignore("return_value_discarded")
_base.report_success()
return self
func report_error(error: String) -> GdUnitArrayAssert:
@warning_ignore("return_value_discarded")
_base.report_error(error)
return self
func override_failure_message(message: String) -> GdUnitArrayAssert:
@warning_ignore("return_value_discarded")
_base.override_failure_message(message)
return self
func append_failure_message(message: String) -> GdUnitArrayAssert:
@warning_ignore("return_value_discarded")
_base.append_failure_message(message)
return self
func _validate_value_type(value: Variant) -> bool:
return value == null or GdArrayTools.is_array_type(value)
func get_current_value() -> Variant:
return _current_value_provider.get_value()
func max_length(left: Variant, right: Variant) -> int:
var ls := str(left).length()
var rs := str(right).length()
return rs if ls < rs else ls
# gdlint: disable=function-name
func _toPackedStringArray(value: Variant) -> PackedStringArray:
if GdArrayTools.is_array_type(value):
@warning_ignore("unsafe_cast")
return PackedStringArray(value as Array)
return PackedStringArray([str(value)])
func _array_equals_div(current: Variant, expected: Variant, case_sensitive: bool = true) -> Array:
var current_value := _toPackedStringArray(current)
var expected_value := _toPackedStringArray(expected)
var index_report := Array()
for index in current_value.size():
var c := current_value[index]
if index < expected_value.size():
var e := expected_value[index]
if not GdObjects.equals(c, e, case_sensitive):
var length := max_length(c, e)
current_value[index] = GdAssertMessages.format_invalid(c.lpad(length))
expected_value[index] = e.lpad(length)
index_report.push_back({"index": index, "current": c, "expected": e})
else:
current_value[index] = GdAssertMessages.format_invalid(c)
index_report.push_back({"index": index, "current": c, "expected": "<N/A>"})
for index in range(current_value.size(), expected_value.size()):
var value := expected_value[index]
expected_value[index] = GdAssertMessages.format_invalid(value)
index_report.push_back({"index": index, "current": "<N/A>", "expected": value})
return [current_value, expected_value, index_report]
func _array_div(compare_mode: GdObjects.COMPARE_MODE, left: Array[Variant], right: Array[Variant], _same_order := false) -> Array[Variant]:
var not_expect := left.duplicate(true)
var not_found := right.duplicate(true)
for index_c in left.size():
var c: Variant = left[index_c]
for index_e in right.size():
var e: Variant = right[index_e]
if GdObjects.equals(c, e, true, compare_mode):
GdArrayTools.erase_value(not_expect, e)
GdArrayTools.erase_value(not_found, c)
break
return [not_expect, not_found]
func _contains(expected: Array, compare_mode: GdObjects.COMPARE_MODE) -> GdUnitArrayAssert:
var by_reference := compare_mode == GdObjects.COMPARE_MODE.OBJECT_REFERENCE
var current_value: Variant = get_current_value()
var expected_value: Variant = _extract_variadic_value(expected)
if not _validate_value_type(expected_value):
return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected_value))
if current_value == null:
return report_error(GdAssertMessages.error_arr_contains(current_value, expected_value, [], expected_value, by_reference))
@warning_ignore("unsafe_cast")
var diffs := _array_div(compare_mode, current_value as Array[Variant], expected_value as Array[Variant])
#var not_expect := diffs[0] as Array
var not_found: Array = diffs[1]
if not not_found.is_empty():
return report_error(GdAssertMessages.error_arr_contains(current_value, expected_value, [], not_found, by_reference))
return report_success()
func _contains_exactly(expected: Array, compare_mode: GdObjects.COMPARE_MODE) -> GdUnitArrayAssert:
var current_value: Variant = get_current_value()
var expected_value: Variant = _extract_variadic_value(expected)
if not _validate_value_type(expected_value):
return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected_value))
if current_value == null:
return report_error(GdAssertMessages.error_arr_contains_exactly(null, expected_value, [], expected_value, compare_mode))
# has same content in same order
if _is_equal(current_value, expected_value, true, compare_mode):
return report_success()
# check has same elements but in different order
if _is_equals_sorted(current_value, expected_value, true, compare_mode):
return report_error(GdAssertMessages.error_arr_contains_exactly(current_value, expected_value, [], [], compare_mode))
# find the difference
@warning_ignore("unsafe_cast")
var diffs := _array_div(compare_mode,
current_value as Array[Variant],
expected_value as Array[Variant],
GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST)
var not_expect: Array[Variant] = diffs[0]
var not_found: Array[Variant] = diffs[1]
return report_error(GdAssertMessages.error_arr_contains_exactly(current_value, expected_value, not_expect, not_found, compare_mode))
func _contains_exactly_in_any_order(expected: Array, compare_mode: GdObjects.COMPARE_MODE) -> GdUnitArrayAssert:
var current_value: Variant = get_current_value()
var expected_value: Variant = _extract_variadic_value(expected)
if not _validate_value_type(expected_value):
return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected_value))
if current_value == null:
return report_error(GdAssertMessages.error_arr_contains_exactly_in_any_order(current_value, expected_value, [],
expected_value, compare_mode))
# find the difference
@warning_ignore("unsafe_cast")
var diffs := _array_div(compare_mode, current_value as Array[Variant], expected_value as Array[Variant], false)
var not_expect: Array[Variant] = diffs[0]
var not_found: Array[Variant] = diffs[1]
if not_expect.is_empty() and not_found.is_empty():
return report_success()
return report_error(GdAssertMessages.error_arr_contains_exactly_in_any_order(current_value, expected_value, not_expect,
not_found, compare_mode))
func _not_contains(expected: Array, compare_mode: GdObjects.COMPARE_MODE) -> GdUnitArrayAssert:
var current_value: Variant = get_current_value()
var expected_value: Variant = _extract_variadic_value(expected)
if not _validate_value_type(expected_value):
return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected_value))
if current_value == null:
return report_error(GdAssertMessages.error_arr_contains_exactly_in_any_order(current_value, expected_value, [],
expected_value, compare_mode))
@warning_ignore("unsafe_cast")
var diffs := _array_div(compare_mode, current_value as Array[Variant], expected_value as Array[Variant])
var found: Array[Variant] = diffs[0]
@warning_ignore("unsafe_cast")
if found.size() == (current_value as Array).size():
return report_success()
@warning_ignore("unsafe_cast")
var diffs2 := _array_div(compare_mode, expected_value as Array[Variant], diffs[1] as Array[Variant])
return report_error(GdAssertMessages.error_arr_not_contains(current_value, expected_value, diffs2[0], compare_mode))
func is_null() -> GdUnitArrayAssert:
@warning_ignore("return_value_discarded")
_base.is_null()
return self
func is_not_null() -> GdUnitArrayAssert:
@warning_ignore("return_value_discarded")
_base.is_not_null()
return self
func is_equal(...expected: Array) -> GdUnitArrayAssert:
var current_value: Variant = get_current_value()
var expected_value: Variant= _extract_variadic_value(expected)
if not _validate_value_type(expected_value):
return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected_value))
if current_value == null and expected_value != null:
return report_error(GdAssertMessages.error_equal(null, expected_value))
if not _is_equal(current_value, expected_value):
var diff := _array_equals_div(current_value, expected_value)
var expected_as_list := GdArrayTools.as_string(diff[0], false)
var current_as_list := GdArrayTools.as_string(diff[1], false)
var index_report: Array = diff[2]
return report_error(GdAssertMessages.error_equal(expected_as_list, current_as_list, index_report))
return report_success()
# Verifies that the current Array is equal to the given one, ignoring case considerations.
func is_equal_ignoring_case(...expected: Array) -> GdUnitArrayAssert:
var current_value: Variant = get_current_value()
var expected_value: Variant = _extract_variadic_value(expected)
if not _validate_value_type(expected_value):
return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected_value))
if current_value == null and expected_value != null:
@warning_ignore("unsafe_cast")
return report_error(GdAssertMessages.error_equal(null, GdArrayTools.as_string(expected_value)))
if not _is_equal(current_value, expected_value, false):
@warning_ignore("unsafe_cast")
var diff := _array_equals_div(current_value, expected_value, true)
var expected_as_list := GdArrayTools.as_string(diff[0])
var current_as_list := GdArrayTools.as_string(diff[1])
var index_report: Array = diff[2]
return report_error(GdAssertMessages.error_equal(expected_as_list, current_as_list, index_report))
return report_success()
func is_not_equal(...expected: Array) -> GdUnitArrayAssert:
var current_value: Variant = get_current_value()
var expected_value: Variant = _extract_variadic_value(expected)
if not _validate_value_type(expected_value):
return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected_value))
if _is_equal(current_value, expected_value):
return report_error(GdAssertMessages.error_not_equal(current_value, expected_value))
return report_success()
func is_not_equal_ignoring_case(...expected: Array) -> GdUnitArrayAssert:
var current_value: Variant = get_current_value()
var expected_value: Variant = _extract_variadic_value(expected)
if not _validate_value_type(expected_value):
return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected_value))
if _is_equal(current_value, expected_value, false):
@warning_ignore("unsafe_cast")
var c := GdArrayTools.as_string(current_value as Array)
@warning_ignore("unsafe_cast")
var e := GdArrayTools.as_string(expected_value)
return report_error(GdAssertMessages.error_not_equal_case_insensetiv(c, e))
return report_success()
func is_empty() -> GdUnitArrayAssert:
var current_value: Variant = get_current_value()
@warning_ignore("unsafe_cast")
if current_value == null or (current_value as Array).size() > 0:
return report_error(GdAssertMessages.error_is_empty(current_value))
return report_success()
func is_not_empty() -> GdUnitArrayAssert:
var current_value: Variant = get_current_value()
@warning_ignore("unsafe_cast")
if current_value != null and (current_value as Array).size() == 0:
return report_error(GdAssertMessages.error_is_not_empty())
return report_success()
@warning_ignore("unused_parameter", "shadowed_global_identifier")
func is_same(expected: Variant) -> GdUnitArrayAssert:
if not _validate_value_type(expected):
return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected))
var current: Variant = get_current_value()
if not is_same(current, expected):
@warning_ignore("return_value_discarded")
report_error(GdAssertMessages.error_is_same(current, expected))
return self
func is_not_same(expected: Variant) -> GdUnitArrayAssert:
if not _validate_value_type(expected):
return report_error("ERROR: expected value: <%s>\n is not a Array Type!" % GdObjects.typeof_as_string(expected))
var current: Variant = get_current_value()
if is_same(current, expected):
@warning_ignore("return_value_discarded")
report_error(GdAssertMessages.error_not_same(current, expected))
return self
func has_size(expected: int) -> GdUnitArrayAssert:
var current_value: Variant = get_current_value()
@warning_ignore("unsafe_cast")
if current_value == null or (current_value as Array).size() != expected:
return report_error(GdAssertMessages.error_has_size(current_value, expected))
return report_success()
func contains(...expected: Array) -> GdUnitArrayAssert:
return _contains(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST)
func contains_exactly(...expected: Array) -> GdUnitArrayAssert:
return _contains_exactly(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST)
func contains_exactly_in_any_order(...expected: Array) -> GdUnitArrayAssert:
return _contains_exactly_in_any_order(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST)
func contains_same(...expected: Array) -> GdUnitArrayAssert:
return _contains(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE)
func contains_same_exactly(...expected: Array) -> GdUnitArrayAssert:
return _contains_exactly(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE)
func contains_same_exactly_in_any_order(...expected: Array) -> GdUnitArrayAssert:
return _contains_exactly_in_any_order(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE)
func not_contains(...expected: Array) -> GdUnitArrayAssert:
return _not_contains(expected, GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST)
func not_contains_same(...expected: Array) -> GdUnitArrayAssert:
return _not_contains(expected, GdObjects.COMPARE_MODE.OBJECT_REFERENCE)
func is_instanceof(expected: Variant) -> GdUnitAssert:
@warning_ignore("unsafe_method_access")
_base.is_instanceof(expected)
return self
func extract(func_name: String, ...func_args: Array) -> GdUnitArrayAssert:
var extracted_elements := Array()
var args: Array = _extract_variadic_value(func_args)
var extractor := GdUnitFuncValueExtractor.new(func_name, args)
var current: Variant = get_current_value()
if current == null:
_current_value_provider = DefaultValueProvider.new(null)
else:
for element: Variant in current:
extracted_elements.append(extractor.extract_value(element))
_current_value_provider = DefaultValueProvider.new(extracted_elements)
return self
func extractv(...extractors: Array) -> GdUnitArrayAssert:
var extracted_elements := Array()
var current: Variant = get_current_value()
if current == null:
_current_value_provider = DefaultValueProvider.new(null)
else:
for element: Variant in current:
var ev: Array[Variant] = []
for index: int in extractors.size():
var extractor: GdUnitValueExtractor = extractors[index]
ev.append(extractor.extract_value(element))
if extractors.size() > 1:
extracted_elements.append(GdUnitTuple.new.callv(ev))
else:
extracted_elements.append(ev[0])
_current_value_provider = DefaultValueProvider.new(extracted_elements)
return self
## Small helper to support the old expected arguments as single array and variadic arguments
func _extract_variadic_value(values: Variant) -> Variant:
@warning_ignore("unsafe_method_access")
if values != null and values.size() == 1 and GdArrayTools.is_array_type(values[0]):
return values[0]
return values
@warning_ignore("incompatible_ternary")
func _is_equal(
left: Variant,
right: Variant,
case_sensitive := true,
compare_mode := GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST) -> bool:
@warning_ignore("unsafe_cast")
return GdObjects.equals(
(left as Array) if GdArrayTools.is_array_type(left) else left,
(right as Array) if GdArrayTools.is_array_type(right) else right,
case_sensitive,
compare_mode
)
func _is_equals_sorted(
left: Variant,
right: Variant,
case_sensitive := true,
compare_mode := GdObjects.COMPARE_MODE.PARAMETER_DEEP_TEST) -> bool:
@warning_ignore("unsafe_cast")
return GdObjects.equals_sorted(
left as Array,
right as Array,
case_sensitive,
compare_mode)
@@ -0,0 +1 @@
uid://0kg74xgfwp6k
@@ -0,0 +1,78 @@
class_name GdUnitAssertImpl
extends GdUnitAssert
var _current :Variant
var _custom_failure_message :String = ""
var _additional_failure_message: String = ""
func _init(current :Variant) -> void:
_current = current
# save the actual assert instance on the current thread context
GdUnitThreadManager.get_current_context().set_assert(self)
GdAssertReports.reset_last_error_line_number()
func current_value() -> Variant:
return _current
func report_success() -> GdUnitAssert:
GdAssertReports.report_success()
return self
func report_error(failure :String, failure_line_number: int = -1) -> GdUnitAssert:
var stack_trace := GdUnitStackTrace.new()
var line_number := failure_line_number if failure_line_number != -1 else stack_trace.get_line_number()
GdAssertReports.set_last_error_line_number(line_number)
var failure_message := GdAssertMessages.build_failure_message(failure, _additional_failure_message, _custom_failure_message)
GdAssertReports.report_error(GdUnitError.new(failure_message, line_number, stack_trace))
return self
func do_fail_not_implemented() -> GdUnitAssert:
return report_error(GdAssertMessages.error_not_implemented())
func do_fail_with_message(message: String) -> GdUnitAssert:
return report_error(GdAssertMessages.error_with_message(message))
func override_failure_message(message: String) -> GdUnitAssert:
_custom_failure_message = message
return self
func append_failure_message(message: String) -> GdUnitAssert:
_additional_failure_message = message
return self
func is_null() -> GdUnitAssert:
var current :Variant = current_value()
if current != null:
return report_error(GdAssertMessages.error_is_null(current))
return report_success()
func is_not_null() -> GdUnitAssert:
var current :Variant = current_value()
if current == null:
return report_error(GdAssertMessages.error_is_not_null())
return report_success()
func is_equal(expected: Variant) -> GdUnitAssert:
var current: Variant = current_value()
if not GdObjects.equals(current, expected):
return report_error(GdAssertMessages.error_equal(current, expected))
return report_success()
func is_not_equal(expected: Variant) -> GdUnitAssert:
var current: Variant = current_value()
if GdObjects.equals(current, expected):
return report_error(GdAssertMessages.error_not_equal(current, expected))
return report_success()
@@ -0,0 +1 @@
uid://bdq1ga71d44gr
@@ -0,0 +1,39 @@
# Preloads all GdUnit assertions
class_name GdUnitAssertions
extends RefCounted
@warning_ignore("return_value_discarded")
func _init() -> void:
# preload all gdunit assertions to speedup testsuite loading time
# gdlint:disable=private-method-call
@warning_ignore_start("return_value_discarded")
GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitAssertImpl.gd")
GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitBoolAssertImpl.gd")
GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitStringAssertImpl.gd")
GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitIntAssertImpl.gd")
GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFloatAssertImpl.gd")
GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitVectorAssertImpl.gd")
GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitArrayAssertImpl.gd")
GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitDictionaryAssertImpl.gd")
GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFileAssertImpl.gd")
GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitObjectAssertImpl.gd")
GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitResultAssertImpl.gd")
GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFuncAssertImpl.gd")
GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitSignalAssertImpl.gd")
GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitFailureAssertImpl.gd")
GdUnitAssertions.__lazy_load("res://addons/gdUnit4/src/asserts/GdUnitGodotErrorAssertImpl.gd")
@warning_ignore_restore("return_value_discarded")
### We now load all used asserts and tool scripts into the cache according to the principle of "lazy loading"
### in order to noticeably reduce the loading time of the test suite.
# We go this hard way to increase the loading performance to avoid reparsing all the used scripts
# for more detailed info -> https://github.com/godotengine/godot/issues/67400
# gdlint:disable=function-name
static func __lazy_load(script_path :String) -> GDScript:
return ResourceLoader.load(script_path, "GDScript", ResourceLoader.CACHE_MODE_REUSE)
static func validate_value_type(value :Variant, type :Variant.Type) -> bool:
return value == null or typeof(value) == type
@@ -0,0 +1 @@
uid://d0y7ocrqr5ebt
Loaded 100 of 618 files, more files were not shown because too many files have changed in this diff. Show more