28 Commits

Author SHA1 Message Date
6b03d623a5 Merge pull request 'feature/whaletown-developer-extra-feature' (#19) from feature/whaletown-developer-extra-feature into main
Reviewed-on: datawhale/whale-town-front#19
2026-03-11 18:22:59 +08:00
f981ef18b1 chore(web): 更新导出后的 web_assets 产物 2026-03-11 18:12:04 +08:00
558a0ff9bc fix(chat): 调整输入交互并加固 WS/告警处理 2026-03-11 18:11:09 +08:00
dc403179f7 fix: tighten home collision footprint 2026-03-11 07:05:58 +08:00
dde92737b6 fix: refine board collisions and world text 2026-03-11 06:53:47 +08:00
7e65137922 fix: remove stray border tiles causing black line in square map 2026-03-11 01:42:32 +08:00
473c9f4692 Revert "ui: update auth scene layout"
This reverts commit 9d5536fd327d6c9c8c09d74675f755ad350450ff.
2026-03-11 00:50:43 +08:00
9d5536fd32 ui: update auth scene layout 2026-03-11 00:00:35 +08:00
3eef8c0814 art: refine crayfish npc sprite 2026-03-10 23:56:38 +08:00
fa38b75d8f feat: add crayfish npc to square map 2026-03-10 22:51:38 +08:00
64b9931eac Merge pull request 'fix/scene-auth-websocket-regressions' (#18) from fix/scene-auth-websocket-regressions into main
Reviewed-on: datawhale/whale-town-front#18
2026-03-10 16:38:35 +08:00
22f4ec5d76 chore:更新Web导出产物并新增本地服务脚本 2026-03-10 16:22:47 +08:00
23d63a9dda config:更新Web导出参数与本地化配置 2026-03-10 16:22:32 +08:00
0b0bdbacfa docs:补充接口文档并更新项目说明 2026-03-10 16:22:13 +08:00
8123bc6b22 chore:新增并完善 Godot CLI 测试技能 2026-03-10 16:18:17 +08:00
64850c2cae test:添加网络与认证回归测试脚本 2026-03-10 16:17:55 +08:00
d96abbb8b9 fix:修复重连状态与认证流程关键缺陷 2026-03-10 16:17:10 +08:00
88a8eaad02 Merge pull request 'config/dev/ignore-comment-files' (#17) from config/dev/ignore-comment-files into main
Reviewed-on: datawhale/whale-town-front#17
2026-02-08 12:09:59 +08:00
26479636ec chore: 调整版本号为4.6,并添加文件忽略 2026-02-08 12:06:00 +08:00
3d1973f284 chore: 忽略*.import和*.uid文件并移除已提交文件的追踪 2026-02-08 12:04:57 +08:00
ca6bf36be3 Merge pull request '修复demo第一次合并版本导致的Welcome/Notice 弹窗关闭与内容显示问题' (#16) from feature/whale-developer-merge into main
Reviewed-on: datawhale/whale-town-front#16
2026-02-08 11:53:11 +08:00
f24a01dbca fix: 修复交互与通知板问题
- 修复Z轴逻辑调整精灵与物品的排序
- 渲染前强制 content_container.modulate.a = 1.0,避免内容区被透明度状态卡住导致看起来空白
2026-02-07 21:07:59 +08:00
19794d896e fix: 修复欢迎板和通知板通知无法关闭问题
- 修复欢迎板和通知板当前被ChatUI 抢占鼠标问题,通过设置启动弹窗时ChatUI 根节点 mouse_filter 临时改为 IGNORE来实现
- ToDo: 后续统一规划事件逻辑
2026-02-07 20:01:48 +08:00
7fcb41d576 fix: 修复欢迎板和通知板图像引用错误问题 2026-02-07 19:33:06 +08:00
王浩
326ab7ce5c 合并主场景和个人小屋 2026-02-07 14:11:00 +08:00
WhaleTown Developer
603e7d9fc6 修bug 2026-01-20 20:11:07 +08:00
王浩
e989b4adf1 docs: 删除 chat_system.md 计划文档 2026-01-14 17:11:48 +08:00
王浩
6e70aac0b9 fix: 修复聊天消息显示问题
- AuthScene: 修复节点路径错误 (WhaleFrame, UsernameInput)
- ChatManager: 修复 timestamp 类型转换 (String -> float)
- ChatMessage: 修复节点引用获取方式和 UI 显示
- ChatUI: 优化消息列表布局对齐
2026-01-14 17:10:48 +08:00
407 changed files with 7673 additions and 24435 deletions

View File

@@ -0,0 +1,105 @@
---
name: godot-cli-test-runner
description: Run Godot CLI commands for this project with emphasis on headless test execution, script runs, scene runs, and export/debug operations. Use when the user asks to run Godot tests or commands (for example “Godot 跑测试”, “执行 Godot 命令”, “检查 Godot 参数”), troubleshoot CLI failures, or request reusable terminal/CI command templates.
---
# Godot CLI Test Runner
## Overview
Use deterministic Godot CLI workflows for Windows terminal and CI-style execution. Prefer `--headless`, explicit `--path`, and `--log-file` for reproducible diagnostics.
## Quick Decision
1. Parse-only check script syntax: `--headless --path . --script <file> --check-only`.
2. If test logic depends on autoload singletons (for example `SceneManager`, `LocationManager`, `EventSystem`), do not use direct `--script` as primary validation; use scene/project context first.
3. For isolated script tests without autoload dependencies, run `--headless --path . --script <file>`.
4. Export build by using `--export-release` / `--export-debug` with an existing preset.
5. Diagnose CLI behavior by adding `--verbose` and always writing `--log-file`.
## Workflow
### 1. Resolve executable and project path
1. Prefer `godot` from PATH.
2. If not available, use explicit exe path (for this machine typically `D:\technology\biancheng\Godot\Godot_v4.5.1-stable_win64_console.exe` or `D:\technology\biancheng\Godot\Godot_v4.5.1-stable_win64.exe`).
3. Run from repository root and always pass `--path .` unless intentionally targeting another project.
### 2. Preflight checks
1. Confirm engine version: `godot --version`.
2. Confirm options when needed: `godot --help`.
3. Confirm project exists: ensure `project.godot` is present under `--path`.
4. Read `project.godot` `[autoload]` and check whether the target test script references those singleton names.
5. Prepare a log output path (for example `.godot/test_xxx.log`) and pass `--log-file`.
### 3. Execute task type
1. Autoload-dependent validation (preferred when script references global singleton names):
`godot --headless --path . --scene res://scenes/MainScene.tscn --quit-after 120 --log-file .godot/smoke_main.log`
2. Scene-specific validation:
`godot --headless --path . --scene res://scenes/Maps/square.tscn --quit-after 90 --log-file .godot/smoke_square.log`
3. Script test (only for isolated logic or known SceneTree tests):
`godot --headless --path . --script tests/unit/test_xxx.gd --log-file .godot/test_xxx.log`
4. Script syntax only:
`godot --headless --path . --script tests/unit/test_xxx.gd --check-only --log-file .godot/check_xxx.log`
5. Export:
`godot --headless --path . --export-release "Web" web_assets/index.html --log-file .godot/export_web_release.log`
### 4. Capture and report results
1. Report exit code, key stdout/stderr lines, and failed command.
2. For failures, include one retry variant (for example add `--verbose`, switch explicit exe path, or switch from `--script` to `--scene` context).
3. Keep output concise and actionable.
4. If `--script` fails with missing singleton identifiers, mark it as context mismatch first, not business regression.
## Command Templates
### Windows (explicit exe)
```powershell
& "D:\technology\biancheng\Godot\Godot_v4.5.1-stable_win64_console.exe" --headless --path . --log-file .godot\test_websocket_close_code.log --script tests/unit/test_websocket_close_code.gd
```
### Generic (PATH)
```powershell
godot --headless --path . --log-file .godot/test_websocket_close_code.log --script tests/unit/test_websocket_close_code.gd
```
### With extra app args
```powershell
godot --headless --path . --log-file .godot/test_runner.log --script tests/unit/test_runner.gd -- --case websocket --timeout 30
```
## Minimal Runnable Examples
Run from repository root (`--path .`).
1. Run one scene-level smoke test (autoload-safe):
```powershell
godot --headless --path . --scene res://scenes/MainScene.tscn --quit-after 120 --log-file .godot/smoke_main.log
```
2. Run one test script (isolated logic):
```powershell
godot --headless --path . --script tests/unit/test_websocket_close_code.gd --log-file .godot/test_websocket_close_code.log
```
3. Run one scene:
```powershell
godot --headless --path . --scene res://scenes/SomeScene.tscn --quit-after 90 --log-file .godot/smoke_scene.log
```
4. Parse script only (syntax check):
```powershell
godot --headless --path . --script tests/unit/test_websocket_close_code.gd --check-only --log-file .godot/check_websocket.log
```
If `godot` is not in PATH, replace `godot` with explicit exe call:
```powershell
& "D:\technology\biancheng\Godot\Godot_v4.5.1-stable_win64.exe" <same arguments>
```
## Option Summary Reference
Use `references/godot-cli-commands.md` for categorized option summary and quick recipes based on `godot --help` output.
## Guardrails
1. Prefer non-interactive commands.
2. Prefer `--headless` for tests and scripts.
3. For this environment, include `--log-file` for reproducible logs and to avoid console build logging issues.
4. Avoid assuming GUT addon exists; check `addons/gut/gut_cmdline.gd` before using GUT command.
5. Use `--check-only` when user requests parse/syntax validation only.
6. For long-running runs, include `--quit-after` when appropriate.
7. Do not classify missing autoload singleton errors in `--script` mode as product regressions until scene/project-context validation is also run.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "Godot CLI Test Runner"
short_description: "Run Godot tests and command-line workflows"
default_prompt: "Use this skill to run Godot headless tests with autoload-aware strategy, check CLI options, and execute Godot commands with reproducible --log-file diagnostics."

View File

@@ -0,0 +1,74 @@
# Godot CLI Commands Summary
This reference summarizes the provided `godot --help` output for fast command selection.
## 1. Core inspection
- Help: `godot --help`
- Version: `godot --version`
- Verbose logs: `godot --verbose`
- Quiet mode: `godot --quiet`
## 2. Project targeting and run mode
- Point to project directory: `godot --path .`
- Run specific scene: `godot --path . --scene res://scenes/MainScene.tscn`
- Headless mode: `godot --headless --path . ...`
- Quit quickly: `godot --path . --quit`
- Quit after N frames: `godot --path . --quit-after 120`
## 3. Script execution and tests
- Run script: `godot --headless --path . --script tests/unit/test_xxx.gd`
- Parse-only script check: `godot --headless --path . --script tests/unit/test_xxx.gd --check-only`
- Pass custom user args to script:
`godot --headless --path . --script tests/unit/test_runner.gd -- --case websocket`
- Autoload-safe smoke test (preferred when test uses singleton globals):
`godot --headless --path . --scene res://scenes/MainScene.tscn --quit-after 120 --log-file .godot/smoke_main.log`
## 4. Debug and diagnostics
- Local debugger: `godot --debug --path .`
- Remote debug: `godot --remote-debug tcp://127.0.0.1:6007 --path .`
- Print FPS: `godot --path . --print-fps`
- Log to file: `godot --path . --log-file logs/godot.log`
- Disable VSync for profiling: `godot --path . --disable-vsync`
## 5. Display and runtime controls
- Fullscreen: `godot --path . --fullscreen`
- Windowed: `godot --path . --windowed`
- Resolution: `godot --path . --resolution 1920x1080`
- Max FPS: `godot --path . --max-fps 60`
- Fixed FPS: `godot --path . --fixed-fps 60`
- Time scale: `godot --path . --time-scale 0.5`
## 6. Export operations (editor build only)
- Release export:
`godot --path . --export-release "Web" build/web/index.html`
- Debug export:
`godot --path . --export-debug "Web" build/web/index.html`
- Pack export:
`godot --path . --export-pack "Web" build/web/game.pck`
- Check preset syntax only:
`godot --path . --export-debug "Web" --check-only`
## 7. Common quick recipes
- Run a unit test script (isolated logic):
`godot --headless --path . --script tests/unit/test_websocket_close_code.gd --log-file .godot/test_websocket_close_code.log`
- Validate script syntax without running:
`godot --headless --path . --script tests/unit/test_websocket_close_code.gd --check-only --log-file .godot/check_websocket.log`
- Run game with verbose logs:
`godot --verbose --path . --log-file .godot/main_verbose.log`
- Run scene and auto-exit after startup checks:
`godot --headless --path . --scene res://scenes/MainScene.tscn --quit-after 120 --log-file .godot/smoke_main.log`
## 8. Windows explicit executable pattern
When `godot` is not in PATH, call the executable directly:
```powershell
& "D:\technology\biancheng\Godot\Godot_v4.5.1-stable_win64.exe" --headless --path . --script tests/unit/test_websocket_close_code.gd
```
## 9. Notes for AI execution
- Prefer `--headless` for tests and scripts in terminal/CI.
- Always include `--path .` for reproducibility.
- Use `--check-only` for parse checks when execution is not needed.
- If a script depends on autoload singleton names from `project.godot` (`SceneManager`, `LocationManager`, `EventSystem`, etc.), validate in scene/project context before concluding regression.
- Prefer `--log-file` for reliable diagnostics and environment-specific logging issues.
- Add `--verbose` when failure context is insufficient.

3
.gitignore vendored
View File

@@ -56,3 +56,6 @@ coverage/
# Dependency directories
node_modules/
vendor/
*.uid
*.import

View File

@@ -6,6 +6,7 @@
},
"network": {
"api_base_url": "https://whaletownend.xinghangee.icu",
"location_ws_url": "wss://whaletownend.xinghangee.icu/game",
"timeout": 30,
"retry_count": 3
},

View File

@@ -1,36 +1,36 @@
{
"ui": {
"login": "登录",
"register": "注册",
"username": "用户名",
"password": "密码",
"email": "邮箱",
"confirm_password": "确认密码",
"verification_code": "验证码",
"send_code": "发送验证码",
"forgot_password": "忘记密码",
"enter_town": "进入小镇",
"logout": "退出登录"
"login": "登录",
"register": "注册",
"username": "用户名",
"password": "密码",
"email": "邮箱",
"confirm_password": "确认密码",
"verification_code": "验证码",
"send_code": "发送验证码",
"forgot_password": "忘记密码",
"enter_town": "进入小镇",
"logout": "退出登录"
},
"messages": {
"login_success": "登录成功!正在进入鲸鱼镇...",
"register_success": "注册成功!欢迎加入鲸鱼镇",
"network_error": "网络连接失败,请检查网络连接",
"invalid_username": "用户名只能包含字母、数字和下划线",
"invalid_email": "请输入有效的邮箱地址",
"password_too_short": "密码长度至少8位",
"password_mismatch": "两次输入的密码不一致",
"verification_code_sent": "验证码已发送到您的邮箱,请查收"
"login_success": "登录成功!正在进入鲸鱼镇...",
"register_success": "注册成功!欢迎加入鲸鱼镇",
"network_error": "网络连接失败,请检查网络连接",
"invalid_username": "用户名只能包含字母、数字和下划线",
"invalid_email": "请输入有效的邮箱地址",
"password_too_short": "密码长度至少8位",
"password_mismatch": "两次输入的密码不一致",
"verification_code_sent": "验证码已发送到您的邮箱,请查收"
},
"game": {
"level": "等级",
"coins": "金币",
"experience": "经验",
"energy": "体力",
"explore": "探索小镇",
"inventory": "背包",
"shop": "商店",
"friends": "好友",
"settings": "设置"
"level": "等级",
"coins": "金币",
"experience": "经验",
"energy": "体力",
"explore": "探索小镇",
"inventory": "背包",
"shop": "商店",
"friends": "好友",
"settings": "设置"
}
}

View File

@@ -155,9 +155,9 @@ WhaleTown/ # 🐋 项目根目录
│ ├── performance/ # ⚡ 性能测试(帧率、内存优化)
│ └── api/ # 🌐 API接口测试
└── 🌐 web_assets/ # 🌍 Web导出资源
├── html/ # 📄 HTML模板文件
├── css/ # 🎨 样式文件
└── js/ # 📜 JavaScript脚本
├── html/ # 📄 HTML模板文件
├── css/ # 🎨 样式文件
└── js/ # 📜 JavaScript脚本
```
### 🔧 核心架构说明

View File

@@ -1 +0,0 @@
uid://qn0imbklx1m0

View File

@@ -1 +0,0 @@
uid://dybcuscku7tyl

View File

@@ -68,6 +68,9 @@ enum LoginMode {
# ============ 成员变量 ============
# 当前用户ID静态变量用于BaseLevel等场景访问
static var current_user_id: String = ""
# 登录状态
var current_login_mode: LoginMode = LoginMode.PASSWORD
var is_processing: bool = false
@@ -91,15 +94,15 @@ var _refresh_token: String = "" # JWT刷新令牌长期用于获取
var _user_info: Dictionary = {} # 用户信息
var _token_expiry: float = 0.0 # access_token过期时间Unix时间戳
# 游戏 token兼容旧代码,保留但标记为废弃)
# 游戏 token废弃,保留供旧接口
var _game_token: String = "" # @deprecated 使用 _access_token 替代
# ============ 生命周期方法 ============
# 初始化管理器
func _init() -> void:
print("AuthManager 初始化完成")
_load_auth_data()
_connect_network_signals()
# 清理资源
func cleanup() -> void:
@@ -108,6 +111,8 @@ func cleanup() -> void:
NetworkManager.cancel_request(request_id)
active_request_ids.clear()
_disconnect_network_signals()
# ============ Token 管理 ============
# 保存 Token 到内存
@@ -121,7 +126,7 @@ func cleanup() -> void:
# - 保存用户信息
func _save_tokens_to_memory(data: Dictionary) -> void:
if not data.has("data"):
print("⚠️ 登录响应中没有 data 字段")
push_warning("AuthManager: 登录响应中没有 data 字段")
return
var token_data: Dictionary = data.data
@@ -130,12 +135,11 @@ func _save_tokens_to_memory(data: Dictionary) -> void:
_user_info = token_data.get("user", {})
_token_expiry = Time.get_unix_time_from_system() + float(token_data.get("expires_in", 0))
# 保持兼容性:设置 _game_token
_game_token = _access_token
# 设置当前用户ID用于BaseLevel等场景
if _user_info.has("id"):
AuthManager.current_user_id = str(_user_info.id)
print("✅ Token已保存到内存")
print(" Access Token: ", _access_token.substr(0, 20) + "...")
print(" 用户: ", _user_info.get("username", "未知"))
_game_token = _access_token
# 保存 Token 到本地ConfigFile
#
@@ -165,10 +169,8 @@ func _save_tokens_to_local(data: Dictionary) -> void:
config.set_value("auth", "saved_at", auth_data["saved_at"])
var error: Error = config.save(AUTH_CONFIG_PATH)
if error == OK:
print("✅ Token已保存到本地: ", AUTH_CONFIG_PATH)
else:
print("❌ 保存Token到本地失败错误码: ", error)
if error != OK:
push_error("AuthManager: 保存Token到本地失败错误码: %d" % error)
# 从本地加载 Token游戏启动时调用
#
@@ -177,14 +179,13 @@ func _save_tokens_to_local(data: Dictionary) -> void:
# - access_token 需要通过 refresh_token 刷新获取
func _load_auth_data() -> void:
if not FileAccess.file_exists(AUTH_CONFIG_PATH):
print(" 本地不存在认证数据")
return
var config: ConfigFile = ConfigFile.new()
var error: Error = config.load(AUTH_CONFIG_PATH)
if error != OK:
print(" 加载本地认证数据失败,错误码: ", error)
push_error("AuthManager: 加载本地认证数据失败,错误码: %d" % error)
return
_refresh_token = config.get_value("auth", "refresh_token", "")
@@ -196,10 +197,8 @@ func _load_auth_data() -> void:
"id": user_id,
"username": username
}
print("✅ 已从本地加载认证数据")
print(" 用户: ", username)
else:
print("⚠️ 本地认证数据无效(没有 refresh_token")
push_warning("AuthManager: 本地认证数据无效(没有 refresh_token")
# 清除本地认证数据(登出时调用)
#
@@ -215,11 +214,10 @@ func _clear_auth_data() -> void:
if FileAccess.file_exists(AUTH_CONFIG_PATH):
DirAccess.remove_absolute(AUTH_CONFIG_PATH)
print("✅ 已清除本地认证数据")
# ============ Token 访问方法 ============
# 设置游戏 token兼容旧代码,推荐使用 _save_tokens_to_memory
# 设置游戏 token建议优先使用 _save_tokens_to_memory
#
# 参数:
# token: String - 游戏认证 token
@@ -230,7 +228,6 @@ func _clear_auth_data() -> void:
func set_game_token(token: String) -> void:
_game_token = token
_access_token = token # 同步更新 access_token
print("AuthManager: 游戏 token 已设置")
# 获取游戏 token
#
@@ -681,7 +678,7 @@ func _on_send_login_code_response(success: bool, data: Dictionary, error_info: D
func _on_forgot_password_response(success: bool, data: Dictionary, error_info: Dictionary):
button_state_changed.emit("forgot_password_btn", false, "忘记密码")
var result = ResponseHandler.handle_send_login_code_response(success, data, error_info)
var result = ResponseHandler.handle_forgot_password_response(success, data, error_info)
if result.should_show_toast:
show_toast_message.emit(result.message, result.success)
@@ -720,10 +717,10 @@ func _can_send_verification_code(email: String) -> bool:
if not email_data.sent:
return true
var current_time = Time.get_time_dict_from_system()
var current_timestamp = current_time.hour * 3600 + current_time.minute * 60 + current_time.second
var current_timestamp: int = int(Time.get_unix_time_from_system())
var sent_timestamp: int = int(email_data.get("time", 0))
return (current_timestamp - email_data.time) >= code_cooldown
return float(current_timestamp - sent_timestamp) >= code_cooldown
# 获取剩余冷却时间
func get_remaining_cooldown_time(email: String) -> int:
@@ -731,15 +728,15 @@ func get_remaining_cooldown_time(email: String) -> int:
return 0
var email_data = verification_codes_sent[email]
var current_time = Time.get_time_dict_from_system()
var current_timestamp = current_time.hour * 3600 + current_time.minute * 60 + current_time.second
var current_timestamp: int = int(Time.get_unix_time_from_system())
var sent_timestamp: int = int(email_data.get("time", 0))
var remaining: int = int(code_cooldown - float(current_timestamp - sent_timestamp))
return int(code_cooldown - (current_timestamp - email_data.time))
return maxi(0, remaining)
# 记录验证码发送状态
func _record_verification_code_sent(email: String):
var current_time = Time.get_time_dict_from_system()
var current_timestamp = current_time.hour * 3600 + current_time.minute * 60 + current_time.second
var current_timestamp: int = int(Time.get_unix_time_from_system())
if not verification_codes_sent.has(email):
verification_codes_sent[email] = {}
@@ -764,6 +761,30 @@ func _has_sent_verification_code(email: String) -> bool:
func _is_valid_identifier(identifier: String) -> bool:
return StringUtils.is_valid_email(identifier) or _is_valid_phone(identifier)
# 连接/断开 NetworkManager 请求信号,用于回收 active_request_ids
func _connect_network_signals() -> void:
if not NetworkManager.request_completed.is_connected(_on_network_request_completed):
NetworkManager.request_completed.connect(_on_network_request_completed)
if not NetworkManager.request_failed.is_connected(_on_network_request_failed):
NetworkManager.request_failed.connect(_on_network_request_failed)
func _disconnect_network_signals() -> void:
if NetworkManager.request_completed.is_connected(_on_network_request_completed):
NetworkManager.request_completed.disconnect(_on_network_request_completed)
if NetworkManager.request_failed.is_connected(_on_network_request_failed):
NetworkManager.request_failed.disconnect(_on_network_request_failed)
func _on_network_request_completed(request_id: String, _success: bool, _data: Dictionary) -> void:
_remove_active_request_id(request_id)
func _on_network_request_failed(request_id: String, _error_type: String, _message: String) -> void:
_remove_active_request_id(request_id)
func _remove_active_request_id(request_id: String) -> void:
var index: int = active_request_ids.find(request_id)
if index != -1:
active_request_ids.remove_at(index)
# 验证手机号格式
func _is_valid_phone(phone: String) -> bool:
var regex = RegEx.new()

View File

@@ -1 +0,0 @@
uid://bpdyraefv0yta

View File

@@ -99,7 +99,7 @@ const CHAT_ERROR_MESSAGES: Dictionary = {
# ============================================================================
# WebSocket 管理器
var _websocket_manager: WebSocketManager
var _websocket_manager: ChatWebSocketManager
# 是否已登录
var _is_logged_in: bool = false
@@ -122,16 +122,22 @@ var _current_map: String = ""
# 游戏 token
var _game_token: String = ""
# 发送后本地回显去重(避免服务端也回发导致重复显示)
const SELF_ECHO_DEDUPE_WINDOW: float = 10.0
var _pending_self_messages: Array[Dictionary] = []
# 空消息类型告警限频(避免日志刷屏)
const EMPTY_MESSAGE_TYPE_WARNING_INTERVAL: float = 10.0
var _last_empty_message_type_warning_at: float = -1000.0
# ============================================================================
# 生命周期方法
# ============================================================================
# 初始化
func _ready() -> void:
print("ChatManager 初始化完成")
# 创建 WebSocket 管理器
_websocket_manager = WebSocketManager.new()
_websocket_manager = ChatWebSocketManager.new()
add_child(_websocket_manager)
# 连接信号
@@ -155,7 +161,6 @@ func _exit_tree() -> void:
# ChatManager.set_game_token("your_game_token")
func set_game_token(token: String) -> void:
_game_token = token
print("ChatManager: 游戏 token 已设置")
# 获取游戏 token
#
@@ -174,13 +179,10 @@ func connect_to_chat_server() -> void:
push_warning("聊天服务器已连接")
return
print("=== ChatManager 开始连接 ===")
_websocket_manager.connect_to_game_server()
# 断开聊天服务器
func disconnect_from_chat_server() -> void:
print("=== ChatManager 断开连接 ===")
# 发送登出消息
if _is_logged_in:
var logout_data := {"type": "logout"}
@@ -240,7 +242,10 @@ func send_chat_message(content: String, scope: String = "local") -> void:
# 发送消息JSON 字符串)
var json_string := JSON.stringify(message_data)
_websocket_manager.send_message(json_string)
var send_err: Error = _websocket_manager.send_message(json_string)
if send_err != OK:
_handle_error("SEND_FAILED", "WebSocket send failed: %s" % error_string(send_err))
return
# 记录发送时间
_record_message_timestamp()
@@ -253,12 +258,27 @@ func send_chat_message(content: String, scope: String = "local") -> void:
"is_self": true
})
print("📤 发送聊天消息: ", content)
var now_timestamp: float = Time.get_unix_time_from_system()
# 记录待去重的“自己消息”(如果服务端也回发 chat_render则避免重复显示
_pending_self_messages.append({
"content": content,
"expires_at": now_timestamp + SELF_ECHO_DEDUPE_WINDOW
})
# 本地回显UI 目前只订阅 CHAT_MESSAGE_RECEIVED所以这里也发一次 received
chat_message_received.emit(_current_username, content, true, now_timestamp)
EventSystem.emit_event(EventNames.CHAT_MESSAGE_RECEIVED, {
"from_user": _current_username,
"content": content,
"show_bubble": true,
"timestamp": now_timestamp,
"is_self": true
})
# 消息发送完成回调
func _on_chat_message_sent(request_id: String, success: bool, data: Dictionary, error_info: Dictionary) -> void:
func _on_chat_message_sent(_request_id: String, success: bool, data: Dictionary, error_info: Dictionary) -> void:
if success:
print("✅ 消息发送成功: ", data)
var message_id: String = str(data.get("data", {}).get("id", ""))
var timestamp: float = Time.get_unix_time_from_system()
chat_message_sent.emit(message_id, timestamp)
@@ -267,7 +287,6 @@ func _on_chat_message_sent(request_id: String, success: bool, data: Dictionary,
"timestamp": timestamp
})
else:
print("❌ 消息发送失败: ", error_info)
_handle_error("SEND_FAILED", error_info.get("message", "发送失败"))
# 更新玩家位置
@@ -294,8 +313,6 @@ func update_player_position(x: float, y: float, map_id: String) -> void:
var json_string := JSON.stringify(position_data)
_websocket_manager.send_message(json_string)
print("📍 更新位置: (%.2f, %.2f) in %s" % [x, y, map_id])
# ============================================================================
# 公共 API - 频率限制
# ============================================================================
@@ -350,7 +367,6 @@ func get_message_history() -> Array[Dictionary]:
# 清空消息历史
func clear_message_history() -> void:
_message_history.clear()
print("🧹 清空消息历史")
# 重置当前会话(每次登录/重连时调用)
#
@@ -367,7 +383,6 @@ func reset_session() -> void:
_history_loading = false
_has_more_history = true
_oldest_message_timestamp = 0.0
print("🔄 重置聊天会话")
# 加载历史消息(按需从 Zulip 后端获取)
#
@@ -386,36 +401,26 @@ func reset_session() -> void:
# 注意:
# - 这是异步操作,需要通过 Zulip API 实现
# - 当前实现为占位符,需要后端 API 支持
func load_history(count: int = HISTORY_PAGE_SIZE) -> void:
func load_history(_count: int = HISTORY_PAGE_SIZE) -> void:
if _history_loading:
print("⏳ 历史消息正在加载中...")
return
if not _has_more_history:
print("📚 没有更多历史消息")
return
_history_loading = true
print("📜 开始加载历史消息,数量: ", count)
# TODO: 实现从 Zulip 后端获取历史消息
# NetworkManager.get_chat_history(_oldest_message_timestamp, count, _on_history_loaded)
# 临时实现:模拟历史消息加载(测试用)
# await get_tree().create_timer(1.0).timeout
# _on_history_loaded([])
# 历史消息加载完成回调
func _on_history_loaded(messages: Array) -> void:
_history_loading = false
if messages.is_empty():
_has_more_history = false
print("📚 没有更多历史消息")
return
print("📜 历史消息加载完成,数量: ", messages.size())
# 将历史消息插入到当前会话历史开头
for i in range(messages.size() - 1, -1, -1):
var message: Dictionary = messages[i]
@@ -427,6 +432,7 @@ func _on_history_loaded(messages: Array) -> void:
"content": message.get("content", ""),
"show_bubble": false,
"timestamp": message.get("timestamp", 0.0),
"is_self": (not _current_username.is_empty() and message.get("from_user", "") == _current_username),
"is_history": true # 标记为历史消息
})
@@ -451,8 +457,6 @@ func _connect_signals() -> void:
# 发送登录消息
func _send_login_message() -> void:
print("📤 发送登录消息...")
var login_data := {
"type": "login",
"token": _game_token
@@ -461,13 +465,8 @@ func _send_login_message() -> void:
var json_string := JSON.stringify(login_data)
_websocket_manager.send_message(json_string)
print(" Token: ", _game_token.left(20) + "..." if _game_token.length() > 20 else _game_token)
# 连接状态变化
func _on_connection_state_changed(state: int) -> void:
var state_names := ["DISCONNECTED", "CONNECTING", "CONNECTED", "RECONNECTING", "ERROR"]
print("📡 ChatManager: 连接状态变化 - ", state_names[state])
# 发射信号
chat_connection_state_changed.emit(state)
@@ -491,19 +490,31 @@ func _on_data_received(message: String) -> void:
var parse_result := json.parse(message)
if parse_result != OK:
print("ChatManager: JSON 解析失败 - ", message)
push_error("ChatManager: JSON 解析失败")
return
var data: Dictionary = json.data
var data_variant: Variant = json.data
if not (data_variant is Dictionary):
push_warning("ChatManager: 收到非对象消息,已忽略")
return
# 检查消息类型字段
var message_type: String = data.get("t", "")
var data: Dictionary = data_variant
# 兼容不同后端字段命名t / type
var message_type: String = str(data.get("t", data.get("type", ""))).strip_edges()
if message_type.is_empty():
_warn_empty_message_type_limited(data)
return
match message_type:
"connected":
pass
"login_success":
_handle_login_success(data)
"login_error":
_handle_login_error(data)
"chat":
_handle_chat_render(data)
"chat_sent":
_handle_chat_sent(data)
"chat_error":
@@ -512,14 +523,24 @@ func _on_data_received(message: String) -> void:
_handle_chat_render(data)
"position_updated":
_handle_position_updated(data)
"error":
_handle_error_response(data)
_:
print("⚠️ ChatManager: 未处理的消息类型 - ", message_type)
print(" 消息内容: ", data)
push_warning("ChatManager: 未处理的消息类型 %s" % message_type)
func _warn_empty_message_type_limited(data: Dictionary) -> void:
var now: float = Time.get_unix_time_from_system()
if now - _last_empty_message_type_warning_at < EMPTY_MESSAGE_TYPE_WARNING_INTERVAL:
return
_last_empty_message_type_warning_at = now
var payload_preview: String = JSON.stringify(data)
if payload_preview.length() > 180:
payload_preview = payload_preview.substr(0, 180) + "..."
push_warning("ChatManager: 收到未带消息类型的消息,已忽略 payload=%s" % payload_preview)
# 处理登录成功
func _handle_login_success(data: Dictionary) -> void:
print("✅ ChatManager: 登录成功")
_is_logged_in = true
_current_username = data.get("username", "")
_current_map = data.get("currentMap", "")
@@ -527,9 +548,6 @@ func _handle_login_success(data: Dictionary) -> void:
# 重置当前会话缓存(每次登录/重连都清空,重新开始接收消息)
reset_session()
print(" 用户名: ", _current_username)
print(" 地图: ", _current_map)
# 通过 EventSystem 广播Signal Up
EventSystem.emit_event(EventNames.CHAT_LOGIN_SUCCESS, {
"username": _current_username,
@@ -540,8 +558,6 @@ func _handle_login_success(data: Dictionary) -> void:
func _handle_login_error(data: Dictionary) -> void:
var error_message: String = data.get("message", "登录失败")
print("❌ ChatManager: 登录失败 - ", error_message)
_is_logged_in = false
# 通过 EventSystem 广播错误Signal Up
@@ -555,8 +571,6 @@ func _handle_chat_sent(data: Dictionary) -> void:
var message_id: String = str(data.get("messageId", ""))
var timestamp: float = data.get("timestamp", 0.0)
print("✅ 消息发送成功: ", message_id)
# 发射信号
chat_message_sent.emit(message_id, timestamp)
@@ -570,29 +584,39 @@ func _handle_chat_sent(data: Dictionary) -> void:
func _handle_chat_error(data: Dictionary) -> void:
var error_message: String = data.get("message", "消息发送失败")
print("❌ ChatManager: 聊天错误 - ", error_message)
# 通过 EventSystem 广播错误Signal Up
EventSystem.emit_event(EventNames.CHAT_ERROR_OCCURRED, {
"error_code": "CHAT_SEND_FAILED",
"message": error_message
})
# 处理接收到的聊天消息
# 处理接收到的聊天消息
func _handle_chat_render(data: Dictionary) -> void:
var from_user: String = data.get("from", "")
var content: String = data.get("txt", "")
var show_bubble: bool = data.get("bubble", false)
var timestamp: float = data.get("timestamp", 0.0)
# 兼容不同后端字段命名:
# - chat_render: {from, txt, bubble, timestamp}
# - chat: {content, scope, (可选 from/username/timestamp)}
var from_user: String = data.get("from", data.get("from_user", data.get("username", "")))
var content: String = data.get("txt", data.get("content", ""))
var show_bubble: bool = bool(data.get("bubble", data.get("show_bubble", false)))
print("📨 收到聊天消息: ", from_user, " -> ", content)
var timestamp: float = _parse_chat_timestamp_to_unix(data.get("timestamp", 0.0))
var is_self: bool = (not _current_username.is_empty() and from_user == _current_username)
if is_self and _consume_pending_self_message(content):
# 已经本地回显过,避免重复显示
return
# 如果服务端没带发送者信息,但内容匹配最近自己发送的消息,则认为是自己消息
if from_user.is_empty() and _consume_pending_self_message(content):
from_user = _current_username
is_self = true
# 添加到历史
_add_message_to_history({
"from_user": from_user,
"content": content,
"timestamp": timestamp,
"is_self": false
"is_self": is_self
})
# 发射信号
@@ -603,16 +627,49 @@ func _handle_chat_render(data: Dictionary) -> void:
"from_user": from_user,
"content": content,
"show_bubble": show_bubble,
"timestamp": timestamp
"timestamp": timestamp,
"is_self": is_self
})
# 解析聊天消息时间戳(兼容 unix 秒 / ISO 8601 字符串)
func _parse_chat_timestamp_to_unix(timestamp_raw: Variant) -> float:
if typeof(timestamp_raw) == TYPE_INT or typeof(timestamp_raw) == TYPE_FLOAT:
var ts := float(timestamp_raw)
return ts if ts > 0.0 else Time.get_unix_time_from_system()
var ts_str := str(timestamp_raw)
if ts_str.strip_edges().is_empty():
return Time.get_unix_time_from_system()
# 纯数字字符串(必须整串都是数字/小数点,避免把 ISO 字符串前缀 "2026" 误判成时间戳)
var numeric_regex := RegEx.new()
numeric_regex.compile("^\\s*-?\\d+(?:\\.\\d+)?\\s*$")
if numeric_regex.search(ts_str) != null:
var ts_num := float(ts_str)
return ts_num if ts_num > 0.0 else Time.get_unix_time_from_system()
# ISO 8601: 2026-01-19T15:15:43.930Z
var regex := RegEx.new()
regex.compile("(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})")
var result := regex.search(ts_str)
if result == null:
return Time.get_unix_time_from_system()
var utc_dict := {
"year": int(result.get_string(1)),
"month": int(result.get_string(2)),
"day": int(result.get_string(3)),
"hour": int(result.get_string(4)),
"minute": int(result.get_string(5)),
"second": int(result.get_string(6))
}
return Time.get_unix_time_from_datetime_dict(utc_dict)
# 处理位置更新成功
func _handle_position_updated(data: Dictionary) -> void:
var stream: String = data.get("stream", "")
var topic: String = data.get("topic", "")
print("✅ 位置更新成功: ", stream, " / ", topic)
# 发射信号
chat_position_updated.emit(stream, topic)
@@ -639,7 +696,7 @@ func _on_socket_error(error: String) -> void:
# 处理错误
func _handle_error(error_code: String, error_message: String) -> void:
print("ChatManager 错误: [", error_code, "] ", error_message)
push_error("ChatManager: [%s] %s" % [error_code, error_message])
# 获取用户友好的错误消息
var user_message: String = CHAT_ERROR_MESSAGES.get(error_code, error_message) as String
@@ -665,6 +722,24 @@ func _record_message_timestamp() -> void:
var current_time := Time.get_unix_time_from_system()
_message_timestamps.append(current_time)
# 消费一个待去重的“自己消息”(允许相同内容多次发送:每次消费一个)
func _consume_pending_self_message(content: String) -> bool:
var now := Time.get_unix_time_from_system()
# 先清理过期项
for i in range(_pending_self_messages.size() - 1, -1, -1):
var item: Dictionary = _pending_self_messages[i]
if float(item.get("expires_at", 0.0)) < now:
_pending_self_messages.remove_at(i)
# 再匹配内容
for i in range(_pending_self_messages.size() - 1, -1, -1):
if str(_pending_self_messages[i].get("content", "")) == content:
_pending_self_messages.remove_at(i)
return true
return false
# 添加消息到当前会话历史
func _add_message_to_history(message: Dictionary) -> void:
_message_history.append(message)

View File

@@ -1 +0,0 @@
uid://b6lnbss2i3pss

View File

@@ -57,7 +57,6 @@ var game_version: String = "1.0.0" # 游戏版本号
# 初始化游戏管理器
# 在节点准备就绪时调用,设置初始状态
func _ready():
print("GameManager 初始化完成")
change_state(GameState.AUTH) # 启动时进入认证状态
# ============ 状态管理方法 ============
@@ -81,9 +80,6 @@ func change_state(new_state: GameState):
previous_state = current_state
current_state = new_state
# 输出状态变更日志
print("游戏状态变更: ", GameState.keys()[previous_state], " -> ", GameState.keys()[current_state])
# 发送状态变更信号
game_state_changed.emit(new_state)
@@ -100,7 +96,7 @@ func get_current_state() -> GameState:
# GameState - 上一个游戏状态
#
# 使用场景:
# - 从暂停状态恢复时,返回到之前的状态
# - 从暂停状态恢复时,返回到暂停前状态
# - 错误处理时回退到安全状态
func get_previous_state() -> GameState:
return previous_state
@@ -114,14 +110,12 @@ func get_previous_state() -> GameState:
#
# 功能:
# - 存储当前登录用户信息
# - 输出用户设置日志
#
# 注意事项:
# - 用户登录成功后调用此方法
# - 用户登出时应传入空字符串
func set_current_user(username: String):
current_user = username
print("当前用户设置为: ", username)
# 获取当前登录用户
#

View File

@@ -1 +0,0 @@
uid://cd8fn73ysjxh8

View File

@@ -0,0 +1,220 @@
extends Node
# ============================================================================
# LocationManager.gd - 位置同步管理器
# ============================================================================
# 负责与后端 WebSocket 服务进行位置同步和多人会话管理
#
# 协议文档: new_docs/game_architecture_design.md
# 后端地址默认值: wss://whaletownend.xinghangee.icu/game
# 可通过以下方式覆盖:
# 1) 环境变量 WHALETOWN_LOCATION_WS_URL
# 2) Config/game_config.json 或 config/game_config.json 中的
# network.location_ws_url / network.game_ws_url
# ============================================================================
signal connected_to_server()
signal connection_closed()
signal connection_error()
signal session_joined(data: Dictionary)
signal user_joined(data: Dictionary)
signal user_left(data: Dictionary)
signal position_updated(data: Dictionary)
const DEFAULT_WS_URL: String = "wss://whaletownend.xinghangee.icu/game"
const WS_URL_ENV_KEY: String = "WHALETOWN_LOCATION_WS_URL"
const PING_INTERVAL = 25.0 # 秒
var _socket: WebSocketPeer
var _connected: bool = false
var _ping_timer: float = 0.0
var _auth_token: String = ""
var _connection_error_reported: bool = false
var _is_connecting: bool = false
var _ws_url: String = DEFAULT_WS_URL
func _ready():
_socket = WebSocketPeer.new()
process_mode = Node.PROCESS_MODE_ALWAYS # 保证暂停时也能处理网络
_ws_url = _resolve_ws_url()
func _process(delta):
_socket.poll()
var state = _socket.get_ready_state()
if state == WebSocketPeer.STATE_OPEN:
_connection_error_reported = false
_is_connecting = false
if not _connected:
_on_connected()
# 处理接收到的数据包
while _socket.get_available_packet_count() > 0:
var packet = _socket.get_packet()
_handle_packet(packet)
# 心跳处理
_ping_timer += delta
if _ping_timer >= PING_INTERVAL:
_send_heartbeat()
_ping_timer = 0.0
elif state == WebSocketPeer.STATE_CLOSED:
if _connected:
_on_disconnected()
elif _is_connecting and not _connection_error_reported:
var close_code := _socket.get_close_code()
var close_reason := _socket.get_close_reason()
push_warning(
"LocationManager: WebSocket 握手失败close_code=%d, reason=%s" % [close_code, close_reason]
)
connection_error.emit()
_connection_error_reported = true
_is_connecting = false
func connect_to_server():
var state: WebSocketPeer.State = _socket.get_ready_state()
if state == WebSocketPeer.STATE_OPEN or state == WebSocketPeer.STATE_CONNECTING:
return
_connection_error_reported = false
_is_connecting = true
var err = _socket.connect_to_url(_ws_url)
if err != OK:
push_error("LocationManager: WebSocket 连接请求失败url=%s, 错误码: %d" % [_ws_url, err])
connection_error.emit()
_connection_error_reported = true
_is_connecting = false
else:
# Godot WebSocket connect is non-blocking, wait for state change in _process
pass
func _resolve_ws_url() -> String:
var env_url: String = OS.get_environment(WS_URL_ENV_KEY).strip_edges()
if not env_url.is_empty():
return env_url
for config_path in ["res://Config/game_config.json", "res://config/game_config.json"]:
var config_url: String = _load_ws_url_from_config(config_path)
if not config_url.is_empty():
return config_url
return DEFAULT_WS_URL
func _load_ws_url_from_config(config_path: String) -> String:
if not FileAccess.file_exists(config_path):
return ""
var content: String = FileAccess.get_file_as_string(config_path)
if content.is_empty():
return ""
var json := JSON.new()
if json.parse(content) != OK:
push_warning("LocationManager: 读取配置失败 %s - %s" % [config_path, json.get_error_message()])
return ""
var data_variant: Variant = json.data
if not (data_variant is Dictionary):
return ""
var root: Dictionary = data_variant
var network_variant: Variant = root.get("network", {})
if not (network_variant is Dictionary):
return ""
var network_config: Dictionary = network_variant
var ws_url: String = str(network_config.get("location_ws_url", network_config.get("game_ws_url", ""))).strip_edges()
return ws_url
func close_connection():
_socket.close()
func set_auth_token(token: String):
_auth_token = token
# ============ 协议发送 ============
func send_packet(event: String, data: Dictionary):
if _socket.get_ready_state() != WebSocketPeer.STATE_OPEN:
return
var message = {
"event": event,
"data": data
}
var json_str = JSON.stringify(message)
_socket.put_packet(json_str.to_utf8_buffer())
func join_session(map_id: String, initial_pos: Vector2):
var data = {
"sessionId": map_id,
"initialPosition": {
"x": initial_pos.x,
"y": initial_pos.y,
"mapId": map_id
},
"token": _auth_token
}
send_packet("join_session", data)
func leave_session(map_id: String):
send_packet("leave_session", {"sessionId": map_id})
func send_position_update(map_id: String, pos: Vector2, anim_data: Dictionary = {}):
var data = {
"x": pos.x,
"y": pos.y,
"mapId": map_id,
"metadata": anim_data
}
if map_id == "":
push_warning("LocationManager: position_update 的 map_id 为空")
send_packet("position_update", data)
func _send_heartbeat():
send_packet("heartbeat", {"timestamp": Time.get_unix_time_from_system()})
# ============ 事件处理 ============
func _on_connected():
_connected = true
connected_to_server.emit()
func _on_disconnected():
_connected = false
connection_closed.emit()
func _handle_packet(packet: PackedByteArray):
var json_str = packet.get_string_from_utf8()
var json = JSON.new()
var err = json.parse(json_str)
if err != OK:
push_error("LocationManager: JSON 解析失败 - %s" % json.get_error_message())
return
var message = json.data
if not message is Dictionary or not message.has("event"):
return
var event = message["event"]
var data = message.get("data", {})
match event:
"session_joined":
session_joined.emit(data)
"user_joined":
user_joined.emit(data)
"user_left":
user_left.emit(data)
"position_update":
position_updated.emit(data)
"heartbeat_response":
pass # 静默处理
"error":
push_error("LocationManager: WebSocket 错误事件 - %s" % JSON.stringify(data))
_:
push_warning("LocationManager: 未处理的 WebSocket 事件 %s" % event)

View File

@@ -109,13 +109,6 @@ class RequestInfo:
var active_requests: Dictionary = {} # 存储所有活动请求 {request_id: RequestInfo}
var request_counter: int = 0 # 请求计数器用于生成唯一ID
# ============ 生命周期方法 ============
# 初始化网络管理器
# 在节点准备就绪时调用
func _ready():
print("NetworkManager 已初始化")
# ============ 公共API接口 ============
# 发送GET请求
@@ -193,14 +186,6 @@ func delete_request(endpoint: String, callback: Callable = Callable(), timeout:
#
# 回调函数签名:
# func callback(success: bool, data: Dictionary, error_info: Dictionary)
#
# 使用示例:
# NetworkManager.login("user@example.com", "password123", func(success, data, error):
# if success:
# print("登录成功: ", data)
# else:
# print("登录失败: ", error.message)
# )
func login(identifier: String, password: String, callback: Callable = Callable()) -> String:
var data = {
"identifier": identifier,
@@ -472,41 +457,24 @@ func send_request(endpoint: String, method: RequestType, headers: PackedStringAr
var godot_method = _convert_to_godot_method(method)
var error = http_request.request(full_url, headers, godot_method, body)
print("=== 发送网络请求 ===")
print("请求ID: ", request_id)
print("URL: ", full_url)
print("方法: ", RequestType.keys()[method])
print("Headers: ", headers)
print("Body: ", body if body.length() < 200 else body.substr(0, 200) + "...")
print("发送结果: ", error)
if error != OK:
print("请求发送失败,错误码: ", error)
push_error("NetworkManager: 请求发送失败,错误码: %d" % error)
_handle_request_error(request_id, ErrorType.NETWORK_ERROR, "网络请求发送失败: " + str(error))
return ""
return request_id
# 请求完成回调
func _on_request_completed(request_id: String, result: int, response_code: int,
headers: PackedStringArray, body: PackedByteArray):
print("=== 网络请求完成 ===")
print("请求ID: ", request_id)
print("结果: ", result)
print("状态码: ", response_code)
print("响应头: ", headers)
func _on_request_completed(request_id: String, _result: int, response_code: int,
_headers: PackedStringArray, body: PackedByteArray):
# 获取请求信息
if not active_requests.has(request_id):
print("警告: 未找到请求ID ", request_id)
push_warning("NetworkManager: 未找到请求ID %s" % request_id)
return
var _request_info = active_requests[request_id]
var response_text = body.get_string_from_utf8()
print("响应体长度: ", body.size(), " 字节")
print("响应内容: ", response_text if response_text.length() < 500 else response_text.substr(0, 500) + "...")
# 处理网络连接失败
if response_code == 0:
_handle_request_error(request_id, ErrorType.NETWORK_ERROR, "网络连接失败,请检查网络连接")
@@ -542,13 +510,11 @@ func _handle_response(request_id: String, response_code: int, data: Dictionary):
# 特殊情况206测试模式 - 根据API文档这是成功的测试模式响应
elif response_code == 206 and error_code == "TEST_MODE_ONLY":
is_success = true
print("🧪 测试模式响应: ", message)
# 201创建成功
elif response_code == 201:
is_success = true
if is_success:
print("✅ 请求成功: ", request_id)
# 发送成功信号
request_completed.emit(request_id, true, data)
@@ -556,8 +522,6 @@ func _handle_response(request_id: String, response_code: int, data: Dictionary):
if request_info.callback.is_valid():
request_info.callback.call(true, data, {})
else:
print("❌ 请求失败: ", request_id, " - HTTP:", response_code, " 错误码:", error_code, " 消息:", message)
# 确定错误类型
var error_type = _determine_error_type(response_code, error_code)
@@ -579,7 +543,7 @@ func _handle_response(request_id: String, response_code: int, data: Dictionary):
# 处理请求错误
func _handle_request_error(request_id: String, error_type: ErrorType, message: String):
print("❌ 请求错误: ", request_id, " - ", message)
push_error("NetworkManager: 请求错误 %s - %s" % [request_id, message])
# 发送错误信号
request_failed.emit(request_id, ErrorType.keys()[error_type], message)
@@ -639,8 +603,6 @@ func _cleanup_request(request_id: String):
# 从活动请求中移除
active_requests.erase(request_id)
print("🧹 清理请求: ", request_id)
# 转换请求方法
func _convert_to_godot_method(method: RequestType) -> HTTPClient.Method:
match method:
@@ -662,14 +624,12 @@ func _convert_to_godot_method(method: RequestType) -> HTTPClient.Method:
# 取消请求
func cancel_request(request_id: String) -> bool:
if active_requests.has(request_id):
print("🚫 取消请求: ", request_id)
_cleanup_request(request_id)
return true
return false
# 取消所有请求
func cancel_all_requests():
print("🚫 取消所有请求")
var request_ids = active_requests.keys()
for request_id in request_ids:
cancel_request(request_id)

View File

@@ -1 +0,0 @@
uid://dr7v30wheetca

View File

@@ -128,21 +128,15 @@ func handle_send_verification_code_response(success: bool, data: Dictionary, err
var error_code = data.get("error_code", "")
if error_code == "TEST_MODE_ONLY":
result.success = true
result.message = "🧪 测试模式:验证码已生成,请查看控制台"
result.message = "🧪 测试模式:验证码已生成"
result.toast_type = "success"
# 在控制台显示验证码
if data.has("data") and data.data.has("verification_code"):
print("🔑 测试模式验证码: ", data.data.verification_code)
result.message += "\n验证码: " + str(data.data.verification_code)
else:
result.success = true
result.message = "📧 验证码已发送到您的邮箱,请查收"
result.toast_type = "success"
# 开发环境下显示验证码
if data.has("data") and data.data.has("verification_code"):
print("🔑 开发环境验证码: ", data.data.verification_code)
else:
result = _handle_send_code_error(data, error_info)
@@ -156,18 +150,15 @@ func handle_send_login_code_response(success: bool, data: Dictionary, error_info
var error_code = data.get("error_code", "")
if error_code == "TEST_MODE_ONLY":
result.success = true
result.message = "测试模式:登录验证码已生成,请查看控制台"
result.message = "测试模式:登录验证码已生成"
result.toast_type = "success"
if data.has("data") and data.data.has("verification_code"):
print("测试模式登录验证码: ", data.data.verification_code)
result.message += "\n验证码: " + str(data.data.verification_code)
else:
result.success = true
result.message = "登录验证码已发送,请查收"
result.toast_type = "success"
if data.has("data") and data.data.has("verification_code"):
print("开发环境登录验证码: ", data.data.verification_code)
else:
result = _handle_send_login_code_error(data, error_info)
@@ -214,18 +205,15 @@ func handle_resend_email_verification_response(success: bool, data: Dictionary,
var error_code = data.get("error_code", "")
if error_code == "TEST_MODE_ONLY":
result.success = true
result.message = "🧪 测试模式:验证码已重新生成,请查看控制台"
result.message = "🧪 测试模式:验证码已重新生成"
result.toast_type = "success"
if data.has("data") and data.data.has("verification_code"):
print("🔑 测试模式重新发送验证码: ", data.data.verification_code)
result.message += "\n验证码: " + str(data.data.verification_code)
else:
result.success = true
result.message = "📧 验证码已重新发送到您的邮箱,请查收"
result.toast_type = "success"
if data.has("data") and data.data.has("verification_code"):
print("🔑 开发环境重新发送验证码: ", data.data.verification_code)
else:
result = _handle_resend_email_verification_error(data, error_info)
@@ -239,18 +227,15 @@ func handle_forgot_password_response(success: bool, data: Dictionary, error_info
var error_code = data.get("error_code", "")
if error_code == "TEST_MODE_ONLY":
result.success = true
result.message = "🧪 测试模式:重置验证码已生成,请查看控制台"
result.message = "🧪 测试模式:重置验证码已生成"
result.toast_type = "success"
if data.has("data") and data.data.has("verification_code"):
print("🔑 测试模式重置验证码: ", data.data.verification_code)
result.message += "\n验证码: " + str(data.data.verification_code)
else:
result.success = true
result.message = "📧 重置验证码已发送,请查收"
result.toast_type = "success"
if data.has("data") and data.data.has("verification_code"):
print("🔑 开发环境重置验证码: ", data.data.verification_code)
else:
result = _handle_forgot_password_error(data, error_info)

View File

@@ -1 +0,0 @@
uid://nseguk2ytiw6

View File

@@ -36,27 +36,21 @@ signal scene_change_started(scene_name: String)
# 场景状态
var current_scene_name: String = "" # 当前场景名称
var is_changing_scene: bool = false # 是否正在切换场景
var _next_scene_position: Variant = null # 下一个场景的初始位置 (Vector2 or null)
var _next_spawn_name: String = "" # 下一个场景的出生点名称 (String)
# 场景路径映射表
# 将场景名称映射到实际的文件路径
# 便于统一管理和修改场景路径
var scene_paths: Dictionary = {
"main": "res://scenes/MainScene.tscn", # 主场景 - 游戏入口
"auth": "res://scenes/ui/LoginWindow.tscn", # 认证场景 - 登录窗口
"game": "res://scenes/maps/game_scene.tscn", # 游戏场景 - 主要游戏内容
"battle": "res://scenes/maps/battle_scene.tscn", # 战斗场景 - 战斗系统
"inventory": "res://scenes/ui/InventoryWindow.tscn", # 背包界面
"shop": "res://scenes/ui/ShopWindow.tscn", # 商店界面
"settings": "res://scenes/ui/SettingsWindow.tscn" # 设置界面
"square": "res://scenes/Maps/square.tscn", # 广场地图
"room": "res://scenes/Maps/room.tscn", # 房间地图
"fountain": "res://scenes/Maps/fountain.tscn", # 喷泉地图
"datawhale_home": "res://scenes/Maps/datawhale_home.tscn", # 数据鲸鱼之家
"community": "res://scenes/Maps/community.tscn" # 社区地图
}
# ============ 生命周期方法 ============
# 初始化场景管理器
# 在节点准备就绪时调用
func _ready():
print("SceneManager 初始化完成")
# ============ 场景切换方法 ============
# 切换到指定场景
@@ -77,8 +71,6 @@ func _ready():
#
# 使用示例:
# var success = SceneManager.change_scene("main", true)
# if success:
# print("场景切换成功")
#
# 注意事项:
# - 场景切换是异步操作
@@ -87,16 +79,15 @@ func _ready():
func change_scene(scene_name: String, use_transition: bool = true):
# 防止重复切换
if is_changing_scene:
print("场景切换中,忽略新的切换请求")
push_warning("SceneManager: 场景切换中,忽略新的切换请求")
return false
# 检查场景是否存在
if not scene_paths.has(scene_name):
print("错误: 未找到场景 ", scene_name)
push_error("SceneManager: 未找到场景 %s" % scene_name)
return false
var scene_path = scene_paths[scene_name]
print("开始切换场景: ", current_scene_name, " -> ", scene_name)
# 设置切换状态
is_changing_scene = true
@@ -109,7 +100,7 @@ func change_scene(scene_name: String, use_transition: bool = true):
# 执行场景切换
var error = get_tree().change_scene_to_file(scene_path)
if error != OK:
print("场景切换失败: ", error)
push_error("SceneManager: 场景切换失败 %s -> %s, 错误码: %d" % [current_scene_name, scene_name, error])
is_changing_scene = false
return false
@@ -122,7 +113,6 @@ func change_scene(scene_name: String, use_transition: bool = true):
if use_transition:
await hide_transition()
print("场景切换完成: ", scene_name)
return true
# ============ 查询方法 ============
@@ -134,6 +124,72 @@ func change_scene(scene_name: String, use_transition: bool = true):
func get_current_scene_name() -> String:
return current_scene_name
# ============ 场景位置和出生点管理 ============
# 设置下一个场景的初始位置
#
# 参数:
# pos: Vector2 - 玩家在下一个场景的初始位置
#
# 功能:
# - 用于场景切换时传递玩家位置信息
# - 配合 DoorTeleport 等传送机制使用
#
# 使用示例:
# SceneManager.set_next_scene_position(Vector2(100, 200))
# SceneManager.change_scene("room")
func set_next_scene_position(pos: Vector2) -> void:
_next_scene_position = pos
# 获取并清除下一个场景的初始位置
#
# 返回值:
# Variant - Vector2 位置或 null如果未设置
#
# 功能:
# - 获取预设的场景初始位置
# - 获取后自动清除,避免影响后续场景切换
#
# 注意事项:
# - 此方法会清除存储的位置,只能获取一次
# - 如果未设置位置,返回 null
func get_next_scene_position() -> Variant:
var pos = _next_scene_position
_next_scene_position = null
return pos
# 设置下一个场景的出生点名称
#
# 参数:
# spawn_name: String - 出生点节点的名称
#
# 功能:
# - 指定玩家在下一个场景应该出现在哪个出生点
# - 配合场景中的 Marker2D 出生点使用
#
# 使用示例:
# SceneManager.set_next_spawn_name("DoorExit")
# SceneManager.change_scene("square")
func set_next_spawn_name(spawn_name: String) -> void:
_next_spawn_name = spawn_name
# 获取并清除下一个场景的出生点名称
#
# 返回值:
# String - 出生点名称(如果未设置则返回空字符串)
#
# 功能:
# - 获取预设的出生点名称
# - 获取后自动清除,避免影响后续场景切换
#
# 注意事项:
# - 此方法会清除存储的名称,只能获取一次
# - 如果未设置名称,返回空字符串
func get_next_spawn_name() -> String:
var spawn_name: String = _next_spawn_name
_next_spawn_name = ""
return spawn_name
# ============ 场景注册方法 ============
# 注册新场景
@@ -150,7 +206,6 @@ func get_current_scene_name() -> String:
# SceneManager.register_scene("boss_battle", "res://scenes/boss/boss_battle.tscn")
func register_scene(scene_name: String, scene_path: String):
scene_paths[scene_name] = scene_path
print("注册场景: ", scene_name, " -> ", scene_path)
# ============ 过渡效果方法 ============
@@ -166,8 +221,7 @@ func register_scene(scene_name: String, scene_path: String):
#
# TODO: 实现淡入淡出、滑动等过渡效果
func show_transition():
# TODO: 实现场景切换过渡效果
print("显示场景切换过渡效果")
# TODO: 实现场景切换过渡效果(当前仅占位延时)
await get_tree().create_timer(0.2).timeout
# 隐藏场景切换过渡效果
@@ -182,6 +236,5 @@ func show_transition():
#
# TODO: 实现与show_transition()对应的隐藏效果
func hide_transition():
# TODO: 隐藏场景切换过渡效果
print("隐藏场景切换过渡效果")
# TODO: 隐藏场景切换过渡效果(当前仅占位延时)
await get_tree().create_timer(0.2).timeout

View File

@@ -1 +0,0 @@
uid://d3l286ti5gqhw

View File

@@ -40,7 +40,6 @@ var toast_counter: int = 0 # Toast计数器用于生成
# container: Control - Toast消息的容器节点
func setup(container: Control):
toast_container = container
print("ToastManager 初始化完成")
# ============ 公共方法 ============
@@ -51,10 +50,9 @@ func setup(container: Control):
# is_success: bool - 是否为成功消息(影响颜色)
func show_toast(message: String, is_success: bool = true):
if toast_container == null:
print("错误: toast_container 节点不存在")
push_error("ToastManager: toast_container 节点不存在")
return
print("显示Toast消息: ", message, " 成功: ", is_success)
_create_toast_instance(message, is_success)
# 清理所有Toast
@@ -123,21 +121,17 @@ func _create_toast_instance(message: String, is_success: bool):
# 平台特定的字体处理
if is_web:
print("Web平台Toast字体处理")
# Web平台使用主题文件
var chinese_theme = load("res://assets/ui/chinese_theme.tres")
if chinese_theme:
text_label.theme = chinese_theme
print("Web平台应用中文主题")
else:
print("Web平台中文主题加载失败")
push_warning("ToastManager: Web平台中文主题加载失败,使用默认字体")
else:
print("桌面平台Toast字体处理")
# 桌面平台直接加载中文字体
var desktop_chinese_font = load("res://assets/fonts/msyh.ttc")
if desktop_chinese_font:
text_label.add_theme_font_override("font", desktop_chinese_font)
print("桌面平台使用中文字体")
text_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
text_label.custom_minimum_size = Vector2(280, 0)

View File

@@ -1 +0,0 @@
uid://buk7d21cag262

View File

@@ -21,7 +21,7 @@ extends Node
# - 通过信号通知连接状态变化
# ============================================================================
class_name WebSocketManager
class_name ChatWebSocketManager
# ============================================================================
# 信号定义
@@ -106,6 +106,9 @@ var _reconnect_timer: Timer = Timer.new()
# 是否为正常关闭(非异常断开)
var _clean_close: bool = true
# 当前 CLOSED 状态是否已经处理过(防止每帧重复处理 close 事件)
var _closed_state_handled: bool = false
# 心跳定时器
var _heartbeat_timer: Timer = Timer.new()
@@ -118,8 +121,6 @@ const HEARTBEAT_INTERVAL: float = 30.0
# 初始化
func _ready() -> void:
print("WebSocketManager 初始化完成")
# 设置重连定时器
_setup_reconnect_timer()
@@ -136,11 +137,6 @@ func _process(_delta: float) -> void:
var state: WebSocketPeer.State = _websocket_peer.get_ready_state()
# 调试:打印状态变化
if _connection_state == ConnectionState.CONNECTING:
var peer_state_name = ["DISCONNECTED", "CONNECTING", "OPEN", "CLOSING", "CLOSED"][state]
print("📡 WebSocket 状态: peer=%s, manager=%s" % [peer_state_name, ConnectionState.keys()[_connection_state]])
if state == WebSocketPeer.STATE_OPEN:
# 接收数据
_websocket_peer.poll()
@@ -153,9 +149,6 @@ func _process(_delta: float) -> void:
# 发射消息接收信号
data_received.emit(message)
# 打印调试信息
print("📨 WebSocket 收到消息: ", message)
# 清理
func _exit_tree() -> void:
_disconnect()
@@ -173,22 +166,24 @@ func _exit_tree() -> void:
# ============================================================================
# 连接到游戏服务器
func connect_to_game_server() -> void:
func connect_to_game_server(is_reconnect_attempt: bool = false) -> void:
if _connection_state == ConnectionState.CONNECTED or _connection_state == ConnectionState.CONNECTING:
push_warning("已经在连接或已连接状态")
return
print("=== WebSocketManager 开始连接 ===")
print("服务器 URL: ", WEBSOCKET_URL)
print("WebSocket 连接中...")
_set_connection_state(ConnectionState.CONNECTING)
if is_reconnect_attempt:
_set_connection_state(ConnectionState.RECONNECTING)
else:
_set_connection_state(ConnectionState.CONNECTING)
_clean_close = true
_reconnect_attempt = 0
_closed_state_handled = false
# 仅在首次/手动连接时重置重连计数,重连流程保持累计尝试次数
if not is_reconnect_attempt:
_reconnect_attempt = 0
var err: Error = _websocket_peer.connect_to_url(WEBSOCKET_URL)
if err != OK:
print("WebSocket 连接失败: ", error_string(err))
push_error("WebSocketManager: 连接失败 - %s" % error_string(err))
_set_connection_state(ConnectionState.ERROR)
return
@@ -197,7 +192,6 @@ func connect_to_game_server() -> void:
# 断开 WebSocket 连接
func disconnect_websocket() -> void:
print("=== WebSocketManager 断开连接 ===")
_disconnect()
# 断开连接(内部方法)
@@ -243,15 +237,14 @@ func get_connection_state() -> ConnectionState:
# Error - 错误码OK 表示成功
func send_message(message: String) -> Error:
if _websocket_peer.get_ready_state() != WebSocketPeer.STATE_OPEN:
print("WebSocket 未连接,无法发送消息")
push_warning("WebSocketManager: 未连接,无法发送消息")
return ERR_UNCONFIGURED
var err: Error = _websocket_peer.send_text(message)
if err != OK:
print("WebSocket 发送消息失败: ", error_string(err))
push_error("WebSocketManager: 发送消息失败 - %s" % error_string(err))
return err
print("📤 发送 WebSocket 消息: ", message)
return OK
# ============================================================================
@@ -272,10 +265,6 @@ func enable_auto_reconnect(enabled: bool, max_attempts: int = DEFAULT_MAX_RECONN
_max_reconnect_attempts = max_attempts
_reconnect_base_delay = base_delay
print("自动重连: ", "启用" if enabled else "禁用")
print("最大重连次数: ", _max_reconnect_attempts)
print("基础重连延迟: ", _reconnect_base_delay, "")
# 获取重连信息
#
# 返回值:
@@ -298,7 +287,6 @@ func _set_connection_state(new_state: ConnectionState) -> void:
return
_connection_state = new_state
print("📡 连接状态变更: ", ConnectionState.keys()[new_state])
# 发射信号
connection_state_changed.emit(new_state)
@@ -316,43 +304,52 @@ func _check_websocket_state() -> void:
match state:
WebSocketPeer.STATE_CONNECTING:
_closed_state_handled = false
# 正在连接
if _connection_state != ConnectionState.CONNECTING and _connection_state != ConnectionState.RECONNECTING:
_set_connection_state(ConnectionState.CONNECTING)
WebSocketPeer.STATE_OPEN:
_closed_state_handled = false
# 连接成功
if _connection_state != ConnectionState.CONNECTED:
_on_websocket_connected()
WebSocketPeer.STATE_CLOSING:
_closed_state_handled = false
# 正在关闭
pass
WebSocketPeer.STATE_CLOSED:
# 连接关闭
var code: int = _websocket_peer.get_close_code()
var reason: String = _websocket_peer.get_close_reason()
print("🔌 WebSocket 关闭: code=%d, reason=%s" % [code, reason])
_on_websocket_closed(code != 0) # code=0 表示正常关闭
if _closed_state_handled:
return
_closed_state_handled = true
# 仅在连接生命周期中发生的关闭才触发关闭处理
var should_handle_close: bool = (
_connection_state == ConnectionState.CONNECTED
or _connection_state == ConnectionState.CONNECTING
or _connection_state == ConnectionState.RECONNECTING
)
if should_handle_close:
var close_code: int = _websocket_peer.get_close_code()
_on_websocket_closed(_is_clean_close_code(close_code))
# WebSocket 连接成功处理
func _on_websocket_connected() -> void:
print("✅ WebSocketManager: WebSocket 连接成功")
# 如果是重连,发射重连成功信号
if _connection_state == ConnectionState.RECONNECTING:
_reconnect_attempt = 0
reconnection_succeeded.emit()
print("🔄 重连成功")
_set_connection_state(ConnectionState.CONNECTED)
# WebSocket 连接关闭处理
func _on_websocket_closed(clean_close: bool) -> void:
print("🔌 WebSocketManager: WebSocket 连接断开")
print(" 正常关闭: ", clean_close)
_clean_close = clean_close
# 如果是异常断开且启用了自动重连
@@ -362,6 +359,11 @@ func _on_websocket_closed(clean_close: bool) -> void:
else:
_set_connection_state(ConnectionState.DISCONNECTED)
# 判断关闭码是否为干净关闭
# Godot 中 close_code == -1 表示非干净关闭(异常断开)
func _is_clean_close_code(close_code: int) -> bool:
return close_code != -1
# ============================================================================
# 内部方法 - 重连机制
# ============================================================================
@@ -379,7 +381,7 @@ func _setup_reconnect_timer() -> void:
func _attempt_reconnect() -> void:
# 检查是否超过最大重连次数
if _reconnect_attempt >= _max_reconnect_attempts:
print("❌ 达到最大重连次数 (", _max_reconnect_attempts, "),停止重连")
push_error("WebSocketManager: 达到最大重连次数 (%d),停止重连" % _max_reconnect_attempts)
reconnection_failed.emit(_reconnect_attempt, _max_reconnect_attempts)
_set_connection_state(ConnectionState.ERROR)
return
@@ -389,8 +391,6 @@ func _attempt_reconnect() -> void:
# 计算重连延迟(指数退避)
var delay: float = _calculate_reconnect_delay()
print("🔄 尝试重连 (", _reconnect_attempt, "/", _max_reconnect_attempts, ")")
print(" 延迟: ", delay, "")
# 启动重连定时器
_reconnect_timer.start(delay)
@@ -405,9 +405,8 @@ func _calculate_reconnect_delay() -> float:
# 重连定时器超时处理
func _on_reconnect_timeout() -> void:
print("⏰ 重连定时器超时,开始重连...")
_clean_close = false
connect_to_game_server()
connect_to_game_server(true)
# ============================================================================
# 内部方法 - 心跳机制

View File

@@ -1 +0,0 @@
uid://dmbgtbf6gyk6t

View File

@@ -27,13 +27,6 @@ extends Node
# 结构: {event_name: [{"callback": Callable, "target": Node}, ...]}
var event_listeners: Dictionary = {}
# ============ 生命周期方法 ============
# 初始化事件系统
# 在节点准备就绪时调用
func _ready():
print("EventSystem 初始化完成")
# ============ 事件监听器管理 ============
# 注册事件监听器
@@ -59,6 +52,11 @@ func connect_event(event_name: String, callback: Callable, target: Node = null):
if not event_listeners.has(event_name):
event_listeners[event_name] = []
# 避免重复注册同一个监听器
for listener in event_listeners[event_name]:
if listener.callback == callback and listener.target == target:
return
# 创建监听器信息
var listener_info = {
"callback": callback,
@@ -67,7 +65,6 @@ func connect_event(event_name: String, callback: Callable, target: Node = null):
# 添加到监听器列表
event_listeners[event_name].append(listener_info)
print("注册事件监听器: ", event_name, " -> ", callback)
# 移除事件监听器
#
@@ -93,7 +90,6 @@ func disconnect_event(event_name: String, callback: Callable, target: Node = nul
# 匹配callback和target
if listener.callback == callback and listener.target == target:
listeners.remove_at(i)
print("移除事件监听器: ", event_name, " -> ", callback)
break
# ============ 事件发送 ============
@@ -117,13 +113,11 @@ func disconnect_event(event_name: String, callback: Callable, target: Node = nul
# - 事件发送是同步的,所有监听器会立即执行
# - 如果监听器执行出错,不会影响其他监听器
func emit_event(event_name: String, data: Variant = null):
print("发送事件: ", event_name, " 数据: ", data)
# 检查是否有监听器
if not event_listeners.has(event_name):
return
var listeners = event_listeners[event_name]
var listeners = event_listeners[event_name].duplicate()
for listener_info in listeners:
var target = listener_info.target
var callback = listener_info.callback
@@ -161,7 +155,6 @@ func cleanup_invalid_listeners():
# 如果目标节点无效,移除监听器
if target != null and not is_instance_valid(target):
listeners.remove_at(i)
print("清理无效监听器: ", event_name)
# ============ 查询方法 ============
@@ -192,4 +185,3 @@ func get_listener_count(event_name: String) -> int:
# - 使用前请确保所有模块都能正确处理监听器丢失
func clear_all_listeners():
event_listeners.clear()
print("清空所有事件监听器")

View File

@@ -1 +0,0 @@
uid://bfheblucmti24

View File

@@ -131,13 +131,14 @@ static func get_grid_rect(grid_pos: Vector2i) -> Rect2:
var world_pos = grid_to_world(grid_pos)
return Rect2(world_pos, Vector2(GRID_SIZE, GRID_SIZE))
# 打印网格信息(调试
static func print_grid_info(world_pos: Vector2) -> void:
# 获取网格信息(调试辅助
static func print_grid_info(world_pos: Vector2) -> Dictionary:
var grid_pos = world_to_grid(world_pos)
var snapped_pos = snap_to_grid(world_pos)
var center_pos = grid_to_world_center(grid_pos)
print("世界坐标: ", world_pos)
print("网格坐标: ", grid_pos)
print("吸附位置: ", snapped_pos)
print("网格中心: ", center_pos)
return {
"world_position": world_pos,
"grid_position": grid_pos,
"snapped_position": snapped_pos,
"center_position": center_pos
}

View File

@@ -1 +0,0 @@
uid://dceqpffgti4jb

View File

@@ -39,7 +39,7 @@ class_name StringUtils
#
# 使用示例:
# if StringUtils.is_valid_email("user@example.com"):
# print("邮箱格式正确")
# # 邮箱格式正确
static func is_valid_email(email: String) -> bool:
var regex = RegEx.new()
regex.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")
@@ -60,7 +60,7 @@ static func is_valid_email(email: String) -> bool:
#
# 使用示例:
# if StringUtils.is_valid_username("user_123"):
# print("用户名格式正确")
# # 用户名格式正确
static func is_valid_username(username: String) -> bool:
# 检查长度
if username.is_empty() or username.length() > 50:
@@ -92,7 +92,7 @@ static func is_valid_username(username: String) -> bool:
# 使用示例:
# var result = StringUtils.validate_password_strength("MyPass123!")
# if result.valid:
# print("密码强度: ", result.message)
# # 可按需展示 result.message
static func validate_password_strength(password: String) -> Dictionary:
var result = {"valid": false, "message": "", "strength": 0}

View File

@@ -1 +0,0 @@
uid://bu8onmk6q8wic

View File

@@ -1,132 +0,0 @@
extends Node2D
# ##############################################################################
# This is a wrapper around the normal and compact gui controls and serves as
# the interface between gut.gd and the gui. The GutRunner creates an instance
# of this and then this takes care of managing the different GUI controls.
# ##############################################################################
@onready var _normal_gui = $Normal
@onready var _compact_gui = $Compact
var gut = null :
set(val):
gut = val
_set_gut(val)
func _ready():
_normal_gui.switch_modes.connect(use_compact_mode.bind(true))
_compact_gui.switch_modes.connect(use_compact_mode.bind(false))
_normal_gui.set_title("GUT")
_compact_gui.set_title("GUT")
_normal_gui.align_right()
_compact_gui.to_bottom_right()
use_compact_mode(false)
if(get_parent() == get_tree().root):
_test_running_setup()
func _test_running_setup():
set_font_size(100)
_normal_gui.get_textbox().text = "hello world, how are you doing?"
# ------------------------
# Private
# ------------------------
func _set_gut(val):
if(_normal_gui.get_gut() == val):
return
_normal_gui.set_gut(val)
_compact_gui.set_gut(val)
val.start_run.connect(_on_gut_start_run)
val.end_run.connect(_on_gut_end_run)
val.start_pause_before_teardown.connect(_on_gut_pause)
val.end_pause_before_teardown.connect(_on_pause_end)
func _set_both_titles(text):
_normal_gui.set_title(text)
_compact_gui.set_title(text)
# ------------------------
# Events
# ------------------------
func _on_gut_start_run():
_set_both_titles('Running')
func _on_gut_end_run():
_set_both_titles('Finished')
func _on_gut_pause():
_set_both_titles('-- Paused --')
func _on_pause_end():
_set_both_titles('Running')
# ------------------------
# Public
# ------------------------
func get_textbox():
return _normal_gui.get_textbox()
func set_font_size(new_size):
var rtl = _normal_gui.get_textbox()
rtl.set('theme_override_font_sizes/bold_italics_font_size', new_size)
rtl.set('theme_override_font_sizes/bold_font_size', new_size)
rtl.set('theme_override_font_sizes/italics_font_size', new_size)
rtl.set('theme_override_font_sizes/normal_font_size', new_size)
func set_font(font_name):
_set_all_fonts_in_rtl(_normal_gui.get_textbox(), font_name)
func _set_font(rtl, font_name, custom_name):
if(font_name == null):
rtl.remove_theme_font_override(custom_name)
else:
var font_path = 'res://addons/gut/fonts/' + font_name + '.ttf'
if(FileAccess.file_exists(font_path)):
var dyn_font = FontFile.new()
dyn_font.load_dynamic_font('res://addons/gut/fonts/' + font_name + '.ttf')
rtl.add_theme_font_override(custom_name, dyn_font)
func _set_all_fonts_in_rtl(rtl, base_name):
if(base_name == 'Default'):
_set_font(rtl, null, 'normal_font')
_set_font(rtl, null, 'bold_font')
_set_font(rtl, null, 'italics_font')
_set_font(rtl, null, 'bold_italics_font')
else:
_set_font(rtl, base_name + '-Regular', 'normal_font')
_set_font(rtl, base_name + '-Bold', 'bold_font')
_set_font(rtl, base_name + '-Italic', 'italics_font')
_set_font(rtl, base_name + '-BoldItalic', 'bold_italics_font')
func set_default_font_color(color):
_normal_gui.get_textbox().set('custom_colors/default_color', color)
func set_background_color(color):
_normal_gui.set_bg_color(color)
func use_compact_mode(should=true):
_compact_gui.visible = should
_normal_gui.visible = !should
func set_opacity(val):
_normal_gui.modulate.a = val
_compact_gui.modulate.a = val
func set_title(text):
_set_both_titles(text)

View File

@@ -1 +0,0 @@
uid://bw7tukh738kw1

View File

@@ -1,16 +0,0 @@
[gd_scene load_steps=4 format=3 uid="uid://m28heqtswbuq"]
[ext_resource type="Script" uid="uid://bw7tukh738kw1" path="res://addons/gut/GutScene.gd" id="1_b4m8y"]
[ext_resource type="PackedScene" uid="uid://duxblir3vu8x7" path="res://addons/gut/gui/NormalGui.tscn" id="2_j6ywb"]
[ext_resource type="PackedScene" uid="uid://cnqqdfsn80ise" path="res://addons/gut/gui/MinGui.tscn" id="3_3glw1"]
[node name="GutScene" type="Node2D"]
script = ExtResource("1_b4m8y")
[node name="Normal" parent="." instance=ExtResource("2_j6ywb")]
[node name="Compact" parent="." instance=ExtResource("3_3glw1")]
offset_left = 5.0
offset_top = 273.0
offset_right = 265.0
offset_bottom = 403.0

View File

@@ -1,22 +0,0 @@
The MIT License (MIT)
=====================
Copyright (c) 2018 Tom "Butch" Wesley
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.

View File

@@ -1,52 +0,0 @@
extends Window
@onready var rtl = $TextDisplay/RichTextLabel
func _get_file_as_text(path):
var to_return = null
var f = FileAccess.open(path, FileAccess.READ)
if(f != null):
to_return = f.get_as_text()
else:
to_return = str('ERROR: Could not open file. Error code ', FileAccess.get_open_error())
return to_return
func _ready():
rtl.clear()
func _on_OpenFile_pressed():
$FileDialog.popup_centered()
func _on_FileDialog_file_selected(path):
show_file(path)
func _on_Close_pressed():
self.hide()
func show_file(path):
var text = _get_file_as_text(path)
if(text == ''):
text = '<Empty File>'
rtl.set_text(text)
self.window_title = path
func show_open():
self.popup_centered()
$FileDialog.popup_centered()
func get_rich_text_label():
return $TextDisplay/RichTextLabel
func _on_Home_pressed():
rtl.scroll_to_line(0)
func _on_End_pressed():
rtl.scroll_to_line(rtl.get_line_count() -1)
func _on_Copy_pressed():
return
# OS.clipboard = rtl.text
func _on_file_dialog_visibility_changed():
if rtl.text.length() == 0 and not $FileDialog.visible:
self.hide()

View File

@@ -1 +0,0 @@
uid://x51wilphva3d

View File

@@ -1,92 +0,0 @@
[gd_scene load_steps=2 format=3 uid="uid://bsm7wtt1gie4v"]
[ext_resource type="Script" uid="uid://x51wilphva3d" path="res://addons/gut/UserFileViewer.gd" id="1"]
[node name="UserFileViewer" type="Window"]
exclusive = true
script = ExtResource("1")
[node name="FileDialog" type="FileDialog" parent="."]
access = 1
show_hidden_files = true
__meta__ = {
"_edit_use_anchors_": false
}
[node name="TextDisplay" type="ColorRect" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 8.0
offset_right = -10.0
offset_bottom = -65.0
color = Color(0.2, 0.188235, 0.188235, 1)
[node name="RichTextLabel" type="RichTextLabel" parent="TextDisplay"]
anchor_right = 1.0
anchor_bottom = 1.0
focus_mode = 2
text = "In publishing and graphic design, Lorem ipsum is a placeholder text commonly used to demonstrate the visual form of a document or a typeface without relying on meaningful content. Lorem ipsum may be used before final copy is available, but it may also be used to temporarily replace copy in a process called greeking, which allows designers to consider form without the meaning of the text influencing the design.
Lorem ipsum is typically a corrupted version of De finibus bonorum et malorum, a first-century BCE text by the Roman statesman and philosopher Cicero, with words altered, added, and removed to make it nonsensical, improper Latin.
Versions of the Lorem ipsum text have been used in typesetting at least since the 1960s, when it was popularized by advertisements for Letraset transfer sheets. Lorem ipsum was introduced to the digital world in the mid-1980s when Aldus employed it in graphic and word-processing templates for its desktop publishing program PageMaker. Other popular word processors including Pages and Microsoft Word have since adopted Lorem ipsum as well."
selection_enabled = true
[node name="OpenFile" type="Button" parent="."]
anchor_left = 1.0
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = -158.0
offset_top = -50.0
offset_right = -84.0
offset_bottom = -30.0
text = "Open File"
[node name="Home" type="Button" parent="."]
anchor_left = 1.0
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = -478.0
offset_top = -50.0
offset_right = -404.0
offset_bottom = -30.0
text = "Home"
[node name="Copy" type="Button" parent="."]
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = 160.0
offset_top = -50.0
offset_right = 234.0
offset_bottom = -30.0
text = "Copy"
[node name="End" type="Button" parent="."]
anchor_left = 1.0
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = -318.0
offset_top = -50.0
offset_right = -244.0
offset_bottom = -30.0
text = "End"
[node name="Close" type="Button" parent="."]
anchor_top = 1.0
anchor_bottom = 1.0
offset_left = 10.0
offset_top = -50.0
offset_right = 80.0
offset_bottom = -30.0
text = "Close"
[connection signal="file_selected" from="FileDialog" to="." method="_on_FileDialog_file_selected"]
[connection signal="visibility_changed" from="FileDialog" to="." method="_on_file_dialog_visibility_changed"]
[connection signal="pressed" from="OpenFile" to="." method="_on_OpenFile_pressed"]
[connection signal="pressed" from="Home" to="." method="_on_Home_pressed"]
[connection signal="pressed" from="Copy" to="." method="_on_Copy_pressed"]
[connection signal="pressed" from="End" to="." method="_on_End_pressed"]
[connection signal="pressed" from="Close" to="." method="_on_Close_pressed"]

View File

@@ -1,86 +0,0 @@
# ##############################################################################
#(G)odot (U)nit (T)est class
#
# ##############################################################################
# The MIT License (MIT)
# =====================
#
# Copyright (c) 2025 Tom "Butch" Wesley
#
# 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.
#
# ##############################################################################
# Class used to keep track of objects to be freed and utilities to free them.
# ##############################################################################
var _to_free = []
var _to_queue_free = []
var _ref_counted_doubles = []
var _all_instance_ids = []
func _add_instance_id(thing):
if(thing.has_method("get_instance_id")):
_all_instance_ids.append(thing.get_instance_id())
func add_free(thing):
if(typeof(thing) == TYPE_OBJECT):
_add_instance_id(thing)
if(!thing is RefCounted):
_to_free.append(thing)
elif(GutUtils.is_double(thing)):
_ref_counted_doubles.append(thing)
func add_queue_free(thing):
if(typeof(thing) == TYPE_OBJECT):
_add_instance_id(thing)
_to_queue_free.append(thing)
func get_queue_free_count():
return _to_queue_free.size()
func get_free_count():
return _to_free.size()
func free_all():
for node in _to_free:
if(is_instance_valid(node)):
if(GutUtils.is_double(node)):
node.__gutdbl_done()
node.free()
_to_free.clear()
for i in range(_to_queue_free.size()):
if(is_instance_valid(_to_queue_free[i])):
_to_queue_free[i].queue_free()
_to_queue_free.clear()
for ref_dbl in _ref_counted_doubles:
ref_dbl.__gutdbl_done()
_ref_counted_doubles.clear()
_all_instance_ids.clear()
func has_instance_id(id):
return _all_instance_ids.has(id)

View File

@@ -1 +0,0 @@
uid://bxjfriqxgwe0r

View File

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

View File

@@ -1 +0,0 @@
uid://ccu4ww35edtdi

View File

@@ -1,239 +0,0 @@
extends SceneTree
var Optparse = load('res://addons/gut/cli/optparse.gd')
var WarningsManager = load("res://addons/gut/warnings_manager.gd")
const WARN_VALUE_PRINT_POSITION = 36
var godot_default_warnings = {
"assert_always_false": 1, "assert_always_true": 1, "confusable_identifier": 1,
"confusable_local_declaration": 1, "confusable_local_usage": 1, "constant_used_as_function": 1,
"deprecated_keyword": 1, "empty_file": 1, "enable": true,
"exclude_addons": true, "function_used_as_property": 1, "get_node_default_without_onready": 2,
"incompatible_ternary": 1, "inference_on_variant": 2, "inferred_declaration": 0,
"int_as_enum_without_cast": 1, "int_as_enum_without_match": 1, "integer_division": 1,
"narrowing_conversion": 1, "native_method_override": 2, "onready_with_export": 2,
"property_used_as_function": 1, "redundant_await": 1, "redundant_static_unload": 1,
"renamed_in_godot_4_hint": 1, "return_value_discarded": 0, "shadowed_global_identifier": 1,
"shadowed_variable": 1, "shadowed_variable_base_class": 1, "standalone_expression": 1,
"standalone_ternary": 1, "static_called_on_instance": 1, "unassigned_variable": 1,
"unassigned_variable_op_assign": 1, "unreachable_code": 1, "unreachable_pattern": 1,
"unsafe_call_argument": 0, "unsafe_cast": 0, "unsafe_method_access": 0,
"unsafe_property_access": 0, "unsafe_void_return": 1, "untyped_declaration": 0,
"unused_local_constant": 1, "unused_parameter": 1, "unused_private_class_variable": 1,
"unused_signal": 1, "unused_variable": 1
}
var gut_default_changes = {
"exclude_addons": false, "redundant_await": 0,
}
var warning_settings = {}
func _setup_warning_settings():
warning_settings["godot_default"] = godot_default_warnings
warning_settings["current"] = WarningsManager.create_warnings_dictionary_from_project_settings()
warning_settings["all_warn"] = WarningsManager.create_warn_all_warnings_dictionary()
var gut_default = godot_default_warnings.duplicate()
gut_default.merge(gut_default_changes, true)
warning_settings["gut_default"] = gut_default
func _warn_value_to_s(value):
var readable = str(value).capitalize()
if(typeof(value) == TYPE_INT):
readable = WarningsManager.WARNING_LOOKUP.get(value, str(readable, ' ???'))
readable = readable.capitalize()
return readable
func _human_readable(warnings):
var to_return = ""
for key in warnings:
var readable = _warn_value_to_s(warnings[key])
to_return += str(key.capitalize().rpad(35, ' '), readable, "\n")
return to_return
func _dump_settings(which):
if(warning_settings.has(which)):
GutUtils.pretty_print(warning_settings[which])
else:
print("UNKNOWN print option ", which)
func _print_settings(which):
if(warning_settings.has(which)):
print(_human_readable(warning_settings[which]))
else:
print("UNKNOWN print option ", which)
func _apply_settings(which):
if(!warning_settings.has(which)):
print("UNKNOWN set option ", which)
return
var pre_settings = warning_settings["current"]
var new_settings = warning_settings[which]
if(new_settings == pre_settings):
print("-- Settings are the same, no changes were made --")
return
WarningsManager.apply_warnings_dictionary(new_settings)
ProjectSettings.save()
print("-- Project Warning Settings have been updated --")
print(_diff_changes_text(pre_settings))
func _diff_text(w1, w2, diff_col_pad=10):
var to_return = ""
for key in w1:
var v1_text = _warn_value_to_s(w1[key])
var v2_text = _warn_value_to_s(w2[key])
var diff_text = v1_text
var prefix = " "
if(v1_text != v2_text):
var diff_prefix = " "
if(w1[key] > w2[key]):
diff_prefix = "-"
else:
diff_prefix = "+"
prefix = "* "
diff_text = str(v1_text.rpad(diff_col_pad, ' '), diff_prefix, v2_text)
to_return += str(str(prefix, key.capitalize()).rpad(WARN_VALUE_PRINT_POSITION, ' '), diff_text, "\n")
return to_return.rstrip("\n")
func _diff_changes_text(pre_settings):
var orig_diff_text = _diff_text(
pre_settings,
WarningsManager.create_warnings_dictionary_from_project_settings(),
0)
# these next two lines are fragile and brute force...enjoy
var diff_text = orig_diff_text.replace("-", " -> ")
diff_text = diff_text.replace("+", " -> ")
if(orig_diff_text == diff_text):
diff_text += "\n-- No changes were made --"
else:
diff_text += "\nChanges will not be visible in Godot until it is restarted.\n"
diff_text += "Even if it asks you to reload...Maybe. Probably."
return diff_text
func _diff(name_1, name_2):
if(warning_settings.has(name_1) and warning_settings.has(name_2)):
var c2_pad = name_1.length() + 2
var heading = str(" ".repeat(WARN_VALUE_PRINT_POSITION), name_1.rpad(c2_pad, ' '), name_2, "\n")
heading += str(
" ".repeat(WARN_VALUE_PRINT_POSITION),
"-".repeat(name_1.length()).rpad(c2_pad, " "),
"-".repeat(name_2.length()),
"\n")
var text = _diff_text(warning_settings[name_1], warning_settings[name_2], c2_pad)
print(heading)
print(text)
var diff_count = 0
for line in text.split("\n"):
if(!line.begins_with(" ")):
diff_count += 1
if(diff_count == 0):
print('-- [', name_1, "] and [", name_2, "] are the same --")
else:
print('-- There are ', diff_count, ' differences between [', name_1, "] and [", name_2, "] --")
else:
print("One or more unknown Warning Level Names:, [", name_1, "] [", name_2, "]")
func _set_settings(nvps):
var pre_settings = warning_settings["current"]
for i in range(nvps.size()/2):
var s_name = nvps[i * 2]
var s_value = nvps[i * 2 + 1]
if(godot_default_warnings.has(s_name)):
var t = typeof(godot_default_warnings[s_name])
if(t == TYPE_INT):
s_value = s_value.to_int()
elif(t == TYPE_BOOL):
s_value = s_value.to_lower() == 'true'
WarningsManager.set_project_setting_warning(s_name, s_value)
ProjectSettings.save()
print(_diff_changes_text(pre_settings))
func _setup_options():
var opts = Optparse.new()
opts.banner = """
This script prints info about or sets the warning settings for the project.
Each action requires one or more Warning Level Names.
Warning Level Names:
* current The current settings for the project.
* godot_default The default settings for Godot.
* gut_default The warning settings that is used when developing GUT.
* all_warn Everything set to warn.
""".dedent()
opts.add('-h', false, 'Print this help')
opts.add('-set', [], "Sets a single setting in the project settings and saves.\n" +
"Use -dump to see a list of setting names and values.\n" +
"Example: -set enabled,true -set unsafe_cast,2 -set unreachable_code,0")
opts.add_heading(" Actions (require Warning Level Name)")
opts.add('-diff', [], "Shows the difference between two Warning Level Names.\n" +
"Example: -diff current,all_warn")
opts.add('-dump', 'none', "Prints a dictionary of the warning values.")
opts.add('-print', 'none', "Print human readable warning values.")
opts.add('-apply', 'none', "Applys one of the Warning Level Names to the project settings. You should restart after using this")
return opts
func _print_help(opts):
opts.print_help()
func _init():
# Testing might set this flag but it should never be disabled for this tool
# or it cannot save project settings, but says it did. Sneakily use the
# private property to get around this property being read-only. Don't
# try this at home.
WarningsManager._disabled = false
_setup_warning_settings()
var opts = _setup_options()
opts.parse()
if(opts.unused.size() != 0):
opts.print_help()
print("Unknown arguments ", opts.unused)
if(opts.values.h):
opts.print_help()
elif(opts.values.print != 'none'):
_print_settings(opts.values.print)
elif(opts.values.dump != 'none'):
_dump_settings(opts.values.dump)
elif(opts.values.apply != 'none'):
_apply_settings(opts.values.apply )
elif(opts.values.diff.size() == 2):
_diff(opts.values.diff[0], opts.values.diff[1])
elif(opts.values.set.size() % 2 == 0):
_set_settings(opts.values.set)
else:
opts.print_help()
print("You didn't specify any options or too many or not the right size or something invalid. I don't know what you want to do.")
quit()

View File

@@ -1 +0,0 @@
uid://1pauyfnd1cre

View File

@@ -1,315 +0,0 @@
extends Node
var Optparse = load('res://addons/gut/cli/optparse.gd')
var Gut = load('res://addons/gut/gut.gd')
var GutRunner = load('res://addons/gut/gui/GutRunner.tscn')
# ------------------------------------------------------------------------------
# Helper class to resolve the various different places where an option can
# be set. Using the get_value method will enforce the order of precedence of:
# 1. command line value
# 2. config file value
# 3. default value
#
# The idea is that you set the base_opts. That will get you a copies of the
# hash with null values for the other types of values. Lower precedented hashes
# will punch through null values of higher precedented hashes.
# ------------------------------------------------------------------------------
class OptionResolver:
var base_opts = {}
var cmd_opts = {}
var config_opts = {}
func get_value(key):
return _nvl(cmd_opts[key], _nvl(config_opts[key], base_opts[key]))
func set_base_opts(opts):
base_opts = opts
cmd_opts = _null_copy(opts)
config_opts = _null_copy(opts)
# creates a copy of a hash with all values null.
func _null_copy(h):
var new_hash = {}
for key in h:
new_hash[key] = null
return new_hash
func _nvl(a, b):
if(a == null):
return b
else:
return a
func _string_it(h):
var to_return = ''
for key in h:
to_return += str('(',key, ':', _nvl(h[key], 'NULL'), ')')
return to_return
func to_s():
return str("base:\n", _string_it(base_opts), "\n", \
"config:\n", _string_it(config_opts), "\n", \
"cmd:\n", _string_it(cmd_opts), "\n", \
"resolved:\n", _string_it(get_resolved_values()))
func get_resolved_values():
var to_return = {}
for key in base_opts:
to_return[key] = get_value(key)
return to_return
func to_s_verbose():
var to_return = ''
var resolved = get_resolved_values()
for key in base_opts:
to_return += str(key, "\n")
to_return += str(' default: ', _nvl(base_opts[key], 'NULL'), "\n")
to_return += str(' config: ', _nvl(config_opts[key], ' --'), "\n")
to_return += str(' cmd: ', _nvl(cmd_opts[key], ' --'), "\n")
to_return += str(' final: ', _nvl(resolved[key], 'NULL'), "\n")
return to_return
# ------------------------------------------------------------------------------
# Here starts the actual script that uses the Options class to kick off Gut
# and run your tests.
# ------------------------------------------------------------------------------
var _gut_config = load('res://addons/gut/gut_config.gd').new()
# array of command line options specified
var _final_opts = []
func setup_options(options, font_names):
var opts = Optparse.new()
opts.banner =\
"""
The GUT CLI
-----------
The default behavior for GUT is to load options from a res://.gutconfig.json if
it exists. Any options specified on the command line will take precedence over
options specified in the gutconfig file. You can specify a different gutconfig
file with the -gconfig option.
To generate a .gutconfig.json file you can use -gprint_gutconfig_sample
To see the effective values of a CLI command and a gutconfig use -gpo
Values for options can be supplied using:
option=value # no space around "="
option value # a space between option and value w/o =
Options whose values are lists/arrays can be specified multiple times:
-gdir=a,b
-gdir c,d
-gdir e
# results in -gdir equaling [a, b, c, d, e]
To not use an empty value instead of a default value, specifiy the option with
an immediate "=":
-gconfig=
"""
opts.add_heading("Test Config:")
opts.add('-gdir', options.dirs, 'List of directories to search for test scripts in.')
opts.add('-ginclude_subdirs', false, 'Flag to include all subdirectories specified with -gdir.')
opts.add('-gtest', [], 'List of full paths to test scripts to run.')
opts.add('-gprefix', options.prefix, 'Prefix used to find tests when specifying -gdir. Default "[default]".')
opts.add('-gsuffix', options.suffix, 'Test script suffix, including .gd extension. Default "[default]".')
opts.add('-gconfig', 'res://.gutconfig.json', 'The config file to load options from. The default is [default]. Use "-gconfig=" to not use a config file.')
opts.add('-gpre_run_script', '', 'pre-run hook script path')
opts.add('-gpost_run_script', '', 'post-run hook script path')
opts.add('-gerrors_do_not_cause_failure', false, 'When an internal GUT error occurs tests will fail. With this option set, that does not happen.')
opts.add('-gdouble_strategy', 'SCRIPT_ONLY', 'Default strategy to use when doubling. Valid values are [INCLUDE_NATIVE, SCRIPT_ONLY]. Default "[default]"')
opts.add_heading("Run Options:")
opts.add('-gselect', '', 'All scripts that contain the specified string in their filename will be ran')
opts.add('-ginner_class', '', 'Only run inner classes that contain the specified string in their name.')
opts.add('-gunit_test_name', '', 'Any test that contains the specified text will be run, all others will be skipped.')
opts.add('-gexit', false, 'Exit after running tests. If not specified you have to manually close the window.')
opts.add('-gexit_on_success', false, 'Only exit if zero tests fail.')
opts.add('-gignore_pause', false, 'Ignores any calls to pause_before_teardown.')
opts.add('-gno_error_tracking', false, 'Disable error tracking.')
opts.add('-gfailure_error_types', options.failure_error_types, 'Error types that will cause tests to fail if the are encountered during the execution of a test. Default "[default]"')
opts.add_heading("Display Settings:")
opts.add('-glog', options.log_level, 'Log level [0-3]. Default [default]')
opts.add('-ghide_orphans', false, 'Display orphan counts for tests and scripts. Default [default].')
opts.add('-gmaximize', false, 'Maximizes test runner window to fit the viewport.')
opts.add('-gcompact_mode', false, 'The runner will be in compact mode. This overrides -gmaximize.')
opts.add('-gopacity', options.opacity, 'Set opacity of test runner window. Use range 0 - 100. 0 = transparent, 100 = opaque.')
opts.add('-gdisable_colors', false, 'Disable command line colors.')
opts.add('-gfont_name', options.font_name, str('Valid values are: ', font_names, '. Default "[default]"'))
opts.add('-gfont_size', options.font_size, 'Font size, default "[default]"')
opts.add('-gbackground_color', options.background_color, 'Background color as an html color, default "[default]"')
opts.add('-gfont_color',options.font_color, 'Font color as an html color, default "[default]"')
opts.add('-gpaint_after', options.paint_after, 'Delay before GUT will add a 1 frame pause to paint the screen/GUI. default [default]')
opts.add('-gwait_log_delay', options.wait_log_delay, 'Delay before GUT will print a message to indicate a test is awaiting one of the wait_* methods. Default [default]')
opts.add_heading("Result Export:")
opts.add('-gjunit_xml_file', options.junit_xml_file, 'Export results of run to this file in the Junit XML format.')
opts.add('-gjunit_xml_timestamp', options.junit_xml_timestamp, 'Include a timestamp in the -gjunit_xml_file, default [default]')
opts.add_heading("Help:")
opts.add('-gh', false, 'Print this help. You did this to see this, so you probably understand.')
opts.add('-gpo', false, 'Print option values from all sources and the value used.')
opts.add('-gprint_gutconfig_sample', false, 'Print out json that can be used to make a gutconfig file.')
# run as in editor, for shelling out purposes through Editor.
var o = opts.add('-graie', false, 'do not use')
o.show_in_help = false
return opts
# Parses options, applying them to the _tester or setting values
# in the options struct.
func extract_command_line_options(from, to):
to.compact_mode = from.get_value_or_null('-gcompact_mode')
to.config_file = from.get_value_or_null('-gconfig')
to.dirs = from.get_value_or_null('-gdir')
to.disable_colors = from.get_value_or_null('-gdisable_colors')
to.double_strategy = from.get_value_or_null('-gdouble_strategy')
to.errors_do_not_cause_failure = from.get_value_or_null('-gerrors_do_not_cause_failure')
to.hide_orphans = from.get_value_or_null('-ghide_orphans')
to.ignore_pause = from.get_value_or_null('-gignore_pause')
to.include_subdirs = from.get_value_or_null('-ginclude_subdirs')
to.inner_class = from.get_value_or_null('-ginner_class')
to.log_level = from.get_value_or_null('-glog')
to.opacity = from.get_value_or_null('-gopacity')
to.post_run_script = from.get_value_or_null('-gpost_run_script')
to.pre_run_script = from.get_value_or_null('-gpre_run_script')
to.prefix = from.get_value_or_null('-gprefix')
to.selected = from.get_value_or_null('-gselect')
to.should_exit = from.get_value_or_null('-gexit')
to.should_exit_on_success = from.get_value_or_null('-gexit_on_success')
to.should_maximize = from.get_value_or_null('-gmaximize')
to.suffix = from.get_value_or_null('-gsuffix')
to.tests = from.get_value_or_null('-gtest')
to.unit_test_name = from.get_value_or_null('-gunit_test_name')
to.wait_log_delay = from.get_value_or_null('-gwait_log_delay')
to.background_color = from.get_value_or_null('-gbackground_color')
to.font_color = from.get_value_or_null('-gfont_color')
to.font_name = from.get_value_or_null('-gfont_name')
to.font_size = from.get_value_or_null('-gfont_size')
to.paint_after = from.get_value_or_null('-gpaint_after')
to.junit_xml_file = from.get_value_or_null('-gjunit_xml_file')
to.junit_xml_timestamp = from.get_value_or_null('-gjunit_xml_timestamp')
to.failure_error_types = from.get_value_or_null('-gfailure_error_types')
to.no_error_tracking = from.get_value_or_null('-gno_error_tracking')
to.raie = from.get_value_or_null('-graie')
func _print_gutconfigs(values):
var header = """Here is a sample of a full .gutconfig.json file.
You do not need to specify all values in your own file. The values supplied in
this sample are what would be used if you ran gut w/o the -gprint_gutconfig_sample
option. Option priority is: command-line, .gutconfig, default)."""
print("\n", header.replace("\n", ' '), "\n")
var resolved = values
# remove_at some options that don't make sense to be in config
resolved.erase("config_file")
resolved.erase("show_help")
print(JSON.stringify(resolved, ' '))
for key in resolved:
resolved[key] = null
print("\n\nAnd here's an empty config for you fill in what you want.")
print(JSON.stringify(resolved, ' '))
func _run_tests(opt_resolver):
_final_opts = opt_resolver.get_resolved_values();
_gut_config.options = _final_opts
var runner = GutRunner.instantiate()
runner.set_gut_config(_gut_config)
get_tree().root.add_child(runner)
if(opt_resolver.cmd_opts.raie):
runner.run_from_editor()
else:
runner.run_tests()
# parse options and run Gut
func main():
var opt_resolver = OptionResolver.new()
opt_resolver.set_base_opts(_gut_config.default_options)
var cli_opts = setup_options(_gut_config.default_options, _gut_config.valid_fonts)
cli_opts.parse()
var all_options_valid = cli_opts.unused.size() == 0
extract_command_line_options(cli_opts, opt_resolver.cmd_opts)
var config_path = opt_resolver.get_value('config_file')
var load_result = 1
# Checking for an empty config path allows us to not use a config file via
# the -gconfig_file option since using "-gconfig_file=" or -gconfig_file=''"
# will result in an empty string.
if(config_path != ''):
load_result = _gut_config.load_options_no_defaults(config_path)
# SHORTCIRCUIT
if(!all_options_valid):
print('Unknown arguments: ', cli_opts.unused)
get_tree().quit(1)
elif(load_result == -1):
print('Invalid gutconfig ', load_result)
get_tree().quit(1)
else:
opt_resolver.config_opts = _gut_config.options
if(cli_opts.get_value('-gh')):
print(GutUtils.version_numbers.get_version_text())
cli_opts.print_help()
get_tree().quit(0)
elif(cli_opts.get_value('-gpo')):
print('All config options and where they are specified. ' +
'The "final" value shows which value will actually be used ' +
'based on order of precedence (default < .gutconfig < cmd line).' + "\n")
print(opt_resolver.to_s_verbose())
get_tree().quit(0)
elif(cli_opts.get_value('-gprint_gutconfig_sample')):
_print_gutconfigs(opt_resolver.get_resolved_values())
get_tree().quit(0)
else:
_run_tests(opt_resolver)
# ##############################################################################
#(G)odot (U)nit (T)est class
#
# ##############################################################################
# The MIT License (MIT)
# =====================
#
# Copyright (c) 2025 Tom "Butch" Wesley
#
# 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.
#
# ##############################################################################

View File

@@ -1 +0,0 @@
uid://bhuudqinp4bth

View File

@@ -1,678 +0,0 @@
## Parses command line arguments, as one might expect.
##
## Parses command line arguments with a bunch of options including generating
## text that displays all the arguments your script accepts. This
## is included in the GUT ClassRef since it might be usable by others and is
## portable (everything it needs is in this one file).
## [br]
## This does alot, if you want to see it in action have a look at
## [url=https://github.com/bitwes/Gut/blob/main/scratch/optparse_example.gd]scratch/optparse_example.gd[/url]
## [codeblock lang=text]
##
## Godot Argument Lists
## -------------------------
## There are two sets of command line arguments that Godot populates:
## OS.get_cmdline_args
## OS.get_cmdline_user_args.
##
## OS.get_cmdline_args contains any arguments that are not used by the engine
## itself. This means options like --help and -d will never appear in this list
## since these are used by the engine. The one exception is the -s option which
## is always included as the first entry and the script path as the second.
## Optparse ignores these values for argument processing but can be accessed
## with my_optparse.options.script_option. This list does not contain any
## arguments that appear in OS.get_cmdline_user_args.
##
## OS.get_cmdline_user_args contains any arguments that appear on the command
## line AFTER " -- " or " ++ ". This list CAN contain options that the engine
## would otherwise use, and are ignored completely by the engine.
##
## The parse method, by default, includes arguments from OS.get_cmdline_args and
## OS.get_cmdline_user_args. You can optionally pass one of these to the parse
## method to limit which arguments are parsed. You can also conjure up your own
## array of arguments and pass that to parse.
##
## See Godot's documentation for get_cmdline_args and get_cmdline_user_args for
## more information.
##
##
## Adding Options
## --------------
## Use the following to add options to be parsed. These methods return the
## created Option instance. See that class above for more info. You can use
## the returned instance to get values, or use get_value/get_value_or_null.
## add("--name", "default", "Description goes here")
## add(["--name", "--aliases"], "default", "Description goes here")
## add_required(["--name", "--aliases"], "default", "Description goes here")
## add_positional("--name", "default", "Description goes here")
## add_positional_required("--name", "default", "Description goes here")
##
## get_value will return the value of the option or the default if it was not
## set. get_value_or_null will return the value of the option or null if it was
## not set.
##
## The Datatype for an option is determined from the default value supplied to
## the various add methods. Supported types are
## String
## Int
## Float
## Array of strings
## Boolean
##
##
## Value Parsing
## -------------
## optparse uses option_name_prefix to differentiate between option names and
## values. Any argument that starts with this value will be treated as an
## argument name. The default is "-". Set this before calling parse if you want
## to change it.
##
## Values for options can be supplied on the command line with or without an "=":
## option=value # no space around "="
## option value # a space between option and value w/o =
## There is no way to escape "=" at this time.
##
## Array options can be specified multiple times and/or set from a comma delimited
## list.
## -gdir=a,b
## -gdir c,d
## -gdir e
## Results in -gdir equaling [a, b, c, d, e]. There is no way to escape commas
## at this time.
##
## To specify an empty list via the command line follow the option with an equal
## sign
## -gdir=
##
## Boolean options will have thier value set to !default when they are supplied
## on the command line. Boolean options cannot have a value on the command line.
## They are either supplied or not.
##
## If a value is not an array and is specified multiple times on the command line
## then the last entry will be used as the value.
##
## Positional argument values are parsed after all named arguments are parsed.
## This means that other options can appear before, between, and after positional
## arguments.
## --foo=bar positional_0_value --disabled --bar foo positional_1_value --a_flag
##
## Anything that is not used by named or positional arguments will appear in the
## unused property. You can use this to detect unrecognized arguments or treat
## everything else provided as a list of things, or whatever you want. You can
## use is_option on the elements of unused (or whatever you want really) to see
## if optparse would treat it as an option name.
##
## Use get_missing_required_options to get an array of Option with all required
## options that were not found when parsing.
##
## The parsed_args property holds the list of arguments that were parsed.
##
##
## Help Generation
## ---------------
## You can call get_help to generate help text, or you can just call print_help
## and this will print it for you.
##
## Set the banner property to any text you want to appear before the usage and
## options sections.
##
## Options are printed in the order they are added. You can add a heading for
## different options sections with add_heading.
## add("--asdf", 1, "This will have no heading")
## add_heading("foo")
## add("--foo", false, "This will have the foo heading")
## add("--another_foo", 1.5, "This too.")
## add_heading("This is after foo")
## add("--bar", true, "You probably get it by now.")
##
## If you include "[default]" in the description of a option, then the help will
## substitue it with the default value.
## [/codeblock]
#-------------------------------------------------------------------------------
# Holds all the properties of a command line option
#
# value will return the default when it has not been set.
#-------------------------------------------------------------------------------
class Option:
var _has_been_set = false
var _value = null
# REMEMBER that when this option is an array, you have to set the value
# before you alter the contents of the array (append etc) or has_been_set
# will return false and it might not be used right. For example
# get_value_or_null will return null when you've actually changed the value.
var value = _value:
get:
return _value
set(val):
_has_been_set = true
_value = val
var option_name = ''
var default = null
var description = ''
var required = false
var aliases: Array[String] = []
var show_in_help = true
func _init(name,default_value,desc=''):
option_name = name
default = default_value
description = desc
_value = default
func wrap_text(text, left_indent, max_length, wiggle_room=15):
var line_indent = str("\n", " ".repeat(left_indent + 1))
var wrapped = ''
var position = 0
var split_length = max_length
while(position < text.length()):
if(position > 0):
wrapped += line_indent
var split_by = split_length
if(position + split_by + wiggle_room >= text.length()):
split_by = text.length() - position
else:
var min_space = text.rfind(' ', position + split_length)
var max_space = text.find(' ', position + split_length)
if(max_space <= position + split_length + wiggle_room):
split_by = max_space - position
else:
split_by = min_space - position
wrapped += text.substr(position, split_by).lstrip(' ')
if(position == 0):
split_length = max_length - left_indent
position += split_by
return wrapped
func to_s(min_space=0, wrap_length=100):
var line_indent = str("\n", " ".repeat(min_space + 1))
var subbed_desc = description
if not aliases.is_empty():
subbed_desc += "\naliases: " + ", ".join(aliases)
subbed_desc = subbed_desc.replace('[default]', str(default))
subbed_desc = subbed_desc.replace("\n", line_indent)
var final = str(option_name.rpad(min_space), ' ', subbed_desc)
if(wrap_length != -1):
final = wrap_text(final, min_space, wrap_length)
return final
func has_been_set():
return _has_been_set
#-------------------------------------------------------------------------------
# A struct for organizing options by a heading
#-------------------------------------------------------------------------------
class OptionHeading:
var options = []
var display = 'default'
#-------------------------------------------------------------------------------
# Organizes options by order, heading, position. Also responsible for all
# help related text generation.
#-------------------------------------------------------------------------------
class Options:
var options = []
var positional = []
var default_heading = OptionHeading.new()
var script_option = Option.new('-s', '?', 'script option provided by Godot')
var _options_by_name = {"--script": script_option, "-s": script_option}
var _options_by_heading = [default_heading]
var _cur_heading = default_heading
func add_heading(display):
var heading = OptionHeading.new()
heading.display = display
_cur_heading = heading
_options_by_heading.append(heading)
func add(option, aliases=null):
options.append(option)
_options_by_name[option.option_name] = option
_cur_heading.options.append(option)
if aliases != null:
for a in aliases:
_options_by_name[a] = option
option.aliases.assign(aliases)
func add_positional(option):
positional.append(option)
_options_by_name[option.option_name] = option
func get_by_name(option_name):
var found_param = null
if(_options_by_name.has(option_name)):
found_param = _options_by_name[option_name]
return found_param
func get_help_text():
var longest = 0
var text = ""
for i in range(options.size()):
if(options[i].option_name.length() > longest):
longest = options[i].option_name.length()
for heading in _options_by_heading:
if(heading != default_heading):
text += str("\n", heading.display, "\n")
for option in heading.options:
if(option.show_in_help):
text += str(' ', option.to_s(longest + 2).replace("\n", "\n "), "\n")
return text
func get_option_value_text():
var text = ""
var i = 0
for option in positional:
text += str(i, '. ', option.option_name, ' = ', option.value)
if(!option.has_been_set()):
text += " (default)"
text += "\n"
i += 1
for option in options:
text += str(option.option_name, ' = ', option.value)
if(!option.has_been_set()):
text += " (default)"
text += "\n"
return text
func print_option_values():
print(get_option_value_text())
func get_missing_required_options():
var to_return = []
for opt in options:
if(opt.required and !opt.has_been_set()):
to_return.append(opt)
for opt in positional:
if(opt.required and !opt.has_been_set()):
to_return.append(opt)
return to_return
func get_usage_text():
var pos_text = ""
for opt in positional:
pos_text += str("[", opt.description, "] ")
if(pos_text != ""):
pos_text += " [opts] "
return "<path to godot> -s " + script_option.value + " [opts] " + pos_text
#-------------------------------------------------------------------------------
#
# optarse
#
#-------------------------------------------------------------------------------
## @ignore
var options := Options.new()
## Set the banner property to any text you want to appear before the usage and
## options sections when printing the options help.
var banner := ''
## optparse uses option_name_prefix to differentiate between option names and
## values. Any argument that starts with this value will be treated as an
## argument name. The default is "-". Set this before calling parse if you want
## to change it.
var option_name_prefix := '-'
## @ignore
var unused = []
## @ignore
var parsed_args = []
## @ignore
var values: Dictionary = {}
func _populate_values_dictionary():
for entry in options.options:
var value_key = entry.option_name.lstrip('-')
values[value_key] = entry.value
for entry in options.positional:
var value_key = entry.option_name.lstrip('-')
values[value_key] = entry.value
func _convert_value_to_array(raw_value):
var split = raw_value.split(',')
# This is what an empty set looks like from the command line. If we do
# not do this then we will always get back [''] which is not what it
# shoudl be.
if(split.size() == 1 and split[0] == ''):
split = []
return split
# REMEMBER raw_value not used for bools.
func _set_option_value(option, raw_value):
var t = typeof(option.default)
# only set values that were specified at the command line so that
# we can punch through default and config values correctly later.
# Without this check, you can't tell the difference between the
# defaults and what was specified, so you can't punch through
# higher level options.
if(t == TYPE_INT):
option.value = int(raw_value)
elif(t == TYPE_STRING):
option.value = str(raw_value)
elif(t == TYPE_ARRAY):
var values = _convert_value_to_array(raw_value)
if(!option.has_been_set()):
option.value = []
option.value.append_array(values)
elif(t == TYPE_BOOL):
option.value = !option.default
elif(t == TYPE_FLOAT):
option.value = float(raw_value)
elif(t == TYPE_NIL):
print(option.option_name + ' cannot be processed, it has a nil datatype')
else:
print(option.option_name + ' cannot be processed, it has unknown datatype:' + str(t))
func _parse_command_line_arguments(args):
var parsed_opts = args.duplicate()
var i = 0
var positional_index = 0
while i < parsed_opts.size():
var opt = ''
var value = ''
var entry = parsed_opts[i]
if(is_option(entry)):
if(entry.find('=') != -1):
var parts = entry.split('=')
opt = parts[0]
value = parts[1]
var the_option = options.get_by_name(opt)
if(the_option != null):
parsed_opts.remove_at(i)
_set_option_value(the_option, value)
else:
i += 1
else:
var the_option = options.get_by_name(entry)
if(the_option != null):
parsed_opts.remove_at(i)
if(typeof(the_option.default) == TYPE_BOOL):
_set_option_value(the_option, null)
elif(i < parsed_opts.size() and !is_option(parsed_opts[i])):
value = parsed_opts[i]
parsed_opts.remove_at(i)
_set_option_value(the_option, value)
else:
i += 1
else:
if(positional_index < options.positional.size()):
_set_option_value(options.positional[positional_index], entry)
parsed_opts.remove_at(i)
positional_index += 1
else:
i += 1
# this is the leftovers that were not extracted.
return parsed_opts
## Test if something is an existing argument. If [code]str(arg)[/code] begins
## with the [member option_name_prefix], it will considered true,
## otherwise it will be considered false.
func is_option(arg) -> bool:
return str(arg).begins_with(option_name_prefix)
## Adds a command line option.
## If [param op_names] is a String, this is set as the argument's name.
## If [param op_names] is an Array of Strings, all elements of the array
## will be aliases for the same argument and will be treated as such during
## parsing.
## [param default] is the default value the option will be set to if it is not
## explicitly set during parsing.
## [param desc] is a human readable text description of the option.
## If the option is successfully added, the Option object will be returned.
## If the option is not successfully added (e.g. a name collision with another
## option occurs), an error message will be printed and [code]null[/code]
## will be returned.
func add(op_names, default, desc: String) -> Option:
var op_name: String
var aliases: Array[String] = []
var new_op: Option = null
if(typeof(op_names) == TYPE_STRING):
op_name = op_names
else:
op_name = op_names[0]
aliases.assign(op_names.slice(1))
var bad_alias: int = aliases.map(
func (a: String) -> bool: return options.get_by_name(a) != null
).find(true)
if(options.get_by_name(op_name) != null):
push_error(str('Option [', op_name, '] already exists.'))
elif bad_alias != -1:
push_error(str('Option [', aliases[bad_alias], '] already exists.'))
else:
new_op = Option.new(op_name, default, desc)
options.add(new_op, aliases)
return new_op
## Adds a required command line option.
## Required options that have not been set may be collected after parsing
## by calling [method get_missing_required_options].
## If [param op_names] is a String, this is set as the argument's name.
## If [param op_names] is an Array of Strings, all elements of the array
## will be aliases for the same argument and will be treated as such during
## parsing.
## [param default] is the default value the option will be set to if it is not
## explicitly set during parsing.
## [param desc] is a human readable text description of the option.
## If the option is successfully added, the Option object will be returned.
## If the option is not successfully added (e.g. a name collision with another
## option occurs), an error message will be printed and [code]null[/code]
## will be returned.
func add_required(op_names, default, desc: String) -> Option:
var op := add(op_names, default, desc)
if(op != null):
op.required = true
return op
## Adds a positional command line option.
## Positional options are parsed by their position in the list of arguments
## are are not assigned by name by the user.
## If [param op_name] is a String, this is set as the argument's name.
## If [param op_name] is an Array of Strings, all elements of the array
## will be aliases for the same argument and will be treated as such during
## parsing.
## [param default] is the default value the option will be set to if it is not
## explicitly set during parsing.
## [param desc] is a human readable text description of the option.
## If the option is successfully added, the Option object will be returned.
## If the option is not successfully added (e.g. a name collision with another
## option occurs), an error message will be printed and [code]null[/code]
## will be returned.
func add_positional(op_name, default, desc: String) -> Option:
var new_op = null
if(options.get_by_name(op_name) != null):
push_error(str('Positional option [', op_name, '] already exists.'))
else:
new_op = Option.new(op_name, default, desc)
options.add_positional(new_op)
return new_op
## Adds a required positional command line option.
## If [param op_name] is a String, this is set as the argument's name.
## Required options that have not been set may be collected after parsing
## by calling [method get_missing_required_options].
## Positional options are parsed by their position in the list of arguments
## are are not assigned by name by the user.
## If [param op_name] is an Array of Strings, all elements of the array
## will be aliases for the same argument and will be treated as such during
## parsing.
## [param default] is the default value the option will be set to if it is not
## explicitly set during parsing.
## [param desc] is a human readable text description of the option.
## If the option is successfully added, the Option object will be returned.
## If the option is not successfully added (e.g. a name collision with another
## option occurs), an error message will be printed and [code]null[/code]
## will be returned.
func add_positional_required(op_name, default, desc: String) -> Option:
var op = add_positional(op_name, default, desc)
if(op != null):
op.required = true
return op
## Headings are used to separate logical groups of command line options
## when printing out options from the help menu.
## Headings are printed out between option descriptions in the order
## that [method add_heading] was called.
func add_heading(display_text: String) -> void:
options.add_heading(display_text)
## Gets the value assigned to an option after parsing.
## [param name] can be the name of the option or an alias of it.
## [param name] specifies the option whose value you wish to query.
## If the option exists, the value assigned to it during parsing is returned.
## Otherwise, an error message is printed and [code]null[/code] is returned.
func get_value(name: String):
var found_param: Option = options.get_by_name(name)
if(found_param != null):
return found_param.value
else:
push_error("COULD NOT FIND OPTION " + name)
return null
## Gets the value assigned to an option after parsing,
## returning null if the option was not assigned instead of its default value.
## [param name] specifies the option whose value you wish to query.
## This can be useful when providing an order of precedence to your values.
## For example if
## [codeblock]
## default value < config file < command line
## [/codeblock]
## then you do not want to get the default value for a command line option or
## it will overwrite the value in a config file.
func get_value_or_null(name: String):
var found_param: Option = options.get_by_name(name)
if(found_param != null and found_param.has_been_set()):
return found_param.value
else:
return null
## Returns the help text for all defined options.
func get_help() -> String:
var sep := '---------------------------------------------------------'
var text := str(sep, "\n", banner, "\n\n")
text += "Usage\n-----------\n"
text += " " + options.get_usage_text() + "\n\n"
text += "\nOptions\n-----------\n"
text += options.get_help_text()
text += str(sep, "\n")
return text
## Prints out the help text for all defined options.
func print_help() -> void:
print(get_help())
## Parses a string for all options that have been set in this optparse.
## if [param cli_args] is passed as a String, then it is parsed.
## Otherwise if [param cli_args] is null,
## aruments passed to the Godot engine at startup are parsed.
## See the explanation at the top of addons/gut/cli/optparse.gd to understand
## which arguments this will have access to.
func parse(cli_args=null) -> void:
parsed_args = cli_args
if(parsed_args == null):
parsed_args = OS.get_cmdline_args()
parsed_args.append_array(OS.get_cmdline_user_args())
unused = _parse_command_line_arguments(parsed_args)
_populate_values_dictionary()
## Get all options that were required and were not set during parsing.
## The return value is an Array of Options.
func get_missing_required_options() -> Array:
return options.get_missing_required_options()
# ##############################################################################
# The MIT License (MIT)
# =====================
#
# Copyright (c) 2025 Tom "Butch" Wesley
#
# 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.
#
# ##############################################################################

View File

@@ -1 +0,0 @@
uid://c8m4fojwln6bq

View File

@@ -1,208 +0,0 @@
# ------------------------------------------------------------------------------
# This holds all the meta information for a test script. It contains the
# name of the inner class and an array of CollectedTests. This does not parse
# anything, it just holds the data about parsed scripts and tests. The
# TestCollector is responsible for populating this object.
#
# This class also facilitates all the exporting and importing of tests.
# ------------------------------------------------------------------------------
var CollectedTest = GutUtils.CollectedTest
var _lgr = null
# One entry per test found in the script. Added externally by TestCollector
var tests = []
# One entry for before_all and after_all (maybe add before_each and after_each).
# These are added by Gut when running before_all and after_all for the script.
var setup_teardown_tests = []
var inner_class_name:StringName
var path:String
# Set externally by test_collector after it can verify that the script was
# actually loaded. This could probably be changed to just hold the GutTest
# script that was loaded, cutting down on complexity elsewhere.
var is_loaded = false
# Set by Gut when it decides that a script should be skipped.
# Right now this is whenever the script has the variable skip_script declared.
# the value of skip_script is put into skip_reason.
var was_skipped = false
var skip_reason = ''
var was_run = false
var name = '' :
get: return path
set(val):pass
func _init(logger=null):
_lgr = logger
func get_new():
var inst = load_script().new()
inst.collected_script = self
return inst
func load_script():
var to_return = load(path)
if(inner_class_name != null and inner_class_name != ''):
# If we wanted to do inner classes in inner classses
# then this would have to become some kind of loop or recursive
# call to go all the way down the chain or this class would
# have to change to hold onto the loaded class instead of
# just path information.
to_return = to_return.get(inner_class_name)
return to_return
# script.gd.InnerClass
func get_filename_and_inner():
var to_return = get_filename()
if(inner_class_name != ''):
to_return += '.' + String(inner_class_name)
return to_return
# res://foo/bar.gd.FooBar
func get_full_name():
var to_return = path
if(inner_class_name != ''):
to_return += '.' + String(inner_class_name)
return to_return
func get_filename():
return path.get_file()
func has_inner_class():
return inner_class_name != ''
# Note: although this no longer needs to export the inner_class names since
# they are pulled from metadata now, it is easier to leave that in
# so we don't have to cut the export down to unique script names.
func export_to(config_file, section):
config_file.set_value(section, 'path', path)
config_file.set_value(section, 'inner_class', inner_class_name)
var names = []
for i in range(tests.size()):
names.append(tests[i].name)
config_file.set_value(section, 'tests', names)
func _remap_path(source_path):
var to_return = source_path
if(!FileAccess.file_exists(source_path)):
_lgr.debug('Checking for remap for: ' + source_path)
var remap_path = source_path.get_basename() + '.gd.remap'
if(FileAccess.file_exists(remap_path)):
var cf = ConfigFile.new()
cf.load(remap_path)
to_return = cf.get_value('remap', 'path')
else:
_lgr.warn('Could not find remap file ' + remap_path)
return to_return
func import_from(config_file, section):
path = config_file.get_value(section, 'path')
path = _remap_path(path)
# Null is an acceptable value, but you can't pass null as a default to
# get_value since it thinks you didn't send a default...then it spits
# out red text. This works around that.
var inner_name = config_file.get_value(section, 'inner_class', 'Placeholder')
if(inner_name != 'Placeholder'):
inner_class_name = inner_name
else: # just being explicit
inner_class_name = StringName("")
func get_test_named(test_name):
return GutUtils.search_array(tests, 'name', test_name)
func get_ran_test_count():
var count = 0
for t in tests:
if(t.was_run):
count += 1
return count
func get_assert_count():
var count = 0
for t in tests:
count += t.pass_texts.size()
count += t.fail_texts.size()
for t in setup_teardown_tests:
count += t.pass_texts.size()
count += t.fail_texts.size()
return count
func get_pass_count():
var count = 0
for t in tests:
count += t.pass_texts.size()
for t in setup_teardown_tests:
count += t.pass_texts.size()
return count
func get_fail_count():
var count = 0
for t in tests:
count += t.fail_texts.size()
for t in setup_teardown_tests:
count += t.fail_texts.size()
return count
func get_pending_count():
var count = 0
for t in tests:
count += t.pending_texts.size()
return count
func get_passing_test_count():
var count = 0
for t in tests:
if(t.is_passing()):
count += 1
return count
func get_failing_test_count():
var count = 0
for t in tests:
if(t.is_failing()):
count += 1
return count
func get_risky_count():
var count = 0
if(was_skipped):
count = 1
else:
for t in tests:
if(t.is_risky()):
count += 1
return count
func to_s():
var to_return = path
if(inner_class_name != null):
to_return += str('.', inner_class_name)
to_return += "\n"
for i in range(tests.size()):
to_return += str(' ', tests[i].to_s())
return to_return

View File

@@ -1 +0,0 @@
uid://bjjcnr1oqvag6

View File

@@ -1,120 +0,0 @@
# ------------------------------------------------------------------------------
# Used to keep track of info about each test ran.
# ------------------------------------------------------------------------------
# the name of the function
var name = ""
# flag to know if the name has been printed yet. Used by the logger.
var has_printed_name = false
# the number of arguments the method has
var arg_count = 0
# the time it took to execute the test in seconds
var time_taken : float = 0
# The number of asserts in the test. Converted to a property for backwards
# compatibility. This now reflects the text sizes instead of being a value
# that can be altered externally.
var assert_count = 0 :
get: return pass_texts.size() + fail_texts.size()
set(val): pass
# Converted to propety for backwards compatibility. This now cannot be set
# externally
var pending = false :
get: return is_pending()
set(val): pass
# the line number when the test fails
var line_number = -1
# Set internally by Gut using whatever reason Gut wants to use to set this.
# Gut will skip these marked true and the test will be listed as risky.
var should_skip = false # -- Currently not used by GUT don't believe ^
var pass_texts = []
var fail_texts = []
var pending_texts = []
var orphans = 0
var was_run = false
var collected_script : WeakRef = null
func did_pass():
return is_passing()
func add_fail(fail_text):
fail_texts.append(fail_text)
func add_pending(pending_text):
pending_texts.append(pending_text)
func add_pass(passing_text):
pass_texts.append(passing_text)
# must have passed an assert and not have any other status to be passing
func is_passing():
return pass_texts.size() > 0 and fail_texts.size() == 0 and pending_texts.size() == 0
# failing takes precedence over everything else, so any failures makes the
# test a failure.
func is_failing():
return fail_texts.size() > 0
# test is only pending if pending was called and the test is not failing.
func is_pending():
return pending_texts.size() > 0 and fail_texts.size() == 0
func is_risky():
return should_skip or (was_run and !did_something())
func did_something():
return is_passing() or is_failing() or is_pending()
func get_status_text():
var to_return = GutUtils.TEST_STATUSES.NO_ASSERTS
if(should_skip):
to_return = GutUtils.TEST_STATUSES.SKIPPED
elif(!was_run):
to_return = GutUtils.TEST_STATUSES.NOT_RUN
elif(pending_texts.size() > 0):
to_return = GutUtils.TEST_STATUSES.PENDING
elif(fail_texts.size() > 0):
to_return = GutUtils.TEST_STATUSES.FAILED
elif(pass_texts.size() > 0):
to_return = GutUtils.TEST_STATUSES.PASSED
return to_return
# Deprecated
func get_status():
return get_status_text()
func to_s():
var pad = ' '
var to_return = str(name, "[", get_status_text(), "]\n")
for i in range(fail_texts.size()):
to_return += str(pad, 'Fail: ', fail_texts[i])
for i in range(pending_texts.size()):
to_return += str(pad, 'Pending: ', pending_texts[i], "\n")
for i in range(pass_texts.size()):
to_return += str(pad, 'Pass: ', pass_texts[i], "\n")
return to_return

View File

@@ -1 +0,0 @@
uid://cl854f1m26a2a

View File

@@ -1,125 +0,0 @@
var _strutils = GutUtils.Strutils.new()
var _max_length = 100
var _should_compare_int_to_float = true
const MISSING = '|__missing__gut__compare__value__|'
func _cannot_compare_text(v1, v2):
return str('Cannot compare ', _strutils.types[typeof(v1)], ' with ',
_strutils.types[typeof(v2)], '.')
func _make_missing_string(text):
return '<missing ' + text + '>'
func _create_missing_result(v1, v2, text):
var to_return = null
var v1_str = format_value(v1)
var v2_str = format_value(v2)
if(typeof(v1) == TYPE_STRING and v1 == MISSING):
v1_str = _make_missing_string(text)
to_return = GutUtils.CompareResult.new()
elif(typeof(v2) == TYPE_STRING and v2 == MISSING):
v2_str = _make_missing_string(text)
to_return = GutUtils.CompareResult.new()
if(to_return != null):
to_return.summary = str(v1_str, ' != ', v2_str)
to_return.are_equal = false
return to_return
func simple(v1, v2, missing_string=''):
var missing_result = _create_missing_result(v1, v2, missing_string)
if(missing_result != null):
return missing_result
var result = GutUtils.CompareResult.new()
var cmp_str = null
var extra = ''
var tv1 = typeof(v1)
var tv2 = typeof(v2)
# print(tv1, '::', tv2, ' ', _strutils.types[tv1], '::', _strutils.types[tv2])
if(_should_compare_int_to_float and [TYPE_INT, TYPE_FLOAT].has(tv1) and [TYPE_INT, TYPE_FLOAT].has(tv2)):
result.are_equal = v1 == v2
elif([TYPE_STRING, TYPE_STRING_NAME].has(tv1) and [TYPE_STRING, TYPE_STRING_NAME].has(tv2)):
result.are_equal = v1 == v2
elif(GutUtils.are_datatypes_same(v1, v2)):
result.are_equal = v1 == v2
if(typeof(v1) == TYPE_DICTIONARY or typeof(v1) == TYPE_ARRAY):
var sub_result = GutUtils.DiffTool.new(v1, v2, GutUtils.DIFF.DEEP)
result.summary = sub_result.get_short_summary()
if(!sub_result.are_equal):
extra = ".\n" + sub_result.get_short_summary()
else:
cmp_str = '!='
result.are_equal = false
extra = str('. ', _cannot_compare_text(v1, v2))
cmp_str = get_compare_symbol(result.are_equal)
result.summary = str(format_value(v1), ' ', cmp_str, ' ', format_value(v2), extra)
return result
func shallow(v1, v2):
var result = null
if(GutUtils.are_datatypes_same(v1, v2)):
if(typeof(v1) in [TYPE_ARRAY, TYPE_DICTIONARY]):
result = GutUtils.DiffTool.new(v1, v2, GutUtils.DIFF.DEEP)
else:
result = simple(v1, v2)
else:
result = simple(v1, v2)
return result
func deep(v1, v2):
var result = null
if(GutUtils.are_datatypes_same(v1, v2)):
if(typeof(v1) in [TYPE_ARRAY, TYPE_DICTIONARY]):
result = GutUtils.DiffTool.new(v1, v2, GutUtils.DIFF.DEEP)
else:
result = simple(v1, v2)
else:
result = simple(v1, v2)
return result
func format_value(val, max_val_length=_max_length):
return _strutils.truncate_string(_strutils.type2str(val), max_val_length)
func compare(v1, v2, diff_type=GutUtils.DIFF.SIMPLE):
var result = null
if(diff_type == GutUtils.DIFF.SIMPLE):
result = simple(v1, v2)
elif(diff_type == GutUtils.DIFF.DEEP):
result = deep(v1, v2)
return result
func get_should_compare_int_to_float():
return _should_compare_int_to_float
func set_should_compare_int_to_float(should_compare_int_float):
_should_compare_int_to_float = should_compare_int_float
func get_compare_symbol(is_equal):
if(is_equal):
return '=='
else:
return '!='

View File

@@ -1 +0,0 @@
uid://bohry7fhscy7y

View File

@@ -1,70 +0,0 @@
var _are_equal = false
var are_equal = false :
get:
return get_are_equal()
set(val):
set_are_equal(val)
var _summary = null
var summary = null :
get:
return get_summary()
set(val):
set_summary(val)
var _max_differences = 30
var max_differences = 30 :
get:
return get_max_differences()
set(val):
set_max_differences(val)
var _differences = {}
var differences :
get:
return get_differences()
set(val):
set_differences(val)
func _block_set(which, val):
push_error(str('cannot set ', which, ', value [', val, '] ignored.'))
func _to_string():
return str(get_summary()) # could be null, gotta str it.
func get_are_equal():
return _are_equal
func set_are_equal(r_eq):
_are_equal = r_eq
func get_summary():
return _summary
func set_summary(smry):
_summary = smry
func get_total_count():
pass
func get_different_count():
pass
func get_short_summary():
return summary
func get_max_differences():
return _max_differences
func set_max_differences(max_diff):
_max_differences = max_diff
func get_differences():
return _differences
func set_differences(diffs):
_block_set('differences', diffs)
func get_brackets():
return null

View File

@@ -1 +0,0 @@
uid://cow1xqmqqvn4e

View File

@@ -1,63 +0,0 @@
var _strutils = GutUtils.Strutils.new()
const INDENT = ' '
var _max_to_display = 30
const ABSOLUTE_MAX_DISPLAYED = 10000
const UNLIMITED = -1
func _single_diff(diff, depth=0):
var to_return = ""
var brackets = diff.get_brackets()
if(brackets != null and !diff.are_equal):
to_return = ''
to_return += str(brackets.open, "\n",
_strutils.indent_text(differences_to_s(diff.differences, depth), depth+1, INDENT), "\n",
brackets.close)
else:
to_return = str(diff)
return to_return
func make_it(diff):
var to_return = ''
if(diff.are_equal):
to_return = diff.summary
else:
if(_max_to_display == ABSOLUTE_MAX_DISPLAYED):
to_return = str(diff.get_value_1(), ' != ', diff.get_value_2())
else:
to_return = diff.get_short_summary()
to_return += str("\n", _strutils.indent_text(_single_diff(diff, 0), 1, ' '))
return to_return
func differences_to_s(differences, depth=0):
var to_return = ''
var keys = differences.keys()
keys.sort()
var limit = min(_max_to_display, differences.size())
for i in range(limit):
var key = keys[i]
to_return += str(key, ": ", _single_diff(differences[key], depth))
if(i != limit -1):
to_return += "\n"
if(differences.size() > _max_to_display):
to_return += str("\n\n... ", differences.size() - _max_to_display, " more.")
return to_return
func get_max_to_display():
return _max_to_display
func set_max_to_display(max_to_display):
_max_to_display = max_to_display
if(_max_to_display == UNLIMITED):
_max_to_display = ABSOLUTE_MAX_DISPLAYED

View File

@@ -1 +0,0 @@
uid://ch2km05phxacd

View File

@@ -1,156 +0,0 @@
extends 'res://addons/gut/compare_result.gd'
const INDENT = ' '
enum {
DEEP,
SIMPLE
}
var _strutils = GutUtils.Strutils.new()
var _compare = GutUtils.Comparator.new()
var _value_1 = null
var _value_2 = null
var _total_count = 0
var _diff_type = null
var _brackets = null
var _valid = true
var _desc_things = 'somethings'
# -------- comapre_result.gd "interface" ---------------------
func set_are_equal(val):
_block_set('are_equal', val)
func get_are_equal():
if(!_valid):
return null
else:
return differences.size() == 0
func set_summary(val):
_block_set('summary', val)
func get_summary():
return summarize()
func get_different_count():
return differences.size()
func get_total_count():
return _total_count
func get_short_summary():
var text = str(_strutils.truncate_string(str(_value_1), 50),
' ', _compare.get_compare_symbol(are_equal), ' ',
_strutils.truncate_string(str(_value_2), 50))
if(!are_equal):
text += str(' ', get_different_count(), ' of ', get_total_count(),
' ', _desc_things, ' do not match.')
return text
func get_brackets():
return _brackets
# -------- comapre_result.gd "interface" ---------------------
func _invalidate():
_valid = false
differences = null
func _init(v1,v2,diff_type=DEEP):
_value_1 = v1
_value_2 = v2
_diff_type = diff_type
_compare.set_should_compare_int_to_float(false)
_find_differences(_value_1, _value_2)
func _find_differences(v1, v2):
if(GutUtils.are_datatypes_same(v1, v2)):
if(typeof(v1) == TYPE_ARRAY):
_brackets = {'open':'[', 'close':']'}
_desc_things = 'indexes'
_diff_array(v1, v2)
elif(typeof(v2) == TYPE_DICTIONARY):
_brackets = {'open':'{', 'close':'}'}
_desc_things = 'keys'
_diff_dictionary(v1, v2)
else:
_invalidate()
GutUtils.get_logger().error('Only Arrays and Dictionaries are supported.')
else:
_invalidate()
GutUtils.get_logger().error('Only Arrays and Dictionaries are supported.')
func _diff_array(a1, a2):
_total_count = max(a1.size(), a2.size())
for i in range(a1.size()):
var result = null
if(i < a2.size()):
if(_diff_type == DEEP):
result = _compare.deep(a1[i], a2[i])
else:
result = _compare.simple(a1[i], a2[i])
else:
result = _compare.simple(a1[i], _compare.MISSING, 'index')
if(!result.are_equal):
differences[i] = result
if(a1.size() < a2.size()):
for i in range(a1.size(), a2.size()):
differences[i] = _compare.simple(_compare.MISSING, a2[i], 'index')
func _diff_dictionary(d1, d2):
var d1_keys = d1.keys()
var d2_keys = d2.keys()
# Process all the keys in d1
_total_count += d1_keys.size()
for key in d1_keys:
if(!d2.has(key)):
differences[key] = _compare.simple(d1[key], _compare.MISSING, 'key')
else:
d2_keys.remove_at(d2_keys.find(key))
var result = null
if(_diff_type == DEEP):
result = _compare.deep(d1[key], d2[key])
else:
result = _compare.simple(d1[key], d2[key])
if(!result.are_equal):
differences[key] = result
# Process all the keys in d2 that didn't exist in d1
_total_count += d2_keys.size()
for i in range(d2_keys.size()):
differences[d2_keys[i]] = _compare.simple(_compare.MISSING, d2[d2_keys[i]], 'key')
func summarize():
var summary = ''
if(are_equal):
summary = get_short_summary()
else:
var formatter = load('res://addons/gut/diff_formatter.gd').new()
formatter.set_max_to_display(max_differences)
summary = formatter.make_it(self)
return summary
func get_diff_type():
return _diff_type
func get_value_1():
return _value_1
func get_value_2():
return _value_2

View File

@@ -1 +0,0 @@
uid://beoxokvl1hjs8

View File

@@ -1,9 +0,0 @@
{func_decleration}
if(__gutdbl == null):
return
__gutdbl.spy_on('{method_name}', {param_array})
if(__gutdbl.is_stubbed_to_call_super('{method_name}', {param_array})):
return {super_call}
else:
return await __gutdbl.handle_other_stubs('{method_name}', {param_array})

View File

@@ -1,4 +0,0 @@
{func_decleration}:
super({super_params})
__gutdbl.spy_on('{method_name}', {param_array})

View File

@@ -1,37 +0,0 @@
# ##############################################################################
# Gut Doubled Script
# ##############################################################################
{extends}
{constants}
{properties}
# ------------------------------------------------------------------------------
# GUT stuff
# ------------------------------------------------------------------------------
var __gutdbl_values = {
thepath = '{path}',
subpath = '{subpath}',
stubber = {stubber_id},
spy = {spy_id},
gut = {gut_id},
from_singleton = '{singleton_name}',
is_partial = {is_partial},
doubled_methods = {doubled_methods},
}
var __gutdbl = load('res://addons/gut/double_tools.gd').new(self)
# Here so other things can check for a method to know if this is a double.
func __gutdbl_check_method__():
pass
# Cleanup called by GUT after tests have finished. Important for RefCounted
# objects. Nodes are freed, and won't have this method called on them.
func __gutdbl_done():
__gutdbl = null
__gutdbl_values.clear()
# ------------------------------------------------------------------------------
# Doubled Methods
# ------------------------------------------------------------------------------

View File

@@ -1,70 +0,0 @@
var thepath = ''
var subpath = ''
var from_singleton = null
var is_partial = null
var double_ref : WeakRef = null
var stubber_ref : WeakRef = null
var spy_ref : WeakRef = null
var gut_ref : WeakRef = null
const NO_DEFAULT_VALUE = '!__gut__no__default__value__!'
func _init(double = null):
if(double != null):
var values = double.__gutdbl_values
double_ref = weakref(double)
thepath = values.thepath
subpath = values.subpath
stubber_ref = weakref_from_id(values.stubber)
spy_ref = weakref_from_id(values.spy)
gut_ref = weakref_from_id(values.gut)
from_singleton = values.from_singleton
is_partial = values.is_partial
if(gut_ref.get_ref() != null):
gut_ref.get_ref().get_autofree().add_free(double_ref.get_ref())
func _get_stubbed_method_to_call(method_name, called_with):
var method = stubber_ref.get_ref().get_call_this(double_ref.get_ref(), method_name, called_with)
if(method != null):
method = method.bindv(called_with)
return method
return method
func weakref_from_id(inst_id):
if(inst_id == -1):
return weakref(null)
else:
return weakref(instance_from_id(inst_id))
func is_stubbed_to_call_super(method_name, called_with):
if(stubber_ref.get_ref() != null):
return stubber_ref.get_ref().should_call_super(double_ref.get_ref(), method_name, called_with)
else:
return false
func handle_other_stubs(method_name, called_with):
if(stubber_ref.get_ref() == null):
return
var method = _get_stubbed_method_to_call(method_name, called_with)
if(method != null):
return await method.call()
else:
return stubber_ref.get_ref().get_return(double_ref.get_ref(), method_name, called_with)
func spy_on(method_name, called_with):
if(spy_ref.get_ref() != null):
spy_ref.get_ref().add_call(double_ref.get_ref(), method_name, called_with)
func default_val(method_name, p_index):
if(stubber_ref.get_ref() == null):
return null
else:
return stubber_ref.get_ref().get_default_value(double_ref.get_ref(), method_name, p_index)

View File

@@ -1 +0,0 @@
uid://tr4khoco1hef

View File

@@ -1,312 +0,0 @@
extends RefCounted
var _base_script_text = GutUtils.get_file_as_text('res://addons/gut/double_templates/script_template.txt')
var _script_collector = GutUtils.ScriptCollector.new()
# used by tests for debugging purposes.
var print_source = false
var inner_class_registry = GutUtils.InnerClassRegistry.new()
# ###############
# Properties
# ###############
var _stubber = GutUtils.Stubber.new()
func get_stubber():
return _stubber
func set_stubber(stubber):
_stubber = stubber
var _lgr = GutUtils.get_logger()
func get_logger():
return _lgr
func set_logger(logger):
_lgr = logger
_method_maker.set_logger(logger)
var _spy = null
func get_spy():
return _spy
func set_spy(spy):
_spy = spy
var _gut = null
func get_gut():
return _gut
func set_gut(gut):
_gut = gut
var _strategy = null
func get_strategy():
return _strategy
func set_strategy(strategy):
if(GutUtils.DOUBLE_STRATEGY.values().has(strategy)):
_strategy = strategy
else:
_lgr.error(str('doubler.gd: invalid double strategy ', strategy))
var _method_maker = GutUtils.MethodMaker.new()
func get_method_maker():
return _method_maker
var _ignored_methods = GutUtils.OneToMany.new()
func get_ignored_methods():
return _ignored_methods
# ###############
# Private
# ###############
func _init(strategy=GutUtils.DOUBLE_STRATEGY.SCRIPT_ONLY):
set_logger(GutUtils.get_logger())
_strategy = strategy
func _get_indented_line(indents, text):
var to_return = ''
for _i in range(indents):
to_return += "\t"
return str(to_return, text, "\n")
func _stub_to_call_super(parsed, method_name):
if(!parsed.get_method(method_name).is_eligible_for_doubling()):
return
var params = GutUtils.StubParams.new(parsed.script_path, method_name, parsed.subpath)
params.to_call_super()
_stubber.add_stub(params)
func _get_base_script_text(parsed, override_path, partial, included_methods):
var path = parsed.script_path
if(override_path != null):
path = override_path
var stubber_id = -1
if(_stubber != null):
stubber_id = _stubber.get_instance_id()
var spy_id = -1
if(_spy != null):
spy_id = _spy.get_instance_id()
var gut_id = -1
if(_gut != null):
gut_id = _gut.get_instance_id()
var extends_text = parsed.get_extends_text()
var values = {
# Top sections
"extends":extends_text,
"constants":'',#obj_info.get_constants_text(),
"properties":'',#obj_info.get_properties_text(),
# metadata values
"path":path,
"subpath":GutUtils.nvl(parsed.subpath, ''),
"stubber_id":stubber_id,
"spy_id":spy_id,
"gut_id":gut_id,
"singleton_name":'',#GutUtils.nvl(obj_info.get_singleton_name(), ''),
"is_partial":partial,
"doubled_methods":included_methods,
}
return _base_script_text.format(values)
func _is_method_eligible_for_doubling(parsed_script, parsed_method):
return !parsed_method.is_accessor() and \
parsed_method.is_eligible_for_doubling() and \
!_ignored_methods.has(parsed_script.resource, parsed_method.meta.name)
# Disable the native_method_override setting so that doubles do not generate
# errors or warnings when doubling with INCLUDE_NATIVE or when a method has
# been added because of param_count stub.
func _create_script_no_warnings(src):
var prev_native_override_value = null
var native_method_override = 'debug/gdscript/warnings/native_method_override'
prev_native_override_value = ProjectSettings.get_setting(native_method_override)
ProjectSettings.set_setting(native_method_override, 0)
var DblClass = GutUtils.create_script_from_source(src)
ProjectSettings.set_setting(native_method_override, prev_native_override_value)
return DblClass
func _create_double(parsed, strategy, override_path, partial):
var dbl_src = ""
var included_methods = []
for method in parsed.get_local_methods():
if(_is_method_eligible_for_doubling(parsed, method)):
included_methods.append(method.meta.name)
dbl_src += _get_func_text(method.meta)
if(strategy == GutUtils.DOUBLE_STRATEGY.INCLUDE_NATIVE):
for method in parsed.get_super_methods():
if(_is_method_eligible_for_doubling(parsed, method)):
included_methods.append(method.meta.name)
_stub_to_call_super(parsed, method.meta.name)
dbl_src += _get_func_text(method.meta)
var base_script = _get_base_script_text(parsed, override_path, partial, included_methods)
dbl_src = base_script + "\n\n" + dbl_src
if(print_source):
var to_print :String = GutUtils.add_line_numbers(dbl_src)
to_print = to_print.rstrip("\n")
_lgr.log(str(to_print))
var DblClass = _create_script_no_warnings(dbl_src)
if(_stubber != null):
_stub_method_default_values(DblClass, parsed, strategy)
if(print_source):
_lgr.log(str(" path | ", DblClass.resource_path, "\n"))
return DblClass
func _stub_method_default_values(which, parsed, strategy):
for method in parsed.get_local_methods():
if(method.is_eligible_for_doubling() and !_ignored_methods.has(parsed.resource, method.meta.name)):
_stubber.stub_defaults_from_meta(parsed.script_path, method.meta)
func _double_scene_and_script(scene, strategy, partial):
var dbl_bundle = scene._bundled.duplicate(true)
var script_obj = GutUtils.get_scene_script_object(scene)
# I'm not sure if the script object for the root node of a packed scene is
# always the first entry in "variants" so this tries to find it.
var script_index = dbl_bundle["variants"].find(script_obj)
var script_dbl = null
if(script_obj != null):
if(partial):
script_dbl = _partial_double(script_obj, strategy, scene.get_path())
else:
script_dbl = _double(script_obj, strategy, scene.get_path())
if(script_index != -1):
dbl_bundle["variants"][script_index] = script_dbl
var doubled_scene = PackedScene.new()
doubled_scene._set_bundled_scene(dbl_bundle)
return doubled_scene
func _get_inst_id_ref_str(inst):
var ref_str = 'null'
if(inst):
ref_str = str('instance_from_id(', inst.get_instance_id(),')')
return ref_str
func _get_func_text(method_hash):
return _method_maker.get_function_text(method_hash) + "\n"
func _parse_script(obj):
var parsed = null
if(GutUtils.is_inner_class(obj)):
if(inner_class_registry.has(obj)):
parsed = _script_collector.parse(inner_class_registry.get_base_resource(obj), obj)
else:
_lgr.error('Doubling Inner Classes requires you register them first. Call register_inner_classes passing the script that contains the inner class.')
else:
parsed = _script_collector.parse(obj)
return parsed
# Override path is used with scenes.
func _double(obj, strategy, override_path=null):
var parsed = _parse_script(obj)
if(parsed != null):
return _create_double(parsed, strategy, override_path, false)
func _partial_double(obj, strategy, override_path=null):
var parsed = _parse_script(obj)
if(parsed != null):
return _create_double(parsed, strategy, override_path, true)
# -------------------------
# Public
# -------------------------
# double a script/object
func double(obj, strategy=_strategy):
return _double(obj, strategy)
func partial_double(obj, strategy=_strategy):
return _partial_double(obj, strategy)
# double a scene
func double_scene(scene, strategy=_strategy):
return _double_scene_and_script(scene, strategy, false)
func partial_double_scene(scene, strategy=_strategy):
return _double_scene_and_script(scene, strategy, true)
func double_gdnative(which):
return _double(which, GutUtils.DOUBLE_STRATEGY.INCLUDE_NATIVE)
func partial_double_gdnative(which):
return _partial_double(which, GutUtils.DOUBLE_STRATEGY.INCLUDE_NATIVE)
func double_inner(parent, inner, strategy=_strategy):
var parsed = _script_collector.parse(parent, inner)
return _create_double(parsed, strategy, null, false)
func partial_double_inner(parent, inner, strategy=_strategy):
var parsed = _script_collector.parse(parent, inner)
return _create_double(parsed, strategy, null, true)
func add_ignored_method(obj, method_name):
_ignored_methods.add(obj, method_name)
# ##############################################################################
#(G)odot (U)nit (T)est class
#
# ##############################################################################
# The MIT License (MIT)
# =====================
#
# Copyright (c) 2025 Tom "Butch" Wesley
#
# 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.
#
# ##############################################################################

View File

@@ -1 +0,0 @@
uid://cpy013l0wqwmg

View File

@@ -1,33 +0,0 @@
@tool
var default_script_name_no_extension = 'gut_dynamic_script'
var default_script_resource_path = 'res://addons/gut/not_a_real_file/'
var default_script_extension = "gd"
var _created_script_count = 0
# Creates a loaded script from the passed in source. This loaded script is
# returned unless there is an error. When an error occcurs the error number
# is returned instead.
func create_script_from_source(source, override_path=null):
_created_script_count += 1
var r_path = str(default_script_resource_path,
default_script_name_no_extension, '_', _created_script_count, ".",
default_script_extension)
if(override_path != null):
r_path = override_path
var DynamicScript = GDScript.new()
DynamicScript.source_code = source.dedent()
# The resource_path must be unique or Godot thinks it is trying
# to load something it has already loaded and generates an error like
# ERROR: Another resource is loaded from path 'workaround for godot
# issue #65263' (possible cyclic resource inclusion).
DynamicScript.resource_path = r_path
var result = DynamicScript.reload()
if(result != OK):
DynamicScript = result
return DynamicScript

View File

@@ -1 +0,0 @@
uid://cnbjsrik0p5uf

View File

@@ -1,207 +0,0 @@
@tool
extends Node
# ##############################################################################
#
# Watches script editors and emits a signal whenever the method, inner class,
# or script changes based on cursor position and other stuff.
#
# Basically, whenever this thing's signal is emitted, then the RunAtCursor
# buttons should be updated to match the data passed to the signal.
# ##############################################################################
# In the editor, whenever a script is opened you get these new things that
# hang off of EditorInterface.get_script_editor()
# * ScriptEditorBase
# * CodeEdit
# ##############################################################################
var _last_info : Dictionary = {}
var _last_line = -1
# This is the control that holds all the individual editors.
var _current_script_editor : ScriptEditor = null
# Reference to the GDScript for the last script we were notified about.
var _current_script = null
var _current_script_is_test_script = false
var _current_editor_base : ScriptEditorBase = null
var _current_editor : CodeEdit = null
# Quick lookup of editors based on the current script.
var _editors_for_scripts : Dictionary= {}
# In order to keep the data that comes back from the emitted signal way more
# usable, we have to know what GUT looks for for an inner-test-class prefix.
# If we didn't do this, then this thing would have to return all the inner
# classes and then we'd have to determine if we were in an inner-test-class
# outside of here by traversing all the classes returned. It makes this thing
# less generic and know too much, but this is probably already too generic as
# it is.
var inner_class_prefix = "Test"
var method_prefix = "test_"
var script_prefix = "test_"
var script_suffix = ".gd"
# Based on cursor and open editors, this will be emitted. You do what you
# want with it.
signal it_changed(change_data)
func _ready():
# This will not change, and should not change, over the course of a session.
_current_script_editor = EditorInterface.get_script_editor()
_current_script_editor.editor_script_changed.connect(_on_editor_script_changed)
_current_script_editor.script_close.connect(_on_editor_script_close)
func _handle_caret_location(which):
var current_line = which.get_caret_line(0) + 1
if(_last_line != current_line):
_last_line = current_line
if(_current_script_is_test_script):
var new_info = _make_info(which, _current_script, _current_script_is_test_script)
if(_last_info != new_info):
_last_info = new_info
it_changed.emit(_last_info.duplicate())
func _get_func_name_from_line(text):
text = text.strip_edges()
var left = text.split("(")[0]
var func_name = left.split(" ")[1]
return func_name
func _get_class_name_from_line(text):
text = text.strip_edges()
var right = text.split(" ")[1]
var the_name = right.rstrip(":")
return the_name
func _make_info(editor, script, test_script_flag):
if(editor == null):
return
var info = {
script = script,
inner_class = null,
method = null,
is_test_script = test_script_flag
}
var start_line = editor.get_caret_line()
var line = start_line
var done_func = false
var done_inner = false
while(line > 0 and (!done_func or !done_inner)):
if(editor.can_fold_line(line)):
var text = editor.get_line(line)
var strip_text = text.strip_edges(true, false) # only left
if(!done_func and strip_text.begins_with("func ")):
info.method = _get_func_name_from_line(text)
done_func = true
# If the func line is left justified then there won't be any
# inner classes above it.
if(editor.get_indent_level(line) == 0):
done_inner = true
if(!done_inner and strip_text.begins_with("class")):
var inner_name = _get_class_name_from_line(text)
# See note about inner_class_prefix, this knows too much, but
# if it was to know less it would insanely more difficult
# everywhere.
if(inner_name.begins_with(inner_class_prefix)):
info.inner_class = inner_name
done_inner = true
done_func = true
line -= 1
# print('parsed lines: ', start_line - line, '(', info.inner_class, ':', info.method, ')')
return info
# -------------
# Events
# -------------
# Fired whenever the script changes. This does not fire if you select something
# other than a script from the tree. So if you click a help file and then
# back to the same file, then this will fire for the same script
#
# This can fire multiple times for the same script when a script is opened.
func _on_editor_script_changed(script):
_last_line = -1
_current_script = script
_current_editor_base = _current_script_editor.get_current_editor()
if(_current_editor_base.get_base_editor() is CodeEdit):
_current_editor = _current_editor_base.get_base_editor()
if(!_current_editor.caret_changed.is_connected(_on_caret_changed)):
_current_editor.caret_changed.connect(_on_caret_changed.bind(_current_editor))
else:
_current_editor = null
_editors_for_scripts[script] = _current_editor
_current_script_is_test_script = is_test_script(_current_script)
_handle_caret_location(_current_editor)
func _on_editor_script_close(script):
var script_editor = _editors_for_scripts.get(script, null)
if(script_editor != null):
if(script_editor.caret_changed.is_connected(_on_caret_changed)):
script_editor.caret_changed.disconnect(_on_caret_changed)
_editors_for_scripts.erase(script)
func _on_caret_changed(which):
# Sometimes this is fired for editors that are not the current. I could
# make this fire by saving a file in an external editor. I was unable to
# get useful data out when it wasn't the current editor so I'm only doing
# anything when it is the current editor.
if(which == _current_editor):
_handle_caret_location(which)
func _could_be_test_script(script):
return script.resource_path.get_file().begins_with(script_prefix) and \
script.resource_path.get_file().ends_with(script_suffix)
# -------------
# Public
# -------------
var _scripts_that_have_been_warned_about = []
var _we_have_warned_enough = false
var _max_warnings = 5
func is_test_script(script):
var base = script.get_base_script()
if(base == null and script.get_script_method_list().size() == 0 and _could_be_test_script(script)):
if(OS.is_stdout_verbose() or (!_scripts_that_have_been_warned_about.has(script.resource_path) and !_we_have_warned_enough)):
_scripts_that_have_been_warned_about.append(script.resource_path)
push_warning(str('[GUT] Treating ', script.resource_path, " as test script: ",
"GUT was not able to retrieve information about this script. If this is ",
"a new script you can ignore this warning. Otherwise, this may ",
"have to do with having VSCode open. Restarting Godot sometimes helps. See ",
"https://github.com/bitwes/Gut/issues/754"))
if(!OS.is_stdout_verbose() and _scripts_that_have_been_warned_about.size() >= _max_warnings):
print("[GUT] Disabling warning.")
_we_have_warned_enough = true
# We can't know if this is a test script. It's more usable if we
# assume this is a test script.
return true
else:
while(base and base.resource_path != 'res://addons/gut/test.gd'):
base = base.get_base_script()
return base != null
func get_info():
return _last_info.duplicate()
func log_values():
print("---------------------------------------------------------------")
print("script ", _current_script)
print("script_editor ", _current_script_editor)
print("editor_base ", _current_editor_base)
print("editor ", _current_editor)

View File

@@ -1 +0,0 @@
uid://c110s7a32x4su

View File

@@ -1,193 +0,0 @@
extends Logger
class_name GutErrorTracker
# ------------------------------------------------------------------------------
# Static methods wrap around add/remove logger to make disabling the logger
# easier and to help avoid misusing add/remove in tests. If GUT needs to
# add/remove a logger then this is how it should do it.
# ------------------------------------------------------------------------------
static var registered_loggers := {}
static var register_loggers = true
static func register_logger(which):
if(register_loggers and !registered_loggers.has(which)):
OS.add_logger(which)
registered_loggers[which] = get_stack()
static func deregister_logger(which):
if(registered_loggers.has(which)):
OS.remove_logger(which)
registered_loggers.erase(which)
# ------------------------------------------------------------------------------
# GutErrorTracker
# ------------------------------------------------------------------------------
var _current_test_id = GutUtils.NO_TEST
var _mutex = Mutex.new()
var errors = GutUtils.OneToMany.new()
var treat_gut_errors_as : GutUtils.TREAT_AS = GutUtils.TREAT_AS.FAILURE
var treat_engine_errors_as : GutUtils.TREAT_AS = GutUtils.TREAT_AS.FAILURE
var treat_push_error_as : GutUtils.TREAT_AS = GutUtils.TREAT_AS.FAILURE
var disabled = false
# ----------------
#region Private
# ----------------
func _get_stack_data(current_test_name):
var test_entry = {}
var stackTrace = get_stack()
if(stackTrace!=null):
var index = 0
while(index < stackTrace.size() and test_entry == {}):
var line = stackTrace[index]
var function = line.get("function")
if function == current_test_name:
test_entry = stackTrace[index]
else:
index += 1
for i in range(index):
stackTrace.remove_at(0)
return {
"test_entry" = test_entry,
"full_stack" = stackTrace
}
func _is_error_failable(error : GutTrackedError):
var is_it = false
if(error.handled == false):
if(error.is_gut_error()):
is_it = treat_gut_errors_as == GutUtils.TREAT_AS.FAILURE
elif(error.is_push_error()):
is_it = treat_push_error_as == GutUtils.TREAT_AS.FAILURE
elif(error.is_engine_error()):
is_it = treat_engine_errors_as == GutUtils.TREAT_AS.FAILURE
return is_it
# ----------------
#endregion
#region Godot's Logger Overrides
# ----------------
# Godot's Logger virtual method for errors
func _log_error(function: String, file: String, line: int,
code: String, rationale: String, editor_notify: bool,
error_type: int, script_backtraces: Array[ScriptBacktrace]) -> void:
add_error(function, file, line,
code, rationale, editor_notify,
error_type, script_backtraces)
# Godot's Logger virtual method for any output?
# func _log_message(message: String, error: bool) -> void:
# pass
# ----------------
#endregion
#region Public
# ----------------
func start_test(test_id):
_current_test_id = test_id
func end_test():
_current_test_id = GutUtils.NO_TEST
func did_test_error(test_id=_current_test_id):
return errors.size(test_id) > 0
func get_current_test_errors():
return errors.items.get(_current_test_id, [])
# This should look through all the errors for a test and see if a failure
# should happen based off of flags.
func should_test_fail_from_errors(test_id = _current_test_id):
var to_return = false
if(errors.items.has(test_id)):
var errs = errors.items[test_id]
var index = 0
while(index < errs.size() and !to_return):
var error = errs[index]
to_return = _is_error_failable(error)
index += 1
return to_return
func get_errors_for_test(test_id=_current_test_id):
var to_return = []
if(errors.items.has(test_id)):
to_return = errors.items[test_id].duplicate()
return to_return
# Returns emtpy string or text for errors that occurred during the test that
# should cause failure based on this class' flags.
func get_fail_text_for_errors(test_id=_current_test_id) -> String:
var error_texts = []
if(errors.items.has(test_id)):
for error in errors.items[test_id]:
if(_is_error_failable(error)):
error_texts.append(str('<', error.get_error_type_name(), '>', error.code))
var to_return = ""
for i in error_texts.size():
if(to_return != ""):
to_return += "\n"
to_return += str("[", i + 1, "] ", error_texts[i])
return to_return
func add_gut_error(text) -> GutTrackedError:
if(_current_test_id != GutUtils.NO_TEST):
var data = _get_stack_data(_current_test_id)
if(data.test_entry != {}):
return add_error(_current_test_id, data.test_entry.source, data.test_entry.line,
text, '', false,
GutUtils.GUT_ERROR_TYPE, data.full_stack)
return add_error(_current_test_id, "unknown", -1,
text, '', false,
GutUtils.GUT_ERROR_TYPE, get_stack())
func add_error(function: String, file: String, line: int,
code: String, rationale: String, editor_notify: bool,
error_type: int, script_backtraces: Array) -> GutTrackedError:
if(disabled):
return
_mutex.lock()
var err := GutTrackedError.new()
err.backtrace = script_backtraces
err.code = code
err.rationale = rationale
err.error_type = error_type
err.editor_notify = editor_notify
err.file = file
err.function = function
err.line = line
errors.add(_current_test_id, err)
_mutex.unlock()
return err

View File

@@ -1 +0,0 @@
uid://35kxgqotjmlu

View File

@@ -1,36 +0,0 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://c8axnpxc0nrk4"
path="res://.godot/imported/AnonymousPro-Bold.ttf-9d8fef4d357af5b52cd60afbe608aa49.fontdata"
[deps]
source_file="res://addons/gut/fonts/AnonymousPro-Bold.ttf"
dest_files=["res://.godot/imported/AnonymousPro-Bold.ttf-9d8fef4d357af5b52cd60afbe608aa49.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=1
subpixel_positioning=1
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}

View File

@@ -1,36 +0,0 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://msst1l2s2s"
path="res://.godot/imported/AnonymousPro-BoldItalic.ttf-4274bf704d3d6b9cd32c4f0754d8c83d.fontdata"
[deps]
source_file="res://addons/gut/fonts/AnonymousPro-BoldItalic.ttf"
dest_files=["res://.godot/imported/AnonymousPro-BoldItalic.ttf-4274bf704d3d6b9cd32c4f0754d8c83d.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=1
subpixel_positioning=1
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}

View File

@@ -1,36 +0,0 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://hf5rdg67jcwc"
path="res://.godot/imported/AnonymousPro-Italic.ttf-9989590b02137b799e13d570de2a42c1.fontdata"
[deps]
source_file="res://addons/gut/fonts/AnonymousPro-Italic.ttf"
dest_files=["res://.godot/imported/AnonymousPro-Italic.ttf-9989590b02137b799e13d570de2a42c1.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=1
subpixel_positioning=1
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}

View File

@@ -1,36 +0,0 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://c6c7gnx36opr0"
path="res://.godot/imported/AnonymousPro-Regular.ttf-856c843fd6f89964d2ca8d8ff1724f13.fontdata"
[deps]
source_file="res://addons/gut/fonts/AnonymousPro-Regular.ttf"
dest_files=["res://.godot/imported/AnonymousPro-Regular.ttf-856c843fd6f89964d2ca8d8ff1724f13.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=1
subpixel_positioning=1
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}

View File

@@ -1,36 +0,0 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://bhjgpy1dovmyq"
path="res://.godot/imported/CourierPrime-Bold.ttf-1f003c66d63ebed70964e7756f4fa235.fontdata"
[deps]
source_file="res://addons/gut/fonts/CourierPrime-Bold.ttf"
dest_files=["res://.godot/imported/CourierPrime-Bold.ttf-1f003c66d63ebed70964e7756f4fa235.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=1
subpixel_positioning=1
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}

View File

@@ -1,36 +0,0 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://n6mxiov5sbgc"
path="res://.godot/imported/CourierPrime-BoldItalic.ttf-65ebcc61dd5e1dfa8f96313da4ad7019.fontdata"
[deps]
source_file="res://addons/gut/fonts/CourierPrime-BoldItalic.ttf"
dest_files=["res://.godot/imported/CourierPrime-BoldItalic.ttf-65ebcc61dd5e1dfa8f96313da4ad7019.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=1
subpixel_positioning=1
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}

View File

@@ -1,36 +0,0 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://mcht266g817e"
path="res://.godot/imported/CourierPrime-Italic.ttf-baa9156a73770735a0f72fb20b907112.fontdata"
[deps]
source_file="res://addons/gut/fonts/CourierPrime-Italic.ttf"
dest_files=["res://.godot/imported/CourierPrime-Italic.ttf-baa9156a73770735a0f72fb20b907112.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=1
subpixel_positioning=1
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}

View File

@@ -1,36 +0,0 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://bnh0lslf4yh87"
path="res://.godot/imported/CourierPrime-Regular.ttf-3babe7e4a7a588dfc9a84c14b4f1fe23.fontdata"
[deps]
source_file="res://addons/gut/fonts/CourierPrime-Regular.ttf"
dest_files=["res://.godot/imported/CourierPrime-Regular.ttf-3babe7e4a7a588dfc9a84c14b4f1fe23.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=1
subpixel_positioning=1
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}

Binary file not shown.

View File

@@ -1,36 +0,0 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://cmiuntu71oyl3"
path="res://.godot/imported/LobsterTwo-Bold.ttf-7c7f734103b58a32491a4788186f3dcb.fontdata"
[deps]
source_file="res://addons/gut/fonts/LobsterTwo-Bold.ttf"
dest_files=["res://.godot/imported/LobsterTwo-Bold.ttf-7c7f734103b58a32491a4788186f3dcb.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=1
subpixel_positioning=1
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}

View File

@@ -1,36 +0,0 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://bll38n2ct6qme"
path="res://.godot/imported/LobsterTwo-BoldItalic.ttf-227406a33e84448e6aa974176016de19.fontdata"
[deps]
source_file="res://addons/gut/fonts/LobsterTwo-BoldItalic.ttf"
dest_files=["res://.godot/imported/LobsterTwo-BoldItalic.ttf-227406a33e84448e6aa974176016de19.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=1
subpixel_positioning=1
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}

View File

@@ -1,36 +0,0 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://dis65h8wxc3f2"
path="res://.godot/imported/LobsterTwo-Italic.ttf-f93abf6c25390c85ad5fb6c4ee75159e.fontdata"
[deps]
source_file="res://addons/gut/fonts/LobsterTwo-Italic.ttf"
dest_files=["res://.godot/imported/LobsterTwo-Italic.ttf-f93abf6c25390c85ad5fb6c4ee75159e.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=1
subpixel_positioning=1
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}

View File

@@ -1,36 +0,0 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://5e8msj0ih2pv"
path="res://.godot/imported/LobsterTwo-Regular.ttf-f3fcfa01cd671c8da433dd875d0fe04b.fontdata"
[deps]
source_file="res://addons/gut/fonts/LobsterTwo-Regular.ttf"
dest_files=["res://.godot/imported/LobsterTwo-Regular.ttf-f3fcfa01cd671c8da433dd875d0fe04b.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=1
subpixel_positioning=1
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}

View File

@@ -1,94 +0,0 @@
Copyright (c) 2009, Mark Simonson (http://www.ms-studio.com, mark@marksimonson.com),
with Reserved Font Name Anonymous Pro.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -1,6 +0,0 @@
# This file is here so we can load it only when we are in the editor so that
# other places do not have to have "EditorInterface" in them, which causes a
# parser error when loaded outside of the editor. The things we have to do in
# order to test things is annoying.
func get_it():
return EditorInterface

Some files were not shown because too many files have changed in this diff Show More