Compare commits
15 Commits
ui-assets
...
625fe0ff6c
| Author | SHA1 | Date | |
|---|---|---|---|
| 625fe0ff6c | |||
|
|
0e5b9f947b | ||
| 7cca58cb07 | |||
|
|
749e2c257b | ||
|
|
e335a35f6c | ||
|
|
136e1344a0 | ||
|
|
25a21f92be | ||
|
|
9c2e3bf15a | ||
|
|
9e288dbb62 | ||
|
|
c8e73bec59 | ||
|
|
16f24ab26f | ||
|
|
414225e8c1 | ||
|
|
fb7cba4088 | ||
| 7b85147994 | |||
| 43e0c2b928 |
7
.claude/settings.local.json
Normal file
7
.claude/settings.local.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Skill(whaletown-developer)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
335
.claude/skills/whaletown-developer/SKILL.md
Normal file
335
.claude/skills/whaletown-developer/SKILL.md
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
---
|
||||||
|
name: whaletown-developer
|
||||||
|
description: Automate WhaleTown project's standard development workflow. Use this skill when implementing features, fixing bugs, creating scenes, or any code development tasks. Guides through 7-step process - architecture analysis, implementation, comment/naming validation, testing, and Git commit generation following project conventions.
|
||||||
|
---
|
||||||
|
|
||||||
|
# WhaleTown Standard Development Workflow Skill
|
||||||
|
|
||||||
|
This skill automates the standard development workflow for the WhaleTown project, ensuring all developers follow unified specifications and quality standards.
|
||||||
|
|
||||||
|
## When to Use This Skill
|
||||||
|
|
||||||
|
Activate this skill when:
|
||||||
|
- Implementing new features ("实现XX功能", "添加XX系统")
|
||||||
|
- Fixing bugs ("修复XX Bug", "解决XX问题")
|
||||||
|
- Creating scenes ("创建XX场景", "设计XX界面")
|
||||||
|
- Developing modules ("开发XX模块", "构建XX组件")
|
||||||
|
- Any code development task requiring adherence to project standards
|
||||||
|
|
||||||
|
## Development Workflow Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
Step 1: Architecture Analysis (读取架构规范)
|
||||||
|
↓
|
||||||
|
Step 2: Implementation (按规范编码)
|
||||||
|
↓
|
||||||
|
Step 3: Comment Validation (注释规范检查)
|
||||||
|
↓
|
||||||
|
Step 4: Naming Validation (命名规范检查)
|
||||||
|
↓
|
||||||
|
Step 5: Test Writing (编写测试代码)
|
||||||
|
↓
|
||||||
|
Step 6: Test Execution (运行测试验证)
|
||||||
|
↓
|
||||||
|
Step 7: Git Commit (生成规范提交信息)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step-by-Step Workflow
|
||||||
|
|
||||||
|
### Step 1: Architecture Analysis
|
||||||
|
|
||||||
|
Read and apply the architecture specifications before implementation.
|
||||||
|
|
||||||
|
**Actions:**
|
||||||
|
1. Read `docs/02-开发规范/架构与通信规范.md`
|
||||||
|
2. Determine file location based on feature type:
|
||||||
|
- Core systems → `_Core/managers/` or `_Core/systems/`
|
||||||
|
- Game scenes → `scenes/Maps/`, `scenes/Entities/`, `scenes/Components/`
|
||||||
|
- UI components → `scenes/ui/`
|
||||||
|
3. Identify communication method (MUST use EventSystem for cross-module communication)
|
||||||
|
4. List dependencies (required managers and systems)
|
||||||
|
5. Design event definitions (add to `_Core/EventNames.gd`)
|
||||||
|
|
||||||
|
**Layered Architecture:**
|
||||||
|
```
|
||||||
|
UI Layer (界面层) → scenes/ui/
|
||||||
|
↑
|
||||||
|
Scenes Layer (游戏层) → scenes/Maps/, scenes/Entities/
|
||||||
|
↑
|
||||||
|
_Core Layer (框架层) → _Core/managers/, _Core/systems/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Communication Principle:** "Signal Up, Call Down"
|
||||||
|
- Parents call child methods (downward calls)
|
||||||
|
- Children emit signals to notify parents (upward signals)
|
||||||
|
- Cross-module communication MUST use EventSystem
|
||||||
|
|
||||||
|
### Step 2: Implementation
|
||||||
|
|
||||||
|
Implement the feature following strict project conventions.
|
||||||
|
|
||||||
|
**Requirements:**
|
||||||
|
- **Type Safety**: All variables and functions MUST have type annotations
|
||||||
|
```gdscript
|
||||||
|
var speed: float = 200.0
|
||||||
|
func move(delta: float) -> void:
|
||||||
|
```
|
||||||
|
- **Godot 4.2+ Syntax**: NO `yield()`, use `await`
|
||||||
|
- **Node Caching**: Use `@onready` to cache node references, avoid `get_node()` in `_process()`
|
||||||
|
- **EventSystem Communication**: Use EventSystem for cross-module messaging
|
||||||
|
```gdscript
|
||||||
|
EventSystem.emit_event(EventNames.PLAYER_MOVED, {"position": global_position})
|
||||||
|
EventSystem.connect_event(EventNames.INTERACT_PRESSED, _on_interact_pressed)
|
||||||
|
```
|
||||||
|
- **Nearest Filter**: All Sprite2D/TileMap resources MUST use Nearest filter (no Linear filter)
|
||||||
|
- **AutoLoad Restrictions**: Only GameManager, SceneManager, EventSystem, NetworkManager, ResponseHandler allowed as autoloads
|
||||||
|
- **Low-level Entities**: Do NOT directly reference GameManager in Player/NPC entities, use events instead
|
||||||
|
|
||||||
|
### Step 3: Comment Validation
|
||||||
|
|
||||||
|
Ensure code comments meet project standards.
|
||||||
|
|
||||||
|
**Actions:**
|
||||||
|
1. Read `docs/02-开发规范/代码注释规范.md`
|
||||||
|
2. Verify file header comment is complete:
|
||||||
|
```gdscript
|
||||||
|
# ============================================================================
|
||||||
|
# 文件名: FeatureName.gd
|
||||||
|
# 作用: 简短描述功能
|
||||||
|
#
|
||||||
|
# 主要功能:
|
||||||
|
# - 功能点1
|
||||||
|
# - 功能点2
|
||||||
|
#
|
||||||
|
# 依赖: 列出依赖的管理器/系统
|
||||||
|
# 作者: [开发者名称]
|
||||||
|
# 创建时间: YYYY-MM-DD
|
||||||
|
# ============================================================================
|
||||||
|
```
|
||||||
|
3. Verify all public functions have complete documentation:
|
||||||
|
```gdscript
|
||||||
|
# 函数功能描述
|
||||||
|
#
|
||||||
|
# 参数:
|
||||||
|
# param_name: Type - 参数说明
|
||||||
|
#
|
||||||
|
# 返回值:
|
||||||
|
# Type - 返回值说明
|
||||||
|
#
|
||||||
|
# 使用示例:
|
||||||
|
# var result = function_name(param)
|
||||||
|
func function_name(param: Type) -> ReturnType:
|
||||||
|
```
|
||||||
|
4. Ensure complex logic has inline comments explaining WHY, not WHAT
|
||||||
|
|
||||||
|
### Step 4: Naming Validation
|
||||||
|
|
||||||
|
Verify all naming follows project conventions.
|
||||||
|
|
||||||
|
**Actions:**
|
||||||
|
1. Read `docs/02-开发规范/命名规范.md`
|
||||||
|
2. Validate naming conventions:
|
||||||
|
- **Class names**: PascalCase (`class_name PlayerController`)
|
||||||
|
- **Variables/Functions**: camelCase (`var moveSpeed: float`, `func updateMovement()`)
|
||||||
|
- **Constants**: UPPER_CASE (`const MAX_HEALTH: int = 100`)
|
||||||
|
- **Private members**: Underscore prefix (`var _velocity: Vector2`)
|
||||||
|
- **Scene files**: snake_case with suffix (`player_scene.tscn`, `enemy_prefab.tscn`)
|
||||||
|
- **Script files**: PascalCase.gd (`PlayerController.gd`, `GameManager.gd`)
|
||||||
|
|
||||||
|
**Common Patterns:**
|
||||||
|
```gdscript
|
||||||
|
# ✅ Correct
|
||||||
|
const MAX_SPEED: float = 300.0
|
||||||
|
var currentHealth: int
|
||||||
|
var _isInitialized: bool = false
|
||||||
|
func getPlayerPosition() -> Vector2:
|
||||||
|
func _calculateDamage(baseDamage: int) -> int:
|
||||||
|
|
||||||
|
# ❌ Incorrect
|
||||||
|
const maxSpeed: float = 300.0 # Constants must be UPPER_CASE
|
||||||
|
var CurrentHealth: int # Variables must be camelCase
|
||||||
|
var is_initialized: bool = false # No snake_case for variables
|
||||||
|
func GetPlayerPosition() -> Vector2: # Functions must be camelCase
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5: Test Writing
|
||||||
|
|
||||||
|
Create unit tests for the implemented functionality.
|
||||||
|
|
||||||
|
**Actions:**
|
||||||
|
1. Read `docs/03-技术实现/测试指南.md`
|
||||||
|
2. For _Core/ managers/systems, MUST create corresponding test file in `tests/unit/`
|
||||||
|
3. Test file naming: `test_[name].gd`
|
||||||
|
4. Test file structure:
|
||||||
|
```gdscript
|
||||||
|
extends GutTest
|
||||||
|
|
||||||
|
## [FeatureName] 单元测试
|
||||||
|
|
||||||
|
var feature: FeatureName
|
||||||
|
|
||||||
|
func before_each():
|
||||||
|
feature = preload("res://_Core/managers/FeatureName.gd").new()
|
||||||
|
add_child(feature)
|
||||||
|
|
||||||
|
func after_each():
|
||||||
|
feature.queue_free()
|
||||||
|
|
||||||
|
func test_initialization():
|
||||||
|
var result = feature.initialize()
|
||||||
|
assert_true(result, "Feature should initialize successfully")
|
||||||
|
|
||||||
|
func test_core_functionality():
|
||||||
|
# Test core functionality
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6: Test Execution
|
||||||
|
|
||||||
|
Run tests to ensure code quality.
|
||||||
|
|
||||||
|
**Actions:**
|
||||||
|
1. Run GUT tests using Bash tool:
|
||||||
|
```bash
|
||||||
|
godot --headless -s addons/gut/gut_cmdline.gd -gdir=res://tests/ -ginclude_subdirs
|
||||||
|
```
|
||||||
|
2. Verify all tests pass
|
||||||
|
3. If tests fail:
|
||||||
|
- Identify the root cause
|
||||||
|
- Fix the implementation or test
|
||||||
|
- Re-run tests until all pass
|
||||||
|
|
||||||
|
### Step 7: Git Commit
|
||||||
|
|
||||||
|
Generate standardized Git commit message.
|
||||||
|
|
||||||
|
**Actions:**
|
||||||
|
1. Read `docs/02-开发规范/Git提交规范.md`
|
||||||
|
2. Determine commit type based on changes:
|
||||||
|
- `feat` - New features
|
||||||
|
- `fix` - Bug fixes
|
||||||
|
- `docs` - Documentation updates
|
||||||
|
- `refactor` - Code refactoring
|
||||||
|
- `perf` - Performance optimization
|
||||||
|
- `test` - Test additions/modifications
|
||||||
|
- `scene` - Scene file changes
|
||||||
|
- `ui` - UI related changes
|
||||||
|
3. Generate commit message using Chinese colon (:):
|
||||||
|
```
|
||||||
|
<类型>:<简短描述>
|
||||||
|
|
||||||
|
[可选的详细描述]
|
||||||
|
```
|
||||||
|
4. Follow principles:
|
||||||
|
- **One commit, one change** - Most important rule
|
||||||
|
- Use imperative verbs (添加, 修复, 更新)
|
||||||
|
- Keep description concise (< 50 characters)
|
||||||
|
- If multiple types of changes, split into separate commits
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```bash
|
||||||
|
# ✅ Good commits
|
||||||
|
git commit -m "feat:实现玩家二段跳功能"
|
||||||
|
git commit -m "fix:修复角色跳跃时的碰撞检测问题"
|
||||||
|
git commit -m "test:添加角色控制器单元测试"
|
||||||
|
|
||||||
|
# ❌ Bad commits
|
||||||
|
git commit -m "fix + feat:修复Bug并添加新功能" # Mixed types
|
||||||
|
git commit -m "update player" # Vague, English
|
||||||
|
```
|
||||||
|
|
||||||
|
## Progress Tracking
|
||||||
|
|
||||||
|
Use TodoWrite tool to track workflow progress:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
TodoWrite.create_todos([
|
||||||
|
"Step 1: 架构分析 - 读取架构规范",
|
||||||
|
"Step 2: 功能实现 - 按规范编码",
|
||||||
|
"Step 3: 注释规范检查",
|
||||||
|
"Step 4: 命名规范检查",
|
||||||
|
"Step 5: 测试代码编写",
|
||||||
|
"Step 6: 测试验证 - 运行测试",
|
||||||
|
"Step 7: Git 提交 - 生成提交信息"
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
||||||
|
Mark each step as `completed` immediately after finishing.
|
||||||
|
|
||||||
|
## Quality Checklist
|
||||||
|
|
||||||
|
Before completing the workflow, verify:
|
||||||
|
|
||||||
|
- [ ] File location follows layered architecture (_Core, scenes, UI)
|
||||||
|
- [ ] Uses EventSystem for cross-module communication
|
||||||
|
- [ ] Event names added to EventNames.gd
|
||||||
|
- [ ] All variables and functions have type annotations
|
||||||
|
- [ ] Naming conventions correct (PascalCase/camelCase/UPPER_CASE)
|
||||||
|
- [ ] File header comment complete
|
||||||
|
- [ ] Public functions have complete documentation
|
||||||
|
- [ ] Unit tests created and passing
|
||||||
|
- [ ] Git commit message follows specification
|
||||||
|
- [ ] No Godot 3.x syntax (yield → await)
|
||||||
|
- [ ] Node references cached with @onready
|
||||||
|
- [ ] Sprite2D/TileMap use Nearest filter
|
||||||
|
|
||||||
|
## Reference Documents
|
||||||
|
|
||||||
|
The skill automatically reads these documents at appropriate steps:
|
||||||
|
|
||||||
|
- Architecture: `docs/02-开发规范/架构与通信规范.md`
|
||||||
|
- Comments: `docs/02-开发规范/代码注释规范.md`
|
||||||
|
- Naming: `docs/02-开发规范/命名规范.md`
|
||||||
|
- Testing: `docs/03-技术实现/测试指南.md`
|
||||||
|
- Git: `docs/02-开发规范/Git提交规范.md`
|
||||||
|
- Project Instructions: `claude.md` (root directory)
|
||||||
|
|
||||||
|
For detailed checklist reference, see `references/checklist.md` in this skill directory.
|
||||||
|
|
||||||
|
## Example Workflow
|
||||||
|
|
||||||
|
User request: "实现玩家二段跳功能"
|
||||||
|
|
||||||
|
1. **Architecture Analysis** ✅
|
||||||
|
- Read architecture spec
|
||||||
|
- Target: `scenes/Entities/Player/Player.gd`
|
||||||
|
- Communication: Emit `PLAYER_DOUBLE_JUMPED` event
|
||||||
|
- Dependencies: EventSystem, Input
|
||||||
|
- Event: Add `PLAYER_DOUBLE_JUMPED` to EventNames.gd
|
||||||
|
|
||||||
|
2. **Implementation** ✅
|
||||||
|
- Create double jump logic with type annotations
|
||||||
|
- Use EventSystem.emit_event() for notifications
|
||||||
|
- Cache references with @onready
|
||||||
|
- Use await instead of yield
|
||||||
|
|
||||||
|
3. **Comment Validation** ✅
|
||||||
|
- Add file header with feature description
|
||||||
|
- Document double jump function parameters
|
||||||
|
- Add inline comments for jump logic
|
||||||
|
|
||||||
|
4. **Naming Validation** ✅
|
||||||
|
- Verify: `var canDoubleJump: bool` (camelCase)
|
||||||
|
- Verify: `const MAX_DOUBLE_JUMPS: int` (UPPER_CASE)
|
||||||
|
- Verify: `func performDoubleJump()` (camelCase)
|
||||||
|
|
||||||
|
5. **Test Writing** ✅
|
||||||
|
- Create `tests/unit/test_player_double_jump.gd`
|
||||||
|
- Test initialization, jump execution, limits
|
||||||
|
|
||||||
|
6. **Test Execution** ✅
|
||||||
|
- Run: `godot --headless -s addons/gut/gut_cmdline.gd`
|
||||||
|
- All tests pass ✅
|
||||||
|
|
||||||
|
7. **Git Commit** ✅
|
||||||
|
```bash
|
||||||
|
git add scenes/Entities/Player/Player.gd _Core/EventNames.gd tests/unit/test_player_double_jump.gd
|
||||||
|
git commit -m "feat:实现玩家二段跳功能"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- This skill enforces quality standards through automated validation
|
||||||
|
- Each step builds upon the previous, ensuring comprehensive quality control
|
||||||
|
- Skipping steps will result in incomplete or non-compliant code
|
||||||
|
- The 7-step workflow is designed for team consistency and maintainability
|
||||||
285
.claude/skills/whaletown-developer/references/checklist.md
Normal file
285
.claude/skills/whaletown-developer/references/checklist.md
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
# WhaleTown Development Quality Checklist
|
||||||
|
|
||||||
|
快速参考检查清单,用于验证代码是否符合项目规范。
|
||||||
|
|
||||||
|
## 架构检查清单
|
||||||
|
|
||||||
|
### 文件位置
|
||||||
|
- [ ] 核心系统文件位于 `_Core/managers/` 或 `_Core/systems/`
|
||||||
|
- [ ] 游戏场景文件位于 `scenes/Maps/`, `scenes/Entities/`, `scenes/Components/`
|
||||||
|
- [ ] UI 组件文件位于 `scenes/ui/`
|
||||||
|
- [ ] 测试文件位于 `tests/unit/` 或 `tests/integration/`
|
||||||
|
|
||||||
|
### 通信方式
|
||||||
|
- [ ] 跨模块通信使用 EventSystem
|
||||||
|
- [ ] 新增事件定义在 `_Core/EventNames.gd` 中
|
||||||
|
- [ ] 遵循 "Signal Up, Call Down" 原则
|
||||||
|
- [ ] 父节点调用子节点方法(向下调用)
|
||||||
|
- [ ] 子节点发出信号通知父节点(向上信号)
|
||||||
|
|
||||||
|
### 依赖管理
|
||||||
|
- [ ] 仅使用允许的自动加载:GameManager, SceneManager, EventSystem, NetworkManager, ResponseHandler
|
||||||
|
- [ ] 底层实体(Player, NPC)不直接访问 GameManager
|
||||||
|
- [ ] 底层实体通过事件系统与全局管理器通信
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 代码规范检查清单
|
||||||
|
|
||||||
|
### 类型安全
|
||||||
|
- [ ] 所有变量都有类型注解:`var speed: float = 200.0`
|
||||||
|
- [ ] 所有函数都有参数和返回值类型:`func move(delta: float) -> void:`
|
||||||
|
- [ ] 常量都有类型注解:`const MAX_HEALTH: int = 100`
|
||||||
|
|
||||||
|
### Godot 4.2+ 语法
|
||||||
|
- [ ] 使用 `await` 代替 `yield()`
|
||||||
|
- [ ] 使用 `@onready` 缓存节点引用
|
||||||
|
- [ ] 避免在 `_process()` 中使用 `get_node()`
|
||||||
|
- [ ] 信号连接使用 `.connect()` 语法
|
||||||
|
|
||||||
|
### 资源设置
|
||||||
|
- [ ] 所有 Sprite2D 使用 Nearest 滤镜(不使用 Linear)
|
||||||
|
- [ ] 所有 TileMap 使用 Nearest 滤镜
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 命名规范检查清单
|
||||||
|
|
||||||
|
### 类名
|
||||||
|
- [ ] 使用 PascalCase:`class_name PlayerController`
|
||||||
|
- [ ] 文件名与类名一致:`PlayerController.gd`
|
||||||
|
|
||||||
|
### 变量
|
||||||
|
- [ ] 公共变量使用 camelCase:`var moveSpeed: float`
|
||||||
|
- [ ] 私有变量使用下划线前缀:`var _velocity: Vector2`
|
||||||
|
- [ ] 布尔变量使用 is/has/can 前缀:`var isJumping: bool`
|
||||||
|
|
||||||
|
### 函数
|
||||||
|
- [ ] 使用 camelCase:`func updateMovement()`
|
||||||
|
- [ ] 获取函数使用 `get` 前缀:`func getPlayerPosition()`
|
||||||
|
- [ ] 设置函数使用 `set` 前缀:`func setHealth(value: int)`
|
||||||
|
- [ ] 判断函数使用 `is/has/can` 前缀:`func isAlive()`, `func canJump()`
|
||||||
|
- [ ] 私有函数使用下划线前缀:`func _calculateDamage()`
|
||||||
|
|
||||||
|
### 常量
|
||||||
|
- [ ] 使用 UPPER_CASE:`const MAX_HEALTH: int = 100`
|
||||||
|
- [ ] 使用下划线分隔:`const JUMP_FORCE: float = -400.0`
|
||||||
|
|
||||||
|
### 枚举
|
||||||
|
- [ ] 枚举类型使用 PascalCase:`enum PlayerState`
|
||||||
|
- [ ] 枚举值使用 UPPER_CASE:`IDLE, WALKING, RUNNING`
|
||||||
|
|
||||||
|
### 文件命名
|
||||||
|
- [ ] 脚本文件:PascalCase.gd (`PlayerController.gd`)
|
||||||
|
- [ ] 场景文件:snake_case_scene.tscn (`main_scene.tscn`)
|
||||||
|
- [ ] 预制体文件:snake_case_prefab.tscn (`player_prefab.tscn`)
|
||||||
|
- [ ] 资源文件:snake_case (`sprite_player_idle.png`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 注释规范检查清单
|
||||||
|
|
||||||
|
### 文件头注释
|
||||||
|
- [ ] 包含文件名
|
||||||
|
- [ ] 包含作用描述
|
||||||
|
- [ ] 列出主要功能
|
||||||
|
- [ ] 列出依赖的管理器/系统
|
||||||
|
- [ ] 包含作者和创建时间
|
||||||
|
|
||||||
|
示例:
|
||||||
|
```gdscript
|
||||||
|
# ============================================================================
|
||||||
|
# 文件名: PlayerController.gd
|
||||||
|
# 作用: 玩家角色控制器,处理玩家输入和移动逻辑
|
||||||
|
#
|
||||||
|
# 主要功能:
|
||||||
|
# - 处理键盘和手柄输入
|
||||||
|
# - 控制角色移动和跳跃
|
||||||
|
# - 管理角色状态切换
|
||||||
|
#
|
||||||
|
# 依赖: EventSystem, InputManager
|
||||||
|
# 作者: [开发者名称]
|
||||||
|
# 创建时间: 2025-01-03
|
||||||
|
# ============================================================================
|
||||||
|
```
|
||||||
|
|
||||||
|
### 函数注释
|
||||||
|
- [ ] 公共函数有完整注释
|
||||||
|
- [ ] 包含功能描述
|
||||||
|
- [ ] 列出参数说明(名称、类型、含义)
|
||||||
|
- [ ] 说明返回值(类型、含义)
|
||||||
|
- [ ] 提供使用示例(对于复杂函数)
|
||||||
|
- [ ] 标注注意事项(如果有)
|
||||||
|
|
||||||
|
示例:
|
||||||
|
```gdscript
|
||||||
|
# 处理玩家输入并更新移动状态
|
||||||
|
#
|
||||||
|
# 参数:
|
||||||
|
# delta: float - 帧时间间隔
|
||||||
|
#
|
||||||
|
# 返回值: 无
|
||||||
|
#
|
||||||
|
# 注意事项:
|
||||||
|
# - 需要在 _physics_process 中调用
|
||||||
|
# - 会自动处理重力和碰撞
|
||||||
|
func handleMovement(delta: float) -> void:
|
||||||
|
```
|
||||||
|
|
||||||
|
### 行内注释
|
||||||
|
- [ ] 复杂逻辑有注释说明
|
||||||
|
- [ ] 注释解释 WHY(为什么),不解释 WHAT(是什么)
|
||||||
|
- [ ] 避免显而易见的注释
|
||||||
|
- [ ] 使用 TODO/FIXME/NOTE 等标记
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 测试规范检查清单
|
||||||
|
|
||||||
|
### 测试文件
|
||||||
|
- [ ] _Core/ 中的管理器/系统都有对应测试文件
|
||||||
|
- [ ] 测试文件位于 `tests/unit/` 或 `tests/integration/`
|
||||||
|
- [ ] 测试文件命名:`test_[name].gd`
|
||||||
|
- [ ] 测试文件继承自 GutTest:`extends GutTest`
|
||||||
|
|
||||||
|
### 测试结构
|
||||||
|
- [ ] 包含测试类注释
|
||||||
|
- [ ] 实现 `before_each()` 进行测试前置设置
|
||||||
|
- [ ] 实现 `after_each()` 进行测试清理
|
||||||
|
- [ ] 测试方法命名:`test_[功能名称]()`
|
||||||
|
|
||||||
|
### 测试覆盖
|
||||||
|
- [ ] 测试核心功能的正常流程
|
||||||
|
- [ ] 测试错误处理和边界条件
|
||||||
|
- [ ] 测试初始化和清理逻辑
|
||||||
|
- [ ] 所有测试都能通过
|
||||||
|
|
||||||
|
示例:
|
||||||
|
```gdscript
|
||||||
|
extends GutTest
|
||||||
|
|
||||||
|
## PlayerController 单元测试
|
||||||
|
|
||||||
|
var player: PlayerController
|
||||||
|
|
||||||
|
func before_each():
|
||||||
|
player = preload("res://scenes/Entities/Player/PlayerController.gd").new()
|
||||||
|
add_child(player)
|
||||||
|
|
||||||
|
func after_each():
|
||||||
|
player.queue_free()
|
||||||
|
|
||||||
|
func test_initialization():
|
||||||
|
var result = player.initialize()
|
||||||
|
assert_true(result, "Player should initialize successfully")
|
||||||
|
|
||||||
|
func test_movement():
|
||||||
|
# 测试移动功能
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Git 提交规范检查清单
|
||||||
|
|
||||||
|
### 提交类型
|
||||||
|
- [ ] 使用正确的提交类型:
|
||||||
|
- `feat` - 新功能
|
||||||
|
- `fix` - Bug 修复
|
||||||
|
- `docs` - 文档更新
|
||||||
|
- `refactor` - 代码重构
|
||||||
|
- `test` - 测试相关
|
||||||
|
- `scene` - 场景文件
|
||||||
|
- `ui` - UI 相关
|
||||||
|
|
||||||
|
### 提交格式
|
||||||
|
- [ ] 使用中文冒号(:)
|
||||||
|
- [ ] 描述简洁明了(< 50 字符)
|
||||||
|
- [ ] 使用动词开头(添加、修复、更新)
|
||||||
|
- [ ] 一次提交只包含一种类型的改动
|
||||||
|
|
||||||
|
### 提交原则
|
||||||
|
- [ ] 一次提交只做一件事
|
||||||
|
- [ ] 提交的代码能够正常运行
|
||||||
|
- [ ] 避免 fix + feat 混合提交
|
||||||
|
- [ ] 如需多种改动,拆分成多次提交
|
||||||
|
|
||||||
|
示例:
|
||||||
|
```bash
|
||||||
|
# ✅ 正确
|
||||||
|
git commit -m "feat:实现玩家二段跳功能"
|
||||||
|
git commit -m "fix:修复角色跳跃时的碰撞检测问题"
|
||||||
|
git commit -m "test:添加角色控制器单元测试"
|
||||||
|
|
||||||
|
# ❌ 错误
|
||||||
|
git commit -m "fix + feat:修复Bug并添加新功能" # 混合类型
|
||||||
|
git commit -m "update player" # 描述不清晰,使用英文
|
||||||
|
git commit -m "fix: 修复Bug" # 使用英文冒号
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 完整工作流检查清单
|
||||||
|
|
||||||
|
使用此清单验证开发任务是否完整执行 7 步工作流:
|
||||||
|
|
||||||
|
### Step 1: 架构分析
|
||||||
|
- [ ] 已读取 `docs/02-开发规范/架构与通信规范.md`
|
||||||
|
- [ ] 已确定文件位置(_Core, scenes, UI)
|
||||||
|
- [ ] 已确定通信方式(EventSystem)
|
||||||
|
- [ ] 已列出依赖的管理器/系统
|
||||||
|
- [ ] 已设计事件定义(如需要)
|
||||||
|
|
||||||
|
### Step 2: 功能实现
|
||||||
|
- [ ] 代码遵循分层架构
|
||||||
|
- [ ] 所有变量和函数有类型注解
|
||||||
|
- [ ] 使用 Godot 4.2+ 语法
|
||||||
|
- [ ] 使用 EventSystem 进行跨模块通信
|
||||||
|
- [ ] 使用 @onready 缓存节点引用
|
||||||
|
|
||||||
|
### Step 3: 注释规范检查
|
||||||
|
- [ ] 已读取 `docs/02-开发规范/代码注释规范.md`
|
||||||
|
- [ ] 文件头注释完整
|
||||||
|
- [ ] 公共函数有完整注释
|
||||||
|
- [ ] 复杂逻辑有行内注释
|
||||||
|
|
||||||
|
### Step 4: 命名规范检查
|
||||||
|
- [ ] 已读取 `docs/02-开发规范/命名规范.md`
|
||||||
|
- [ ] 类名使用 PascalCase
|
||||||
|
- [ ] 变量/函数使用 camelCase
|
||||||
|
- [ ] 常量使用 UPPER_CASE
|
||||||
|
- [ ] 私有成员使用下划线前缀
|
||||||
|
|
||||||
|
### Step 5: 测试代码编写
|
||||||
|
- [ ] 已读取 `docs/03-技术实现/测试指南.md`
|
||||||
|
- [ ] 创建了测试文件 `tests/unit/test_[name].gd`
|
||||||
|
- [ ] 测试文件继承自 GutTest
|
||||||
|
- [ ] 编写了核心功能测试
|
||||||
|
|
||||||
|
### Step 6: 测试验证
|
||||||
|
- [ ] 运行了 GUT 测试命令
|
||||||
|
- [ ] 所有测试通过
|
||||||
|
- [ ] 如有失败,已修复并重新测试
|
||||||
|
|
||||||
|
### Step 7: Git 提交
|
||||||
|
- [ ] 已读取 `docs/02-开发规范/Git提交规范.md`
|
||||||
|
- [ ] 生成了符合规范的提交信息
|
||||||
|
- [ ] 提交类型正确
|
||||||
|
- [ ] 使用中文冒号
|
||||||
|
- [ ] 遵循"一次提交只做一件事"原则
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 快速自检问题
|
||||||
|
|
||||||
|
在提交代码前,问自己以下问题:
|
||||||
|
|
||||||
|
1. **架构**: 文件放在正确的位置了吗?
|
||||||
|
2. **通信**: 是否使用 EventSystem 进行跨模块通信?
|
||||||
|
3. **类型**: 所有变量和函数都有类型注解吗?
|
||||||
|
4. **命名**: 命名是否符合规范(PascalCase/camelCase/UPPER_CASE)?
|
||||||
|
5. **注释**: 文件头和公共函数有完整注释吗?
|
||||||
|
6. **测试**: 创建并运行测试了吗?所有测试都通过了吗?
|
||||||
|
7. **提交**: Git 提交信息符合规范吗?
|
||||||
|
|
||||||
|
如果以上问题都能回答"是",那么代码已经符合 WhaleTown 项目的质量标准!✅
|
||||||
312
.claude/skills/whaletown-developer/使用说明.md
Normal file
312
.claude/skills/whaletown-developer/使用说明.md
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
# WhaleTown Developer Skill 使用说明
|
||||||
|
|
||||||
|
## 📖 简介
|
||||||
|
|
||||||
|
`whaletown-developer` 是 WhaleTown 项目的标准开发工作流自动化技能,确保所有开发任务都遵循统一的项目规范和质量标准。
|
||||||
|
|
||||||
|
## 🎯 适用场景
|
||||||
|
|
||||||
|
在以下情况下使用此 skill:
|
||||||
|
|
||||||
|
- ✅ 实现新功能("实现玩家二段跳"、"添加存档系统")
|
||||||
|
- ✅ 修复 Bug("修复角色碰撞问题"、"解决UI显示错误")
|
||||||
|
- ✅ 创建场景("创建商店场景"、"设计背包界面")
|
||||||
|
- ✅ 开发模块("开发任务系统"、"构建战斗组件")
|
||||||
|
- ✅ 任何需要遵循项目规范的代码开发任务
|
||||||
|
|
||||||
|
## 🚀 调用方式
|
||||||
|
|
||||||
|
### 方式一:通过 Claude(推荐)
|
||||||
|
|
||||||
|
```
|
||||||
|
用户:帮我实现一个 NPC
|
||||||
|
Claude:/whaletown-developer 实现一个 NPC
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方式二:直接请求
|
||||||
|
|
||||||
|
```
|
||||||
|
用户:使用 whaletown-developer skill 创建玩家移动系统
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 7 步工作流程
|
||||||
|
|
||||||
|
skill 会自动执行以下标准化流程:
|
||||||
|
|
||||||
|
```
|
||||||
|
Step 1: 架构分析
|
||||||
|
↓ 读取架构规范,确定文件位置、通信方式、依赖关系
|
||||||
|
|
||||||
|
Step 2: 功能实现
|
||||||
|
↓ 按照类型安全、命名规范、EventSystem 通信等要求编码
|
||||||
|
|
||||||
|
Step 3: 注释规范检查
|
||||||
|
↓ 验证文件头、函数文档、行内注释是否完整
|
||||||
|
|
||||||
|
Step 4: 命名规范检查
|
||||||
|
↓ 验证 PascalCase/camelCase/UPPER_CASE 命名规范
|
||||||
|
|
||||||
|
Step 5: 测试代码编写
|
||||||
|
↓ 为核心功能创建 GUT 单元测试
|
||||||
|
|
||||||
|
Step 6: 测试验证
|
||||||
|
↓ 运行测试确保功能正常
|
||||||
|
|
||||||
|
Step 7: Git 提交
|
||||||
|
↓ 生成符合规范的提交信息
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💡 使用示例
|
||||||
|
|
||||||
|
### 示例 1:创建玩家控制器
|
||||||
|
|
||||||
|
**用户输入:**
|
||||||
|
```
|
||||||
|
帮我创建一个玩家角色控制器
|
||||||
|
```
|
||||||
|
|
||||||
|
**Skill 执行:**
|
||||||
|
1. 分析架构 → 确定放在 `scenes/Entities/Player/PlayerController.gd`
|
||||||
|
2. 实现功能 → 创建带类型注解的移动、跳跃逻辑
|
||||||
|
3. 检查注释 → 添加完整的文件头和函数文档
|
||||||
|
4. 检查命名 → 确保 `moveSpeed`、`MAX_HEALTH` 等命名正确
|
||||||
|
5. 编写测试 → 创建 `tests/unit/test_player_controller.gd`
|
||||||
|
6. 运行测试 → 验证功能正常
|
||||||
|
7. 生成提交 → `feat:实现玩家角色控制器`
|
||||||
|
|
||||||
|
### 示例 2:修复跳跃 Bug
|
||||||
|
|
||||||
|
**用户输入:**
|
||||||
|
```
|
||||||
|
修复玩家跳跃时的碰撞检测问题
|
||||||
|
```
|
||||||
|
|
||||||
|
**Skill 执行:**
|
||||||
|
1. 分析架构 → 定位到 `scenes/Entities/Player/Player.gd`
|
||||||
|
2. 实现修复 → 修改碰撞检测逻辑
|
||||||
|
3. 检查注释 → 添加修复说明注释
|
||||||
|
4. 检查命名 → 确保变量命名规范
|
||||||
|
5. 编写测试 → 添加碰撞测试用例
|
||||||
|
6. 运行测试 → 确认 Bug 已修复
|
||||||
|
7. 生成提交 → `fix:修复玩家跳跃时的碰撞检测问题`
|
||||||
|
|
||||||
|
### 示例 3:添加事件通信
|
||||||
|
|
||||||
|
**用户输入:**
|
||||||
|
```
|
||||||
|
实现 NPC 对话系统的事件通信
|
||||||
|
```
|
||||||
|
|
||||||
|
**Skill 执行:**
|
||||||
|
1. 分析架构 → 使用 EventSystem 跨模块通信
|
||||||
|
2. 实现功能 → 在 EventNames.gd 添加 `NPC_DIALOG_STARTED` 事件
|
||||||
|
3. 检查注释 → 文档化事件数据格式
|
||||||
|
4. 检查命名 → 确保事件名称符合规范
|
||||||
|
5. 编写测试 → 测试事件发送和接收
|
||||||
|
6. 运行测试 → 验证通信正常
|
||||||
|
7. 生成提交 → `feat:实现NPC对话系统的事件通信`
|
||||||
|
|
||||||
|
## ✅ 质量保证
|
||||||
|
|
||||||
|
每次使用 skill 后,代码都会符合以下标准:
|
||||||
|
|
||||||
|
### 架构层面
|
||||||
|
- ✅ 文件位置符合分层架构(_Core、scenes、UI)
|
||||||
|
- ✅ 使用 EventSystem 实现跨模块通信
|
||||||
|
- ✅ 事件名称已添加到 EventNames.gd
|
||||||
|
- ✅ 遵循"Signal Up, Call Down"原则
|
||||||
|
|
||||||
|
### 代码质量
|
||||||
|
- ✅ 所有变量和函数都有类型注解
|
||||||
|
- ✅ 命名规范正确(PascalCase/camelCase/UPPER_CASE)
|
||||||
|
- ✅ 使用 Godot 4.2+ 语法(await 而非 yield)
|
||||||
|
- ✅ 节点引用使用 @onready 缓存
|
||||||
|
|
||||||
|
### 文档规范
|
||||||
|
- ✅ 文件头注释完整(文件名、作用、功能、依赖)
|
||||||
|
- ✅ 公共函数有完整文档(参数、返回值、示例)
|
||||||
|
- ✅ 复杂逻辑有行内注释说明
|
||||||
|
|
||||||
|
### 测试覆盖
|
||||||
|
- ✅ 核心功能有单元测试
|
||||||
|
- ✅ 测试文件命名规范(test_*.gd)
|
||||||
|
- ✅ 测试通过验证
|
||||||
|
|
||||||
|
### Git 规范
|
||||||
|
- ✅ 提交信息格式正确(类型:描述)
|
||||||
|
- ✅ 遵循"一次提交只做一件事"原则
|
||||||
|
- ✅ 使用中文冒号和动词开头
|
||||||
|
|
||||||
|
## 📚 相关文档
|
||||||
|
|
||||||
|
Skill 会自动读取以下规范文档:
|
||||||
|
|
||||||
|
- `docs/02-开发规范/架构与通信规范.md` - 分层架构和 EventSystem
|
||||||
|
- `docs/02-开发规范/代码注释规范.md` - 注释格式要求
|
||||||
|
- `docs/02-开发规范/命名规范.md` - 命名约定
|
||||||
|
- `docs/03-技术实现/测试指南.md` - 测试框架使用
|
||||||
|
- `docs/02-开发规范/Git提交规范.md` - 提交信息格式
|
||||||
|
- `CLAUDE.md` - 项目总体指导
|
||||||
|
|
||||||
|
## ⚙️ 配置文件
|
||||||
|
|
||||||
|
Skill 相关配置文件位置:
|
||||||
|
|
||||||
|
```
|
||||||
|
.claude/skills/whaletown-developer/
|
||||||
|
├── SKILL.md # Skill 定义文件
|
||||||
|
├── 使用说明.md # 本文档
|
||||||
|
└── references/
|
||||||
|
└── checklist.md # 质量检查清单
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 工作流程可视化
|
||||||
|
|
||||||
|
```
|
||||||
|
用户请求
|
||||||
|
↓
|
||||||
|
调用 whaletown-developer skill
|
||||||
|
↓
|
||||||
|
[Step 1] 架构分析
|
||||||
|
├─ 读取架构规范文档
|
||||||
|
├─ 确定文件位置
|
||||||
|
├─ 识别通信方式
|
||||||
|
└─ 设计事件定义
|
||||||
|
↓
|
||||||
|
[Step 2] 功能实现
|
||||||
|
├─ 遵循类型安全
|
||||||
|
├─ 使用 EventSystem
|
||||||
|
├─ 缓存节点引用
|
||||||
|
└─ 使用 Godot 4.2+ 语法
|
||||||
|
↓
|
||||||
|
[Step 3] 注释规范检查
|
||||||
|
├─ 验证文件头注释
|
||||||
|
├─ 验证函数文档
|
||||||
|
└─ 检查行内注释
|
||||||
|
↓
|
||||||
|
[Step 4] 命名规范检查
|
||||||
|
├─ 类名 PascalCase
|
||||||
|
├─ 变量/函数 camelCase
|
||||||
|
├─ 常量 UPPER_CASE
|
||||||
|
└─ 私有成员 _prefix
|
||||||
|
↓
|
||||||
|
[Step 5] 测试代码编写
|
||||||
|
├─ 创建测试文件
|
||||||
|
├─ 编写测试用例
|
||||||
|
└─ 覆盖核心功能
|
||||||
|
↓
|
||||||
|
[Step 6] 测试验证
|
||||||
|
├─ 运行 GUT 测试
|
||||||
|
├─ 验证测试通过
|
||||||
|
└─ 修复失败测试
|
||||||
|
↓
|
||||||
|
[Step 7] Git 提交
|
||||||
|
├─ 确定提交类型
|
||||||
|
├─ 生成提交信息
|
||||||
|
└─ 遵循提交规范
|
||||||
|
↓
|
||||||
|
完成 ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎓 最佳实践
|
||||||
|
|
||||||
|
### 1. 明确任务描述
|
||||||
|
```bash
|
||||||
|
# ✅ 好的描述
|
||||||
|
"实现玩家二段跳功能"
|
||||||
|
"修复敌人AI路径寻找Bug"
|
||||||
|
"创建商店购买界面"
|
||||||
|
|
||||||
|
# ❌ 模糊描述
|
||||||
|
"改一下玩家"
|
||||||
|
"修复Bug"
|
||||||
|
"做个界面"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 一次处理一个功能
|
||||||
|
```bash
|
||||||
|
# ✅ 推荐
|
||||||
|
用户:实现玩家移动
|
||||||
|
用户:实现玩家跳跃
|
||||||
|
|
||||||
|
# ❌ 不推荐
|
||||||
|
用户:实现玩家移动、跳跃、攻击、技能系统
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 信任 Skill 流程
|
||||||
|
- Skill 会按照 7 步流程确保质量
|
||||||
|
- 不需要手动检查命名、注释等细节
|
||||||
|
- 专注于功能需求和业务逻辑
|
||||||
|
|
||||||
|
### 4. 查看生成的提交信息
|
||||||
|
- Skill 会在 Step 7 生成规范的提交信息
|
||||||
|
- 可以直接使用或根据需要微调
|
||||||
|
|
||||||
|
## ⚠️ 注意事项
|
||||||
|
|
||||||
|
1. **首次使用**
|
||||||
|
- 确保已阅读 `CLAUDE.md` 了解项目规范
|
||||||
|
- 确认所有规范文档都已存在
|
||||||
|
|
||||||
|
2. **测试环境**
|
||||||
|
- 确保 GUT 测试框架已安装(如需运行测试)
|
||||||
|
- Godot 可执行文件在 PATH 中(Step 6 测试执行)
|
||||||
|
|
||||||
|
3. **中断处理**
|
||||||
|
- 如果工作流被中断,可以继续执行剩余步骤
|
||||||
|
- Skill 使用 TodoWrite 追踪进度
|
||||||
|
|
||||||
|
4. **规范更新**
|
||||||
|
- 项目规范文档更新时,Skill 会自动读取最新版本
|
||||||
|
- 无需手动同步
|
||||||
|
|
||||||
|
## 🤝 反馈与改进
|
||||||
|
|
||||||
|
如果遇到问题或有改进建议:
|
||||||
|
|
||||||
|
1. 检查是否所有规范文档都已更新
|
||||||
|
2. 确认任务描述清晰明确
|
||||||
|
3. 查看 Skill 执行日志定位问题
|
||||||
|
4. 向团队报告问题或建议
|
||||||
|
|
||||||
|
## 📊 效果对比
|
||||||
|
|
||||||
|
### 不使用 Skill
|
||||||
|
```
|
||||||
|
开发者手动:
|
||||||
|
1. 不确定文件放哪里 ❌
|
||||||
|
2. 可能忘记类型注解 ❌
|
||||||
|
3. 注释不完整 ❌
|
||||||
|
4. 命名不一致 ❌
|
||||||
|
5. 没有测试 ❌
|
||||||
|
6. 提交信息格式错误 ❌
|
||||||
|
结果:代码质量参差不齐
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用 Skill
|
||||||
|
```
|
||||||
|
Skill 自动化:
|
||||||
|
1. 自动确定正确位置 ✅
|
||||||
|
2. 强制类型安全 ✅
|
||||||
|
3. 完整注释文档 ✅
|
||||||
|
4. 统一命名规范 ✅
|
||||||
|
5. 自动生成测试 ✅
|
||||||
|
6. 规范提交信息 ✅
|
||||||
|
结果:高质量、一致性代码
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 总结
|
||||||
|
|
||||||
|
whaletown-developer skill 是你的开发助手,它会:
|
||||||
|
|
||||||
|
- 🤖 **自动化** 7 步标准流程
|
||||||
|
- 📏 **标准化** 代码质量
|
||||||
|
- 🔒 **保证** 规范遵循
|
||||||
|
- ⚡ **加速** 开发效率
|
||||||
|
- 🧪 **确保** 测试覆盖
|
||||||
|
|
||||||
|
**记住:专注于功能实现,让 Skill 处理规范和质量!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**开始使用:** 只需告诉 Claude "帮我实现 XXX 功能" 即可!
|
||||||
@@ -57,4 +57,15 @@ const GRID_SNAP_REQUESTED = "grid_snap_requested"
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
# 测试事件
|
# 测试事件
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
const TEST_EVENT = "test_event"
|
const TEST_EVENT = "test_event"
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 聊天事件
|
||||||
|
# ============================================================================
|
||||||
|
const CHAT_MESSAGE_SENT = "chat_message_sent"
|
||||||
|
const CHAT_MESSAGE_RECEIVED = "chat_message_received"
|
||||||
|
const CHAT_ERROR_OCCURRED = "chat_error_occurred"
|
||||||
|
const CHAT_CONNECTION_STATE_CHANGED = "chat_connection_state_changed"
|
||||||
|
const CHAT_POSITION_UPDATED = "chat_position_updated"
|
||||||
|
const CHAT_LOGIN_SUCCESS = "chat_login_success"
|
||||||
|
const CHAT_LOGIN_FAILED = "chat_login_failed"
|
||||||
@@ -80,19 +80,197 @@ var current_email: String = ""
|
|||||||
# 网络请求管理
|
# 网络请求管理
|
||||||
var active_request_ids: Array = []
|
var active_request_ids: Array = []
|
||||||
|
|
||||||
|
# ============ Token 管理 ============
|
||||||
|
|
||||||
|
# 本地存储路径常量
|
||||||
|
const AUTH_CONFIG_PATH: String = "user://auth.cfg"
|
||||||
|
|
||||||
|
# Token 存储(内存中,用于快速访问)
|
||||||
|
var _access_token: String = "" # JWT访问令牌(短期,用于API和WebSocket)
|
||||||
|
var _refresh_token: String = "" # JWT刷新令牌(长期,用于获取新access_token)
|
||||||
|
var _user_info: Dictionary = {} # 用户信息
|
||||||
|
var _token_expiry: float = 0.0 # access_token过期时间(Unix时间戳)
|
||||||
|
|
||||||
|
# 游戏 token(兼容旧代码,保留但标记为废弃)
|
||||||
|
var _game_token: String = "" # @deprecated 使用 _access_token 替代
|
||||||
|
|
||||||
# ============ 生命周期方法 ============
|
# ============ 生命周期方法 ============
|
||||||
|
|
||||||
# 初始化管理器
|
# 初始化管理器
|
||||||
func _init():
|
func _init() -> void:
|
||||||
print("AuthManager 初始化完成")
|
print("AuthManager 初始化完成")
|
||||||
|
_load_auth_data()
|
||||||
|
|
||||||
# 清理资源
|
# 清理资源
|
||||||
func cleanup():
|
func cleanup() -> void:
|
||||||
# 取消所有活动的网络请求
|
# 取消所有活动的网络请求
|
||||||
for request_id in active_request_ids:
|
for request_id in active_request_ids:
|
||||||
NetworkManager.cancel_request(request_id)
|
NetworkManager.cancel_request(request_id)
|
||||||
active_request_ids.clear()
|
active_request_ids.clear()
|
||||||
|
|
||||||
|
# ============ Token 管理 ============
|
||||||
|
|
||||||
|
# 保存 Token 到内存
|
||||||
|
#
|
||||||
|
# 参数:
|
||||||
|
# data: Dictionary - 登录响应数据
|
||||||
|
#
|
||||||
|
# 功能:
|
||||||
|
# - 从登录响应中提取 access_token 和 refresh_token
|
||||||
|
# - 保存到内存变量中
|
||||||
|
# - 保存用户信息
|
||||||
|
func _save_tokens_to_memory(data: Dictionary) -> void:
|
||||||
|
if not data.has("data"):
|
||||||
|
print("⚠️ 登录响应中没有 data 字段")
|
||||||
|
return
|
||||||
|
|
||||||
|
var token_data: Dictionary = data.data
|
||||||
|
_access_token = token_data.get("access_token", "")
|
||||||
|
_refresh_token = token_data.get("refresh_token", "")
|
||||||
|
_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
|
||||||
|
|
||||||
|
print("✅ Token已保存到内存")
|
||||||
|
print(" Access Token: ", _access_token.substr(0, 20) + "...")
|
||||||
|
print(" 用户: ", _user_info.get("username", "未知"))
|
||||||
|
|
||||||
|
# 保存 Token 到本地(ConfigFile)
|
||||||
|
#
|
||||||
|
# 参数:
|
||||||
|
# data: Dictionary - 登录响应数据
|
||||||
|
#
|
||||||
|
# 功能:
|
||||||
|
# - 将 refresh_token 和用户信息保存到 ConfigFile
|
||||||
|
# - access_token 不保存到本地,仅保存在内存中
|
||||||
|
func _save_tokens_to_local(data: Dictionary) -> void:
|
||||||
|
if not data.has("data"):
|
||||||
|
return
|
||||||
|
|
||||||
|
var token_data: Dictionary = data.data
|
||||||
|
var auth_data: Dictionary = {
|
||||||
|
"refresh_token": token_data.get("refresh_token", ""),
|
||||||
|
"user_id": token_data.get("user", {}).get("id", ""),
|
||||||
|
"username": token_data.get("user", {}).get("username", ""),
|
||||||
|
"saved_at": Time.get_unix_time_from_system()
|
||||||
|
}
|
||||||
|
|
||||||
|
var config: ConfigFile = ConfigFile.new()
|
||||||
|
config.load(AUTH_CONFIG_PATH)
|
||||||
|
config.set_value("auth", "refresh_token", auth_data["refresh_token"])
|
||||||
|
config.set_value("auth", "user_id", auth_data["user_id"])
|
||||||
|
config.set_value("auth", "username", auth_data["username"])
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 从本地加载 Token(游戏启动时调用)
|
||||||
|
#
|
||||||
|
# 功能:
|
||||||
|
# - 从 ConfigFile 加载 refresh_token 和用户信息
|
||||||
|
# - 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)
|
||||||
|
return
|
||||||
|
|
||||||
|
_refresh_token = config.get_value("auth", "refresh_token", "")
|
||||||
|
var user_id: String = config.get_value("auth", "user_id", "")
|
||||||
|
var username: String = config.get_value("auth", "username", "")
|
||||||
|
|
||||||
|
if not _refresh_token.is_empty():
|
||||||
|
_user_info = {
|
||||||
|
"id": user_id,
|
||||||
|
"username": username
|
||||||
|
}
|
||||||
|
print("✅ 已从本地加载认证数据")
|
||||||
|
print(" 用户: ", username)
|
||||||
|
else:
|
||||||
|
print("⚠️ 本地认证数据无效(没有 refresh_token)")
|
||||||
|
|
||||||
|
# 清除本地认证数据(登出时调用)
|
||||||
|
#
|
||||||
|
# 功能:
|
||||||
|
# - 清除内存中的 Token
|
||||||
|
# - 删除本地 ConfigFile
|
||||||
|
func _clear_auth_data() -> void:
|
||||||
|
_access_token = ""
|
||||||
|
_refresh_token = ""
|
||||||
|
_user_info = {}
|
||||||
|
_token_expiry = 0.0
|
||||||
|
_game_token = ""
|
||||||
|
|
||||||
|
if FileAccess.file_exists(AUTH_CONFIG_PATH):
|
||||||
|
DirAccess.remove_absolute(AUTH_CONFIG_PATH)
|
||||||
|
print("✅ 已清除本地认证数据")
|
||||||
|
|
||||||
|
# ============ Token 访问方法 ============
|
||||||
|
|
||||||
|
# 设置游戏 token(兼容旧代码,推荐使用 _save_tokens_to_memory)
|
||||||
|
#
|
||||||
|
# 参数:
|
||||||
|
# token: String - 游戏认证 token
|
||||||
|
#
|
||||||
|
# 使用场景:
|
||||||
|
# - 登录成功后设置 token
|
||||||
|
# - 从服务器响应中获取 token
|
||||||
|
func set_game_token(token: String) -> void:
|
||||||
|
_game_token = token
|
||||||
|
_access_token = token # 同步更新 access_token
|
||||||
|
print("AuthManager: 游戏 token 已设置")
|
||||||
|
|
||||||
|
# 获取游戏 token
|
||||||
|
#
|
||||||
|
# 返回值:
|
||||||
|
# String - access_token(如果未设置则返回空字符串)
|
||||||
|
#
|
||||||
|
# 使用场景:
|
||||||
|
# - ChatManager 连接 WebSocket 时需要 token
|
||||||
|
# - 其他需要游戏认证的场景
|
||||||
|
func get_game_token() -> String:
|
||||||
|
return _access_token
|
||||||
|
|
||||||
|
# 获取 access token
|
||||||
|
#
|
||||||
|
# 返回值:
|
||||||
|
# String - JWT访问令牌
|
||||||
|
#
|
||||||
|
# 使用场景:
|
||||||
|
# - API请求认证
|
||||||
|
# - WebSocket聊天认证
|
||||||
|
func get_access_token() -> String:
|
||||||
|
return _access_token
|
||||||
|
|
||||||
|
# 获取 refresh token
|
||||||
|
#
|
||||||
|
# 返回值:
|
||||||
|
# String - JWT刷新令牌
|
||||||
|
#
|
||||||
|
# 使用场景:
|
||||||
|
# - 刷新过期的 access token
|
||||||
|
func get_refresh_token() -> String:
|
||||||
|
return _refresh_token
|
||||||
|
|
||||||
|
# 获取用户信息
|
||||||
|
#
|
||||||
|
# 返回值:
|
||||||
|
# Dictionary - 用户信息字典
|
||||||
|
func get_user_info() -> Dictionary:
|
||||||
|
return _user_info
|
||||||
|
|
||||||
# ============ 登录相关方法 ============
|
# ============ 登录相关方法 ============
|
||||||
|
|
||||||
# 执行密码登录
|
# 执行密码登录
|
||||||
@@ -421,19 +599,21 @@ func validate_verification_code(code: String) -> Dictionary:
|
|||||||
# ============ 网络响应处理 ============
|
# ============ 网络响应处理 ============
|
||||||
|
|
||||||
# 处理登录响应
|
# 处理登录响应
|
||||||
func _on_login_response(success: bool, data: Dictionary, error_info: Dictionary):
|
func _on_login_response(success: bool, data: Dictionary, error_info: Dictionary) -> void:
|
||||||
_reset_login_state()
|
_reset_login_state()
|
||||||
|
|
||||||
var result = ResponseHandler.handle_login_response(success, data, error_info)
|
var result = ResponseHandler.handle_login_response(success, data, error_info)
|
||||||
|
|
||||||
if result.should_show_toast:
|
if result.should_show_toast:
|
||||||
show_toast_message.emit(result.message, result.success)
|
show_toast_message.emit(result.message, result.success)
|
||||||
|
|
||||||
if result.success:
|
if result.success:
|
||||||
var username = ""
|
# 保存 Token 到内存和本地
|
||||||
if data.has("data") and data.data.has("user") and data.data.user.has("username"):
|
_save_tokens_to_memory(data)
|
||||||
username = data.data.user.username
|
_save_tokens_to_local(data)
|
||||||
|
|
||||||
|
var username: String = _user_info.get("username", "")
|
||||||
|
|
||||||
# 延迟发送登录成功信号
|
# 延迟发送登录成功信号
|
||||||
await Engine.get_main_loop().create_timer(1.0).timeout
|
await Engine.get_main_loop().create_timer(1.0).timeout
|
||||||
login_success.emit(username)
|
login_success.emit(username)
|
||||||
@@ -441,19 +621,21 @@ func _on_login_response(success: bool, data: Dictionary, error_info: Dictionary)
|
|||||||
login_failed.emit(result.message)
|
login_failed.emit(result.message)
|
||||||
|
|
||||||
# 处理验证码登录响应
|
# 处理验证码登录响应
|
||||||
func _on_verification_login_response(success: bool, data: Dictionary, error_info: Dictionary):
|
func _on_verification_login_response(success: bool, data: Dictionary, error_info: Dictionary) -> void:
|
||||||
_reset_login_state()
|
_reset_login_state()
|
||||||
|
|
||||||
var result = ResponseHandler.handle_verification_code_login_response(success, data, error_info)
|
var result = ResponseHandler.handle_verification_code_login_response(success, data, error_info)
|
||||||
|
|
||||||
if result.should_show_toast:
|
if result.should_show_toast:
|
||||||
show_toast_message.emit(result.message, result.success)
|
show_toast_message.emit(result.message, result.success)
|
||||||
|
|
||||||
if result.success:
|
if result.success:
|
||||||
var username = ""
|
# 保存 Token 到内存和本地
|
||||||
if data.has("data") and data.data.has("user") and data.data.user.has("username"):
|
_save_tokens_to_memory(data)
|
||||||
username = data.data.user.username
|
_save_tokens_to_local(data)
|
||||||
|
|
||||||
|
var username: String = _user_info.get("username", "")
|
||||||
|
|
||||||
await Engine.get_main_loop().create_timer(1.0).timeout
|
await Engine.get_main_loop().create_timer(1.0).timeout
|
||||||
login_success.emit(username)
|
login_success.emit(username)
|
||||||
else:
|
else:
|
||||||
|
|||||||
678
_Core/managers/ChatManager.gd
Normal file
678
_Core/managers/ChatManager.gd
Normal file
@@ -0,0 +1,678 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# ChatManager.gd - 聊天系统业务逻辑核心
|
||||||
|
# ============================================================================
|
||||||
|
# 管理聊天功能的核心业务逻辑
|
||||||
|
#
|
||||||
|
# 核心职责:
|
||||||
|
# - 聊天消息发送/接收协调
|
||||||
|
# - 客户端频率限制(10条/分钟)
|
||||||
|
# - 消息历史管理(最多100条)
|
||||||
|
# - Signal Up: 通过信号和 EventSystem 向上通知
|
||||||
|
# - 整合 AuthManager 获取 token
|
||||||
|
#
|
||||||
|
# 使用方式:
|
||||||
|
# ChatManager.connect_to_chat_server()
|
||||||
|
# ChatManager.send_chat_message("Hello", "local")
|
||||||
|
# ChatManager.chat_message_received.connect(_on_message_received)
|
||||||
|
#
|
||||||
|
# 注意事项:
|
||||||
|
# - 作为自动加载单例,全局可访问
|
||||||
|
# - 遵循 "Signal Up, Call Down" 架构
|
||||||
|
# - 所有聊天事件通过 EventSystem 广播
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 信号定义 (Signal Up)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 聊天消息已发送信号
|
||||||
|
# 参数:
|
||||||
|
# message_id: String - 消息 ID
|
||||||
|
# timestamp: float - 时间戳
|
||||||
|
signal chat_message_sent(message_id: String, timestamp: float)
|
||||||
|
|
||||||
|
# 聊天消息已接收信号
|
||||||
|
# 参数:
|
||||||
|
# from_user: String - 发送者用户名
|
||||||
|
# content: String - 消息内容
|
||||||
|
# show_bubble: bool - 是否显示气泡
|
||||||
|
# timestamp: float - 时间戳
|
||||||
|
signal chat_message_received(from_user: String, content: String, show_bubble: bool, timestamp: float)
|
||||||
|
|
||||||
|
# 聊天错误发生信号
|
||||||
|
# 参数:
|
||||||
|
# error_code: String - 错误代码
|
||||||
|
# message: String - 错误消息
|
||||||
|
signal chat_error_occurred(error_code: String, message: String)
|
||||||
|
|
||||||
|
# 聊天连接状态变化信号
|
||||||
|
# 参数:
|
||||||
|
# state: int - 连接状态(0=DISCONNECTED, 1=CONNECTING, 2=CONNECTED, 3=RECONNECTING, 4=ERROR)
|
||||||
|
signal chat_connection_state_changed(state: int)
|
||||||
|
|
||||||
|
# 位置更新成功信号
|
||||||
|
# 参数:
|
||||||
|
# stream: String - Stream 名称
|
||||||
|
# topic: String - Topic 名称
|
||||||
|
signal chat_position_updated(stream: String, topic: String)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 常量定义
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# WebSocket 服务器 URL(原生 WebSocket)
|
||||||
|
const WEBSOCKET_URL: String = "wss://whaletownend.xinghangee.icu/game"
|
||||||
|
|
||||||
|
# 重连配置
|
||||||
|
const RECONNECT_MAX_ATTEMPTS: int = 5
|
||||||
|
const RECONNECT_BASE_DELAY: float = 3.0
|
||||||
|
|
||||||
|
# 频率限制配置
|
||||||
|
const RATE_LIMIT_MESSAGES: int = 10
|
||||||
|
const RATE_LIMIT_WINDOW: float = 60.0 # 秒
|
||||||
|
|
||||||
|
# 消息限制
|
||||||
|
const MAX_MESSAGE_LENGTH: int = 1000
|
||||||
|
|
||||||
|
# 当前会话消息限制(当前游戏会话,超过后删除最旧的)
|
||||||
|
const MAX_SESSION_MESSAGES: int = 100
|
||||||
|
|
||||||
|
# 历史消息分页大小(从 Zulip 后端每次加载的数量)
|
||||||
|
const HISTORY_PAGE_SIZE: int = 100
|
||||||
|
|
||||||
|
# 错误消息映射
|
||||||
|
const CHAT_ERROR_MESSAGES: Dictionary = {
|
||||||
|
"AUTH_FAILED": "聊天认证失败,请重新登录",
|
||||||
|
"RATE_LIMIT": "消息发送过于频繁,请稍后再试",
|
||||||
|
"CONTENT_FILTERED": "消息内容包含违规内容",
|
||||||
|
"CONTENT_TOO_LONG": "消息内容过长(最大1000字符)",
|
||||||
|
"PERMISSION_DENIED": "您没有权限发送消息",
|
||||||
|
"SESSION_EXPIRED": "会话已过期,请重新连接",
|
||||||
|
"ZULIP_ERROR": "消息服务暂时不可用",
|
||||||
|
"INTERNAL_ERROR": "服务器内部错误"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 成员变量
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# WebSocket 管理器
|
||||||
|
var _websocket_manager: WebSocketManager
|
||||||
|
|
||||||
|
# 是否已登录
|
||||||
|
var _is_logged_in: bool = false
|
||||||
|
|
||||||
|
# 消息历史记录(当前会话,最多100条,超过后删除最旧的)
|
||||||
|
var _message_history: Array[Dictionary] = []
|
||||||
|
|
||||||
|
# 历史消息加载状态
|
||||||
|
var _history_loading: bool = false
|
||||||
|
var _has_more_history: bool = true
|
||||||
|
var _oldest_message_timestamp: float = 0.0
|
||||||
|
|
||||||
|
# 消息发送时间戳(用于频率限制)
|
||||||
|
var _message_timestamps: Array[float] = []
|
||||||
|
|
||||||
|
# 当前用户信息
|
||||||
|
var _current_username: String = ""
|
||||||
|
var _current_map: String = ""
|
||||||
|
|
||||||
|
# 游戏 token
|
||||||
|
var _game_token: String = ""
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 生命周期方法
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 初始化
|
||||||
|
func _ready() -> void:
|
||||||
|
print("ChatManager 初始化完成")
|
||||||
|
|
||||||
|
# 创建 WebSocket 管理器
|
||||||
|
_websocket_manager = WebSocketManager.new()
|
||||||
|
add_child(_websocket_manager)
|
||||||
|
|
||||||
|
# 连接信号
|
||||||
|
_connect_signals()
|
||||||
|
|
||||||
|
# 清理
|
||||||
|
func _exit_tree() -> void:
|
||||||
|
if is_instance_valid(_websocket_manager):
|
||||||
|
_websocket_manager.queue_free()
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 公共 API - Token 管理
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 设置游戏 token
|
||||||
|
#
|
||||||
|
# 参数:
|
||||||
|
# token: String - 游戏认证 token
|
||||||
|
#
|
||||||
|
# 使用示例:
|
||||||
|
# ChatManager.set_game_token("your_game_token")
|
||||||
|
func set_game_token(token: String) -> void:
|
||||||
|
_game_token = token
|
||||||
|
print("ChatManager: 游戏 token 已设置")
|
||||||
|
|
||||||
|
# 获取游戏 token
|
||||||
|
#
|
||||||
|
# 返回值:
|
||||||
|
# String - 当前游戏 token
|
||||||
|
func get_game_token() -> String:
|
||||||
|
return _game_token
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 公共 API - 连接管理
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 连接到聊天服务器
|
||||||
|
func connect_to_chat_server() -> void:
|
||||||
|
if _websocket_manager.is_websocket_connected():
|
||||||
|
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"}
|
||||||
|
_websocket_manager.send_message(JSON.stringify(logout_data))
|
||||||
|
_is_logged_in = false
|
||||||
|
|
||||||
|
# 断开连接
|
||||||
|
_websocket_manager.disconnect_websocket()
|
||||||
|
|
||||||
|
# 检查是否已连接
|
||||||
|
#
|
||||||
|
# 返回值:
|
||||||
|
# bool - 是否已连接
|
||||||
|
func is_chat_connected() -> bool:
|
||||||
|
return _websocket_manager.is_websocket_connected()
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 公共 API - 聊天操作
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 发送聊天消息
|
||||||
|
#
|
||||||
|
# 参数:
|
||||||
|
# content: String - 消息内容
|
||||||
|
# scope: String - 消息范围("local" 或具体 topic 名称)
|
||||||
|
#
|
||||||
|
# 使用示例:
|
||||||
|
# ChatManager.send_chat_message("Hello, world!", "local")
|
||||||
|
func send_chat_message(content: String, scope: String = "local") -> void:
|
||||||
|
# 检查连接状态
|
||||||
|
if not _websocket_manager.is_websocket_connected():
|
||||||
|
_handle_error("NOT_CONNECTED", "未连接到聊天服务器")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 检查登录状态
|
||||||
|
if not _is_logged_in:
|
||||||
|
_handle_error("NOT_LOGGED_IN", "尚未登录聊天服务器")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 检查消息长度
|
||||||
|
if content.length() > MAX_MESSAGE_LENGTH:
|
||||||
|
_handle_error("CONTENT_TOO_LONG", "消息内容过长")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 检查频率限制
|
||||||
|
if not can_send_message():
|
||||||
|
var wait_time := get_time_until_next_message()
|
||||||
|
_handle_error("RATE_LIMIT", "请等待 %.1f 秒后再试" % wait_time)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 构建消息数据
|
||||||
|
var message_data := {
|
||||||
|
"type": "chat",
|
||||||
|
"content": content,
|
||||||
|
"scope": scope
|
||||||
|
}
|
||||||
|
|
||||||
|
# 发送消息(JSON 字符串)
|
||||||
|
var json_string := JSON.stringify(message_data)
|
||||||
|
_websocket_manager.send_message(json_string)
|
||||||
|
|
||||||
|
# 记录发送时间
|
||||||
|
_record_message_timestamp()
|
||||||
|
|
||||||
|
# 添加到历史
|
||||||
|
_add_message_to_history({
|
||||||
|
"from_user": _current_username,
|
||||||
|
"content": content,
|
||||||
|
"timestamp": Time.get_unix_time_from_system(),
|
||||||
|
"is_self": true
|
||||||
|
})
|
||||||
|
|
||||||
|
print("📤 发送聊天消息: ", content)
|
||||||
|
|
||||||
|
# 消息发送完成回调
|
||||||
|
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)
|
||||||
|
EventSystem.emit_event(EventNames.CHAT_MESSAGE_SENT, {
|
||||||
|
"message_id": message_id,
|
||||||
|
"timestamp": timestamp
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
print("❌ 消息发送失败: ", error_info)
|
||||||
|
_handle_error("SEND_FAILED", error_info.get("message", "发送失败"))
|
||||||
|
|
||||||
|
# 更新玩家位置
|
||||||
|
#
|
||||||
|
# 参数:
|
||||||
|
# x: float - X 坐标
|
||||||
|
# y: float - Y 坐标
|
||||||
|
# map_id: String - 地图 ID
|
||||||
|
#
|
||||||
|
# 使用示例:
|
||||||
|
# ChatManager.update_player_position(150.0, 200.0, "novice_village")
|
||||||
|
func update_player_position(x: float, y: float, map_id: String) -> void:
|
||||||
|
if not _websocket_manager.is_websocket_connected():
|
||||||
|
return
|
||||||
|
|
||||||
|
var position_data := {
|
||||||
|
"type": "position",
|
||||||
|
"x": x,
|
||||||
|
"y": y,
|
||||||
|
"mapId": map_id
|
||||||
|
}
|
||||||
|
|
||||||
|
# 发送消息(JSON 字符串)
|
||||||
|
var json_string := JSON.stringify(position_data)
|
||||||
|
_websocket_manager.send_message(json_string)
|
||||||
|
|
||||||
|
print("📍 更新位置: (%.2f, %.2f) in %s" % [x, y, map_id])
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 公共 API - 频率限制
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 检查是否可以发送消息
|
||||||
|
#
|
||||||
|
# 返回值:
|
||||||
|
# bool - 是否可以发送
|
||||||
|
func can_send_message() -> bool:
|
||||||
|
var current_time := Time.get_unix_time_from_system()
|
||||||
|
|
||||||
|
# 清理过期的时间戳
|
||||||
|
var filter_func := func(timestamp: float) -> bool:
|
||||||
|
return current_time - timestamp < RATE_LIMIT_WINDOW
|
||||||
|
_message_timestamps = _message_timestamps.filter(filter_func)
|
||||||
|
|
||||||
|
# 检查数量
|
||||||
|
return _message_timestamps.size() < RATE_LIMIT_MESSAGES
|
||||||
|
|
||||||
|
# 获取距离下次可发送消息的时间
|
||||||
|
#
|
||||||
|
# 返回值:
|
||||||
|
# float - 等待时间(秒)
|
||||||
|
func get_time_until_next_message() -> float:
|
||||||
|
if _message_timestamps.is_empty():
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
if _message_timestamps.size() < RATE_LIMIT_MESSAGES:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
# 找到最早的时间戳
|
||||||
|
var earliest_timestamp: float = _message_timestamps[0]
|
||||||
|
var current_time := Time.get_unix_time_from_system()
|
||||||
|
var elapsed := current_time - earliest_timestamp
|
||||||
|
|
||||||
|
if elapsed >= RATE_LIMIT_WINDOW:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
return RATE_LIMIT_WINDOW - elapsed
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 公共 API - 消息历史
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 获取消息历史
|
||||||
|
#
|
||||||
|
# 返回值:
|
||||||
|
# Array[Dictionary] - 消息历史数组
|
||||||
|
func get_message_history() -> Array[Dictionary]:
|
||||||
|
return _message_history.duplicate()
|
||||||
|
|
||||||
|
# 清空消息历史
|
||||||
|
func clear_message_history() -> void:
|
||||||
|
_message_history.clear()
|
||||||
|
print("🧹 清空消息历史")
|
||||||
|
|
||||||
|
# 重置当前会话(每次登录/重连时调用)
|
||||||
|
#
|
||||||
|
# 功能:
|
||||||
|
# - 清空当前会话消息缓存
|
||||||
|
# - 重置历史消息加载状态
|
||||||
|
# - 不影响 Zulip 后端的历史消息
|
||||||
|
#
|
||||||
|
# 使用场景:
|
||||||
|
# - 用户登录成功后
|
||||||
|
# - 重新连接到聊天服务器后
|
||||||
|
func reset_session() -> void:
|
||||||
|
_message_history.clear()
|
||||||
|
_history_loading = false
|
||||||
|
_has_more_history = true
|
||||||
|
_oldest_message_timestamp = 0.0
|
||||||
|
print("🔄 重置聊天会话")
|
||||||
|
|
||||||
|
# 加载历史消息(按需从 Zulip 后端获取)
|
||||||
|
#
|
||||||
|
# 参数:
|
||||||
|
# count: int - 要加载的消息数量(默认 HISTORY_PAGE_SIZE)
|
||||||
|
#
|
||||||
|
# 功能:
|
||||||
|
# - 从 Zulip 后端获取历史消息
|
||||||
|
# - 添加到当前会话历史开头
|
||||||
|
# - 触发 CHAT_MESSAGE_RECEIVED 事件显示消息
|
||||||
|
#
|
||||||
|
# 使用场景:
|
||||||
|
# - 用户滚动到聊天窗口顶部
|
||||||
|
# - 用户主动点击"加载历史"按钮
|
||||||
|
#
|
||||||
|
# 注意:
|
||||||
|
# - 这是异步操作,需要通过 Zulip API 实现
|
||||||
|
# - 当前实现为占位符,需要后端 API 支持
|
||||||
|
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]
|
||||||
|
_message_history.push_front(message)
|
||||||
|
|
||||||
|
# 触发事件显示消息(Signal Up)
|
||||||
|
EventSystem.emit_event(EventNames.CHAT_MESSAGE_RECEIVED, {
|
||||||
|
"from_user": message.get("from_user", ""),
|
||||||
|
"content": message.get("content", ""),
|
||||||
|
"show_bubble": false,
|
||||||
|
"timestamp": message.get("timestamp", 0.0),
|
||||||
|
"is_history": true # 标记为历史消息
|
||||||
|
})
|
||||||
|
|
||||||
|
# 更新最旧消息时间戳
|
||||||
|
var oldest: Dictionary = messages.back()
|
||||||
|
if oldest.has("timestamp"):
|
||||||
|
_oldest_message_timestamp = oldest.timestamp
|
||||||
|
|
||||||
|
# 检查是否还有更多历史
|
||||||
|
if messages.size() < HISTORY_PAGE_SIZE:
|
||||||
|
_has_more_history = false
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 内部方法 - 信号连接
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 连接信号
|
||||||
|
func _connect_signals() -> void:
|
||||||
|
# WebSocket 管理器信号
|
||||||
|
_websocket_manager.connection_state_changed.connect(_on_connection_state_changed)
|
||||||
|
_websocket_manager.data_received.connect(_on_data_received)
|
||||||
|
|
||||||
|
# 发送登录消息
|
||||||
|
func _send_login_message() -> void:
|
||||||
|
print("📤 发送登录消息...")
|
||||||
|
|
||||||
|
var login_data := {
|
||||||
|
"type": "login",
|
||||||
|
"token": _game_token
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 通过 EventSystem 广播(Signal Up)
|
||||||
|
EventSystem.emit_event(EventNames.CHAT_CONNECTION_STATE_CHANGED, {
|
||||||
|
"state": state
|
||||||
|
})
|
||||||
|
|
||||||
|
# 如果连接成功,发送登录消息
|
||||||
|
if state == 2: # CONNECTED
|
||||||
|
_send_login_message()
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 内部方法 - 消息处理
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# WebSocket 数据接收
|
||||||
|
func _on_data_received(message: String) -> void:
|
||||||
|
# 解析 JSON 消息
|
||||||
|
var json := JSON.new()
|
||||||
|
var parse_result := json.parse(message)
|
||||||
|
|
||||||
|
if parse_result != OK:
|
||||||
|
print("❌ ChatManager: JSON 解析失败 - ", message)
|
||||||
|
return
|
||||||
|
|
||||||
|
var data: Dictionary = json.data
|
||||||
|
|
||||||
|
# 检查消息类型字段
|
||||||
|
var message_type: String = data.get("t", "")
|
||||||
|
|
||||||
|
match message_type:
|
||||||
|
"login_success":
|
||||||
|
_handle_login_success(data)
|
||||||
|
"login_error":
|
||||||
|
_handle_login_error(data)
|
||||||
|
"chat_sent":
|
||||||
|
_handle_chat_sent(data)
|
||||||
|
"chat_error":
|
||||||
|
_handle_chat_error(data)
|
||||||
|
"chat_render":
|
||||||
|
_handle_chat_render(data)
|
||||||
|
"position_updated":
|
||||||
|
_handle_position_updated(data)
|
||||||
|
_:
|
||||||
|
print("⚠️ ChatManager: 未处理的消息类型 - ", message_type)
|
||||||
|
print(" 消息内容: ", data)
|
||||||
|
|
||||||
|
# 处理登录成功
|
||||||
|
func _handle_login_success(data: Dictionary) -> void:
|
||||||
|
print("✅ ChatManager: 登录成功")
|
||||||
|
|
||||||
|
_is_logged_in = true
|
||||||
|
_current_username = data.get("username", "")
|
||||||
|
_current_map = data.get("currentMap", "")
|
||||||
|
|
||||||
|
# 重置当前会话缓存(每次登录/重连都清空,重新开始接收消息)
|
||||||
|
reset_session()
|
||||||
|
|
||||||
|
print(" 用户名: ", _current_username)
|
||||||
|
print(" 地图: ", _current_map)
|
||||||
|
|
||||||
|
# 通过 EventSystem 广播(Signal Up)
|
||||||
|
EventSystem.emit_event(EventNames.CHAT_LOGIN_SUCCESS, {
|
||||||
|
"username": _current_username,
|
||||||
|
"current_map": _current_map
|
||||||
|
})
|
||||||
|
|
||||||
|
# 处理登录失败
|
||||||
|
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)
|
||||||
|
EventSystem.emit_event(EventNames.CHAT_LOGIN_FAILED, {
|
||||||
|
"error_code": "LOGIN_FAILED",
|
||||||
|
"message": error_message
|
||||||
|
})
|
||||||
|
|
||||||
|
# 处理聊天消息发送成功
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 通过 EventSystem 广播(Signal Up)
|
||||||
|
EventSystem.emit_event(EventNames.CHAT_MESSAGE_SENT, {
|
||||||
|
"message_id": message_id,
|
||||||
|
"timestamp": timestamp
|
||||||
|
})
|
||||||
|
|
||||||
|
# 处理聊天消息发送失败
|
||||||
|
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)
|
||||||
|
|
||||||
|
print("📨 收到聊天消息: ", from_user, " -> ", content)
|
||||||
|
|
||||||
|
# 添加到历史
|
||||||
|
_add_message_to_history({
|
||||||
|
"from_user": from_user,
|
||||||
|
"content": content,
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"is_self": false
|
||||||
|
})
|
||||||
|
|
||||||
|
# 发射信号
|
||||||
|
chat_message_received.emit(from_user, content, show_bubble, timestamp)
|
||||||
|
|
||||||
|
# 通过 EventSystem 广播(Signal Up)
|
||||||
|
EventSystem.emit_event(EventNames.CHAT_MESSAGE_RECEIVED, {
|
||||||
|
"from_user": from_user,
|
||||||
|
"content": content,
|
||||||
|
"show_bubble": show_bubble,
|
||||||
|
"timestamp": timestamp
|
||||||
|
})
|
||||||
|
|
||||||
|
# 处理位置更新成功
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 通过 EventSystem 广播(Signal Up)
|
||||||
|
EventSystem.emit_event(EventNames.CHAT_POSITION_UPDATED, {
|
||||||
|
"stream": stream,
|
||||||
|
"topic": topic
|
||||||
|
})
|
||||||
|
|
||||||
|
# 处理错误响应(如果需要)
|
||||||
|
func _handle_error_response(data: Dictionary) -> void:
|
||||||
|
var error_code: String = data.get("code", "")
|
||||||
|
var error_message: String = data.get("message", "")
|
||||||
|
|
||||||
|
_handle_error(error_code, error_message)
|
||||||
|
|
||||||
|
# 处理 Socket 错误(如果需要)
|
||||||
|
func _on_socket_error(error: String) -> void:
|
||||||
|
_handle_error("SOCKET_ERROR", error)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 内部方法 - 工具函数
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 处理错误
|
||||||
|
func _handle_error(error_code: String, error_message: String) -> void:
|
||||||
|
print("❌ ChatManager 错误: [", error_code, "] ", error_message)
|
||||||
|
|
||||||
|
# 获取用户友好的错误消息
|
||||||
|
var user_message: String = CHAT_ERROR_MESSAGES.get(error_code, error_message) as String
|
||||||
|
|
||||||
|
# 发射信号
|
||||||
|
chat_error_occurred.emit(error_code, user_message)
|
||||||
|
|
||||||
|
# 通过 EventSystem 广播(Signal Up)
|
||||||
|
EventSystem.emit_event(EventNames.CHAT_ERROR_OCCURRED, {
|
||||||
|
"error_code": error_code,
|
||||||
|
"message": user_message
|
||||||
|
})
|
||||||
|
|
||||||
|
# 特殊处理认证失败
|
||||||
|
if error_code == "AUTH_FAILED" or error_code == "SESSION_EXPIRED":
|
||||||
|
_is_logged_in = false
|
||||||
|
EventSystem.emit_event(EventNames.CHAT_LOGIN_FAILED, {
|
||||||
|
"error_code": error_code
|
||||||
|
})
|
||||||
|
|
||||||
|
# 记录消息发送时间戳
|
||||||
|
func _record_message_timestamp() -> void:
|
||||||
|
var current_time := Time.get_unix_time_from_system()
|
||||||
|
_message_timestamps.append(current_time)
|
||||||
|
|
||||||
|
# 添加消息到当前会话历史
|
||||||
|
func _add_message_to_history(message: Dictionary) -> void:
|
||||||
|
_message_history.append(message)
|
||||||
|
|
||||||
|
# 更新最旧消息时间戳(用于历史消息加载)
|
||||||
|
if _oldest_message_timestamp == 0.0 or message.timestamp < _oldest_message_timestamp:
|
||||||
|
_oldest_message_timestamp = message.timestamp
|
||||||
|
|
||||||
|
# 限制当前会话消息数量(超过后删除最旧的)
|
||||||
|
if _message_history.size() > MAX_SESSION_MESSAGES:
|
||||||
|
_message_history.pop_front()
|
||||||
1
_Core/managers/ChatManager.gd.uid
Normal file
1
_Core/managers/ChatManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://b6lnbss2i3pss
|
||||||
461
_Core/managers/WebSocketManager.gd
Normal file
461
_Core/managers/WebSocketManager.gd
Normal file
@@ -0,0 +1,461 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# WebSocketManager.gd - WebSocket 连接生命周期管理(原生 WebSocket 版本)
|
||||||
|
# ============================================================================
|
||||||
|
# 管理 WebSocket 连接状态、自动重连和错误恢复
|
||||||
|
#
|
||||||
|
# 核心职责:
|
||||||
|
# - 连接状态管理(断开、连接中、已连接、重连中)
|
||||||
|
# - 自动重连机制(指数退避)
|
||||||
|
# - 连接错误恢复
|
||||||
|
# - WebSocket 消息发送/接收
|
||||||
|
# ============================================================================
|
||||||
|
# 使用方式:
|
||||||
|
# WebSocketManager.connect_to_game_server()
|
||||||
|
# WebSocketManager.connection_state_changed.connect(_on_state_changed)
|
||||||
|
#
|
||||||
|
# 注意事项:
|
||||||
|
# - 作为自动加载单例,全局可访问
|
||||||
|
# - 自动处理连接断开和重连
|
||||||
|
# - 通过信号通知连接状态变化
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
class_name WebSocketManager
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 信号定义
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 连接状态变化信号
|
||||||
|
# 参数:
|
||||||
|
# new_state: ConnectionState - 新的连接状态
|
||||||
|
signal connection_state_changed(new_state: ConnectionState)
|
||||||
|
|
||||||
|
# 连接丢失信号
|
||||||
|
signal connection_lost()
|
||||||
|
|
||||||
|
# 重连成功信号
|
||||||
|
signal reconnection_succeeded()
|
||||||
|
|
||||||
|
# 重连失败信号
|
||||||
|
# 参数:
|
||||||
|
# attempt: int - 当前重连尝试次数
|
||||||
|
# max_attempts: int - 最大重连次数
|
||||||
|
signal reconnection_failed(attempt: int, max_attempts: int)
|
||||||
|
|
||||||
|
# WebSocket 消息接收信号
|
||||||
|
# 参数:
|
||||||
|
# message: String - 接收到的消息内容(JSON 字符串)
|
||||||
|
signal data_received(message: String)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 枚举定义
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 连接状态枚举
|
||||||
|
enum ConnectionState {
|
||||||
|
DISCONNECTED, # 未连接
|
||||||
|
CONNECTING, # 连接中
|
||||||
|
CONNECTED, # 已连接
|
||||||
|
RECONNECTING, # 重连中
|
||||||
|
ERROR # 错误状态
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 常量定义
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# WebSocket 服务器 URL(原生 WebSocket)
|
||||||
|
const WEBSOCKET_URL: String = "wss://whaletownend.xinghangee.icu/game"
|
||||||
|
|
||||||
|
# 默认最大重连次数
|
||||||
|
const DEFAULT_MAX_RECONNECT_ATTEMPTS: int = 5
|
||||||
|
|
||||||
|
# 默认重连基础延迟(秒)
|
||||||
|
const DEFAULT_RECONNECT_BASE_DELAY: float = 3.0
|
||||||
|
|
||||||
|
# 最大重连延迟(秒)
|
||||||
|
const MAX_RECONNECT_DELAY: float = 30.0
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 成员变量
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# WebSocket peer
|
||||||
|
var _websocket_peer: WebSocketPeer = WebSocketPeer.new()
|
||||||
|
|
||||||
|
# 当前连接状态
|
||||||
|
var _connection_state: ConnectionState = ConnectionState.DISCONNECTED
|
||||||
|
|
||||||
|
# 自动重连启用标志
|
||||||
|
var _auto_reconnect_enabled: bool = true
|
||||||
|
|
||||||
|
# 最大重连次数
|
||||||
|
var _max_reconnect_attempts: int = DEFAULT_MAX_RECONNECT_ATTEMPTS
|
||||||
|
|
||||||
|
# 重连基础延迟
|
||||||
|
var _reconnect_base_delay: float = DEFAULT_RECONNECT_BASE_DELAY
|
||||||
|
|
||||||
|
# 当前重连尝试次数
|
||||||
|
var _reconnect_attempt: int = 0
|
||||||
|
|
||||||
|
# 重连定时器
|
||||||
|
var _reconnect_timer: Timer = Timer.new()
|
||||||
|
|
||||||
|
# 是否为正常关闭(非异常断开)
|
||||||
|
var _clean_close: bool = true
|
||||||
|
|
||||||
|
# 心跳定时器
|
||||||
|
var _heartbeat_timer: Timer = Timer.new()
|
||||||
|
|
||||||
|
# 心跳间隔(秒)
|
||||||
|
const HEARTBEAT_INTERVAL: float = 30.0
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 生命周期方法
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 初始化
|
||||||
|
func _ready() -> void:
|
||||||
|
print("WebSocketManager 初始化完成")
|
||||||
|
|
||||||
|
# 设置重连定时器
|
||||||
|
_setup_reconnect_timer()
|
||||||
|
|
||||||
|
# 设置心跳定时器
|
||||||
|
_setup_heartbeat_timer()
|
||||||
|
|
||||||
|
# 启动处理循环
|
||||||
|
set_process(true)
|
||||||
|
|
||||||
|
# 处理每帧
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
# 检查 WebSocket 状态变化
|
||||||
|
_check_websocket_state()
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
# 处理收到的数据
|
||||||
|
while _websocket_peer.get_available_packet_count() > 0:
|
||||||
|
var packet: PackedByteArray = _websocket_peer.get_packet()
|
||||||
|
var message: String = packet.get_string_from_utf8()
|
||||||
|
|
||||||
|
# 发射消息接收信号
|
||||||
|
data_received.emit(message)
|
||||||
|
|
||||||
|
# 打印调试信息
|
||||||
|
print("📨 WebSocket 收到消息: ", message)
|
||||||
|
|
||||||
|
# 清理
|
||||||
|
func _exit_tree() -> void:
|
||||||
|
_disconnect()
|
||||||
|
|
||||||
|
if is_instance_valid(_reconnect_timer):
|
||||||
|
_reconnect_timer.stop()
|
||||||
|
_reconnect_timer.queue_free()
|
||||||
|
|
||||||
|
if is_instance_valid(_heartbeat_timer):
|
||||||
|
_heartbeat_timer.stop()
|
||||||
|
_heartbeat_timer.queue_free()
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 公共 API - 连接管理
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 连接到游戏服务器
|
||||||
|
func connect_to_game_server() -> 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)
|
||||||
|
_clean_close = true
|
||||||
|
_reconnect_attempt = 0
|
||||||
|
|
||||||
|
var err: Error = _websocket_peer.connect_to_url(WEBSOCKET_URL)
|
||||||
|
if err != OK:
|
||||||
|
print("❌ WebSocket 连接失败: ", error_string(err))
|
||||||
|
_set_connection_state(ConnectionState.ERROR)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 启动心跳
|
||||||
|
_start_heartbeat()
|
||||||
|
|
||||||
|
# 断开 WebSocket 连接
|
||||||
|
func disconnect_websocket() -> void:
|
||||||
|
print("=== WebSocketManager 断开连接 ===")
|
||||||
|
_disconnect()
|
||||||
|
|
||||||
|
# 断开连接(内部方法)
|
||||||
|
func _disconnect() -> void:
|
||||||
|
_clean_close = true
|
||||||
|
|
||||||
|
# 停止重连定时器
|
||||||
|
_reconnect_timer.stop()
|
||||||
|
|
||||||
|
# 停止心跳
|
||||||
|
_heartbeat_timer.stop()
|
||||||
|
|
||||||
|
# 关闭 WebSocket
|
||||||
|
if _websocket_peer.get_ready_state() == WebSocketPeer.STATE_OPEN:
|
||||||
|
_websocket_peer.close()
|
||||||
|
|
||||||
|
_set_connection_state(ConnectionState.DISCONNECTED)
|
||||||
|
|
||||||
|
# 检查 WebSocket 是否已连接
|
||||||
|
#
|
||||||
|
# 返回值:
|
||||||
|
# bool - WebSocket 是否已连接
|
||||||
|
func is_websocket_connected() -> bool:
|
||||||
|
return _connection_state == ConnectionState.CONNECTED
|
||||||
|
|
||||||
|
# 获取当前连接状态
|
||||||
|
#
|
||||||
|
# 返回值:
|
||||||
|
# ConnectionState - 当前连接状态
|
||||||
|
func get_connection_state() -> ConnectionState:
|
||||||
|
return _connection_state
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 公共 API - 消息发送
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 发送 WebSocket 消息
|
||||||
|
#
|
||||||
|
# 参数:
|
||||||
|
# message: String - 要发送的消息内容(JSON 字符串)
|
||||||
|
#
|
||||||
|
# 返回值:
|
||||||
|
# Error - 错误码,OK 表示成功
|
||||||
|
func send_message(message: String) -> Error:
|
||||||
|
if _websocket_peer.get_ready_state() != WebSocketPeer.STATE_OPEN:
|
||||||
|
print("❌ WebSocket 未连接,无法发送消息")
|
||||||
|
return ERR_UNCONFIGURED
|
||||||
|
|
||||||
|
var err: Error = _websocket_peer.send_text(message)
|
||||||
|
if err != OK:
|
||||||
|
print("❌ WebSocket 发送消息失败: ", error_string(err))
|
||||||
|
return err
|
||||||
|
|
||||||
|
print("📤 发送 WebSocket 消息: ", message)
|
||||||
|
return OK
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 公共 API - 自动重连
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 启用/禁用自动重连
|
||||||
|
#
|
||||||
|
# 参数:
|
||||||
|
# enabled: bool - 是否启用自动重连
|
||||||
|
# max_attempts: int - 最大重连次数(默认 5)
|
||||||
|
# base_delay: float - 基础重连延迟,秒(默认 3.0)
|
||||||
|
#
|
||||||
|
# 使用示例:
|
||||||
|
# WebSocketManager.enable_auto_reconnect(true, 5, 3.0)
|
||||||
|
func enable_auto_reconnect(enabled: bool, max_attempts: int = DEFAULT_MAX_RECONNECT_ATTEMPTS, base_delay: float = DEFAULT_RECONNECT_BASE_DELAY) -> void:
|
||||||
|
_auto_reconnect_enabled = enabled
|
||||||
|
_max_reconnect_attempts = max_attempts
|
||||||
|
_reconnect_base_delay = base_delay
|
||||||
|
|
||||||
|
print("自动重连: ", "启用" if enabled else "禁用")
|
||||||
|
print("最大重连次数: ", _max_reconnect_attempts)
|
||||||
|
print("基础重连延迟: ", _reconnect_base_delay, " 秒")
|
||||||
|
|
||||||
|
# 获取重连信息
|
||||||
|
#
|
||||||
|
# 返回值:
|
||||||
|
# Dictionary - 重连信息 {enabled, attempt, max_attempts, delay}
|
||||||
|
func get_reconnect_info() -> Dictionary:
|
||||||
|
return {
|
||||||
|
"enabled": _auto_reconnect_enabled,
|
||||||
|
"attempt": _reconnect_attempt,
|
||||||
|
"max_attempts": _max_reconnect_attempts,
|
||||||
|
"next_delay": _calculate_reconnect_delay() if _connection_state == ConnectionState.RECONNECTING else 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 内部方法 - 连接状态管理
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 设置连接状态
|
||||||
|
func _set_connection_state(new_state: ConnectionState) -> void:
|
||||||
|
if _connection_state == new_state:
|
||||||
|
return
|
||||||
|
|
||||||
|
_connection_state = new_state
|
||||||
|
print("📡 连接状态变更: ", ConnectionState.keys()[new_state])
|
||||||
|
|
||||||
|
# 发射信号
|
||||||
|
connection_state_changed.emit(new_state)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 内部方法 - WebSocket 状态监控
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 检查 WebSocket 状态变化
|
||||||
|
func _check_websocket_state() -> void:
|
||||||
|
# 必须先 poll 才能获取最新状态
|
||||||
|
_websocket_peer.poll()
|
||||||
|
|
||||||
|
var state: WebSocketPeer.State = _websocket_peer.get_ready_state()
|
||||||
|
|
||||||
|
match state:
|
||||||
|
WebSocketPeer.STATE_CONNECTING:
|
||||||
|
# 正在连接
|
||||||
|
if _connection_state != ConnectionState.CONNECTING and _connection_state != ConnectionState.RECONNECTING:
|
||||||
|
_set_connection_state(ConnectionState.CONNECTING)
|
||||||
|
|
||||||
|
WebSocketPeer.STATE_OPEN:
|
||||||
|
# 连接成功
|
||||||
|
if _connection_state != ConnectionState.CONNECTED:
|
||||||
|
_on_websocket_connected()
|
||||||
|
|
||||||
|
WebSocketPeer.STATE_CLOSING:
|
||||||
|
# 正在关闭
|
||||||
|
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 表示正常关闭
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# 如果是异常断开且启用了自动重连
|
||||||
|
if not clean_close and _auto_reconnect_enabled:
|
||||||
|
connection_lost.emit()
|
||||||
|
_attempt_reconnect()
|
||||||
|
else:
|
||||||
|
_set_connection_state(ConnectionState.DISCONNECTED)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 内部方法 - 重连机制
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 设置重连定时器
|
||||||
|
func _setup_reconnect_timer() -> void:
|
||||||
|
_reconnect_timer = Timer.new()
|
||||||
|
_reconnect_timer.one_shot = true
|
||||||
|
_reconnect_timer.autostart = false
|
||||||
|
add_child(_reconnect_timer)
|
||||||
|
|
||||||
|
_reconnect_timer.timeout.connect(_on_reconnect_timeout)
|
||||||
|
|
||||||
|
# 尝试重连
|
||||||
|
func _attempt_reconnect() -> void:
|
||||||
|
# 检查是否超过最大重连次数
|
||||||
|
if _reconnect_attempt >= _max_reconnect_attempts:
|
||||||
|
print("❌ 达到最大重连次数 (", _max_reconnect_attempts, "),停止重连")
|
||||||
|
reconnection_failed.emit(_reconnect_attempt, _max_reconnect_attempts)
|
||||||
|
_set_connection_state(ConnectionState.ERROR)
|
||||||
|
return
|
||||||
|
|
||||||
|
_reconnect_attempt += 1
|
||||||
|
_set_connection_state(ConnectionState.RECONNECTING)
|
||||||
|
|
||||||
|
# 计算重连延迟(指数退避)
|
||||||
|
var delay: float = _calculate_reconnect_delay()
|
||||||
|
print("🔄 尝试重连 (", _reconnect_attempt, "/", _max_reconnect_attempts, ")")
|
||||||
|
print(" 延迟: ", delay, " 秒")
|
||||||
|
|
||||||
|
# 启动重连定时器
|
||||||
|
_reconnect_timer.start(delay)
|
||||||
|
|
||||||
|
# 计算重连延迟(指数退避)
|
||||||
|
func _calculate_reconnect_delay() -> float:
|
||||||
|
# 指数退避: base_delay * 2^(attempt-1)
|
||||||
|
var delay: float = _reconnect_base_delay * pow(2.0, _reconnect_attempt - 1)
|
||||||
|
|
||||||
|
# 限制最大延迟
|
||||||
|
return min(delay, MAX_RECONNECT_DELAY)
|
||||||
|
|
||||||
|
# 重连定时器超时处理
|
||||||
|
func _on_reconnect_timeout() -> void:
|
||||||
|
print("⏰ 重连定时器超时,开始重连...")
|
||||||
|
_clean_close = false
|
||||||
|
connect_to_game_server()
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 内部方法 - 心跳机制
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 设置心跳定时器
|
||||||
|
func _setup_heartbeat_timer() -> void:
|
||||||
|
_heartbeat_timer = Timer.new()
|
||||||
|
_heartbeat_timer.wait_time = HEARTBEAT_INTERVAL
|
||||||
|
_heartbeat_timer.one_shot = false
|
||||||
|
_heartbeat_timer.autostart = false
|
||||||
|
add_child(_heartbeat_timer)
|
||||||
|
|
||||||
|
_heartbeat_timer.timeout.connect(_on_heartbeat)
|
||||||
|
|
||||||
|
# 启动心跳
|
||||||
|
func _start_heartbeat() -> void:
|
||||||
|
_heartbeat_timer.start()
|
||||||
|
|
||||||
|
# 停止心跳
|
||||||
|
func _stop_heartbeat() -> void:
|
||||||
|
_heartbeat_timer.stop()
|
||||||
|
|
||||||
|
# 心跳超时处理
|
||||||
|
func _on_heartbeat() -> void:
|
||||||
|
# 不发送心跳,避免服务器返回 "消息格式错误"
|
||||||
|
# 如果需要心跳,服务器应该支持特定格式
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 工具方法
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 获取连接状态描述
|
||||||
|
#
|
||||||
|
# 返回值:
|
||||||
|
# String - 连接状态描述
|
||||||
|
func get_state_description() -> String:
|
||||||
|
match _connection_state:
|
||||||
|
ConnectionState.DISCONNECTED:
|
||||||
|
return "未连接"
|
||||||
|
ConnectionState.CONNECTING:
|
||||||
|
return "连接中"
|
||||||
|
ConnectionState.CONNECTED:
|
||||||
|
return "已连接"
|
||||||
|
ConnectionState.RECONNECTING:
|
||||||
|
return "重连中 (%d/%d)" % [_reconnect_attempt, _max_reconnect_attempts]
|
||||||
|
ConnectionState.ERROR:
|
||||||
|
return "错误"
|
||||||
|
_:
|
||||||
|
return "未知状态"
|
||||||
1
_Core/managers/WebSocketManager.gd.uid
Normal file
1
_Core/managers/WebSocketManager.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dmbgtbf6gyk6t
|
||||||
132
addons/gut/GutScene.gd
Normal file
132
addons/gut/GutScene.gd
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
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)
|
||||||
1
addons/gut/GutScene.gd.uid
Normal file
1
addons/gut/GutScene.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bw7tukh738kw1
|
||||||
16
addons/gut/GutScene.tscn
Normal file
16
addons/gut/GutScene.tscn
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[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
|
||||||
22
addons/gut/LICENSE.md
Normal file
22
addons/gut/LICENSE.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
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.
|
||||||
52
addons/gut/UserFileViewer.gd
Normal file
52
addons/gut/UserFileViewer.gd
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
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()
|
||||||
1
addons/gut/UserFileViewer.gd.uid
Normal file
1
addons/gut/UserFileViewer.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://x51wilphva3d
|
||||||
92
addons/gut/UserFileViewer.tscn
Normal file
92
addons/gut/UserFileViewer.tscn
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
[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"]
|
||||||
86
addons/gut/autofree.gd
Normal file
86
addons/gut/autofree.gd
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# ##############################################################################
|
||||||
|
#(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)
|
||||||
1
addons/gut/autofree.gd.uid
Normal file
1
addons/gut/autofree.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bxjfriqxgwe0r
|
||||||
201
addons/gut/awaiter.gd
Normal file
201
addons/gut/awaiter.gd
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
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
|
||||||
1
addons/gut/awaiter.gd.uid
Normal file
1
addons/gut/awaiter.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://ccu4ww35edtdi
|
||||||
239
addons/gut/cli/change_project_warnings.gd
Normal file
239
addons/gut/cli/change_project_warnings.gd
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
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()
|
||||||
1
addons/gut/cli/change_project_warnings.gd.uid
Normal file
1
addons/gut/cli/change_project_warnings.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://1pauyfnd1cre
|
||||||
315
addons/gut/cli/gut_cli.gd
Normal file
315
addons/gut/cli/gut_cli.gd
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
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.
|
||||||
|
#
|
||||||
|
# ##############################################################################
|
||||||
1
addons/gut/cli/gut_cli.gd.uid
Normal file
1
addons/gut/cli/gut_cli.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bhuudqinp4bth
|
||||||
678
addons/gut/cli/optparse.gd
Normal file
678
addons/gut/cli/optparse.gd
Normal file
@@ -0,0 +1,678 @@
|
|||||||
|
## 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.
|
||||||
|
#
|
||||||
|
# ##############################################################################
|
||||||
1
addons/gut/cli/optparse.gd.uid
Normal file
1
addons/gut/cli/optparse.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://c8m4fojwln6bq
|
||||||
208
addons/gut/collected_script.gd
Normal file
208
addons/gut/collected_script.gd
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# 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
|
||||||
1
addons/gut/collected_script.gd.uid
Normal file
1
addons/gut/collected_script.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bjjcnr1oqvag6
|
||||||
120
addons/gut/collected_test.gd
Normal file
120
addons/gut/collected_test.gd
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
1
addons/gut/collected_test.gd.uid
Normal file
1
addons/gut/collected_test.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cl854f1m26a2a
|
||||||
125
addons/gut/comparator.gd
Normal file
125
addons/gut/comparator.gd
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
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 '!='
|
||||||
1
addons/gut/comparator.gd.uid
Normal file
1
addons/gut/comparator.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bohry7fhscy7y
|
||||||
70
addons/gut/compare_result.gd
Normal file
70
addons/gut/compare_result.gd
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
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
|
||||||
|
|
||||||
1
addons/gut/compare_result.gd.uid
Normal file
1
addons/gut/compare_result.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cow1xqmqqvn4e
|
||||||
63
addons/gut/diff_formatter.gd
Normal file
63
addons/gut/diff_formatter.gd
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
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
|
||||||
|
|
||||||
1
addons/gut/diff_formatter.gd.uid
Normal file
1
addons/gut/diff_formatter.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://ch2km05phxacd
|
||||||
156
addons/gut/diff_tool.gd
Normal file
156
addons/gut/diff_tool.gd
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
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
|
||||||
1
addons/gut/diff_tool.gd.uid
Normal file
1
addons/gut/diff_tool.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://beoxokvl1hjs8
|
||||||
9
addons/gut/double_templates/function_template.txt
Normal file
9
addons/gut/double_templates/function_template.txt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{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})
|
||||||
4
addons/gut/double_templates/init_template.txt
Normal file
4
addons/gut/double_templates/init_template.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{func_decleration}:
|
||||||
|
super({super_params})
|
||||||
|
__gutdbl.spy_on('{method_name}', {param_array})
|
||||||
|
|
||||||
37
addons/gut/double_templates/script_template.txt
Normal file
37
addons/gut/double_templates/script_template.txt
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# ##############################################################################
|
||||||
|
# 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
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
70
addons/gut/double_tools.gd
Normal file
70
addons/gut/double_tools.gd
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
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)
|
||||||
1
addons/gut/double_tools.gd.uid
Normal file
1
addons/gut/double_tools.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://tr4khoco1hef
|
||||||
312
addons/gut/doubler.gd
Normal file
312
addons/gut/doubler.gd
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
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.
|
||||||
|
#
|
||||||
|
# ##############################################################################
|
||||||
1
addons/gut/doubler.gd.uid
Normal file
1
addons/gut/doubler.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cpy013l0wqwmg
|
||||||
33
addons/gut/dynamic_gdscript.gd
Normal file
33
addons/gut/dynamic_gdscript.gd
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
@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
|
||||||
|
|
||||||
1
addons/gut/dynamic_gdscript.gd.uid
Normal file
1
addons/gut/dynamic_gdscript.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cnbjsrik0p5uf
|
||||||
207
addons/gut/editor_caret_context_notifier.gd
Normal file
207
addons/gut/editor_caret_context_notifier.gd
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
@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)
|
||||||
1
addons/gut/editor_caret_context_notifier.gd.uid
Normal file
1
addons/gut/editor_caret_context_notifier.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://c110s7a32x4su
|
||||||
193
addons/gut/error_tracker.gd
Normal file
193
addons/gut/error_tracker.gd
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
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
|
||||||
1
addons/gut/error_tracker.gd.uid
Normal file
1
addons/gut/error_tracker.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://35kxgqotjmlu
|
||||||
BIN
addons/gut/fonts/AnonymousPro-Bold.ttf
Normal file
BIN
addons/gut/fonts/AnonymousPro-Bold.ttf
Normal file
Binary file not shown.
36
addons/gut/fonts/AnonymousPro-Bold.ttf.import
Normal file
36
addons/gut/fonts/AnonymousPro-Bold.ttf.import
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
[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={}
|
||||||
BIN
addons/gut/fonts/AnonymousPro-BoldItalic.ttf
Normal file
BIN
addons/gut/fonts/AnonymousPro-BoldItalic.ttf
Normal file
Binary file not shown.
36
addons/gut/fonts/AnonymousPro-BoldItalic.ttf.import
Normal file
36
addons/gut/fonts/AnonymousPro-BoldItalic.ttf.import
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
[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={}
|
||||||
BIN
addons/gut/fonts/AnonymousPro-Italic.ttf
Normal file
BIN
addons/gut/fonts/AnonymousPro-Italic.ttf
Normal file
Binary file not shown.
36
addons/gut/fonts/AnonymousPro-Italic.ttf.import
Normal file
36
addons/gut/fonts/AnonymousPro-Italic.ttf.import
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
[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={}
|
||||||
BIN
addons/gut/fonts/AnonymousPro-Regular.ttf
Normal file
BIN
addons/gut/fonts/AnonymousPro-Regular.ttf
Normal file
Binary file not shown.
36
addons/gut/fonts/AnonymousPro-Regular.ttf.import
Normal file
36
addons/gut/fonts/AnonymousPro-Regular.ttf.import
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
[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={}
|
||||||
BIN
addons/gut/fonts/CourierPrime-Bold.ttf
Normal file
BIN
addons/gut/fonts/CourierPrime-Bold.ttf
Normal file
Binary file not shown.
36
addons/gut/fonts/CourierPrime-Bold.ttf.import
Normal file
36
addons/gut/fonts/CourierPrime-Bold.ttf.import
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
[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={}
|
||||||
BIN
addons/gut/fonts/CourierPrime-BoldItalic.ttf
Normal file
BIN
addons/gut/fonts/CourierPrime-BoldItalic.ttf
Normal file
Binary file not shown.
36
addons/gut/fonts/CourierPrime-BoldItalic.ttf.import
Normal file
36
addons/gut/fonts/CourierPrime-BoldItalic.ttf.import
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
[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={}
|
||||||
BIN
addons/gut/fonts/CourierPrime-Italic.ttf
Normal file
BIN
addons/gut/fonts/CourierPrime-Italic.ttf
Normal file
Binary file not shown.
36
addons/gut/fonts/CourierPrime-Italic.ttf.import
Normal file
36
addons/gut/fonts/CourierPrime-Italic.ttf.import
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
[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={}
|
||||||
BIN
addons/gut/fonts/CourierPrime-Regular.ttf
Normal file
BIN
addons/gut/fonts/CourierPrime-Regular.ttf
Normal file
Binary file not shown.
36
addons/gut/fonts/CourierPrime-Regular.ttf.import
Normal file
36
addons/gut/fonts/CourierPrime-Regular.ttf.import
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
[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={}
|
||||||
BIN
addons/gut/fonts/LobsterTwo-Bold.ttf
Normal file
BIN
addons/gut/fonts/LobsterTwo-Bold.ttf
Normal file
Binary file not shown.
36
addons/gut/fonts/LobsterTwo-Bold.ttf.import
Normal file
36
addons/gut/fonts/LobsterTwo-Bold.ttf.import
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
[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={}
|
||||||
BIN
addons/gut/fonts/LobsterTwo-BoldItalic.ttf
Normal file
BIN
addons/gut/fonts/LobsterTwo-BoldItalic.ttf
Normal file
Binary file not shown.
36
addons/gut/fonts/LobsterTwo-BoldItalic.ttf.import
Normal file
36
addons/gut/fonts/LobsterTwo-BoldItalic.ttf.import
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
[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={}
|
||||||
BIN
addons/gut/fonts/LobsterTwo-Italic.ttf
Normal file
BIN
addons/gut/fonts/LobsterTwo-Italic.ttf
Normal file
Binary file not shown.
36
addons/gut/fonts/LobsterTwo-Italic.ttf.import
Normal file
36
addons/gut/fonts/LobsterTwo-Italic.ttf.import
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
[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={}
|
||||||
BIN
addons/gut/fonts/LobsterTwo-Regular.ttf
Normal file
BIN
addons/gut/fonts/LobsterTwo-Regular.ttf
Normal file
Binary file not shown.
36
addons/gut/fonts/LobsterTwo-Regular.ttf.import
Normal file
36
addons/gut/fonts/LobsterTwo-Regular.ttf.import
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
[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={}
|
||||||
94
addons/gut/fonts/OFL.txt
Normal file
94
addons/gut/fonts/OFL.txt
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
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.
|
||||||
6
addons/gut/get_editor_interface.gd
Normal file
6
addons/gut/get_editor_interface.gd
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# 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
|
||||||
1
addons/gut/get_editor_interface.gd.uid
Normal file
1
addons/gut/get_editor_interface.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://fgn2xo217kg1
|
||||||
13
addons/gut/gui/EditorRadioButton.tres
Normal file
13
addons/gut/gui/EditorRadioButton.tres
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[gd_resource type="Theme" load_steps=3 format=3 uid="uid://dssgvu257o1si"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_u716c"]
|
||||||
|
bg_color = Color(0.43137255, 0.8784314, 0.6156863, 0.5254902)
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ht2pf"]
|
||||||
|
bg_color = Color(0, 0.44705883, 0.23921569, 1)
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
Button/colors/font_hover_pressed_color = Color(1, 1, 1, 1)
|
||||||
|
Button/colors/font_pressed_color = Color(1, 1, 1, 1)
|
||||||
|
Button/styles/hover = SubResource("StyleBoxFlat_u716c")
|
||||||
|
Button/styles/pressed = SubResource("StyleBoxFlat_ht2pf")
|
||||||
543
addons/gut/gui/GutBottomPanel.gd
Normal file
543
addons/gut/gui/GutBottomPanel.gd
Normal file
@@ -0,0 +1,543 @@
|
|||||||
|
@tool
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')
|
||||||
|
var GutConfigGui = load('res://addons/gut/gui/gut_config_gui.gd')
|
||||||
|
var AboutWindow = load("res://addons/gut/gui/about.tscn")
|
||||||
|
|
||||||
|
var _interface = null;
|
||||||
|
var _is_running = false :
|
||||||
|
set(val):
|
||||||
|
_is_running = val
|
||||||
|
_disable_run_buttons(_is_running)
|
||||||
|
|
||||||
|
var _gut_config = load('res://addons/gut/gut_config.gd').new()
|
||||||
|
var _gut_config_gui = null
|
||||||
|
var _gut_plugin = null
|
||||||
|
var _light_color = Color(0, 0, 0, .5) :
|
||||||
|
set(val):
|
||||||
|
_light_color = val
|
||||||
|
if(is_inside_tree()):
|
||||||
|
_ctrls.light.queue_redraw()
|
||||||
|
var _panel_button = null
|
||||||
|
var _user_prefs = null
|
||||||
|
var _shell_out_panel = null
|
||||||
|
|
||||||
|
|
||||||
|
var menu_manager = null :
|
||||||
|
set(val):
|
||||||
|
menu_manager = val
|
||||||
|
if(val != null):
|
||||||
|
_apply_shortcuts()
|
||||||
|
menu_manager.toggle_windowed.connect(_on_toggle_windowed)
|
||||||
|
menu_manager.about.connect(show_about)
|
||||||
|
menu_manager.run_all.connect(_run_all)
|
||||||
|
menu_manager.show_gut.connect(_on_show_gut)
|
||||||
|
|
||||||
|
|
||||||
|
@onready var _ctrls = {
|
||||||
|
about = %ExtraButtons/About,
|
||||||
|
light = %StatusIndicator,
|
||||||
|
output_button = %ExtraButtons/OutputBtn,
|
||||||
|
run_button = $layout/ControlBar/RunAll,
|
||||||
|
run_externally_dialog = $ShellOutOptions,
|
||||||
|
run_mode = %ExtraButtons/RunMode,
|
||||||
|
run_at_cursor = $layout/ControlBar/RunAtCursor,
|
||||||
|
run_results_button = %ExtraButtons/RunResultsBtn,
|
||||||
|
settings = $layout/RSplit/sc/Settings,
|
||||||
|
settings_button = %ExtraButtons/Settings,
|
||||||
|
shortcut_dialog = $ShortcutDialog,
|
||||||
|
shortcuts_button = %ExtraButtons/Shortcuts,
|
||||||
|
|
||||||
|
results = {
|
||||||
|
bar = $layout/ControlBar2,
|
||||||
|
errors = %errors_value,
|
||||||
|
failing = %failing_value,
|
||||||
|
orphans = %orphans_value,
|
||||||
|
passing = %passing_value,
|
||||||
|
pending = %pending_value,
|
||||||
|
warnings = %warnings_value,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@onready var results_v_split = %VSplitResults
|
||||||
|
@onready var results_h_split = %HSplitResults
|
||||||
|
@onready var results_tree = %RunResults
|
||||||
|
@onready var results_text = %OutputText
|
||||||
|
@onready var make_floating_btn = %MakeFloating
|
||||||
|
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
if(get_parent() is SubViewport):
|
||||||
|
return
|
||||||
|
|
||||||
|
GutEditorGlobals.create_temp_directory()
|
||||||
|
|
||||||
|
_user_prefs = GutEditorGlobals.user_prefs
|
||||||
|
_gut_config_gui = GutConfigGui.new(_ctrls.settings)
|
||||||
|
|
||||||
|
_ctrls.results.bar.connect('draw', _on_results_bar_draw.bind(_ctrls.results.bar))
|
||||||
|
hide_settings(!_ctrls.settings_button.button_pressed)
|
||||||
|
|
||||||
|
_gut_config.load_options(GutEditorGlobals.editor_run_gut_config_path)
|
||||||
|
_gut_config_gui.set_options(_gut_config.options)
|
||||||
|
|
||||||
|
_ctrls.shortcuts_button.icon = get_theme_icon('Shortcut', 'EditorIcons')
|
||||||
|
_ctrls.settings_button.icon = get_theme_icon('Tools', 'EditorIcons')
|
||||||
|
_ctrls.run_results_button.icon = get_theme_icon('AnimationTrackGroup', 'EditorIcons') # Tree
|
||||||
|
_ctrls.output_button.icon = get_theme_icon('Font', 'EditorIcons')
|
||||||
|
make_floating_btn.icon = get_theme_icon("MakeFloating", 'EditorIcons')
|
||||||
|
make_floating_btn.text = ''
|
||||||
|
_ctrls.about.icon = get_theme_icon('Info', 'EditorIcons')
|
||||||
|
_ctrls.about.text = ''
|
||||||
|
_ctrls.run_mode.icon = get_theme_icon("ViewportSpeed", 'EditorIcons')
|
||||||
|
|
||||||
|
results_tree.set_output_control(results_text)
|
||||||
|
|
||||||
|
var check_import = load('res://addons/gut/images/HSplitContainer.svg')
|
||||||
|
if(check_import == null):
|
||||||
|
results_tree.add_centered_text("GUT got some new images that are not imported yet. Please restart Godot.")
|
||||||
|
print('GUT got some new images that are not imported yet. Please restart Godot.')
|
||||||
|
else:
|
||||||
|
results_tree.add_centered_text("Let's run some tests!")
|
||||||
|
|
||||||
|
_ctrls.run_externally_dialog.load_from_file()
|
||||||
|
_apply_options_to_controls()
|
||||||
|
|
||||||
|
results_vert_layout()
|
||||||
|
|
||||||
|
|
||||||
|
func _process(_delta):
|
||||||
|
if(_is_running):
|
||||||
|
if(_ctrls.run_externally_dialog.should_run_externally()):
|
||||||
|
if(!is_instance_valid(_shell_out_panel)):
|
||||||
|
_is_running = false
|
||||||
|
show_me()
|
||||||
|
elif(!_interface.is_playing_scene()):
|
||||||
|
_is_running = false
|
||||||
|
results_text.add_text("\ndone")
|
||||||
|
load_result_output()
|
||||||
|
show_me()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------
|
||||||
|
# Private
|
||||||
|
# ---------------
|
||||||
|
func _apply_options_to_controls():
|
||||||
|
hide_settings(_user_prefs.hide_settings.value)
|
||||||
|
hide_result_tree(_user_prefs.hide_result_tree.value)
|
||||||
|
hide_output_text(_user_prefs.hide_output_text.value)
|
||||||
|
results_tree.set_show_orphans(!_gut_config.options.hide_orphans)
|
||||||
|
var shell_dialog_size = _user_prefs.run_externally_options_dialog_size.value
|
||||||
|
|
||||||
|
if(shell_dialog_size != Vector2i(-1, -1)):
|
||||||
|
_ctrls.run_externally_dialog.size = Vector2i(shell_dialog_size)
|
||||||
|
|
||||||
|
if(_user_prefs.shortcuts_dialog_size.value != Vector2i(-1, -1)):
|
||||||
|
_ctrls.shortcut_dialog.size = _user_prefs.shortcuts_dialog_size.value
|
||||||
|
|
||||||
|
var mode_ind = 'Ed'
|
||||||
|
if(_ctrls.run_externally_dialog.run_mode == _ctrls.run_externally_dialog.RUN_MODE_BLOCKING):
|
||||||
|
mode_ind = 'ExB'
|
||||||
|
elif(_ctrls.run_externally_dialog.run_mode == _ctrls.run_externally_dialog.RUN_MODE_NON_BLOCKING):
|
||||||
|
mode_ind = 'ExN'
|
||||||
|
_ctrls.run_mode.text = mode_ind
|
||||||
|
|
||||||
|
_ctrls.run_at_cursor.apply_gut_config(_gut_config)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func _disable_run_buttons(should):
|
||||||
|
_ctrls.run_button.disabled = should
|
||||||
|
_ctrls.run_at_cursor.disabled = should
|
||||||
|
|
||||||
|
|
||||||
|
func _is_test_script(script):
|
||||||
|
var from = script.get_base_script()
|
||||||
|
while(from and from.resource_path != 'res://addons/gut/test.gd'):
|
||||||
|
from = from.get_base_script()
|
||||||
|
|
||||||
|
return from != null
|
||||||
|
|
||||||
|
|
||||||
|
func _show_errors(errs):
|
||||||
|
results_text.clear()
|
||||||
|
var text = "Cannot run tests, you have a configuration error:\n"
|
||||||
|
for e in errs:
|
||||||
|
text += str('* ', e, "\n")
|
||||||
|
text += "Check your settings ----->"
|
||||||
|
results_text.add_text(text)
|
||||||
|
hide_output_text(false)
|
||||||
|
hide_settings(false)
|
||||||
|
|
||||||
|
|
||||||
|
func _save_user_prefs():
|
||||||
|
_user_prefs.hide_settings.value = !_ctrls.settings_button.button_pressed
|
||||||
|
_user_prefs.hide_result_tree.value = !_ctrls.run_results_button.button_pressed
|
||||||
|
_user_prefs.hide_output_text.value = !_ctrls.output_button.button_pressed
|
||||||
|
_user_prefs.shortcuts_dialog_size.value = _ctrls.shortcut_dialog.size
|
||||||
|
|
||||||
|
_user_prefs.run_externally.value = _ctrls.run_externally_dialog.run_mode != _ctrls.run_externally_dialog.RUN_MODE_EDITOR
|
||||||
|
_user_prefs.run_externally_options_dialog_size.value = _ctrls.run_externally_dialog.size
|
||||||
|
|
||||||
|
_user_prefs.save_it()
|
||||||
|
|
||||||
|
|
||||||
|
func _save_config():
|
||||||
|
_save_user_prefs()
|
||||||
|
|
||||||
|
_gut_config.options = _gut_config_gui.get_options(_gut_config.options)
|
||||||
|
var w_result = _gut_config.write_options(GutEditorGlobals.editor_run_gut_config_path)
|
||||||
|
if(w_result != OK):
|
||||||
|
push_error(str('Could not write options to ', GutEditorGlobals.editor_run_gut_config_path, ': ', w_result))
|
||||||
|
else:
|
||||||
|
_gut_config_gui.mark_saved()
|
||||||
|
|
||||||
|
|
||||||
|
func _run_externally():
|
||||||
|
_shell_out_panel = GutUtils.RunExternallyScene.instantiate()
|
||||||
|
_shell_out_panel.bottom_panel = self
|
||||||
|
_shell_out_panel.blocking_mode = _ctrls.run_externally_dialog.run_mode
|
||||||
|
_shell_out_panel.additional_arguments = _ctrls.run_externally_dialog.get_additional_arguments_array()
|
||||||
|
|
||||||
|
add_child(_shell_out_panel)
|
||||||
|
_shell_out_panel.run_tests()
|
||||||
|
|
||||||
|
|
||||||
|
func _run_tests():
|
||||||
|
show_me()
|
||||||
|
if(_is_running):
|
||||||
|
push_error("GUT: Cannot run tests, tests are already running.")
|
||||||
|
return
|
||||||
|
|
||||||
|
clear_results()
|
||||||
|
GutEditorGlobals.create_temp_directory()
|
||||||
|
_light_color = Color.BLUE
|
||||||
|
|
||||||
|
var issues = _gut_config_gui.get_config_issues()
|
||||||
|
if(issues.size() > 0):
|
||||||
|
_show_errors(issues)
|
||||||
|
return
|
||||||
|
|
||||||
|
write_file(GutEditorGlobals.editor_run_bbcode_results_path, 'Run in progress')
|
||||||
|
write_file(GutEditorGlobals.editor_run_json_results_path, '')
|
||||||
|
_save_config()
|
||||||
|
_apply_options_to_controls()
|
||||||
|
|
||||||
|
results_text.clear()
|
||||||
|
results_tree.clear()
|
||||||
|
results_tree.add_centered_text('Running...')
|
||||||
|
|
||||||
|
_is_running = true
|
||||||
|
results_text.add_text('Running...')
|
||||||
|
|
||||||
|
if(_ctrls.run_externally_dialog.should_run_externally()):
|
||||||
|
_gut_plugin.make_bottom_panel_item_visible(self)
|
||||||
|
_run_externally()
|
||||||
|
else:
|
||||||
|
_interface.play_custom_scene('res://addons/gut/gui/run_from_editor.tscn')
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_shortcuts():
|
||||||
|
if(menu_manager != null):
|
||||||
|
menu_manager.apply_gut_shortcuts(_ctrls.shortcut_dialog)
|
||||||
|
|
||||||
|
_ctrls.run_button.shortcut = \
|
||||||
|
_ctrls.shortcut_dialog.scbtn_run_all.get_shortcut()
|
||||||
|
_ctrls.run_at_cursor.get_script_button().shortcut = \
|
||||||
|
_ctrls.shortcut_dialog.scbtn_run_current_script.get_shortcut()
|
||||||
|
_ctrls.run_at_cursor.get_inner_button().shortcut = \
|
||||||
|
_ctrls.shortcut_dialog.scbtn_run_current_inner.get_shortcut()
|
||||||
|
_ctrls.run_at_cursor.get_test_button().shortcut = \
|
||||||
|
_ctrls.shortcut_dialog.scbtn_run_current_test.get_shortcut()
|
||||||
|
# Took this out because it seems to break using the shortcut when docked.
|
||||||
|
# Though it does allow the shortcut to work when windowed. Shortcuts
|
||||||
|
# are weird.
|
||||||
|
# make_floating_btn.shortcut = \
|
||||||
|
# _ctrls.shortcut_dialog.scbtn_windowed.get_shortcut()
|
||||||
|
|
||||||
|
|
||||||
|
if(_panel_button != null):
|
||||||
|
_panel_button.shortcut = _ctrls.shortcut_dialog.scbtn_panel.get_shortcut()
|
||||||
|
|
||||||
|
|
||||||
|
func _run_all():
|
||||||
|
_gut_config.options.selected = null
|
||||||
|
_gut_config.options.inner_class = null
|
||||||
|
_gut_config.options.unit_test_name = null
|
||||||
|
|
||||||
|
_run_tests()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------
|
||||||
|
# Events
|
||||||
|
# ---------------
|
||||||
|
func _on_results_bar_draw(bar):
|
||||||
|
bar.draw_rect(Rect2(Vector2(0, 0), bar.size), Color(0, 0, 0, .2))
|
||||||
|
|
||||||
|
|
||||||
|
func _on_Light_draw():
|
||||||
|
var l = _ctrls.light
|
||||||
|
l.draw_circle(Vector2(l.size.x / 2, l.size.y / 2), l.size.x / 2, _light_color)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_RunAll_pressed():
|
||||||
|
_run_all()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_Shortcuts_pressed():
|
||||||
|
_ctrls.shortcut_dialog.popup_centered()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_sortcut_dialog_confirmed() -> void:
|
||||||
|
_apply_shortcuts()
|
||||||
|
_ctrls.shortcut_dialog.save_shortcuts()
|
||||||
|
_save_user_prefs()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_RunAtCursor_run_tests(what):
|
||||||
|
_gut_config.options.selected = what.script
|
||||||
|
_gut_config.options.inner_class = what.inner_class
|
||||||
|
_gut_config.options.unit_test_name = what.method
|
||||||
|
|
||||||
|
_run_tests()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_Settings_pressed():
|
||||||
|
hide_settings(!_ctrls.settings_button.button_pressed)
|
||||||
|
_save_config()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_OutputBtn_pressed():
|
||||||
|
hide_output_text(!_ctrls.output_button.button_pressed)
|
||||||
|
_save_config()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_RunResultsBtn_pressed():
|
||||||
|
hide_result_tree(! _ctrls.run_results_button.button_pressed)
|
||||||
|
_save_config()
|
||||||
|
|
||||||
|
|
||||||
|
# Currently not used, but will be when I figure out how to put
|
||||||
|
# colors into the text results
|
||||||
|
func _on_UseColors_pressed():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
func _on_shell_out_options_confirmed() -> void:
|
||||||
|
_ctrls.run_externally_dialog.save_to_file()
|
||||||
|
_save_user_prefs()
|
||||||
|
_apply_options_to_controls()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_run_mode_pressed() -> void:
|
||||||
|
_ctrls.run_externally_dialog.popup_centered()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_toggle_windowed():
|
||||||
|
_gut_plugin.toggle_windowed()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_to_window_pressed() -> void:
|
||||||
|
_gut_plugin.toggle_windowed()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_show_gut() -> void:
|
||||||
|
show_hide()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_about_pressed() -> void:
|
||||||
|
show_about()
|
||||||
|
|
||||||
|
# ---------------
|
||||||
|
# Public
|
||||||
|
# ---------------
|
||||||
|
func load_shortcuts():
|
||||||
|
_ctrls.shortcut_dialog.load_shortcuts()
|
||||||
|
_apply_shortcuts()
|
||||||
|
|
||||||
|
|
||||||
|
func hide_result_tree(should):
|
||||||
|
results_tree.visible = !should
|
||||||
|
_ctrls.run_results_button.button_pressed = !should
|
||||||
|
|
||||||
|
|
||||||
|
func hide_settings(should):
|
||||||
|
var s_scroll = _ctrls.settings.get_parent()
|
||||||
|
s_scroll.visible = !should
|
||||||
|
|
||||||
|
# collapse only collapses the first control, so we move
|
||||||
|
# settings around to be the collapsed one
|
||||||
|
if(should):
|
||||||
|
s_scroll.get_parent().move_child(s_scroll, 0)
|
||||||
|
else:
|
||||||
|
s_scroll.get_parent().move_child(s_scroll, 1)
|
||||||
|
|
||||||
|
$layout/RSplit.collapsed = should
|
||||||
|
_ctrls.settings_button.button_pressed = !should
|
||||||
|
|
||||||
|
|
||||||
|
func hide_output_text(should):
|
||||||
|
results_text.visible = !should
|
||||||
|
_ctrls.output_button.button_pressed = !should
|
||||||
|
|
||||||
|
|
||||||
|
func clear_results():
|
||||||
|
_light_color = Color(0, 0, 0, .5)
|
||||||
|
|
||||||
|
_ctrls.results.passing.text = "0"
|
||||||
|
_ctrls.results.passing.get_parent().visible = false
|
||||||
|
|
||||||
|
_ctrls.results.failing.text = "0"
|
||||||
|
_ctrls.results.failing.get_parent().visible = false
|
||||||
|
|
||||||
|
_ctrls.results.pending.text = "0"
|
||||||
|
_ctrls.results.pending.get_parent().visible = false
|
||||||
|
|
||||||
|
_ctrls.results.errors.text = "0"
|
||||||
|
_ctrls.results.errors.get_parent().visible = false
|
||||||
|
|
||||||
|
_ctrls.results.warnings.text = "0"
|
||||||
|
_ctrls.results.warnings.get_parent().visible = false
|
||||||
|
|
||||||
|
_ctrls.results.orphans.text = "0"
|
||||||
|
_ctrls.results.orphans.get_parent().visible = false
|
||||||
|
|
||||||
|
|
||||||
|
func load_result_json():
|
||||||
|
var summary = get_file_as_text(GutEditorGlobals.editor_run_json_results_path)
|
||||||
|
var test_json_conv = JSON.new()
|
||||||
|
if (test_json_conv.parse(summary) != OK):
|
||||||
|
return
|
||||||
|
var results = test_json_conv.get_data()
|
||||||
|
|
||||||
|
results_tree.load_json_results(results)
|
||||||
|
|
||||||
|
var summary_json = results['test_scripts']['props']
|
||||||
|
_ctrls.results.passing.text = str(int(summary_json.passing))
|
||||||
|
_ctrls.results.passing.get_parent().visible = true
|
||||||
|
|
||||||
|
_ctrls.results.failing.text = str(int(summary_json.failures))
|
||||||
|
_ctrls.results.failing.get_parent().visible = true
|
||||||
|
|
||||||
|
_ctrls.results.pending.text = str(int(summary_json.pending) + int(summary_json.risky))
|
||||||
|
_ctrls.results.pending.get_parent().visible = _ctrls.results.pending.text != '0'
|
||||||
|
|
||||||
|
_ctrls.results.errors.text = str(int(summary_json.errors))
|
||||||
|
_ctrls.results.errors.get_parent().visible = _ctrls.results.errors.text != '0'
|
||||||
|
|
||||||
|
_ctrls.results.warnings.text = str(int(summary_json.warnings))
|
||||||
|
_ctrls.results.warnings.get_parent().visible = _ctrls.results.warnings.text != '0'
|
||||||
|
|
||||||
|
_ctrls.results.orphans.text = str(int(summary_json.orphans))
|
||||||
|
_ctrls.results.orphans.get_parent().visible = _ctrls.results.orphans.text != '0' and !_gut_config.options.hide_orphans
|
||||||
|
|
||||||
|
if(summary_json.tests == 0):
|
||||||
|
_light_color = Color(1, 0, 0, .75)
|
||||||
|
elif(summary_json.failures != 0):
|
||||||
|
_light_color = Color(1, 0, 0, .75)
|
||||||
|
elif(summary_json.pending != 0 or summary_json.risky != 0):
|
||||||
|
_light_color = Color(1, 1, 0, .75)
|
||||||
|
else:
|
||||||
|
_light_color = Color(0, 1, 0, .75)
|
||||||
|
|
||||||
|
_ctrls.light.visible = true
|
||||||
|
|
||||||
|
|
||||||
|
func load_result_text():
|
||||||
|
results_text.load_file(GutEditorGlobals.editor_run_bbcode_results_path)
|
||||||
|
|
||||||
|
|
||||||
|
func load_result_output():
|
||||||
|
load_result_text()
|
||||||
|
load_result_json()
|
||||||
|
|
||||||
|
|
||||||
|
func set_interface(value):
|
||||||
|
_interface = value
|
||||||
|
results_tree.set_interface(_interface)
|
||||||
|
|
||||||
|
|
||||||
|
func set_plugin(value):
|
||||||
|
_gut_plugin = value
|
||||||
|
|
||||||
|
|
||||||
|
func set_panel_button(value):
|
||||||
|
_panel_button = value
|
||||||
|
|
||||||
|
|
||||||
|
func write_file(path, content):
|
||||||
|
var f = FileAccess.open(path, FileAccess.WRITE)
|
||||||
|
if(f != null):
|
||||||
|
f.store_string(content)
|
||||||
|
f = null;
|
||||||
|
|
||||||
|
return FileAccess.get_open_error()
|
||||||
|
|
||||||
|
|
||||||
|
func get_file_as_text(path):
|
||||||
|
var to_return = ''
|
||||||
|
var f = FileAccess.open(path, FileAccess.READ)
|
||||||
|
if(f != null):
|
||||||
|
to_return = f.get_as_text()
|
||||||
|
f = null
|
||||||
|
return to_return
|
||||||
|
|
||||||
|
|
||||||
|
func get_text_output_control():
|
||||||
|
return results_text
|
||||||
|
|
||||||
|
|
||||||
|
func add_output_text(text):
|
||||||
|
results_text.add_text(text)
|
||||||
|
|
||||||
|
|
||||||
|
func show_about():
|
||||||
|
var about = AboutWindow.instantiate()
|
||||||
|
add_child(about)
|
||||||
|
about.popup_centered()
|
||||||
|
about.confirmed.connect(about.queue_free)
|
||||||
|
|
||||||
|
|
||||||
|
func show_me():
|
||||||
|
if(owner is Window):
|
||||||
|
owner.grab_focus()
|
||||||
|
else:
|
||||||
|
_gut_plugin.make_bottom_panel_item_visible(self)
|
||||||
|
|
||||||
|
|
||||||
|
func show_hide():
|
||||||
|
if(owner is Window):
|
||||||
|
if(owner.has_focus()):
|
||||||
|
var win_to_focus_on = EditorInterface.get_editor_main_screen().get_parent()
|
||||||
|
while(win_to_focus_on != null and win_to_focus_on is not Window):
|
||||||
|
win_to_focus_on = win_to_focus_on.get_parent()
|
||||||
|
if(win_to_focus_on != null):
|
||||||
|
win_to_focus_on.grab_focus()
|
||||||
|
else:
|
||||||
|
owner.grab_focus()
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
# We don't have to do anything when we are docked because the GUT
|
||||||
|
# bottom panel has the shortcut and it does the toggling all on its
|
||||||
|
# own.
|
||||||
|
|
||||||
|
|
||||||
|
func get_shortcut_dialog():
|
||||||
|
return _ctrls.shortcut_dialog
|
||||||
|
|
||||||
|
|
||||||
|
func results_vert_layout():
|
||||||
|
if(results_tree.get_parent() != results_v_split):
|
||||||
|
results_tree.reparent(results_v_split)
|
||||||
|
results_text.reparent(results_v_split)
|
||||||
|
results_v_split.visible = true
|
||||||
|
results_h_split.visible = false
|
||||||
|
|
||||||
|
|
||||||
|
func results_horiz_layout():
|
||||||
|
if(results_tree.get_parent() != results_h_split):
|
||||||
|
results_tree.reparent(results_h_split)
|
||||||
|
results_text.reparent(results_h_split)
|
||||||
|
results_v_split.visible = false
|
||||||
|
results_h_split.visible = true
|
||||||
1
addons/gut/gui/GutBottomPanel.gd.uid
Normal file
1
addons/gut/gui/GutBottomPanel.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dtvnb0xatk0my
|
||||||
295
addons/gut/gui/GutBottomPanel.tscn
Normal file
295
addons/gut/gui/GutBottomPanel.tscn
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
[gd_scene load_steps=10 format=3 uid="uid://b3bostcslstem"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://dtvnb0xatk0my" path="res://addons/gut/gui/GutBottomPanel.gd" id="1"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://0yunjxtaa8iw" path="res://addons/gut/gui/RunAtCursor.tscn" id="3"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://cr6tvdv0ve6cv" path="res://addons/gut/gui/play.png" id="4"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://bvo0uao7deu0q" path="res://addons/gut/icon.png" id="4_xv2r3"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://4gyyn12um08h" path="res://addons/gut/gui/RunResults.tscn" id="5"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://bqmo4dj64c7yl" path="res://addons/gut/gui/OutputText.tscn" id="6"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://dj5ve0bq7xa5j" path="res://addons/gut/gui/ShortcutDialog.tscn" id="7_srqj5"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://ckv5eh8xyrwbk" path="res://addons/gut/gui/ShellOutOptions.tscn" id="7_xv2r3"]
|
||||||
|
|
||||||
|
[sub_resource type="Shortcut" id="9"]
|
||||||
|
|
||||||
|
[node name="GutBottomPanel" type="Control"]
|
||||||
|
custom_minimum_size = Vector2(250, 250)
|
||||||
|
layout_mode = 3
|
||||||
|
anchor_left = -0.0025866
|
||||||
|
anchor_top = -0.00176575
|
||||||
|
anchor_right = 0.997413
|
||||||
|
anchor_bottom = 0.998234
|
||||||
|
offset_left = 2.64868
|
||||||
|
offset_top = 1.05945
|
||||||
|
offset_right = 2.64862
|
||||||
|
offset_bottom = 1.05945
|
||||||
|
script = ExtResource("1")
|
||||||
|
|
||||||
|
[node name="layout" type="VBoxContainer" parent="."]
|
||||||
|
layout_mode = 0
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
|
||||||
|
[node name="ControlBar" type="HBoxContainer" parent="layout"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="RunAll" type="Button" parent="layout/ControlBar"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 11
|
||||||
|
shortcut = SubResource("9")
|
||||||
|
text = "Run All"
|
||||||
|
icon = ExtResource("4")
|
||||||
|
|
||||||
|
[node name="Sep3" type="ColorRect" parent="layout/ControlBar"]
|
||||||
|
custom_minimum_size = Vector2(1, 2.08165e-12)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="RunAtCursor" parent="layout/ControlBar" instance=ExtResource("3")]
|
||||||
|
custom_minimum_size = Vector2(648, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="CenterContainer2" type="CenterContainer" parent="layout/ControlBar"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="MakeFloating" type="Button" parent="layout/ControlBar"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "Move the GUT panel to a window."
|
||||||
|
icon = ExtResource("4_xv2r3")
|
||||||
|
flat = true
|
||||||
|
|
||||||
|
[node name="ControlBar2" type="HBoxContainer" parent="layout"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Sep2" type="ColorRect" parent="layout/ControlBar2"]
|
||||||
|
custom_minimum_size = Vector2(1, 2.08165e-12)
|
||||||
|
layout_mode = 2
|
||||||
|
color = Color(1, 1, 1, 0)
|
||||||
|
|
||||||
|
[node name="StatusIndicator" type="Control" parent="layout/ControlBar2"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(30, 30)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Passing" type="HBoxContainer" parent="layout/ControlBar2"]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Sep" type="ColorRect" parent="layout/ControlBar2/Passing"]
|
||||||
|
custom_minimum_size = Vector2(1, 2.08165e-12)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="label" type="Label" parent="layout/ControlBar2/Passing"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Pass"
|
||||||
|
|
||||||
|
[node name="passing_value" type="Label" parent="layout/ControlBar2/Passing"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "---"
|
||||||
|
|
||||||
|
[node name="Failing" type="HBoxContainer" parent="layout/ControlBar2"]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Sep" type="ColorRect" parent="layout/ControlBar2/Failing"]
|
||||||
|
custom_minimum_size = Vector2(1, 2.08165e-12)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="label" type="Label" parent="layout/ControlBar2/Failing"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Fail"
|
||||||
|
|
||||||
|
[node name="failing_value" type="Label" parent="layout/ControlBar2/Failing"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "---"
|
||||||
|
|
||||||
|
[node name="Pending" type="HBoxContainer" parent="layout/ControlBar2"]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Sep" type="ColorRect" parent="layout/ControlBar2/Pending"]
|
||||||
|
custom_minimum_size = Vector2(1, 2.08165e-12)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="label" type="Label" parent="layout/ControlBar2/Pending"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Risky"
|
||||||
|
|
||||||
|
[node name="pending_value" type="Label" parent="layout/ControlBar2/Pending"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "---"
|
||||||
|
|
||||||
|
[node name="Orphans" type="HBoxContainer" parent="layout/ControlBar2"]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Sep" type="ColorRect" parent="layout/ControlBar2/Orphans"]
|
||||||
|
custom_minimum_size = Vector2(1, 2.08165e-12)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="label" type="Label" parent="layout/ControlBar2/Orphans"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Orphans"
|
||||||
|
|
||||||
|
[node name="orphans_value" type="Label" parent="layout/ControlBar2/Orphans"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "---"
|
||||||
|
|
||||||
|
[node name="Errors" type="HBoxContainer" parent="layout/ControlBar2"]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Sep" type="ColorRect" parent="layout/ControlBar2/Errors"]
|
||||||
|
custom_minimum_size = Vector2(1, 2.08165e-12)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="label" type="Label" parent="layout/ControlBar2/Errors"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Errors"
|
||||||
|
|
||||||
|
[node name="errors_value" type="Label" parent="layout/ControlBar2/Errors"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "---"
|
||||||
|
|
||||||
|
[node name="Warnings" type="HBoxContainer" parent="layout/ControlBar2"]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Sep" type="ColorRect" parent="layout/ControlBar2/Warnings"]
|
||||||
|
custom_minimum_size = Vector2(1, 2.08165e-12)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="label" type="Label" parent="layout/ControlBar2/Warnings"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Warnings"
|
||||||
|
|
||||||
|
[node name="warnings_value" type="Label" parent="layout/ControlBar2/Warnings"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
text = "---"
|
||||||
|
|
||||||
|
[node name="CenterContainer" type="CenterContainer" parent="layout/ControlBar2"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="ExtraButtons" type="HBoxContainer" parent="layout/ControlBar2"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Sep1" type="ColorRect" parent="layout/ControlBar2/ExtraButtons"]
|
||||||
|
custom_minimum_size = Vector2(1, 2.08165e-12)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="RunMode" type="Button" parent="layout/ControlBar2/ExtraButtons"]
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "Run Mode. Run tests through the editor or externally in a different process."
|
||||||
|
text = "ExN"
|
||||||
|
icon = ExtResource("4_xv2r3")
|
||||||
|
|
||||||
|
[node name="Sep2" type="ColorRect" parent="layout/ControlBar2/ExtraButtons"]
|
||||||
|
custom_minimum_size = Vector2(1, 2.08165e-12)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="RunResultsBtn" type="Button" parent="layout/ControlBar2/ExtraButtons"]
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "Show/Hide Result Tree"
|
||||||
|
toggle_mode = true
|
||||||
|
button_pressed = true
|
||||||
|
icon = ExtResource("4_xv2r3")
|
||||||
|
|
||||||
|
[node name="OutputBtn" type="Button" parent="layout/ControlBar2/ExtraButtons"]
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "Show/Hide Text Output"
|
||||||
|
toggle_mode = true
|
||||||
|
button_pressed = true
|
||||||
|
icon = ExtResource("4_xv2r3")
|
||||||
|
|
||||||
|
[node name="Settings" type="Button" parent="layout/ControlBar2/ExtraButtons"]
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "GUT Settings"
|
||||||
|
toggle_mode = true
|
||||||
|
button_pressed = true
|
||||||
|
icon = ExtResource("4_xv2r3")
|
||||||
|
|
||||||
|
[node name="Sep3" type="ColorRect" parent="layout/ControlBar2/ExtraButtons"]
|
||||||
|
custom_minimum_size = Vector2(1, 2.08165e-12)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Shortcuts" type="Button" parent="layout/ControlBar2/ExtraButtons"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 11
|
||||||
|
tooltip_text = "GUT Shortcuts"
|
||||||
|
icon = ExtResource("4_xv2r3")
|
||||||
|
|
||||||
|
[node name="About" type="Button" parent="layout/ControlBar2/ExtraButtons"]
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "About"
|
||||||
|
text = "A"
|
||||||
|
|
||||||
|
[node name="RSplit" type="HSplitContainer" parent="layout"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="CResults" type="VBoxContainer" parent="layout/RSplit"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="HSplitResults" type="HSplitContainer" parent="layout/RSplit/CResults"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="RunResults" parent="layout/RSplit/CResults/HSplitResults" instance=ExtResource("5")]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(255, 255)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="OutputText" parent="layout/RSplit/CResults/HSplitResults" instance=ExtResource("6")]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="VSplitResults" type="VSplitContainer" parent="layout/RSplit/CResults"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="sc" type="ScrollContainer" parent="layout/RSplit"]
|
||||||
|
custom_minimum_size = Vector2(500, 2.08165e-12)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="Settings" type="VBoxContainer" parent="layout/RSplit/sc"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="ShortcutDialog" parent="." instance=ExtResource("7_srqj5")]
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="ShellOutOptions" parent="." instance=ExtResource("7_xv2r3")]
|
||||||
|
size = Vector2i(1300, 1336)
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[connection signal="pressed" from="layout/ControlBar/RunAll" to="." method="_on_RunAll_pressed"]
|
||||||
|
[connection signal="run_tests" from="layout/ControlBar/RunAtCursor" to="." method="_on_RunAtCursor_run_tests"]
|
||||||
|
[connection signal="pressed" from="layout/ControlBar/MakeFloating" to="." method="_on_to_window_pressed"]
|
||||||
|
[connection signal="draw" from="layout/ControlBar2/StatusIndicator" to="." method="_on_Light_draw"]
|
||||||
|
[connection signal="pressed" from="layout/ControlBar2/ExtraButtons/RunMode" to="." method="_on_run_mode_pressed"]
|
||||||
|
[connection signal="pressed" from="layout/ControlBar2/ExtraButtons/RunResultsBtn" to="." method="_on_RunResultsBtn_pressed"]
|
||||||
|
[connection signal="pressed" from="layout/ControlBar2/ExtraButtons/OutputBtn" to="." method="_on_OutputBtn_pressed"]
|
||||||
|
[connection signal="pressed" from="layout/ControlBar2/ExtraButtons/Settings" to="." method="_on_Settings_pressed"]
|
||||||
|
[connection signal="pressed" from="layout/ControlBar2/ExtraButtons/Shortcuts" to="." method="_on_Shortcuts_pressed"]
|
||||||
|
[connection signal="pressed" from="layout/ControlBar2/ExtraButtons/About" to="." method="_on_about_pressed"]
|
||||||
|
[connection signal="confirmed" from="ShortcutDialog" to="." method="_on_sortcut_dialog_confirmed"]
|
||||||
|
[connection signal="confirmed" from="ShellOutOptions" to="." method="_on_shell_out_options_confirmed"]
|
||||||
326
addons/gut/gui/GutControl.gd
Normal file
326
addons/gut/gui/GutControl.gd
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
@tool
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
const RUNNER_JSON_PATH = 'res://.gut_editor_config.json'
|
||||||
|
|
||||||
|
var GutConfig = load('res://addons/gut/gut_config.gd')
|
||||||
|
var GutRunnerScene = load('res://addons/gut/gui/GutRunner.tscn')
|
||||||
|
var GutConfigGui = load('res://addons/gut/gui/gut_config_gui.gd')
|
||||||
|
|
||||||
|
var _config = GutConfig.new()
|
||||||
|
var _config_gui = null
|
||||||
|
var _gut_runner = null
|
||||||
|
var _tree_root : TreeItem = null
|
||||||
|
|
||||||
|
var _script_icon = load('res://addons/gut/images/Script.svg')
|
||||||
|
var _folder_icon = load('res://addons/gut/images/Folder.svg')
|
||||||
|
|
||||||
|
var _tree_scripts = {}
|
||||||
|
var _tree_directories = {}
|
||||||
|
|
||||||
|
const TREE_SCRIPT = 'Script'
|
||||||
|
const TREE_DIR = 'Directory'
|
||||||
|
|
||||||
|
@onready var _ctrls = {
|
||||||
|
run_tests_button = $VBox/Buttons/RunTests,
|
||||||
|
run_selected = $VBox/Buttons/RunSelected,
|
||||||
|
test_tree = $VBox/Tabs/Tests,
|
||||||
|
settings_vbox = $VBox/Tabs/SettingsScroll/Settings,
|
||||||
|
tabs = $VBox/Tabs,
|
||||||
|
bg = $Bg
|
||||||
|
}
|
||||||
|
|
||||||
|
@export var bg_color : Color = Color(.36, .36, .36) :
|
||||||
|
get: return bg_color
|
||||||
|
set(val):
|
||||||
|
bg_color = val
|
||||||
|
if(is_inside_tree()):
|
||||||
|
$Bg.color = bg_color
|
||||||
|
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
if Engine.is_editor_hint():
|
||||||
|
return
|
||||||
|
|
||||||
|
_gut_runner = GutRunnerScene.instantiate()
|
||||||
|
$Bg.color = bg_color
|
||||||
|
_ctrls.tabs.set_tab_title(0, 'Tests')
|
||||||
|
_ctrls.tabs.set_tab_title(1, 'Settings')
|
||||||
|
|
||||||
|
_config_gui = GutConfigGui.new(_ctrls.settings_vbox)
|
||||||
|
|
||||||
|
_ctrls.test_tree.hide_root = true
|
||||||
|
add_child(_gut_runner)
|
||||||
|
|
||||||
|
# TODO This might not need to be called deferred after changing GutUtils to
|
||||||
|
# an all static class.
|
||||||
|
call_deferred('_post_ready')
|
||||||
|
|
||||||
|
|
||||||
|
func _draw():
|
||||||
|
if Engine.is_editor_hint():
|
||||||
|
return
|
||||||
|
|
||||||
|
var gut = _gut_runner.get_gut()
|
||||||
|
if(!gut.is_running()):
|
||||||
|
var r = Rect2(Vector2(0, 0), get_rect().size)
|
||||||
|
draw_rect(r, Color.BLACK, false, 2)
|
||||||
|
|
||||||
|
|
||||||
|
func _post_ready():
|
||||||
|
var gut = _gut_runner.get_gut()
|
||||||
|
gut.start_run.connect(_on_gut_run_started)
|
||||||
|
gut.end_run.connect(_on_gut_run_ended)
|
||||||
|
_refresh_tree_and_settings()
|
||||||
|
|
||||||
|
|
||||||
|
func _set_meta_for_script_tree_item(item, script, test=null):
|
||||||
|
var meta = {
|
||||||
|
type = TREE_SCRIPT,
|
||||||
|
script = script.path,
|
||||||
|
inner_class = script.inner_class_name,
|
||||||
|
test = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if(test != null):
|
||||||
|
meta.test = test.name
|
||||||
|
|
||||||
|
item.set_metadata(0, meta)
|
||||||
|
|
||||||
|
|
||||||
|
func _set_meta_for_directory_tree_item(item, path, temp_item):
|
||||||
|
var meta = {
|
||||||
|
type = TREE_DIR,
|
||||||
|
path = path,
|
||||||
|
temp_item = temp_item
|
||||||
|
}
|
||||||
|
item.set_metadata(0, meta)
|
||||||
|
|
||||||
|
|
||||||
|
func _get_script_tree_item(script, parent_item):
|
||||||
|
if(!_tree_scripts.has(script.path)):
|
||||||
|
var item = _ctrls.test_tree.create_item(parent_item)
|
||||||
|
item.set_text(0, script.path.get_file())
|
||||||
|
item.set_icon(0, _script_icon)
|
||||||
|
_tree_scripts[script.path] = item
|
||||||
|
_set_meta_for_script_tree_item(item, script)
|
||||||
|
|
||||||
|
return _tree_scripts[script.path]
|
||||||
|
|
||||||
|
|
||||||
|
func _get_directory_tree_item(path):
|
||||||
|
var parent = _tree_root
|
||||||
|
if(!_tree_directories.has(path)):
|
||||||
|
|
||||||
|
var item : TreeItem = null
|
||||||
|
if(parent != _tree_root):
|
||||||
|
item = parent.create_child(0)
|
||||||
|
else:
|
||||||
|
item = parent.create_child()
|
||||||
|
|
||||||
|
_tree_directories[path] = item
|
||||||
|
item.collapsed = false
|
||||||
|
item.set_text(0, path)
|
||||||
|
item.set_icon(0, _folder_icon)
|
||||||
|
item.set_icon_modulate(0, Color.ROYAL_BLUE)
|
||||||
|
# temp_item is used in calls with move_before since you must use
|
||||||
|
# move_before or move_after to reparent tree items. This ensures that
|
||||||
|
# there is an item on all directories. These are deleted later.
|
||||||
|
var temp_item = item.create_child()
|
||||||
|
temp_item.set_text(0, '<temp>')
|
||||||
|
|
||||||
|
_set_meta_for_directory_tree_item(item, path, temp_item)
|
||||||
|
|
||||||
|
return _tree_directories[path]
|
||||||
|
|
||||||
|
|
||||||
|
func _find_dir_item_to_move_before(path):
|
||||||
|
var max_matching_len = 0
|
||||||
|
var best_parent = null
|
||||||
|
|
||||||
|
# Go through all the directory items finding the one that has the longest
|
||||||
|
# path that contains our path.
|
||||||
|
for key in _tree_directories.keys():
|
||||||
|
if(path != key and path.begins_with(key) and key.length() > max_matching_len):
|
||||||
|
max_matching_len = key.length()
|
||||||
|
best_parent = _tree_directories[key]
|
||||||
|
|
||||||
|
var to_return = null
|
||||||
|
if(best_parent != null):
|
||||||
|
to_return = best_parent.get_metadata(0).temp_item
|
||||||
|
return to_return
|
||||||
|
|
||||||
|
|
||||||
|
func _reorder_dir_items():
|
||||||
|
var the_keys = _tree_directories.keys()
|
||||||
|
the_keys.sort()
|
||||||
|
for key in _tree_directories.keys():
|
||||||
|
var to_move = _tree_directories[key]
|
||||||
|
to_move.collapsed = false
|
||||||
|
var move_before = _find_dir_item_to_move_before(key)
|
||||||
|
if(move_before != null):
|
||||||
|
to_move.move_before(move_before)
|
||||||
|
var new_text = key.substr(move_before.get_parent().get_metadata(0).path.length())
|
||||||
|
to_move.set_text(0, new_text)
|
||||||
|
|
||||||
|
|
||||||
|
func _remove_dir_temp_items():
|
||||||
|
for key in _tree_directories.keys():
|
||||||
|
var item = _tree_directories[key].get_metadata(0).temp_item
|
||||||
|
_tree_directories[key].remove_child(item)
|
||||||
|
|
||||||
|
|
||||||
|
func _add_dir_and_script_tree_items():
|
||||||
|
var tree : Tree = _ctrls.test_tree
|
||||||
|
tree.clear()
|
||||||
|
_tree_root = _ctrls.test_tree.create_item()
|
||||||
|
|
||||||
|
var scripts = _gut_runner.get_gut().get_test_collector().scripts
|
||||||
|
for script in scripts:
|
||||||
|
var dir_item = _get_directory_tree_item(script.path.get_base_dir())
|
||||||
|
var item = _get_script_tree_item(script, dir_item)
|
||||||
|
|
||||||
|
if(script.inner_class_name != ''):
|
||||||
|
var inner_item = tree.create_item(item)
|
||||||
|
inner_item.set_text(0, script.inner_class_name)
|
||||||
|
_set_meta_for_script_tree_item(inner_item, script)
|
||||||
|
item = inner_item
|
||||||
|
|
||||||
|
for test in script.tests:
|
||||||
|
var test_item = tree.create_item(item)
|
||||||
|
test_item.set_text(0, test.name)
|
||||||
|
_set_meta_for_script_tree_item(test_item, script, test)
|
||||||
|
|
||||||
|
|
||||||
|
func _populate_tree():
|
||||||
|
_add_dir_and_script_tree_items()
|
||||||
|
_tree_root.set_collapsed_recursive(true)
|
||||||
|
_tree_root.set_collapsed(false)
|
||||||
|
_reorder_dir_items()
|
||||||
|
_remove_dir_temp_items()
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_tree_and_settings():
|
||||||
|
_config.apply_options(_gut_runner.get_gut())
|
||||||
|
_gut_runner.set_gut_config(_config)
|
||||||
|
_populate_tree()
|
||||||
|
|
||||||
|
# ---------------------------
|
||||||
|
# Events
|
||||||
|
# ---------------------------
|
||||||
|
func _on_gut_run_started():
|
||||||
|
_ctrls.run_tests_button.disabled = true
|
||||||
|
_ctrls.run_selected.visible = false
|
||||||
|
_ctrls.tabs.visible = false
|
||||||
|
_ctrls.bg.visible = false
|
||||||
|
_ctrls.run_tests_button.text = 'Running'
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_gut_run_ended():
|
||||||
|
_ctrls.run_tests_button.disabled = false
|
||||||
|
_ctrls.run_selected.visible = true
|
||||||
|
_ctrls.tabs.visible = true
|
||||||
|
_ctrls.bg.visible = true
|
||||||
|
_ctrls.run_tests_button.text = 'Run All'
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_run_tests_pressed():
|
||||||
|
run_all()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_run_selected_pressed():
|
||||||
|
run_selected()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_tests_item_activated():
|
||||||
|
run_selected()
|
||||||
|
|
||||||
|
# ---------------------------
|
||||||
|
# Public
|
||||||
|
# ---------------------------
|
||||||
|
func get_gut():
|
||||||
|
return _gut_runner.get_gut()
|
||||||
|
|
||||||
|
|
||||||
|
func get_config():
|
||||||
|
return _config
|
||||||
|
|
||||||
|
|
||||||
|
func run_all():
|
||||||
|
_config.options.selected = ''
|
||||||
|
_config.options.inner_class_name = ''
|
||||||
|
_config.options.unit_test_name = ''
|
||||||
|
run_tests()
|
||||||
|
|
||||||
|
|
||||||
|
func run_tests(options = null):
|
||||||
|
if(options == null):
|
||||||
|
_config.options = _config_gui.get_options(_config.options)
|
||||||
|
else:
|
||||||
|
_config.options = options
|
||||||
|
|
||||||
|
# We ar running from within the game, so we should not exit, ever.
|
||||||
|
_config.options.should_exit_on_success = false
|
||||||
|
_config.options.should_exit = false
|
||||||
|
|
||||||
|
_gut_runner.get_gut().get_test_collector().clear()
|
||||||
|
_gut_runner.set_gut_config(_config)
|
||||||
|
_gut_runner.run_tests()
|
||||||
|
|
||||||
|
|
||||||
|
func run_selected():
|
||||||
|
var sel_item = _ctrls.test_tree.get_selected()
|
||||||
|
if(sel_item == null):
|
||||||
|
return
|
||||||
|
|
||||||
|
var options = _config_gui.get_options(_config.options)
|
||||||
|
var meta = sel_item.get_metadata(0)
|
||||||
|
if(meta.type == TREE_SCRIPT):
|
||||||
|
options.selected = meta.script.get_file()
|
||||||
|
options.inner_class_name = meta.inner_class
|
||||||
|
options.unit_test_name = meta.test
|
||||||
|
elif(meta.type == TREE_DIR):
|
||||||
|
options.dirs = [meta.path]
|
||||||
|
options.include_subdirectories = true
|
||||||
|
options.selected = ''
|
||||||
|
options.inner_class_name = ''
|
||||||
|
options.unit_test_name = ''
|
||||||
|
|
||||||
|
run_tests(options)
|
||||||
|
|
||||||
|
|
||||||
|
func load_config_file(path):
|
||||||
|
_config.load_options(path)
|
||||||
|
_config.options.selected = ''
|
||||||
|
_config.options.inner_class_name = ''
|
||||||
|
_config.options.unit_test_name = ''
|
||||||
|
_config_gui.load_file(path)
|
||||||
|
|
||||||
|
|
||||||
|
# ##############################################################################
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
# ##############################################################################
|
||||||
1
addons/gut/gui/GutControl.gd.uid
Normal file
1
addons/gut/gui/GutControl.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cqlvpwidawld6
|
||||||
63
addons/gut/gui/GutControl.tscn
Normal file
63
addons/gut/gui/GutControl.tscn
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
[gd_scene load_steps=2 format=3 uid="uid://4jb53yqktyfg"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://cqlvpwidawld6" path="res://addons/gut/gui/GutControl.gd" id="1_eprql"]
|
||||||
|
|
||||||
|
[node name="GutControl" type="Control"]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 0
|
||||||
|
offset_right = 295.0
|
||||||
|
offset_bottom = 419.0
|
||||||
|
script = ExtResource("1_eprql")
|
||||||
|
|
||||||
|
[node name="Bg" type="ColorRect" parent="."]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
color = Color(0.36, 0.36, 0.36, 1)
|
||||||
|
|
||||||
|
[node name="VBox" type="VBoxContainer" parent="."]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
|
||||||
|
[node name="Tabs" type="TabContainer" parent="VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="Tests" type="Tree" parent="VBox/Tabs"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
hide_root = true
|
||||||
|
|
||||||
|
[node name="SettingsScroll" type="ScrollContainer" parent="VBox/Tabs"]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="Settings" type="VBoxContainer" parent="VBox/Tabs/SettingsScroll"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="Buttons" type="HBoxContainer" parent="VBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="RunTests" type="Button" parent="VBox/Buttons"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
text = "Run All"
|
||||||
|
|
||||||
|
[node name="RunSelected" type="Button" parent="VBox/Buttons"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
text = "Run Selected"
|
||||||
|
|
||||||
|
[connection signal="item_activated" from="VBox/Tabs/Tests" to="." method="_on_tests_item_activated"]
|
||||||
|
[connection signal="pressed" from="VBox/Buttons/RunTests" to="." method="_on_run_tests_pressed"]
|
||||||
|
[connection signal="pressed" from="VBox/Buttons/RunSelected" to="." method="_on_run_selected_pressed"]
|
||||||
88
addons/gut/gui/GutEditorWindow.gd
Normal file
88
addons/gut/gui/GutEditorWindow.gd
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
@tool
|
||||||
|
extends Window
|
||||||
|
|
||||||
|
|
||||||
|
var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')
|
||||||
|
|
||||||
|
@onready var _chk_always_on_top = $Layout/WinControls/OnTop
|
||||||
|
|
||||||
|
var _bottom_panel = null
|
||||||
|
var _ready_to_go = false
|
||||||
|
var _gut_shortcuts = []
|
||||||
|
|
||||||
|
var gut_plugin = null
|
||||||
|
var interface = null
|
||||||
|
|
||||||
|
|
||||||
|
func _unhandled_key_input(event: InputEvent) -> void:
|
||||||
|
if(event is InputEventKey):
|
||||||
|
if(_gut_shortcuts.has(event.as_text_keycode())):
|
||||||
|
get_tree().root.push_input(event)
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
var pref_size = GutEditorGlobals.user_prefs.gut_window_size.value
|
||||||
|
if(pref_size.x < 0):
|
||||||
|
size = Vector2(800, 800)
|
||||||
|
else:
|
||||||
|
size = pref_size
|
||||||
|
always_on_top = GutEditorGlobals.user_prefs.gut_window_on_top.value
|
||||||
|
_chk_always_on_top.button_pressed = always_on_top
|
||||||
|
|
||||||
|
|
||||||
|
# --------
|
||||||
|
# Events
|
||||||
|
# --------
|
||||||
|
func _on_on_top_toggled(toggled_on: bool) -> void:
|
||||||
|
always_on_top = toggled_on
|
||||||
|
GutEditorGlobals.user_prefs.gut_window_on_top.value = toggled_on
|
||||||
|
|
||||||
|
|
||||||
|
func _on_size_changed() -> void:
|
||||||
|
if(_ready_to_go):
|
||||||
|
GutEditorGlobals.user_prefs.gut_window_size.value = size
|
||||||
|
|
||||||
|
|
||||||
|
func _on_close_requested() -> void:
|
||||||
|
gut_plugin.toggle_windowed()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func _on_vert_layout_pressed() -> void:
|
||||||
|
_bottom_panel.results_vert_layout()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_horiz_layout_pressed() -> void:
|
||||||
|
_bottom_panel.results_horiz_layout()
|
||||||
|
|
||||||
|
|
||||||
|
# --------
|
||||||
|
# Public
|
||||||
|
# --------
|
||||||
|
func add_gut_panel(panel : Control):
|
||||||
|
$Layout.add_child(panel)
|
||||||
|
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
panel.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
panel.visible = true
|
||||||
|
_bottom_panel = panel
|
||||||
|
_ready_to_go = true
|
||||||
|
|
||||||
|
panel.owner = self
|
||||||
|
|
||||||
|
# This stunk to figure out.
|
||||||
|
theme = interface.get_editor_theme()
|
||||||
|
var settings = interface.get_editor_settings()
|
||||||
|
$ColorRect.color = settings.get_setting("interface/theme/base_color")
|
||||||
|
|
||||||
|
set_gut_shortcuts(_bottom_panel._ctrls.shortcut_dialog)
|
||||||
|
|
||||||
|
|
||||||
|
func remove_panel():
|
||||||
|
$Layout.remove_child(_bottom_panel)
|
||||||
|
_bottom_panel.owner = null
|
||||||
|
|
||||||
|
|
||||||
|
func set_gut_shortcuts(shortcuts_dialog):
|
||||||
|
_gut_shortcuts.clear()
|
||||||
|
for btn in shortcuts_dialog.all_buttons:
|
||||||
|
_gut_shortcuts.append(btn.get_input_event().as_text_keycode())
|
||||||
1
addons/gut/gui/GutEditorWindow.gd.uid
Normal file
1
addons/gut/gui/GutEditorWindow.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://crp2af6k4nmf5
|
||||||
127
addons/gut/gui/GutEditorWindow.tscn
Normal file
127
addons/gut/gui/GutEditorWindow.tscn
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
[gd_scene load_steps=10 format=3 uid="uid://dnnvwlplf1km7"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://crp2af6k4nmf5" path="res://addons/gut/gui/GutEditorWindow.gd" id="1_qevl3"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://ljc2viafngwd" path="res://addons/gut/images/HSplitContainer.svg" id="2_xw0o2"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://bhew20crsywxr" path="res://addons/gut/images/VSplitContainer.svg" id="3_fqfwy"]
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qevl3"]
|
||||||
|
content_margin_left = 8.0
|
||||||
|
content_margin_top = 8.0
|
||||||
|
content_margin_right = 8.0
|
||||||
|
content_margin_bottom = 8.0
|
||||||
|
bg_color = Color(0.115499996, 0.132, 0.15949999, 1)
|
||||||
|
corner_detail = 1
|
||||||
|
anti_aliasing = false
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_af010"]
|
||||||
|
content_margin_left = 8.0
|
||||||
|
content_margin_top = 12.0
|
||||||
|
content_margin_right = 8.0
|
||||||
|
content_margin_bottom = 8.0
|
||||||
|
bg_color = Color(0.21, 0.24, 0.29, 1)
|
||||||
|
border_color = Color(0.08399999, 0.095999986, 0.116, 1)
|
||||||
|
corner_radius_top_left = 6
|
||||||
|
corner_radius_top_right = 6
|
||||||
|
corner_radius_bottom_right = 6
|
||||||
|
corner_radius_bottom_left = 6
|
||||||
|
corner_detail = 5
|
||||||
|
anti_aliasing = false
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_xw0o2"]
|
||||||
|
content_margin_left = 0.0
|
||||||
|
content_margin_top = 8.0
|
||||||
|
content_margin_right = 0.0
|
||||||
|
content_margin_bottom = 0.0
|
||||||
|
bg_color = Color(0.21, 0.24, 0.29, 1)
|
||||||
|
border_color = Color(0.08399999, 0.095999986, 0.116, 1)
|
||||||
|
corner_radius_bottom_right = 6
|
||||||
|
corner_radius_bottom_left = 6
|
||||||
|
corner_detail = 5
|
||||||
|
anti_aliasing = false
|
||||||
|
|
||||||
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_fqfwy"]
|
||||||
|
content_margin_left = 12.0
|
||||||
|
content_margin_top = 8.0
|
||||||
|
content_margin_right = 12.0
|
||||||
|
content_margin_bottom = 8.0
|
||||||
|
bg_color = Color(0.14699998, 0.16799998, 0.203, 1)
|
||||||
|
corner_radius_top_left = 6
|
||||||
|
corner_radius_top_right = 6
|
||||||
|
corner_radius_bottom_right = 6
|
||||||
|
corner_radius_bottom_left = 6
|
||||||
|
corner_detail = 5
|
||||||
|
anti_aliasing = false
|
||||||
|
|
||||||
|
[sub_resource type="Theme" id="Theme_fqfwy"]
|
||||||
|
Editor/colors/accent_color = Color(0.44, 0.73, 0.98, 1)
|
||||||
|
Editor/colors/background = Color(0.115499996, 0.132, 0.15949999, 1)
|
||||||
|
Editor/colors/base_color = Color(0.21, 0.24, 0.29, 1)
|
||||||
|
EditorStyles/styles/Background = SubResource("StyleBoxFlat_qevl3")
|
||||||
|
EditorStyles/styles/BottomPanel = SubResource("StyleBoxFlat_af010")
|
||||||
|
EditorStyles/styles/Content = SubResource("StyleBoxFlat_xw0o2")
|
||||||
|
Panel/styles/panel = SubResource("StyleBoxFlat_fqfwy")
|
||||||
|
|
||||||
|
[sub_resource type="ButtonGroup" id="ButtonGroup_qevl3"]
|
||||||
|
|
||||||
|
[node name="GutEditorWindow" type="Window"]
|
||||||
|
oversampling_override = 1.0
|
||||||
|
title = "GUT"
|
||||||
|
position = Vector2i(0, 36)
|
||||||
|
size = Vector2i(1408, 1728)
|
||||||
|
min_size = Vector2i(800, 600)
|
||||||
|
script = ExtResource("1_qevl3")
|
||||||
|
|
||||||
|
[node name="ColorRect" type="ColorRect" parent="."]
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
theme = SubResource("Theme_fqfwy")
|
||||||
|
color = Color(0.18717614, 0.18717614, 0.18717614, 1)
|
||||||
|
|
||||||
|
[node name="Layout" type="VBoxContainer" parent="."]
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
|
||||||
|
[node name="WinControls" type="HBoxContainer" parent="Layout"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="MenuBar" type="MenuBar" parent="Layout/WinControls"]
|
||||||
|
custom_minimum_size = Vector2(300, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
flat = true
|
||||||
|
prefer_global_menu = false
|
||||||
|
|
||||||
|
[node name="CenterContainer" type="CenterContainer" parent="Layout/WinControls"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="OnTop" type="CheckButton" parent="Layout/WinControls"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Always on Top"
|
||||||
|
|
||||||
|
[node name="HorizLayout" type="Button" parent="Layout/WinControls"]
|
||||||
|
texture_filter = 1
|
||||||
|
layout_mode = 2
|
||||||
|
toggle_mode = true
|
||||||
|
button_pressed = true
|
||||||
|
button_group = SubResource("ButtonGroup_qevl3")
|
||||||
|
icon = ExtResource("2_xw0o2")
|
||||||
|
icon_alignment = 1
|
||||||
|
|
||||||
|
[node name="VertLayout" type="Button" parent="Layout/WinControls"]
|
||||||
|
layout_mode = 2
|
||||||
|
toggle_mode = true
|
||||||
|
button_group = SubResource("ButtonGroup_qevl3")
|
||||||
|
icon = ExtResource("3_fqfwy")
|
||||||
|
|
||||||
|
[connection signal="close_requested" from="." to="." method="_on_close_requested"]
|
||||||
|
[connection signal="size_changed" from="." to="." method="_on_size_changed"]
|
||||||
|
[connection signal="toggled" from="Layout/WinControls/OnTop" to="." method="_on_on_top_toggled"]
|
||||||
|
[connection signal="pressed" from="Layout/WinControls/HorizLayout" to="." method="_on_horiz_layout_pressed"]
|
||||||
|
[connection signal="pressed" from="Layout/WinControls/VertLayout" to="." method="_on_vert_layout_pressed"]
|
||||||
36
addons/gut/gui/GutLogo.tscn
Normal file
36
addons/gut/gui/GutLogo.tscn
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
[gd_scene load_steps=4 format=3 uid="uid://bjkn8mhx2fmt1"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://b8lvgepb64m8t" path="res://addons/gut/gui/gut_logo.gd" id="1_ba6lh"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://dyxbmyvpkkcvs" path="res://addons/gut/images/GutIconV2_base.png" id="2_ba6lh"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://dx0yxxn5q7doc" path="res://addons/gut/images/eyey.png" id="3_rc8fb"]
|
||||||
|
|
||||||
|
[node name="Logo" type="Node2D"]
|
||||||
|
script = ExtResource("1_ba6lh")
|
||||||
|
|
||||||
|
[node name="BaseLogo" type="Sprite2D" parent="."]
|
||||||
|
scale = Vector2(0.5, 0.5)
|
||||||
|
texture = ExtResource("2_ba6lh")
|
||||||
|
|
||||||
|
[node name="LeftEye" type="Sprite2D" parent="BaseLogo"]
|
||||||
|
visible = false
|
||||||
|
position = Vector2(-238, 16)
|
||||||
|
texture = ExtResource("3_rc8fb")
|
||||||
|
|
||||||
|
[node name="RightEye" type="Sprite2D" parent="BaseLogo"]
|
||||||
|
visible = false
|
||||||
|
position = Vector2(239, 16)
|
||||||
|
texture = ExtResource("3_rc8fb")
|
||||||
|
|
||||||
|
[node name="ResetTimer" type="Timer" parent="."]
|
||||||
|
wait_time = 5.0
|
||||||
|
one_shot = true
|
||||||
|
|
||||||
|
[node name="FaceButton" type="Button" parent="."]
|
||||||
|
modulate = Color(1, 1, 1, 0)
|
||||||
|
offset_left = -141.0
|
||||||
|
offset_top = -113.0
|
||||||
|
offset_right = 140.0
|
||||||
|
offset_bottom = 175.0
|
||||||
|
|
||||||
|
[connection signal="timeout" from="ResetTimer" to="." method="_on_reset_timer_timeout"]
|
||||||
|
[connection signal="pressed" from="FaceButton" to="." method="_on_face_button_pressed"]
|
||||||
243
addons/gut/gui/GutRunner.gd
Normal file
243
addons/gut/gui/GutRunner.gd
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
# ##############################################################################
|
||||||
|
# This class joins together GUT, GUT Gui, GutConfig and is THE way to kick off a
|
||||||
|
# run of a test suite.
|
||||||
|
#
|
||||||
|
# This creates its own instance of gut.gd that it manages. You can set the
|
||||||
|
# gut.gd instance if you need to for testing.
|
||||||
|
#
|
||||||
|
# Set gut_config to an instance of a configured gut_config.gd instance prior to
|
||||||
|
# running tests.
|
||||||
|
#
|
||||||
|
# This will create a GUI and wire it up and apply gut_config.gd options.
|
||||||
|
#
|
||||||
|
# Running tests: Call run_tests
|
||||||
|
# ##############################################################################
|
||||||
|
extends Node2D
|
||||||
|
|
||||||
|
const EXIT_OK = 0
|
||||||
|
const EXIT_ERROR = 1
|
||||||
|
|
||||||
|
var Gut = load('res://addons/gut/gut.gd')
|
||||||
|
var ResultExporter = load('res://addons/gut/result_exporter.gd')
|
||||||
|
var GutConfig = load('res://addons/gut/gut_config.gd')
|
||||||
|
|
||||||
|
var runner_json_path = null
|
||||||
|
var result_bbcode_path = null
|
||||||
|
var result_json_path = null
|
||||||
|
|
||||||
|
var lgr = GutUtils.get_logger()
|
||||||
|
var gut_config = null
|
||||||
|
|
||||||
|
var error_tracker = GutUtils.get_error_tracker()
|
||||||
|
|
||||||
|
var _hid_gut = null;
|
||||||
|
# Lazy loaded gut instance. Settable for testing purposes.
|
||||||
|
var gut = _hid_gut :
|
||||||
|
get:
|
||||||
|
if(_hid_gut == null):
|
||||||
|
_hid_gut = Gut.new(lgr)
|
||||||
|
_hid_gut.error_tracker = error_tracker
|
||||||
|
return _hid_gut
|
||||||
|
set(val):
|
||||||
|
_hid_gut = val
|
||||||
|
|
||||||
|
var _wrote_results = false
|
||||||
|
var _ran_from_editor = false
|
||||||
|
|
||||||
|
@onready var _gut_layer = $GutLayer
|
||||||
|
@onready var _gui = $GutLayer/GutScene
|
||||||
|
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
GutUtils.WarningsManager.apply_warnings_dictionary(
|
||||||
|
GutUtils.warnings_at_start)
|
||||||
|
|
||||||
|
|
||||||
|
func _exit_tree():
|
||||||
|
if(!_wrote_results and _ran_from_editor):
|
||||||
|
_write_results_for_gut_panel()
|
||||||
|
|
||||||
|
|
||||||
|
func _setup_gui(show_gui):
|
||||||
|
if(show_gui):
|
||||||
|
_gui.gut = gut
|
||||||
|
var printer = gut.logger.get_printer('gui')
|
||||||
|
printer.set_textbox(_gui.get_textbox())
|
||||||
|
else:
|
||||||
|
gut.logger.disable_printer('gui', true)
|
||||||
|
_gui.visible = false
|
||||||
|
|
||||||
|
var opts = gut_config.options
|
||||||
|
_gui.set_font_size(opts.font_size)
|
||||||
|
_gui.set_font(opts.font_name)
|
||||||
|
if(opts.font_color != null and opts.font_color.is_valid_html_color()):
|
||||||
|
_gui.set_default_font_color(Color(opts.font_color))
|
||||||
|
if(opts.background_color != null and opts.background_color.is_valid_html_color()):
|
||||||
|
_gui.set_background_color(Color(opts.background_color))
|
||||||
|
|
||||||
|
_gui.set_opacity(min(1.0, float(opts.opacity) / 100))
|
||||||
|
_gui.use_compact_mode(opts.compact_mode)
|
||||||
|
|
||||||
|
|
||||||
|
func _write_results_for_gut_panel():
|
||||||
|
var content = _gui.get_textbox().get_parsed_text() #_gut.logger.get_gui_bbcode()
|
||||||
|
var f = FileAccess.open(result_bbcode_path, FileAccess.WRITE)
|
||||||
|
if(f != null):
|
||||||
|
f.store_string(content)
|
||||||
|
f = null # closes file
|
||||||
|
else:
|
||||||
|
push_error('Could not save bbcode, result = ', FileAccess.get_open_error())
|
||||||
|
|
||||||
|
var exporter = ResultExporter.new()
|
||||||
|
# TODO this should be checked and _wrote_results should maybe not be set, or
|
||||||
|
# maybe we do not care. Whichever, it should be clear.
|
||||||
|
var _f_result = exporter.write_json_file(gut, result_json_path)
|
||||||
|
_wrote_results = true
|
||||||
|
|
||||||
|
|
||||||
|
func _handle_quit(should_exit, should_exit_on_success, override_exit_code=EXIT_OK):
|
||||||
|
var quitting_time = should_exit or \
|
||||||
|
(should_exit_on_success and gut.get_fail_count() == 0)
|
||||||
|
|
||||||
|
if(!quitting_time):
|
||||||
|
if(should_exit_on_success):
|
||||||
|
lgr.log("There are failing tests, exit manually.")
|
||||||
|
_gui.use_compact_mode(false)
|
||||||
|
return
|
||||||
|
|
||||||
|
# For some reason, tests fail asserting that quit was called with 0 if we
|
||||||
|
# do not do this, but everything is defaulted so I don't know why it gets
|
||||||
|
# null.
|
||||||
|
var exit_code = GutUtils.nvl(override_exit_code, EXIT_OK)
|
||||||
|
|
||||||
|
if(gut.get_fail_count() > 0):
|
||||||
|
exit_code = EXIT_ERROR
|
||||||
|
|
||||||
|
# Overwrite the exit code with the post_script's exit code if it is set
|
||||||
|
var post_hook_inst = gut.get_post_run_script_instance()
|
||||||
|
if(post_hook_inst != null and post_hook_inst.get_exit_code() != null):
|
||||||
|
exit_code = post_hook_inst.get_exit_code()
|
||||||
|
|
||||||
|
quit(exit_code)
|
||||||
|
|
||||||
|
|
||||||
|
func _end_run(override_exit_code=EXIT_OK):
|
||||||
|
if(_ran_from_editor):
|
||||||
|
_write_results_for_gut_panel()
|
||||||
|
|
||||||
|
GutErrorTracker.deregister_logger(error_tracker)
|
||||||
|
|
||||||
|
_handle_quit(gut_config.options.should_exit,
|
||||||
|
gut_config.options.should_exit_on_success,
|
||||||
|
override_exit_code)
|
||||||
|
|
||||||
|
|
||||||
|
# -------------
|
||||||
|
# Events
|
||||||
|
# -------------
|
||||||
|
func _on_tests_finished():
|
||||||
|
_end_run()
|
||||||
|
|
||||||
|
|
||||||
|
# -------------
|
||||||
|
# Public
|
||||||
|
# -------------
|
||||||
|
# For internal use only, but still public. Consider it "protected" and you
|
||||||
|
# don't have my permission to call this, unless "you" is "me".
|
||||||
|
func run_from_editor():
|
||||||
|
_ran_from_editor = true
|
||||||
|
var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')
|
||||||
|
runner_json_path = GutUtils.nvl(runner_json_path, GutEditorGlobals.editor_run_gut_config_path)
|
||||||
|
result_bbcode_path = GutUtils.nvl(result_bbcode_path, GutEditorGlobals.editor_run_bbcode_results_path)
|
||||||
|
result_json_path = GutUtils.nvl(result_json_path, GutEditorGlobals.editor_run_json_results_path)
|
||||||
|
|
||||||
|
if(gut_config == null):
|
||||||
|
gut_config = GutConfig.new()
|
||||||
|
gut_config.load_options(runner_json_path)
|
||||||
|
|
||||||
|
call_deferred('run_tests')
|
||||||
|
|
||||||
|
|
||||||
|
func run_tests(show_gui=true):
|
||||||
|
_setup_gui(show_gui)
|
||||||
|
|
||||||
|
if(gut_config.options.dirs.size() + gut_config.options.tests.size() == 0):
|
||||||
|
var err_text = "You do not have any directories configured, so GUT " + \
|
||||||
|
"doesn't know where to find the tests. Tell GUT where to find the " + \
|
||||||
|
"tests and GUT shall run the tests."
|
||||||
|
lgr.error(err_text)
|
||||||
|
push_error(err_text)
|
||||||
|
_end_run(EXIT_ERROR)
|
||||||
|
return
|
||||||
|
|
||||||
|
var install_check_text = GutUtils.make_install_check_text()
|
||||||
|
if(install_check_text != GutUtils.INSTALL_OK_TEXT):
|
||||||
|
print("\n\n", GutUtils.version_numbers.get_version_text())
|
||||||
|
lgr.error(install_check_text)
|
||||||
|
push_error(install_check_text)
|
||||||
|
_end_run(EXIT_ERROR)
|
||||||
|
return
|
||||||
|
|
||||||
|
gut.add_children_to = self
|
||||||
|
if(gut.get_parent() == null):
|
||||||
|
if(gut_config.options.gut_on_top):
|
||||||
|
_gut_layer.add_child(gut)
|
||||||
|
else:
|
||||||
|
add_child(gut)
|
||||||
|
|
||||||
|
if(!gut.end_run.is_connected(_on_tests_finished)):
|
||||||
|
gut.end_run.connect(_on_tests_finished)
|
||||||
|
|
||||||
|
gut_config.apply_options(gut)
|
||||||
|
var run_rest_of_scripts = gut_config.options.unit_test_name == ''
|
||||||
|
GutErrorTracker.register_logger(error_tracker)
|
||||||
|
gut.test_scripts(run_rest_of_scripts)
|
||||||
|
|
||||||
|
|
||||||
|
func set_gut_config(which):
|
||||||
|
gut_config = which
|
||||||
|
|
||||||
|
|
||||||
|
# for backwards compatibility
|
||||||
|
func get_gut():
|
||||||
|
return gut
|
||||||
|
|
||||||
|
|
||||||
|
func quit(exit_code):
|
||||||
|
# Sometimes quitting takes a few seconds. This gives some indicator
|
||||||
|
# of what is going on.
|
||||||
|
_gui.set_title("Exiting")
|
||||||
|
|
||||||
|
await get_tree().process_frame
|
||||||
|
|
||||||
|
lgr.info(str('Exiting with code ', exit_code))
|
||||||
|
get_tree().quit(exit_code)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ##############################################################################
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
# ##############################################################################
|
||||||
1
addons/gut/gui/GutRunner.gd.uid
Normal file
1
addons/gut/gui/GutRunner.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://eg8k46gd42a4
|
||||||
12
addons/gut/gui/GutRunner.tscn
Normal file
12
addons/gut/gui/GutRunner.tscn
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[gd_scene load_steps=3 format=3 uid="uid://bqy3ikt6vu4b5"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://eg8k46gd42a4" path="res://addons/gut/gui/GutRunner.gd" id="1"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://m28heqtswbuq" path="res://addons/gut/GutScene.tscn" id="2_6ruxb"]
|
||||||
|
|
||||||
|
[node name="GutRunner" type="Node2D"]
|
||||||
|
script = ExtResource("1")
|
||||||
|
|
||||||
|
[node name="GutLayer" type="CanvasLayer" parent="."]
|
||||||
|
layer = 128
|
||||||
|
|
||||||
|
[node name="GutScene" parent="GutLayer" instance=ExtResource("2_6ruxb")]
|
||||||
7
addons/gut/gui/GutSceneTheme.tres
Normal file
7
addons/gut/gui/GutSceneTheme.tres
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[gd_resource type="Theme" load_steps=2 format=3 uid="uid://cstkhwkpajvqu"]
|
||||||
|
|
||||||
|
[ext_resource type="FontFile" uid="uid://c6c7gnx36opr0" path="res://addons/gut/fonts/AnonymousPro-Regular.ttf" id="1_df57p"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
default_font = ExtResource("1_df57p")
|
||||||
|
Label/fonts/font = ExtResource("1_df57p")
|
||||||
161
addons/gut/gui/MinGui.tscn
Normal file
161
addons/gut/gui/MinGui.tscn
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
[gd_scene load_steps=5 format=3 uid="uid://cnqqdfsn80ise"]
|
||||||
|
|
||||||
|
[ext_resource type="Theme" uid="uid://cstkhwkpajvqu" path="res://addons/gut/gui/GutSceneTheme.tres" id="1_farmq"]
|
||||||
|
[ext_resource type="FontFile" uid="uid://bnh0lslf4yh87" path="res://addons/gut/fonts/CourierPrime-Regular.ttf" id="2_a2e2l"]
|
||||||
|
[ext_resource type="Script" uid="uid://blvhsbnsvfyow" path="res://addons/gut/gui/gut_gui.gd" id="2_eokrf"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://bvrqqgjpyouse" path="res://addons/gut/gui/ResizeHandle.tscn" id="4_xrhva"]
|
||||||
|
|
||||||
|
[node name="Min" type="Panel"]
|
||||||
|
clip_contents = true
|
||||||
|
custom_minimum_size = Vector2(280, 145)
|
||||||
|
offset_right = 280.0
|
||||||
|
offset_bottom = 145.0
|
||||||
|
theme = ExtResource("1_farmq")
|
||||||
|
script = ExtResource("2_eokrf")
|
||||||
|
|
||||||
|
[node name="MainBox" type="VBoxContainer" parent="."]
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
metadata/_edit_layout_mode = 1
|
||||||
|
|
||||||
|
[node name="TitleBar" type="Panel" parent="MainBox"]
|
||||||
|
custom_minimum_size = Vector2(0, 25)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="TitleBox" type="HBoxContainer" parent="MainBox/TitleBar"]
|
||||||
|
layout_mode = 0
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_top = 2.0
|
||||||
|
offset_bottom = 3.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
metadata/_edit_layout_mode = 1
|
||||||
|
|
||||||
|
[node name="Spacer1" type="CenterContainer" parent="MainBox/TitleBar/TitleBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="Title" type="Label" parent="MainBox/TitleBar/TitleBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Title"
|
||||||
|
|
||||||
|
[node name="Spacer2" type="CenterContainer" parent="MainBox/TitleBar/TitleBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="TimeLabel" type="Label" parent="MainBox/TitleBar/TitleBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "0.000s"
|
||||||
|
|
||||||
|
[node name="Body" type="HBoxContainer" parent="MainBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="LeftMargin" type="CenterContainer" parent="MainBox/Body"]
|
||||||
|
custom_minimum_size = Vector2(5, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="BodyRows" type="VBoxContainer" parent="MainBox/Body"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="ProgressBars" type="HBoxContainer" parent="MainBox/Body/BodyRows"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="HBoxContainer" type="HBoxContainer" parent="MainBox/Body/BodyRows/ProgressBars"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="MainBox/Body/BodyRows/ProgressBars/HBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "T:"
|
||||||
|
|
||||||
|
[node name="ProgressTest" type="ProgressBar" parent="MainBox/Body/BodyRows/ProgressBars/HBoxContainer"]
|
||||||
|
custom_minimum_size = Vector2(100, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
value = 25.0
|
||||||
|
|
||||||
|
[node name="HBoxContainer2" type="HBoxContainer" parent="MainBox/Body/BodyRows/ProgressBars"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="MainBox/Body/BodyRows/ProgressBars/HBoxContainer2"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "S:"
|
||||||
|
|
||||||
|
[node name="ProgressScript" type="ProgressBar" parent="MainBox/Body/BodyRows/ProgressBars/HBoxContainer2"]
|
||||||
|
custom_minimum_size = Vector2(100, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
value = 75.0
|
||||||
|
|
||||||
|
[node name="PathDisplay" type="VBoxContainer" parent="MainBox/Body/BodyRows"]
|
||||||
|
clip_contents = true
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="Path" type="Label" parent="MainBox/Body/BodyRows/PathDisplay"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_fonts/font = ExtResource("2_a2e2l")
|
||||||
|
theme_override_font_sizes/font_size = 14
|
||||||
|
text = "res://test/integration/whatever"
|
||||||
|
clip_text = true
|
||||||
|
text_overrun_behavior = 3
|
||||||
|
|
||||||
|
[node name="HBoxContainer" type="HBoxContainer" parent="MainBox/Body/BodyRows/PathDisplay"]
|
||||||
|
clip_contents = true
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="S3" type="CenterContainer" parent="MainBox/Body/BodyRows/PathDisplay/HBoxContainer"]
|
||||||
|
custom_minimum_size = Vector2(5, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="File" type="Label" parent="MainBox/Body/BodyRows/PathDisplay/HBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
theme_override_fonts/font = ExtResource("2_a2e2l")
|
||||||
|
theme_override_font_sizes/font_size = 14
|
||||||
|
text = "test_this_thing.gd"
|
||||||
|
text_overrun_behavior = 3
|
||||||
|
|
||||||
|
[node name="Footer" type="HBoxContainer" parent="MainBox/Body/BodyRows"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="HandleLeft" parent="MainBox/Body/BodyRows/Footer" node_paths=PackedStringArray("resize_control") instance=ExtResource("4_xrhva")]
|
||||||
|
layout_mode = 2
|
||||||
|
orientation = 0
|
||||||
|
resize_control = NodePath("../../../../..")
|
||||||
|
vertical_resize = false
|
||||||
|
|
||||||
|
[node name="SwitchModes" type="Button" parent="MainBox/Body/BodyRows/Footer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Expand"
|
||||||
|
|
||||||
|
[node name="CenterContainer" type="CenterContainer" parent="MainBox/Body/BodyRows/Footer"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="Continue" type="Button" parent="MainBox/Body/BodyRows/Footer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Continue
|
||||||
|
"
|
||||||
|
|
||||||
|
[node name="HandleRight" parent="MainBox/Body/BodyRows/Footer" node_paths=PackedStringArray("resize_control") instance=ExtResource("4_xrhva")]
|
||||||
|
layout_mode = 2
|
||||||
|
resize_control = NodePath("../../../../..")
|
||||||
|
vertical_resize = false
|
||||||
|
|
||||||
|
[node name="RightMargin" type="CenterContainer" parent="MainBox/Body"]
|
||||||
|
custom_minimum_size = Vector2(5, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="CenterContainer" type="CenterContainer" parent="MainBox"]
|
||||||
|
custom_minimum_size = Vector2(2.08165e-12, 2)
|
||||||
|
layout_mode = 2
|
||||||
215
addons/gut/gui/NormalGui.tscn
Normal file
215
addons/gut/gui/NormalGui.tscn
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
[gd_scene load_steps=5 format=3 uid="uid://duxblir3vu8x7"]
|
||||||
|
|
||||||
|
[ext_resource type="Theme" uid="uid://cstkhwkpajvqu" path="res://addons/gut/gui/GutSceneTheme.tres" id="1_5hlsm"]
|
||||||
|
[ext_resource type="Script" uid="uid://blvhsbnsvfyow" path="res://addons/gut/gui/gut_gui.gd" id="2_fue6q"]
|
||||||
|
[ext_resource type="FontFile" uid="uid://bnh0lslf4yh87" path="res://addons/gut/fonts/CourierPrime-Regular.ttf" id="2_u5uc1"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://bvrqqgjpyouse" path="res://addons/gut/gui/ResizeHandle.tscn" id="4_2r8a8"]
|
||||||
|
|
||||||
|
[node name="Large" type="Panel"]
|
||||||
|
custom_minimum_size = Vector2(500, 150)
|
||||||
|
offset_right = 632.0
|
||||||
|
offset_bottom = 260.0
|
||||||
|
theme = ExtResource("1_5hlsm")
|
||||||
|
script = ExtResource("2_fue6q")
|
||||||
|
|
||||||
|
[node name="MainBox" type="VBoxContainer" parent="."]
|
||||||
|
layout_mode = 0
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
metadata/_edit_layout_mode = 1
|
||||||
|
|
||||||
|
[node name="TitleBar" type="Panel" parent="MainBox"]
|
||||||
|
custom_minimum_size = Vector2(0, 25)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="TitleBox" type="HBoxContainer" parent="MainBox/TitleBar"]
|
||||||
|
layout_mode = 0
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
offset_top = 2.0
|
||||||
|
offset_bottom = 3.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
metadata/_edit_layout_mode = 1
|
||||||
|
|
||||||
|
[node name="Spacer1" type="CenterContainer" parent="MainBox/TitleBar/TitleBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="Title" type="Label" parent="MainBox/TitleBar/TitleBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Title"
|
||||||
|
|
||||||
|
[node name="Spacer2" type="CenterContainer" parent="MainBox/TitleBar/TitleBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="TimeLabel" type="Label" parent="MainBox/TitleBar/TitleBox"]
|
||||||
|
custom_minimum_size = Vector2(90, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
text = "999.999s"
|
||||||
|
|
||||||
|
[node name="HBoxContainer" type="HBoxContainer" parent="MainBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="MainBox/HBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="OutputBG" type="ColorRect" parent="MainBox/HBoxContainer/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
color = Color(0.0745098, 0.0705882, 0.0784314, 1)
|
||||||
|
metadata/_edit_layout_mode = 1
|
||||||
|
|
||||||
|
[node name="HBoxContainer" type="HBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/OutputBG"]
|
||||||
|
layout_mode = 0
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
|
||||||
|
[node name="S2" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/OutputBG/HBoxContainer"]
|
||||||
|
custom_minimum_size = Vector2(5, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="TestOutput" type="RichTextLabel" parent="MainBox/HBoxContainer/VBoxContainer/OutputBG/HBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
focus_mode = 2
|
||||||
|
bbcode_enabled = true
|
||||||
|
scroll_following = true
|
||||||
|
autowrap_mode = 0
|
||||||
|
selection_enabled = true
|
||||||
|
|
||||||
|
[node name="S1" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/OutputBG/HBoxContainer"]
|
||||||
|
custom_minimum_size = Vector2(5, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="ControlBox" type="HBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="S1" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"]
|
||||||
|
custom_minimum_size = Vector2(5, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="ProgressBars" type="VBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"]
|
||||||
|
custom_minimum_size = Vector2(2.08165e-12, 2.08165e-12)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="TestBox" type="HBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/TestBox"]
|
||||||
|
custom_minimum_size = Vector2(60, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
text = "Tests"
|
||||||
|
|
||||||
|
[node name="ProgressTest" type="ProgressBar" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/TestBox"]
|
||||||
|
custom_minimum_size = Vector2(100, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
value = 25.0
|
||||||
|
|
||||||
|
[node name="ScriptBox" type="HBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/ScriptBox"]
|
||||||
|
custom_minimum_size = Vector2(60, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
text = "Scripts"
|
||||||
|
|
||||||
|
[node name="ProgressScript" type="ProgressBar" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/ScriptBox"]
|
||||||
|
custom_minimum_size = Vector2(100, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
value = 75.0
|
||||||
|
|
||||||
|
[node name="PathDisplay" type="VBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="Path" type="Label" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 6
|
||||||
|
theme_override_fonts/font = ExtResource("2_u5uc1")
|
||||||
|
theme_override_font_sizes/font_size = 14
|
||||||
|
text = "res://test/integration/whatever"
|
||||||
|
text_overrun_behavior = 3
|
||||||
|
|
||||||
|
[node name="HBoxContainer" type="HBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="S3" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay/HBoxContainer"]
|
||||||
|
custom_minimum_size = Vector2(5, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="File" type="Label" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay/HBoxContainer"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
theme_override_fonts/font = ExtResource("2_u5uc1")
|
||||||
|
theme_override_font_sizes/font_size = 14
|
||||||
|
text = "test_this_thing.gd"
|
||||||
|
text_overrun_behavior = 3
|
||||||
|
|
||||||
|
[node name="Buttons" type="VBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Continue" type="Button" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/Buttons"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_vertical = 4
|
||||||
|
text = "Continue
|
||||||
|
"
|
||||||
|
|
||||||
|
[node name="WordWrap" type="CheckButton" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/Buttons"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Word Wrap"
|
||||||
|
|
||||||
|
[node name="S3" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"]
|
||||||
|
custom_minimum_size = Vector2(5, 0)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="BottomPad" type="CenterContainer" parent="MainBox"]
|
||||||
|
custom_minimum_size = Vector2(0, 5)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="Footer" type="HBoxContainer" parent="MainBox"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="SidePad1" type="CenterContainer" parent="MainBox/Footer"]
|
||||||
|
custom_minimum_size = Vector2(2, 2.08165e-12)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="ResizeHandle3" parent="MainBox/Footer" node_paths=PackedStringArray("resize_control") instance=ExtResource("4_2r8a8")]
|
||||||
|
custom_minimum_size = Vector2(25, 25)
|
||||||
|
layout_mode = 2
|
||||||
|
orientation = 0
|
||||||
|
resize_control = NodePath("../../..")
|
||||||
|
|
||||||
|
[node name="SwitchModes" type="Button" parent="MainBox/Footer"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Compact
|
||||||
|
"
|
||||||
|
|
||||||
|
[node name="CenterContainer" type="CenterContainer" parent="MainBox/Footer"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="ResizeHandle2" parent="MainBox/Footer" node_paths=PackedStringArray("resize_control") instance=ExtResource("4_2r8a8")]
|
||||||
|
custom_minimum_size = Vector2(25, 25)
|
||||||
|
layout_mode = 2
|
||||||
|
resize_control = NodePath("../../..")
|
||||||
|
|
||||||
|
[node name="SidePad2" type="CenterContainer" parent="MainBox/Footer"]
|
||||||
|
custom_minimum_size = Vector2(2, 2.08165e-12)
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="BottomPad2" type="CenterContainer" parent="MainBox"]
|
||||||
|
custom_minimum_size = Vector2(2.08165e-12, 2)
|
||||||
|
layout_mode = 2
|
||||||
352
addons/gut/gui/OutputText.gd
Normal file
352
addons/gut/gui/OutputText.gd
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
@tool
|
||||||
|
extends VBoxContainer
|
||||||
|
|
||||||
|
var GutEditorGlobals = load('res://addons/gut/gui/editor_globals.gd')
|
||||||
|
var PanelControls = load('res://addons/gut/gui/panel_controls.gd')
|
||||||
|
|
||||||
|
# ##############################################################################
|
||||||
|
# Keeps search results from the TextEdit
|
||||||
|
# ##############################################################################
|
||||||
|
class TextEditSearcher:
|
||||||
|
var te : TextEdit
|
||||||
|
var _last_term = ''
|
||||||
|
var _last_pos = Vector2(-1, -1)
|
||||||
|
var _ignore_caret_change = false
|
||||||
|
|
||||||
|
func set_text_edit(which):
|
||||||
|
te = which
|
||||||
|
te.caret_changed.connect(_on_caret_changed)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_caret_changed():
|
||||||
|
if(_ignore_caret_change):
|
||||||
|
_ignore_caret_change = false
|
||||||
|
else:
|
||||||
|
_last_pos = _get_caret();
|
||||||
|
|
||||||
|
|
||||||
|
func _get_caret():
|
||||||
|
return Vector2(te.get_caret_column(), te.get_caret_line())
|
||||||
|
|
||||||
|
|
||||||
|
func _set_caret_and_sel(pos, len):
|
||||||
|
te.set_caret_line(pos.y)
|
||||||
|
te.set_caret_column(pos.x)
|
||||||
|
if(len > 0):
|
||||||
|
te.select(pos.y, pos.x, pos.y, pos.x + len)
|
||||||
|
|
||||||
|
|
||||||
|
func _find(term, search_flags):
|
||||||
|
var pos = _get_caret()
|
||||||
|
if(term == _last_term):
|
||||||
|
if(search_flags == 0):
|
||||||
|
pos = _last_pos
|
||||||
|
pos.x += 1
|
||||||
|
else:
|
||||||
|
pos = _last_pos
|
||||||
|
pos.x -= 1
|
||||||
|
|
||||||
|
var result = te.search(term, search_flags, pos.y, pos.x)
|
||||||
|
# print('searching from ', pos, ' for "', term, '" = ', result)
|
||||||
|
if(result.y != -1):
|
||||||
|
_ignore_caret_change = true
|
||||||
|
_set_caret_and_sel(result, term.length())
|
||||||
|
_last_pos = result
|
||||||
|
|
||||||
|
_last_term = term
|
||||||
|
|
||||||
|
func find_next(term):
|
||||||
|
_find(term, 0)
|
||||||
|
|
||||||
|
func find_prev(term):
|
||||||
|
_find(term, te.SEARCH_BACKWARDS)
|
||||||
|
|
||||||
|
|
||||||
|
# ##############################################################################
|
||||||
|
# Start OutputText control code
|
||||||
|
# ##############################################################################
|
||||||
|
@onready var _ctrls = {
|
||||||
|
output = $Output,
|
||||||
|
settings_bar = $Settings,
|
||||||
|
use_colors = $Settings/UseColors,
|
||||||
|
word_wrap = $Settings/WordWrap,
|
||||||
|
|
||||||
|
copy_button = $Toolbar/CopyButton,
|
||||||
|
clear_button = $Toolbar/ClearButton,
|
||||||
|
show_search = $Toolbar/ShowSearch,
|
||||||
|
caret_position = $Toolbar/LblPosition,
|
||||||
|
|
||||||
|
search_bar = {
|
||||||
|
bar = $Search,
|
||||||
|
search_term = $Search/SearchTerm,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _sr = TextEditSearcher.new()
|
||||||
|
var _highlighter : CodeHighlighter
|
||||||
|
var _font_name = null
|
||||||
|
var _user_prefs = GutEditorGlobals.user_prefs
|
||||||
|
var _font_name_pctrl = null
|
||||||
|
var _font_size_pctrl = null
|
||||||
|
|
||||||
|
var keywords = [
|
||||||
|
['Failed', Color.RED],
|
||||||
|
['Passed', Color.GREEN],
|
||||||
|
['Pending', Color.YELLOW],
|
||||||
|
['Risky', Color.YELLOW],
|
||||||
|
['Orphans', Color.YELLOW],
|
||||||
|
['WARNING', Color.YELLOW],
|
||||||
|
['ERROR', Color.RED],
|
||||||
|
['ExpectedError', Color.LIGHT_BLUE],
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Automatically used when running the OutputText scene from the editor. Changes
|
||||||
|
# to this method only affect test-running the control through the editor.
|
||||||
|
func _test_running_setup():
|
||||||
|
_ctrls.use_colors.text = 'use colors'
|
||||||
|
_ctrls.show_search.text = 'search'
|
||||||
|
_ctrls.word_wrap.text = 'ww'
|
||||||
|
|
||||||
|
set_all_fonts("CourierPrime")
|
||||||
|
set_font_size(30)
|
||||||
|
|
||||||
|
_ctrls.output.queue_redraw()
|
||||||
|
load_file('user://.gut_editor.bbcode')
|
||||||
|
await get_tree().process_frame
|
||||||
|
|
||||||
|
show_search(true)
|
||||||
|
_ctrls.output.set_caret_line(0)
|
||||||
|
_ctrls.output.scroll_vertical = 0
|
||||||
|
_ctrls.output.caret_changed.connect(_on_caret_changed)
|
||||||
|
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
if(get_parent() is SubViewport):
|
||||||
|
return
|
||||||
|
|
||||||
|
_sr.set_text_edit(_ctrls.output)
|
||||||
|
_ctrls.use_colors.icon = get_theme_icon('RichTextEffect', 'EditorIcons')
|
||||||
|
_ctrls.show_search.icon = get_theme_icon('Search', 'EditorIcons')
|
||||||
|
_ctrls.word_wrap.icon = get_theme_icon('Loop', 'EditorIcons')
|
||||||
|
|
||||||
|
_setup_colors()
|
||||||
|
_ctrls.use_colors.button_pressed = true
|
||||||
|
_use_highlighting(true)
|
||||||
|
|
||||||
|
if(get_parent() == get_tree().root):
|
||||||
|
_test_running_setup()
|
||||||
|
|
||||||
|
_ctrls.settings_bar.visible = false
|
||||||
|
_add_other_ctrls()
|
||||||
|
|
||||||
|
|
||||||
|
func _add_other_ctrls():
|
||||||
|
var fname = GutUtils.gut_fonts.DEFAULT_CUSTOM_FONT_NAME
|
||||||
|
if(_user_prefs != null):
|
||||||
|
fname = _user_prefs.output_font_name.value
|
||||||
|
_font_name_pctrl = PanelControls.SelectControl.new('Font', fname, GutUtils.avail_fonts,
|
||||||
|
"The font, you know, for the text below. Change it, see what it does.")
|
||||||
|
_font_name_pctrl.changed.connect(_on_font_name_changed)
|
||||||
|
_font_name_pctrl.label.size_flags_horizontal = SIZE_SHRINK_BEGIN
|
||||||
|
_ctrls.settings_bar.add_child(_font_name_pctrl)
|
||||||
|
set_all_fonts(fname)
|
||||||
|
|
||||||
|
var fsize = 30
|
||||||
|
if(_user_prefs != null):
|
||||||
|
fsize = _user_prefs.output_font_size.value
|
||||||
|
_font_size_pctrl = PanelControls.NumberControl.new('Font Size', fsize , 5, 100,
|
||||||
|
"The size of 'The Font'.")
|
||||||
|
_font_size_pctrl.changed.connect(_on_font_size_changed)
|
||||||
|
_font_size_pctrl.label.size_flags_horizontal = SIZE_SHRINK_BEGIN
|
||||||
|
_ctrls.settings_bar.add_child(_font_size_pctrl)
|
||||||
|
set_font_size(fsize)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------
|
||||||
|
# Private
|
||||||
|
# ------------------
|
||||||
|
|
||||||
|
# Call this after changes in colors and the like to get them to apply. reloads
|
||||||
|
# the text of the output control.
|
||||||
|
func _refresh_output():
|
||||||
|
var orig_pos = _ctrls.output.scroll_vertical
|
||||||
|
var text = _ctrls.output.text
|
||||||
|
|
||||||
|
_ctrls.output.text = text
|
||||||
|
_ctrls.output.scroll_vertical = orig_pos
|
||||||
|
|
||||||
|
|
||||||
|
func _create_highlighter(default_color=Color(1, 1, 1, 1)):
|
||||||
|
var to_return = CodeHighlighter.new()
|
||||||
|
|
||||||
|
to_return.function_color = default_color
|
||||||
|
to_return.number_color = default_color
|
||||||
|
to_return.symbol_color = default_color
|
||||||
|
to_return.member_variable_color = default_color
|
||||||
|
|
||||||
|
for keyword in keywords:
|
||||||
|
to_return.add_keyword_color(keyword[0], keyword[1])
|
||||||
|
|
||||||
|
return to_return
|
||||||
|
|
||||||
|
|
||||||
|
func _setup_colors():
|
||||||
|
_ctrls.output.clear()
|
||||||
|
_highlighter = _create_highlighter()
|
||||||
|
_ctrls.output.queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func _use_highlighting(should):
|
||||||
|
if(should):
|
||||||
|
_ctrls.output.syntax_highlighter = _highlighter
|
||||||
|
else:
|
||||||
|
_ctrls.output.syntax_highlighter = null
|
||||||
|
_refresh_output()
|
||||||
|
|
||||||
|
# ------------------
|
||||||
|
# Events
|
||||||
|
# ------------------
|
||||||
|
func _on_caret_changed():
|
||||||
|
var txt = str("line:",_ctrls.output.get_caret_line(), ' col:', _ctrls.output.get_caret_column())
|
||||||
|
_ctrls.caret_position.text = str(txt)
|
||||||
|
|
||||||
|
func _on_font_size_changed():
|
||||||
|
set_font_size(_font_size_pctrl.value)
|
||||||
|
if(_user_prefs != null):
|
||||||
|
_user_prefs.output_font_size.value = _font_size_pctrl.value
|
||||||
|
_user_prefs.output_font_size.save_it()
|
||||||
|
|
||||||
|
func _on_font_name_changed():
|
||||||
|
set_all_fonts(_font_name_pctrl.text)
|
||||||
|
if(_user_prefs != null):
|
||||||
|
_user_prefs.output_font_name.value = _font_name_pctrl.text
|
||||||
|
_user_prefs.output_font_name.save_it()
|
||||||
|
|
||||||
|
func _on_CopyButton_pressed():
|
||||||
|
copy_to_clipboard()
|
||||||
|
|
||||||
|
func _on_UseColors_pressed():
|
||||||
|
_use_highlighting(_ctrls.use_colors.button_pressed)
|
||||||
|
|
||||||
|
func _on_ClearButton_pressed():
|
||||||
|
clear()
|
||||||
|
|
||||||
|
func _on_ShowSearch_pressed():
|
||||||
|
show_search(_ctrls.show_search.button_pressed)
|
||||||
|
|
||||||
|
func _on_SearchTerm_focus_entered():
|
||||||
|
_ctrls.search_bar.search_term.call_deferred('select_all')
|
||||||
|
|
||||||
|
func _on_SearchNext_pressed():
|
||||||
|
_sr.find_next(_ctrls.search_bar.search_term.text)
|
||||||
|
|
||||||
|
func _on_SearchPrev_pressed():
|
||||||
|
_sr.find_prev(_ctrls.search_bar.search_term.text)
|
||||||
|
|
||||||
|
func _on_SearchTerm_text_changed(new_text):
|
||||||
|
if(new_text == ''):
|
||||||
|
_ctrls.output.deselect()
|
||||||
|
else:
|
||||||
|
_sr.find_next(new_text)
|
||||||
|
|
||||||
|
func _on_SearchTerm_text_entered(new_text):
|
||||||
|
if(Input.is_physical_key_pressed(KEY_SHIFT)):
|
||||||
|
_sr.find_prev(new_text)
|
||||||
|
else:
|
||||||
|
_sr.find_next(new_text)
|
||||||
|
|
||||||
|
func _on_SearchTerm_gui_input(event):
|
||||||
|
if(event is InputEventKey and !event.pressed and event.keycode == KEY_ESCAPE):
|
||||||
|
show_search(false)
|
||||||
|
|
||||||
|
func _on_WordWrap_pressed():
|
||||||
|
if(_ctrls.word_wrap.button_pressed):
|
||||||
|
_ctrls.output.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY
|
||||||
|
else:
|
||||||
|
_ctrls.output.wrap_mode = TextEdit.LINE_WRAPPING_NONE
|
||||||
|
|
||||||
|
_ctrls.output.queue_redraw()
|
||||||
|
|
||||||
|
func _on_settings_pressed():
|
||||||
|
_ctrls.settings_bar.visible = $Toolbar/ShowSettings.button_pressed
|
||||||
|
|
||||||
|
# ------------------
|
||||||
|
# Public
|
||||||
|
# ------------------
|
||||||
|
func show_search(should):
|
||||||
|
_ctrls.search_bar.bar.visible = should
|
||||||
|
if(should):
|
||||||
|
_ctrls.search_bar.search_term.grab_focus()
|
||||||
|
_ctrls.search_bar.search_term.select_all()
|
||||||
|
_ctrls.show_search.button_pressed = should
|
||||||
|
|
||||||
|
|
||||||
|
func search(text, start_pos, highlight=true):
|
||||||
|
return _sr.find_next(text)
|
||||||
|
|
||||||
|
|
||||||
|
func copy_to_clipboard():
|
||||||
|
var selected = _ctrls.output.get_selected_text()
|
||||||
|
if(selected != ''):
|
||||||
|
DisplayServer.clipboard_set(selected)
|
||||||
|
else:
|
||||||
|
DisplayServer.clipboard_set(_ctrls.output.text)
|
||||||
|
|
||||||
|
|
||||||
|
func clear():
|
||||||
|
_ctrls.output.text = ''
|
||||||
|
|
||||||
|
|
||||||
|
func _set_font(custom_name, theme_font_name):
|
||||||
|
var font = GutUtils.gut_fonts.get_font_for_theme_font_name(theme_font_name, custom_name)
|
||||||
|
_ctrls.output.add_theme_font_override(theme_font_name, font)
|
||||||
|
|
||||||
|
|
||||||
|
func set_all_fonts(base_name):
|
||||||
|
_font_name = GutUtils.nvl(base_name, 'Default')
|
||||||
|
|
||||||
|
_set_font(base_name, 'font')
|
||||||
|
_set_font(base_name, 'normal_font')
|
||||||
|
_set_font(base_name, 'bold_font')
|
||||||
|
_set_font(base_name, 'italics_font')
|
||||||
|
_set_font(base_name, 'bold_italics_font')
|
||||||
|
|
||||||
|
|
||||||
|
func set_font_size(new_size):
|
||||||
|
_ctrls.output.set("theme_override_font_sizes/font_size", new_size)
|
||||||
|
|
||||||
|
|
||||||
|
func set_use_colors(value):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
func get_use_colors():
|
||||||
|
return false;
|
||||||
|
|
||||||
|
|
||||||
|
func get_rich_text_edit():
|
||||||
|
return _ctrls.output
|
||||||
|
|
||||||
|
|
||||||
|
func load_file(path):
|
||||||
|
var f = FileAccess.open(path, FileAccess.READ)
|
||||||
|
if(f == null):
|
||||||
|
return
|
||||||
|
|
||||||
|
var t = f.get_as_text()
|
||||||
|
f = null # closes file
|
||||||
|
_ctrls.output.text = t
|
||||||
|
_ctrls.output.scroll_vertical = _ctrls.output.get_line_count()
|
||||||
|
_ctrls.output.set_deferred('scroll_vertical', _ctrls.output.get_line_count())
|
||||||
|
|
||||||
|
|
||||||
|
func add_text(text):
|
||||||
|
if(is_inside_tree()):
|
||||||
|
_ctrls.output.text += text
|
||||||
|
|
||||||
|
|
||||||
|
func scroll_to_line(line):
|
||||||
|
_ctrls.output.scroll_vertical = line
|
||||||
|
_ctrls.output.set_caret_line(line)
|
||||||
1
addons/gut/gui/OutputText.gd.uid
Normal file
1
addons/gut/gui/OutputText.gd.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cax5phqs8acmu
|
||||||
121
addons/gut/gui/OutputText.tscn
Normal file
121
addons/gut/gui/OutputText.tscn
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
[gd_scene load_steps=5 format=3 uid="uid://bqmo4dj64c7yl"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://cax5phqs8acmu" path="res://addons/gut/gui/OutputText.gd" id="1"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://bvo0uao7deu0q" path="res://addons/gut/icon.png" id="2_b4xqv"]
|
||||||
|
|
||||||
|
[sub_resource type="DPITexture" id="DPITexture_lygvu"]
|
||||||
|
_source = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\"><path fill=\"#ff5d5d\" d=\"M2 1v8.586l1.293-1.293a1 1 0 0 1 1.414 0L7 10.587l2.293-2.293a1 1 0 0 1 1.414 0L13 10.586l1-1V6H9V1H2zm8 0v4h4zm-6 9.414-2 2V15h12v-2.586l-.293.293a1 1 0 0 1-1.414 0L10 10.414l-2.293 2.293a1 1 0 0 1-1.414 0L4 10.414z\"/></svg>
|
||||||
|
"
|
||||||
|
|
||||||
|
[sub_resource type="CodeHighlighter" id="CodeHighlighter_8ynmy"]
|
||||||
|
number_color = Color(1, 1, 1, 1)
|
||||||
|
symbol_color = Color(1, 1, 1, 1)
|
||||||
|
function_color = Color(1, 1, 1, 1)
|
||||||
|
member_variable_color = Color(1, 1, 1, 1)
|
||||||
|
keyword_colors = {
|
||||||
|
"ERROR": Color(1, 0, 0, 1),
|
||||||
|
"ExpectedError": Color(0.6784314, 0.84705883, 0.9019608, 1),
|
||||||
|
"Failed": Color(1, 0, 0, 1),
|
||||||
|
"Orphans": Color(1, 1, 0, 1),
|
||||||
|
"Passed": Color(0, 1, 0, 1),
|
||||||
|
"Pending": Color(1, 1, 0, 1),
|
||||||
|
"Risky": Color(1, 1, 0, 1),
|
||||||
|
"WARNING": Color(1, 1, 0, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
[node name="OutputText" type="VBoxContainer"]
|
||||||
|
offset_right = 862.0
|
||||||
|
offset_bottom = 523.0
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
script = ExtResource("1")
|
||||||
|
|
||||||
|
[node name="Toolbar" type="HBoxContainer" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="ShowSearch" type="Button" parent="Toolbar"]
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "Search"
|
||||||
|
toggle_mode = true
|
||||||
|
icon = ExtResource("2_b4xqv")
|
||||||
|
|
||||||
|
[node name="ShowSettings" type="Button" parent="Toolbar"]
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "Settings"
|
||||||
|
toggle_mode = true
|
||||||
|
text = "..."
|
||||||
|
|
||||||
|
[node name="CenterContainer" type="CenterContainer" parent="Toolbar"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="LblPosition" type="Label" parent="Toolbar"]
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="CopyButton" type="Button" parent="Toolbar"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = " Copy "
|
||||||
|
|
||||||
|
[node name="ClearButton" type="Button" parent="Toolbar"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = " Clear "
|
||||||
|
|
||||||
|
[node name="Settings" type="HBoxContainer" parent="."]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="WordWrap" type="Button" parent="Settings"]
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "Word Wrap"
|
||||||
|
toggle_mode = true
|
||||||
|
icon = SubResource("DPITexture_lygvu")
|
||||||
|
|
||||||
|
[node name="UseColors" type="Button" parent="Settings"]
|
||||||
|
layout_mode = 2
|
||||||
|
tooltip_text = "Colorized Text"
|
||||||
|
toggle_mode = true
|
||||||
|
button_pressed = true
|
||||||
|
icon = SubResource("DPITexture_lygvu")
|
||||||
|
|
||||||
|
[node name="Output" type="TextEdit" parent="."]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
theme_override_font_sizes/font_size = 30
|
||||||
|
deselect_on_focus_loss_enabled = false
|
||||||
|
virtual_keyboard_enabled = false
|
||||||
|
middle_mouse_paste_enabled = false
|
||||||
|
scroll_smooth = true
|
||||||
|
syntax_highlighter = SubResource("CodeHighlighter_8ynmy")
|
||||||
|
highlight_all_occurrences = true
|
||||||
|
highlight_current_line = true
|
||||||
|
|
||||||
|
[node name="Search" type="HBoxContainer" parent="."]
|
||||||
|
visible = false
|
||||||
|
layout_mode = 2
|
||||||
|
|
||||||
|
[node name="SearchTerm" type="LineEdit" parent="Search"]
|
||||||
|
layout_mode = 2
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="SearchNext" type="Button" parent="Search"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Next"
|
||||||
|
|
||||||
|
[node name="SearchPrev" type="Button" parent="Search"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "Prev"
|
||||||
|
|
||||||
|
[connection signal="pressed" from="Toolbar/ShowSearch" to="." method="_on_ShowSearch_pressed"]
|
||||||
|
[connection signal="pressed" from="Toolbar/ShowSettings" to="." method="_on_settings_pressed"]
|
||||||
|
[connection signal="pressed" from="Toolbar/CopyButton" to="." method="_on_CopyButton_pressed"]
|
||||||
|
[connection signal="pressed" from="Toolbar/ClearButton" to="." method="_on_ClearButton_pressed"]
|
||||||
|
[connection signal="pressed" from="Settings/WordWrap" to="." method="_on_WordWrap_pressed"]
|
||||||
|
[connection signal="pressed" from="Settings/UseColors" to="." method="_on_UseColors_pressed"]
|
||||||
|
[connection signal="focus_entered" from="Search/SearchTerm" to="." method="_on_SearchTerm_focus_entered"]
|
||||||
|
[connection signal="gui_input" from="Search/SearchTerm" to="." method="_on_SearchTerm_gui_input"]
|
||||||
|
[connection signal="text_changed" from="Search/SearchTerm" to="." method="_on_SearchTerm_text_changed"]
|
||||||
|
[connection signal="text_submitted" from="Search/SearchTerm" to="." method="_on_SearchTerm_text_entered"]
|
||||||
|
[connection signal="pressed" from="Search/SearchNext" to="." method="_on_SearchNext_pressed"]
|
||||||
|
[connection signal="pressed" from="Search/SearchPrev" to="." method="_on_SearchPrev_pressed"]
|
||||||
107
addons/gut/gui/ResizeHandle.gd
Normal file
107
addons/gut/gui/ResizeHandle.gd
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
@tool
|
||||||
|
extends ColorRect
|
||||||
|
# #############################################################################
|
||||||
|
# Resize Handle control. Place onto a control. Set the orientation, then
|
||||||
|
# set the control that this should resize. Then you can resize the control
|
||||||
|
# by dragging this thing around. It's pretty neat.
|
||||||
|
# #############################################################################
|
||||||
|
enum ORIENTATION {
|
||||||
|
LEFT,
|
||||||
|
RIGHT
|
||||||
|
}
|
||||||
|
|
||||||
|
@export var orientation := ORIENTATION.RIGHT :
|
||||||
|
get: return orientation
|
||||||
|
set(val):
|
||||||
|
orientation = val
|
||||||
|
queue_redraw()
|
||||||
|
@export var resize_control : Control = null
|
||||||
|
@export var vertical_resize := true
|
||||||
|
|
||||||
|
var _line_width = .5
|
||||||
|
var _line_color = Color(.4, .4, .4)
|
||||||
|
var _active_line_color = Color(.3, .3, .3)
|
||||||
|
var _invalid_line_color = Color(1, 0, 0)
|
||||||
|
|
||||||
|
var _line_space = 3
|
||||||
|
var _num_lines = 8
|
||||||
|
|
||||||
|
var _mouse_down = false
|
||||||
|
# Called when the node enters the scene tree for the first time.
|
||||||
|
|
||||||
|
|
||||||
|
func _draw():
|
||||||
|
var c = _line_color
|
||||||
|
if(resize_control == null):
|
||||||
|
c = _invalid_line_color
|
||||||
|
elif(_mouse_down):
|
||||||
|
c = _active_line_color
|
||||||
|
|
||||||
|
if(orientation == ORIENTATION.LEFT):
|
||||||
|
_draw_resize_handle_left(c)
|
||||||
|
else:
|
||||||
|
_draw_resize_handle_right(c)
|
||||||
|
|
||||||
|
|
||||||
|
func _gui_input(event):
|
||||||
|
if(resize_control == null):
|
||||||
|
return
|
||||||
|
|
||||||
|
if(orientation == ORIENTATION.LEFT):
|
||||||
|
_handle_left_input(event)
|
||||||
|
else:
|
||||||
|
_handle_right_input(event)
|
||||||
|
|
||||||
|
|
||||||
|
# Draw the lines in the corner to show where you can
|
||||||
|
# drag to resize the dialog
|
||||||
|
func _draw_resize_handle_right(draw_color):
|
||||||
|
var br = size
|
||||||
|
|
||||||
|
for i in range(_num_lines):
|
||||||
|
var start = br - Vector2(i * _line_space, 0)
|
||||||
|
var end = br - Vector2(0, i * _line_space)
|
||||||
|
draw_line(start, end, draw_color, _line_width, true)
|
||||||
|
|
||||||
|
|
||||||
|
func _draw_resize_handle_left(draw_color):
|
||||||
|
var bl = Vector2(0, size.y)
|
||||||
|
|
||||||
|
for i in range(_num_lines):
|
||||||
|
var start = bl + Vector2(i * _line_space, 0)
|
||||||
|
var end = bl - Vector2(0, i * _line_space)
|
||||||
|
draw_line(start, end, draw_color, _line_width, true)
|
||||||
|
|
||||||
|
|
||||||
|
func _handle_right_input(event : InputEvent):
|
||||||
|
if(event is InputEventMouseMotion):
|
||||||
|
if(_mouse_down and
|
||||||
|
event.global_position.x > 0 and
|
||||||
|
event.global_position.y < DisplayServer.window_get_size().y):
|
||||||
|
|
||||||
|
if(vertical_resize):
|
||||||
|
resize_control.size.y += event.relative.y
|
||||||
|
resize_control.size.x += event.relative.x
|
||||||
|
elif(event is InputEventMouseButton):
|
||||||
|
if(event.button_index == MOUSE_BUTTON_LEFT):
|
||||||
|
_mouse_down = event.pressed
|
||||||
|
queue_redraw()
|
||||||
|
|
||||||
|
|
||||||
|
func _handle_left_input(event : InputEvent):
|
||||||
|
if(event is InputEventMouseMotion):
|
||||||
|
if(_mouse_down and
|
||||||
|
event.global_position.x > 0 and
|
||||||
|
event.global_position.y < DisplayServer.window_get_size().y):
|
||||||
|
|
||||||
|
var start_size = resize_control.size
|
||||||
|
resize_control.size.x -= event.relative.x
|
||||||
|
if(resize_control.size.x != start_size.x):
|
||||||
|
resize_control.global_position.x += event.relative.x
|
||||||
|
|
||||||
|
if(vertical_resize):
|
||||||
|
resize_control.size.y += event.relative.y
|
||||||
|
elif(event is InputEventMouseButton):
|
||||||
|
if(event.button_index == MOUSE_BUTTON_LEFT):
|
||||||
|
_mouse_down = event.pressed
|
||||||
|
queue_redraw()
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user