diff --git a/.gitignore b/.gitignore index a58cc1c..f39c8a0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ # Godot-specific ignores *.import export.cfg -export_presets.cfg +export_presets.cfg.bak # Imported translations (automatically generated from CSV files) *.translation @@ -21,15 +21,29 @@ mono_crash.*.json *.swp *.swo Thumbs.db +desktop.ini # Build results builds/ *.exe *.pck - +*.wasm *.dmg *.app +# Web export +web_release/ +web_release.zip + +# Godot executables (too large) +Godot*.exe +Godot*.zip + +# Temporary files +*.tmp +*.bak +*~.gd + # Node.js (for server) node_modules/ npm-debug.log @@ -55,3 +69,8 @@ server/data/*.json # Logs *.log logs/ + +# Server data +server/data/*.json +!server/data/.gitkeep +server/data/backups/ \ No newline at end of file diff --git a/.kiro/specs/multi-client-sync-fix/design.md b/.kiro/specs/multi-client-sync-fix/design.md new file mode 100644 index 0000000..e69de29 diff --git a/.kiro/specs/multi-client-sync-fix/requirements.md b/.kiro/specs/multi-client-sync-fix/requirements.md new file mode 100644 index 0000000..9855634 --- /dev/null +++ b/.kiro/specs/multi-client-sync-fix/requirements.md @@ -0,0 +1,92 @@ +# 需求文档 + +## 简介 + +本文档规定了修复 AI Town 游戏中多客户端同步问题的需求。目前,当多个 Web 客户端同时连接时,存在严重的同步问题:新角色的外观无法正确显示,新创建的角色看不到已存在的角色,角色之间无法相互交流。 + +## 术语表 + +- **客户端(Client)**: 运行游戏的 Web 浏览器实例 +- **角色(Character)**: 游戏世界中由玩家控制的实体 +- **服务器(Server)**: 管理游戏状态的 WebSocket 服务器 +- **世界状态(World State)**: 游戏中所有角色及其属性的完整状态 +- **角色外观(Character Appearance)**: 视觉属性,包括身体颜色、头部颜色和精灵信息 +- **角色状态消息(Character State Message)**: 包含角色信息的网络消息,包括 ID、名称、位置、在线状态和外观数据 +- **世界状态消息(World State Message)**: 包含所有角色及其状态的完整列表的网络消息 +- **广播(Broadcast)**: 向除发送者外的所有已连接客户端发送消息 + +## 需求 + +### 需求 1 + +**用户故事:** 作为玩家,我希望在其他角色加入游戏时看到他们的正确外观,以便我可以在视觉上区分不同的玩家。 + +#### 验收标准 + +1. WHEN 创建新角色时 THEN 服务器 SHALL 向所有已连接的客户端广播包含外观数据的完整角色状态 +2. WHEN 客户端接收到角色状态消息时 THEN 客户端 SHALL 提取外观数据并应用到角色精灵上 +3. WHEN 角色在世界中生成时 THEN 客户端 SHALL 使用外观数据中的正确身体颜色和头部颜色渲染角色 +4. WHEN 角色状态消息中缺少外观数据时 THEN 客户端 SHALL 使用默认外观值 +5. WHILE 角色在世界中可见时 THEN 客户端 SHALL 始终保持角色的外观属性一致 + +### 需求 2 + +**用户故事:** 作为加入游戏的新玩家,我希望看到世界中所有现有的角色,以便我可以与已经在线的其他玩家互动。 + +#### 验收标准 + +1. WHEN 客户端成功认证时 THEN 服务器 SHALL 发送包含所有在线角色的世界状态消息 +2. WHEN 客户端接收到世界状态消息时 THEN 客户端 SHALL 生成或更新消息中列出的所有角色 +3. WHEN 从世界状态生成角色时 THEN 客户端 SHALL 包含完整的角色数据(包括外观) +4. WHEN 角色已在本地存在时 THEN 客户端 SHALL 更新现有角色而不是创建重复角色 +5. WHILE 处理世界状态时 THEN 客户端 SHALL 从远程角色生成中排除玩家自己的角色 + +### 需求 3 + +**用户故事:** 作为玩家,我希望向其他玩家发送消息并接收他们的消息,以便我可以在游戏世界中进行交流和互动。 + +#### 验收标准 + +1. WHEN 玩家发送对话消息时 THEN 服务器 SHALL 向所有已连接的客户端广播该消息 +2. WHEN 客户端接收到对话消息时 THEN 客户端 SHALL 在适当的 UI 元素中显示该消息 +3. WHEN 显示对话消息时 THEN 客户端 SHALL 显示发送者的角色名称 +4. WHEN 消息发送给特定角色时 THEN 服务器 SHALL 仅将消息传递给该角色的客户端 +5. WHILE 对话系统处于活动状态时 THEN 客户端 SHALL 为每个对话维护消息历史记录 + +### 需求 4 + +**用户故事:** 作为开发者,我希望服务器在所有状态消息中包含完整的角色数据,以便客户端拥有正确渲染角色所需的所有信息。 + +#### 验收标准 + +1. WHEN 创建角色状态消息时 THEN 服务器 SHALL 包含 id、name、position、isOnline 和 appearance 字段 +2. WHEN 广播角色状态时 THEN 服务器 SHALL 序列化完整的角色对象(包括嵌套的外观数据) +3. WHEN 角色外观发生变化时 THEN 服务器 SHALL 向所有客户端广播更新的外观 +4. WHEN 从磁盘加载角色时 THEN 服务器 SHALL 保留所有外观数据 +5. WHILE 维护角色状态时 THEN 服务器 SHALL 确保外观数据在保存和加载操作中的完整性 + +### 需求 5 + +**用户故事:** 作为玩家,我希望角色同步在多个客户端之间可靠工作,以便所有玩家的游戏状态保持一致。 + +#### 验收标准 + +1. WHEN 角色移动时 THEN 服务器 SHALL 向所有其他客户端广播位置更新 +2. WHEN 角色上线时 THEN 服务器 SHALL 向所有客户端广播在线状态 +3. WHEN 角色下线时 THEN 服务器 SHALL 向所有客户端广播离线状态 +4. WHEN 接收到角色状态更新时 THEN 客户端 SHALL 仅将更新应用于远程角色而不是玩家自己的角色 +5. WHILE 多个客户端连接时 THEN 服务器 SHALL 在所有客户端之间维护一致的状态 + +### 需求 6 + +**用户故事:** 作为开发者,我希望所有新的同步功能与现有系统逻辑完美融合,以便系统保持稳定且不引入新的错误。 + +#### 验收标准 + +1. WHEN 实现新的同步逻辑时 THEN 系统 SHALL 保持与现有网络管理器的兼容性 +2. WHEN 修改角色状态广播时 THEN 系统 SHALL 保持与现有 MessageProtocol 的消息格式一致性 +3. WHEN 更新世界管理器时 THEN 系统 SHALL 保持与现有角色生成和移除逻辑的兼容性 +4. WHEN 处理角色外观数据时 THEN 系统 SHALL 保持与现有 CharacterData 和 CharacterController 的数据结构一致性 +5. WHEN 添加新的消息处理逻辑时 THEN 系统 SHALL 不破坏现有的安全验证、速率限制和错误处理机制 +6. WHEN 修改客户端同步逻辑时 THEN 系统 SHALL 保持与现有游戏状态管理器的状态转换逻辑一致性 +7. WHILE 实现多客户端同步时 THEN 系统 SHALL 确保不与现有的单客户端功能(如玩家移动、对话系统)产生冲突 diff --git a/CAMERA_CONTROLS.md b/CAMERA_CONTROLS.md deleted file mode 100644 index 6cb3106..0000000 --- a/CAMERA_CONTROLS.md +++ /dev/null @@ -1,152 +0,0 @@ -# 相机控制说明 - -## 🎮 如何在运行的游戏中移动相机 - -我已经添加了调试相机控制功能,现在你可以在运行场景时自由移动相机查看整个办公室了! - -## ⌨️ 控制方式 - -### 移动相机 -- **WASD** 或 **方向键** - 上下左右移动相机 - - W / ↑ - 向上移动 - - S / ↓ - 向下移动 - - A / ← - 向左移动 - - D / → - 向右移动 - -### 缩放相机 -- **Q** - 缩小(看到更多场景) -- **E** - 放大(看到更多细节) -- **鼠标滚轮** - 上滚放大,下滚缩小 - -### 重置相机 -- **R** - 重置相机到初始位置和缩放 - -## 🗺️ 场景导览 - -使用相机控制,你可以查看所有 4 个 Logo 位置: - -### 1. 欢迎标识(已经看到) -- **位置**: 左上角 -- **操作**: 初始位置就能看到 -- ✅ 你已经在截图中看到了这个 - -### 2. 主展示区 -- **位置**: 右侧中央 -- **操作**: 按 **D** 或 **→** 向右移动相机 -- 📍 坐标约 (1400, 400) - -### 3. 成就墙 -- **位置**: 右下方 -- **操作**: 按 **D** 向右,然后按 **S** 向下 -- 📍 坐标约 (1200, 900) - -### 4. 地板水印 -- **位置**: 场景中央地板 -- **操作**: 移动到场景中央,可能需要缩小(按 **Q**)才能看清 -- 📍 坐标约 (1000, 700) - -## 🎯 推荐查看路线 - -1. **起点**(当前位置)- 欢迎标识 ✅ -2. 按 **D** 向右移动 → 看到主展示区的大 Logo -3. 按 **S** 向下移动 → 看到成就墙顶部的 Logo -4. 按 **Q** 缩小视图 → 看到地板中央的淡水印 -5. 按 **R** 重置 → 回到起点 - -## 📊 场景布局示意 - -``` -┌─────────────────────────────────────────────┐ -│ [欢迎标识+Logo] │ ← 你在这里 -│ ↓ 按 S │ -│ 入口区 │ -│ ┌─┐ │ -│ └─┘ │ -│ │ -│ 工作区 按 D → 展示区 │ -│ ┌─┐┌─┐┌─┐ ┌──────┐ │ -│ └─┘└─┘└─┘ │ LOGO │ ← 2 │ -│ ┌─┐┌─┐┌─┐ └──────┘ │ -│ └─┘└─┘└─┘ │ -│ │ -│ [地板水印] 成就墙 │ -│ 会议区 ↑ 4 ┌──────┐ │ -│ ┌────┐ 按 Q 缩小 │ LOGO │ ← 3 │ -│ │ │ │ 成就 │ │ -│ └────┘ └──────┘ │ -│ │ -│ 休息区 │ -│ ┌──┐ │ -│ └──┘ │ -└─────────────────────────────────────────────┘ -``` - -## 🔧 测试步骤 - -1. **重新运行场景** - - 停止当前运行(如果还在运行) - - 按 F6 重新运行 DatawhaleOffice.tscn - -2. **查看控制台** - - 应该看到 "Debug camera controls enabled" 消息 - -3. **测试移动** - - 按 WASD 或方向键移动 - - 相机应该平滑移动 - -4. **查看所有 Logo** - - 按照上面的路线查看 4 个位置 - -## 💡 提示 - -- **移动速度**: 500 像素/秒,可以快速浏览场景 -- **缩放范围**: 0.3x 到 2.0x -- **平滑移动**: 相机移动是平滑的,不会突然跳跃 -- **边界限制**: 相机不会移出场景边界(0-2000, 0-1500) - -## 🎨 查看 Logo 的最佳方式 - -### 主展示区 Logo(最重要) -``` -1. 按 D 向右移动约 3-4 秒 -2. 应该能看到白色背景板 + 蓝色边框 + 大 Logo -3. 这是最显眼的 Logo 展示 -``` - -### 成就墙 Logo -``` -1. 从主展示区,按 S 向下移动约 2-3 秒 -2. 或按 D 向右 + S 向下 -3. 应该能看到成就墙顶部的 Logo -``` - -### 地板水印 -``` -1. 按 R 重置到中央 -2. 按 Q 缩小视图(多按几次) -3. 应该能看到淡淡的大 Logo 水印 -``` - -## ❓ 常见问题 - -**Q: 按键没反应?** -A: 确保游戏窗口是激活状态(点击一下窗口) - -**Q: 移动太快/太慢?** -A: 可以在 `scripts/DebugCamera.gd` 中调整 `move_speed` 值 - -**Q: 看不到某个 Logo?** -A: 尝试缩小视图(按 Q)或移动到不同位置 - -**Q: 想回到起点?** -A: 按 R 键重置相机 - -## 🚀 准备好了吗? - -现在重新运行场景(F6),然后: -1. 按 **D** 向右移动,查看主展示区的大 Logo -2. 按 **S** 向下移动,查看成就墙的 Logo -3. 按 **Q** 缩小,查看地板水印 -4. 按 **R** 重置 - -享受探索你的 Datawhale 办公室吧!🎉 diff --git a/CODING_STYLE.md b/CODING_STYLE.md deleted file mode 100644 index 3118d84..0000000 --- a/CODING_STYLE.md +++ /dev/null @@ -1,369 +0,0 @@ -# 代码风格指南 - -## 概述 - -本文档定义了 AI Town Game 项目的代码风格和最佳实践。遵循这些规范有助于保持代码的一致性、可读性和可维护性。 - -## 命名规范 - -### 变量和函数 -- 使用 `snake_case` 命名法 -- 变量名应该描述性强,避免缩写 -- 布尔变量使用 `is_`, `has_`, `can_` 等前缀 - -```gdscript -# 好的例子 -var player_character: CharacterController -var is_connected: bool -var has_valid_data: bool - -# 避免的例子 -var pc: CharacterController -var conn: bool -var data: bool -``` - -### 类和枚举 -- 使用 `PascalCase` 命名法 -- 类名应该是名词 -- 枚举值使用 `UPPER_CASE` - -```gdscript -# 类名 -class_name NetworkManager -class_name CharacterController - -# 枚举 -enum GameState { - LOGIN, - CHARACTER_CREATION, - IN_GAME -} -``` - -### 常量 -- 使用 `UPPER_CASE` 命名法 -- 相关常量可以组织在字典中 - -```gdscript -const MAX_PLAYERS = 50 -const DEFAULT_TIMEOUT = 10.0 - -const COLORS = { - "online": Color.GREEN, - "offline": Color.GRAY -} -``` - -### 信号 -- 使用 `snake_case` 命名法 -- 使用过去时态描述已发生的事件 - -```gdscript -signal character_spawned(character_id: String) -signal connection_established() -signal message_received(data: Dictionary) -``` - -## 代码组织 - -### 文件结构 -每个脚本文件应该按以下顺序组织: - -1. `extends` 和 `class_name` 声明 -2. 类文档注释 -3. 信号定义 -4. 常量定义 -5. 导出变量 -6. 公共变量 -7. 私有变量 -8. 内置函数(`_ready`, `_process` 等) -9. 公共函数 -10. 私有函数 - -```gdscript -extends Node -class_name ExampleClass -## 示例类 -## 展示代码组织结构 - -# 信号 -signal data_changed(new_data: Dictionary) - -# 常量 -const MAX_ITEMS = 100 - -# 导出变量 -@export var item_count: int = 0 - -# 公共变量 -var current_state: GameState - -# 私有变量 -var _internal_data: Dictionary = {} - -func _ready(): - """初始化函数""" - pass - -func public_function() -> void: - """公共函数""" - pass - -func _private_function() -> void: - """私有函数""" - pass -``` - -### 函数组织 -- 相关功能的函数应该放在一起 -- 使用注释分隔不同的功能区域 -- 私有函数以下划线开头 - -```gdscript -## === 网络相关函数 === - -func connect_to_server() -> void: - """连接到服务器""" - pass - -func disconnect_from_server() -> void: - """断开服务器连接""" - pass - -func _handle_network_error() -> void: - """处理网络错误(私有函数)""" - pass - -## === UI相关函数 === - -func show_notification() -> void: - """显示通知""" - pass -``` - -## 注释和文档 - -### 类文档 -每个类都应该有文档注释,说明其用途和职责: - -```gdscript -extends Node -class_name NetworkManager -## 网络管理器 -## 负责管理客户端与服务器的 WebSocket 连接 -## -## 主要功能: -## - 建立和维护网络连接 -## - 处理消息收发 -## - 管理重连逻辑 -``` - -### 函数文档 -公共函数应该有详细的文档注释: - -```gdscript -func spawn_character(character_data: Dictionary, is_player: bool = false) -> CharacterController: - """ - 在世界中生成角色 - - @param character_data: 角色数据字典,必须包含 id 和 name 字段 - @param is_player: 是否为玩家角色,默认为 false - @return: 生成的角色控制器实例,失败时返回 null - - 示例: - var data = {"id": "char_123", "name": "Hero"} - var character = spawn_character(data, true) - """ - pass -``` - -### 行内注释 -- 解释复杂的逻辑 -- 说明为什么这样做,而不是做了什么 -- 避免显而易见的注释 - -```gdscript -# 好的注释 - 解释原因 -# 使用指数退避算法避免服务器过载 -_reconnect_timer = _reconnect_delay * pow(2, _reconnect_attempts) - -# 避免的注释 - 重复代码 -# 设置重连计时器为延迟时间 -_reconnect_timer = _reconnect_delay -``` - -## 错误处理 - -### 使用统一的错误处理 -使用项目的 `ErrorHandler` 类记录错误: - -```gdscript -# 记录网络错误 -ErrorHandler.log_network_error("Connection failed", {"url": server_url}) - -# 记录游戏逻辑错误 -ErrorHandler.log_game_error("Invalid character data", {"character_id": char_id}) -``` - -### 防御性编程 -- 验证输入参数 -- 检查空引用 -- 处理边界情况 - -```gdscript -func update_character_position(character_id: String, position: Vector2) -> void: - """更新角色位置""" - # 验证输入 - if character_id.is_empty(): - ErrorHandler.log_game_error("Empty character ID provided") - return - - # 检查角色是否存在 - if not characters.has(character_id): - ErrorHandler.log_game_error("Character not found", {"id": character_id}) - return - - # 执行更新 - var character = characters[character_id] - character.set_position_smooth(position) -``` - -## 性能最佳实践 - -### 避免不必要的计算 -```gdscript -# 好的做法 - 缓存计算结果 -var distance_squared = position.distance_squared_to(target) -if distance_squared < interaction_range_squared: - # 执行交互 - -# 避免的做法 - 重复计算 -if position.distance_to(target) < interaction_range: - # 执行交互 -``` - -### 使用对象池 -对于频繁创建和销毁的对象,考虑使用对象池: - -```gdscript -# 从对象池获取对象而不是创建新对象 -var bullet = BulletPool.get_bullet() -bullet.initialize(position, direction) -``` - -### 合理使用信号 -- 避免在每帧都发射信号 -- 使用一次性连接(`connect(..., CONNECT_ONE_SHOT)`)当适用时 - -## 测试 - -### 测试命名 -测试函数应该清楚地描述测试内容: - -```gdscript -func test_character_creation_with_valid_data(): - """测试使用有效数据创建角色""" - pass - -func test_network_connection_timeout(): - """测试网络连接超时处理""" - pass -``` - -### 属性测试标注 -属性测试必须包含特定的注释格式: - -```gdscript -## Feature: godot-ai-town-game, Property 1: 角色创建唯一性 -func test_property_character_id_uniqueness(): - """ - 属性测试:角色创建唯一性 - 对于任意两个成功创建的角色,它们的角色 ID 应该是唯一的 - """ - pass -``` - -## 配置管理 - -### 使用配置类 -避免硬编码常量,使用 `GameConfig` 类: - -```gdscript -# 好的做法 -var move_speed = GameConfig.get_character_move_speed() -var timeout = GameConfig.NETWORK.connection_timeout - -# 避免的做法 -var move_speed = 200.0 -var timeout = 10.0 -``` - -## Git 提交规范 - -### 提交消息格式 -``` -(): - -[optional body] - -[optional footer] -``` - -### 类型 -- `feat`: 新功能 -- `fix`: 修复bug -- `refactor`: 重构代码 -- `docs`: 文档更新 -- `test`: 测试相关 -- `style`: 代码格式调整 -- `perf`: 性能优化 - -### 示例 -``` -feat(network): add automatic reconnection logic - -Implement exponential backoff algorithm for reconnection attempts. -Maximum 3 attempts with increasing delay between attempts. - -Closes #123 -``` - -## 工具使用 - -### 使用工具类 -项目提供了多个工具类,应该优先使用: - -```gdscript -# 使用 Utils 类的工具函数 -if Utils.is_string_blank(character_name): - return false - -var unique_id = Utils.generate_unique_id("char_") -var label = Utils.create_label_with_shadow("Player Name") - -# 使用深度比较 -if Utils.deep_equals(data1, data2): - # 数据相同 -``` - -### 性能监控 -在关键路径上使用性能监控: - -```gdscript -# 记录网络延迟 -var start_time = Time.get_ticks_msec() -# ... 网络操作 ... -var latency = Time.get_ticks_msec() - start_time -PerformanceMonitor.record_network_latency(latency) -``` - -## 总结 - -遵循这些代码风格指南将有助于: -- 提高代码可读性和可维护性 -- 减少bug和错误 -- 提升团队协作效率 -- 保持项目的长期健康发展 - -所有团队成员都应该熟悉并遵循这些规范。在代码审查时,这些规范也是重要的检查点。 \ No newline at end of file diff --git a/COMPATIBILITY_TEST.md b/COMPATIBILITY_TEST.md deleted file mode 100644 index 58eb4aa..0000000 --- a/COMPATIBILITY_TEST.md +++ /dev/null @@ -1,295 +0,0 @@ -# 跨平台兼容性测试报告 - -## 🎯 测试目标 - -验证 AI Town Game 在不同平台、浏览器和设备上的兼容性,确保用户在各种环境下都能获得一致的游戏体验。 - -## 📱 测试平台 - -### 桌面平台 -- **Windows 10/11** (x64) -- **macOS 12+ Monterey** (Intel & Apple Silicon) -- **Ubuntu 20.04/22.04 LTS** (x64) - -### Web 浏览器 -- **Google Chrome** 120+ -- **Mozilla Firefox** 121+ -- **Safari** 17+ (macOS) -- **Microsoft Edge** 120+ - -### 移动设备 (Web) -- **iOS Safari** (iPhone/iPad) -- **Android Chrome** (各厂商设备) - -## ✅ 兼容性测试结果 - -### 1. Windows 平台测试 - -#### Windows 11 Pro (x64) ✅ PASSED -- **Godot 版本**: 4.5.1 stable -- **测试结果**: - - 游戏启动: ✅ 正常 (2.8 秒) - - 场景加载: ✅ 正常 (1.2 秒) - - 角色移动: ✅ 流畅 (60 FPS) - - 网络连接: ✅ 稳定 - - 音频播放: ✅ 正常 - - 内存使用: ✅ 82 MB - - CPU 使用: ✅ 12% - -#### Windows 10 Home (x64) ✅ PASSED -- **测试结果**: 与 Windows 11 表现一致 -- **特殊注意**: DirectX 11 兼容性良好 - -### 2. macOS 平台测试 - -#### macOS 13 Ventura (Intel) ✅ PASSED -- **测试结果**: - - 游戏启动: ✅ 正常 (3.1 秒) - - 场景加载: ✅ 正常 (1.4 秒) - - 角色移动: ✅ 流畅 (60 FPS) - - 网络连接: ✅ 稳定 - - 内存使用: ✅ 88 MB - - CPU 使用: ✅ 15% - -#### macOS 14 Sonoma (Apple Silicon) ✅ PASSED -- **测试结果**: - - 游戏启动: ✅ 正常 (2.5 秒) - - 性能表现: ✅ 优秀 (M1/M2 优化良好) - - 电池续航: ✅ 影响较小 - -### 3. Linux 平台测试 - -#### Ubuntu 22.04 LTS ✅ PASSED -- **测试结果**: - - 游戏启动: ✅ 正常 (3.5 秒) - - 场景加载: ✅ 正常 (1.6 秒) - - 角色移动: ✅ 流畅 (55+ FPS) - - 网络连接: ✅ 稳定 - - 依赖库: ✅ 无额外依赖需求 - -#### Fedora 39 ✅ PASSED -- **测试结果**: 与 Ubuntu 表现一致 -- **包管理**: DNF 安装依赖正常 - -## 🌐 Web 浏览器兼容性 - -### Chrome 120+ ✅ PASSED -- **WebGL 支持**: ✅ 完全支持 -- **WebSocket**: ✅ 连接稳定 -- **性能表现**: ✅ 优秀 (60 FPS) -- **内存使用**: ✅ 95 MB -- **加载时间**: ✅ 4.2 秒 - -**测试功能**: -- [x] 游戏加载和初始化 -- [x] 角色创建和移动 -- [x] 网络通信 -- [x] 音频播放 -- [x] 全屏模式 -- [x] 键盘输入 -- [x] 鼠标交互 - -### Firefox 121+ ✅ PASSED -- **WebGL 支持**: ✅ 完全支持 -- **WebSocket**: ✅ 连接稳定 -- **性能表现**: ✅ 良好 (55+ FPS) -- **内存使用**: ✅ 102 MB -- **加载时间**: ✅ 4.8 秒 - -**特殊注意**: -- Firefox 的 WebGL 实现略有差异,但不影响游戏体验 -- 内存使用稍高,但在可接受范围内 - -### Safari 17+ ✅ PASSED -- **WebGL 支持**: ✅ 支持良好 -- **WebSocket**: ✅ 连接稳定 -- **性能表现**: ✅ 良好 (50+ FPS) -- **内存使用**: ✅ 89 MB -- **加载时间**: ✅ 5.1 秒 - -**Safari 特殊处理**: -- 音频自动播放策略需要用户交互 -- WebGL 上下文创建稍慢 -- 整体兼容性良好 - -### Edge 120+ ✅ PASSED -- **WebGL 支持**: ✅ 完全支持 -- **WebSocket**: ✅ 连接稳定 -- **性能表现**: ✅ 优秀 (58+ FPS) -- **内存使用**: ✅ 91 MB -- **加载时间**: ✅ 4.5 秒 - -**测试结果**: 与 Chrome 表现几乎一致 - -## 📱 移动设备兼容性 - -### iOS 设备测试 - -#### iPhone 14 Pro (iOS 17) ✅ PASSED -- **Safari 浏览器**: 完全兼容 -- **触摸控制**: ✅ 虚拟摇杆响应良好 -- **性能表现**: ✅ 流畅 (60 FPS) -- **电池消耗**: ✅ 合理 -- **网络连接**: ✅ WiFi/5G 都稳定 - -#### iPad Air (iOS 16) ✅ PASSED -- **屏幕适配**: ✅ 自动适配平板分辨率 -- **触摸体验**: ✅ 大屏幕操作舒适 -- **性能表现**: ✅ 优秀 - -### Android 设备测试 - -#### Samsung Galaxy S23 ✅ PASSED -- **Chrome 浏览器**: 完全兼容 -- **触摸控制**: ✅ 响应准确 -- **性能表现**: ✅ 流畅 (55+ FPS) -- **发热控制**: ✅ 温度正常 - -#### Google Pixel 7 ✅ PASSED -- **原生 Android**: 兼容性优秀 -- **性能表现**: ✅ 稳定流畅 - -## 🔧 分辨率适配测试 - -### 常见分辨率支持 - -#### 桌面分辨率 ✅ PASSED -- **1920x1080 (Full HD)**: ✅ 完美显示 -- **2560x1440 (2K)**: ✅ 高清显示 -- **3840x2160 (4K)**: ✅ 超清显示 -- **1366x768**: ✅ 自动缩放适配 -- **1280x720**: ✅ 自动缩放适配 - -#### 移动设备分辨率 ✅ PASSED -- **iPhone 分辨率**: ✅ 完美适配 -- **Android 各种分辨率**: ✅ 自动适配 -- **平板分辨率**: ✅ 优化显示 - -### UI 响应式测试 ✅ PASSED -- **按钮大小**: 自动调整适合触摸 -- **文字大小**: 根据屏幕 DPI 调整 -- **布局适配**: 保持比例和可用性 -- **虚拟控件**: 移动端自动显示 - -## 🎮 输入设备兼容性 - -### 键盘输入 ✅ PASSED -- **QWERTY 布局**: ✅ 完全支持 -- **AZERTY 布局**: ✅ 支持 (法语) -- **其他布局**: ✅ 基本支持 -- **功能键**: ✅ ESC、Enter 等正常 - -### 鼠标输入 ✅ PASSED -- **左键点击**: ✅ 正常 -- **右键菜单**: ✅ 正常 -- **滚轮缩放**: ✅ 正常 -- **拖拽操作**: ✅ 正常 - -### 触摸输入 ✅ PASSED -- **单点触摸**: ✅ 准确响应 -- **多点触摸**: ✅ 支持缩放 -- **手势识别**: ✅ 基本手势支持 -- **虚拟摇杆**: ✅ 响应灵敏 - -### 游戏手柄 ⚠️ 部分支持 -- **Xbox 控制器**: ✅ 基本支持 -- **PlayStation 控制器**: ✅ 基本支持 -- **注意**: 需要额外配置,非核心功能 - -## 🌍 网络环境测试 - -### 网络连接类型 ✅ PASSED -- **有线网络**: ✅ 最佳性能 -- **WiFi 连接**: ✅ 稳定连接 -- **移动网络 (4G/5G)**: ✅ 可用,延迟稍高 -- **低带宽网络**: ✅ 可用,加载较慢 - -### 网络延迟测试 ✅ PASSED -- **本地网络 (<10ms)**: ✅ 完美体验 -- **城域网 (10-50ms)**: ✅ 良好体验 -- **广域网 (50-100ms)**: ✅ 可接受 -- **高延迟 (>100ms)**: ⚠️ 体验下降但可用 - -## 🔒 安全兼容性 - -### HTTPS 支持 ✅ PASSED -- **SSL/TLS 连接**: ✅ 完全支持 -- **混合内容**: ✅ 无安全警告 -- **证书验证**: ✅ 正常 - -### 内容安全策略 ✅ PASSED -- **CSP 兼容**: ✅ 符合安全策略 -- **XSS 防护**: ✅ 无安全漏洞 -- **CORS 配置**: ✅ 正确配置 - -## 📊 性能基准测试 - -### 不同平台性能对比 - -| 平台 | 启动时间 | 帧率 | 内存使用 | CPU 使用 | -|------|----------|------|----------|----------| -| Windows 11 | 2.8s | 60 FPS | 82 MB | 12% | -| macOS 13 | 3.1s | 60 FPS | 88 MB | 15% | -| Ubuntu 22.04 | 3.5s | 55 FPS | 91 MB | 18% | -| Chrome (Web) | 4.2s | 60 FPS | 95 MB | 20% | -| Firefox (Web) | 4.8s | 55 FPS | 102 MB | 22% | -| Safari (Web) | 5.1s | 50 FPS | 89 MB | 25% | -| iOS Safari | 5.5s | 60 FPS | 78 MB | N/A | -| Android Chrome | 6.2s | 55 FPS | 85 MB | N/A | - -### 性能等级评定 -- **优秀** (>55 FPS, <100 MB): Windows, macOS, Chrome, iOS -- **良好** (>45 FPS, <120 MB): Linux, Firefox, Android -- **可用** (>30 FPS, <150 MB): 所有测试平台 - -## 🐛 已知兼容性问题 - -### 已解决问题 -1. **Safari 音频延迟** - 已通过用户交互触发解决 -2. **Firefox WebGL 上下文** - 已优化初始化流程 -3. **移动端触摸精度** - 已调整触摸区域大小 - -### 当前限制 -1. **游戏手柄支持** - 需要手动配置,非核心功能 -2. **IE 浏览器** - 不支持,已过时浏览器 -3. **Android 4.x** - 不支持,系统版本过低 - -### 建议的最低要求 -- **桌面**: Windows 10, macOS 12, Ubuntu 20.04 -- **浏览器**: Chrome 100+, Firefox 100+, Safari 15+, Edge 100+ -- **移动**: iOS 15+, Android 8.0+ -- **硬件**: 2GB RAM, 集成显卡, 1GB 存储空间 - -## ✅ 兼容性测试结论 - -### 总体兼容性: 🟢 优秀 - -**支持平台覆盖率**: 95%+ -**主流浏览器兼容**: 100% -**移动设备支持**: 90%+ -**性能表现**: 优秀到良好 - -### 发布建议: ✅ 推荐发布 - -AI Town Game 在跨平台兼容性方面表现优秀: - -1. **广泛兼容**: 支持所有主流平台和浏览器 -2. **性能稳定**: 在各平台都有良好的性能表现 -3. **用户体验**: 在不同设备上提供一致的体验 -4. **技术先进**: 充分利用现代 Web 技术 - -### 部署建议 - -1. **优先支持**: Chrome, Firefox, Safari, Edge 最新版本 -2. **移动优化**: 重点优化 iOS Safari 和 Android Chrome -3. **性能监控**: 部署后持续监控各平台性能表现 -4. **用户反馈**: 收集不同平台用户的使用反馈 - ---- - -**测试完成日期**: 2024年12月5日 -**测试覆盖平台**: 8 个主要平台 -**测试设备数量**: 12 台设备 -**兼容性评级**: A+ (优秀) - -此报告确认 AI Town Game 具有出色的跨平台兼容性,可以安全发布到生产环境。 \ No newline at end of file diff --git a/DEMO_GUIDE.md b/DEMO_GUIDE.md deleted file mode 100644 index 0b41d85..0000000 --- a/DEMO_GUIDE.md +++ /dev/null @@ -1,249 +0,0 @@ -# AI Town Game 项目演示指南 - -## 🎯 演示概述 - -本指南帮助您快速展示 AI Town Game 的核心功能和技术特性,适用于项目演示、技术分享和功能验证。 - -## 🚀 快速演示流程 - -### 准备工作 (5 分钟) - -1. **启动服务器** -```bash -cd server -yarn dev -# 等待看到 "🚀 Server started on port 8080" -``` - -2. **打开 Godot 项目** -- 启动 Godot 4.5.1 -- 导入项目 (选择 `project.godot`) -- 等待项目加载完成 - -3. **验证环境** -- 打开 `scenes/TestGameplay.tscn` -- 按 F6 运行场景 -- 确认角色可以移动,场景正常显示 - -### 核心功能演示 (15 分钟) - -#### 1. 游戏场景展示 (3 分钟) - -**演示要点**: -- Datawhale 办公室场景设计 -- 品牌元素集成 (Logo、色彩方案) -- 场景布局 (入口、工作区、会议区、休息区、展示区) - -**操作步骤**: -1. 运行 `scenes/DatawhaleOffice.tscn` -2. 使用相机控制查看整个场景: - - WASD 移动相机 - - 鼠标滚轮缩放 - - R 键重置视图 -3. 指出各个功能区域和品牌元素 - -#### 2. 角色系统演示 (4 分钟) - -**演示要点**: -- 角色移动和动画 -- 碰撞检测 -- 相机跟随 - -**操作步骤**: -1. 运行 `scenes/TestGameplay.tscn` -2. 使用 WASD 移动角色 -3. 展示碰撞检测 (角色无法穿墙) -4. 展示相机跟随效果 -5. 展示角色动画 (行走/静止) - -#### 3. 网络系统演示 (4 分钟) - -**演示要点**: -- 客户端-服务器连接 -- 实时数据同步 -- 多客户端支持 - -**操作步骤**: -1. 运行主场景 `scenes/Main.tscn` -2. 展示登录流程 -3. 创建角色 -4. 展示网络连接状态 -5. 如有条件,开启第二个客户端展示多人互动 - -#### 4. 测试系统演示 (4 分钟) - -**演示要点**: -- 自动化测试覆盖 -- 属性测试 (Property-Based Testing) -- 测试结果展示 - -**操作步骤**: -1. 运行 `tests/RunAllTests.tscn` -2. 展示测试执行过程 -3. 解释测试覆盖范围: - - 5 个测试套件 - - 18 个单元测试 - - 6 个属性测试 - - 600+ 次测试迭代 -4. 展示测试通过结果 - -### 技术特性展示 (10 分钟) - -#### 1. 架构设计 (3 分钟) - -**展示内容**: -- 客户端-服务器架构图 -- 模块化设计 -- 组件职责分离 - -**演示方式**: -- 打开 `DEVELOPER_GUIDE.md` 展示架构图 -- 简要介绍各个组件的职责 -- 展示代码组织结构 - -#### 2. 数据持久化 (2 分钟) - -**展示内容**: -- JSON 数据存储 -- 自动备份机制 -- 数据恢复功能 - -**演示方式**: -- 展示 `server/data/characters.json` 文件 -- 展示备份目录结构 -- 演示数据保存和加载 - -#### 3. 监控和管理 (3 分钟) - -**展示内容**: -- Web 管理界面 -- 系统监控 -- 日志管理 - -**演示方式**: -- 访问 `http://localhost:8081/admin/` -- 展示系统状态监控 -- 展示日志分析功能 - -#### 4. 跨平台支持 (2 分钟) - -**展示内容**: -- Web 导出功能 -- 响应式 UI 设计 -- 移动端适配 - -**演示方式**: -- 展示 Godot 导出设置 -- 如有条件,展示 Web 版本运行 -- 展示 UI 在不同分辨率下的适配 - -## 🎨 演示脚本 - -### 开场介绍 (2 分钟) - -"大家好,今天我要演示的是 AI Town Game,这是一款基于 Godot 引擎开发的 2D 多人在线游戏。项目的核心特色是: - -1. **多人在线互动** - 支持实时多人游戏 -2. **持久化世界** - 角色在玩家离线时仍作为 NPC 存在 -3. **品牌场景** - 精心设计的 Datawhale 办公室环境 -4. **跨平台支持** - 支持 Web 和桌面平台 -5. **完整测试** - 包含单元测试和属性测试 - -让我们开始演示..." - -### 场景展示脚本 (3 分钟) - -"首先看到的是我们的主要游戏场景 - Datawhale 办公室。这个场景包含了: - -- **入口区域** - 带有欢迎标识的门厅 -- **工作区** - 配有办公桌和电脑的工作空间 -- **会议区** - 用于团队讨论的会议室 -- **休息区** - 放松交流的休闲空间 -- **展示区** - 展示 Datawhale 品牌和成就 - -注意场景中的品牌元素,包括 Datawhale Logo 和统一的蓝色配色方案..." - -### 技术演示脚本 (8 分钟) - -"现在让我展示游戏的技术实现: - -**角色系统**:角色可以自由移动,具有完整的碰撞检测。相机会智能跟随角色,提供流畅的游戏体验。 - -**网络系统**:游戏使用 WebSocket 实现实时通信。客户端和服务器之间保持持续连接,确保数据同步。 - -**测试系统**:项目包含完整的测试套件,包括传统的单元测试和先进的属性测试。属性测试通过生成随机数据来验证系统的正确性属性。 - -**数据管理**:所有游戏数据都持久化存储,支持自动备份和恢复。系统还提供了 Web 管理界面用于监控和维护..." - -### 结尾总结 (2 分钟) - -"通过这次演示,我们看到了 AI Town Game 的主要特性: - -1. **完整的游戏功能** - 从角色创建到多人互动 -2. **稳定的技术架构** - 模块化设计,易于扩展 -3. **全面的测试覆盖** - 确保代码质量和系统稳定性 -4. **专业的运维支持** - 监控、备份、日志管理 - -这个项目展示了现代游戏开发的最佳实践,包括测试驱动开发、持续集成和自动化运维。 - -谢谢大家,有什么问题欢迎提问!" - -## 🔧 演示准备清单 - -### 环境检查 -- [ ] Godot 4.5.1 已安装并可正常运行 -- [ ] Node.js 和 Yarn 已安装 -- [ ] 项目代码已下载并配置完成 -- [ ] 服务器可以正常启动 -- [ ] 所有测试都能通过 - -### 演示材料 -- [ ] 项目架构图 (可打印或投影) -- [ ] 功能特性列表 -- [ ] 技术栈说明 -- [ ] 演示脚本备份 - -### 备用方案 -- [ ] 录制好的演示视频 (网络问题时使用) -- [ ] 静态截图集合 (设备问题时使用) -- [ ] 离线版本演示 (服务器问题时使用) - -## 🎯 不同场景的演示重点 - -### 技术分享会 -- 重点展示架构设计和技术实现 -- 详细介绍测试框架和开发流程 -- 分享开发过程中的技术挑战和解决方案 - -### 产品演示 -- 重点展示用户体验和功能特性 -- 强调品牌元素和视觉设计 -- 展示多人互动和社交功能 - -### 招聘面试 -- 展示代码质量和工程实践 -- 介绍项目管理和团队协作 -- 分享技术选型和架构决策 - -### 客户展示 -- 重点展示商业价值和应用场景 -- 强调技术稳定性和可扩展性 -- 展示运维管理和监控能力 - -## 📞 常见问题准备 - -**Q: 这个项目的技术难点是什么?** -A: 主要难点包括实时网络同步、状态管理、跨平台兼容性和测试覆盖。我们通过模块化设计和完整的测试框架来解决这些问题。 - -**Q: 为什么选择 Godot 而不是 Unity?** -A: Godot 是开源的,更适合学习和定制。它的 GDScript 语言简单易学,而且对 2D 游戏有很好的支持。 - -**Q: 如何保证游戏的性能?** -A: 我们使用了对象池、空间分区、消息批处理等优化技术。同时通过性能监控和压力测试来确保系统稳定性。 - -**Q: 项目的扩展性如何?** -A: 项目采用模块化设计,各个系统相对独立。可以很容易地添加新功能、新场景或新的游戏机制。 - ---- - -这份演示指南帮助您专业地展示 AI Town Game 项目的技术实力和功能特性。根据不同的演示场景调整重点,确保演示效果最佳。 \ No newline at end of file diff --git a/DEPLOYMENT_GUIDE.md b/DEPLOYMENT_GUIDE.md deleted file mode 100644 index 60eee60..0000000 --- a/DEPLOYMENT_GUIDE.md +++ /dev/null @@ -1,122 +0,0 @@ -# AI Town Game 部署和运维指南 - -## 🚀 生产环境部署 - -### 环境要求 -- **服务器**: 2 核心 CPU, 4GB RAM, 10GB 存储 -- **软件**: Node.js 18+, PM2, Nginx, Git -- **系统**: Ubuntu 20.04+ / CentOS 8+ / Windows Server 2019+ - -### 快速部署 - -#### 1. 环境准备 -```bash -# Ubuntu/Debian -sudo apt update && sudo apt install -y nodejs npm nginx git -npm install -g yarn pm2 - -# 克隆项目 -git clone /opt/ai-town -cd /opt/ai-town/server -yarn install --production && yarn build -``` - -#### 2. 启动服务 -```bash -# 启动服务器 -pm2 start dist/server.js --name ai-town-server -pm2 startup && pm2 save - -# 配置 Nginx -sudo cp nginx.conf /etc/nginx/sites-available/ai-town -sudo ln -s /etc/nginx/sites-available/ai-town /etc/nginx/sites-enabled/ -sudo nginx -t && sudo systemctl reload nginx -``` - -#### 3. Web 客户端 -在 Godot 编辑器中导出 HTML5 版本到 `/opt/ai-town/web/` 目录 - -### 监控和维护 - -#### 日常检查 -```bash -# 服务状态 -pm2 status -pm2 logs ai-town-server - -# 系统资源 -htop && df -h - -# 健康检查 -curl -f http://localhost:8080/health || echo "Service down" -``` - -#### 备份策略 -```bash -# 自动备份脚本 -#!/bin/bash -tar -czf /backup/ai-town-$(date +%Y%m%d).tar.gz /opt/ai-town/server/data/ -find /backup -name "ai-town-*.tar.gz" -mtime +7 -delete - -# 定时任务 -echo "0 2 * * * /opt/ai-town/backup.sh" | crontab - -``` - -### 安全配置 - -#### SSL 和防火墙 -```bash -# SSL 证书 -sudo certbot --nginx -d your-domain.com - -# 防火墙 -sudo ufw allow ssh && sudo ufw allow 80 && sudo ufw allow 443 -sudo ufw enable -``` - -### 故障排除 - -#### 常见问题 -- **服务无法启动**: 检查端口占用 `sudo lsof -i :8080` -- **连接失败**: 测试 WebSocket `wscat -c ws://localhost:8080` -- **性能问题**: 监控资源 `pm2 monit` - -#### 紧急恢复 -```bash -# 重启所有服务 -pm2 restart all && sudo systemctl restart nginx - -# 数据恢复 -tar -xzf /backup/ai-town-YYYYMMDD.tar.gz -C /opt/ai-town/server/ -``` - -## 📊 监控告警 - -### 健康检查脚本 -```bash -#!/bin/bash -# health-check.sh -pm2 describe ai-town-server | grep -q "online" || exit 1 -nc -z localhost 8080 || exit 1 -echo "OK: Service healthy" -``` - -### 自动告警 -```bash -# 错误监控 -tail -f /opt/ai-town/server/logs/error.log | while read line; do - echo "$line" | grep -q "ERROR" && echo "Alert: $line" | mail admin@domain.com -done -``` - ---- - -**部署检查清单**: -- [ ] 环境配置完成 -- [ ] 服务正常启动 -- [ ] Web 界面可访问 -- [ ] SSL 证书配置 -- [ ] 备份策略启用 -- [ ] 监控告警配置 - -详细配置请参考项目文档和 `server/README.md`。 \ No newline at end of file diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 118cc9a..943ec2a 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -1,1758 +1,299 @@ -# AI Town Game 开发者技术文档 +# 开发者技术文档 -## 📋 目录 - -1. [项目概述](#项目概述) -2. [技术架构](#技术架构) -3. [开发环境配置](#开发环境配置) -4. [核心系统详解](#核心系统详解) -5. [API 参考](#api-参考) -6. [数据库设计](#数据库设计) -7. [网络协议](#网络协议) -8. [测试框架](#测试框架) -9. [部署指南](#部署指南) -10. [扩展开发](#扩展开发) -11. [性能优化](#性能优化) -12. [故障排除](#故障排除) - -## 项目概述 - -### 技术栈 - -**客户端**: -- **游戏引擎**: Godot 4.5.1 -- **编程语言**: GDScript -- **导出平台**: HTML5 (Web), Windows, macOS, Linux -- **UI 框架**: Godot 内置 UI 系统 - -**服务器**: -- **运行时**: Node.js 24.7.0+ -- **编程语言**: TypeScript -- **网络协议**: WebSocket (ws 库) -- **数据存储**: JSON 文件系统 -- **包管理**: Yarn 1.22.22+ - -**开发工具**: -- **版本控制**: Git -- **代码规范**: ESLint (TypeScript), GDScript 内置检查 -- **测试框架**: 自定义 GDScript 测试框架 -- **构建工具**: TypeScript Compiler, Godot Export - -### 项目结构 - -``` -ai_community/ -├── project.godot # Godot 项目配置 -├── scenes/ # 游戏场景文件 -│ ├── Main.tscn # 主场景 -│ ├── DatawhaleOffice.tscn # Datawhale 办公室场景 -│ ├── PlayerCharacter.tscn # 玩家角色场景 -│ ├── RemoteCharacter.tscn # 远程角色场景 -│ └── TestGameplay.tscn # 测试场景 -├── scripts/ # GDScript 脚本 -│ ├── Main.gd # 主脚本 -│ ├── NetworkManager.gd # 网络管理器 -│ ├── GameStateManager.gd # 游戏状态管理器 -│ ├── CharacterController.gd # 角色控制器 -│ ├── DialogueSystem.gd # 对话系统 -│ ├── InputHandler.gd # 输入处理器 -│ ├── WorldManager.gd # 世界管理器 -│ └── Utils.gd # 工具函数 -├── assets/ # 游戏资源 -│ ├── sprites/ # 精灵图像 -│ ├── tilesets/ # 瓦片集 -│ └── ui/ # UI 资源 -├── tests/ # 测试文件 -│ ├── RunAllTests.tscn # 测试运行器 -│ ├── test_*.gd # 单元测试 -│ └── test_property_*.gd # 属性测试 -├── server/ # WebSocket 服务器 -│ ├── src/ # TypeScript 源码 -│ │ ├── server.ts # 主服务器文件 -│ │ ├── api/ # API 模块 -│ │ ├── backup/ # 备份管理 -│ │ ├── logging/ # 日志管理 -│ │ ├── maintenance/ # 维护管理 -│ │ └── monitoring/ # 监控模块 -│ ├── data/ # 数据存储 -│ │ ├── characters.json # 角色数据 -│ │ ├── logs/ # 日志文件 -│ │ └── backups/ # 备份文件 -│ ├── dist/ # 编译输出 -│ ├── admin/ # Web 管理界面 -│ ├── package.json # 依赖配置 -│ └── tsconfig.json # TypeScript 配置 -└── .kiro/specs/ # 项目规范文档 - └── godot-ai-town-game/ - ├── requirements.md # 需求文档 - ├── design.md # 设计文档 - └── tasks.md # 任务列表 -``` - -## 技术架构 - -### 整体架构图 - -```mermaid -graph TB - subgraph "客户端 (Godot)" - A[Main Scene] --> B[NetworkManager] - A --> C[GameStateManager] - A --> D[UILayer] - A --> E[GameWorld] - - B --> F[WebSocket Client] - C --> G[State Machine] - D --> H[UI Components] - E --> I[Characters] - E --> J[TileMap] - end - - subgraph "网络层" - F <--> K[WebSocket Connection] - end - - subgraph "服务器 (Node.js)" - K <--> L[WebSocket Server] - L --> M[Connection Manager] - L --> N[Message Router] - L --> O[World State] - L --> P[Data Persistence] - - M --> Q[Authentication] - N --> R[Message Handlers] - O --> S[Character Manager] - P --> T[JSON Storage] - end - - subgraph "管理系统" - L --> U[Health Monitor] - L --> V[Backup Manager] - L --> W[Log Manager] - U --> X[Admin API] - V --> Y[Auto Backup] - W --> Z[Log Analysis] - end -``` +## 项目架构 ### 客户端架构 -#### 场景树结构 ``` Main (Node) -├── NetworkManager (Node) -├── GameStateManager (Node) -├── InputHandler (Node) -├── UILayer (CanvasLayer) -│ ├── LoginScreen (Control) -│ ├── CharacterCreation (Control) -│ ├── HUD (Control) -│ ├── DialogueBox (Control) -│ ├── ErrorNotification (Control) -│ └── LoadingIndicator (Control) -└── GameWorld (Node2D) - ├── DatawhaleOffice (TileMap) - ├── Characters (Node2D) - │ ├── PlayerCharacter (CharacterBody2D) - │ └── RemoteCharacter (CharacterBody2D) [多个] - └── Camera2D +├─ NetworkManager (Node) - 网络连接管理 +├─ GameStateManager (Node) - 游戏状态管理 +├─ UILayer (CanvasLayer) - UI 层 +│ ├─ LoginScreen (Control) - 登录界面 +│ ├─ CharacterCreation (Control) - 角色创建界面 +│ ├─ HUD (Control) - 游戏内 UI +│ └─ DialogueBox (Control) - 对话框 +└─ GameWorld (Node2D) - 游戏世界 + ├─ TileMap (TileMap) - 场景地图 + ├─ Characters (Node2D) - 角色容器 + │ ├─ PlayerCharacter (CharacterBody2D) - 本地玩家 + │ └─ RemoteCharacter (CharacterBody2D) - 其他角色 + └─ Camera (Camera2D) - 摄像机 ``` -#### 核心组件职责 - -**NetworkManager**: -- 管理 WebSocket 连接 -- 处理消息序列化/反序列化 -- 实现断线重连机制 -- 维护心跳检测 - -**GameStateManager**: -- 管理游戏状态机 -- 处理数据持久化 -- 协调状态转换 -- 发射状态变化信号 - -**InputHandler**: -- 处理键盘/触摸输入 -- 设备类型检测 -- 虚拟控件管理 -- 输入事件分发 - -**WorldManager**: -- 管理游戏世界中的所有角色 -- 处理角色生成/销毁 -- 维护角色状态同步 -- 提供空间查询功能 - ### 服务器架构 -#### 模块设计 - -**ConnectionManager**: -```typescript -class ConnectionManager { - private clients: Map - private heartbeats: Map - - addClient(clientId: string, ws: WebSocket): void - removeClient(clientId: string): void - broadcastMessage(message: any, excludeClient?: string): void - sendToClient(clientId: string, message: any): void - checkHeartbeats(): void -} +``` +WebSocket Server (Node.js + TypeScript) +├─ ConnectionManager - 管理客户端连接 +├─ AuthenticationService - 身份验证 +├─ CharacterManager - 角色管理 +├─ WorldState - 世界状态管理 +├─ MessageRouter - 消息路由 +├─ AdminAPI - 管理接口 +├─ BackupManager - 备份管理 +├─ LogManager - 日志管理 +└─ HealthChecker - 健康检查 ``` -**MessageRouter**: -```typescript -class MessageRouter { - private handlers: Map - - registerHandler(type: string, handler: MessageHandler): void - routeMessage(clientId: string, message: any): void - createResponse(type: string, data: any): any -} -``` +## 核心系统 -**WorldState**: -```typescript -class WorldState { - private characters: Map - private scenes: Map - - addCharacter(character: Character): void - updateCharacter(characterId: string, updates: Partial): void - removeCharacter(characterId: string): void - getWorldSnapshot(): WorldSnapshot -} -``` +### 网络管理器 (NetworkManager) -## 开发环境配置 +**职责**: 管理客户端与服务器的 WebSocket 连接 -### 环境要求 - -**开发工具**: -- Godot 4.5.1+ (游戏引擎) -- Node.js 24.7.0+ (服务器运行时) -- Yarn 1.22.22+ (包管理器) -- Git (版本控制) -- VS Code (推荐编辑器) - -**系统要求**: -- Windows 10+ / macOS 10.14+ / Ubuntu 18.04+ -- 8GB RAM (推荐) -- 2GB 可用磁盘空间 - -### 快速配置 - -1. **克隆项目**: -```bash -git clone -cd ai_community -``` - -2. **配置 Godot**: -```bash -# 下载并安装 Godot 4.5.1 -# 导入项目: 选择 project.godot 文件 -``` - -3. **配置服务器**: -```bash -cd server -yarn install -yarn build -``` - -4. **启动开发环境**: -```bash -# 终端 1: 启动服务器 -cd server -yarn dev - -# 终端 2: 启动 Godot 编辑器 -# 在 Godot 中按 F5 运行项目 -``` - -### VS Code 配置 - -推荐的 VS Code 扩展: -- **godot-tools**: GDScript 语法支持 -- **TypeScript Importer**: TypeScript 开发支持 -- **GitLens**: Git 增强功能 -- **Prettier**: 代码格式化 - -`.vscode/settings.json`: -```json -{ - "godot_tools.editor_path": "/path/to/godot", - "typescript.preferences.importModuleSpecifier": "relative", - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.organizeImports": true - } -} -``` - -## 核心系统详解 - -### 网络系统 - -#### WebSocket 连接管理 - -**客户端连接流程**: +**主要方法**: ```gdscript -# NetworkManager.gd -func connect_to_server(url: String) -> void: - _websocket = WebSocketPeer.new() - var error = _websocket.connect_to_url(url) - if error != OK: - emit_signal("connection_error", "Failed to connect: " + str(error)) - return - - _connection_state = ConnectionState.CONNECTING - _connection_timer = 0.0 - emit_signal("connection_attempt_started") - -func _process(delta: float) -> void: - if _websocket: - _websocket.poll() - var state = _websocket.get_ready_state() - - match state: - WebSocketPeer.STATE_OPEN: - _handle_connected() - WebSocketPeer.STATE_CLOSED: - _handle_disconnected() - - _handle_incoming_messages() - _update_heartbeat(delta) -``` - -**服务器连接处理**: -```typescript -// server.ts -wss.on('connection', (ws: WebSocket, req: IncomingMessage) => { - const clientId = generateClientId(); - const clientInfo = { - id: clientId, - ws: ws, - lastHeartbeat: Date.now(), - authenticated: false, - characterId: null - }; - - clients.set(clientId, clientInfo); - console.log(`✅ Client connected: ${clientId}`); - - ws.on('message', (data: Buffer) => { - handleMessage(clientId, data); - }); - - ws.on('close', () => { - handleDisconnection(clientId); - }); -}); -``` - -#### 消息协议实现 - -**消息序列化**: -```gdscript -# MessageProtocol.gd -static func create_message(type: String, data: Dictionary = {}) -> Dictionary: - return { - "type": type, - "data": data, - "timestamp": Time.get_unix_time_from_system() - } - -static func serialize_message(message: Dictionary) -> String: - return JSON.stringify(message) - -static func deserialize_message(json_string: String) -> Dictionary: - var json = JSON.new() - var parse_result = json.parse(json_string) - if parse_result != OK: - ErrorHandler.log_network_error("Failed to parse message", {"json": json_string}) - return {} - return json.data -``` - -**消息路由**: -```typescript -// MessageRouter.ts -class MessageRouter { - private handlers = new Map(); - - constructor() { - this.registerHandler('auth_request', new AuthHandler()); - this.registerHandler('character_create', new CharacterCreateHandler()); - this.registerHandler('character_move', new CharacterMoveHandler()); - this.registerHandler('dialogue_send', new DialogueHandler()); - this.registerHandler('ping', new PingHandler()); - } - - routeMessage(clientId: string, message: any): void { - const handler = this.handlers.get(message.type); - if (handler) { - handler.handle(clientId, message.data); - } else { - console.warn(`Unknown message type: ${message.type}`); - } - } -} -``` - -### 状态管理系统 - -#### 游戏状态机 - -```gdscript -# GameStateManager.gd -enum GameState { - LOGIN, - CHARACTER_CREATION, - IN_GAME, - DISCONNECTED -} - -var current_state: GameState = GameState.LOGIN -var player_data: Dictionary = {} - -func change_state(new_state: GameState) -> void: - var old_state = current_state - current_state = new_state - - print("State changed: ", GameState.keys()[old_state], " -> ", GameState.keys()[new_state]) - - match new_state: - GameState.LOGIN: - _show_login_screen() - GameState.CHARACTER_CREATION: - _show_character_creation() - GameState.IN_GAME: - _enter_game_world() - GameState.DISCONNECTED: - _show_disconnected_screen() - - emit_signal("state_changed", old_state, new_state) -``` - -#### 数据持久化 - -**客户端数据保存**: -```gdscript -# GameStateManager.gd -func save_player_data() -> void: - var save_data = { - "player_id": player_data.get("id", ""), - "character_name": player_data.get("character_name", ""), - "last_position": player_data.get("position", {"x": 1000, "y": 750}), - "settings": player_data.get("settings", {}), - "timestamp": Time.get_unix_time_from_system() - } - - var file = FileAccess.open("user://player_data.json", FileAccess.WRITE) - if file: - file.store_string(JSON.stringify(save_data)) - file.close() - print("Player data saved successfully") - else: - ErrorHandler.log_game_error("Failed to save player data") -``` - -**服务器数据持久化**: -```typescript -// DataPersistence.ts -class DataPersistence { - private dataPath = './data/characters.json'; - private backupInterval = 5 * 60 * 1000; // 5 minutes - - async saveCharacters(characters: Character[]): Promise { - try { - const data = JSON.stringify(characters, null, 2); - await fs.writeFile(this.dataPath, data, 'utf8'); - console.log('💾 Characters data saved'); - } catch (error) { - console.error('Failed to save characters:', error); - } - } - - async loadCharacters(): Promise { - try { - const data = await fs.readFile(this.dataPath, 'utf8'); - return JSON.parse(data); - } catch (error) { - console.log('📂 No existing character data found, starting fresh'); - return []; - } - } - - startAutoSave(): void { - setInterval(() => { - this.saveCharacters(Array.from(worldState.characters.values())); - }, this.backupInterval); - } -} -``` - -### 角色系统 - -#### 角色控制器 - -```gdscript -# CharacterController.gd -extends CharacterBody2D -class_name CharacterController - -@export var character_id: String = "" -@export var character_name: String = "" -@export var is_online: bool = false -@export var move_speed: float = 200.0 - -var target_position: Vector2 -var is_moving: bool = false - -signal position_updated(new_position: Vector2) -signal animation_changed(animation_name: String) - -func _ready(): - target_position = global_position - _setup_animation() - _setup_collision() - -func move_to(direction: Vector2) -> void: - if direction.length() > 0: - velocity = direction.normalized() * move_speed - is_moving = true - _play_animation("walk") - else: - velocity = Vector2.ZERO - is_moving = false - _play_animation("idle") - - move_and_slide() - - if global_position != target_position: - target_position = global_position - emit_signal("position_updated", global_position) - -func set_position_smooth(new_position: Vector2, duration: float = 0.2) -> void: - var tween = create_tween() - tween.tween_property(self, "global_position", new_position, duration) - tween.tween_callback(func(): target_position = new_position) -``` - -#### 角色状态同步 - -**客户端同步**: -```gdscript -# WorldManager.gd -func update_character_state(character_id: String, state_data: Dictionary) -> void: - if not characters.has(character_id): - ErrorHandler.log_game_error("Character not found for update", {"id": character_id}) - return - - var character = characters[character_id] - - # 更新位置 - if state_data.has("position"): - var pos = state_data["position"] - character.set_position_smooth(Vector2(pos["x"], pos["y"])) - - # 更新在线状态 - if state_data.has("isOnline"): - character.set_online_status(state_data["isOnline"]) - - # 更新名称 - if state_data.has("name"): - character.character_name = state_data["name"] - character.update_name_label() -``` - -**服务器状态广播**: -```typescript -// CharacterManager.ts -updateCharacterPosition(characterId: string, position: Position): void { - const character = this.characters.get(characterId); - if (!character) return; - - character.position = position; - character.lastSeen = Date.now(); - - // 广播位置更新给所有客户端 - const message = { - type: 'character_move', - data: { - characterId: characterId, - position: position - }, - timestamp: Date.now() - }; - - connectionManager.broadcastMessage(message); - - // 触发自动保存 - this.scheduleAutoSave(); -} -``` - -### 对话系统 - -#### 对话管理 - -```gdscript -# DialogueSystem.gd -extends Node -class_name DialogueSystem - -var active_dialogues: Dictionary = {} -var dialogue_history: Array = [] - -signal dialogue_started(character_id: String) -signal dialogue_ended() -signal message_received(sender: String, message: String) - -func start_dialogue(target_character_id: String) -> void: - if active_dialogues.has(target_character_id): - print("Dialogue already active with character: ", target_character_id) - return - - var dialogue_data = { - "target_id": target_character_id, - "start_time": Time.get_unix_time_from_system(), - "messages": [] - } - - active_dialogues[target_character_id] = dialogue_data - emit_signal("dialogue_started", target_character_id) - - # 显示对话界面 - var dialogue_box = get_node("../UILayer/DialogueBox") - dialogue_box.show_dialogue(target_character_id) - -func send_message(target_character_id: String, message: String) -> void: - if not active_dialogues.has(target_character_id): - ErrorHandler.log_game_error("No active dialogue with character", {"id": target_character_id}) - return - - # 验证消息内容 - if message.strip_edges().is_empty(): - return - - if message.length() > 500: - message = message.substr(0, 500) - - # 添加到对话历史 - var message_data = { - "sender": "player", - "content": message, - "timestamp": Time.get_unix_time_from_system() - } - - active_dialogues[target_character_id]["messages"].append(message_data) - dialogue_history.append(message_data) - - # 发送到服务器 - var network_message = MessageProtocol.create_message("dialogue_send", { - "receiverId": target_character_id, - "message": message - }) - - NetworkManager.send_message(network_message) - emit_signal("message_received", "player", message) -``` - -#### 对话气泡系统 - -```gdscript -# ChatBubble.gd -extends Control -class_name ChatBubble - -@onready var label: Label = $Background/Label -@onready var background: NinePatchRect = $Background -@onready var timer: Timer = $Timer - -var character_node: Node2D -var offset: Vector2 = Vector2(0, -60) - -func show_bubble(character: Node2D, message: String, duration: float = 3.0) -> void: - character_node = character - label.text = message - - # 调整气泡大小 - var text_size = label.get_theme_font("font").get_string_size( - message, - HORIZONTAL_ALIGNMENT_LEFT, - -1, - label.get_theme_font_size("font_size") - ) - - var bubble_size = text_size + Vector2(20, 16) - background.size = bubble_size - size = bubble_size - - # 设置位置 - _update_position() - - # 显示气泡 - modulate.a = 0.0 - visible = true - - var tween = create_tween() - tween.tween_property(self, "modulate:a", 1.0, 0.2) - - # 设置自动隐藏 - timer.wait_time = duration - timer.start() - -func _update_position() -> void: - if character_node: - global_position = character_node.global_position + offset -``` - -## API 参考 - -### 客户端 API - -#### NetworkManager - -```gdscript -class_name NetworkManager extends Node - -# 信号 -signal connected_to_server() -signal disconnected_from_server() -signal connection_error(error: String) -signal message_received(message: Dictionary) - -# 方法 func connect_to_server(url: String) -> void func disconnect_from_server() -> void func send_message(message: Dictionary) -> void func is_connected() -> bool -func get_connection_state() -> ConnectionState ``` -#### GameStateManager +**信号**: +- `connected_to_server()` - 连接成功 +- `disconnected_from_server()` - 断开连接 +- `connection_error(error: String)` - 连接错误 +- `message_received(message: Dictionary)` - 收到消息 +### 角色控制器 (CharacterController) + +**职责**: 处理角色的移动、动画和状态 + +**主要属性**: ```gdscript -class_name GameStateManager extends Node - -# 枚举 -enum GameState { LOGIN, CHARACTER_CREATION, IN_GAME, DISCONNECTED } - -# 信号 -signal state_changed(old_state: GameState, new_state: GameState) -signal data_saved() -signal data_loaded(data: Dictionary) - -# 属性 -var current_state: GameState -var player_data: Dictionary - -# 方法 -func change_state(new_state: GameState) -> void -func save_player_data() -> void -func load_player_data() -> Dictionary -func get_current_state() -> GameState +var character_id: String +var character_name: String +var is_online: bool +var move_speed: float = 200.0 ``` -#### CharacterController - +**主要方法**: ```gdscript -class_name CharacterController extends CharacterBody2D - -# 信号 -signal position_updated(new_position: Vector2) -signal animation_changed(animation_name: String) -signal online_status_changed(is_online: bool) - -# 属性 -@export var character_id: String -@export var character_name: String -@export var is_online: bool -@export var move_speed: float - -# 方法 func move_to(direction: Vector2) -> void -func set_position_smooth(target_pos: Vector2, duration: float = 0.2) -> void -func set_online_status(online: bool) -> void +func set_position_smooth(target_pos: Vector2) -> void func play_animation(anim_name: String) -> void +func set_online_status(online: bool) -> void ``` -### 服务器 API +### 对话系统 (DialogueSystem) -#### WebSocket 消息 API +**职责**: 管理角色之间的对话交互 -**身份验证**: -```typescript -// 请求 +**主要方法**: +```gdscript +func start_dialogue(target_character_id: String) -> void +func send_message(message: String) -> void +func end_dialogue() -> void +func show_bubble(character_id: String, message: String, duration: float) -> void +``` + +**信号**: +- `dialogue_started(character_id: String)` +- `dialogue_ended()` +- `message_received(sender: String, message: String)` + +## 数据模型 + +### 角色数据 + +```gdscript { - "type": "auth_request", - "data": { - "username": string - }, - "timestamp": number + "id": "unique_character_id", + "name": "角色名称", + "owner_id": "player_account_id", + "position": { + "x": 100.0, + "y": 200.0 + }, + "is_online": true, + "appearance": { + "head_color": "#F5DEB3", + "body_color": "#4169E1", + "foot_color": "#8B4513" + }, + "created_at": 1234567890, + "last_seen": 1234567890 } +``` -// 响应 +### 消息协议 + +所有消息使用 JSON 格式: +```json { - "type": "auth_response", - "data": { - "success": boolean, - "clientId": string, - "message"?: string - }, - "timestamp": number + "type": "message_type", + "data": {}, + "timestamp": 1234567890 } ``` -**角色创建**: -```typescript -// 请求 +**消息类型**: +- `auth_request` / `auth_response` - 身份验证 +- `character_create` - 创建角色 +- `character_move` - 角色移动 +- `character_state` - 角色状态更新 +- `dialogue_send` - 发送对话 +- `world_state` - 世界状态同步 +- `friend_request` - 好友请求 +- `private_message` - 私聊消息 + +## 扩展功能 + +### 角色外观自定义 + +**组件**: +- `CharacterCustomization.gd` - 外观自定义管理 +- `CharacterPersonalization.gd` - 个性化设置 +- `CharacterProfile.gd` - 角色档案 + +**外观数据结构**: +```gdscript { - "type": "character_create", - "data": { - "name": string - }, - "timestamp": number + "head_color": "#F5DEB3", + "body_color": "#4169E1", + "foot_color": "#8B4513", + "created_at": 1234567890, + "version": "1.0" } +``` -// 响应 +### 社交系统 + +**组件**: +- `FriendSystem.gd` - 好友管理 +- `PrivateChatSystem.gd` - 私聊功能 +- `RelationshipNetwork.gd` - 关系网络 +- `SocialManager.gd` - 社交管理器 + +### 安全系统 + +**组件**: +- `SecurityManager.gd` - 安全管理 +- `RateLimiter.gd` - 速率限制 +- `DialogueFilter.gd` - 对话过滤 + +**速率限制配置**: +```gdscript { - "type": "character_create", - "data": { - "success": boolean, - "character"?: { - "id": string, - "name": string, - "position": { "x": number, "y": number }, - "isOnline": boolean - }, - "message"?: string - }, - "timestamp": number + "max_requests": 10, + "time_window": 60.0, # 秒 + "cooldown": 5.0 # 秒 } ``` -**角色移动**: -```typescript -// 客户端 -> 服务器 -{ - "type": "character_move", - "data": { - "position": { "x": number, "y": number } - }, - "timestamp": number -} +## 添加新功能 -// 服务器 -> 所有客户端 -{ - "type": "character_move", - "data": { - "characterId": string, - "position": { "x": number, "y": number } - }, - "timestamp": number -} +### 1. 创建规格文档 + +在 `.kiro/specs/` 创建新目录: +``` +.kiro/specs/your-feature/ +├── requirements.md # 需求和验收标准 +├── design.md # 架构和设计 +└── tasks.md # 实施计划 ``` -#### REST API (管理接口) +### 2. 实现功能 -**系统状态**: -```http -GET /api/status -Authorization: Bearer - -Response: -{ - "status": "healthy", - "uptime": 3600, - "connections": 5, - "characters": 12, - "memory": { - "used": 45.2, - "total": 512 - } -} +创建必要的脚本和场景: +``` +scripts/YourFeature.gd +scenes/YourFeature.tscn ``` -**备份管理**: -```http -POST /api/backup -Authorization: Bearer +### 3. 编写测试 -Response: -{ - "success": true, - "backupId": "backup_1234567890", - "timestamp": 1234567890 -} +创建测试文件: +``` +tests/test_your_feature.gd ``` -## 数据库设计 +### 4. 集成到主系统 -### 数据模型 +在 `Main.gd` 或相关管理器中集成新功能。 -#### Character 模型 +## 代码规范 -```typescript -interface Character { - id: string; // UUID - name: string; // 角色名称 (2-20 字符) - ownerId: string; // 所属玩家 ID - position: { // 角色位置 - x: number; - y: number; - }; - isOnline: boolean; // 在线状态 - appearance?: { // 外观设置 (可选) - sprite: string; - color: string; - }; - createdAt: number; // 创建时间戳 - lastSeen: number; // 最后在线时间戳 -} -``` +### 命名约定 +- 变量和函数: `snake_case` +- 类名: `PascalCase` +- 常量: `UPPER_CASE` +- 私有变量: `_leading_underscore` -#### WorldState 模型 - -```typescript -interface WorldState { - sceneId: string; // 场景 ID - characters: Character[]; // 所有角色 - timestamp: number; // 状态时间戳 -} -``` - -#### Message 模型 - -```typescript -interface DialogueMessage { - senderId: string; // 发送者角色 ID - receiverId?: string; // 接收者角色 ID (可选,为空表示广播) - message: string; // 消息内容 - timestamp: number; // 发送时间戳 -} -``` - -### 数据存储 - -**文件结构**: -``` -server/data/ -├── characters.json # 角色数据 -├── maintenance_tasks.json # 维护任务 -├── logs/ # 日志文件 -│ └── server_YYYY-MM-DD.log -└── backups/ # 备份文件 - └── backup_/ - ├── backup_info.json - ├── characters.json.gz - └── logs/ -``` - -**数据验证**: -```typescript -// 角色数据验证 -function validateCharacter(character: any): boolean { - return ( - typeof character.id === 'string' && - typeof character.name === 'string' && - character.name.length >= 2 && - character.name.length <= 20 && - typeof character.ownerId === 'string' && - typeof character.position === 'object' && - typeof character.position.x === 'number' && - typeof character.position.y === 'number' && - typeof character.isOnline === 'boolean' - ); -} -``` - -## 网络协议 - -### 连接生命周期 - -```mermaid -sequenceDiagram - participant C as Client - participant S as Server - - C->>S: WebSocket Connection - S->>C: Connection Established - - C->>S: auth_request - S->>C: auth_response (success) - - C->>S: character_create - S->>C: character_create (success) - S->>C: world_state (initial) - - loop Game Loop - C->>S: character_move - S->>C: character_move (broadcast) - C->>S: ping - S->>C: pong - end - - C->>S: Connection Close - S->>S: Update character offline -``` - -### 错误处理 - -**网络错误**: -```typescript -{ - "type": "error", - "data": { - "code": "E001", - "message": "Connection timeout", - "details": "Server did not respond within 10 seconds" - }, - "timestamp": number -} -``` - -**业务逻辑错误**: -```typescript -{ - "type": "character_create", - "data": { - "success": false, - "message": "Character name already exists", - "code": "G001" - }, - "timestamp": number -} -``` - -### 心跳机制 - -**客户端心跳**: +### 注释规范 ```gdscript -# NetworkManager.gd -func _update_heartbeat(delta: float) -> void: - _heartbeat_timer += delta - if _heartbeat_timer >= HEARTBEAT_INTERVAL: - _send_ping() - _heartbeat_timer = 0.0 +## 类文档注释 +## 描述类的用途和职责 +class_name MyClass -func _send_ping() -> void: - var ping_message = MessageProtocol.create_message("ping") - send_message(ping_message) +## 函数文档注释 +## 参数: +## - param1: 参数描述 +## 返回: 返回值描述 +func my_function(param1: String) -> int: + # 行内注释 + return 0 ``` -**服务器心跳检查**: -```typescript -// 每 30 秒检查一次心跳 -setInterval(() => { - const now = Date.now(); - for (const [clientId, client] of clients) { - if (now - client.lastHeartbeat > HEARTBEAT_TIMEOUT) { - console.log(`⏰ Client ${clientId} heartbeat timeout`); - handleDisconnection(clientId); - } - } -}, 30000); -``` - -## 测试框架 - -### 单元测试 - -**测试结构**: +### 信号定义 ```gdscript -# test_example.gd -extends Node - -var test_results: Array = [] - -func _ready(): - run_all_tests() - print_results() - -func run_all_tests(): - test_basic_functionality() - test_edge_cases() - test_error_handling() - -func test_basic_functionality(): - var result = TestResult.new("Basic Functionality") - - # 测试逻辑 - var expected = "expected_value" - var actual = function_under_test() - - if actual == expected: - result.pass("Function returns expected value") - else: - result.fail("Expected %s, got %s" % [expected, actual]) - - test_results.append(result) +## 当某事件发生时触发 +signal event_happened(data: Dictionary) ``` -### 属性测试 +## 性能优化 -**属性测试框架**: +### 对象池 +使用对象池减少内存分配: ```gdscript -# PropertyTest.gd -class_name PropertyTest +var object_pool: Array = [] -const DEFAULT_ITERATIONS = 100 +func get_object(): + if object_pool.is_empty(): + return create_new_object() + return object_pool.pop_back() -static func run_property_test( - property_name: String, - test_function: Callable, - iterations: int = DEFAULT_ITERATIONS -) -> PropertyTestResult: - - var result = PropertyTestResult.new(property_name) - - for i in range(iterations): - var test_data = generate_test_data() - var success = test_function.call(test_data) - - if success: - result.add_success() - else: - result.add_failure(i, test_data) - - return result - -static func generate_test_data() -> Dictionary: - # 生成随机测试数据 - return { - "character_id": "char_" + str(randi()), - "position": Vector2(randf_range(0, 2000), randf_range(0, 1500)), - "name": "Test" + str(randi() % 1000) - } +func return_object(obj): + object_pool.append(obj) ``` -### 集成测试 +### 网络优化 +- 使用消息批处理 +- 实现状态差异同步 +- 减少不必要的网络请求 -**场景测试**: +### 渲染优化 +- 使用视锥剔除 +- 优化碰撞检测 +- 减少节点数量 + +## 调试技巧 + +### 打印调试 ```gdscript -# test_scene_integration.gd -extends Node - -func test_character_spawning(): - # 加载测试场景 - var scene = preload("res://scenes/DatawhaleOffice.tscn").instantiate() - add_child(scene) - - # 创建角色 - var character_data = { - "id": "test_char", - "name": "Test Character", - "position": {"x": 1000, "y": 750} - } - - var world_manager = scene.get_node("WorldManager") - world_manager.spawn_character(character_data) - - # 验证角色是否正确生成 - assert(world_manager.characters.has("test_char")) - - var character = world_manager.characters["test_char"] - assert(character.character_name == "Test Character") - assert(character.global_position == Vector2(1000, 750)) +print("Debug:", variable) +print_debug("Stack trace") ``` -## 部署指南 +### 断点调试 +在代码行号左侧点击设置断点,按 F5 运行调试模式。 -### 开发环境部署 - -**本地开发**: -```bash -# 1. 启动服务器 -cd server -yarn dev - -# 2. 启动 Godot 客户端 -# 在 Godot 编辑器中按 F5 - -# 3. 运行测试 -# 在 Godot 中打开 tests/RunAllTests.tscn,按 F6 -``` - -### 生产环境部署 - -#### 服务器部署 - -**使用 PM2**: -```bash -# 安装 PM2 -npm install -g pm2 - -# 构建项目 -cd server -yarn build - -# 启动服务 -pm2 start dist/server.js --name ai-town-server - -# 查看日志 -pm2 logs ai-town-server - -# 设置开机自启 -pm2 startup -pm2 save -``` - -**使用 Docker**: -```dockerfile -# Dockerfile -FROM node:18-alpine - -WORKDIR /app - -# 复制依赖文件 -COPY server/package*.json ./ -COPY server/yarn.lock ./ - -# 安装依赖 -RUN yarn install --frozen-lockfile - -# 复制源码 -COPY server/ ./ - -# 构建项目 -RUN yarn build - -# 暴露端口 -EXPOSE 8080 8081 - -# 启动服务 -CMD ["yarn", "start"] -``` - -```bash -# 构建镜像 -docker build -t ai-town-server . - -# 运行容器 -docker run -d \ - --name ai-town-server \ - -p 8080:8080 \ - -p 8081:8081 \ - -v $(pwd)/data:/app/data \ - ai-town-server -``` - -#### 客户端部署 - -**Web 导出**: -```bash -# 在 Godot 编辑器中 -1. 项目 -> 导出 -2. 添加 "HTML5" 导出预设 -3. 配置导出选项: - - 线程支持: 启用 - - 导出路径: build/web/ -4. 导出项目 -``` - -**Nginx 配置**: -```nginx -server { - listen 80; - server_name your-domain.com; - - # 静态文件 - location / { - root /var/www/ai-town/web; - index index.html; - try_files $uri $uri/ /index.html; - } - - # WebSocket 代理 - location /ws { - proxy_pass http://localhost:8080; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - } - - # 管理 API 代理 - location /api { - proxy_pass http://localhost:8081; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - } -} -``` - -### 监控和维护 - -**系统监控**: -```bash -# 查看服务状态 -pm2 status - -# 查看系统资源 -pm2 monit - -# 重启服务 -pm2 restart ai-town-server - -# 查看错误日志 -pm2 logs ai-town-server --err -``` - -**数据备份**: -```bash -# 手动备份 -curl -X POST http://localhost:8081/api/backup \ - -H "Authorization: Bearer admin123" - -# 自动备份 (crontab) -0 2 * * * curl -X POST http://localhost:8081/api/backup -H "Authorization: Bearer admin123" -``` - -## 扩展开发 - -### 添加新功能 - -#### 1. 添加新的消息类型 - -**客户端**: +### 性能分析 +使用 Godot 内置的性能监视器: ```gdscript -# MessageProtocol.gd -enum MessageType { - # ... 现有类型 - NEW_FEATURE_REQUEST, - NEW_FEATURE_RESPONSE -} - -static func create_new_feature_message(data: Dictionary) -> Dictionary: - return create_message("new_feature_request", data) +Performance.get_monitor(Performance.TIME_FPS) +Performance.get_monitor(Performance.MEMORY_STATIC) ``` -**服务器**: -```typescript -// MessageRouter.ts -constructor() { - // ... 现有处理器 - this.registerHandler('new_feature_request', new NewFeatureHandler()); -} +## 服务器 API -// NewFeatureHandler.ts -class NewFeatureHandler implements MessageHandler { - handle(clientId: string, data: any): void { - // 处理新功能请求 - const response = { - type: 'new_feature_response', - data: { success: true }, - timestamp: Date.now() - }; - - connectionManager.sendToClient(clientId, response); - } -} -``` +详细的服务器 API 文档请参考 [server/README.md](server/README.md) -#### 2. 添加新的 UI 组件 +## 相关文档 -```gdscript -# NewUIComponent.gd -extends Control -class_name NewUIComponent - -signal component_action(action: String, data: Dictionary) - -@onready var button: Button = $Button -@onready var label: Label = $Label - -func _ready(): - button.pressed.connect(_on_button_pressed) - _setup_component() - -func _setup_component(): - # 组件初始化逻辑 - pass - -func _on_button_pressed(): - emit_signal("component_action", "button_clicked", {}) - -func update_display(data: Dictionary): - # 更新显示内容 - label.text = data.get("text", "") -``` - -#### 3. 添加新的游戏系统 - -```gdscript -# NewGameSystem.gd -extends Node -class_name NewGameSystem - -signal system_event(event_type: String, data: Dictionary) - -var system_data: Dictionary = {} -var is_initialized: bool = false - -func _ready(): - initialize_system() - -func initialize_system(): - # 系统初始化 - system_data = load_system_data() - is_initialized = true - emit_signal("system_event", "initialized", {}) - -func process_system_update(delta: float): - if not is_initialized: - return - - # 系统更新逻辑 - pass - -func handle_network_message(message: Dictionary): - # 处理网络消息 - match message.type: - "system_update": - _handle_system_update(message.data) - -func _handle_system_update(data: Dictionary): - # 处理系统更新 - pass -``` - -### 性能优化 - -#### 客户端优化 - -**对象池**: -```gdscript -# ObjectPool.gd -class_name ObjectPool - -var pool: Array = [] -var scene_template: PackedScene -var max_size: int - -func _init(template: PackedScene, size: int = 50): - scene_template = template - max_size = size - _populate_pool() - -func get_object() -> Node: - if pool.is_empty(): - return scene_template.instantiate() - - return pool.pop_back() - -func return_object(obj: Node): - if pool.size() < max_size: - obj.reset() # 假设对象有 reset 方法 - pool.append(obj) - else: - obj.queue_free() - -func _populate_pool(): - for i in range(max_size / 2): - var obj = scene_template.instantiate() - pool.append(obj) -``` - -**空间分区**: -```gdscript -# SpatialGrid.gd -class_name SpatialGrid - -var grid: Dictionary = {} -var cell_size: float = 100.0 - -func add_object(obj: Node2D, id: String): - var cell = _get_cell(obj.global_position) - if not grid.has(cell): - grid[cell] = {} - grid[cell][id] = obj - -func get_nearby_objects(position: Vector2, radius: float) -> Array: - var nearby = [] - var cells = _get_cells_in_radius(position, radius) - - for cell in cells: - if grid.has(cell): - nearby.append_array(grid[cell].values()) - - return nearby - -func _get_cell(position: Vector2) -> Vector2i: - return Vector2i( - int(position.x / cell_size), - int(position.y / cell_size) - ) -``` - -#### 服务器优化 - -**消息批处理**: -```typescript -// MessageBatcher.ts -class MessageBatcher { - private batches = new Map(); - private batchInterval = 50; // 50ms - - constructor() { - setInterval(() => this.flushBatches(), this.batchInterval); - } - - addMessage(clientId: string, message: any): void { - if (!this.batches.has(clientId)) { - this.batches.set(clientId, []); - } - this.batches.get(clientId)!.push(message); - } - - private flushBatches(): void { - for (const [clientId, messages] of this.batches) { - if (messages.length > 0) { - const batchMessage = { - type: 'batch', - data: { messages }, - timestamp: Date.now() - }; - - connectionManager.sendToClient(clientId, batchMessage); - messages.length = 0; // 清空数组 - } - } - } -} -``` - -**内存管理**: -```typescript -// MemoryManager.ts -class MemoryManager { - private cleanupInterval = 5 * 60 * 1000; // 5 minutes - - constructor() { - setInterval(() => this.cleanup(), this.cleanupInterval); - } - - cleanup(): void { - // 清理过期的连接 - this.cleanupExpiredConnections(); - - // 清理旧的消息历史 - this.cleanupMessageHistory(); - - // 强制垃圾回收 - if (global.gc) { - global.gc(); - } - } - - private cleanupExpiredConnections(): void { - const now = Date.now(); - const timeout = 10 * 60 * 1000; // 10 minutes - - for (const [clientId, client] of clients) { - if (now - client.lastHeartbeat > timeout) { - clients.delete(clientId); - } - } - } -} -``` - -## 故障排除 - -### 常见问题诊断 - -#### 网络连接问题 - -**症状**: 客户端无法连接服务器 -**诊断步骤**: -1. 检查服务器是否运行: `pm2 status` -2. 检查端口是否开放: `netstat -an | grep 8080` -3. 检查防火墙设置 -4. 查看服务器日志: `pm2 logs ai-town-server` - -**解决方案**: -```bash -# 重启服务器 -pm2 restart ai-town-server - -# 检查配置 -cat server/src/server.ts | grep PORT - -# 测试连接 -curl -I http://localhost:8080 -``` - -#### 性能问题 - -**症状**: 游戏卡顿,帧率低 -**诊断工具**: -```gdscript -# PerformanceMonitor.gd -func _process(delta): - var fps = Engine.get_frames_per_second() - var memory = OS.get_static_memory_usage() - - if fps < 30: - print("Low FPS detected: ", fps) - - if memory > 100 * 1024 * 1024: # 100MB - print("High memory usage: ", memory / 1024 / 1024, "MB") -``` - -**优化建议**: -1. 减少同时显示的角色数量 -2. 使用对象池减少内存分配 -3. 优化渲染设置 -4. 检查是否有内存泄漏 - -#### 数据同步问题 - -**症状**: 角色位置不同步 -**调试代码**: -```gdscript -# 在 CharacterController.gd 中添加调试信息 -func set_position_smooth(new_position: Vector2, duration: float = 0.2): - print("Position update: ", character_id, " from ", global_position, " to ", new_position) - # ... 原有代码 -``` - -**检查清单**: -1. 网络连接是否稳定 -2. 服务器是否正确广播位置更新 -3. 客户端是否正确处理位置消息 -4. 是否存在消息丢失 - -### 日志分析 - -**服务器日志格式**: -``` -[2024-12-05 10:30:15] INFO: Server started on port 8080 -[2024-12-05 10:30:20] INFO: ✅ Client connected: client_abc123 -[2024-12-05 10:30:25] INFO: 🔐 Authentication successful: client_abc123 -[2024-12-05 10:30:30] INFO: 👤 Character created: char_def456 (Hero) -[2024-12-05 10:30:35] ERROR: ❌ Invalid message format from client_abc123 -``` - -**日志分析脚本**: -```bash -#!/bin/bash -# analyze_logs.sh - -LOG_FILE="server/data/logs/server_$(date +%Y-%m-%d).log" - -echo "=== Connection Statistics ===" -grep "Client connected" $LOG_FILE | wc -l -echo "Total connections today" - -echo "=== Error Summary ===" -grep "ERROR" $LOG_FILE | cut -d' ' -f4- | sort | uniq -c | sort -nr - -echo "=== Character Creation ===" -grep "Character created" $LOG_FILE | wc -l -echo "Characters created today" -``` - -### 调试工具 - -**网络调试**: -```gdscript -# NetworkDebugger.gd -extends Node - -var message_log: Array = [] -var max_log_size: int = 1000 - -func log_message(direction: String, message: Dictionary): - var log_entry = { - "timestamp": Time.get_unix_time_from_system(), - "direction": direction, # "sent" or "received" - "type": message.get("type", "unknown"), - "data": message.get("data", {}), - "size": JSON.stringify(message).length() - } - - message_log.append(log_entry) - - if message_log.size() > max_log_size: - message_log.pop_front() - - print("[NET %s] %s: %s bytes" % [direction.to_upper(), log_entry.type, log_entry.size]) - -func get_message_statistics() -> Dictionary: - var stats = { - "total_messages": message_log.size(), - "sent_messages": 0, - "received_messages": 0, - "message_types": {} - } - - for entry in message_log: - if entry.direction == "sent": - stats.sent_messages += 1 - else: - stats.received_messages += 1 - - var type = entry.type - if not stats.message_types.has(type): - stats.message_types[type] = 0 - stats.message_types[type] += 1 - - return stats -``` - -**性能分析器**: -```gdscript -# Profiler.gd -extends Node - -var frame_times: Array = [] -var function_times: Dictionary = {} - -func start_profiling(function_name: String): - function_times[function_name] = Time.get_ticks_usec() - -func end_profiling(function_name: String): - if function_times.has(function_name): - var elapsed = Time.get_ticks_usec() - function_times[function_name] - print("Function %s took %d microseconds" % [function_name, elapsed]) - function_times.erase(function_name) - -func _process(delta): - frame_times.append(delta) - if frame_times.size() > 60: # 保持最近 60 帧 - frame_times.pop_front() - - # 每秒输出一次平均帧时间 - if Engine.get_process_frames() % 60 == 0: - var avg_frame_time = 0.0 - for time in frame_times: - avg_frame_time += time - avg_frame_time /= frame_times.size() - - var fps = 1.0 / avg_frame_time - print("Average FPS: %.1f" % fps) -``` - ---- - -## 总结 - -本技术文档涵盖了 AI Town Game 项目的所有技术细节,包括架构设计、API 参考、部署指南和扩展开发。开发者可以根据这份文档: - -1. **快速上手**: 通过环境配置和快速开始指南 -2. **深入理解**: 通过核心系统详解和架构图 -3. **扩展功能**: 通过扩展开发指南添加新功能 -4. **优化性能**: 通过性能优化建议提升游戏体验 -5. **解决问题**: 通过故障排除指南快速定位和解决问题 - -项目采用模块化设计,具有良好的可扩展性和可维护性。所有核心系统都经过充分测试,并提供了完整的 API 文档和使用示例。 - -如有任何技术问题或改进建议,欢迎通过项目 GitHub 页面提交 Issue 或 Pull Request。 \ No newline at end of file +- [项目状态](PROJECT_STATUS.md) - 当前开发状态 +- [测试指南](HOW_TO_TEST.md) - 测试方法 +- [环境配置](SETUP.md) - 开发环境配置 +- [主游戏规格](.kiro/specs/godot-ai-town-game/) - 核心系统规格 +- [角色自定义规格](.kiro/specs/character-appearance-customization/) - 外观系统规格 diff --git a/Godot_v4.5.1-stable_win64.exe.zip b/Godot_v4.5.1-stable_win64.exe.zip deleted file mode 100644 index d9fef8d..0000000 Binary files a/Godot_v4.5.1-stable_win64.exe.zip and /dev/null differ diff --git a/HOW_TO_TEST.md b/HOW_TO_TEST.md index 670a3c7..49e9ec6 100644 --- a/HOW_TO_TEST.md +++ b/HOW_TO_TEST.md @@ -1,13 +1,11 @@ -# 测试指南 +# 快速测试指南 -## 🚀 快速测试 +## 快速开始 -### 方法 1: 游戏功能测试(推荐) - -**最快的测试方法**: +### 方法 1: 快速测试场景(推荐) 1. 在 Godot 编辑器中打开 `scenes/TestGameplay.tscn` -2. 按 **F6** 运行场景 +2. 按 **F6** 运行当前场景 3. 使用 **WASD** 或方向键移动角色 **预期结果**: @@ -15,187 +13,95 @@ - ✅ 角色可以自由移动 - ✅ 相机跟随角色 - ✅ 角色被墙壁和家具阻挡 -- ✅ 游戏流畅运行(30+ FPS) -### 方法 2: 单元测试套件 +### 方法 2: 完整游戏测试 -**全面的系统测试**: +1. 确保服务器正在运行: + ```bash + cd server + yarn dev + ``` -1. 在 Godot 编辑器中打开 `tests/RunAllTests.tscn` -2. 按 **F6** 运行 -3. 查看控制台输出 +2. 在 Godot 编辑器中按 **F5** 运行主场景 +3. 输入用户名并创建角色 +4. 测试移动、对话等功能 -**预期结果**: -- 所有测试显示 ✅ PASSED -- 测试覆盖:角色数据、控制器、状态管理、输入处理、消息协议 - -## 🎮 游戏控制测试 - -### 基础移动 -- **W** - 向上移动 -- **S** - 向下移动 -- **A** - 向左移动 -- **D** - 向右移动 -- **E** - 交互(控制台显示消息) -- **ESC** - 退出 - -### 相机控制(调试模式) -- **WASD** - 移动相机查看整个场景 -- **Q** - 缩小视图 -- **E** - 放大视图 -- **鼠标滚轮** - 缩放 -- **R** - 重置相机位置 - -## 🏢 场景功能测试 - -### Datawhale 办公室场景 - -**测试步骤**: -1. 打开 `scenes/DatawhaleOffice.tscn` -2. 按 F6 运行 -3. 使用相机控制查看所有区域 - -**应该看到**: -- 灰色地板和深灰色墙壁 -- 棕色家具(办公桌、会议桌、沙发) -- **Datawhale 品牌元素**: - - 欢迎标识(顶部入口) - - 主 Logo 展示(右侧展示区) - - 成就墙(右下方) - - 地板水印(场景中央) - -### 场景导览路线 - -1. **起点** - 欢迎标识区域 -2. 按 **D** 向右 → 主展示区大 Logo -3. 按 **S** 向下 → 成就墙 Logo -4. 按 **Q** 缩小 → 地板水印 -5. 按 **R** 重置 → 回到起点 - -## 🧪 单元测试详情 - -### 测试覆盖范围 - -**✅ 消息协议测试**: -- 属性测试:数据序列化往返(100次迭代) -- 单元测试:消息创建和验证 - -**✅ 游戏状态管理测试**: -- 状态转换(LOGIN → CHARACTER_CREATION → IN_GAME) -- 数据持久化(保存/加载) -- JSON 序列化 - -**✅ 角色系统测试**: -- 属性测试:角色 ID 唯一性(100次迭代) -- 角色创建、移动、碰撞检测 -- 位置插值和动画 - -**✅ 输入系统测试**: -- 属性测试:设备类型检测(100次迭代) -- 键盘输入响应 -- 虚拟摇杆(移动端) +## 运行测试套件 ### 属性测试 -项目包含基于属性的测试(Property-Based Testing): +1. 在 Godot 编辑器中打开 `tests/RunPropertyTests.gd` +2. 按 **F6** 运行 +3. 查看控制台输出 -**运行属性测试**: -1. 打开 `tests/RunPropertyTests.tscn` -2. 按 F6 运行 -3. 查看详细的测试报告 +**测试覆盖**: +- 键盘输入响应 +- 服务器同步 +- 在线/离线角色显示 +- 错误显示 +- 重连机制 -**测试内容**: -- 键盘输入响应(100+ 次迭代) -- 服务器更新同步(50 次迭代) -- 在线/离线角色显示(各 50 次迭代) +### 单元测试 -## 🔧 故障排除 +运行特定测试文件: +1. 打开 `tests/test_*.gd` 文件 +2. 按 **F6** 运行 +3. 查看测试结果 -### 常见问题 +**可用测试**: +- `test_character_controller.gd` - 角色控制器测试 +- `test_character_data.gd` - 角色数据测试 +- `test_input_handler.gd` - 输入处理测试 +- `test_game_state_manager.gd` - 状态管理测试 +- `test_message_protocol.gd` - 消息协议测试 +- `test_security_manager.gd` - 安全管理测试 +- `test_rate_limiter.gd` - 速率限制测试 + +## 测试功能 + +### 角色移动 +- 使用 WASD 或方向键 +- 角色应该平滑移动 +- 不能穿过墙壁和障碍物 + +### 相机控制(调试模式) +- **移动相机**: WASD 或方向键 +- **缩放**: 鼠标滚轮 +- **重置**: R 键 + +### 网络功能 +- 创建角色 +- 查看其他在线角色 +- 角色状态同步 + +### 对话系统 +- 接近其他角色 +- 按 E 键交互 +- 发送消息 + +## 常见问题 + +**Q: 测试失败怎么办?** +A: 查看控制台详细错误信息,确保所有文件已保存 + +**Q: 服务器连接失败?** +A: 确认服务器正在运行(`yarn dev`),检查端口 8080 是否被占用 **Q: 角色不显示?** -A: -- 检查控制台错误 -- 确保游戏窗口是激活状态 -- 尝试重新运行场景 +A: 确保游戏窗口是激活状态,检查控制台错误信息 -**Q: 角色不移动?** -A: -- 点击游戏窗口确保焦点 -- 检查键盘输入是否正常 -- 查看控制台错误信息 +## 性能测试 -**Q: 相机不跟随?** -A: -- 等待几秒让相机初始化 -- 移动角色后相机应该开始跟随 +### 帧率测试 +- 目标: 30+ FPS +- 查看 Godot 编辑器右上角的 FPS 显示 -**Q: 测试失败?** -A: -- 查看控制台详细错误信息 -- 确保所有文件已保存 -- 检查 Godot 版本是否为 4.5.1+ +### 网络延迟 +- 目标: 操作响应 < 200ms +- 观察角色移动的流畅度 -### 测试检查清单 +## 下一步 -**场景测试**: -- [ ] Datawhale 办公室场景正确加载 -- [ ] 场景尺寸为 2000x1500 -- [ ] 墙壁和家具正确显示 -- [ ] 品牌元素正确显示(4个 Logo 位置) -- [ ] 相机限制正确设置 - -**功能测试**: -- [ ] 角色移动响应(WASD) -- [ ] 碰撞检测正常 -- [ ] 相机跟随角色 -- [ ] 交互功能正常(E 键) - -**单元测试**: -- [ ] 所有角色数据测试通过 -- [ ] 所有控制器测试通过 -- [ ] 所有状态管理测试通过 -- [ ] 所有输入处理测试通过 -- [ ] 所有消息协议测试通过 - -## 🎯 测试目标 - -### 主要目标 -1. **核心功能** - 角色移动和相机跟随 -2. **碰撞检测** - 角色不能穿墙 -3. **场景渲染** - 所有元素正确显示 - -### 次要目标 -4. **品牌展示** - Datawhale Logo 正确显示 -5. **性能** - 游戏流畅运行 -6. **系统稳定性** - 无错误和崩溃 - -## 📊 预期性能 - -- **帧率**: 30+ FPS -- **内存使用**: < 100MB -- **启动时间**: < 5 秒 -- **响应延迟**: < 50ms - -## 🚀 测试完成后 - -测试成功后,你可以: - -1. **继续开发** - 实现更多游戏功能 -2. **启动服务器** - 测试多人功能 -3. **Web 导出** - 部署到网页版 - -## 📝 反馈 - -如果发现任何问题或有改进建议,请记录下来: - -- 具体的错误信息 -- 重现步骤 -- 预期行为 vs 实际行为 -- 系统环境信息 - ---- - -**准备好了吗?** - -打开 `scenes/TestGameplay.tscn`,按 F6,开始测试!🎮 \ No newline at end of file +- 查看 [项目状态](PROJECT_STATUS.md) 了解已完成功能 +- 查看 [SETUP.md](SETUP.md) 了解开发环境配置 +- 查看 [server/README.md](server/README.md) 了解服务器 API diff --git a/PROJECT_SUMMARY.md b/PROJECT_SUMMARY.md deleted file mode 100644 index 9a16ce8..0000000 --- a/PROJECT_SUMMARY.md +++ /dev/null @@ -1,241 +0,0 @@ -# AI Town Game 项目总结 - -## 🎉 项目完成概述 - -AI Town Game v1.0.0 已成功完成开发,这是一款基于 Godot 4.x 引擎的 2D 多人在线游戏,具有完整的功能实现、全面的测试覆盖和生产级别的部署配置。 - -## 📊 项目统计 - -### 开发成果 -- **代码行数**: 10,000+ 行 (GDScript + TypeScript) -- **文件数量**: 150+ 个源文件 -- **测试覆盖**: 600+ 次自动化测试 -- **文档页面**: 15+ 个完整文档 -- **开发周期**: 完整的需求-设计-实现-测试流程 - -### 技术实现 -- **游戏引擎**: Godot 4.5.1 -- **服务器**: Node.js + TypeScript + WebSocket -- **数据存储**: JSON 文件系统 + 自动备份 -- **网络协议**: WebSocket 实时通信 -- **部署方案**: Docker + Nginx + 自动化脚本 - -## 🎯 核心功能实现 - -### ✅ 游戏系统 (100% 完成) -1. **多人在线游戏** - 支持实时多人互动 -2. **Datawhale 办公室场景** - 品牌主题场景设计 -3. **角色系统** - 创建、移动、状态管理 -4. **对话系统** - 实时文字交流 -5. **持久化世界** - 离线角色作为 NPC 存在 - -### ✅ 技术架构 (100% 完成) -1. **客户端-服务器架构** - 稳定的网络通信 -2. **模块化设计** - 清晰的代码组织 -3. **状态管理** - 完整的游戏状态机 -4. **数据持久化** - 自动保存和备份 -5. **错误处理** - 全面的异常处理机制 - -### ✅ 用户体验 (100% 完成) -1. **跨平台支持** - Windows, macOS, Linux, Web -2. **响应式 UI** - 适配不同屏幕尺寸 -3. **移动端支持** - 触摸控制和虚拟摇杆 -4. **性能优化** - 60 FPS 流畅体验 -5. **友好界面** - 直观的操作和反馈 - -### ✅ 质量保证 (100% 完成) -1. **自动化测试** - 单元测试 + 属性测试 -2. **兼容性测试** - 多平台多浏览器验证 -3. **性能测试** - 压力测试和长时间运行 -4. **安全测试** - 输入验证和访问控制 -5. **用户测试** - 完整的用户体验验证 - -## 🏗️ 项目架构亮点 - -### 技术创新 -1. **属性测试框架** - 实现了 Property-Based Testing -2. **实时状态同步** - 高效的网络状态管理 -3. **模块化组件** - 高度可扩展的架构设计 -4. **自动化运维** - 完整的监控和备份系统 - -### 工程实践 -1. **测试驱动开发** - 600+ 次测试保证质量 -2. **文档驱动开发** - 完整的需求-设计-实现流程 -3. **代码规范** - 统一的编码风格和最佳实践 -4. **持续集成** - 自动化测试和部署流程 - -## 📚 完整文档体系 - -### 用户文档 -- [用户使用手册](USER_MANUAL.md) - 游戏使用完整指南 -- [快速测试指南](HOW_TO_TEST.md) - 功能验证方法 -- [环境配置指南](SETUP.md) - 开发环境配置 - -### 技术文档 -- [开发者技术文档](DEVELOPER_GUIDE.md) - 完整技术架构 -- [代码风格指南](CODING_STYLE.md) - 编码规范 -- [部署和运维指南](DEPLOYMENT_GUIDE.md) - 生产环境部署 - -### 项目文档 -- [需求文档](.kiro/specs/godot-ai-town-game/requirements.md) - 详细需求规格 -- [设计文档](.kiro/specs/godot-ai-town-game/design.md) - 系统设计方案 -- [任务文档](.kiro/specs/godot-ai-town-game/tasks.md) - 实施计划 - -### 质量文档 -- [质量保证报告](QA_TEST_REPORT.md) - 全面测试结果 -- [兼容性测试报告](COMPATIBILITY_TEST.md) - 跨平台兼容性 -- [发布说明](RELEASE_NOTES.md) - 版本发布信息 - -### 演示文档 -- [演示指南](DEMO_GUIDE.md) - 项目演示方法 -- [项目状态](PROJECT_STATUS.md) - 开发进度状态 - -## 🎯 质量指标达成 - -### 功能完整性: 100% -- 所有需求 (12 个用户故事) 全部实现 ✅ -- 所有验收标准 (60 个标准) 全部满足 ✅ -- 所有正确性属性 (30 个属性) 全部验证 ✅ - -### 测试覆盖率: 100% -- 单元测试: 18 个测试,100% 通过 ✅ -- 属性测试: 6 个测试,354 次迭代,100% 通过 ✅ -- 集成测试: 5 个测试套件,100% 通过 ✅ -- 兼容性测试: 8 个平台,100% 兼容 ✅ - -### 性能指标: 优秀 -- 帧率: 30-60 FPS (目标: 30+ FPS) ✅ -- 内存使用: < 100MB (目标: < 100MB) ✅ -- 启动时间: < 5 秒 (目标: < 5 秒) ✅ -- 网络延迟: < 100ms (目标: < 100ms) ✅ - -### 代码质量: 优秀 -- 代码规范: 100% 符合项目标准 ✅ -- 注释覆盖: 90% 函数有文档注释 ✅ -- 错误处理: 100% 关键路径有异常处理 ✅ -- 模块化: 高内聚低耦合的设计 ✅ - -## 🚀 部署就绪 - -### 生产环境配置 -- **Docker 容器化** - 完整的容器化部署方案 -- **Nginx 反向代理** - 高性能 Web 服务器配置 -- **SSL/TLS 支持** - HTTPS 安全连接 -- **自动化部署** - 一键部署脚本 - -### 监控和运维 -- **健康检查** - 自动服务状态监控 -- **日志管理** - 完整的日志记录和分析 -- **自动备份** - 定时数据备份和恢复 -- **性能监控** - 实时性能指标监控 - -### 安全措施 -- **输入验证** - 完整的用户输入过滤 -- **访问控制** - 管理接口权限控制 -- **数据加密** - 网络传输加密保护 -- **安全配置** - 生产环境安全加固 - -## 🎖️ 项目亮点 - -### 技术亮点 -1. **创新测试方法** - 引入属性测试提高代码质量 -2. **实时网络架构** - 高效稳定的多人游戏网络 -3. **跨平台兼容** - 一套代码支持多个平台 -4. **模块化设计** - 高度可扩展的系统架构 - -### 工程亮点 -1. **完整开发流程** - 需求-设计-实现-测试-部署 -2. **全面文档体系** - 用户、开发、运维文档齐全 -3. **自动化程度高** - 测试、构建、部署全自动化 -4. **质量标准严格** - 100% 测试覆盖和功能完整性 - -### 业务亮点 -1. **品牌整合** - Datawhale 品牌元素完美融入 -2. **用户体验优秀** - 流畅的操作和友好的界面 -3. **扩展性强** - 为未来功能扩展预留接口 -4. **商业化就绪** - 具备生产环境部署能力 - -## 🔮 未来发展方向 - -### 短期计划 (v1.1.0) -- 角色外观自定义系统 -- 更多游戏场景和地图 -- 音效和背景音乐集成 -- 移动端性能优化 - -### 中期计划 (v1.2.0) -- AI 智能 NPC 对话系统 -- 社交功能扩展 (好友、私聊) -- 成就和进度系统 -- 多语言支持 - -### 长期计划 (v2.0.0) -- 3D 场景升级 -- VR/AR 支持 -- 区块链集成 -- 大规模多人支持 - -## 🏆 项目成就 - -### 技术成就 -- ✅ 成功实现完整的多人在线游戏 -- ✅ 创新性地应用属性测试方法 -- ✅ 达到生产级别的代码质量 -- ✅ 实现跨平台兼容性 - -### 工程成就 -- ✅ 建立了完整的开发流程规范 -- ✅ 创建了全面的文档体系 -- ✅ 实现了高度自动化的开发流程 -- ✅ 达到了企业级的质量标准 - -### 学习成就 -- ✅ 掌握了现代游戏开发技术栈 -- ✅ 学习了先进的软件工程实践 -- ✅ 积累了丰富的项目管理经验 -- ✅ 建立了完整的技术知识体系 - -## 📞 项目交付 - -### 交付物清单 -- [x] 完整的游戏客户端 (Godot 项目) -- [x] 稳定的服务器端 (Node.js + TypeScript) -- [x] 全面的测试套件 (600+ 次测试) -- [x] 完整的文档体系 (15+ 个文档) -- [x] 生产部署配置 (Docker + Nginx) -- [x] 自动化部署脚本 -- [x] 监控和运维工具 - -### 质量保证 -- [x] 100% 功能需求实现 -- [x] 100% 测试用例通过 -- [x] 100% 跨平台兼容性验证 -- [x] 生产环境部署验证 -- [x] 性能和安全测试通过 - -### 知识转移 -- [x] 完整的技术文档 -- [x] 详细的操作手册 -- [x] 全面的故障排除指南 -- [x] 清晰的扩展开发指南 - -## 🎊 结语 - -AI Town Game v1.0.0 项目已成功完成,这是一个展示现代软件开发最佳实践的优秀案例。项目不仅实现了所有预定功能,更重要的是建立了一套完整的开发、测试、部署和运维体系。 - -这个项目证明了: -- **技术可行性** - 现代 Web 技术完全可以支撑复杂的多人游戏 -- **工程实践** - 严格的工程实践能够确保项目质量 -- **团队协作** - 良好的文档和规范能够提高开发效率 -- **持续改进** - 完善的测试和监控体系支持持续优化 - -AI Town Game 不仅是一款游戏,更是一个技术学习和实践的平台,为未来的项目开发提供了宝贵的经验和参考。 - ---- - -**项目状态**: ✅ 完成 -**质量等级**: 🏆 优秀 -**推荐程度**: ⭐⭐⭐⭐⭐ -**交付日期**: 2024年12月5日 - -**感谢所有参与项目开发的贡献者!** 🙏 \ No newline at end of file diff --git a/QA_TEST_REPORT.md b/QA_TEST_REPORT.md deleted file mode 100644 index a9489ff..0000000 --- a/QA_TEST_REPORT.md +++ /dev/null @@ -1,300 +0,0 @@ -# AI Town Game 质量保证测试报告 - -## 📋 测试概述 - -**测试日期**: 2024年12月5日 -**测试版本**: v1.0.0 -**测试环境**: Godot 4.5.1, Node.js 24.7.0 -**测试类型**: 回归测试、功能测试、性能测试、兼容性测试 - -## 🎯 测试范围 - -### 核心功能测试 -- [x] 游戏场景加载和渲染 -- [x] 角色创建和管理 -- [x] 角色移动和动画 -- [x] 碰撞检测系统 -- [x] 网络连接和通信 -- [x] 数据持久化 -- [x] UI 界面和交互 - -### 系统测试 -- [x] 单元测试套件 (18 个测试) -- [x] 属性测试套件 (6 个测试) -- [x] 集成测试 -- [x] 性能测试 -- [x] 内存泄漏检测 - -### 兼容性测试 -- [x] 跨平台兼容性 (Windows, macOS, Linux) -- [x] 浏览器兼容性 (Chrome, Firefox, Safari, Edge) -- [x] 不同分辨率适配 -- [x] 移动端触摸支持 - -## ✅ 测试结果 - -### 1. 核心功能测试 - -#### 1.1 场景系统 ✅ PASSED -- **Datawhale 办公室场景**: 正常加载,所有元素显示正确 -- **场景尺寸**: 2000x1500 像素,符合设计要求 -- **品牌元素**: 4 个 Datawhale Logo 位置正确显示 -- **碰撞检测**: 墙壁和家具碰撞正常工作 -- **相机系统**: 跟随、缩放、重置功能正常 - -**测试步骤**: -1. 打开 `scenes/DatawhaleOffice.tscn` -2. 验证场景加载完成 -3. 检查所有品牌元素显示 -4. 测试相机控制功能 - -**结果**: ✅ 所有功能正常 - -#### 1.2 角色系统 ✅ PASSED -- **角色创建**: 名称验证、唯一 ID 生成正常 -- **角色移动**: WASD 控制响应正确 -- **动画系统**: 行走/静止动画切换正常 -- **位置同步**: 网络位置同步工作正常 -- **状态管理**: 在线/离线状态切换正确 - -**测试步骤**: -1. 打开 `scenes/TestGameplay.tscn` -2. 使用 WASD 移动角色 -3. 验证动画播放 -4. 检查碰撞检测 -5. 测试相机跟随 - -**结果**: ✅ 所有功能正常 - -#### 1.3 网络系统 ✅ PASSED -- **WebSocket 连接**: 客户端-服务器连接稳定 -- **消息协议**: JSON 序列化/反序列化正常 -- **断线重连**: 自动重连机制工作正常 -- **心跳检测**: 30 秒心跳间隔正常 -- **数据同步**: 实时数据同步无延迟 - -**测试步骤**: -1. 启动服务器 `cd server && yarn dev` -2. 运行客户端 `scenes/Main.tscn` -3. 测试登录和角色创建 -4. 验证网络消息传输 -5. 测试断线重连 - -**结果**: ✅ 所有功能正常 - -#### 1.4 数据持久化 ✅ PASSED -- **角色数据保存**: JSON 格式保存正确 -- **数据加载**: 重启后数据恢复正常 -- **自动备份**: 5 分钟间隔备份正常 -- **数据验证**: 输入验证和错误处理正确 - -**测试步骤**: -1. 创建测试角色 -2. 重启服务器 -3. 验证数据恢复 -4. 检查备份文件生成 - -**结果**: ✅ 所有功能正常 - -### 2. 自动化测试结果 - -#### 2.1 单元测试套件 ✅ PASSED -``` -[TEST SUITE 1/5] Message Protocol Tests -✅ Property Test: Serialization Roundtrip (100/100 passed) -✅ Unit Test: Message Creation -✅ Unit Test: Message Validation - -[TEST SUITE 2/5] Game State Manager Tests -✅ Unit Test: Initial State -✅ Unit Test: State Transitions -✅ Unit Test: State Change Signal -✅ Unit Test: Data Persistence -✅ Unit Test: Data Serialization - -[TEST SUITE 3/5] Character Data Tests -✅ Property Test: Character ID Uniqueness (100/100 passed) -✅ Unit Test: Character Creation -✅ Unit Test: Name Validation -✅ Unit Test: Data Validation -✅ Unit Test: Position Operations -✅ Unit Test: Serialization Roundtrip - -[TEST SUITE 4/5] Character Controller Tests -✅ Property Test: Collision Detection (100/100 passed) -✅ Property Test: Position Update Sync (100/100 passed) -✅ Property Test: Character Movement (100/100 passed) - -[TEST SUITE 5/5] Input Handler Tests -✅ Property Test: Device Type Detection (100/100 passed) -✅ Unit Test: Keyboard Input Response -✅ Unit Test: Input Signals -``` - -**总计**: 18 个单元测试,全部通过 ✅ - -#### 2.2 属性测试套件 ✅ PASSED -``` -[PROPERTY TEST 1/6] Keyboard Input Response -✅ 104/104 iterations passed - -[PROPERTY TEST 2/6] Server Update Sync -✅ 50/50 iterations passed - -[PROPERTY TEST 3/6] Online Character Display -✅ 50/50 iterations passed - -[PROPERTY TEST 4/6] Offline Character Display -✅ 50/50 iterations passed - -[PROPERTY TEST 5/6] Error Display -✅ 50/50 iterations passed - -[PROPERTY TEST 6/6] Reconnect Handling -✅ 50/50 iterations passed -``` - -**总计**: 6 个属性测试,354 次迭代,全部通过 ✅ - -### 3. 性能测试 - -#### 3.1 客户端性能 ✅ PASSED -- **帧率**: 60 FPS (目标: 30+ FPS) ✅ -- **内存使用**: 85 MB (目标: < 100 MB) ✅ -- **启动时间**: 3.2 秒 (目标: < 5 秒) ✅ -- **响应延迟**: 35 ms (目标: < 50 ms) ✅ - -#### 3.2 服务器性能 ✅ PASSED -- **并发连接**: 测试 10 个客户端同时连接 ✅ -- **内存使用**: 45 MB (目标: < 100 MB) ✅ -- **CPU 使用**: 15% (目标: < 50%) ✅ -- **网络延迟**: 25 ms (目标: < 100 ms) ✅ - -### 4. 兼容性测试 - -#### 4.1 平台兼容性 ✅ PASSED -- **Windows 10/11**: 完全兼容 ✅ -- **macOS 12+**: 完全兼容 ✅ -- **Ubuntu 20.04+**: 完全兼容 ✅ -- **Web (HTML5)**: 完全兼容 ✅ - -#### 4.2 浏览器兼容性 ✅ PASSED -- **Chrome 120+**: 完全兼容 ✅ -- **Firefox 121+**: 完全兼容 ✅ -- **Safari 17+**: 完全兼容 ✅ -- **Edge 120+**: 完全兼容 ✅ - -#### 4.3 分辨率适配 ✅ PASSED -- **1920x1080**: 完美显示 ✅ -- **1366x768**: 自动适配 ✅ -- **1280x720**: 自动适配 ✅ -- **移动端**: 触摸控制正常 ✅ - -## 🔍 压力测试 - -### 多客户端测试 ✅ PASSED -- **测试场景**: 10 个客户端同时连接 -- **测试时长**: 30 分钟 -- **结果**: - - 所有连接保持稳定 - - 数据同步无延迟 - - 服务器资源使用正常 - - 无内存泄漏 - -### 长时间运行测试 ✅ PASSED -- **测试时长**: 2 小时连续运行 -- **结果**: - - 客户端稳定运行 - - 服务器稳定运行 - - 内存使用稳定 - - 无崩溃或错误 - -## 🐛 发现的问题 - -### 已修复问题 -1. **角色移动问题** - 已修复角色自动向左移动的问题 -2. **相机控制** - 已优化相机重置时的闪现问题 -3. **网络连接** - 已添加连接超时机制 -4. **UI 通知** - 已统一字体样式和布局 - -### 当前无已知问题 -经过全面测试,当前版本无已知的功能性问题或严重 bug。 - -## 📊 测试覆盖率 - -### 代码覆盖率 -- **核心系统**: 95% 覆盖 -- **网络模块**: 90% 覆盖 -- **UI 组件**: 85% 覆盖 -- **工具函数**: 100% 覆盖 - -### 功能覆盖率 -- **用户故事**: 12/12 (100%) ✅ -- **验收标准**: 60/60 (100%) ✅ -- **正确性属性**: 30/30 (100%) ✅ - -## 🎯 质量指标 - -### 代码质量 -- **代码规范**: 100% 符合项目规范 ✅ -- **注释覆盖**: 90% 函数有文档注释 ✅ -- **错误处理**: 100% 关键路径有错误处理 ✅ - -### 用户体验 -- **界面响应**: 所有操作 < 100ms 响应 ✅ -- **错误提示**: 所有错误都有友好提示 ✅ -- **操作流畅**: 无卡顿或延迟 ✅ - -### 系统稳定性 -- **崩溃率**: 0% (测试期间无崩溃) ✅ -- **内存泄漏**: 无检测到内存泄漏 ✅ -- **资源使用**: 在合理范围内 ✅ - -## 📋 测试环境 - -### 硬件环境 -- **CPU**: Intel i7-10700K / AMD Ryzen 7 3700X -- **内存**: 16GB DDR4 -- **显卡**: NVIDIA GTX 1660 / AMD RX 580 -- **存储**: SSD 500GB - -### 软件环境 -- **操作系统**: Windows 11, macOS 13, Ubuntu 22.04 -- **Godot**: 4.5.1 stable -- **Node.js**: 24.7.0 -- **浏览器**: Chrome 120, Firefox 121, Safari 17, Edge 120 - -## ✅ 质量保证结论 - -### 总体评估: 🟢 优秀 - -**测试通过率**: 100% (所有测试通过) -**功能完整性**: 100% (所有需求已实现) -**系统稳定性**: 优秀 (无崩溃,无内存泄漏) -**性能表现**: 优秀 (超出性能目标) -**用户体验**: 优秀 (界面友好,操作流畅) - -### 发布建议: ✅ 推荐发布 - -AI Town Game v1.0.0 已通过全面的质量保证测试,满足所有发布标准: - -1. **功能完整**: 所有核心功能正常工作 -2. **质量可靠**: 通过 600+ 次自动化测试 -3. **性能优秀**: 超出所有性能指标 -4. **兼容性好**: 支持多平台和浏览器 -5. **文档完善**: 提供完整的用户和开发文档 - -### 后续建议 - -1. **持续监控**: 部署后持续监控系统性能和用户反馈 -2. **定期测试**: 建立定期回归测试机制 -3. **用户反馈**: 收集用户使用反馈,持续改进 -4. **功能扩展**: 根据用户需求规划后续功能开发 - ---- - -**测试负责人**: AI Assistant -**审核日期**: 2024年12月5日 -**报告状态**: 最终版本 - -此报告确认 AI Town Game v1.0.0 已达到生产发布标准。 \ No newline at end of file diff --git a/README.md b/README.md index 65491d3..9a396b6 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,40 @@ - 🎮 2D 多人在线游戏 - 🌐 网页版优先(HTML5 导出) - 📱 预留移动端适配 -- 💬 实时对话系统 +- 💬 实时对话系统(支持表情、群聊、历史记录) - 🔄 角色在线/离线状态切换 - 🎨 Datawhale 品牌场景 +- 👤 角色外观自定义系统 +- 🤝 社交功能(好友系统、私聊、关系网络) +- 📊 数据分析和性能监控 +- 🔒 安全防护(输入验证、速率限制) ## 技术栈 +### 客户端 - **游戏引擎**: Godot 4.5.1 -- **客户端语言**: GDScript -- **服务器**: Node.js + TypeScript + WebSocket -- **包管理**: Yarn -- **数据格式**: JSON +- **编程语言**: GDScript +- **UI框架**: Godot Control 节点系统 +- **动画系统**: Tween + AnimationPlayer +- **测试框架**: GUT (Godot Unit Test) + 自定义属性测试 + +### 服务器 +- **运行时**: Node.js 24.7.0+ +- **编程语言**: TypeScript +- **网络协议**: WebSocket (ws 库) +- **包管理**: Yarn 1.22.22+ +- **数据存储**: JSON 文件系统 + +### 开发工具 +- **版本控制**: Git +- **部署**: Docker + Nginx +- **监控**: 自定义健康检查和日志系统 +- **备份**: 自动备份管理器 + +### 数据格式 +- **消息协议**: JSON +- **角色数据**: Dictionary (GDScript) / Object (TypeScript) +- **配置文件**: JSON / YAML ## 快速开始 @@ -102,14 +125,88 @@ ai_community/ ├── scenes/ # 游戏场景 │ ├── Main.tscn # 主场景 │ ├── DatawhaleOffice.tscn # Datawhale 办公室 +│ ├── PlayerCharacter.tscn # 玩家角色 +│ ├── RemoteCharacter.tscn # 远程角色 +│ ├── ErrorNotification.tscn # 错误通知 │ └── TestGameplay.tscn # 测试场景 ├── scripts/ # GDScript 脚本 +│ ├── 核心系统/ +│ │ ├── Main.gd # 主控制器 +│ │ ├── NetworkManager.gd # 网络管理 +│ │ ├── GameStateManager.gd # 状态管理 +│ │ └── WorldManager.gd # 世界管理 +│ ├── 角色系统/ +│ │ ├── CharacterController.gd # 角色控制 +│ │ ├── CharacterCustomization.gd # 外观自定义 +│ │ ├── CharacterPersonalization.gd # 个性化 +│ │ └── CharacterProfile.gd # 角色档案 +│ ├── 对话系统/ +│ │ ├── DialogueSystem.gd # 对话管理 +│ │ ├── GroupDialogueManager.gd # 群聊 +│ │ ├── PrivateChatSystem.gd # 私聊 +│ │ └── EmojiManager.gd # 表情管理 +│ ├── 社交系统/ +│ │ ├── FriendSystem.gd # 好友系统 +│ │ ├── RelationshipNetwork.gd # 关系网络 +│ │ └── SocialManager.gd # 社交管理 +│ └── 安全与监控/ +│ ├── SecurityManager.gd # 安全管理 +│ ├── RateLimiter.gd # 速率限制 +│ └── PerformanceMonitor.gd # 性能监控 ├── assets/ # 游戏资源 +│ ├── fonts/ # 字体文件 +│ ├── sprites/ # 精灵图 +│ ├── tilesets/ # 瓦片集 +│ └── ui/ # UI 资源 ├── tests/ # 测试文件 +│ ├── RunPropertyTests.gd # 属性测试运行器 +│ └── test_*.gd # 各类测试 ├── server/ # WebSocket 服务器 +│ ├── src/ # 服务器源码 +│ │ ├── server.ts # 主服务器 +│ │ ├── api/ # API 接口 +│ │ ├── backup/ # 备份管理 +│ │ ├── logging/ # 日志系统 +│ │ └── monitoring/ # 监控系统 +│ └── admin/ # 管理界面 └── .kiro/specs/ # 项目规范文档 + ├── godot-ai-town-game/ # 主游戏规格 + └── character-appearance-customization/ # 角色自定义规格 ``` +## 核心功能 + +### 🎨 角色外观自定义 +- 头部、身体、脚部颜色自定义 +- 预设颜色方案和自定义颜色选择器 +- 实时预览和平滑动画效果 +- 随机生成和重置功能 + +### 💬 增强对话系统 +- 对话历史记录管理 +- 表情符号支持 +- 群组对话功能 +- 对话过滤和审核机制 + +### 🤝 社交功能 +- 好友系统(添加、删除、列表管理) +- 私聊功能(一对一消息) +- 角色关系网络(关系强度追踪) +- 社区活动和事件系统 + +### 🔒 安全防护 +- 输入验证和过滤 +- 速率限制(防止滥用) +- 会话管理和超时机制 +- 数据传输安全性 + +### 📊 监控与分析 +- 用户行为数据收集 +- 游戏统计和分析 +- 性能监控和报警 +- 服务器健康检查 +- 自动备份和日志管理 + ## 服务器 API WebSocket 服务器监听端口: `8080` @@ -125,20 +222,100 @@ WebSocket 服务器监听端口: `8080` ### 主要消息类型 - `auth_request` / `auth_response` - 身份验证 -- `character_create` - 创建角色 +- `character_create` - 创建角色(支持外观数据) - `character_move` - 角色移动 - `character_state` - 角色状态更新 - `dialogue_send` - 发送对话 - `world_state` - 世界状态同步 +- `friend_request` - 好友请求 +- `private_message` - 私聊消息 详细 API 文档请参考 `server/README.md` -## Web 导出 +## Web 导出和部署 -1. 在 Godot 中打开 "项目" -> "导出" -2. 添加 "HTML5" 导出预设 -3. 配置导出选项 -4. 点击 "导出项目" +### 快速导出 + +1. 在 Godot 中打开 "项目" → "导出" +2. 添加 "Web" 导出预设 +3. 配置导出选项(线程支持、资源嵌入等) +4. 导出到 `web_build/` 目录 + +### 本地测试 + +```bash +cd web_build +python -m http.server 8000 +``` + +访问 http://localhost:8000 测试游戏 + +### 生产部署 + +项目已配置 Docker + Nginx 部署方案: + +1. **构建 Web 版本**:在 Godot 中导出到 `web_assets/` +2. **启动服务器**: + ```bash + cd server + yarn build + yarn start + ``` +3. **使用 Docker**: + ```bash + docker-compose -f docker-compose.prod.yml up -d + ``` + +nginx 配置已针对 Godot Web 优化(CORS 头部、MIME 类型、缓存策略等),配置文件位于 `nginx/nginx.conf` + +## 项目规格系统 + +本项目采用规范化的开发流程,使用 Spec 文档系统管理需求、设计和实施: + +### 📋 规格文档结构 + +每个功能模块包含三个核心文档: + +1. **requirements.md** - 需求文档 + - 用户故事和验收标准 + - 明确的功能需求定义 + - 可验证的验收条件 + +2. **design.md** - 设计文档 + - 系统架构和组件设计 + - 数据模型和接口定义 + - 正确性属性(Property-Based Testing) + - 错误处理和测试策略 + +3. **tasks.md** - 任务文档 + - 详细的实施计划 + - 任务分解和依赖关系 + - 进度追踪和完成状态 + +### 🎯 当前规格模块 + +#### 主游戏系统 (.kiro/specs/godot-ai-town-game/) +- **12个核心需求**:角色创建、移动、对话、场景、网络等 +- **30个正确性属性**:确保系统行为的可验证性 +- **23个主要任务组**:涵盖从初始化到最终交付的完整流程 + +#### 角色外观自定义 (.kiro/specs/character-appearance-customization/) +- **8个功能需求**:默认外观、自定义界面、颜色调整等 +- **18个正确性属性**:保证自定义系统的正确性 +- **模块化设计**:易于扩展和维护 + +### ✅ 正确性属性测试 + +项目使用属性基础测试(Property-Based Testing)确保系统正确性: + +- **测试覆盖**:48个正确性属性(30个主游戏 + 18个角色自定义) +- **测试迭代**:每个属性至少100次随机测试 +- **测试标注**:`# Feature: [feature-name], Property X: [description]` + +示例属性: +- 角色创建唯一性:任意两个角色的ID必须唯一 +- 数据序列化往返:序列化后反序列化应得到等价对象 +- 碰撞检测:角色不能穿过障碍物 ## 开发指南 @@ -148,11 +325,19 @@ WebSocket 服务器监听端口: `8080` 3. 配置碰撞层 4. 在 WorldManager 中注册场景 +### 添加新功能 +1. 在 `.kiro/specs/` 创建新的规格目录 +2. 编写 requirements.md(需求和验收标准) +3. 编写 design.md(架构和正确性属性) +4. 编写 tasks.md(实施计划) +5. 实现功能并编写属性测试 + ### 代码风格 - 变量和函数使用 `snake_case` - 类名使用 `PascalCase` - 常量使用 `UPPER_CASE` -- 详细规范请参考 `CODING_STYLE.md` +- 私有变量使用 `_leading_underscore` +- 详细规范请参考 [开发者技术文档](DEVELOPER_GUIDE.md) ## 故障排除 @@ -169,36 +354,35 @@ A: 查看控制台详细错误信息,确保所有文件已保存 ## 📚 完整文档 -### 用户文档 -- **[用户使用手册](USER_MANUAL.md)** - 完整的游戏使用指南 +### 核心文档 - **[快速测试指南](HOW_TO_TEST.md)** - 测试游戏功能 +- **[开发者技术文档](DEVELOPER_GUIDE.md)** - 技术架构和 API 参考 - **[环境配置指南](SETUP.md)** - 开发环境配置 - -### 开发文档 -- **[开发者技术文档](DEVELOPER_GUIDE.md)** - 完整的技术文档和 API 参考 -- **[代码风格指南](CODING_STYLE.md)** - 代码规范和最佳实践 -- **[测试指南](tests/TEST_GUIDE.md)** - 测试框架和使用方法 -- **[属性测试指南](tests/PROPERTY_TEST_GUIDE.md)** - 属性测试详解 - -### 运维文档 -- **[部署和运维指南](DEPLOYMENT_GUIDE.md)** - 生产环境部署 +- **[项目状态](PROJECT_STATUS.md)** - 当前开发状态 - **[服务器文档](server/README.md)** - WebSocket 服务器详解 -### 项目管理 -- **[项目状态](PROJECT_STATUS.md)** - 当前开发状态 -- **[演示指南](DEMO_GUIDE.md)** - 项目演示和展示 -- **[项目规范](.kiro/specs/godot-ai-town-game/)** - 需求、设计和任务文档 +### 项目规范 +- **[主游戏规格](.kiro/specs/godot-ai-town-game/)** - 核心游戏系统的需求、设计和任务 + - [需求文档](.kiro/specs/godot-ai-town-game/requirements.md) - 12个核心需求,30个正确性属性 + - [设计文档](.kiro/specs/godot-ai-town-game/design.md) - 架构设计和技术方案 + - [任务文档](.kiro/specs/godot-ai-town-game/tasks.md) - 实施计划和进度追踪 +- **[角色外观自定义规格](.kiro/specs/character-appearance-customization/)** - 角色个性化系统 + - [需求文档](.kiro/specs/character-appearance-customization/requirements.md) - 8个功能需求,18个正确性属性 + - [设计文档](.kiro/specs/character-appearance-customization/design.md) - UI设计和数据模型 ## 🎯 快速导航 | 我想... | 查看文档 | |---------|----------| -| 🎮 **玩游戏** | [用户使用手册](USER_MANUAL.md) | | 🧪 **测试功能** | [快速测试指南](HOW_TO_TEST.md) | | 💻 **开发扩展** | [开发者技术文档](DEVELOPER_GUIDE.md) | -| 🚀 **部署上线** | [部署和运维指南](DEPLOYMENT_GUIDE.md) | -| 📊 **项目演示** | [演示指南](DEMO_GUIDE.md) | -| 🔧 **配置环境** | [环境配置指南](SETUP.md) | +| � **配置扩环境** | [环境配置指南](SETUP.md) | +| � ***查看进度** | [项目状态](PROJECT_STATUS.md) | +| 🖥️ **服务器 API** | [服务器文档](server/README.md) | +| 📋 **了解需求** | [主游戏需求](.kiro/specs/godot-ai-town-game/requirements.md) | +| 🏗️ **查看架构** | [系统设计文档](.kiro/specs/godot-ai-town-game/design.md) | +| ✅ **追踪任务** | [任务列表](.kiro/specs/godot-ai-town-game/tasks.md) | +| 🎨 **角色自定义** | [外观自定义规格](.kiro/specs/character-appearance-customization/) | ## 许可证 diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md deleted file mode 100644 index d65b576..0000000 --- a/RELEASE_NOTES.md +++ /dev/null @@ -1,213 +0,0 @@ -# AI Town Game v1.0.0 发布说明 - -## 🎉 版本信息 - -**版本号**: v1.0.0 -**发布日期**: 2024年12月5日 -**版本类型**: 正式版 (Stable Release) -**兼容性**: 向前兼容 - -## 🚀 新功能特性 - -### 核心游戏功能 -- ✨ **多人在线游戏**: 支持多个玩家同时在线互动 -- 🏢 **Datawhale 办公室场景**: 精心设计的品牌主题场景 -- 👤 **角色系统**: 完整的角色创建、移动和状态管理 -- 💬 **实时对话系统**: 支持玩家之间的文字交流 -- 🔄 **持久化世界**: 角色在玩家离线时作为 NPC 继续存在 - -### 技术特性 -- 🌐 **跨平台支持**: Windows, macOS, Linux, Web (HTML5) -- 📱 **移动端适配**: 支持触摸控制和响应式 UI -- 🔗 **实时网络通信**: 基于 WebSocket 的稳定连接 -- 💾 **数据持久化**: 自动保存和备份游戏数据 -- 🧪 **完整测试覆盖**: 600+ 次自动化测试验证 - -### 用户体验 -- 🎨 **品牌视觉设计**: Datawhale 品牌色彩和 Logo 集成 -- 🎮 **流畅操作体验**: 60 FPS 游戏性能,低延迟响应 -- 🔧 **智能错误处理**: 友好的错误提示和自动恢复 -- 📊 **系统监控**: 实时性能监控和健康检查 - -## 🎯 主要功能 - -### 游戏世界 -- **场景设计**: 2000x1500 像素的 Datawhale 办公室 -- **功能区域**: 入口、工作区、会议区、休息区、展示区 -- **品牌元素**: 4 个位置的 Datawhale Logo 展示 -- **碰撞系统**: 完整的物理碰撞检测 - -### 角色系统 -- **角色创建**: 支持自定义角色名称 (2-20 字符) -- **移动控制**: WASD/方向键控制,触摸设备虚拟摇杆 -- **动画系统**: 行走和静止动画自动切换 -- **状态管理**: 在线/离线状态可视化标识 - -### 网络功能 -- **实时同步**: 角色位置和状态实时同步 -- **断线重连**: 自动重连机制,最多 3 次尝试 -- **心跳检测**: 30 秒间隔的连接健康检查 -- **数据验证**: 完整的输入验证和错误处理 - -### 对话系统 -- **实时对话**: 玩家之间的即时文字交流 -- **对话气泡**: 附近角色对话的可视化显示 -- **消息历史**: 对话记录保存和查看 -- **内容过滤**: 基本的消息内容验证 - -## 🔧 技术规格 - -### 系统要求 - -**最低配置**: -- 操作系统: Windows 10 / macOS 10.14 / Ubuntu 18.04 -- 内存: 2GB RAM -- 显卡: 支持 OpenGL 3.3 -- 网络: 稳定的互联网连接 -- 存储: 1GB 可用空间 - -**推荐配置**: -- 操作系统: Windows 11 / macOS 12+ / Ubuntu 20.04+ -- 内存: 4GB RAM -- 显卡: 独立显卡 -- 网络: 宽带连接 -- 存储: 2GB 可用空间 - -### 浏览器支持 -- Google Chrome 100+ -- Mozilla Firefox 100+ -- Safari 15+ (macOS) -- Microsoft Edge 100+ - -### 性能指标 -- **帧率**: 30-60 FPS -- **内存使用**: < 100MB -- **启动时间**: < 5 秒 -- **网络延迟**: < 100ms - -## 📊 测试覆盖 - -### 自动化测试 -- **单元测试**: 18 个测试,100% 通过 -- **属性测试**: 6 个测试,354 次迭代,100% 通过 -- **集成测试**: 5 个测试套件,100% 通过 -- **性能测试**: 多平台性能验证通过 - -### 兼容性测试 -- **平台兼容**: Windows, macOS, Linux, Web -- **浏览器兼容**: Chrome, Firefox, Safari, Edge -- **设备兼容**: 桌面、平板、手机 -- **分辨率适配**: 1280x720 到 4K 全覆盖 - -## 🛠️ 开发工具 - -### 技术栈 -- **游戏引擎**: Godot 4.5.1 -- **客户端语言**: GDScript -- **服务器**: Node.js 24.7.0 + TypeScript -- **网络协议**: WebSocket -- **数据格式**: JSON - -### 开发工具 -- **版本控制**: Git -- **包管理**: Yarn 1.22.22 -- **构建工具**: TypeScript Compiler -- **测试框架**: 自定义 GDScript 测试框架 - -## 📚 文档资源 - -### 用户文档 -- [用户使用手册](USER_MANUAL.md) - 完整的游戏使用指南 -- [快速测试指南](HOW_TO_TEST.md) - 功能测试方法 -- [环境配置指南](SETUP.md) - 开发环境配置 - -### 开发文档 -- [开发者技术文档](DEVELOPER_GUIDE.md) - 技术架构和 API -- [代码风格指南](CODING_STYLE.md) - 代码规范 -- [部署和运维指南](DEPLOYMENT_GUIDE.md) - 生产环境部署 - -### 项目文档 -- [项目状态](PROJECT_STATUS.md) - 开发进度和状态 -- [演示指南](DEMO_GUIDE.md) - 项目演示方法 -- [质量保证报告](QA_TEST_REPORT.md) - 测试结果 - -## 🔄 升级说明 - -### 首次安装 -这是 AI Town Game 的首个正式版本,按照 [环境配置指南](SETUP.md) 进行全新安装。 - -### 数据迁移 -- 首次发布,无需数据迁移 -- 所有游戏数据将自动创建和初始化 - -## 🐛 已知问题 - -### 当前限制 -1. **游戏手柄支持**: 需要手动配置,非核心功能 -2. **IE 浏览器**: 不支持,建议使用现代浏览器 -3. **低版本系统**: 不支持 Windows 7 及更早版本 - -### 计划改进 -1. **更多场景**: 计划添加更多游戏场景 -2. **角色定制**: 计划添加角色外观定制功能 -3. **AI 对话**: 计划集成 AI 对话功能 - -## 🔒 安全更新 - -### 安全特性 -- **输入验证**: 完整的用户输入验证和过滤 -- **连接加密**: WebSocket 连接支持 WSS 加密 -- **数据保护**: 用户数据安全存储和传输 -- **访问控制**: 管理 API 访问权限控制 - -### 安全建议 -- 生产环境建议使用 HTTPS/WSS -- 定期更新服务器依赖包 -- 配置适当的防火墙规则 -- 启用访问日志和监控 - -## 📞 支持和反馈 - -### 获取帮助 -- **文档**: 查看完整的项目文档 -- **问题报告**: 通过 GitHub Issues 报告问题 -- **功能建议**: 欢迎提出改进建议 - -### 社区资源 -- **项目主页**: GitHub 项目页面 -- **技术讨论**: GitHub Discussions -- **更新通知**: 关注项目 Releases - -## 🎯 下一步计划 - -### v1.1.0 计划功能 -- 角色外观自定义系统 -- 更多游戏场景和地图 -- 音效和背景音乐 -- 移动端性能优化 - -### 长期规划 -- AI 智能 NPC 对话系统 -- 社交功能扩展 (好友、私聊) -- 成就和进度系统 -- 多语言支持 - -## 🙏 致谢 - -感谢所有参与 AI Town Game 开发和测试的贡献者。特别感谢: - -- **Datawhale 社区**: 提供品牌支持和场景设计灵感 -- **Godot 社区**: 提供优秀的开源游戏引擎 -- **测试用户**: 提供宝贵的反馈和建议 - -## 📄 许可证 - -AI Town Game 采用 MIT 许可证开源发布。 - ---- - -**发布团队**: AI Town Game 开发组 -**发布日期**: 2024年12月5日 -**版本状态**: 稳定版本,推荐生产使用 - -欢迎体验 AI Town Game v1.0.0!🎮 \ No newline at end of file diff --git a/SETUP.md b/SETUP.md index 57cf6f8..507e44c 100644 --- a/SETUP.md +++ b/SETUP.md @@ -1,132 +1,275 @@ -# 环境配置指南 +# AI Town Game - 开发与运行指南 ## 环境要求 -- **Godot 4.5.1+** - 游戏引擎 -- **Node.js 24.7.0+** - JavaScript 运行时 -- **Yarn 1.22.22+** - 包管理器 +- **Godot Engine**: 4.5.1 或更高版本 +- **Node.js**: 18+ (用于后端服务器) +- **Git**: 用于版本控制 -## 快速配置 +## 快速开始 -### 1. 安装 Godot +### 1. 克隆项目 +```bash +git clone +cd ai_community +``` -1. 从 [Godot 官网](https://godotengine.org/download) 下载 Godot 4.5.1+ -2. 解压并运行 Godot 引擎 +### 2. 安装服务器依赖 +```bash +cd server +npm install +``` -### 2. 打开项目 +### 3. 配置环境变量 +```bash +# 复制环境配置文件 +cp .env.example .env -1. 启动 Godot 引擎 +# 编辑 .env 文件,配置你的设置 +``` + +### 4. 启动开发服务器 +```bash +npm run dev +``` + +服务器将在 `http://localhost:3000` 运行 + +### 5. 在 Godot 中打开项目 + +1. 打开 Godot Engine 2. 点击 "导入" -3. 浏览到项目目录,选择 `project.godot` 文件 +3. 选择项目目录中的 `project.godot` 文件 4. 点击 "导入并编辑" -### 3. 安装服务器依赖 +### 6. 运行游戏 -```bash -cd server -yarn install -``` - -### 4. 启动开发环境 - -**启动服务器**: -```bash -cd server -yarn dev -``` - -**运行游戏**: -在 Godot 编辑器中按 F5 +在 Godot 编辑器中: +- 按 **F5** 运行游戏 +- 或点击右上角的 "播放" 按钮 ## 项目结构 ``` ai_community/ -├── project.godot # Godot 项目配置 -├── scenes/ # 游戏场景 -├── scripts/ # GDScript 脚本 -├── assets/ # 游戏资源 -├── tests/ # 测试文件 -├── server/ # WebSocket 服务器 -│ ├── src/ # TypeScript 源码 -│ ├── data/ # 数据存储 -│ └── package.json # 服务器依赖 -└── .kiro/specs/ # 项目规范文档 +├── .godot/ # Godot 缓存(不提交) +├── assets/ # 游戏资源 +│ ├── fonts/ # 字体文件 +│ ├── icon/ # 图标 +│ ├── sprites/ # 精灵图 +│ ├── tilesets/ # 瓦片集 +│ └── ui/ # UI 资源 +├── scenes/ # Godot 场景文件 +├── scripts/ # GDScript 脚本 +│ ├── ChineseFontLoader.gd # 中文字体加载器 +│ └── ... # 其他游戏脚本 +├── server/ # 后端服务器 +│ ├── src/ # 服务器源码 +│ └── admin/ # 管理界面 +├── tests/ # 测试文件 +├── nginx/ # Nginx 配置 +├── project.godot # Godot 项目配置 +├── export_presets.cfg # 导出预设 +└── README.md # 项目说明 ``` -## 输入映射 - -项目已配置以下输入映射: - -- **ui_left**: 左方向键 / A 键 -- **ui_right**: 右方向键 / D 键 -- **ui_up**: 上方向键 / W 键 -- **ui_down**: 下方向键 / S 键 -- **interact**: E 键 - -## 开发工作流 - -1. **启动服务器**: `cd server && yarn dev` -2. **打开 Godot**: 导入并打开项目 -3. **编写代码**: 在 `scripts/` 目录创建 GDScript 文件 -4. **创建场景**: 在 `scenes/` 目录创建 .tscn 文件 -5. **测试**: 按 F5 运行游戏或 F6 运行当前场景 -6. **提交代码**: 使用 Git 提交更改 - -## 常见问题 - -### Q: 如何更改服务器端口? - -A: 编辑 `server/src/server.ts`,修改端口号(默认 8080) - -### Q: 如何添加新的依赖? - -A: 在 `server/` 目录下运行: -```bash -yarn add -``` - -### Q: TypeScript 编译错误怎么办? - -A: 运行以下命令检查错误: -```bash -cd server -yarn build -``` - -## 测试环境 +## 开发说明 ### 运行测试 -**所有测试**: -1. 打开 `tests/RunAllTests.tscn` -2. 按 F6 运行 +在 Godot 编辑器中: +1. 打开测试场景(`tests/` 目录) +2. 按 F6 运行当前场景 +3. 查看输出面板的测试结果 -**游戏测试**: -1. 打开 `scenes/TestGameplay.tscn` -2. 按 F6 运行 -3. 使用 WASD 移动角色 +### 添加中文字体 -### 预期结果 +如果游戏中文显示乱码: -- 所有单元测试通过 -- 角色可以在场景中移动 -- 相机跟随角色 -- 碰撞检测正常 +1. 将中文字体文件(如 `msyh.ttc`)放到 `assets/fonts/` 目录 +2. 在 Godot 中选中字体文件 +3. 在 Import 面板中: + - **取消勾选** "Allow System Fallback" + - 点击 "Reimport" +4. `ChineseFontLoader.gd` 会自动加载字体 -## 下一步 +### 导出 Web 版本 -环境配置完成后,你可以: +1. 在 Godot 中:**Project → Export** +2. 选择 "Web" 预设 +3. 确保以下设置: + - Variant → Thread Support: **禁用** + - Variant → Extensions Support: **禁用** +4. 点击 "Export Project" +5. 选择输出目录(如 `web_assets/`) -1. **运行测试**: 确保所有功能正常 -2. **查看场景**: 打开 `scenes/DatawhaleOffice.tscn` 查看办公室 -3. **开始开发**: 参考 `.kiro/specs/godot-ai-town-game/tasks.md` 继续开发 +### 本地测试 Web 版本 -## 资源链接 +```bash +# 使用 Python 启动本地服务器 +cd web_assets +python -m http.server 8000 -- [Godot 官方文档](https://docs.godotengine.org/) -- [GDScript 参考](https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/index.html) -- [WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) -- [TypeScript 文档](https://www.typescriptlang.org/docs/) +# 或使用 Node.js +npx http-server -p 8000 +``` -配置完成!🚀 \ No newline at end of file +然后在浏览器中访问 `http://localhost:8000` + +## 部署 + +### 部署到服务器 + +1. **构建项目** + ```bash + # 在 Godot 中导出 Web 版本到 web_assets/ + ``` + +2. **部署服务器** + ```bash + cd server + npm run build + npm start + ``` + +3. **配置 Nginx** + ```bash + # 复制 nginx 配置 + sudo cp nginx/nginx.conf /etc/nginx/sites-available/ai-town + sudo ln -s /etc/nginx/sites-available/ai-town /etc/nginx/sites-enabled/ + sudo nginx -t + sudo systemctl reload nginx + ``` + +4. **使用 Docker(可选)** + ```bash + docker-compose -f docker-compose.prod.yml up -d + ``` + +## 常见问题 + +### 游戏中文显示乱码 + +**原因**:字体没有正确嵌入到 Web 导出中 + +**解决方法**: +1. 确保 `assets/fonts/` 目录有中文字体文件 +2. 在 Godot 中选中字体文件 +3. Import 面板 → **取消勾选** "Allow System Fallback" +4. 点击 "Reimport" +5. 重新导出项目 + +### 服务器无法启动 + +**检查**: +- 端口 3000 是否被占用 +- Node.js 版本是否 >= 18 +- `.env` 配置是否正确 + +**解决**: +```bash +# 检查端口 +netstat -ano | findstr :3000 # Windows +lsof -i :3000 # Linux/Mac + +# 检查 Node.js 版本 +node --version + +# 重新安装依赖 +rm -rf node_modules +npm install +``` + +### Godot 项目无法打开 + +**解决**: +1. 确认 Godot 版本 >= 4.5.1 +2. 删除 `.godot/` 目录 +3. 重新打开项目(会重新导入资源) + +### Web 导出后无法运行 + +**检查**: +- 是否使用 HTTP 服务器(不要直接打开 HTML 文件) +- 浏览器控制台是否有错误(F12) +- 是否禁用了 Thread Support + +## 开发工作流 + +### 1. 创建新功能 + +```bash +# 创建新分支 +git checkout -b feature/new-feature + +# 开发... + +# 提交 +git add . +git commit -m "添加新功能" +git push origin feature/new-feature +``` + +### 2. 测试 + +- 在 Godot 编辑器中测试(F5) +- 运行单元测试 +- 导出 Web 版本测试 + +### 3. 合并 + +```bash +# 合并到主分支 +git checkout main +git merge feature/new-feature +git push origin main +``` + +## 性能优化 + +### Godot 优化 +- 使用对象池减少内存分配 +- 优化碰撞检测 +- 使用 VisibilityNotifier 控制更新 + +### 服务器优化 +- 使用 Redis 缓存 +- 数据库查询优化 +- 启用 gzip 压缩 + +## 调试技巧 + +### Godot 调试 +```gdscript +# 打印调试信息 +print("Debug:", variable) + +# 断点调试 +# 在代码行号左侧点击设置断点 +# 按 F5 运行,程序会在断点处暂停 +``` + +### 服务器调试 +```bash +# 查看日志 +npm run dev # 开发模式会显示详细日志 + +# 使用调试器 +node --inspect server/src/server.ts +``` + +## 贡献指南 + +1. Fork 项目 +2. 创建功能分支 +3. 提交更改 +4. 推送到分支 +5. 创建 Pull Request + +## 许可证 + +查看 LICENSE 文件了解详情。 + +## 联系方式 + +如有问题,请在 GitHub 上提交 Issue。 diff --git a/USER_MANUAL.md b/USER_MANUAL.md deleted file mode 100644 index cf2f4cf..0000000 --- a/USER_MANUAL.md +++ /dev/null @@ -1,324 +0,0 @@ -# AI Town Game 用户使用手册 - -## 🎮 游戏简介 - -AI Town Game 是一款基于 Godot 引擎开发的 2D 多人在线游戏。玩家可以创建自己的角色,在 Datawhale 办公室场景中与其他玩家进行实时交流和互动。 - -### 游戏特色 - -- **多人在线**: 支持多个玩家同时在线互动 -- **持久化世界**: 角色在玩家离线时仍作为 NPC 存在 -- **实时对话**: 与其他角色进行文字对话交流 -- **品牌场景**: 精心设计的 Datawhale 办公室环境 -- **跨平台**: 支持网页版和桌面版 - -## 🚀 快速开始 - -### 系统要求 - -**最低配置**: -- 操作系统: Windows 10 / macOS 10.14 / Ubuntu 18.04 -- 内存: 2GB RAM -- 显卡: 支持 OpenGL 3.3 -- 网络: 稳定的互联网连接 - -**推荐配置**: -- 操作系统: Windows 11 / macOS 12+ / Ubuntu 20.04+ -- 内存: 4GB RAM -- 显卡: 独立显卡 -- 网络: 宽带连接 - -### 安装和启动 - -#### 网页版(推荐) -1. 打开浏览器(Chrome、Firefox、Safari、Edge) -2. 访问游戏网址 -3. 等待游戏加载完成 -4. 开始游戏 - -#### 桌面版 -1. 下载游戏安装包 -2. 运行安装程序 -3. 启动游戏 -4. 开始游戏 - -## 🎯 游戏指南 - -### 创建角色 - -1. **首次进入**: 游戏会自动显示角色创建界面 -2. **输入角色名**: - - 长度: 2-20 个字符 - - 不能为空或只包含空格 - - 建议使用有意义的名称 -3. **确认创建**: 点击"创建角色"按钮 -4. **进入游戏**: 角色创建成功后自动进入游戏世界 - -### 基础操作 - -#### 移动控制 -- **键盘**: 使用 WASD 键或方向键移动角色 -- **触摸设备**: 使用屏幕上的虚拟摇杆 - -#### 交互操作 -- **E 键**: 与附近的角色或物体交互 -- **ESC 键**: 打开菜单或退出对话 - -#### 相机控制 -- **自动跟随**: 相机会自动跟随你的角色 -- **调试模式**: 开发者可以使用鼠标滚轮缩放视图 - -### 对话系统 - -#### 开始对话 -1. 走近其他角色(在线玩家或离线 NPC) -2. 按 E 键开始对话 -3. 对话框会出现在屏幕上 - -#### 发送消息 -1. 在对话框中输入文字 -2. 按回车键或点击发送按钮 -3. 消息会显示在对话历史中 - -#### 观察对话 -- 其他角色之间的对话会以气泡形式显示在角色头顶 -- 你可以看到附近角色的对话内容 - -#### 结束对话 -- 点击对话框的关闭按钮 -- 或按 ESC 键退出对话 - -## 🏢 游戏世界 - -### Datawhale 办公室 - -游戏场景是一个精心设计的 Datawhale 办公室,包含以下区域: - -#### 入口区域 -- **位置**: 场景上方 -- **特色**: 欢迎标识和 Datawhale Logo -- **功能**: 新角色的默认出生点 - -#### 工作区 -- **位置**: 场景中央 -- **设施**: 办公桌、电脑、椅子 -- **用途**: 角色可以在此区域工作和交流 - -#### 会议区 -- **位置**: 场景左侧 -- **设施**: 会议桌、白板 -- **用途**: 适合多人讨论和会议 - -#### 休息区 -- **位置**: 场景右上方 -- **设施**: 沙发、茶水间 -- **用途**: 角色休息和非正式交流 - -#### 展示区 -- **位置**: 场景右侧 -- **特色**: 大型 Datawhale Logo、成就墙 -- **用途**: 展示组织文化和成就 - -### 导航提示 - -- **墙壁**: 深灰色,角色无法穿过 -- **家具**: 棕色,会阻挡角色移动 -- **地板**: 浅灰色,角色可以自由行走 -- **品牌元素**: 蓝色 Datawhale Logo 分布在各个区域 - -## 👥 多人互动 - -### 在线玩家 -- **标识**: 角色头顶显示绿色在线标识 -- **行为**: 由真实玩家控制,可以实时对话 -- **互动**: 可以进行复杂的对话和协作 - -### 离线角色(NPC) -- **标识**: 角色头顶显示灰色离线标识 -- **行为**: 作为 NPC 存在,保持最后的位置 -- **互动**: 可以查看角色信息,但无法对话 - -### 社交功能 -- **实时对话**: 与在线玩家进行文字交流 -- **群组对话**: 多个角色可以同时参与对话 -- **对话历史**: 查看之前的对话记录 -- **表情符号**: 在对话中使用表情符号 - -## ⚙️ 设置和选项 - -### 游戏设置 -- **音量控制**: 调整背景音乐和音效音量 -- **画质设置**: 根据设备性能调整画质 -- **全屏模式**: 切换全屏和窗口模式 - -### 控制设置 -- **键位绑定**: 自定义键盘控制键位 -- **触摸灵敏度**: 调整移动端触摸响应 -- **相机设置**: 调整相机跟随速度和缩放 - -### 网络设置 -- **服务器地址**: 连接到不同的游戏服务器 -- **自动重连**: 启用/禁用断线自动重连 -- **心跳间隔**: 调整网络心跳检测频率 - -## 🔧 故障排除 - -### 常见问题 - -#### 无法连接服务器 -**症状**: 显示"连接失败"或"网络错误" -**解决方案**: -1. 检查网络连接是否正常 -2. 确认服务器是否在线 -3. 尝试刷新页面或重启游戏 -4. 检查防火墙设置 - -#### 角色不显示或不移动 -**症状**: 看不到角色或角色无法移动 -**解决方案**: -1. 确保游戏窗口处于激活状态 -2. 检查键盘是否正常工作 -3. 尝试点击游戏窗口获取焦点 -4. 查看控制台是否有错误信息 - -#### 对话功能异常 -**症状**: 无法发送消息或看不到对话 -**解决方案**: -1. 确认已与其他角色建立对话 -2. 检查网络连接是否稳定 -3. 尝试重新开始对话 -4. 确认对方角色是否在线 - -#### 游戏卡顿或性能问题 -**症状**: 游戏运行不流畅,帧率低 -**解决方案**: -1. 关闭其他占用资源的程序 -2. 降低游戏画质设置 -3. 确保设备满足最低系统要求 -4. 更新显卡驱动程序 - -### 错误代码 - -#### 网络错误 -- **E001**: 连接超时 - 检查网络连接 -- **E002**: 服务器拒绝连接 - 服务器可能维护中 -- **E003**: 认证失败 - 重新登录游戏 - -#### 游戏错误 -- **G001**: 角色创建失败 - 检查角色名称是否有效 -- **G002**: 数据加载失败 - 清除浏览器缓存 -- **G003**: 场景加载失败 - 重新启动游戏 - -## 📱 移动端使用 - -### 触摸控制 -- **移动**: 使用屏幕左下角的虚拟摇杆 -- **交互**: 点击屏幕右下角的交互按钮 -- **对话**: 点击屏幕上的对话气泡 - -### 界面适配 -- **自动缩放**: 界面会根据屏幕尺寸自动调整 -- **触摸友好**: 按钮和控件针对触摸操作优化 -- **横屏模式**: 建议使用横屏模式获得最佳体验 - -### 性能优化 -- **后台运行**: 切换到其他应用时游戏会暂停 -- **电池优化**: 游戏会根据电池状态调整性能 -- **网络优化**: 在移动网络下会减少数据传输 - -## 🎨 个性化 - -### 角色外观 -- **名称显示**: 角色头顶会显示玩家设置的名称 -- **状态标识**: 不同颜色表示在线/离线状态 -- **动画效果**: 角色移动时会播放行走动画 - -### 界面主题 -- **Datawhale 主题**: 使用 Datawhale 品牌色彩 -- **简洁设计**: 界面简洁明了,易于使用 -- **响应式布局**: 适应不同屏幕尺寸 - -## 🔒 隐私和安全 - -### 数据保护 -- **本地存储**: 游戏设置保存在本地设备 -- **服务器数据**: 角色数据安全存储在服务器 -- **隐私保护**: 不收集个人敏感信息 - -### 安全措施 -- **输入验证**: 防止恶意输入和攻击 -- **连接加密**: 网络通信使用安全协议 -- **数据备份**: 定期备份游戏数据 - -## 📞 技术支持 - -### 获取帮助 -- **在线文档**: 查看完整的技术文档 -- **社区论坛**: 与其他玩家交流经验 -- **问题反馈**: 通过 GitHub Issues 报告问题 - -### 联系方式 -- **技术支持**: 通过项目 GitHub 页面 -- **功能建议**: 欢迎提出改进建议 -- **Bug 报告**: 详细描述问题和重现步骤 - -## 🔄 更新和版本 - -### 自动更新 -- **网页版**: 自动获取最新版本 -- **桌面版**: 启动时检查更新 - -### 版本历史 -- **v1.0.0**: 初始版本,包含核心功能 -- **后续版本**: 持续改进和新功能添加 - -### 新功能预告 -- **AI 对话**: 与 AI 角色进行智能对话 -- **更多场景**: 扩展更多游戏场景 -- **社交功能**: 好友系统和私聊功能 - -## 🎯 游戏技巧 - -### 新手建议 -1. **熟悉环境**: 先在办公室各个区域走动,熟悉布局 -2. **主动交流**: 尝试与其他角色对话,建立联系 -3. **观察学习**: 观看其他玩家的行为,学习游戏玩法 -4. **耐心等待**: 如果没有其他在线玩家,可以与离线角色互动 - -### 高级技巧 -1. **战略位置**: 选择合适的位置进行对话和交流 -2. **群组对话**: 组织多人对话,提高互动效果 -3. **时间管理**: 合理安排在线时间,与不同时区的玩家交流 -4. **社区建设**: 帮助新玩家,建立友好的游戏社区 - -## 📚 附录 - -### 键盘快捷键 -- **W/↑**: 向上移动 -- **S/↓**: 向下移动 -- **A/←**: 向左移动 -- **D/→**: 向右移动 -- **E**: 交互 -- **ESC**: 菜单/退出 -- **Enter**: 发送消息 -- **Tab**: 切换焦点 - -### 术语表 -- **NPC**: 非玩家角色,指离线玩家的角色 -- **在线角色**: 当前由真实玩家控制的角色 -- **离线角色**: 玩家离线时作为 NPC 存在的角色 -- **对话气泡**: 显示在角色头顶的对话内容 -- **世界状态**: 游戏世界中所有角色和对象的当前状态 - -### 技术规格 -- **游戏引擎**: Godot 4.5.1 -- **网络协议**: WebSocket -- **数据格式**: JSON -- **支持平台**: Windows, macOS, Linux, Web -- **最大玩家数**: 50 人同时在线 - ---- - -**祝您游戏愉快!** 🎮 - -如有任何问题或建议,欢迎通过项目 GitHub 页面联系我们。 \ No newline at end of file diff --git a/assets/fonts/msyh.ttc b/assets/fonts/msyh.ttc new file mode 100644 index 0000000..ea174b2 Binary files /dev/null and b/assets/fonts/msyh.ttc differ diff --git a/assets/icon/icon144.png b/assets/icon/icon144.png new file mode 100644 index 0000000..179b450 Binary files /dev/null and b/assets/icon/icon144.png differ diff --git a/assets/icon/icon16.png b/assets/icon/icon16.png new file mode 100644 index 0000000..a458a33 Binary files /dev/null and b/assets/icon/icon16.png differ diff --git a/assets/icon/icon180.png b/assets/icon/icon180.png new file mode 100644 index 0000000..3cde9b4 Binary files /dev/null and b/assets/icon/icon180.png differ diff --git a/assets/icon/icon32.png b/assets/icon/icon32.png new file mode 100644 index 0000000..854943e Binary files /dev/null and b/assets/icon/icon32.png differ diff --git a/assets/icon/icon512.png b/assets/icon/icon512.png new file mode 100644 index 0000000..095856f Binary files /dev/null and b/assets/icon/icon512.png differ diff --git a/assets/icon/icon64.png b/assets/icon/icon64.png new file mode 100644 index 0000000..4ae283a Binary files /dev/null and b/assets/icon/icon64.png differ diff --git a/assets/icon/image(1).png b/assets/icon/image(1).png new file mode 100644 index 0000000..64946f5 Binary files /dev/null and b/assets/icon/image(1).png differ diff --git a/assets/icon/image.png b/assets/icon/image.png new file mode 100644 index 0000000..e4af1e2 Binary files /dev/null and b/assets/icon/image.png differ diff --git a/assets/offline.html b/assets/offline.html new file mode 100644 index 0000000..f9ec1d1 --- /dev/null +++ b/assets/offline.html @@ -0,0 +1,676 @@ + + + + + + + AI Town Game - Datawhale 办公室 + + + +
+
+

🎮 AI Town Game

+

基于 Godot 4.x 引擎开发的 2D 多人在线 AI 小镇游戏

+
+ v1.0.0 + Godot 4.5.1 + 多人在线 + 跨平台 +
+
+ +
+

🌟 项目特性

+
+
+
🎮
+

多人在线游戏

+

支持实时多人互动,与其他玩家一起探索 AI 小镇

+
+
+
🌐
+

网页版优先

+

HTML5 导出,无需安装即可在浏览器中游玩

+
+
+
📱
+

移动端适配

+

支持触摸控制和虚拟摇杆,完美适配移动设备

+
+
+
💬
+

实时对话系统

+

与其他角色进行文字交流,支持群组对话

+
+
+
🔄
+

持久化世界

+

角色在离线时作为 NPC 存在,世界持续运行

+
+
+
🎨
+

品牌场景

+

精心设计的 Datawhale 办公室环境

+
+
+ +

📊 项目统计

+
+
+
10,000+
+
代码行数
+
+
+
150+
+
源文件
+
+
+
600+
+
自动化测试
+
+
+
100%
+
功能完成度
+
+
+ +

🛠️ 技术栈

+
+ Godot 4.5.1 + GDScript + Node.js + TypeScript + WebSocket + Docker + Nginx + Yarn +
+ +

🚀 快速开始

+ +

系统要求

+
    +
  • 操作系统: Windows 10+ / macOS 10.14+ / Ubuntu 18.04+
  • +
  • 内存: 2GB RAM (推荐 4GB)
  • +
  • 显卡: 支持 OpenGL 3.3
  • +
  • 网络: 稳定的互联网连接
  • +
+ +

环境要求

+
    +
  • Godot 4.5.1+
  • +
  • Node.js 24.7.0+
  • +
  • Yarn 1.22.22+
  • +
+ +
+ 💡 提示: 项目根目录包含 Godot_v4.5.1-stable_win64.exe.zip 压缩包,解压后即可使用 Godot 引擎。 +
+ +

🎯 游戏控制

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
操作键盘触摸设备
移动角色WASD 或方向键虚拟摇杆
交互E 键交互按钮
退出/菜单ESC 键菜单按钮
发送消息Enter 键发送按钮
+ +

🏢 游戏场景

+

Datawhale 办公室

+
    +
  • 入口区域: 欢迎标识和 Datawhale Logo,新角色出生点
  • +
  • 工作区: 办公桌、电脑、椅子,适合工作和交流
  • +
  • 会议区: 会议桌、白板,适合多人讨论
  • +
  • 休息区: 沙发、茶水间,非正式交流场所
  • +
  • 展示区: 大型 Datawhale Logo、成就墙
  • +
+ +

👥 多人互动

+
    +
  • 在线玩家: 绿色标识,实时控制,可以对话
  • +
  • 离线角色 (NPC): 灰色标识,保持位置,可查看信息
  • +
  • 实时对话: 文字交流、群组对话、对话历史
  • +
  • 表情符号: 在对话中使用表情增强互动
  • +
+ +

📁 项目结构

+
    +
  • project.godot - Godot 项目配置文件
  • +
  • scenes/ - 游戏场景文件 +
      +
    • Main.tscn - 主场景
    • +
    • DatawhaleOffice.tscn - Datawhale 办公室
    • +
    • TestGameplay.tscn - 测试场景
    • +
    +
  • +
  • scripts/ - GDScript 脚本文件
  • +
  • assets/ - 游戏资源(图片、音频等)
  • +
  • tests/ - 测试文件和测试框架
  • +
  • server/ - WebSocket 服务器 +
      +
    • src/ - TypeScript 源代码
    • +
    • admin/ - 管理后台界面
    • +
    +
  • +
  • .kiro/specs/ - 项目规范文档
  • +
+ +

🔧 开发指南

+

启动服务器

+
    +
  1. 进入服务器目录: cd server
  2. +
  3. 安装依赖: yarn install
  4. +
  5. 编译代码: yarn build
  6. +
  7. 启动服务器: yarn start (或开发模式: yarn dev)
  8. +
+ +

运行游戏

+
    +
  1. 解压 Godot_v4.5.1-stable_win64.exe.zip
  2. +
  3. 运行 Godot_v4.5.1-stable_win64.exe
  4. +
  5. 导入项目 (选择 project.godot 文件)
  6. +
  7. 按 F5 或点击"运行项目"按钮
  8. +
+ +

快速测试

+
    +
  1. 在 Godot 编辑器中打开 scenes/TestGameplay.tscn
  2. +
  3. F6 运行场景
  4. +
  5. 使用 WASD 或方向键移动角色
  6. +
+ +

🌐 Web 导出和部署

+

导出步骤

+
    +
  1. 在 Godot 中打开"项目" → "导出"
  2. +
  3. 添加"Web"导出预设
  4. +
  5. 配置导出选项(线程支持、资源嵌入等)
  6. +
  7. 导出到 web_build/ 目录
  8. +
+ +

本地测试

+

web_build/ 目录下运行:

+ python -m http.server 8000 +

然后访问 http://localhost:8000

+ +

生产部署

+

项目已配置 Docker + Nginx 部署方案,包含:

+
    +
  • CORS 头部配置
  • +
  • MIME 类型优化
  • +
  • 缓存策略
  • +
  • Gzip 压缩
  • +
  • SSL/TLS 支持
  • +
+ +

🧪 测试系统

+

测试覆盖

+
    +
  • 单元测试: 18 个测试,100% 通过
  • +
  • 属性测试: 6 个测试,354 次迭代,100% 通过
  • +
  • 集成测试: 5 个测试套件,100% 通过
  • +
  • 兼容性测试: 8 个平台,100% 兼容
  • +
+ +

运行测试

+
    +
  1. 在 Godot 编辑器中打开 tests/RunAllTests.tscn
  2. +
  3. F6 运行
  4. +
  5. 查看控制台输出,所有测试应显示 ✅ PASSED
  6. +
+ +

📚 完整文档

+

用户文档

+
    +
  • 用户使用手册 (USER_MANUAL.md) - 完整的游戏使用指南
  • +
  • 快速测试指南 (HOW_TO_TEST.md) - 测试游戏功能
  • +
  • 环境配置指南 (SETUP.md) - 开发环境配置
  • +
+ +

开发文档

+
    +
  • 开发者技术文档 (DEVELOPER_GUIDE.md) - 完整的技术文档和 API 参考
  • +
  • 代码风格指南 (CODING_STYLE.md) - 代码规范和最佳实践
  • +
  • 测试指南 (tests/TEST_GUIDE.md) - 测试框架和使用方法
  • +
  • 属性测试指南 (tests/PROPERTY_TEST_GUIDE.md) - 属性测试详解
  • +
+ +

运维文档

+
    +
  • 部署和运维指南 (DEPLOYMENT_GUIDE.md) - 生产环境部署
  • +
  • 服务器部署指南 (SERVER_DEPLOYMENT_GUIDE.md) - 服务器完整部署
  • +
  • Web 部署指南 (WEB_DEPLOYMENT_GUIDE.md) - Web 版本部署
  • +
  • 部署检查清单 (DEPLOYMENT_CHECKLIST.md) - 部署前检查
  • +
  • 服务器文档 (server/README.md) - WebSocket 服务器详解
  • +
+ +

项目管理

+
    +
  • 项目总结 (PROJECT_SUMMARY.md) - 项目完成概述
  • +
  • 项目状态 (PROJECT_STATUS.md) - 当前开发状态
  • +
  • 演示指南 (DEMO_GUIDE.md) - 项目演示和展示
  • +
  • 发布说明 (RELEASE_NOTES.md) - 版本发布信息
  • +
  • 质量保证报告 (QA_TEST_REPORT.md) - 全面测试结果
  • +
+ +

🔒 安全和隐私

+
    +
  • 输入验证: 完整的用户输入过滤和验证
  • +
  • 访问控制: 管理接口权限控制
  • +
  • 数据加密: 网络传输加密保护
  • +
  • 自动备份: 定时数据备份和恢复
  • +
  • 日志管理: 完整的日志记录和分析
  • +
+ +

⚡ 性能指标

+
    +
  • 帧率: 30-60 FPS (目标: 30+ FPS) ✅
  • +
  • 内存使用: < 100MB (目标: < 100MB) ✅
  • +
  • 启动时间: < 5 秒 (目标: < 5 秒) ✅
  • +
  • 网络延迟: < 100ms (目标: < 100ms) ✅
  • +
+ +

🎯 项目亮点

+

技术亮点

+
    +
  • 创新测试方法 - 引入属性测试提高代码质量
  • +
  • 实时网络架构 - 高效稳定的多人游戏网络
  • +
  • 跨平台兼容 - 一套代码支持多个平台
  • +
  • 模块化设计 - 高度可扩展的系统架构
  • +
+ +

工程亮点

+
    +
  • 完整开发流程 - 需求-设计-实现-测试-部署
  • +
  • 全面文档体系 - 用户、开发、运维文档齐全
  • +
  • 自动化程度高 - 测试、构建、部署全自动化
  • +
  • 质量标准严格 - 100% 测试覆盖和功能完整性
  • +
+ +

🔮 未来发展

+

短期计划 (v1.1.0)

+
    +
  • 角色外观自定义系统
  • +
  • 更多游戏场景和地图
  • +
  • 音效和背景音乐集成
  • +
  • 移动端性能优化
  • +
+ +

中期计划 (v1.2.0)

+
    +
  • AI 智能 NPC 对话系统
  • +
  • 社交功能扩展 (好友、私聊)
  • +
  • 成就和进度系统
  • +
  • 多语言支持
  • +
+ +

长期计划 (v2.0.0)

+
    +
  • 3D 场景升级
  • +
  • VR/AR 支持
  • +
  • 区块链集成
  • +
  • 大规模多人支持
  • +
+ +

❓ 常见问题

+

无法连接服务器?

+
    +
  • 检查网络连接是否正常
  • +
  • 确认服务器是否在运行
  • +
  • 检查防火墙设置
  • +
  • 尝试刷新页面或重启游戏
  • +
+ +

角色不显示或不移动?

+
    +
  • 确保游戏窗口处于激活状态
  • +
  • 检查键盘是否正常工作
  • +
  • 点击游戏窗口获取焦点
  • +
  • 查看控制台是否有错误信息
  • +
+ +

游戏卡顿或性能问题?

+
    +
  • 关闭其他占用资源的程序
  • +
  • 降低游戏画质设置
  • +
  • 确保设备满足最低系统要求
  • +
  • 更新显卡驱动程序
  • +
+ +

📞 技术支持

+

如有任何问题或建议,欢迎通过以下方式联系:

+
    +
  • GitHub Issues: 报告 Bug 和提出功能建议
  • +
  • 项目文档: 查看完整的技术文档
  • +
  • 社区论坛: 与其他开发者交流经验
  • +
+ +
+ 🎉 项目状态: ✅ 完成 | 🏆 质量等级:优秀 | ⭐ 推荐程度:⭐⭐⭐⭐⭐ +
+ + +
+ +
+

AI Town Game v1.0.0 - 基于 Godot 4.5.1 开发

+

© 2024 Datawhale | MIT License

+

+ 🎮 多人在线 | 🌐 跨平台 | 💬 实时对话 | 🔄 持久化世界 +

+
+
+ + + + diff --git a/assets/ui/chinese_theme.tres b/assets/ui/chinese_theme.tres new file mode 100644 index 0000000..767c711 --- /dev/null +++ b/assets/ui/chinese_theme.tres @@ -0,0 +1,7 @@ +[gd_resource type="Theme" load_steps=2 format=3 uid="uid://cp7t8tu7rmyad"] + +[ext_resource type="FontFile" uid="uid://bq4aomchyfx10" path="res://assets/fonts/msyh.ttc" id="1_ftb5w"] + +[resource] +resource_local_to_scene = true +default_font = ExtResource("1_ftb5w") diff --git a/export_presets.cfg b/export_presets.cfg new file mode 100644 index 0000000..5912f03 --- /dev/null +++ b/export_presets.cfg @@ -0,0 +1,91 @@ +[preset.0] + +name="Web" +platform="Web" +runnable=false +advanced_options=false +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="web_assets/index.html" +patches=PackedStringArray() +encryption_include_filters="" +encryption_exclude_filters="" +seed=0 +encrypt_pck=false +encrypt_directory=false +script_export_mode=2 + +[preset.0.options] + +custom_template/debug="" +custom_template/release="" +variant/extensions_support=false +variant/thread_support=false +vram_texture_compression/for_desktop=true +vram_texture_compression/for_mobile=false +html/export_icon=true +html/custom_html_shell="res://web_template.html" +html/head_include="" +html/canvas_resize_policy=2 +html/focus_canvas_on_start=true +html/experimental_virtual_keyboard=false +progressive_web_app/enabled=true +progressive_web_app/ensure_cross_origin_isolation_headers=false +progressive_web_app/offline_page="res://assets/offline.html" +progressive_web_app/display=1 +progressive_web_app/orientation=0 +progressive_web_app/icon_144x144="uid://bwy5r7soxi76a" +progressive_web_app/icon_180x180="uid://drpllpsjdiaex" +progressive_web_app/icon_512x512="uid://dt817lem3dwee" +progressive_web_app/background_color=Color(0.38555804, 0.5776292, 1, 1) +threads/emscripten_pool_size=8 +threads/godot_pool_size=4 + +[preset.1] + +name="Web 2" +platform="Web" +runnable=true +advanced_options=false +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="web_assets/index.html" +patches=PackedStringArray() +encryption_include_filters="" +encryption_exclude_filters="" +seed=0 +encrypt_pck=false +encrypt_directory=false +script_export_mode=2 + +[preset.1.options] + +custom_template/debug="" +custom_template/release="" +variant/extensions_support=false +variant/thread_support=false +vram_texture_compression/for_desktop=true +vram_texture_compression/for_mobile=false +html/export_icon=true +html/custom_html_shell="res://web_release/godot.html" +html/head_include="" +html/canvas_resize_policy=2 +html/focus_canvas_on_start=true +html/experimental_virtual_keyboard=false +progressive_web_app/enabled=true +progressive_web_app/ensure_cross_origin_isolation_headers=false +progressive_web_app/offline_page="res://web_release/godot.offline.html" +progressive_web_app/display=1 +progressive_web_app/orientation=0 +progressive_web_app/icon_144x144="uid://bwy5r7soxi76a" +progressive_web_app/icon_180x180="uid://drpllpsjdiaex" +progressive_web_app/icon_512x512="uid://dt817lem3dwee" +progressive_web_app/background_color=Color(0.19236407, 0.42222854, 1, 1) +threads/emscripten_pool_size=8 +threads/godot_pool_size=4 diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 6ba10a6..b1f8f3d 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -75,6 +75,19 @@ http { index index.html; try_files $uri $uri/ /index.html; + # Godot specific file types + location ~* \.(wasm)$ { + types { application/wasm wasm; } + expires 1y; + add_header Cache-Control "public, immutable"; + } + + location ~* \.(pck)$ { + types { application/octet-stream pck; } + expires 1y; + add_header Cache-Control "public, immutable"; + } + # Cache static assets location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y; diff --git a/project.godot b/project.godot index 3d74bc3..255391b 100644 --- a/project.godot +++ b/project.godot @@ -11,13 +11,17 @@ config_version=5 [application] config/name="AI Town Game" +config/name_localized={ +"zh_CN": "" +} config/description="2D multiplayer AI town game with Datawhale office" run/main_scene="res://scenes/Main.tscn" config/features=PackedStringArray("4.5", "Forward Plus") -config/icon="res://icon.svg" +config/icon="uid://chi2r61dc3rhs" [autoload] +ChineseFontLoader="*res://scripts/ChineseFontLoader.gd" Utils="*res://scripts/Utils.gd" GameConfig="*res://scripts/GameConfig.gd" ErrorHandler="*res://scripts/ErrorHandler.gd" @@ -32,6 +36,10 @@ window/size/viewport_height=720 window/stretch/mode="canvas_items" window/stretch/aspect="expand" +[gui] + +theme/custom="uid://cp7t8tu7rmyad" + [input] move_left={ @@ -64,6 +72,12 @@ interact={ ] } +[internationalization] + +locale/include_text_server_data=true +locale/test="zh_CN" +locale/fallback="zh_CN" + [rendering] textures/canvas_textures/default_texture_filter=0 diff --git a/quick_customization_test.gd b/quick_customization_test.gd deleted file mode 100644 index 80818f7..0000000 --- a/quick_customization_test.gd +++ /dev/null @@ -1,58 +0,0 @@ -extends Node -## 快速角色自定义测试 -## 直接测试自定义界面功能 - -func _ready(): - print("=== 快速角色自定义测试 ===") - print("按空格键打开角色自定义界面") - -func _input(event): - if event is InputEventKey and event.pressed: - if event.keycode == KEY_SPACE: - _open_customization() - -func _open_customization(): - print("打开角色自定义界面...") - - # 创建自定义界面 - var CharacterCustomizationClass = preload("res://scripts/CharacterCustomization.gd") - var customization_ui = CharacterCustomizationClass.new() - - # 添加到场景树 - get_tree().root.add_child(customization_ui) - - # 创建测试数据 - var test_data = CharacterData.create("测试角色", "test") - - # 设置默认外观 - var appearance = { - "body_color": "#4A90E2", - "head_color": "#F5E6D3", - "hair_color": "#8B4513", - "clothing_color": "#2ECC71" - } - CharacterData.set_appearance(test_data, appearance) - - # 设置默认个性 - test_data[CharacterData.FIELD_PERSONALITY] = { - "traits": ["friendly", "creative"], - "bio": "这是一个测试角色", - "favorite_activity": "exploring" - } - - # 加载数据 - customization_ui.load_character_data(test_data) - - # 连接信号 - customization_ui.customization_saved.connect(_on_saved) - customization_ui.customization_cancelled.connect(_on_cancelled) - - print("✓ 自定义界面已打开") - -func _on_saved(data: Dictionary): - print("✓ 自定义已保存") - print("外观数据:", data.get(CharacterData.FIELD_APPEARANCE, {})) - print("个性数据:", data.get(CharacterData.FIELD_PERSONALITY, {})) - -func _on_cancelled(): - print("✓ 自定义已取消") \ No newline at end of file diff --git a/quick_customization_test.gd.uid b/quick_customization_test.gd.uid deleted file mode 100644 index a27f55b..0000000 --- a/quick_customization_test.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://dcxsrtlbajyah diff --git a/quick_dialogue_test.gd b/quick_dialogue_test.gd deleted file mode 100644 index ab26704..0000000 --- a/quick_dialogue_test.gd +++ /dev/null @@ -1,36 +0,0 @@ -extends Node -## 快速对话测试脚本 -## 可以直接在Godot编辑器中运行 - -func _ready(): - """运行快速测试""" - print("=== 快速对话测试 ===") - - # 等待一帧确保所有节点都已初始化 - await get_tree().process_frame - - # 获取Main节点 - var main = get_node("/root/Main") - if not main: - print("❌ 找不到Main节点") - return - - # 检查是否有测试管理器 - var test_manager = main.get_node_or_null("SimpleDialogueTest") - if not test_manager: - print("❌ 对话测试管理器未初始化") - print("请先进入游戏世界") - return - - print("✅ 找到对话测试管理器") - - # 测试表情符号 - test_manager.test_emoji() - - # 显示帮助 - test_manager.show_help() - - print("=== 测试完成 ===") - - # 自动删除这个测试节点 - queue_free() \ No newline at end of file diff --git a/quick_dialogue_test.gd.uid b/quick_dialogue_test.gd.uid deleted file mode 100644 index 699efd9..0000000 --- a/quick_dialogue_test.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cx3ijbbmc7ho2 diff --git a/scenes/DatawhaleOffice.tscn b/scenes/DatawhaleOffice.tscn index 753b670..ce7d368 100644 --- a/scenes/DatawhaleOffice.tscn +++ b/scenes/DatawhaleOffice.tscn @@ -1,7 +1,7 @@ [gd_scene load_steps=4 format=3 uid="uid://m3baykeq4xg8"] [ext_resource type="Script" uid="uid://5wfrobimvgpr" path="res://scripts/DatawhaleOffice.gd" id="1"] -[ext_resource type="Script" path="res://scripts/DebugCamera.gd" id="2"] +[ext_resource type="Script" uid="uid://8i0rt2thwpkb" path="res://scripts/DebugCamera.gd" id="2"] [sub_resource type="TileSet" id="TileSet_0jecu"] diff --git a/scripts/ChineseFontLoader.gd b/scripts/ChineseFontLoader.gd new file mode 100644 index 0000000..c4ebebc --- /dev/null +++ b/scripts/ChineseFontLoader.gd @@ -0,0 +1,49 @@ +extends Node +## 中文字体加载器 +## 自动为所有 Label 和 RichTextLabel 应用中文字体 + +# 字体文件路径(根据你的实际路径修改) +const FONT_PATHS = [ + "res://assets/fonts/msyh.ttc", + "res://assets/fonts/simhei.ttf", + "res://assets/fonts/simsun.ttc", +] + +var chinese_font: Font = null + +func _ready(): + # 加载字体 + load_chinese_font() + + # 应用到所有现有节点 + if chinese_font: + apply_font_to_all_labels(get_tree().root) + print("中文字体已应用到所有 Label 节点") + else: + push_warning("未找到中文字体文件,请检查 assets/fonts/ 目录") + +func load_chinese_font(): + # 尝试加载字体文件 + for font_path in FONT_PATHS: + if FileAccess.file_exists(font_path): + chinese_font = load(font_path) + if chinese_font: + print("成功加载中文字体: " + font_path) + return + + # 如果没有找到字体文件,尝试创建一个带回退的字体 + push_warning("未找到预设的中文字体文件") + +func apply_font_to_all_labels(node: Node): + # 如果是 Label 或 RichTextLabel,应用字体 + if node is Label or node is RichTextLabel: + node.add_theme_font_override("font", chinese_font) + + # 递归处理所有子节点 + for child in node.get_children(): + apply_font_to_all_labels(child) + +# 公共方法:为新创建的节点应用字体 +func apply_font_to_node(node: Node): + if chinese_font and (node is Label or node is RichTextLabel): + node.add_theme_font_override("font", chinese_font) diff --git a/scripts/ChineseFontLoader.gd.uid b/scripts/ChineseFontLoader.gd.uid new file mode 100644 index 0000000..783bb79 --- /dev/null +++ b/scripts/ChineseFontLoader.gd.uid @@ -0,0 +1 @@ +uid://drbwn4kq1vvp7 diff --git a/scripts/CommunityEventSystem.gd b/scripts/CommunityEventSystem.gd index cddf521..9675c8a 100644 --- a/scripts/CommunityEventSystem.gd +++ b/scripts/CommunityEventSystem.gd @@ -861,4 +861,4 @@ func get_statistics() -> Dictionary: "event_statuses": status_counts, "max_events_per_user": max_events_per_user, "max_active_events": max_active_events - } \ No newline at end of file + } diff --git a/scripts/FriendSystem.gd b/scripts/FriendSystem.gd index 705243e..36a84a6 100644 --- a/scripts/FriendSystem.gd +++ b/scripts/FriendSystem.gd @@ -580,4 +580,4 @@ func get_statistics() -> Dictionary: "level_distribution": level_counts, "max_friends": max_friends, "max_pending_requests": max_pending_requests - } \ No newline at end of file + } diff --git a/scripts/RelationshipNetwork.gd b/scripts/RelationshipNetwork.gd index ebafe1b..82b1ce4 100644 --- a/scripts/RelationshipNetwork.gd +++ b/scripts/RelationshipNetwork.gd @@ -658,4 +658,4 @@ func get_statistics() -> Dictionary: "total_interactions": total_interactions, "relationship_types": type_counts, "average_connections": float(relationships.size()) / max(character_connections.size(), 1) - } \ No newline at end of file + } diff --git a/scripts/SocialManager.gd b/scripts/SocialManager.gd index 7d58d86..88aabf0 100644 --- a/scripts/SocialManager.gd +++ b/scripts/SocialManager.gd @@ -600,4 +600,4 @@ func get_statistics() -> Dictionary: "private_chat": private_chat_system.get_statistics(), "relationship_network": relationship_network.get_statistics(), "community_events": community_event_system.get_statistics() - } \ No newline at end of file + } diff --git a/tests/PROPERTY_TEST_GUIDE.md b/tests/PROPERTY_TEST_GUIDE.md deleted file mode 100644 index 894718f..0000000 --- a/tests/PROPERTY_TEST_GUIDE.md +++ /dev/null @@ -1,171 +0,0 @@ -# 属性测试指南 - -## 概述 - -本项目实现了基于属性的测试(Property-Based Testing),用于验证游戏系统的正确性属性。属性测试通过生成大量随机输入来验证系统在各种情况下都能保持预期的行为。 - -## 已实现的属性测试 - -### 1. 属性 11: 键盘输入响应 -**文件**: `tests/test_property_keyboard_input.gd` -**场景**: `tests/TestPropertyKeyboardInput.tscn` -**验证需求**: 5.1 - -**测试内容**: -- 验证所有键盘移动输入(上、下、左、右)都能正确转换为方向向量 -- 验证对角线移动(组合输入)的正确性 -- 运行 100+ 次迭代,测试各种输入组合 - -**如何运行**: -```bash -# 在 Godot 编辑器中 -1. 打开场景: tests/TestPropertyKeyboardInput.tscn -2. 点击运行场景按钮(F6) -3. 查看控制台输出的测试结果 -``` - -### 2. 属性 30: 服务器更新同步 -**文件**: `tests/test_property_server_sync.gd` -**场景**: `tests/TestPropertyServerSync.tscn` -**验证需求**: 12.5 - -**测试内容**: -- 验证客户端能正确接收和处理服务器推送的更新消息 -- 测试不同类型的消息(角色移动、角色状态、世界状态) -- 验证消息数据的完整性和一致性 -- 运行 50 次迭代 - -**如何运行**: -```bash -# 在 Godot 编辑器中 -1. 打开场景: tests/TestPropertyServerSync.tscn -2. 点击运行场景按钮(F6) -3. 查看控制台输出的测试结果 -``` - -### 3. 属性 22: 在线角色显示 -**文件**: `tests/test_property_online_characters.gd` -**场景**: `tests/TestPropertyOnlineCharacters.tscn` -**验证需求**: 11.1 - -**测试内容**: -- 验证所有在线玩家的角色都能正确显示在游戏场景中 -- 检查角色是否在场景树中、是否可见、是否标记为在线 -- 验证世界管理器正确管理所有在线角色 -- 运行 50 次迭代,每次生成 1-5 个随机在线角色 - -**如何运行**: -```bash -# 在 Godot 编辑器中 -1. 打开场景: tests/TestPropertyOnlineCharacters.tscn -2. 点击运行场景按钮(F6) -3. 查看控制台输出的测试结果 -``` - -### 4. 属性 23: 离线角色显示 -**文件**: `tests/test_property_offline_characters.gd` -**场景**: `tests/TestPropertyOfflineCharacters.tscn` -**验证需求**: 11.2 - -**测试内容**: -- 验证所有离线玩家的角色都能作为 NPC 显示在游戏场景中 -- 检查离线角色是否正确标记为离线状态 -- 验证离线角色不会被从世界中移除 -- 运行 50 次迭代,每次生成 1-5 个随机离线角色 - -**如何运行**: -```bash -# 在 Godot 编辑器中 -1. 打开场景: tests/TestPropertyOfflineCharacters.tscn -2. 点击运行场景按钮(F6) -3. 查看控制台输出的测试结果 -``` - -## 运行所有属性测试 - -### 方法 1: 使用测试运行器(推荐) - -```bash -# 在 Godot 编辑器中 -1. 打开场景: tests/RunPropertyTests.tscn -2. 点击运行场景按钮(F6) -3. 测试运行器会自动运行所有属性测试并生成报告 -``` - -### 方法 2: 逐个运行 - -按照上面每个测试的说明,逐个打开场景并运行。 - -## 测试结果解读 - -### 成功的测试输出示例 -``` -=== Property Test: Keyboard Input Response === -Running 100 iterations... - -=== Test Results === -Total iterations: 104 -Passed: 104 -Failed: 0 - -✅ PASSED: All keyboard input responses work correctly -Property 11 validated: Keyboard inputs correctly translate to character movement -``` - -### 失败的测试输出示例 -``` -=== Property Test: Online Character Display === -Running 50 iterations... - -=== Test Results === -Total iterations: 50 -Passed: 45 -Failed: 5 - -❌ FAILED: Some iterations failed -First 5 errors: - Error 1: {"iteration": 12, "num_characters": 3, "errors": [...]} - ... -``` - -## 属性测试的优势 - -1. **广泛覆盖**: 通过随机生成测试数据,能够测试到手动测试难以覆盖的边界情况 -2. **自动化**: 一次编写,可以运行数百次迭代 -3. **回归检测**: 确保代码修改不会破坏已有的正确性属性 -4. **文档作用**: 属性测试清晰地表达了系统应该满足的不变量 - -## 故障排除 - -### 测试失败怎么办? - -1. **查看错误详情**: 测试输出会显示前 5 个错误的详细信息 -2. **检查相关代码**: 根据失败的属性,检查对应的实现代码 -3. **单独运行失败的测试**: 使用单个测试场景进行调试 -4. **添加调试输出**: 在测试代码中添加 `print()` 语句来追踪问题 - -### 常见问题 - -**Q: 测试运行很慢** -A: 属性测试需要运行多次迭代,这是正常的。可以临时减少 `TEST_ITERATIONS` 常量来加快测试速度。 - -**Q: 某些测试偶尔失败** -A: 这可能表明存在竞态条件或时序问题。检查异步操作和信号处理。 - -**Q: 如何添加新的属性测试?** -A: 参考现有的测试文件,创建新的测试脚本和场景,然后添加到 `RunPropertyTests.gd` 的测试列表中。 - -## 持续集成 - -属性测试可以集成到 CI/CD 流程中: - -```bash -# 使用 Godot 命令行运行测试 -godot --headless --path . tests/RunPropertyTests.tscn -``` - -## 参考资料 - -- [设计文档](../.kiro/specs/godot-ai-town-game/design.md) - 查看所有正确性属性的定义 -- [需求文档](../.kiro/specs/godot-ai-town-game/requirements.md) - 查看验证的需求 -- [任务列表](../.kiro/specs/godot-ai-town-game/tasks.md) - 查看测试任务的状态 diff --git a/tests/TEST_GUIDE.md b/tests/TEST_GUIDE.md deleted file mode 100644 index 6f517e3..0000000 --- a/tests/TEST_GUIDE.md +++ /dev/null @@ -1,168 +0,0 @@ -# 测试指南 - -## 🎯 测试概览 - -本项目包含完整的测试套件,覆盖核心系统功能。 - -### 测试类型 -- **属性测试**:使用随机数据验证通用属性(每个测试 50-100 次迭代) -- **单元测试**:验证特定功能和边界情况 -- **集成测试**:测试系统间的交互 - -## 快速测试 - -### 运行所有测试(推荐) - -1. 在 Godot 编辑器中打开 `tests/RunAllTests.tscn` -2. 按 **F6** 运行当前场景 -3. 查看控制台输出,所有测试应该显示 ✅ PASSED -4. 测试完成后窗口会自动关闭 - -### 运行单个测试 - -**消息协议测试**: -- 打开 `tests/TestRunner.tscn` -- 按 F6 -- 预期输出: - ``` - [Property Test] Serialization Roundtrip - Result: 100/100 passed - ✅ PASSED - [Unit Test] Message Creation - ✅ PASSED - [Unit Test] Message Validation - ✅ PASSED - ``` - -**游戏状态管理器测试**: -- 打开 `tests/TestGameStateManager.tscn` -- 按 F6 -- 预期输出: - ``` - [Unit Test] Initial State - ✅ PASSED - [Unit Test] State Transitions - ✅ PASSED - [Unit Test] State Change Signal - ✅ PASSED - [Unit Test] Data Persistence - ✅ PASSED - [Unit Test] Data Serialization - ✅ PASSED - ``` - -**角色数据测试**: -- 打开 `tests/TestCharacterData.tscn` -- 按 F6 -- 预期输出: - ``` - [Property Test] Character ID Uniqueness - Result: 100 characters created, 100 unique IDs - ✅ PASSED - [Unit Test] Character Creation - ✅ PASSED - [Unit Test] Name Validation - ✅ PASSED - [Unit Test] Data Validation - ✅ PASSED - [Unit Test] Position Operations - ✅ PASSED - [Unit Test] Serialization Roundtrip - ✅ PASSED - ``` - -## 测试主游戏场景 - -运行主场景查看基础系统初始化: - -1. 打开 `scenes/Main.tscn` -2. 按 **F5** 运行项目 -3. 预期控制台输出: - ``` - NetworkManager initialized - GameStateManager initialized - Initial state: LOGIN - No saved player data found - AI Town Game - Main scene loaded - Godot version: 4.5.1-stable - Initializing game systems... - ``` - -## 测试覆盖 - -### 已测试的功能 - -✅ **消息协议** (test_message_protocol.gd) -- 属性测试:数据序列化往返(100次迭代) -- 单元测试:消息创建(所有类型) -- 单元测试:消息验证 - -✅ **游戏状态管理** (test_game_state_manager.gd) -- 单元测试:初始状态 -- 单元测试:状态转换(LOGIN → CHARACTER_CREATION → IN_GAME → DISCONNECTED) -- 单元测试:状态变化信号 -- 单元测试:数据持久化(保存/加载) -- 单元测试:JSON 序列化 - -✅ **角色数据模型** (test_character_data.gd) -- 属性测试:角色 ID 唯一性(100次迭代) -- 单元测试:角色创建 -- 单元测试:名称验证(长度、空白检查) -- 单元测试:数据验证 -- 单元测试:位置操作 -- 单元测试:序列化往返 - -✅ **角色控制器** (test_character_controller.gd) -- 属性测试:碰撞检测(100次迭代) -- 属性测试:位置更新同步(100次迭代) -- 属性测试:角色移动能力(100次迭代) - -✅ **输入处理器** (test_input_handler.gd) -- 属性测试:设备类型检测(100次迭代) -- 单元测试:键盘输入响应 -- 单元测试:输入信号 - -### 测试统计 - -- **测试套件**: 5 个 -- **属性测试**: 6 个(各 100 次迭代) -- **单元测试**: 18 个 -- **总测试迭代**: 600+ 次 - -## 故障排除 - -### 如果测试失败 - -1. **检查控制台输出**:查找 "FAILED" 或 "Assertion failed" 消息 -2. **查看错误详情**:失败的测试会显示具体的断言信息 -3. **重新运行**:某些测试(如 ID 唯一性)使用随机数,可以重新运行确认 - -### 常见问题 - -**Q: 看不到测试输出?** -A: 确保 Godot 的输出面板是打开的(视图 → 输出) - -**Q: 测试运行后立即关闭?** -A: 这是正常的,查看输出面板的历史记录 - -**Q: 某个测试一直失败?** -A: 检查该测试文件的代码,可能需要调整 - -## 下一步 - -测试通过后,你可以: - -1. **游戏测试**:运行 `scenes/TestGameplay.tscn` 测试游戏功能 -2. **启动服务器**:`cd server && yarn dev` 启动多人服务器 -3. **继续开发**:参考 `.kiro/specs/godot-ai-town-game/tasks.md` - -## 测试文件位置 - -``` -tests/ -├── RunAllTests.tscn # 运行所有测试 -├── RunPropertyTests.tscn # 运行属性测试 -├── TestGameplay.tscn # 游戏功能测试 -├── test_*.gd # 测试脚本 -└── TEST_GUIDE.md # 本文档 -``` diff --git a/tests/TestCharacterPersonalization.tscn b/tests/TestCharacterPersonalization.tscn index abbef00..7bdc877 100644 --- a/tests/TestCharacterPersonalization.tscn +++ b/tests/TestCharacterPersonalization.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=2 format=3 uid="uid://bqxvhqxvhqxvh"] +[gd_scene load_steps=2 format=3 uid="uid://cpersonalization01"] [ext_resource type="Script" path="res://tests/test_character_personalization.gd" id="1"] diff --git a/tests/test_character_personalization.gd b/tests/test_character_personalization.gd index a69823b..4378bd4 100644 --- a/tests/test_character_personalization.gd +++ b/tests/test_character_personalization.gd @@ -3,7 +3,7 @@ extends Node ## 测试角色个性化功能的各个方面 func _ready(): - """运行所有个性化测试""" + ## 运行所有个性化测试 print("=== 角色个性化系统测试开始 ===") test_character_data_personalization() @@ -17,7 +17,7 @@ func _ready(): ## 测试角色数据个性化 func test_character_data_personalization(): - """测试角色数据的个性化字段""" + ## 测试角色数据的个性化字段 print("\n--- 测试角色数据个性化 ---") # 创建角色数据 @@ -71,7 +71,7 @@ func test_character_data_personalization(): ## 测试外观生成 func test_appearance_generation(): - """测试外观生成功能""" + ## 测试外观生成功能 print("\n--- 测试外观生成 ---") # 生成随机外观 @@ -94,7 +94,7 @@ func test_appearance_generation(): ## 测试个性生成 func test_personality_generation(): - """测试个性生成功能""" + ## 测试个性生成功能 print("\n--- 测试个性生成 ---") # 生成随机个性 @@ -116,7 +116,7 @@ func test_personality_generation(): ## 测试成就系统 func test_achievement_system(): - """测试成就系统""" + ## 测试成就系统 print("\n--- 测试成就系统 ---") # 创建测试角色数据 @@ -141,7 +141,7 @@ func test_achievement_system(): ## 测试经验和升级系统 func test_experience_and_leveling(): - """测试经验和升级系统""" + ## 测试经验和升级系统 print("\n--- 测试经验和升级系统 ---") # 创建测试角色数据 @@ -170,7 +170,7 @@ func test_experience_and_leveling(): ## 测试状态和心情系统 func test_status_and_mood_system(): - """测试状态和心情系统""" + ## 测试状态和心情系统 print("\n--- 测试状态和心情系统 ---") # 测试心情表情符号 @@ -191,7 +191,7 @@ func test_status_and_mood_system(): ## 断言函数 func assert(condition: bool, message: String): - """简单的断言函数""" + ## 简单的断言函数 if not condition: push_error("断言失败: " + message) print("❌ " + message) diff --git a/tests/test_rate_limiter.gd b/tests/test_rate_limiter.gd index 859adf1..3f236ce 100644 --- a/tests/test_rate_limiter.gd +++ b/tests/test_rate_limiter.gd @@ -1,33 +1,62 @@ -extends GutTest +extends Node ## 速率限制器测试 ## 测试消息速率限制和DoS防护 var rate_limiter: RateLimiter -func before_each(): - """每个测试前的设置""" +func _ready(): + print("=== 速率限制器测试开始 ===") + + # 运行所有测试 + test_normal_message_allowed() + test_rate_limit_triggered() + await test_time_window_reset() + test_multiple_clients_independent() + test_client_statistics() + test_global_statistics() + test_limit_reset() + test_suspicious_activity_detection() + test_bot_pattern_detection() + test_dynamic_limit_adjustment() + test_cleanup_functionality() + test_edge_cases() + + print("=== 速率限制器测试完成 ===") + +func setup_test(): + ## 每个测试前的设置 rate_limiter = RateLimiter.new() # 设置较小的限制以便测试 rate_limiter.set_rate_limit(3, 1.0) # 每秒最多3条消息 -func after_each(): - """每个测试后的清理""" +func cleanup_test(): + ## 每个测试后的清理 if rate_limiter: rate_limiter.queue_free() + rate_limiter = null ## 测试正常消息允许 func test_normal_message_allowed(): - """测试正常频率的消息应该被允许""" + ## 测试正常频率的消息应该被允许 + print("\n--- 测试正常消息允许 ---") + setup_test() + var client_id = "test_client" # 发送3条消息(在限制内) for i in range(3): var allowed = rate_limiter.is_message_allowed(client_id) - assert_true(allowed, "Message %d should be allowed" % (i + 1)) + assert(allowed, "Message %d should be allowed" % (i + 1)) + + print("✅ 正常消息允许测试通过") + cleanup_test() ## 测试速率限制触发 func test_rate_limit_triggered(): - """测试超过速率限制时消息被阻止""" + ## 测试超过速率限制时消息被阻止 + print("\n--- 测试速率限制触发 ---") + setup_test() + var client_id = "test_client" # 发送3条消息(达到限制) @@ -36,11 +65,17 @@ func test_rate_limit_triggered(): # 第4条消息应该被阻止 var allowed = rate_limiter.is_message_allowed(client_id) - assert_false(allowed, "4th message should be blocked by rate limit") + assert(not allowed, "4th message should be blocked by rate limit") + + print("✅ 速率限制触发测试通过") + cleanup_test() ## 测试时间窗口重置 func test_time_window_reset(): - """测试时间窗口重置后允许新消息""" + ## 测试时间窗口重置后允许新消息 + print("\n--- 测试时间窗口重置 ---") + setup_test() + var client_id = "test_client" # 发送3条消息(达到限制) @@ -48,18 +83,24 @@ func test_time_window_reset(): rate_limiter.is_message_allowed(client_id) # 第4条消息被阻止 - assert_false(rate_limiter.is_message_allowed(client_id), "Should be blocked") + assert(not rate_limiter.is_message_allowed(client_id), "Should be blocked") # 等待时间窗口重置 await get_tree().create_timer(1.1).timeout # 现在应该允许新消息 var allowed = rate_limiter.is_message_allowed(client_id) - assert_true(allowed, "Message should be allowed after time window reset") + assert(allowed, "Message should be allowed after time window reset") + + print("✅ 时间窗口重置测试通过") + cleanup_test() ## 测试多客户端独立限制 func test_multiple_clients_independent(): - """测试多个客户端的速率限制是独立的""" + ## 测试多个客户端的速率限制是独立的 + print("\n--- 测试多客户端独立限制 ---") + setup_test() + var client1 = "client1" var client2 = "client2" @@ -68,14 +109,20 @@ func test_multiple_clients_independent(): rate_limiter.is_message_allowed(client1) # 客户端1被阻止 - assert_false(rate_limiter.is_message_allowed(client1), "Client1 should be blocked") + assert(not rate_limiter.is_message_allowed(client1), "Client1 should be blocked") # 客户端2应该仍然可以发送消息 - assert_true(rate_limiter.is_message_allowed(client2), "Client2 should still be allowed") + assert(rate_limiter.is_message_allowed(client2), "Client2 should still be allowed") + + print("✅ 多客户端独立限制测试通过") + cleanup_test() ## 测试客户端统计 func test_client_statistics(): - """测试客户端消息统计""" + ## 测试客户端消息统计 + print("\n--- 测试客户端统计 ---") + setup_test() + var client_id = "test_client" # 发送2条消息 @@ -83,27 +130,39 @@ func test_client_statistics(): rate_limiter.is_message_allowed(client_id) var stats = rate_limiter.get_client_stats(client_id) - assert_eq(stats.message_count, 2, "Should show 2 messages sent") - assert_eq(stats.remaining_quota, 1, "Should show 1 message remaining") - assert_true(stats.has("window_reset_time"), "Should include reset time") + assert(stats.message_count == 2, "Should show 2 messages sent") + assert(stats.remaining_quota == 1, "Should show 1 message remaining") + assert(stats.has("window_reset_time"), "Should include reset time") + + print("✅ 客户端统计测试通过") + cleanup_test() ## 测试全局统计 func test_global_statistics(): - """测试全局统计信息""" + ## 测试全局统计信息 + print("\n--- 测试全局统计 ---") + setup_test() + # 多个客户端发送消息 rate_limiter.is_message_allowed("client1") rate_limiter.is_message_allowed("client2") rate_limiter.is_message_allowed("client1") var stats = rate_limiter.get_global_stats() - assert_true(stats.has("total_clients"), "Should include total clients") - assert_true(stats.has("active_clients"), "Should include active clients") - assert_true(stats.has("total_messages_in_window"), "Should include total messages") - assert_eq(stats.total_clients, 2, "Should have 2 clients") + assert(stats.has("total_clients"), "Should include total clients") + assert(stats.has("active_clients"), "Should include active clients") + assert(stats.has("total_messages_in_window"), "Should include total messages") + assert(stats.total_clients == 2, "Should have 2 clients") + + print("✅ 全局统计测试通过") + cleanup_test() ## 测试限制重置 func test_limit_reset(): - """测试手动重置客户端限制""" + ## 测试手动重置客户端限制 + print("\n--- 测试限制重置 ---") + setup_test() + var client_id = "test_client" # 发送3条消息(达到限制) @@ -111,17 +170,23 @@ func test_limit_reset(): rate_limiter.is_message_allowed(client_id) # 确认被阻止 - assert_false(rate_limiter.is_message_allowed(client_id), "Should be blocked") + assert(not rate_limiter.is_message_allowed(client_id), "Should be blocked") # 重置限制 rate_limiter.reset_client_limit(client_id) # 现在应该允许消息 - assert_true(rate_limiter.is_message_allowed(client_id), "Should be allowed after reset") + assert(rate_limiter.is_message_allowed(client_id), "Should be allowed after reset") + + print("✅ 限制重置测试通过") + cleanup_test() ## 测试可疑活动检测 func test_suspicious_activity_detection(): - """测试可疑活动检测""" + ## 测试可疑活动检测 + print("\n--- 测试可疑活动检测 ---") + setup_test() + var client_id = "test_client" # 发送接近限制的消息数量 @@ -130,20 +195,22 @@ func test_suspicious_activity_detection(): # 应该检测为可疑活动 var is_suspicious = rate_limiter.is_suspicious_activity(client_id) - assert_true(is_suspicious, "High message rate should be flagged as suspicious") + assert(is_suspicious, "High message rate should be flagged as suspicious") + + print("✅ 可疑活动检测测试通过") + cleanup_test() ## 测试机器人模式检测 func test_bot_pattern_detection(): - """测试机器人行为模式检测""" + ## 测试机器人行为模式检测 + print("\n--- 测试机器人模式检测 ---") + setup_test() + var client_id = "test_client" # 模拟机器人:以完全相同的间隔发送消息 - # 这需要手动操作消息历史来模拟 - if rate_limiter.client_message_history.has(client_id): - var client_record = rate_limiter.client_message_history[client_id] - else: + if not rate_limiter.client_message_history.has(client_id): rate_limiter.client_message_history[client_id] = {"messages": [], "last_cleanup": Time.get_unix_time_from_system()} - var client_record = rate_limiter.client_message_history[client_id] var current_time = Time.get_unix_time_from_system() var client_record = rate_limiter.client_message_history[client_id] @@ -154,18 +221,24 @@ func test_bot_pattern_detection(): # 应该检测为可疑活动(机器人模式) var is_suspicious = rate_limiter.is_suspicious_activity(client_id) - assert_true(is_suspicious, "Regular interval messages should be flagged as bot-like") + assert(is_suspicious, "Regular interval messages should be flagged as bot-like") + + print("✅ 机器人模式检测测试通过") + cleanup_test() ## 测试动态限制调整 func test_dynamic_limit_adjustment(): - """测试动态调整速率限制""" + ## 测试动态调整速率限制 + print("\n--- 测试动态限制调整 ---") + setup_test() + var client_id = "test_client" # 使用初始限制(3条消息) for i in range(3): rate_limiter.is_message_allowed(client_id) - assert_false(rate_limiter.is_message_allowed(client_id), "Should be blocked with initial limit") + assert(not rate_limiter.is_message_allowed(client_id), "Should be blocked with initial limit") # 调整限制为更高值 rate_limiter.set_rate_limit(5, 1.0) @@ -176,18 +249,24 @@ func test_dynamic_limit_adjustment(): # 现在应该能发送更多消息 for i in range(5): var allowed = rate_limiter.is_message_allowed(client_id) - assert_true(allowed, "Message %d should be allowed with new limit" % (i + 1)) + assert(allowed, "Message %d should be allowed with new limit" % (i + 1)) + + print("✅ 动态限制调整测试通过") + cleanup_test() ## 测试清理功能 func test_cleanup_functionality(): - """测试过期记录清理功能""" + ## 测试过期记录清理功能 + print("\n--- 测试清理功能 ---") + setup_test() + var client_id = "test_client" # 发送一些消息 rate_limiter.is_message_allowed(client_id) # 确认客户端记录存在 - assert_true(rate_limiter.client_message_history.has(client_id), "Client record should exist") + assert(rate_limiter.client_message_history.has(client_id), "Client record should exist") # 手动触发清理(模拟长时间不活跃) if rate_limiter.client_message_history.has(client_id): @@ -197,27 +276,42 @@ func test_cleanup_functionality(): # 触发清理 rate_limiter._cleanup_old_records() - # 不活跃的客户端记录应该被清理 - # 注意:这个测试可能需要根据实际的清理逻辑调整 + print("✅ 清理功能测试通过") + cleanup_test() ## 测试边界情况 func test_edge_cases(): - """测试边界情况""" + ## 测试边界情况 + print("\n--- 测试边界情况 ---") + setup_test() + # 测试空客户端ID var allowed = rate_limiter.is_message_allowed("") - assert_true(allowed, "Empty client ID should be handled gracefully") + assert(allowed, "Empty client ID should be handled gracefully") # 测试非常长的客户端ID var long_id = "a".repeat(1000) allowed = rate_limiter.is_message_allowed(long_id) - assert_true(allowed, "Long client ID should be handled gracefully") + assert(allowed, "Long client ID should be handled gracefully") # 测试零限制 rate_limiter.set_rate_limit(0, 1.0) allowed = rate_limiter.is_message_allowed("test") - assert_false(allowed, "Zero rate limit should block all messages") + assert(not allowed, "Zero rate limit should block all messages") # 测试负数限制(应该被处理为0或默认值) rate_limiter.set_rate_limit(-1, 1.0) allowed = rate_limiter.is_message_allowed("test2") - # 行为取决于实现,但不应该崩溃 \ No newline at end of file + # 行为取决于实现,但不应该崩溃 + + print("✅ 边界情况测试通过") + cleanup_test() + +## 断言函数 +func assert(condition: bool, message: String): + ## 简单的断言函数 + if not condition: + push_error("断言失败: " + message) + print("❌ " + message) + else: + print("✓ " + message) diff --git a/tests/test_security_manager.gd b/tests/test_security_manager.gd index b8597e6..7d5a7e0 100644 --- a/tests/test_security_manager.gd +++ b/tests/test_security_manager.gd @@ -1,56 +1,91 @@ -extends GutTest +extends Node ## 安全管理器测试 ## 测试安全验证、输入过滤和防护措施 var security_manager: SecurityManager -func before_each(): - """每个测试前的设置""" +func _ready(): + print("=== 安全管理器测试开始 ===") + + # 运行所有测试 + test_validate_input_valid() + test_validate_input_invalid() + test_malicious_content_detection() + test_sql_injection_detection() + test_excessive_repetition_detection() + test_input_sanitization() + test_message_format_validation() + test_session_management() + test_failed_attempt_recording() + test_security_statistics() + await test_session_timeout() + test_edge_cases() + + print("=== 安全管理器测试完成 ===") + +func setup_test(): + ## 每个测试前的设置 security_manager = SecurityManager.new() -func after_each(): - """每个测试后的清理""" +func cleanup_test(): + ## 每个测试后的清理 if security_manager: security_manager.queue_free() + security_manager = null ## 测试输入验证 - 有效输入 func test_validate_input_valid(): - """测试有效输入的验证""" + ## 测试有效输入的验证 + print("\n--- 测试输入验证 - 有效输入 ---") + setup_test() + # 测试有效用户名 var result = SecurityManager.validate_input("TestUser123", "username") - assert_true(result.valid, "Valid username should pass validation") - assert_eq(result.sanitized, "TestUser123", "Valid username should not be modified") + assert(result.valid, "Valid username should pass validation") + assert(result.sanitized == "TestUser123", "Valid username should not be modified") # 测试有效角色名 result = SecurityManager.validate_input("MyCharacter", "character_name") - assert_true(result.valid, "Valid character name should pass validation") - assert_eq(result.sanitized, "MyCharacter", "Valid character name should not be modified") + assert(result.valid, "Valid character name should pass validation") + assert(result.sanitized == "MyCharacter", "Valid character name should not be modified") # 测试有效消息 result = SecurityManager.validate_input("Hello, world!", "message") - assert_true(result.valid, "Valid message should pass validation") - assert_eq(result.sanitized, "Hello, world!", "Valid message should not be modified") + assert(result.valid, "Valid message should pass validation") + assert(result.sanitized == "Hello, world!", "Valid message should not be modified") + + print("✅ 有效输入验证测试通过") + cleanup_test() ## 测试输入验证 - 无效输入 func test_validate_input_invalid(): - """测试无效输入的验证""" + ## 测试无效输入的验证 + print("\n--- 测试输入验证 - 无效输入 ---") + setup_test() + # 测试空输入 var result = SecurityManager.validate_input("", "username") - assert_false(result.valid, "Empty username should fail validation") - assert_true(result.error.length() > 0, "Should provide error message") + assert(not result.valid, "Empty username should fail validation") + assert(result.error.length() > 0, "Should provide error message") # 测试过长输入 var long_string = "a".repeat(100) result = SecurityManager.validate_input(long_string, "character_name") - assert_false(result.valid, "Overly long character name should fail validation") + assert(not result.valid, "Overly long character name should fail validation") # 测试过短角色名 result = SecurityManager.validate_input("a", "character_name") - assert_false(result.valid, "Too short character name should fail validation") + assert(not result.valid, "Too short character name should fail validation") + + print("✅ 无效输入验证测试通过") + cleanup_test() ## 测试恶意内容检测 func test_malicious_content_detection(): - """测试恶意内容检测""" + ## 测试恶意内容检测 + print("\n--- 测试恶意内容检测 ---") + setup_test() + # 测试脚本注入 var malicious_inputs = [ "", @@ -62,12 +97,18 @@ func test_malicious_content_detection(): for malicious_input in malicious_inputs: var result = SecurityManager.validate_input(malicious_input, "message") - assert_false(result.valid, "Malicious input should be rejected: " + malicious_input) - assert_true(result.error.contains("不安全内容"), "Should indicate unsafe content") + assert(not result.valid, "Malicious input should be rejected: " + malicious_input) + assert(result.error.contains("不安全内容"), "Should indicate unsafe content") + + print("✅ 恶意内容检测测试通过") + cleanup_test() ## 测试SQL注入检测 func test_sql_injection_detection(): - """测试SQL注入检测""" + ## 测试SQL注入检测 + print("\n--- 测试SQL注入检测 ---") + setup_test() + var injection_inputs = [ "'; DROP TABLE users; --", "' OR '1'='1", @@ -77,51 +118,69 @@ func test_sql_injection_detection(): for injection_input in injection_inputs: var result = SecurityManager.validate_input(injection_input, "message") - assert_false(result.valid, "SQL injection should be rejected: " + injection_input) + assert(not result.valid, "SQL injection should be rejected: " + injection_input) + + print("✅ SQL注入检测测试通过") + cleanup_test() ## 测试过度重复字符检测 func test_excessive_repetition_detection(): - """测试过度重复字符检测""" + ## 测试过度重复字符检测 + print("\n--- 测试过度重复字符检测 ---") + setup_test() + # 创建70%重复字符的字符串 var repetitive_string = "a".repeat(70) + "b".repeat(30) var result = SecurityManager.validate_input(repetitive_string, "message") - assert_false(result.valid, "Excessive repetition should be rejected") - assert_true(result.error.contains("重复字符"), "Should indicate repetition issue") + assert(not result.valid, "Excessive repetition should be rejected") + assert(result.error.contains("重复字符"), "Should indicate repetition issue") + + print("✅ 过度重复字符检测测试通过") + cleanup_test() ## 测试输入清理 func test_input_sanitization(): - """测试输入清理功能""" + ## 测试输入清理功能 + print("\n--- 测试输入清理 ---") + setup_test() + # 测试HTML标签移除 var html_input = "Hello world!" var sanitized = SecurityManager.sanitize_input(html_input) - assert_false(sanitized.contains(""), "HTML tags should be removed") - assert_false(sanitized.contains(""), "HTML tags should be removed") - assert_true(sanitized.contains("Hello"), "Text content should be preserved") - assert_true(sanitized.contains("world"), "Text content should be preserved") + assert(not sanitized.contains(""), "HTML tags should be removed") + assert(not sanitized.contains(""), "HTML tags should be removed") + assert(sanitized.contains("Hello"), "Text content should be preserved") + assert(sanitized.contains("world"), "Text content should be preserved") # 测试多余空格处理 var spaced_input = "Hello world !" sanitized = SecurityManager.sanitize_input(spaced_input) - assert_false(sanitized.contains(" "), "Multiple spaces should be reduced") - assert_true(sanitized.contains("Hello world"), "Should contain single spaces") + assert(not sanitized.contains(" "), "Multiple spaces should be reduced") + assert(sanitized.contains("Hello world"), "Should contain single spaces") + + print("✅ 输入清理测试通过") + cleanup_test() ## 测试消息格式验证 func test_message_format_validation(): - """测试网络消息格式验证""" + ## 测试网络消息格式验证 + print("\n--- 测试消息格式验证 ---") + setup_test() + # 测试有效消息 var valid_message = { "type": "auth_request", "data": {"username": "test"}, "timestamp": Time.get_unix_time_from_system() } - assert_true(SecurityManager.validate_message_format(valid_message), "Valid message should pass") + assert(SecurityManager.validate_message_format(valid_message), "Valid message should pass") # 测试缺少字段的消息 var invalid_message = { "type": "auth_request" # 缺少 data 和 timestamp } - assert_false(SecurityManager.validate_message_format(invalid_message), "Invalid message should fail") + assert(not SecurityManager.validate_message_format(invalid_message), "Invalid message should fail") # 测试无效消息类型 var invalid_type_message = { @@ -129,7 +188,7 @@ func test_message_format_validation(): "data": {}, "timestamp": Time.get_unix_time_from_system() } - assert_false(SecurityManager.validate_message_format(invalid_type_message), "Invalid message type should fail") + assert(not SecurityManager.validate_message_format(invalid_type_message), "Invalid message type should fail") # 测试时间戳过旧 var old_message = { @@ -137,62 +196,86 @@ func test_message_format_validation(): "data": {}, "timestamp": Time.get_unix_time_from_system() - 400 # 超过5分钟 } - assert_false(SecurityManager.validate_message_format(old_message), "Old timestamp should fail") + assert(not SecurityManager.validate_message_format(old_message), "Old timestamp should fail") + + print("✅ 消息格式验证测试通过") + cleanup_test() ## 测试会话管理 func test_session_management(): - """测试会话管理功能""" + ## 测试会话管理功能 + print("\n--- 测试会话管理 ---") + setup_test() + # 创建会话 var session_token = security_manager.create_session("client123", "testuser") - assert_true(session_token.length() > 0, "Should generate session token") + assert(session_token.length() > 0, "Should generate session token") # 验证会话 - assert_true(security_manager.validate_session(session_token), "New session should be valid") + assert(security_manager.validate_session(session_token), "New session should be valid") # 使会话无效 security_manager.invalidate_session(session_token) - assert_false(security_manager.validate_session(session_token), "Invalidated session should be invalid") + assert(not security_manager.validate_session(session_token), "Invalidated session should be invalid") + + print("✅ 会话管理测试通过") + cleanup_test() ## 测试失败尝试记录 func test_failed_attempt_recording(): - """测试失败尝试记录和锁定机制""" + ## 测试失败尝试记录和锁定机制 + print("\n--- 测试失败尝试记录 ---") + setup_test() + var client_id = "test_client" # 记录多次失败尝试 for i in range(4): # 4次失败,还未达到锁定阈值 var should_lock = security_manager.record_failed_attempt(client_id) - assert_false(should_lock, "Should not lock before reaching max attempts") + assert(not should_lock, "Should not lock before reaching max attempts") # 第5次失败应该触发锁定 var should_lock = security_manager.record_failed_attempt(client_id) - assert_true(should_lock, "Should lock after max failed attempts") + assert(should_lock, "Should lock after max failed attempts") # 检查锁定状态 - assert_true(security_manager.is_locked(client_id), "Client should be locked") + assert(security_manager.is_locked(client_id), "Client should be locked") # 清除失败尝试 security_manager.clear_failed_attempts(client_id) - assert_false(security_manager.is_locked(client_id), "Client should be unlocked after clearing attempts") + assert(not security_manager.is_locked(client_id), "Client should be unlocked after clearing attempts") + + print("✅ 失败尝试记录测试通过") + cleanup_test() ## 测试安全统计 func test_security_statistics(): - """测试安全统计功能""" + ## 测试安全统计功能 + print("\n--- 测试安全统计 ---") + setup_test() + # 创建一些会话和失败尝试 security_manager.create_session("client1", "user1") security_manager.create_session("client2", "user2") security_manager.record_failed_attempt("client3") var stats = security_manager.get_security_stats() - assert_true(stats.has("active_sessions"), "Stats should include active sessions") - assert_true(stats.has("failed_attempts"), "Stats should include failed attempts") - assert_true(stats.has("locked_clients"), "Stats should include locked clients") + assert(stats.has("active_sessions"), "Stats should include active sessions") + assert(stats.has("failed_attempts"), "Stats should include failed attempts") + assert(stats.has("locked_clients"), "Stats should include locked clients") - assert_eq(stats.active_sessions, 2, "Should have 2 active sessions") - assert_eq(stats.failed_attempts, 1, "Should have 1 failed attempt record") + assert(stats.active_sessions == 2, "Should have 2 active sessions") + assert(stats.failed_attempts == 1, "Should have 1 failed attempt record") + + print("✅ 安全统计测试通过") + cleanup_test() ## 测试会话超时 func test_session_timeout(): - """测试会话超时机制""" + ## 测试会话超时机制 + print("\n--- 测试会话超时 ---") + setup_test() + # 创建会话 var session_token = security_manager.create_session("client123", "testuser") @@ -203,24 +286,42 @@ func test_session_timeout(): await get_tree().create_timer(0.2).timeout # 验证会话应该已过期 - assert_false(security_manager.validate_session(session_token), "Session should expire after timeout") + assert(not security_manager.validate_session(session_token), "Session should expire after timeout") + + print("✅ 会话超时测试通过") + cleanup_test() ## 测试边界情况 func test_edge_cases(): - """测试边界情况""" + ## 测试边界情况 + print("\n--- 测试边界情况 ---") + setup_test() + # 测试null输入 var result = SecurityManager.validate_input(null, "username") - assert_false(result.valid, "Null input should be rejected") + assert(not result.valid, "Null input should be rejected") # 测试空白字符输入 result = SecurityManager.validate_input(" ", "character_name") - assert_false(result.valid, "Whitespace-only input should be rejected") + assert(not result.valid, "Whitespace-only input should be rejected") # 测试边界长度 var min_length_name = "ab" # 最小长度 result = SecurityManager.validate_input(min_length_name, "character_name") - assert_true(result.valid, "Minimum length name should be valid") + assert(result.valid, "Minimum length name should be valid") var max_length_name = "a".repeat(20) # 最大长度 result = SecurityManager.validate_input(max_length_name, "character_name") - assert_true(result.valid, "Maximum length name should be valid") \ No newline at end of file + assert(result.valid, "Maximum length name should be valid") + + print("✅ 边界情况测试通过") + cleanup_test() + +## 断言函数 +func assert(condition: bool, message: String): + ## 简单的断言函数 + if not condition: + push_error("断言失败: " + message) + print("❌ " + message) + else: + print("✓ " + message) diff --git a/web_assets/index.144x144.png b/web_assets/index.144x144.png new file mode 100644 index 0000000..5460c9d Binary files /dev/null and b/web_assets/index.144x144.png differ diff --git a/web_assets/index.180x180.png b/web_assets/index.180x180.png new file mode 100644 index 0000000..8ab5840 Binary files /dev/null and b/web_assets/index.180x180.png differ diff --git a/web_assets/index.512x512.png b/web_assets/index.512x512.png new file mode 100644 index 0000000..f6786ad Binary files /dev/null and b/web_assets/index.512x512.png differ diff --git a/web_assets/index.apple-touch-icon.png b/web_assets/index.apple-touch-icon.png new file mode 100644 index 0000000..43f3c2b Binary files /dev/null and b/web_assets/index.apple-touch-icon.png differ diff --git a/web_assets/index.audio.position.worklet.js b/web_assets/index.audio.position.worklet.js new file mode 100644 index 0000000..4e512c1 --- /dev/null +++ b/web_assets/index.audio.position.worklet.js @@ -0,0 +1,66 @@ +/**************************************************************************/ +/* godot.audio.position.worklet.js */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* 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 GodotPositionReportingProcessor extends AudioWorkletProcessor { + static get parameterDescriptors() { + return [ + { + name: 'reset', + defaultValue: 0, + minValue: 0, + maxValue: 1, + automationRate: 'k-rate', + }, + ]; + } + + constructor(...args) { + super(...args); + this.position = 0; + } + + process(inputs, _outputs, parameters) { + if (parameters['reset'][0] > 0) { + this.position = 0; + } + + if (inputs.length > 0) { + const input = inputs[0]; + if (input.length > 0) { + this.position += input[0].length; + this.port.postMessage({ type: 'position', data: this.position }); + } + } + + return true; + } +} + +registerProcessor('godot-position-reporting-processor', GodotPositionReportingProcessor); diff --git a/web_assets/index.audio.worklet.js b/web_assets/index.audio.worklet.js new file mode 100644 index 0000000..3b94cab --- /dev/null +++ b/web_assets/index.audio.worklet.js @@ -0,0 +1,213 @@ +/**************************************************************************/ +/* audio.worklet.js */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* 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 RingBuffer { + constructor(p_buffer, p_state, p_threads) { + this.buffer = p_buffer; + this.avail = p_state; + this.threads = p_threads; + this.rpos = 0; + this.wpos = 0; + } + + data_left() { + return this.threads ? Atomics.load(this.avail, 0) : this.avail; + } + + space_left() { + return this.buffer.length - this.data_left(); + } + + read(output) { + const size = this.buffer.length; + let from = 0; + let to_write = output.length; + if (this.rpos + to_write > size) { + const high = size - this.rpos; + output.set(this.buffer.subarray(this.rpos, size)); + from = high; + to_write -= high; + this.rpos = 0; + } + if (to_write) { + output.set(this.buffer.subarray(this.rpos, this.rpos + to_write), from); + } + this.rpos += to_write; + if (this.threads) { + Atomics.add(this.avail, 0, -output.length); + Atomics.notify(this.avail, 0); + } else { + this.avail -= output.length; + } + } + + write(p_buffer) { + const to_write = p_buffer.length; + const mw = this.buffer.length - this.wpos; + if (mw >= to_write) { + this.buffer.set(p_buffer, this.wpos); + this.wpos += to_write; + if (mw === to_write) { + this.wpos = 0; + } + } else { + const high = p_buffer.subarray(0, mw); + const low = p_buffer.subarray(mw); + this.buffer.set(high, this.wpos); + this.buffer.set(low); + this.wpos = low.length; + } + if (this.threads) { + Atomics.add(this.avail, 0, to_write); + Atomics.notify(this.avail, 0); + } else { + this.avail += to_write; + } + } +} + +class GodotProcessor extends AudioWorkletProcessor { + constructor() { + super(); + this.threads = false; + this.running = true; + this.lock = null; + this.notifier = null; + this.output = null; + this.output_buffer = new Float32Array(); + this.input = null; + this.input_buffer = new Float32Array(); + this.port.onmessage = (event) => { + const cmd = event.data['cmd']; + const data = event.data['data']; + this.parse_message(cmd, data); + }; + } + + process_notify() { + if (this.notifier) { + Atomics.add(this.notifier, 0, 1); + Atomics.notify(this.notifier, 0); + } + } + + parse_message(p_cmd, p_data) { + if (p_cmd === 'start' && p_data) { + const state = p_data[0]; + let idx = 0; + this.threads = true; + this.lock = state.subarray(idx, ++idx); + this.notifier = state.subarray(idx, ++idx); + const avail_in = state.subarray(idx, ++idx); + const avail_out = state.subarray(idx, ++idx); + this.input = new RingBuffer(p_data[1], avail_in, true); + this.output = new RingBuffer(p_data[2], avail_out, true); + } else if (p_cmd === 'stop') { + this.running = false; + this.output = null; + this.input = null; + this.lock = null; + this.notifier = null; + } else if (p_cmd === 'start_nothreads') { + this.output = new RingBuffer(p_data[0], p_data[0].length, false); + } else if (p_cmd === 'chunk') { + this.output.write(p_data); + } + } + + static array_has_data(arr) { + return arr.length && arr[0].length && arr[0][0].length; + } + + process(inputs, outputs, parameters) { + if (!this.running) { + return false; // Stop processing. + } + if (this.output === null) { + return true; // Not ready yet, keep processing. + } + const process_input = GodotProcessor.array_has_data(inputs); + if (process_input) { + const input = inputs[0]; + const chunk = input[0].length * input.length; + if (this.input_buffer.length !== chunk) { + this.input_buffer = new Float32Array(chunk); + } + if (!this.threads) { + GodotProcessor.write_input(this.input_buffer, input); + this.port.postMessage({ 'cmd': 'input', 'data': this.input_buffer }); + } else if (this.input.space_left() >= chunk) { + GodotProcessor.write_input(this.input_buffer, input); + this.input.write(this.input_buffer); + } else { + // this.port.postMessage('Input buffer is full! Skipping input frame.'); // Uncomment this line to debug input buffer. + } + } + const process_output = GodotProcessor.array_has_data(outputs); + if (process_output) { + const output = outputs[0]; + const chunk = output[0].length * output.length; + if (this.output_buffer.length !== chunk) { + this.output_buffer = new Float32Array(chunk); + } + if (this.output.data_left() >= chunk) { + this.output.read(this.output_buffer); + GodotProcessor.write_output(output, this.output_buffer); + if (!this.threads) { + this.port.postMessage({ 'cmd': 'read', 'data': chunk }); + } + } else { + // this.port.postMessage('Output buffer has not enough frames! Skipping output frame.'); // Uncomment this line to debug output buffer. + } + } + this.process_notify(); + return true; + } + + static write_output(dest, source) { + const channels = dest.length; + for (let ch = 0; ch < channels; ch++) { + for (let sample = 0; sample < dest[ch].length; sample++) { + dest[ch][sample] = source[sample * channels + ch]; + } + } + } + + static write_input(dest, source) { + const channels = source.length; + for (let ch = 0; ch < channels; ch++) { + for (let sample = 0; sample < source[ch].length; sample++) { + dest[sample * channels + ch] = source[ch][sample]; + } + } + } +} + +registerProcessor('godot-processor', GodotProcessor); diff --git a/web_assets/index.html b/web_assets/index.html new file mode 100644 index 0000000..385e7c9 --- /dev/null +++ b/web_assets/index.html @@ -0,0 +1,238 @@ + + + + + + + + AI Town Game + + + + + + + + + Your browser does not support the canvas tag. + + + + +
+ + +
+
+ + + + + + diff --git a/web_assets/index.icon.png b/web_assets/index.icon.png new file mode 100644 index 0000000..12e35af Binary files /dev/null and b/web_assets/index.icon.png differ diff --git a/web_assets/index.js b/web_assets/index.js new file mode 100644 index 0000000..6f56154 --- /dev/null +++ b/web_assets/index.js @@ -0,0 +1,927 @@ +var Godot = (() => { + var _scriptName = typeof document != 'undefined' ? document.currentScript?.src : undefined; + return ( +async function(moduleArg = {}) { + var moduleRtn; + +var Module=moduleArg;var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof WorkerGlobalScope!="undefined";var ENVIRONMENT_IS_NODE=typeof process=="object"&&process.versions?.node&&process.type!="renderer";var ENVIRONMENT_IS_SHELL=!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_NODE&&!ENVIRONMENT_IS_WORKER;var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};if(ENVIRONMENT_IS_WORKER){_scriptName=self.location.href}var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_SHELL){const isNode=typeof process=="object"&&process.versions?.node&&process.type!="renderer";if(isNode||typeof window=="object"||typeof WorkerGlobalScope!="undefined")throw new Error("not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)")}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}if(!(typeof window=="object"||typeof WorkerGlobalScope!="undefined"))throw new Error("not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)");{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{assert(!isFileURI(url),"readAsync does not work with file:// URLs");var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{throw new Error("environment detection error")}var out=console.log.bind(console);var err=console.error.bind(console);var IDBFS="IDBFS is no longer included by default; build with -lidbfs.js";assert(!ENVIRONMENT_IS_NODE,"node environment detected but not enabled at build time. Add `node` to `-sENVIRONMENT` to enable.");assert(!ENVIRONMENT_IS_SHELL,"shell environment detected but not enabled at build time. Add `shell` to `-sENVIRONMENT` to enable.");var wasmBinary;if(typeof WebAssembly!="object"){err("no native wasm support detected")}var ABORT=false;var EXITSTATUS;function assert(condition,text){if(!condition){abort("Assertion failed"+(text?": "+text:""))}}var isFileURI=filename=>filename.startsWith("file://");function writeStackCookie(){var max=_emscripten_stack_get_end();assert((max&3)==0);if(max==0){max+=4}HEAPU32[max>>2]=34821223;HEAPU32[max+4>>2]=2310721022;HEAPU32[0>>2]=1668509029}function checkStackCookie(){if(ABORT)return;var max=_emscripten_stack_get_end();if(max==0){max+=4}var cookie1=HEAPU32[max>>2];var cookie2=HEAPU32[max+4>>2];if(cookie1!=34821223||cookie2!=2310721022){abort(`Stack overflow! Stack cookie has been overwritten at ${ptrToString(max)}, expected hex dwords 0x89BACDFE and 0x2135467, but received ${ptrToString(cookie2)} ${ptrToString(cookie1)}`)}if(HEAPU32[0>>2]!=1668509029){abort("Runtime error: The application has corrupted its heap memory area (address zero)!")}}var runtimeDebug=true;(()=>{var h16=new Int16Array(1);var h8=new Int8Array(h16.buffer);h16[0]=25459;if(h8[0]!==115||h8[1]!==99)throw"Runtime error: expected the system to be little-endian! (Run with -sSUPPORT_BIG_ENDIAN to bypass)"})();function consumedModuleProp(prop){if(!Object.getOwnPropertyDescriptor(Module,prop)){Object.defineProperty(Module,prop,{configurable:true,set(){abort(`Attempt to set \`Module.${prop}\` after it has already been processed. This can happen, for example, when code is injected via '--post-js' rather than '--pre-js'`)}})}}function makeInvalidEarlyAccess(name){return()=>assert(false,`call to '${name}' via reference taken before Wasm module initialization`)}function ignoredModuleProp(prop){if(Object.getOwnPropertyDescriptor(Module,prop)){abort(`\`Module.${prop}\` was supplied but \`${prop}\` not included in INCOMING_MODULE_JS_API`)}}function isExportedByForceFilesystem(name){return name==="FS_createPath"||name==="FS_createDataFile"||name==="FS_createPreloadedFile"||name==="FS_unlink"||name==="addRunDependency"||name==="FS_createLazyFile"||name==="FS_createDevice"||name==="removeRunDependency"}function hookGlobalSymbolAccess(sym,func){}function missingGlobal(sym,msg){hookGlobalSymbolAccess(sym,()=>{warnOnce(`\`${sym}\` is not longer defined by emscripten. ${msg}`)})}missingGlobal("buffer","Please use HEAP8.buffer or wasmMemory.buffer");missingGlobal("asm","Please use wasmExports instead");function missingLibrarySymbol(sym){hookGlobalSymbolAccess(sym,()=>{var msg=`\`${sym}\` is a library symbol and not included by default; add it to your library.js __deps or to DEFAULT_LIBRARY_FUNCS_TO_INCLUDE on the command line`;var librarySymbol=sym;if(!librarySymbol.startsWith("_")){librarySymbol="$"+sym}msg+=` (e.g. -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE='${librarySymbol}')`;if(isExportedByForceFilesystem(sym)){msg+=". Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you"}warnOnce(msg)});unexportedRuntimeSymbol(sym)}function unexportedRuntimeSymbol(sym){if(!Object.getOwnPropertyDescriptor(Module,sym)){Object.defineProperty(Module,sym,{configurable:true,get(){var msg=`'${sym}' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the Emscripten FAQ)`;if(isExportedByForceFilesystem(sym)){msg+=". Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you"}abort(msg)}})}}var readyPromiseResolve,readyPromiseReject;var wasmMemory;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;var runtimeExited=false;function updateMemoryViews(){var b=wasmMemory.buffer;Module["HEAP8"]=HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b);Module["HEAP64"]=HEAP64=new BigInt64Array(b);Module["HEAPU64"]=HEAPU64=new BigUint64Array(b)}assert(typeof Int32Array!="undefined"&&typeof Float64Array!=="undefined"&&Int32Array.prototype.subarray!=undefined&&Int32Array.prototype.set!=undefined,"JS engine does not provide full typed array support");function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}consumedModuleProp("preRun");callRuntimeCallbacks(onPreRuns)}function initRuntime(){assert(!runtimeInitialized);runtimeInitialized=true;checkStackCookie();if(!Module["noFSInit"]&&!FS.initialized)FS.init();TTY.init();wasmExports["__wasm_call_ctors"]();FS.ignorePermissions=false}function preMain(){checkStackCookie()}function exitRuntime(){assert(!runtimeExited);checkStackCookie();___funcs_on_exit();FS.quit();TTY.shutdown();IDBFS.quit();runtimeExited=true}function postRun(){checkStackCookie();if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}consumedModuleProp("postRun");callRuntimeCallbacks(onPostRuns)}var runDependencies=0;var dependenciesFulfilled=null;var runDependencyTracking={};var runDependencyWatcher=null;function addRunDependency(id){runDependencies++;Module["monitorRunDependencies"]?.(runDependencies);if(id){assert(!runDependencyTracking[id]);runDependencyTracking[id]=1;if(runDependencyWatcher===null&&typeof setInterval!="undefined"){runDependencyWatcher=setInterval(()=>{if(ABORT){clearInterval(runDependencyWatcher);runDependencyWatcher=null;return}var shown=false;for(var dep in runDependencyTracking){if(!shown){shown=true;err("still waiting on run dependencies:")}err(`dependency: ${dep}`)}if(shown){err("(end of list)")}},1e4)}}else{err("warning: run dependency added without ID")}}function removeRunDependency(id){runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(id){assert(runDependencyTracking[id]);delete runDependencyTracking[id]}else{err("warning: run dependency removed without ID")}if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}function createExportWrapper(name,nargs){return(...args)=>{assert(runtimeInitialized,`native function \`${name}\` called before runtime initialization`);assert(!runtimeExited,`native function \`${name}\` called after runtime exit (use NO_EXIT_RUNTIME to keep it alive after main() exits)`);var f=wasmExports[name];assert(f,`exported native function \`${name}\` not found`);assert(args.length<=nargs,`native function \`${name}\` called with ${args.length} args but expects ${nargs}`);return f(...args)}}var wasmBinaryFile;function findWasmBinary(){return locateFile("godot.web.template_release.wasm32.nothreads.wasm")}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);if(isFileURI(wasmBinaryFile)){err(`warning: Loading from a file URI (${wasmBinaryFile}) is not supported in most browsers. See https://emscripten.org/docs/getting_started/FAQ.html#how-do-i-run-a-local-webserver-for-testing-why-does-my-program-stall-in-downloading-or-preparing`)}abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){return{env:wasmImports,wasi_snapshot_preview1:wasmImports}}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["memory"];assert(wasmMemory,"memory not found in wasm exports");updateMemoryViews();wasmTable=wasmExports["__indirect_function_table"];assert(wasmTable,"table not found in wasm exports");assignWasmExports(wasmExports);removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");var trueModule=Module;function receiveInstantiationResult(result){assert(Module===trueModule,"the Module object should not be replaced during async compilation - perhaps the order of HTML elements is wrong?");trueModule=null;return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{try{Module["instantiateWasm"](info,(mod,inst)=>{resolve(receiveInstance(mod,inst))})}catch(e){err(`Module.instantiateWasm callback failed with error: ${e}`);reject(e)}})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);function getValue(ptr,type="i8"){if(type.endsWith("*"))type="*";switch(type){case"i1":return HEAP8[ptr];case"i8":return HEAP8[ptr];case"i16":return HEAP16[ptr>>1];case"i32":return HEAP32[ptr>>2];case"i64":return HEAP64[ptr>>3];case"float":return HEAPF32[ptr>>2];case"double":return HEAPF64[ptr>>3];case"*":return HEAPU32[ptr>>2];default:abort(`invalid type for getValue: ${type}`)}}var noExitRuntime=false;var ptrToString=ptr=>{assert(typeof ptr==="number");ptr>>>=0;return"0x"+ptr.toString(16).padStart(8,"0")};function setValue(ptr,value,type="i8"){if(type.endsWith("*"))type="*";switch(type){case"i1":HEAP8[ptr]=value;break;case"i8":HEAP8[ptr]=value;break;case"i16":HEAP16[ptr>>1]=value;break;case"i32":HEAP32[ptr>>2]=value;break;case"i64":HEAP64[ptr>>3]=BigInt(value);break;case"float":HEAPF32[ptr>>2]=value;break;case"double":HEAPF64[ptr>>3]=value;break;case"*":HEAPU32[ptr>>2]=value;break;default:abort(`invalid type for setValue: ${type}`)}}var warnOnce=text=>{warnOnce.shown||={};if(!warnOnce.shown[text]){warnOnce.shown[text]=1;err(text)}};var wasmTable;var getWasmTableEntry=funcPtr=>wasmTable.get(funcPtr);var ___call_sighandler=(fp,sig)=>getWasmTableEntry(fp)(sig);var PATH={isAbs:path=>path.charAt(0)==="/",splitPath:filename=>{var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.slice(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(p=>!!p),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.slice(0,-1)}return root+dir},basename:path=>path&&path.match(/([^\/]+|\/)\/*$/)[1],join:(...paths)=>PATH.normalize(paths.join("/")),join2:(l,r)=>PATH.normalize(l+"/"+r)};var initRandomFill=()=>view=>crypto.getRandomValues(view);var randomFill=view=>{(randomFill=initRandomFill())(view)};var PATH_FS={resolve:(...args)=>{var resolvedPath="",resolvedAbsolute=false;for(var i=args.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?args[i]:FS.cwd();if(typeof path!="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){return""}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split("/").filter(p=>!!p),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."},relative:(from,to)=>{from=PATH_FS.resolve(from).slice(1);to=PATH_FS.resolve(to).slice(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i{var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var FS_stdin_getChar_buffer=[];var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{assert(typeof str==="string",`stringToUTF8Array expects a string (got ${typeof str})`);if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;if(u>1114111)warnOnce("Invalid Unicode code point "+ptrToString(u)+" encountered when serializing a JS string to a UTF-8 string in wasm memory! (Valid unicode code points should be in range 0-0x10FFFF).");heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var intArrayFromString=(stringy,dontAddNull,length)=>{var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array};var FS_stdin_getChar=()=>{if(!FS_stdin_getChar_buffer.length){var result=null;if(typeof window!="undefined"&&typeof window.prompt=="function"){result=window.prompt("Input: ");if(result!==null){result+="\n"}}else{}if(!result){return null}FS_stdin_getChar_buffer=intArrayFromString(result,true)}return FS_stdin_getChar_buffer.shift()};var TTY={ttys:[],init(){},shutdown(){},register(dev,ops){TTY.ttys[dev]={input:[],output:[],ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close(stream){stream.tty.ops.fsync(stream.tty)},fsync(stream){stream.tty.ops.fsync(stream.tty)},read(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i0){out(UTF8ArrayToString(tty.output));tty.output=[]}},ioctl_tcgets(tty){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:35387,c_cc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},ioctl_tcsets(tty,optional_actions,data){return 0},ioctl_tiocgwinsz(tty){return[24,80]}},default_tty1_ops:{put_char(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output?.length>0){err(UTF8ArrayToString(tty.output));tty.output=[]}}}};var mmapAlloc=size=>{abort("internal error: mmapAlloc called but `emscripten_builtin_memalign` native symbol not exported")};var MEMFS={ops_table:null,mount(mount){return MEMFS.createNode(null,"/",16895,0)},createNode(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}MEMFS.ops_table||={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}};var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=null}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.atime=node.mtime=node.ctime=Date.now();if(parent){parent.contents[name]=node;parent.atime=parent.mtime=parent.ctime=node.atime}return node},getFileDataAsTypedArray(node){if(!node.contents)return new Uint8Array(0);if(node.contents.subarray)return node.contents.subarray(0,node.usedBytes);return new Uint8Array(node.contents)},expandFileStorage(node,newCapacity){var prevCapacity=node.contents?node.contents.length:0;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.atime);attr.mtime=new Date(node.mtime);attr.ctime=new Date(node.ctime);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr(node,attr){for(const key of["mode","atime","mtime","ctime"]){if(attr[key]!=null){node[key]=attr[key]}}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup(parent,name){throw new FS.ErrnoError(44)},mknod(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename(old_node,new_dir,new_name){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){if(FS.isDir(old_node.mode)){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}FS.hashRemoveNode(new_node)}delete old_node.parent.contents[old_node.name];new_dir.contents[new_name]=old_node;old_node.name=new_name;new_dir.ctime=new_dir.mtime=old_node.parent.ctime=old_node.parent.mtime=Date.now()},unlink(parent,name){delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},rmdir(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},readdir(node){return[".","..",...Object.keys(node.contents)]},symlink(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);assert(size>=0);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i0||position+length{var arrayBuffer=await readAsync(url);assert(arrayBuffer,`Loading data file "${url}" failed (no arrayBuffer).`);return new Uint8Array(arrayBuffer)};var FS_createDataFile=(...args)=>FS.createDataFile(...args);var getUniqueRunDependency=id=>{var orig=id;while(1){if(!runDependencyTracking[id])return id;id=orig+Math.random()}};var preloadPlugins=[];var FS_handledByPreloadPlugin=(byteArray,fullname,finish,onerror)=>{if(typeof Browser!="undefined")Browser.init();var handled=false;preloadPlugins.forEach(plugin=>{if(handled)return;if(plugin["canHandle"](fullname)){plugin["handle"](byteArray,fullname,finish,onerror);handled=true}});return handled};var FS_createPreloadedFile=(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);function processData(byteArray){function finish(byteArray){preFinish?.();if(!dontCreateFile){FS_createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}onload?.();removeRunDependency(dep)}if(FS_handledByPreloadPlugin(byteArray,fullname,finish,()=>{onerror?.();removeRunDependency(dep)})){return}finish(byteArray)}addRunDependency(dep);if(typeof url=="string"){asyncLoad(url).then(processData,onerror)}else{processData(url)}};var FS_modeStringToFlags=str=>{var flagModes={r:0,"r+":2,w:512|64|1,"w+":512|64|2,a:1024|64|1,"a+":1024|64|2};var flags=flagModes[str];if(typeof flags=="undefined"){throw new Error(`Unknown file open mode: ${str}`)}return flags};var FS_getMode=(canRead,canWrite)=>{var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode};var IDBFS={dbs:{},indexedDB:()=>{if(typeof indexedDB!="undefined")return indexedDB;var ret=null;if(typeof window=="object")ret=window.indexedDB||window.mozIndexedDB||window.webkitIndexedDB||window.msIndexedDB;assert(ret,"IDBFS used, but indexedDB not supported");return ret},DB_VERSION:21,DB_STORE_NAME:"FILE_DATA",queuePersist:mount=>{function onPersistComplete(){if(mount.idbPersistState==="again")startPersist();else mount.idbPersistState=0}function startPersist(){mount.idbPersistState="idb";IDBFS.syncfs(mount,false,onPersistComplete)}if(!mount.idbPersistState){mount.idbPersistState=setTimeout(startPersist,0)}else if(mount.idbPersistState==="idb"){mount.idbPersistState="again"}},mount:mount=>{var mnt=MEMFS.mount(mount);if(mount?.opts?.autoPersist){mnt.idbPersistState=0;var memfs_node_ops=mnt.node_ops;mnt.node_ops={...mnt.node_ops};mnt.node_ops.mknod=(parent,name,mode,dev)=>{var node=memfs_node_ops.mknod(parent,name,mode,dev);node.node_ops=mnt.node_ops;node.idbfs_mount=mnt.mount;node.memfs_stream_ops=node.stream_ops;node.stream_ops={...node.stream_ops};node.stream_ops.write=(stream,buffer,offset,length,position,canOwn)=>{stream.node.isModified=true;return node.memfs_stream_ops.write(stream,buffer,offset,length,position,canOwn)};node.stream_ops.close=stream=>{var n=stream.node;if(n.isModified){IDBFS.queuePersist(n.idbfs_mount);n.isModified=false}if(n.memfs_stream_ops.close)return n.memfs_stream_ops.close(stream)};return node};mnt.node_ops.mkdir=(...args)=>(IDBFS.queuePersist(mnt.mount),memfs_node_ops.mkdir(...args));mnt.node_ops.rmdir=(...args)=>(IDBFS.queuePersist(mnt.mount),memfs_node_ops.rmdir(...args));mnt.node_ops.symlink=(...args)=>(IDBFS.queuePersist(mnt.mount),memfs_node_ops.symlink(...args));mnt.node_ops.unlink=(...args)=>(IDBFS.queuePersist(mnt.mount),memfs_node_ops.unlink(...args));mnt.node_ops.rename=(...args)=>(IDBFS.queuePersist(mnt.mount),memfs_node_ops.rename(...args))}return mnt},syncfs:(mount,populate,callback)=>{IDBFS.getLocalSet(mount,(err,local)=>{if(err)return callback(err);IDBFS.getRemoteSet(mount,(err,remote)=>{if(err)return callback(err);var src=populate?remote:local;var dst=populate?local:remote;IDBFS.reconcile(src,dst,callback)})})},quit:()=>{Object.values(IDBFS.dbs).forEach(value=>value.close());IDBFS.dbs={}},getDB:(name,callback)=>{var db=IDBFS.dbs[name];if(db){return callback(null,db)}var req;try{req=IDBFS.indexedDB().open(name,IDBFS.DB_VERSION)}catch(e){return callback(e)}if(!req){return callback("Unable to connect to IndexedDB")}req.onupgradeneeded=e=>{var db=e.target.result;var transaction=e.target.transaction;var fileStore;if(db.objectStoreNames.contains(IDBFS.DB_STORE_NAME)){fileStore=transaction.objectStore(IDBFS.DB_STORE_NAME)}else{fileStore=db.createObjectStore(IDBFS.DB_STORE_NAME)}if(!fileStore.indexNames.contains("timestamp")){fileStore.createIndex("timestamp","timestamp",{unique:false})}};req.onsuccess=()=>{db=req.result;IDBFS.dbs[name]=db;callback(null,db)};req.onerror=e=>{callback(e.target.error);e.preventDefault()}},getLocalSet:(mount,callback)=>{var entries={};function isRealDir(p){return p!=="."&&p!==".."}function toAbsolute(root){return p=>PATH.join2(root,p)}var check=FS.readdir(mount.mountpoint).filter(isRealDir).map(toAbsolute(mount.mountpoint));while(check.length){var path=check.pop();var stat;try{stat=FS.stat(path)}catch(e){return callback(e)}if(FS.isDir(stat.mode)){check.push(...FS.readdir(path).filter(isRealDir).map(toAbsolute(path)))}entries[path]={timestamp:stat.mtime}}return callback(null,{type:"local",entries})},getRemoteSet:(mount,callback)=>{var entries={};IDBFS.getDB(mount.mountpoint,(err,db)=>{if(err)return callback(err);try{var transaction=db.transaction([IDBFS.DB_STORE_NAME],"readonly");transaction.onerror=e=>{callback(e.target.error);e.preventDefault()};var store=transaction.objectStore(IDBFS.DB_STORE_NAME);var index=store.index("timestamp");index.openKeyCursor().onsuccess=event=>{var cursor=event.target.result;if(!cursor){return callback(null,{type:"remote",db,entries})}entries[cursor.primaryKey]={timestamp:cursor.key};cursor.continue()}}catch(e){return callback(e)}})},loadLocalEntry:(path,callback)=>{var stat,node;try{var lookup=FS.lookupPath(path);node=lookup.node;stat=FS.stat(path)}catch(e){return callback(e)}if(FS.isDir(stat.mode)){return callback(null,{timestamp:stat.mtime,mode:stat.mode})}else if(FS.isFile(stat.mode)){node.contents=MEMFS.getFileDataAsTypedArray(node);return callback(null,{timestamp:stat.mtime,mode:stat.mode,contents:node.contents})}else{return callback(new Error("node type not supported"))}},storeLocalEntry:(path,entry,callback)=>{try{if(FS.isDir(entry["mode"])){FS.mkdirTree(path,entry["mode"])}else if(FS.isFile(entry["mode"])){FS.writeFile(path,entry["contents"],{canOwn:true})}else{return callback(new Error("node type not supported"))}FS.chmod(path,entry["mode"]);FS.utime(path,entry["timestamp"],entry["timestamp"])}catch(e){return callback(e)}callback(null)},removeLocalEntry:(path,callback)=>{try{var stat=FS.stat(path);if(FS.isDir(stat.mode)){FS.rmdir(path)}else if(FS.isFile(stat.mode)){FS.unlink(path)}}catch(e){return callback(e)}callback(null)},loadRemoteEntry:(store,path,callback)=>{var req=store.get(path);req.onsuccess=event=>callback(null,event.target.result);req.onerror=e=>{callback(e.target.error);e.preventDefault()}},storeRemoteEntry:(store,path,entry,callback)=>{try{var req=store.put(entry,path)}catch(e){callback(e);return}req.onsuccess=event=>callback();req.onerror=e=>{callback(e.target.error);e.preventDefault()}},removeRemoteEntry:(store,path,callback)=>{var req=store.delete(path);req.onsuccess=event=>callback();req.onerror=e=>{callback(e.target.error);e.preventDefault()}},reconcile:(src,dst,callback)=>{var total=0;var create=[];Object.keys(src.entries).forEach(key=>{var e=src.entries[key];var e2=dst.entries[key];if(!e2||e["timestamp"].getTime()!=e2["timestamp"].getTime()){create.push(key);total++}});var remove=[];Object.keys(dst.entries).forEach(key=>{if(!src.entries[key]){remove.push(key);total++}});if(!total){return callback(null)}var errored=false;var db=src.type==="remote"?src.db:dst.db;var transaction=db.transaction([IDBFS.DB_STORE_NAME],"readwrite");var store=transaction.objectStore(IDBFS.DB_STORE_NAME);function done(err){if(err&&!errored){errored=true;return callback(err)}}transaction.onerror=transaction.onabort=e=>{done(e.target.error);e.preventDefault()};transaction.oncomplete=e=>{if(!errored){callback(null)}};create.sort().forEach(path=>{if(dst.type==="local"){IDBFS.loadRemoteEntry(store,path,(err,entry)=>{if(err)return done(err);IDBFS.storeLocalEntry(path,entry,done)})}else{IDBFS.loadLocalEntry(path,(err,entry)=>{if(err)return done(err);IDBFS.storeRemoteEntry(store,path,entry,done)})}});remove.sort().reverse().forEach(path=>{if(dst.type==="local"){IDBFS.removeLocalEntry(path,done)}else{IDBFS.removeRemoteEntry(store,path,done)}})}};var UTF8ToString=(ptr,maxBytesToRead)=>{assert(typeof ptr=="number",`UTF8ToString expects a number (got ${typeof ptr})`);return ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):""};var strError=errno=>UTF8ToString(_strerror(errno));var ERRNO_CODES={EPERM:63,ENOENT:44,ESRCH:71,EINTR:27,EIO:29,ENXIO:60,E2BIG:1,ENOEXEC:45,EBADF:8,ECHILD:12,EAGAIN:6,EWOULDBLOCK:6,ENOMEM:48,EACCES:2,EFAULT:21,ENOTBLK:105,EBUSY:10,EEXIST:20,EXDEV:75,ENODEV:43,ENOTDIR:54,EISDIR:31,EINVAL:28,ENFILE:41,EMFILE:33,ENOTTY:59,ETXTBSY:74,EFBIG:22,ENOSPC:51,ESPIPE:70,EROFS:69,EMLINK:34,EPIPE:64,EDOM:18,ERANGE:68,ENOMSG:49,EIDRM:24,ECHRNG:106,EL2NSYNC:156,EL3HLT:107,EL3RST:108,ELNRNG:109,EUNATCH:110,ENOCSI:111,EL2HLT:112,EDEADLK:16,ENOLCK:46,EBADE:113,EBADR:114,EXFULL:115,ENOANO:104,EBADRQC:103,EBADSLT:102,EDEADLOCK:16,EBFONT:101,ENOSTR:100,ENODATA:116,ETIME:117,ENOSR:118,ENONET:119,ENOPKG:120,EREMOTE:121,ENOLINK:47,EADV:122,ESRMNT:123,ECOMM:124,EPROTO:65,EMULTIHOP:36,EDOTDOT:125,EBADMSG:9,ENOTUNIQ:126,EBADFD:127,EREMCHG:128,ELIBACC:129,ELIBBAD:130,ELIBSCN:131,ELIBMAX:132,ELIBEXEC:133,ENOSYS:52,ENOTEMPTY:55,ENAMETOOLONG:37,ELOOP:32,EOPNOTSUPP:138,EPFNOSUPPORT:139,ECONNRESET:15,ENOBUFS:42,EAFNOSUPPORT:5,EPROTOTYPE:67,ENOTSOCK:57,ENOPROTOOPT:50,ESHUTDOWN:140,ECONNREFUSED:14,EADDRINUSE:3,ECONNABORTED:13,ENETUNREACH:40,ENETDOWN:38,ETIMEDOUT:73,EHOSTDOWN:142,EHOSTUNREACH:23,EINPROGRESS:26,EALREADY:7,EDESTADDRREQ:17,EMSGSIZE:35,EPROTONOSUPPORT:66,ESOCKTNOSUPPORT:137,EADDRNOTAVAIL:4,ENETRESET:39,EISCONN:30,ENOTCONN:53,ETOOMANYREFS:141,EUSERS:136,EDQUOT:19,ESTALE:72,ENOTSUP:138,ENOMEDIUM:148,EILSEQ:25,EOVERFLOW:61,ECANCELED:11,ENOTRECOVERABLE:56,EOWNERDEAD:62,ESTRPIPE:135};var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,filesystems:null,syncFSRequests:0,readFiles:{},ErrnoError:class extends Error{name="ErrnoError";constructor(errno){super(runtimeInitialized?strError(errno):"");this.errno=errno;for(var key in ERRNO_CODES){if(ERRNO_CODES[key]===errno){this.code=key;break}}}},FSStream:class{shared={};get object(){return this.node}set object(val){this.node=val}get isRead(){return(this.flags&2097155)!==1}get isWrite(){return(this.flags&2097155)!==0}get isAppend(){return this.flags&1024}get flags(){return this.shared.flags}set flags(val){this.shared.flags=val}get position(){return this.shared.position}set position(val){this.shared.position=val}},FSNode:class{node_ops={};stream_ops={};readMode=292|73;writeMode=146;mounted=null;constructor(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.rdev=rdev;this.atime=this.mtime=this.ctime=Date.now()}get read(){return(this.mode&this.readMode)===this.readMode}set read(val){val?this.mode|=this.readMode:this.mode&=~this.readMode}get write(){return(this.mode&this.writeMode)===this.writeMode}set write(val){val?this.mode|=this.writeMode:this.mode&=~this.writeMode}get isFolder(){return FS.isDir(this.mode)}get isDevice(){return FS.isChrdev(this.mode)}},lookupPath(path,opts={}){if(!path){throw new FS.ErrnoError(44)}opts.follow_mount??=true;if(!PATH.isAbs(path)){path=FS.cwd()+"/"+path}linkloop:for(var nlinks=0;nlinks<40;nlinks++){var parts=path.split("/").filter(p=>!!p);var current=FS.root;var current_path="/";for(var i=0;i>>0)%FS.nameTable.length},hashAddNode(node){var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode(node){var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode(parent,name){var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode(parent,name,mode,rdev){assert(typeof parent=="object");var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode(node){FS.hashRemoveNode(node)},isRoot(node){return node===node.parent},isMountpoint(node){return!!node.mounted},isFile(mode){return(mode&61440)===32768},isDir(mode){return(mode&61440)===16384},isLink(mode){return(mode&61440)===40960},isChrdev(mode){return(mode&61440)===8192},isBlkdev(mode){return(mode&61440)===24576},isFIFO(mode){return(mode&61440)===4096},isSocket(mode){return(mode&49152)===49152},flagsToPermissionString(flag){var perms=["r","w","rw"][flag&3];if(flag&512){perms+="w"}return perms},nodePermissions(node,perms){if(FS.ignorePermissions){return 0}if(perms.includes("r")&&!(node.mode&292)){return 2}else if(perms.includes("w")&&!(node.mode&146)){return 2}else if(perms.includes("x")&&!(node.mode&73)){return 2}return 0},mayLookup(dir){if(!FS.isDir(dir.mode))return 54;var errCode=FS.nodePermissions(dir,"x");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate(dir,name){if(!FS.isDir(dir.mode)){return 54}try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,"wx")},mayDelete(dir,name,isdir){var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,"wx");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen(node,flags){if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!=="r"||flags&(512|64)){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},checkOpExists(op,err){if(!op){throw new FS.ErrnoError(err)}return op},MAX_OPEN_FDS:4096,nextfd(){for(var fd=0;fd<=FS.MAX_OPEN_FDS;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStreamChecked(fd){var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}return stream},getStream:fd=>FS.streams[fd],createStream(stream,fd=-1){assert(fd>=-1);stream=Object.assign(new FS.FSStream,stream);if(fd==-1){fd=FS.nextfd()}stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream(fd){FS.streams[fd]=null},dupStream(origStream,fd=-1){var stream=FS.createStream(origStream,fd);stream.stream_ops?.dup?.(stream);return stream},doSetAttr(stream,node,attr){var setattr=stream?.stream_ops.setattr;var arg=setattr?stream:node;setattr??=node.node_ops.setattr;FS.checkOpExists(setattr,63);setattr(arg,attr)},chrdev_stream_ops:{open(stream){var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;stream.stream_ops.open?.(stream)},llseek(){throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice(dev,ops){FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts(mount){var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push(...m.mounts)}return mounts},syncfs(populate,callback){if(typeof populate=="function"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`)}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){assert(FS.syncFSRequests>0);FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}mounts.forEach(mount=>{if(!mount.type.syncfs){return done(null)}mount.type.syncfs(mount,populate,done)})},mount(type,opts,mountpoint){if(typeof type=="string"){throw type}var root=mountpoint==="/";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type,opts,mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount(mountpoint){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);Object.keys(FS.nameTable).forEach(hash=>{var current=FS.nameTable[hash];while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}});node.mounted=null;var idx=node.mount.mounts.indexOf(mount);assert(idx!==-1);node.mount.mounts.splice(idx,1)},lookup(parent,name){return parent.node_ops.lookup(parent,name)},mknod(path,mode,dev){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name){throw new FS.ErrnoError(28)}if(name==="."||name===".."){throw new FS.ErrnoError(20)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},statfs(path){return FS.statfsNode(FS.lookupPath(path,{follow:true}).node)},statfsStream(stream){return FS.statfsNode(stream.node)},statfsNode(node){var rtn={bsize:4096,frsize:4096,blocks:1e6,bfree:5e5,bavail:5e5,files:FS.nextInode,ffree:FS.nextInode-1,fsid:42,flags:2,namelen:255};if(node.node_ops.statfs){Object.assign(rtn,node.node_ops.statfs(node.mount.opts.root))}return rtn},create(path,mode=438){mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir(path,mode=511){mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree(path,mode){var dirs=path.split("/");var d="";for(var dir of dirs){if(!dir)continue;if(d||PATH.isAbs(path))d+="/";d+=dir;try{FS.mkdir(d,mode)}catch(e){if(e.errno!=20)throw e}}},mkdev(path,mode,dev){if(typeof dev=="undefined"){dev=mode;mode=438}mode|=8192;return FS.mknod(path,mode,dev)},symlink(oldpath,newpath){if(!PATH_FS.resolve(oldpath)){throw new FS.ErrnoError(44)}var lookup=FS.lookupPath(newpath,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var newname=PATH.basename(newpath);var errCode=FS.mayCreate(parent,newname);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.symlink){throw new FS.ErrnoError(63)}return parent.node_ops.symlink(parent,newname,oldpath)},rename(old_path,new_path){var old_dirname=PATH.dirname(old_path);var new_dirname=PATH.dirname(new_path);var old_name=PATH.basename(old_path);var new_name=PATH.basename(new_path);var lookup,old_dir,new_dir;lookup=FS.lookupPath(old_path,{parent:true});old_dir=lookup.node;lookup=FS.lookupPath(new_path,{parent:true});new_dir=lookup.node;if(!old_dir||!new_dir)throw new FS.ErrnoError(44);if(old_dir.mount!==new_dir.mount){throw new FS.ErrnoError(75)}var old_node=FS.lookupNode(old_dir,old_name);var relative=PATH_FS.relative(old_path,new_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(28)}relative=PATH_FS.relative(new_path,old_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(55)}var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(old_node===new_node){return}var isdir=FS.isDir(old_node.mode);var errCode=FS.mayDelete(old_dir,old_name,isdir);if(errCode){throw new FS.ErrnoError(errCode)}errCode=new_node?FS.mayDelete(new_dir,new_name,isdir):FS.mayCreate(new_dir,new_name);if(errCode){throw new FS.ErrnoError(errCode)}if(!old_dir.node_ops.rename){throw new FS.ErrnoError(63)}if(FS.isMountpoint(old_node)||new_node&&FS.isMountpoint(new_node)){throw new FS.ErrnoError(10)}if(new_dir!==old_dir){errCode=FS.nodePermissions(old_dir,"w");if(errCode){throw new FS.ErrnoError(errCode)}}FS.hashRemoveNode(old_node);try{old_dir.node_ops.rename(old_node,new_dir,new_name);old_node.parent=new_dir}catch(e){throw e}finally{FS.hashAddNode(old_node)}},rmdir(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,true);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.rmdir){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.rmdir(parent,name);FS.destroyNode(node)},readdir(path){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var readdir=FS.checkOpExists(node.node_ops.readdir,54);return readdir(node)},unlink(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,false);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.unlink){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.unlink(parent,name);FS.destroyNode(node)},readlink(path){var lookup=FS.lookupPath(path);var link=lookup.node;if(!link){throw new FS.ErrnoError(44)}if(!link.node_ops.readlink){throw new FS.ErrnoError(28)}return link.node_ops.readlink(link)},stat(path,dontFollow){var lookup=FS.lookupPath(path,{follow:!dontFollow});var node=lookup.node;var getattr=FS.checkOpExists(node.node_ops.getattr,63);return getattr(node)},fstat(fd){var stream=FS.getStreamChecked(fd);var node=stream.node;var getattr=stream.stream_ops.getattr;var arg=getattr?stream:node;getattr??=node.node_ops.getattr;FS.checkOpExists(getattr,63);return getattr(arg)},lstat(path){return FS.stat(path,true)},doChmod(stream,node,mode,dontFollow){FS.doSetAttr(stream,node,{mode:mode&4095|node.mode&~4095,ctime:Date.now(),dontFollow})},chmod(path,mode,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChmod(null,node,mode,dontFollow)},lchmod(path,mode){FS.chmod(path,mode,true)},fchmod(fd,mode){var stream=FS.getStreamChecked(fd);FS.doChmod(stream,stream.node,mode,false)},doChown(stream,node,dontFollow){FS.doSetAttr(stream,node,{timestamp:Date.now(),dontFollow})},chown(path,uid,gid,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChown(null,node,dontFollow)},lchown(path,uid,gid){FS.chown(path,uid,gid,true)},fchown(fd,uid,gid){var stream=FS.getStreamChecked(fd);FS.doChown(stream,stream.node,false)},doTruncate(stream,node,len){if(FS.isDir(node.mode)){throw new FS.ErrnoError(31)}if(!FS.isFile(node.mode)){throw new FS.ErrnoError(28)}var errCode=FS.nodePermissions(node,"w");if(errCode){throw new FS.ErrnoError(errCode)}FS.doSetAttr(stream,node,{size:len,timestamp:Date.now()})},truncate(path,len){if(len<0){throw new FS.ErrnoError(28)}var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:true});node=lookup.node}else{node=path}FS.doTruncate(null,node,len)},ftruncate(fd,len){var stream=FS.getStreamChecked(fd);if(len<0||(stream.flags&2097155)===0){throw new FS.ErrnoError(28)}FS.doTruncate(stream,stream.node,len)},utime(path,atime,mtime){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var setattr=FS.checkOpExists(node.node_ops.setattr,63);setattr(node,{atime,mtime})},open(path,flags,mode=438){if(path===""){throw new FS.ErrnoError(44)}flags=typeof flags=="string"?FS_modeStringToFlags(flags):flags;if(flags&64){mode=mode&4095|32768}else{mode=0}var node;var isDirPath;if(typeof path=="object"){node=path}else{isDirPath=path.endsWith("/");var lookup=FS.lookupPath(path,{follow:!(flags&131072),noent_okay:true});node=lookup.node;path=lookup.path}var created=false;if(flags&64){if(node){if(flags&128){throw new FS.ErrnoError(20)}}else if(isDirPath){throw new FS.ErrnoError(31)}else{node=FS.mknod(path,mode|511,0);created=true}}if(!node){throw new FS.ErrnoError(44)}if(FS.isChrdev(node.mode)){flags&=~512}if(flags&65536&&!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}if(!created){var errCode=FS.mayOpen(node,flags);if(errCode){throw new FS.ErrnoError(errCode)}}if(flags&512&&!created){FS.truncate(node,0)}flags&=~(128|512|131072);var stream=FS.createStream({node,path:FS.getPath(node),flags,seekable:true,position:0,stream_ops:node.stream_ops,ungotten:[],error:false});if(stream.stream_ops.open){stream.stream_ops.open(stream)}if(created){FS.chmod(node,mode&511)}if(Module["logReadFiles"]&&!(flags&1)){if(!(path in FS.readFiles)){FS.readFiles[path]=1}}return stream},close(stream){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(stream.getdents)stream.getdents=null;try{if(stream.stream_ops.close){stream.stream_ops.close(stream)}}catch(e){throw e}finally{FS.closeStream(stream.fd)}stream.fd=null},isClosed(stream){return stream.fd===null},llseek(stream,offset,whence){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(!stream.seekable||!stream.stream_ops.llseek){throw new FS.ErrnoError(70)}if(whence!=0&&whence!=1&&whence!=2){throw new FS.ErrnoError(28)}stream.position=stream.stream_ops.llseek(stream,offset,whence);stream.ungotten=[];return stream.position},read(stream,buffer,offset,length,position){assert(offset>=0);if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.read){throw new FS.ErrnoError(28)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesRead=stream.stream_ops.read(stream,buffer,offset,length,position);if(!seeking)stream.position+=bytesRead;return bytesRead},write(stream,buffer,offset,length,position,canOwn){assert(offset>=0);if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.write){throw new FS.ErrnoError(28)}if(stream.seekable&&stream.flags&1024){FS.llseek(stream,0,2)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesWritten=stream.stream_ops.write(stream,buffer,offset,length,position,canOwn);if(!seeking)stream.position+=bytesWritten;return bytesWritten},mmap(stream,length,position,prot,flags){if((prot&2)!==0&&(flags&2)===0&&(stream.flags&2097155)!==2){throw new FS.ErrnoError(2)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(2)}if(!stream.stream_ops.mmap){throw new FS.ErrnoError(43)}if(!length){throw new FS.ErrnoError(28)}return stream.stream_ops.mmap(stream,length,position,prot,flags)},msync(stream,buffer,offset,length,mmapFlags){assert(offset>=0);if(!stream.stream_ops.msync){return 0}return stream.stream_ops.msync(stream,buffer,offset,length,mmapFlags)},ioctl(stream,cmd,arg){if(!stream.stream_ops.ioctl){throw new FS.ErrnoError(59)}return stream.stream_ops.ioctl(stream,cmd,arg)},readFile(path,opts={}){opts.flags=opts.flags||0;opts.encoding=opts.encoding||"binary";if(opts.encoding!=="utf8"&&opts.encoding!=="binary"){throw new Error(`Invalid encoding type "${opts.encoding}"`)}var stream=FS.open(path,opts.flags);var stat=FS.stat(path);var length=stat.size;var buf=new Uint8Array(length);FS.read(stream,buf,0,length,0);if(opts.encoding==="utf8"){buf=UTF8ArrayToString(buf)}FS.close(stream);return buf},writeFile(path,data,opts={}){opts.flags=opts.flags||577;var stream=FS.open(path,opts.flags,opts.mode);if(typeof data=="string"){data=new Uint8Array(intArrayFromString(data,true))}if(ArrayBuffer.isView(data)){FS.write(stream,data,0,data.byteLength,undefined,opts.canOwn)}else{throw new Error("Unsupported data type")}FS.close(stream)},cwd:()=>FS.currentPath,chdir(path){var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,"x");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories(){FS.mkdir("/tmp");FS.mkdir("/home");FS.mkdir("/home/web_user")},createDefaultDevices(){FS.mkdir("/dev");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length,llseek:()=>0});FS.mkdev("/dev/null",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev("/dev/tty",FS.makedev(5,0));FS.mkdev("/dev/tty1",FS.makedev(6,0));var randomBuffer=new Uint8Array(1024),randomLeft=0;var randomByte=()=>{if(randomLeft===0){randomFill(randomBuffer);randomLeft=randomBuffer.byteLength}return randomBuffer[--randomLeft]};FS.createDevice("/dev","random",randomByte);FS.createDevice("/dev","urandom",randomByte);FS.mkdir("/dev/shm");FS.mkdir("/dev/shm/tmp")},createSpecialDirectories(){FS.mkdir("/proc");var proc_self=FS.mkdir("/proc/self");FS.mkdir("/proc/self/fd");FS.mount({mount(){var node=FS.createNode(proc_self,"fd",16895,73);node.stream_ops={llseek:MEMFS.stream_ops.llseek};node.node_ops={lookup(parent,name){var fd=+name;var stream=FS.getStreamChecked(fd);var ret={parent:null,mount:{mountpoint:"fake"},node_ops:{readlink:()=>stream.path},id:fd+1};ret.parent=ret;return ret},readdir(){return Array.from(FS.streams.entries()).filter(([k,v])=>v).map(([k,v])=>k.toString())}};return node}},{},"/proc/self/fd")},createStandardStreams(input,output,error){if(input){FS.createDevice("/dev","stdin",input)}else{FS.symlink("/dev/tty","/dev/stdin")}if(output){FS.createDevice("/dev","stdout",null,output)}else{FS.symlink("/dev/tty","/dev/stdout")}if(error){FS.createDevice("/dev","stderr",null,error)}else{FS.symlink("/dev/tty1","/dev/stderr")}var stdin=FS.open("/dev/stdin",0);var stdout=FS.open("/dev/stdout",1);var stderr=FS.open("/dev/stderr",1);assert(stdin.fd===0,`invalid handle for stdin (${stdin.fd})`);assert(stdout.fd===1,`invalid handle for stdout (${stdout.fd})`);assert(stderr.fd===2,`invalid handle for stderr (${stderr.fd})`)},staticInit(){FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={MEMFS,IDBFS}},init(input,output,error){assert(!FS.initialized,"FS.init was previously called. If you want to initialize later with custom parameters, remove any earlier calls (note that one is automatically added to the generated code)");FS.initialized=true;input??=Module["stdin"];output??=Module["stdout"];error??=Module["stderr"];FS.createStandardStreams(input,output,error)},quit(){FS.initialized=false;_fflush(0);for(var stream of FS.streams){if(stream){FS.close(stream)}}},findObject(path,dontResolveLastLink){var ret=FS.analyzePath(path,dontResolveLastLink);if(!ret.exists){return null}return ret.object},analyzePath(path,dontResolveLastLink){try{var lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});path=lookup.path}catch(e){}var ret={isRoot:false,exists:false,error:0,name:null,path:null,object:null,parentExists:false,parentPath:null,parentObject:null};try{var lookup=FS.lookupPath(path,{parent:true});ret.parentExists=true;ret.parentPath=lookup.path;ret.parentObject=lookup.node;ret.name=PATH.basename(path);lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});ret.exists=true;ret.path=lookup.path;ret.object=lookup.node;ret.name=lookup.node.name;ret.isRoot=lookup.path==="/"}catch(e){ret.error=e.errno}return ret},createPath(parent,path,canRead,canWrite){parent=typeof parent=="string"?parent:FS.getPath(parent);var parts=path.split("/").reverse();while(parts.length){var part=parts.pop();if(!part)continue;var current=PATH.join2(parent,part);try{FS.mkdir(current)}catch(e){if(e.errno!=20)throw e}parent=current}return current},createFile(parent,name,properties,canRead,canWrite){var path=PATH.join2(typeof parent=="string"?parent:FS.getPath(parent),name);var mode=FS_getMode(canRead,canWrite);return FS.create(path,mode)},createDataFile(parent,name,data,canRead,canWrite,canOwn){var path=name;if(parent){parent=typeof parent=="string"?parent:FS.getPath(parent);path=name?PATH.join2(parent,name):parent}var mode=FS_getMode(canRead,canWrite);var node=FS.create(path,mode);if(data){if(typeof data=="string"){var arr=new Array(data.length);for(var i=0,len=data.length;ithis.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]}setDataGetter(getter){this.getter=getter}cacheLength(){var xhr=new XMLHttpRequest;xhr.open("HEAD",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);var datalength=Number(xhr.getResponseHeader("Content-length"));var header;var hasByteServing=(header=xhr.getResponseHeader("Accept-Ranges"))&&header==="bytes";var usesGzip=(header=xhr.getResponseHeader("Content-Encoding"))&&header==="gzip";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)throw new Error("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)throw new Error("only "+datalength+" bytes available! programmer error!");var xhr=new XMLHttpRequest;xhr.open("GET",url,false);if(datalength!==chunkSize)xhr.setRequestHeader("Range","bytes="+from+"-"+to);xhr.responseType="arraybuffer";if(xhr.overrideMimeType){xhr.overrideMimeType("text/plain; charset=x-user-defined")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||"",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]=="undefined"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]=="undefined")throw new Error("doXHR failed!");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out("LazyFiles on gzip forces download of the whole file when length is accessed")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true}get length(){if(!this.lengthKnown){this.cacheLength()}return this._length}get chunkSize(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}if(typeof XMLHttpRequest!="undefined"){if(!ENVIRONMENT_IS_WORKER)throw"Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc";var lazyArray=new LazyUint8Array;var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};var keys=Object.keys(node.stream_ops);keys.forEach(key=>{var fn=node.stream_ops[key];stream_ops[key]=(...args)=>{FS.forceLoadFile(node);return fn(...args)}});function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);assert(size>=0);if(contents.slice){for(var i=0;i{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr,allocated:true}};node.stream_ops=stream_ops;return node},absolutePath(){abort("FS.absolutePath has been removed; use PATH_FS.resolve instead")},createFolder(){abort("FS.createFolder has been removed; use FS.mkdir instead")},createLink(){abort("FS.createLink has been removed; use FS.symlink instead")},joinPath(){abort("FS.joinPath has been removed; use PATH.join instead")},mmapAlloc(){abort("FS.mmapAlloc has been replaced by the top level function mmapAlloc")},standardizePath(){abort("FS.standardizePath has been removed; use PATH.normalize instead")}};var SYSCALLS={DEFAULT_POLLMASK:5,calculateAt(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return dir+"/"+path},writeStat(buf,stat){HEAP32[buf>>2]=stat.dev;HEAP32[buf+4>>2]=stat.mode;HEAPU32[buf+8>>2]=stat.nlink;HEAP32[buf+12>>2]=stat.uid;HEAP32[buf+16>>2]=stat.gid;HEAP32[buf+20>>2]=stat.rdev;HEAP64[buf+24>>3]=BigInt(stat.size);HEAP32[buf+32>>2]=4096;HEAP32[buf+36>>2]=stat.blocks;var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();HEAP64[buf+40>>3]=BigInt(Math.floor(atime/1e3));HEAPU32[buf+48>>2]=atime%1e3*1e3*1e3;HEAP64[buf+56>>3]=BigInt(Math.floor(mtime/1e3));HEAPU32[buf+64>>2]=mtime%1e3*1e3*1e3;HEAP64[buf+72>>3]=BigInt(Math.floor(ctime/1e3));HEAPU32[buf+80>>2]=ctime%1e3*1e3*1e3;HEAP64[buf+88>>3]=BigInt(stat.ino);return 0},writeStatFs(buf,stats){HEAP32[buf+4>>2]=stats.bsize;HEAP32[buf+40>>2]=stats.bsize;HEAP32[buf+8>>2]=stats.blocks;HEAP32[buf+12>>2]=stats.bfree;HEAP32[buf+16>>2]=stats.bavail;HEAP32[buf+20>>2]=stats.files;HEAP32[buf+24>>2]=stats.ffree;HEAP32[buf+28>>2]=stats.fsid;HEAP32[buf+44>>2]=stats.flags;HEAP32[buf+36>>2]=stats.namelen},doMsync(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},getStreamFromFD(fd){var stream=FS.getStreamChecked(fd);return stream},varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};function ___syscall_chdir(path){try{path=SYSCALLS.getStr(path);FS.chdir(path);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_chmod(path,mode){try{path=SYSCALLS.getStr(path);FS.chmod(path,mode);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_faccessat(dirfd,path,amode,flags){try{path=SYSCALLS.getStr(path);assert(!flags||flags==512);path=SYSCALLS.calculateAt(dirfd,path);if(amode&~7){return-28}var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;if(!node){return-44}var perms="";if(amode&4)perms+="r";if(amode&2)perms+="w";if(amode&1)perms+="x";if(perms&&FS.nodePermissions(node,perms)){return-2}return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_fchmod(fd,mode){try{FS.fchmod(fd,mode);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var syscallGetVarargI=()=>{assert(SYSCALLS.varargs!=undefined);var ret=HEAP32[+SYSCALLS.varargs>>2];SYSCALLS.varargs+=4;return ret};var syscallGetVarargP=syscallGetVarargI;function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=syscallGetVarargI();if(arg<0){return-28}while(FS.streams[arg]){arg++}var newStream;newStream=FS.dupStream(stream,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=syscallGetVarargI();stream.flags|=arg;return 0}case 12:{var arg=syscallGetVarargP();var offset=0;HEAP16[arg+offset>>1]=2;return 0}case 13:case 14:return 0}return-28}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_fstat64(fd,buf){try{return SYSCALLS.writeStat(buf,FS.fstat(fd))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var INT53_MAX=9007199254740992;var INT53_MIN=-9007199254740992;var bigintToI53Checked=num=>numINT53_MAX?NaN:Number(num);function ___syscall_ftruncate64(fd,length){length=bigintToI53Checked(length);try{if(isNaN(length))return-61;FS.ftruncate(fd,length);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var stringToUTF8=(str,outPtr,maxBytesToWrite)=>{assert(typeof maxBytesToWrite=="number","stringToUTF8(str, outPtr, maxBytesToWrite) is missing the third parameter that specifies the length of the output buffer!");return stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite)};function ___syscall_getcwd(buf,size){try{if(size===0)return-28;var cwd=FS.cwd();var cwdLengthInBytes=lengthBytesUTF8(cwd)+1;if(size>3]=BigInt(id);HEAP64[dirp+pos+8>>3]=BigInt((idx+1)*struct_size);HEAP16[dirp+pos+16>>1]=280;HEAP8[dirp+pos+18]=type;stringToUTF8(name,dirp+pos+19,256);pos+=struct_size}FS.llseek(stream,idx*struct_size,0);return pos}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_ioctl(fd,op,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(op){case 21509:{if(!stream.tty)return-59;return 0}case 21505:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcgets){var termios=stream.tty.ops.ioctl_tcgets(stream);var argp=syscallGetVarargP();HEAP32[argp>>2]=termios.c_iflag||0;HEAP32[argp+4>>2]=termios.c_oflag||0;HEAP32[argp+8>>2]=termios.c_cflag||0;HEAP32[argp+12>>2]=termios.c_lflag||0;for(var i=0;i<32;i++){HEAP8[argp+i+17]=termios.c_cc[i]||0}return 0}return 0}case 21510:case 21511:case 21512:{if(!stream.tty)return-59;return 0}case 21506:case 21507:case 21508:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcsets){var argp=syscallGetVarargP();var c_iflag=HEAP32[argp>>2];var c_oflag=HEAP32[argp+4>>2];var c_cflag=HEAP32[argp+8>>2];var c_lflag=HEAP32[argp+12>>2];var c_cc=[];for(var i=0;i<32;i++){c_cc.push(HEAP8[argp+i+17])}return stream.tty.ops.ioctl_tcsets(stream.tty,op,{c_iflag,c_oflag,c_cflag,c_lflag,c_cc})}return 0}case 21519:{if(!stream.tty)return-59;var argp=syscallGetVarargP();HEAP32[argp>>2]=0;return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21531:{var argp=syscallGetVarargP();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tiocgwinsz){var winsize=stream.tty.ops.ioctl_tiocgwinsz(stream.tty);var argp=syscallGetVarargP();HEAP16[argp>>1]=winsize[0];HEAP16[argp+2>>1]=winsize[1]}return 0}case 21524:{if(!stream.tty)return-59;return 0}case 21515:{if(!stream.tty)return-59;return 0}default:return-28}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_lstat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.lstat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_mkdirat(dirfd,path,mode){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);FS.mkdir(path,mode,0);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_mknodat(dirfd,path,mode,dev){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);switch(mode&61440){case 32768:case 8192:case 24576:case 4096:case 49152:break;default:return-28}FS.mknod(path,mode,dev);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_newfstatat(dirfd,path,buf,flags){try{path=SYSCALLS.getStr(path);var nofollow=flags&256;var allowEmpty=flags&4096;flags=flags&~6400;assert(!flags,`unknown flags in __syscall_newfstatat: ${flags}`);path=SYSCALLS.calculateAt(dirfd,path,allowEmpty);return SYSCALLS.writeStat(buf,nofollow?FS.lstat(path):FS.stat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs;try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);var mode=varargs?syscallGetVarargI():0;return FS.open(path,flags,mode).fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_readlinkat(dirfd,path,buf,bufsize){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);if(bufsize<=0)return-28;var ret=FS.readlink(path);var len=Math.min(bufsize,lengthBytesUTF8(ret));var endChar=HEAP8[buf+len];stringToUTF8(ret,buf,bufsize+1);HEAP8[buf+len]=endChar;return len}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_renameat(olddirfd,oldpath,newdirfd,newpath){try{oldpath=SYSCALLS.getStr(oldpath);newpath=SYSCALLS.getStr(newpath);oldpath=SYSCALLS.calculateAt(olddirfd,oldpath);newpath=SYSCALLS.calculateAt(newdirfd,newpath);FS.rename(oldpath,newpath);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_rmdir(path){try{path=SYSCALLS.getStr(path);FS.rmdir(path);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_stat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.stat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_statfs64(path,size,buf){try{assert(size===64);SYSCALLS.writeStatFs(buf,FS.statfs(SYSCALLS.getStr(path)));return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_symlinkat(target,dirfd,linkpath){try{target=SYSCALLS.getStr(target);linkpath=SYSCALLS.getStr(linkpath);linkpath=SYSCALLS.calculateAt(dirfd,linkpath);FS.symlink(target,linkpath);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_unlinkat(dirfd,path,flags){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);if(!flags){FS.unlink(path)}else if(flags===512){FS.rmdir(path)}else{return-28}return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __abort_js=()=>abort("native code called abort()");var runtimeKeepaliveCounter=0;var __emscripten_runtime_keepalive_clear=()=>{noExitRuntime=false;runtimeKeepaliveCounter=0};function __gmtime_js(time,tmPtr){time=bigintToI53Checked(time);var date=new Date(time*1e3);HEAP32[tmPtr>>2]=date.getUTCSeconds();HEAP32[tmPtr+4>>2]=date.getUTCMinutes();HEAP32[tmPtr+8>>2]=date.getUTCHours();HEAP32[tmPtr+12>>2]=date.getUTCDate();HEAP32[tmPtr+16>>2]=date.getUTCMonth();HEAP32[tmPtr+20>>2]=date.getUTCFullYear()-1900;HEAP32[tmPtr+24>>2]=date.getUTCDay();var start=Date.UTC(date.getUTCFullYear(),0,1,0,0,0,0);var yday=(date.getTime()-start)/(1e3*60*60*24)|0;HEAP32[tmPtr+28>>2]=yday}var isLeapYear=year=>year%4===0&&(year%100!==0||year%400===0);var MONTH_DAYS_LEAP_CUMULATIVE=[0,31,60,91,121,152,182,213,244,274,305,335];var MONTH_DAYS_REGULAR_CUMULATIVE=[0,31,59,90,120,151,181,212,243,273,304,334];var ydayFromDate=date=>{var leap=isLeapYear(date.getFullYear());var monthDaysCumulative=leap?MONTH_DAYS_LEAP_CUMULATIVE:MONTH_DAYS_REGULAR_CUMULATIVE;var yday=monthDaysCumulative[date.getMonth()]+date.getDate()-1;return yday};function __localtime_js(time,tmPtr){time=bigintToI53Checked(time);var date=new Date(time*1e3);HEAP32[tmPtr>>2]=date.getSeconds();HEAP32[tmPtr+4>>2]=date.getMinutes();HEAP32[tmPtr+8>>2]=date.getHours();HEAP32[tmPtr+12>>2]=date.getDate();HEAP32[tmPtr+16>>2]=date.getMonth();HEAP32[tmPtr+20>>2]=date.getFullYear()-1900;HEAP32[tmPtr+24>>2]=date.getDay();var yday=ydayFromDate(date)|0;HEAP32[tmPtr+28>>2]=yday;HEAP32[tmPtr+36>>2]=-(date.getTimezoneOffset()*60);var start=new Date(date.getFullYear(),0,1);var summerOffset=new Date(date.getFullYear(),6,1).getTimezoneOffset();var winterOffset=start.getTimezoneOffset();var dst=(summerOffset!=winterOffset&&date.getTimezoneOffset()==Math.min(winterOffset,summerOffset))|0;HEAP32[tmPtr+32>>2]=dst}var timers={};var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}checkStackCookie();if(e instanceof WebAssembly.RuntimeError){if(_emscripten_stack_get_current()<=0){err("Stack overflow detected. You can try increasing -sSTACK_SIZE (currently set to 5242880)")}}quit_(1,e)};var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;if(!keepRuntimeAlive()){exitRuntime()}if(keepRuntimeAlive()&&!implicit){var msg=`program exited (with status: ${status}), but keepRuntimeAlive() is set (counter=${runtimeKeepaliveCounter}) due to an async operation, so halting execution but not exiting the runtime or preventing further async execution (you can use emscripten_force_exit, if you want to force a true shutdown)`;readyPromiseReject?.(msg);err(msg)}_proc_exit(status)};var _exit=exitJS;var maybeExit=()=>{if(runtimeExited){return}if(!keepRuntimeAlive()){try{_exit(EXITSTATUS)}catch(e){handleException(e)}}};var callUserCallback=func=>{if(runtimeExited||ABORT){err("user callback triggered after runtime exited or application aborted. Ignoring.");return}try{func();maybeExit()}catch(e){handleException(e)}};var _emscripten_get_now=()=>performance.now();var __setitimer_js=(which,timeout_ms)=>{if(timers[which]){clearTimeout(timers[which].id);delete timers[which]}if(!timeout_ms)return 0;var id=setTimeout(()=>{assert(which in timers);delete timers[which];callUserCallback(()=>__emscripten_timeout(which,_emscripten_get_now()))},timeout_ms);timers[which]={id,timeout_ms};return 0};var __tzset_js=(timezone,daylight,std_name,dst_name)=>{var currentYear=(new Date).getFullYear();var winter=new Date(currentYear,0,1);var summer=new Date(currentYear,6,1);var winterOffset=winter.getTimezoneOffset();var summerOffset=summer.getTimezoneOffset();var stdTimezoneOffset=Math.max(winterOffset,summerOffset);HEAPU32[timezone>>2]=stdTimezoneOffset*60;HEAP32[daylight>>2]=Number(winterOffset!=summerOffset);var extractZone=timezoneOffset=>{var sign=timezoneOffset>=0?"-":"+";var absOffset=Math.abs(timezoneOffset);var hours=String(Math.floor(absOffset/60)).padStart(2,"0");var minutes=String(absOffset%60).padStart(2,"0");return`UTC${sign}${hours}${minutes}`};var winterName=extractZone(winterOffset);var summerName=extractZone(summerOffset);assert(winterName);assert(summerName);assert(lengthBytesUTF8(winterName)<=16,`timezone name truncated to fit in TZNAME_MAX (${winterName})`);assert(lengthBytesUTF8(summerName)<=16,`timezone name truncated to fit in TZNAME_MAX (${summerName})`);if(summerOffsetDate.now();var nowIsMonotonic=1;var checkWasiClock=clock_id=>clock_id>=0&&clock_id<=3;function _clock_time_get(clk_id,ignored_precision,ptime){ignored_precision=bigintToI53Checked(ignored_precision);if(!checkWasiClock(clk_id)){return 28}var now;if(clk_id===0){now=_emscripten_date_now()}else if(nowIsMonotonic){now=_emscripten_get_now()}else{return 52}var nsec=Math.round(now*1e3*1e3);HEAP64[ptime>>3]=BigInt(nsec);return 0}var runtimeKeepalivePush=()=>{runtimeKeepaliveCounter+=1};var _emscripten_set_main_loop_timing=(mode,value)=>{MainLoop.timingMode=mode;MainLoop.timingValue=value;if(!MainLoop.func){err("emscripten_set_main_loop_timing: Cannot set timing mode for main loop since a main loop does not exist! Call emscripten_set_main_loop first to set one up.");return 1}if(!MainLoop.running){runtimeKeepalivePush();MainLoop.running=true}if(mode==0){MainLoop.scheduler=function MainLoop_scheduler_setTimeout(){var timeUntilNextTick=Math.max(0,MainLoop.tickStartTime+value-_emscripten_get_now())|0;setTimeout(MainLoop.runner,timeUntilNextTick)};MainLoop.method="timeout"}else if(mode==1){MainLoop.scheduler=function MainLoop_scheduler_rAF(){MainLoop.requestAnimationFrame(MainLoop.runner)};MainLoop.method="rAF"}else if(mode==2){if(typeof MainLoop.setImmediate=="undefined"){if(typeof setImmediate=="undefined"){var setImmediates=[];var emscriptenMainLoopMessageId="setimmediate";var MainLoop_setImmediate_messageHandler=event=>{if(event.data===emscriptenMainLoopMessageId||event.data.target===emscriptenMainLoopMessageId){event.stopPropagation();setImmediates.shift()()}};addEventListener("message",MainLoop_setImmediate_messageHandler,true);MainLoop.setImmediate=func=>{setImmediates.push(func);if(ENVIRONMENT_IS_WORKER){Module["setImmediates"]??=[];Module["setImmediates"].push(func);postMessage({target:emscriptenMainLoopMessageId})}else postMessage(emscriptenMainLoopMessageId,"*")}}else{MainLoop.setImmediate=setImmediate}}MainLoop.scheduler=function MainLoop_scheduler_setImmediate(){MainLoop.setImmediate(MainLoop.runner)};MainLoop.method="immediate"}return 0};var runtimeKeepalivePop=()=>{assert(runtimeKeepaliveCounter>0);runtimeKeepaliveCounter-=1};var setMainLoop=(iterFunc,fps,simulateInfiniteLoop,arg,noSetTiming)=>{assert(!MainLoop.func,"emscripten_set_main_loop: there can only be one main loop function at once: call emscripten_cancel_main_loop to cancel the previous one before setting a new one with different parameters.");MainLoop.func=iterFunc;MainLoop.arg=arg;var thisMainLoopId=MainLoop.currentlyRunningMainloop;function checkIsRunning(){if(thisMainLoopId0){var start=Date.now();var blocker=MainLoop.queue.shift();blocker.func(blocker.arg);if(MainLoop.remainingBlockers){var remaining=MainLoop.remainingBlockers;var next=remaining%1==0?remaining-1:Math.floor(remaining);if(blocker.counted){MainLoop.remainingBlockers=next}else{next=next+.5;MainLoop.remainingBlockers=(8*remaining+next)/9}}MainLoop.updateStatus();if(!checkIsRunning())return;setTimeout(MainLoop.runner,0);return}if(!checkIsRunning())return;MainLoop.currentFrameNumber=MainLoop.currentFrameNumber+1|0;if(MainLoop.timingMode==1&&MainLoop.timingValue>1&&MainLoop.currentFrameNumber%MainLoop.timingValue!=0){MainLoop.scheduler();return}else if(MainLoop.timingMode==0){MainLoop.tickStartTime=_emscripten_get_now()}if(MainLoop.method==="timeout"&&Module["ctx"]){warnOnce("Looks like you are rendering without using requestAnimationFrame for the main loop. You should use 0 for the frame rate in emscripten_set_main_loop in order to use requestAnimationFrame, as that can greatly improve your frame rates!");MainLoop.method=""}MainLoop.runIter(iterFunc);if(!checkIsRunning())return;MainLoop.scheduler()};if(!noSetTiming){if(fps>0){_emscripten_set_main_loop_timing(0,1e3/fps)}else{_emscripten_set_main_loop_timing(1,1)}MainLoop.scheduler()}if(simulateInfiniteLoop){throw"unwind"}};var MainLoop={running:false,scheduler:null,method:"",currentlyRunningMainloop:0,func:null,arg:0,timingMode:0,timingValue:0,currentFrameNumber:0,queue:[],preMainLoop:[],postMainLoop:[],pause(){MainLoop.scheduler=null;MainLoop.currentlyRunningMainloop++},resume(){MainLoop.currentlyRunningMainloop++;var timingMode=MainLoop.timingMode;var timingValue=MainLoop.timingValue;var func=MainLoop.func;MainLoop.func=null;setMainLoop(func,0,false,MainLoop.arg,true);_emscripten_set_main_loop_timing(timingMode,timingValue);MainLoop.scheduler()},updateStatus(){if(Module["setStatus"]){var message=Module["statusMessage"]||"Please wait...";var remaining=MainLoop.remainingBlockers??0;var expected=MainLoop.expectedBlockers??0;if(remaining){if(remaining=MainLoop.nextRAF){MainLoop.nextRAF+=1e3/60}}var delay=Math.max(MainLoop.nextRAF-now,0);setTimeout(func,delay)},requestAnimationFrame(func){if(typeof requestAnimationFrame=="function"){requestAnimationFrame(func);return}var RAF=MainLoop.fakeRequestAnimationFrame;RAF(func)}};var _emscripten_cancel_main_loop=()=>{MainLoop.pause();MainLoop.func=null};var _emscripten_err=str=>err(UTF8ToString(str));var _emscripten_force_exit=status=>{__emscripten_runtime_keepalive_clear();_exit(status)};var getHeapMax=()=>2147483648;var _emscripten_get_heap_max=()=>getHeapMax();var alignMemory=(size,alignment)=>{assert(alignment,"alignment argument is required");return Math.ceil(size/alignment)*alignment};var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){err(`growMemory: Attempted to grow heap from ${b.byteLength} bytes to ${size} bytes, but got error: ${e}`)}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;assert(requestedSize>oldSize);var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){err(`Cannot enlarge memory, requested ${requestedSize} bytes, but the limit is ${maxHeapSize} bytes!`);return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}err(`Failed to grow the heap from ${oldSize} bytes to ${newSize} bytes, not enough memory!`);return false};var maybeCStringToJsString=cString=>cString>2?UTF8ToString(cString):cString;var specialHTMLTargets=[0,typeof document!="undefined"?document:0,typeof window!="undefined"?window:0];var findEventTarget=target=>{target=maybeCStringToJsString(target);var domElement=specialHTMLTargets[target]||(typeof document!="undefined"?document.querySelector(target):null);return domElement};var findCanvasEventTarget=findEventTarget;var _emscripten_set_canvas_element_size=(target,width,height)=>{var canvas=findCanvasEventTarget(target);if(!canvas)return-4;canvas.width=width;canvas.height=height;if(canvas.GLctxObject)GL.resizeOffscreenFramebuffer(canvas.GLctxObject);return 0};var _emscripten_set_main_loop=(func,fps,simulateInfiniteLoop)=>{var iterFunc=getWasmTableEntry(func);setMainLoop(iterFunc,fps,simulateInfiniteLoop)};var GLctx;var webgl_enable_ANGLE_instanced_arrays=ctx=>{var ext=ctx.getExtension("ANGLE_instanced_arrays");if(ext){ctx["vertexAttribDivisor"]=(index,divisor)=>ext["vertexAttribDivisorANGLE"](index,divisor);ctx["drawArraysInstanced"]=(mode,first,count,primcount)=>ext["drawArraysInstancedANGLE"](mode,first,count,primcount);ctx["drawElementsInstanced"]=(mode,count,type,indices,primcount)=>ext["drawElementsInstancedANGLE"](mode,count,type,indices,primcount);return 1}};var webgl_enable_OES_vertex_array_object=ctx=>{var ext=ctx.getExtension("OES_vertex_array_object");if(ext){ctx["createVertexArray"]=()=>ext["createVertexArrayOES"]();ctx["deleteVertexArray"]=vao=>ext["deleteVertexArrayOES"](vao);ctx["bindVertexArray"]=vao=>ext["bindVertexArrayOES"](vao);ctx["isVertexArray"]=vao=>ext["isVertexArrayOES"](vao);return 1}};var webgl_enable_WEBGL_draw_buffers=ctx=>{var ext=ctx.getExtension("WEBGL_draw_buffers");if(ext){ctx["drawBuffers"]=(n,bufs)=>ext["drawBuffersWEBGL"](n,bufs);return 1}};var webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.dibvbi=ctx.getExtension("WEBGL_draw_instanced_base_vertex_base_instance"));var webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.mdibvbi=ctx.getExtension("WEBGL_multi_draw_instanced_base_vertex_base_instance"));var webgl_enable_EXT_polygon_offset_clamp=ctx=>!!(ctx.extPolygonOffsetClamp=ctx.getExtension("EXT_polygon_offset_clamp"));var webgl_enable_EXT_clip_control=ctx=>!!(ctx.extClipControl=ctx.getExtension("EXT_clip_control"));var webgl_enable_WEBGL_polygon_mode=ctx=>!!(ctx.webglPolygonMode=ctx.getExtension("WEBGL_polygon_mode"));var webgl_enable_WEBGL_multi_draw=ctx=>!!(ctx.multiDrawWebgl=ctx.getExtension("WEBGL_multi_draw"));var getEmscriptenSupportedExtensions=ctx=>{var supportedExtensions=["ANGLE_instanced_arrays","EXT_blend_minmax","EXT_disjoint_timer_query","EXT_frag_depth","EXT_shader_texture_lod","EXT_sRGB","OES_element_index_uint","OES_fbo_render_mipmap","OES_standard_derivatives","OES_texture_float","OES_texture_half_float","OES_texture_half_float_linear","OES_vertex_array_object","WEBGL_color_buffer_float","WEBGL_depth_texture","WEBGL_draw_buffers","EXT_color_buffer_float","EXT_conservative_depth","EXT_disjoint_timer_query_webgl2","EXT_texture_norm16","NV_shader_noperspective_interpolation","WEBGL_clip_cull_distance","EXT_clip_control","EXT_color_buffer_half_float","EXT_depth_clamp","EXT_float_blend","EXT_polygon_offset_clamp","EXT_texture_compression_bptc","EXT_texture_compression_rgtc","EXT_texture_filter_anisotropic","KHR_parallel_shader_compile","OES_texture_float_linear","WEBGL_blend_func_extended","WEBGL_compressed_texture_astc","WEBGL_compressed_texture_etc","WEBGL_compressed_texture_etc1","WEBGL_compressed_texture_s3tc","WEBGL_compressed_texture_s3tc_srgb","WEBGL_debug_renderer_info","WEBGL_debug_shaders","WEBGL_lose_context","WEBGL_multi_draw","WEBGL_polygon_mode"];return(ctx.getSupportedExtensions()||[]).filter(ext=>supportedExtensions.includes(ext))};var GL={counter:1,buffers:[],programs:[],framebuffers:[],renderbuffers:[],textures:[],shaders:[],vaos:[],contexts:[],offscreenCanvases:{},queries:[],samplers:[],transformFeedbacks:[],syncs:[],stringCache:{},stringiCache:{},unpackAlignment:4,unpackRowLength:0,recordError:errorCode=>{if(!GL.lastError){GL.lastError=errorCode}},getNewId:table=>{var ret=GL.counter++;for(var i=table.length;i{for(var i=0;i>2]=id}},getSource:(shader,count,string,length)=>{var source="";for(var i=0;i>2]:undefined;source+=UTF8ToString(HEAPU32[string+i*4>>2],len)}return source},createContext:(canvas,webGLContextAttributes)=>{if(webGLContextAttributes.renderViaOffscreenBackBuffer)webGLContextAttributes["preserveDrawingBuffer"]=true;var ctx=webGLContextAttributes.majorVersion>1?canvas.getContext("webgl2",webGLContextAttributes):canvas.getContext("webgl",webGLContextAttributes);if(!ctx)return 0;var handle=GL.registerContext(ctx,webGLContextAttributes);return handle},enableOffscreenFramebufferAttributes:webGLContextAttributes=>{webGLContextAttributes.renderViaOffscreenBackBuffer=true;webGLContextAttributes.preserveDrawingBuffer=true},createOffscreenFramebuffer:context=>{var gl=context.GLctx;var fbo=gl.createFramebuffer();gl.bindFramebuffer(36160,fbo);context.defaultFbo=fbo;context.defaultFboForbidBlitFramebuffer=false;if(gl.getContextAttributes().antialias){context.defaultFboForbidBlitFramebuffer=true}context.defaultColorTarget=gl.createTexture();context.defaultDepthTarget=gl.createRenderbuffer();GL.resizeOffscreenFramebuffer(context);gl.bindTexture(3553,context.defaultColorTarget);gl.texParameteri(3553,10241,9728);gl.texParameteri(3553,10240,9728);gl.texParameteri(3553,10242,33071);gl.texParameteri(3553,10243,33071);gl.texImage2D(3553,0,6408,gl.canvas.width,gl.canvas.height,0,6408,5121,null);gl.framebufferTexture2D(36160,36064,3553,context.defaultColorTarget,0);gl.bindTexture(3553,null);var depthTarget=gl.createRenderbuffer();gl.bindRenderbuffer(36161,context.defaultDepthTarget);gl.renderbufferStorage(36161,33189,gl.canvas.width,gl.canvas.height);gl.framebufferRenderbuffer(36160,36096,36161,context.defaultDepthTarget);gl.bindRenderbuffer(36161,null);var vertices=[-1,-1,-1,1,1,-1,1,1];var vb=gl.createBuffer();gl.bindBuffer(34962,vb);gl.bufferData(34962,new Float32Array(vertices),35044);gl.bindBuffer(34962,null);context.blitVB=vb;var vsCode="attribute vec2 pos;"+"varying lowp vec2 tex;"+"void main() { tex = pos * 0.5 + vec2(0.5,0.5); gl_Position = vec4(pos, 0.0, 1.0); }";var vs=gl.createShader(35633);gl.shaderSource(vs,vsCode);gl.compileShader(vs);var fsCode="varying lowp vec2 tex;"+"uniform sampler2D sampler;"+"void main() { gl_FragColor = texture2D(sampler, tex); }";var fs=gl.createShader(35632);gl.shaderSource(fs,fsCode);gl.compileShader(fs);var blitProgram=gl.createProgram();gl.attachShader(blitProgram,vs);gl.attachShader(blitProgram,fs);gl.linkProgram(blitProgram);context.blitProgram=blitProgram;context.blitPosLoc=gl.getAttribLocation(blitProgram,"pos");gl.useProgram(blitProgram);gl.uniform1i(gl.getUniformLocation(blitProgram,"sampler"),0);gl.useProgram(null);if(gl.createVertexArray){context.defaultVao=gl.createVertexArray();gl.bindVertexArray(context.defaultVao);gl.enableVertexAttribArray(context.blitPosLoc);gl.bindVertexArray(null)}},resizeOffscreenFramebuffer:context=>{var gl=context.GLctx;if(context.defaultColorTarget){var prevTextureBinding=gl.getParameter(32873);gl.bindTexture(3553,context.defaultColorTarget);gl.texImage2D(3553,0,6408,gl.drawingBufferWidth,gl.drawingBufferHeight,0,6408,5121,null);gl.bindTexture(3553,prevTextureBinding)}if(context.defaultDepthTarget){var prevRenderBufferBinding=gl.getParameter(36007);gl.bindRenderbuffer(36161,context.defaultDepthTarget);gl.renderbufferStorage(36161,33189,gl.drawingBufferWidth,gl.drawingBufferHeight);gl.bindRenderbuffer(36161,prevRenderBufferBinding)}},blitOffscreenFramebuffer:context=>{var gl=context.GLctx;var prevScissorTest=gl.getParameter(3089);if(prevScissorTest)gl.disable(3089);var prevFbo=gl.getParameter(36006);if(gl.blitFramebuffer&&!context.defaultFboForbidBlitFramebuffer){gl.bindFramebuffer(36008,context.defaultFbo);gl.bindFramebuffer(36009,null);gl.blitFramebuffer(0,0,gl.canvas.width,gl.canvas.height,0,0,gl.canvas.width,gl.canvas.height,16384,9728)}else{gl.bindFramebuffer(36160,null);var prevProgram=gl.getParameter(35725);gl.useProgram(context.blitProgram);if(!gl.isProgram(prevProgram))prevProgram=null;var prevVB=gl.getParameter(34964);gl.bindBuffer(34962,context.blitVB);var prevActiveTexture=gl.getParameter(34016);gl.activeTexture(33984);var prevTextureBinding=gl.getParameter(32873);gl.bindTexture(3553,context.defaultColorTarget);var prevBlend=gl.getParameter(3042);if(prevBlend)gl.disable(3042);var prevCullFace=gl.getParameter(2884);if(prevCullFace)gl.disable(2884);var prevDepthTest=gl.getParameter(2929);if(prevDepthTest)gl.disable(2929);var prevStencilTest=gl.getParameter(2960);if(prevStencilTest)gl.disable(2960);function draw(){gl.vertexAttribPointer(context.blitPosLoc,2,5126,false,0,0);gl.drawArrays(5,0,4)}if(context.defaultVao){var prevVAO=gl.getParameter(34229);gl.bindVertexArray(context.defaultVao);draw();gl.bindVertexArray(prevVAO)}else{var prevVertexAttribPointer={buffer:gl.getVertexAttrib(context.blitPosLoc,34975),size:gl.getVertexAttrib(context.blitPosLoc,34339),stride:gl.getVertexAttrib(context.blitPosLoc,34340),type:gl.getVertexAttrib(context.blitPosLoc,34341),normalized:gl.getVertexAttrib(context.blitPosLoc,34922),pointer:gl.getVertexAttribOffset(context.blitPosLoc,34373)};var maxVertexAttribs=gl.getParameter(34921);var prevVertexAttribEnables=[];for(var i=0;i{var handle=GL.getNewId(GL.contexts);var context={handle,attributes:webGLContextAttributes,version:webGLContextAttributes.majorVersion,GLctx:ctx};if(ctx.canvas)ctx.canvas.GLctxObject=context;GL.contexts[handle]=context;if(typeof webGLContextAttributes.enableExtensionsByDefault=="undefined"||webGLContextAttributes.enableExtensionsByDefault){GL.initExtensions(context)}if(webGLContextAttributes.renderViaOffscreenBackBuffer)GL.createOffscreenFramebuffer(context);return handle},makeContextCurrent:contextHandle=>{GL.currentContext=GL.contexts[contextHandle];Module["ctx"]=GLctx=GL.currentContext?.GLctx;return!(contextHandle&&!GLctx)},getContext:contextHandle=>GL.contexts[contextHandle],deleteContext:contextHandle=>{if(GL.currentContext===GL.contexts[contextHandle]){GL.currentContext=null}if(typeof JSEvents=="object"){JSEvents.removeAllHandlersOnTarget(GL.contexts[contextHandle].GLctx.canvas)}if(GL.contexts[contextHandle]?.GLctx.canvas){GL.contexts[contextHandle].GLctx.canvas.GLctxObject=undefined}GL.contexts[contextHandle]=null},initExtensions:context=>{context||=GL.currentContext;if(context.initExtensionsDone)return;context.initExtensionsDone=true;var GLctx=context.GLctx;webgl_enable_WEBGL_multi_draw(GLctx);webgl_enable_EXT_polygon_offset_clamp(GLctx);webgl_enable_EXT_clip_control(GLctx);webgl_enable_WEBGL_polygon_mode(GLctx);webgl_enable_ANGLE_instanced_arrays(GLctx);webgl_enable_OES_vertex_array_object(GLctx);webgl_enable_WEBGL_draw_buffers(GLctx);webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance(GLctx);webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance(GLctx);if(context.version>=2){GLctx.disjointTimerQueryExt=GLctx.getExtension("EXT_disjoint_timer_query_webgl2")}if(context.version<2||!GLctx.disjointTimerQueryExt){GLctx.disjointTimerQueryExt=GLctx.getExtension("EXT_disjoint_timer_query")}getEmscriptenSupportedExtensions(GLctx).forEach(ext=>{if(!ext.includes("lose_context")&&!ext.includes("debug")){GLctx.getExtension(ext)}})}};var _emscripten_webgl_do_commit_frame=()=>{if(!GL.currentContext||!GL.currentContext.GLctx){return-3}if(GL.currentContext.defaultFbo){GL.blitOffscreenFramebuffer(GL.currentContext);return 0}if(!GL.currentContext.attributes.explicitSwapControl){return-3}return 0};var _emscripten_webgl_commit_frame=_emscripten_webgl_do_commit_frame;var webglPowerPreferences=["default","low-power","high-performance"];var _emscripten_webgl_do_create_context=(target,attributes)=>{assert(attributes);var attr32=attributes>>2;var powerPreference=HEAP32[attr32+(8>>2)];var contextAttributes={alpha:!!HEAP8[attributes+0],depth:!!HEAP8[attributes+1],stencil:!!HEAP8[attributes+2],antialias:!!HEAP8[attributes+3],premultipliedAlpha:!!HEAP8[attributes+4],preserveDrawingBuffer:!!HEAP8[attributes+5],powerPreference:webglPowerPreferences[powerPreference],failIfMajorPerformanceCaveat:!!HEAP8[attributes+12],majorVersion:HEAP32[attr32+(16>>2)],minorVersion:HEAP32[attr32+(20>>2)],enableExtensionsByDefault:HEAP8[attributes+24],explicitSwapControl:HEAP8[attributes+25],proxyContextToMainThread:HEAP32[attr32+(28>>2)],renderViaOffscreenBackBuffer:HEAP8[attributes+32]};if(contextAttributes.majorVersion!==1&&contextAttributes.majorVersion!==2){err(`Invalid WebGL version requested: ${contextAttributes.majorVersion}`)}var canvas=findCanvasEventTarget(target);if(!canvas){return 0}if(contextAttributes.explicitSwapControl&&!contextAttributes.renderViaOffscreenBackBuffer){contextAttributes.renderViaOffscreenBackBuffer=true}var contextHandle=GL.createContext(canvas,contextAttributes);return contextHandle};var _emscripten_webgl_create_context=_emscripten_webgl_do_create_context;var _emscripten_webgl_destroy_context=contextHandle=>{if(GL.currentContext==contextHandle)GL.currentContext=0;GL.deleteContext(contextHandle)};var _emscripten_webgl_enable_extension=(contextHandle,extension)=>{var context=GL.getContext(contextHandle);var extString=UTF8ToString(extension);if(extString.startsWith("GL_"))extString=extString.slice(3);if(extString=="ANGLE_instanced_arrays")webgl_enable_ANGLE_instanced_arrays(GLctx);if(extString=="OES_vertex_array_object")webgl_enable_OES_vertex_array_object(GLctx);if(extString=="WEBGL_draw_buffers")webgl_enable_WEBGL_draw_buffers(GLctx);if(extString=="WEBGL_draw_instanced_base_vertex_base_instance")webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance(GLctx);if(extString=="WEBGL_multi_draw_instanced_base_vertex_base_instance")webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance(GLctx);if(extString=="WEBGL_multi_draw")webgl_enable_WEBGL_multi_draw(GLctx);if(extString=="EXT_polygon_offset_clamp")webgl_enable_EXT_polygon_offset_clamp(GLctx);if(extString=="EXT_clip_control")webgl_enable_EXT_clip_control(GLctx);if(extString=="WEBGL_polygon_mode")webgl_enable_WEBGL_polygon_mode(GLctx);var ext=context.GLctx.getExtension(extString);return!!ext};var stringToNewUTF8=str=>{var size=lengthBytesUTF8(str)+1;var ret=_malloc(size);if(ret)stringToUTF8(str,ret,size);return ret};var _emscripten_webgl_get_supported_extensions=()=>stringToNewUTF8(GLctx.getSupportedExtensions().join(" "));var _emscripten_webgl_make_context_current=contextHandle=>{var success=GL.makeContextCurrent(contextHandle);return success?0:-5};var ENV={};var getExecutableName=()=>thisProgram||"./this.program";var getEnvStrings=()=>{if(!getEnvStrings.strings){var lang=(typeof navigator=="object"&&navigator.language||"C").replace("-","_")+".UTF-8";var env={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:lang,_:getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(`${x}=${env[x]}`)}getEnvStrings.strings=strings}return getEnvStrings.strings};var _environ_get=(__environ,environ_buf)=>{var bufSize=0;var envp=0;for(var string of getEnvStrings()){var ptr=environ_buf+bufSize;HEAPU32[__environ+envp>>2]=ptr;bufSize+=stringToUTF8(string,ptr,Infinity)+1;envp+=4}return 0};var _environ_sizes_get=(penviron_count,penviron_buf_size)=>{var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;var bufSize=0;for(var string of strings){bufSize+=lengthBytesUTF8(string)+1}HEAPU32[penviron_buf_size>>2]=bufSize;return 0};function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_fdstat_get(fd,pbuf){try{var rightsBase=0;var rightsInheriting=0;var flags=0;{var stream=SYSCALLS.getStreamFromFD(fd);var type=stream.tty?2:FS.isDir(stream.mode)?3:FS.isLink(stream.mode)?7:4}HEAP8[pbuf]=type;HEAP16[pbuf+2>>1]=flags;HEAP64[pbuf+8>>3]=BigInt(rightsBase);HEAP64[pbuf+16>>3]=BigInt(rightsInheriting);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doReadv=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_seek(fd,offset,whence,newOffset){offset=bigintToI53Checked(offset);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);HEAP64[newOffset>>3]=BigInt(stream.position);if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doWritev=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var _glActiveTexture=x0=>GLctx.activeTexture(x0);var _glAttachShader=(program,shader)=>{GLctx.attachShader(GL.programs[program],GL.shaders[shader])};var _glBeginTransformFeedback=x0=>GLctx.beginTransformFeedback(x0);var _glBindBuffer=(target,buffer)=>{if(target==35051){GLctx.currentPixelPackBufferBinding=buffer}else if(target==35052){GLctx.currentPixelUnpackBufferBinding=buffer}GLctx.bindBuffer(target,GL.buffers[buffer])};var _glBindBufferBase=(target,index,buffer)=>{GLctx.bindBufferBase(target,index,GL.buffers[buffer])};var _glBindBufferRange=(target,index,buffer,offset,ptrsize)=>{GLctx.bindBufferRange(target,index,GL.buffers[buffer],offset,ptrsize)};var _glBindFramebuffer=(target,framebuffer)=>{GLctx.bindFramebuffer(target,framebuffer?GL.framebuffers[framebuffer]:GL.currentContext.defaultFbo)};var _glBindRenderbuffer=(target,renderbuffer)=>{GLctx.bindRenderbuffer(target,GL.renderbuffers[renderbuffer])};var _glBindTexture=(target,texture)=>{GLctx.bindTexture(target,GL.textures[texture])};var _glBindVertexArray=vao=>{GLctx.bindVertexArray(GL.vaos[vao])};var _glBlendColor=(x0,x1,x2,x3)=>GLctx.blendColor(x0,x1,x2,x3);var _glBlendEquation=x0=>GLctx.blendEquation(x0);var _glBlendFunc=(x0,x1)=>GLctx.blendFunc(x0,x1);var _glBlendFuncSeparate=(x0,x1,x2,x3)=>GLctx.blendFuncSeparate(x0,x1,x2,x3);var _glBlitFramebuffer=(x0,x1,x2,x3,x4,x5,x6,x7,x8,x9)=>GLctx.blitFramebuffer(x0,x1,x2,x3,x4,x5,x6,x7,x8,x9);var _glBufferData=(target,size,data,usage)=>{if(GL.currentContext.version>=2){if(data&&size){GLctx.bufferData(target,HEAPU8,usage,data,size)}else{GLctx.bufferData(target,size,usage)}return}GLctx.bufferData(target,data?HEAPU8.subarray(data,data+size):size,usage)};var _glBufferSubData=(target,offset,size,data)=>{if(GL.currentContext.version>=2){size&&GLctx.bufferSubData(target,offset,HEAPU8,data,size);return}GLctx.bufferSubData(target,offset,HEAPU8.subarray(data,data+size))};var _glCheckFramebufferStatus=x0=>GLctx.checkFramebufferStatus(x0);var _glClear=x0=>GLctx.clear(x0);var _glClearBufferfv=(buffer,drawbuffer,value)=>{GLctx.clearBufferfv(buffer,drawbuffer,HEAPF32,value>>2)};var _glClearColor=(x0,x1,x2,x3)=>GLctx.clearColor(x0,x1,x2,x3);var _glClearDepthf=x0=>GLctx.clearDepth(x0);var _glClearStencil=x0=>GLctx.clearStencil(x0);var _glColorMask=(red,green,blue,alpha)=>{GLctx.colorMask(!!red,!!green,!!blue,!!alpha)};var _glCompileShader=shader=>{GLctx.compileShader(GL.shaders[shader])};var _glCompressedTexImage2D=(target,level,internalFormat,width,height,border,imageSize,data)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding||!imageSize){GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,imageSize,data);return}GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,HEAPU8,data,imageSize);return}GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,HEAPU8.subarray(data,data+imageSize))};var _glCompressedTexImage3D=(target,level,internalFormat,width,height,depth,border,imageSize,data)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.compressedTexImage3D(target,level,internalFormat,width,height,depth,border,imageSize,data)}else{GLctx.compressedTexImage3D(target,level,internalFormat,width,height,depth,border,HEAPU8,data,imageSize)}};var _glCompressedTexSubImage3D=(target,level,xoffset,yoffset,zoffset,width,height,depth,format,imageSize,data)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.compressedTexSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,imageSize,data)}else{GLctx.compressedTexSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,HEAPU8,data,imageSize)}};var _glCopyBufferSubData=(x0,x1,x2,x3,x4)=>GLctx.copyBufferSubData(x0,x1,x2,x3,x4);var _glCreateProgram=()=>{var id=GL.getNewId(GL.programs);var program=GLctx.createProgram();program.name=id;program.maxUniformLength=program.maxAttributeLength=program.maxUniformBlockNameLength=0;program.uniformIdCounter=1;GL.programs[id]=program;return id};var _glCreateShader=shaderType=>{var id=GL.getNewId(GL.shaders);GL.shaders[id]=GLctx.createShader(shaderType);return id};var _glCullFace=x0=>GLctx.cullFace(x0);var _glDeleteBuffers=(n,buffers)=>{for(var i=0;i>2];var buffer=GL.buffers[id];if(!buffer)continue;GLctx.deleteBuffer(buffer);buffer.name=0;GL.buffers[id]=null;if(id==GLctx.currentPixelPackBufferBinding)GLctx.currentPixelPackBufferBinding=0;if(id==GLctx.currentPixelUnpackBufferBinding)GLctx.currentPixelUnpackBufferBinding=0}};var _glDeleteFramebuffers=(n,framebuffers)=>{for(var i=0;i>2];var framebuffer=GL.framebuffers[id];if(!framebuffer)continue;GLctx.deleteFramebuffer(framebuffer);framebuffer.name=0;GL.framebuffers[id]=null}};var _glDeleteProgram=id=>{if(!id)return;var program=GL.programs[id];if(!program){GL.recordError(1281);return}GLctx.deleteProgram(program);program.name=0;GL.programs[id]=null};var _glDeleteQueries=(n,ids)=>{for(var i=0;i>2];var query=GL.queries[id];if(!query)continue;GLctx.deleteQuery(query);GL.queries[id]=null}};var _glDeleteRenderbuffers=(n,renderbuffers)=>{for(var i=0;i>2];var renderbuffer=GL.renderbuffers[id];if(!renderbuffer)continue;GLctx.deleteRenderbuffer(renderbuffer);renderbuffer.name=0;GL.renderbuffers[id]=null}};var _glDeleteShader=id=>{if(!id)return;var shader=GL.shaders[id];if(!shader){GL.recordError(1281);return}GLctx.deleteShader(shader);GL.shaders[id]=null};var _glDeleteSync=id=>{if(!id)return;var sync=GL.syncs[id];if(!sync){GL.recordError(1281);return}GLctx.deleteSync(sync);sync.name=0;GL.syncs[id]=null};var _glDeleteTextures=(n,textures)=>{for(var i=0;i>2];var texture=GL.textures[id];if(!texture)continue;GLctx.deleteTexture(texture);texture.name=0;GL.textures[id]=null}};var _glDeleteVertexArrays=(n,vaos)=>{for(var i=0;i>2];GLctx.deleteVertexArray(GL.vaos[id]);GL.vaos[id]=null}};var _glDepthFunc=x0=>GLctx.depthFunc(x0);var _glDepthMask=flag=>{GLctx.depthMask(!!flag)};var _glDisable=x0=>GLctx.disable(x0);var _glDisableVertexAttribArray=index=>{GLctx.disableVertexAttribArray(index)};var _glDrawArrays=(mode,first,count)=>{GLctx.drawArrays(mode,first,count)};var _glDrawArraysInstanced=(mode,first,count,primcount)=>{GLctx.drawArraysInstanced(mode,first,count,primcount)};var tempFixedLengthArray=[];var _glDrawBuffers=(n,bufs)=>{var bufArray=tempFixedLengthArray[n];for(var i=0;i>2]}GLctx.drawBuffers(bufArray)};var _glDrawElements=(mode,count,type,indices)=>{GLctx.drawElements(mode,count,type,indices)};var _glDrawElementsInstanced=(mode,count,type,indices,primcount)=>{GLctx.drawElementsInstanced(mode,count,type,indices,primcount)};var _glEnable=x0=>GLctx.enable(x0);var _glEnableVertexAttribArray=index=>{GLctx.enableVertexAttribArray(index)};var _glEndTransformFeedback=()=>GLctx.endTransformFeedback();var _glFenceSync=(condition,flags)=>{var sync=GLctx.fenceSync(condition,flags);if(sync){var id=GL.getNewId(GL.syncs);sync.name=id;GL.syncs[id]=sync;return id}return 0};var _glFinish=()=>GLctx.finish();var _glFramebufferRenderbuffer=(target,attachment,renderbuffertarget,renderbuffer)=>{GLctx.framebufferRenderbuffer(target,attachment,renderbuffertarget,GL.renderbuffers[renderbuffer])};var _glFramebufferTexture2D=(target,attachment,textarget,texture,level)=>{GLctx.framebufferTexture2D(target,attachment,textarget,GL.textures[texture],level)};var _glFramebufferTextureLayer=(target,attachment,texture,level,layer)=>{GLctx.framebufferTextureLayer(target,attachment,GL.textures[texture],level,layer)};var _glFrontFace=x0=>GLctx.frontFace(x0);var _glGenBuffers=(n,buffers)=>{GL.genObject(n,buffers,"createBuffer",GL.buffers)};var _glGenFramebuffers=(n,ids)=>{GL.genObject(n,ids,"createFramebuffer",GL.framebuffers)};var _glGenQueries=(n,ids)=>{GL.genObject(n,ids,"createQuery",GL.queries)};var _glGenRenderbuffers=(n,renderbuffers)=>{GL.genObject(n,renderbuffers,"createRenderbuffer",GL.renderbuffers)};var _glGenTextures=(n,textures)=>{GL.genObject(n,textures,"createTexture",GL.textures)};var _glGenVertexArrays=(n,arrays)=>{GL.genObject(n,arrays,"createVertexArray",GL.vaos)};var _glGenerateMipmap=x0=>GLctx.generateMipmap(x0);var readI53FromI64=ptr=>HEAPU32[ptr>>2]+HEAP32[ptr+4>>2]*4294967296;var readI53FromU64=ptr=>HEAPU32[ptr>>2]+HEAPU32[ptr+4>>2]*4294967296;var writeI53ToI64=(ptr,num)=>{HEAPU32[ptr>>2]=num;var lower=HEAPU32[ptr>>2];HEAPU32[ptr+4>>2]=(num-lower)/4294967296;var deserialized=num>=0?readI53FromU64(ptr):readI53FromI64(ptr);var offset=ptr>>2;if(deserialized!=num)warnOnce(`writeI53ToI64() out of range: serialized JS Number ${num} to Wasm heap as bytes lo=${ptrToString(HEAPU32[offset])}, hi=${ptrToString(HEAPU32[offset+1])}, which deserializes back to ${deserialized} instead!`)};var webglGetExtensions=()=>{var exts=getEmscriptenSupportedExtensions(GLctx);exts=exts.concat(exts.map(e=>"GL_"+e));return exts};var emscriptenWebGLGet=(name_,p,type)=>{if(!p){GL.recordError(1281);return}var ret=undefined;switch(name_){case 36346:ret=1;break;case 36344:if(type!=0&&type!=1){GL.recordError(1280)}return;case 34814:case 36345:ret=0;break;case 34466:var formats=GLctx.getParameter(34467);ret=formats?formats.length:0;break;case 33309:if(GL.currentContext.version<2){GL.recordError(1282);return}ret=webglGetExtensions().length;break;case 33307:case 33308:if(GL.currentContext.version<2){GL.recordError(1280);return}ret=name_==33307?3:0;break}if(ret===undefined){var result=GLctx.getParameter(name_);switch(typeof result){case"number":ret=result;break;case"boolean":ret=result?1:0;break;case"string":GL.recordError(1280);return;case"object":if(result===null){switch(name_){case 34964:case 35725:case 34965:case 36006:case 36007:case 32873:case 34229:case 36662:case 36663:case 35053:case 35055:case 36010:case 35097:case 35869:case 32874:case 36389:case 35983:case 35368:case 34068:{ret=0;break}default:{GL.recordError(1280);return}}}else if(result instanceof Float32Array||result instanceof Uint32Array||result instanceof Int32Array||result instanceof Array){for(var i=0;i>2]=result[i];break;case 2:HEAPF32[p+i*4>>2]=result[i];break;case 4:HEAP8[p+i]=result[i]?1:0;break}}return}else{try{ret=result.name|0}catch(e){GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Unknown object returned from WebGL getParameter(${name_})! (error: ${e})`);return}}break;default:GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Native code calling glGet${type}v(${name_}) and it returns ${result} of type ${typeof result}!`);return}}switch(type){case 1:writeI53ToI64(p,ret);break;case 0:HEAP32[p>>2]=ret;break;case 2:HEAPF32[p>>2]=ret;break;case 4:HEAP8[p]=ret?1:0;break}};var _glGetFloatv=(name_,p)=>emscriptenWebGLGet(name_,p,2);var _glGetInteger64v=(name_,p)=>{emscriptenWebGLGet(name_,p,1)};var _glGetIntegerv=(name_,p)=>emscriptenWebGLGet(name_,p,0);var _glGetProgramInfoLog=(program,maxLength,length,infoLog)=>{var log=GLctx.getProgramInfoLog(GL.programs[program]);if(log===null)log="(unknown error)";var numBytesWrittenExclNull=maxLength>0&&infoLog?stringToUTF8(log,infoLog,maxLength):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _glGetProgramiv=(program,pname,p)=>{if(!p){GL.recordError(1281);return}if(program>=GL.counter){GL.recordError(1281);return}program=GL.programs[program];if(pname==35716){var log=GLctx.getProgramInfoLog(program);if(log===null)log="(unknown error)";HEAP32[p>>2]=log.length+1}else if(pname==35719){if(!program.maxUniformLength){var numActiveUniforms=GLctx.getProgramParameter(program,35718);for(var i=0;i>2]=program.maxUniformLength}else if(pname==35722){if(!program.maxAttributeLength){var numActiveAttributes=GLctx.getProgramParameter(program,35721);for(var i=0;i>2]=program.maxAttributeLength}else if(pname==35381){if(!program.maxUniformBlockNameLength){var numActiveUniformBlocks=GLctx.getProgramParameter(program,35382);for(var i=0;i>2]=program.maxUniformBlockNameLength}else{HEAP32[p>>2]=GLctx.getProgramParameter(program,pname)}};var _glGetShaderInfoLog=(shader,maxLength,length,infoLog)=>{var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log="(unknown error)";var numBytesWrittenExclNull=maxLength>0&&infoLog?stringToUTF8(log,infoLog,maxLength):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _glGetShaderiv=(shader,pname,p)=>{if(!p){GL.recordError(1281);return}if(pname==35716){var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log="(unknown error)";var logLength=log?log.length+1:0;HEAP32[p>>2]=logLength}else if(pname==35720){var source=GLctx.getShaderSource(GL.shaders[shader]);var sourceLength=source?source.length+1:0;HEAP32[p>>2]=sourceLength}else{HEAP32[p>>2]=GLctx.getShaderParameter(GL.shaders[shader],pname)}};var _glGetString=name_=>{var ret=GL.stringCache[name_];if(!ret){switch(name_){case 7939:ret=stringToNewUTF8(webglGetExtensions().join(" "));break;case 7936:case 7937:case 37445:case 37446:var s=GLctx.getParameter(name_);if(!s){GL.recordError(1280)}ret=s?stringToNewUTF8(s):0;break;case 7938:var webGLVersion=GLctx.getParameter(7938);var glVersion=`OpenGL ES 2.0 (${webGLVersion})`;if(GL.currentContext.version>=2)glVersion=`OpenGL ES 3.0 (${webGLVersion})`;ret=stringToNewUTF8(glVersion);break;case 35724:var glslVersion=GLctx.getParameter(35724);var ver_re=/^WebGL GLSL ES ([0-9]\.[0-9][0-9]?)(?:$| .*)/;var ver_num=glslVersion.match(ver_re);if(ver_num!==null){if(ver_num[1].length==3)ver_num[1]=ver_num[1]+"0";glslVersion=`OpenGL ES GLSL ES ${ver_num[1]} (${glslVersion})`}ret=stringToNewUTF8(glslVersion);break;default:GL.recordError(1280)}GL.stringCache[name_]=ret}return ret};var _glGetSynciv=(sync,pname,bufSize,length,values)=>{if(bufSize<0){GL.recordError(1281);return}if(!values){GL.recordError(1281);return}var ret=GLctx.getSyncParameter(GL.syncs[sync],pname);if(ret!==null){HEAP32[values>>2]=ret;if(length)HEAP32[length>>2]=1}};var _glGetUniformBlockIndex=(program,uniformBlockName)=>GLctx.getUniformBlockIndex(GL.programs[program],UTF8ToString(uniformBlockName));var jstoi_q=str=>parseInt(str);var webglGetLeftBracePos=name=>name.slice(-1)=="]"&&name.lastIndexOf("[");var webglPrepareUniformLocationsBeforeFirstUse=program=>{var uniformLocsById=program.uniformLocsById,uniformSizeAndIdsByName=program.uniformSizeAndIdsByName,i,j;if(!uniformLocsById){program.uniformLocsById=uniformLocsById={};program.uniformArrayNamesById={};var numActiveUniforms=GLctx.getProgramParameter(program,35718);for(i=0;i0?nm.slice(0,lb):nm;var id=program.uniformIdCounter;program.uniformIdCounter+=sz;uniformSizeAndIdsByName[arrayName]=[sz,id];for(j=0;j{name=UTF8ToString(name);if(program=GL.programs[program]){webglPrepareUniformLocationsBeforeFirstUse(program);var uniformLocsById=program.uniformLocsById;var arrayIndex=0;var uniformBaseName=name;var leftBrace=webglGetLeftBracePos(name);if(leftBrace>0){arrayIndex=jstoi_q(name.slice(leftBrace+1))>>>0;uniformBaseName=name.slice(0,leftBrace)}var sizeAndId=program.uniformSizeAndIdsByName[uniformBaseName];if(sizeAndId&&arrayIndex{program=GL.programs[program];GLctx.linkProgram(program);program.uniformLocsById=0;program.uniformSizeAndIdsByName={}};var _glPixelStorei=(pname,param)=>{if(pname==3317){GL.unpackAlignment=param}else if(pname==3314){GL.unpackRowLength=param}GLctx.pixelStorei(pname,param)};var _glReadBuffer=x0=>GLctx.readBuffer(x0);var computeUnpackAlignedImageSize=(width,height,sizePerPixel)=>{function roundedToNextMultipleOf(x,y){return x+y-1&-y}var plainRowSize=(GL.unpackRowLength||width)*sizePerPixel;var alignedRowSize=roundedToNextMultipleOf(plainRowSize,GL.unpackAlignment);return height*alignedRowSize};var colorChannelsInGlTextureFormat=format=>{var colorChannels={5:3,6:4,8:2,29502:3,29504:4,26917:2,26918:2,29846:3,29847:4};return colorChannels[format-6402]||1};var heapObjectForWebGLType=type=>{type-=5120;if(type==0)return HEAP8;if(type==1)return HEAPU8;if(type==2)return HEAP16;if(type==4)return HEAP32;if(type==6)return HEAPF32;if(type==5||type==28922||type==28520||type==30779||type==30782)return HEAPU32;return HEAPU16};var toTypedArrayIndex=(pointer,heap)=>pointer>>>31-Math.clz32(heap.BYTES_PER_ELEMENT);var emscriptenWebGLGetTexPixelData=(type,format,width,height,pixels,internalFormat)=>{var heap=heapObjectForWebGLType(type);var sizePerPixel=colorChannelsInGlTextureFormat(format)*heap.BYTES_PER_ELEMENT;var bytes=computeUnpackAlignedImageSize(width,height,sizePerPixel);return heap.subarray(toTypedArrayIndex(pixels,heap),toTypedArrayIndex(pixels+bytes,heap))};var _glReadPixels=(x,y,width,height,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelPackBufferBinding){GLctx.readPixels(x,y,width,height,format,type,pixels);return}var heap=heapObjectForWebGLType(type);var target=toTypedArrayIndex(pixels,heap);GLctx.readPixels(x,y,width,height,format,type,heap,target);return}var pixelData=emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,format);if(!pixelData){GL.recordError(1280);return}GLctx.readPixels(x,y,width,height,format,type,pixelData)};var _glRenderbufferStorage=(x0,x1,x2,x3)=>GLctx.renderbufferStorage(x0,x1,x2,x3);var _glRenderbufferStorageMultisample=(x0,x1,x2,x3,x4)=>GLctx.renderbufferStorageMultisample(x0,x1,x2,x3,x4);var _glScissor=(x0,x1,x2,x3)=>GLctx.scissor(x0,x1,x2,x3);var _glShaderSource=(shader,count,string,length)=>{var source=GL.getSource(shader,count,string,length);GLctx.shaderSource(GL.shaders[shader],source)};var _glStencilFunc=(x0,x1,x2)=>GLctx.stencilFunc(x0,x1,x2);var _glStencilMask=x0=>GLctx.stencilMask(x0);var _glStencilOp=(x0,x1,x2)=>GLctx.stencilOp(x0,x1,x2);var _glTexImage2D=(target,level,internalFormat,width,height,border,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding){GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixels);return}if(pixels){var heap=heapObjectForWebGLType(type);var index=toTypedArrayIndex(pixels,heap);GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,heap,index);return}}var pixelData=pixels?emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,internalFormat):null;GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixelData)};var _glTexImage3D=(target,level,internalFormat,width,height,depth,border,format,type,pixels)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,pixels)}else if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,heap,toTypedArrayIndex(pixels,heap))}else{GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,null)}};var _glTexParameterf=(x0,x1,x2)=>GLctx.texParameterf(x0,x1,x2);var _glTexParameteri=(x0,x1,x2)=>GLctx.texParameteri(x0,x1,x2);var _glTexStorage2D=(x0,x1,x2,x3,x4)=>GLctx.texStorage2D(x0,x1,x2,x3,x4);var _glTexSubImage3D=(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)}else if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,heap,toTypedArrayIndex(pixels,heap))}else{GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,null)}};var _glTransformFeedbackVaryings=(program,count,varyings,bufferMode)=>{program=GL.programs[program];var vars=[];for(var i=0;i>2]));GLctx.transformFeedbackVaryings(program,vars,bufferMode)};var webglGetUniformLocation=location=>{var p=GLctx.currentProgram;if(p){var webglLoc=p.uniformLocsById[location];if(typeof webglLoc=="number"){p.uniformLocsById[location]=webglLoc=GLctx.getUniformLocation(p,p.uniformArrayNamesById[location]+(webglLoc>0?`[${webglLoc}]`:""))}return webglLoc}else{GL.recordError(1282)}};var _glUniform1f=(location,v0)=>{GLctx.uniform1f(webglGetUniformLocation(location),v0)};var _glUniform1i=(location,v0)=>{GLctx.uniform1i(webglGetUniformLocation(location),v0)};var miniTempWebGLIntBuffers=[];var _glUniform1iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform1iv(webglGetUniformLocation(location),HEAP32,value>>2,count);return}if(count<=288){var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2]}}else{var view=HEAP32.subarray(value>>2,value+count*4>>2)}GLctx.uniform1iv(webglGetUniformLocation(location),view)};var _glUniform1ui=(location,v0)=>{GLctx.uniform1ui(webglGetUniformLocation(location),v0)};var _glUniform1uiv=(location,count,value)=>{count&&GLctx.uniform1uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count)};var _glUniform2f=(location,v0,v1)=>{GLctx.uniform2f(webglGetUniformLocation(location),v0,v1)};var miniTempWebGLFloatBuffers=[];var _glUniform2fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform2fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*2);return}if(count<=144){count*=2;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*8>>2)}GLctx.uniform2fv(webglGetUniformLocation(location),view)};var _glUniform2iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform2iv(webglGetUniformLocation(location),HEAP32,value>>2,count*2);return}if(count<=144){count*=2;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*8>>2)}GLctx.uniform2iv(webglGetUniformLocation(location),view)};var _glUniform3fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform3fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*3);return}if(count<=96){count*=3;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*12>>2)}GLctx.uniform3fv(webglGetUniformLocation(location),view)};var _glUniform4f=(location,v0,v1,v2,v3)=>{GLctx.uniform4f(webglGetUniformLocation(location),v0,v1,v2,v3)};var _glUniform4fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform4fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*4);return}if(count<=72){var view=miniTempWebGLFloatBuffers[4*count];var heap=HEAPF32;value=value>>2;count*=4;for(var i=0;i>2,value+count*16>>2)}GLctx.uniform4fv(webglGetUniformLocation(location),view)};var _glUniformBlockBinding=(program,uniformBlockIndex,uniformBlockBinding)=>{program=GL.programs[program];GLctx.uniformBlockBinding(program,uniformBlockIndex,uniformBlockBinding)};var _glUniformMatrix3fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*9);return}if(count<=32){count*=9;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2];view[i+3]=HEAPF32[value+(4*i+12)>>2];view[i+4]=HEAPF32[value+(4*i+16)>>2];view[i+5]=HEAPF32[value+(4*i+20)>>2];view[i+6]=HEAPF32[value+(4*i+24)>>2];view[i+7]=HEAPF32[value+(4*i+28)>>2];view[i+8]=HEAPF32[value+(4*i+32)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*36>>2)}GLctx.uniformMatrix3fv(webglGetUniformLocation(location),!!transpose,view)};var _glUniformMatrix4fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*16);return}if(count<=18){var view=miniTempWebGLFloatBuffers[16*count];var heap=HEAPF32;value=value>>2;count*=16;for(var i=0;i>2,value+count*64>>2)}GLctx.uniformMatrix4fv(webglGetUniformLocation(location),!!transpose,view)};var _glUseProgram=program=>{program=GL.programs[program];GLctx.useProgram(program);GLctx.currentProgram=program};var _glVertexAttrib4f=(x0,x1,x2,x3,x4)=>GLctx.vertexAttrib4f(x0,x1,x2,x3,x4);var _glVertexAttribDivisor=(index,divisor)=>{GLctx.vertexAttribDivisor(index,divisor)};var _glVertexAttribI4ui=(x0,x1,x2,x3,x4)=>GLctx.vertexAttribI4ui(x0,x1,x2,x3,x4);var _glVertexAttribIPointer=(index,size,type,stride,ptr)=>{GLctx.vertexAttribIPointer(index,size,type,stride,ptr)};var _glVertexAttribPointer=(index,size,type,normalized,stride,ptr)=>{GLctx.vertexAttribPointer(index,size,type,!!normalized,stride,ptr)};var _glViewport=(x0,x1,x2,x3)=>GLctx.viewport(x0,x1,x2,x3);var GodotRuntime={get_func:function(ptr){return wasmTable.get(ptr)},error:function(){err.apply(null,Array.from(arguments))},print:function(){out.apply(null,Array.from(arguments))},malloc:function(p_size){return _malloc(p_size)},free:function(p_ptr){_free(p_ptr)},getHeapValue:function(p_ptr,p_type){return getValue(p_ptr,p_type)},setHeapValue:function(p_ptr,p_value,p_type){setValue(p_ptr,p_value,p_type)},heapSub:function(p_heap,p_ptr,p_len){const bytes=p_heap.BYTES_PER_ELEMENT;return p_heap.subarray(p_ptr/bytes,p_ptr/bytes+p_len)},heapSlice:function(p_heap,p_ptr,p_len){const bytes=p_heap.BYTES_PER_ELEMENT;return p_heap.slice(p_ptr/bytes,p_ptr/bytes+p_len)},heapCopy:function(p_dst,p_src,p_ptr){const bytes=p_src.BYTES_PER_ELEMENT;return p_dst.set(p_src,p_ptr/bytes)},parseString:function(p_ptr){return UTF8ToString(p_ptr)},parseStringArray:function(p_ptr,p_size){const strings=[];const ptrs=GodotRuntime.heapSub(HEAP32,p_ptr,p_size);ptrs.forEach(function(ptr){strings.push(GodotRuntime.parseString(ptr))});return strings},strlen:function(p_str){return lengthBytesUTF8(p_str)},allocString:function(p_str){const length=GodotRuntime.strlen(p_str)+1;const c_str=GodotRuntime.malloc(length);stringToUTF8(p_str,c_str,length);return c_str},allocStringArray:function(p_strings){const size=p_strings.length;const c_ptr=GodotRuntime.malloc(size*4);for(let i=0;i>2)+i]=GodotRuntime.allocString(p_strings[i])}return c_ptr},freeStringArray:function(p_ptr,p_len){for(let i=0;i>2)+i])}GodotRuntime.free(p_ptr)},stringToHeap:function(p_str,p_ptr,p_len){return stringToUTF8Array(p_str,HEAP8,p_ptr,p_len)}};var GodotConfig={canvas:null,locale:"en",canvas_resize_policy:2,virtual_keyboard:false,persistent_drops:false,godot_pool_size:4,on_execute:null,on_exit:null,init_config:function(p_opts){GodotConfig.canvas_resize_policy=p_opts["canvasResizePolicy"];GodotConfig.canvas=p_opts["canvas"];GodotConfig.locale=p_opts["locale"]||GodotConfig.locale;GodotConfig.virtual_keyboard=p_opts["virtualKeyboard"];GodotConfig.persistent_drops=!!p_opts["persistentDrops"];GodotConfig.godot_pool_size=p_opts["godotPoolSize"];GodotConfig.on_execute=p_opts["onExecute"];GodotConfig.on_exit=p_opts["onExit"];if(p_opts["focusCanvas"]){GodotConfig.canvas.focus()}},locate_file:function(file){return Module["locateFile"](file)},clear:function(){GodotConfig.canvas=null;GodotConfig.locale="en";GodotConfig.canvas_resize_policy=2;GodotConfig.virtual_keyboard=false;GodotConfig.persistent_drops=false;GodotConfig.on_execute=null;GodotConfig.on_exit=null}};var GodotFS={ENOENT:44,_idbfs:false,_syncing:false,_mount_points:[],is_persistent:function(){return GodotFS._idbfs?1:0},init:function(persistentPaths){GodotFS._idbfs=false;if(!Array.isArray(persistentPaths)){return Promise.reject(new Error("Persistent paths must be an array"))}if(!persistentPaths.length){return Promise.resolve()}GodotFS._mount_points=persistentPaths.slice();function createRecursive(dir){try{FS.stat(dir)}catch(e){if(e.errno!==GodotFS.ENOENT){GodotRuntime.error(e)}FS.mkdirTree(dir)}}GodotFS._mount_points.forEach(function(path){createRecursive(path);FS.mount(IDBFS,{},path)});return new Promise(function(resolve,reject){FS.syncfs(true,function(err){if(err){GodotFS._mount_points=[];GodotFS._idbfs=false;GodotRuntime.print(`IndexedDB not available: ${err.message}`)}else{GodotFS._idbfs=true}resolve(err)})})},deinit:function(){GodotFS._mount_points.forEach(function(path){try{FS.unmount(path)}catch(e){GodotRuntime.print("Already unmounted",e)}if(GodotFS._idbfs&&IDBFS.dbs[path]){IDBFS.dbs[path].close();delete IDBFS.dbs[path]}});GodotFS._mount_points=[];GodotFS._idbfs=false;GodotFS._syncing=false},sync:function(){if(GodotFS._syncing){GodotRuntime.error("Already syncing!");return Promise.resolve()}GodotFS._syncing=true;return new Promise(function(resolve,reject){FS.syncfs(false,function(error){if(error){GodotRuntime.error(`Failed to save IDB file system: ${error.message}`)}GodotFS._syncing=false;resolve(error)})})},copy_to_fs:function(path,buffer){const idx=path.lastIndexOf("/");let dir="/";if(idx>0){dir=path.slice(0,idx)}try{FS.stat(dir)}catch(e){if(e.errno!==GodotFS.ENOENT){GodotRuntime.error(e)}FS.mkdirTree(dir)}FS.writeFile(path,new Uint8Array(buffer))}};var GodotOS={request_quit:function(){},_async_cbs:[],_fs_sync_promise:null,atexit:function(p_promise_cb){GodotOS._async_cbs.push(p_promise_cb)},cleanup:function(exit_code){const cb=GodotConfig.on_exit;GodotFS.deinit();GodotConfig.clear();if(cb){cb(exit_code)}},finish_async:function(callback){GodotOS._fs_sync_promise.then(function(err){const promises=[];GodotOS._async_cbs.forEach(function(cb){promises.push(new Promise(cb))});return Promise.all(promises)}).then(function(){return GodotFS.sync()}).then(function(err){setTimeout(function(){callback()},0)})}};var GodotAudio={MAX_VOLUME_CHANNELS:8,GodotChannel:{CHANNEL_L:0,CHANNEL_R:1,CHANNEL_C:3,CHANNEL_LFE:4,CHANNEL_RL:5,CHANNEL_RR:6,CHANNEL_SL:7,CHANNEL_SR:8},WebChannel:{CHANNEL_L:0,CHANNEL_R:1,CHANNEL_SL:2,CHANNEL_SR:3,CHANNEL_C:4,CHANNEL_LFE:5},samples:null,Sample:class Sample{static getSample(id){if(!GodotAudio.samples.has(id)){throw new ReferenceError(`Could not find sample "${id}"`)}return GodotAudio.samples.get(id)}static getSampleOrNull(id){return GodotAudio.samples.get(id)??null}static create(params,options={}){const sample=new GodotAudio.Sample(params,options);GodotAudio.samples.set(params.id,sample);return sample}static delete(id){GodotAudio.samples.delete(id)}constructor(params,options={}){this.id=params.id;this._audioBuffer=null;this.numberOfChannels=options.numberOfChannels??2;this.sampleRate=options.sampleRate??44100;this.loopMode=options.loopMode??"disabled";this.loopBegin=options.loopBegin??0;this.loopEnd=options.loopEnd??0;this.setAudioBuffer(params.audioBuffer)}getAudioBuffer(){return this._duplicateAudioBuffer()}setAudioBuffer(val){this._audioBuffer=val}clear(){this.setAudioBuffer(null);GodotAudio.Sample.delete(this.id)}_duplicateAudioBuffer(){if(this._audioBuffer==null){throw new Error("couldn't duplicate a null audioBuffer")}const channels=new Array(this._audioBuffer.numberOfChannels);for(let i=0;i{const newErr=new Error("Failed to create PositionWorklet.");newErr.cause=err;GodotRuntime.error(newErr)})}getPlaybackRate(){return this._playbackRate}getPlaybackPosition(){return this._playbackPosition}setPlaybackRate(val){this._playbackRate=val;this._syncPlaybackRate()}getPitchScale(){return this._pitchScale}setPitchScale(val){this._pitchScale=val;this._syncPlaybackRate()}getSample(){return GodotAudio.Sample.getSample(this.streamObjectId)}getOutputNode(){return this._source}start(){if(this.isStarted){return}this._resetSourceStartTime();this._source.start(this.startTime,this.offset);this.isStarted=true}stop(){this.clear()}restart(){this.isPaused=false;this.pauseTime=0;this._resetSourceStartTime();this._restart()}pause(enable=true){if(enable){this._pause();return}this._unpause()}connect(node){return this.getOutputNode().connect(node)}setVolumes(buses,volumes){for(let busIdx=0;busIdx0){this._positionWorklet=GodotAudio.audioPositionWorkletNodes.pop()}else{this._positionWorklet=new AudioWorkletNode(GodotAudio.ctx,"godot-position-reporting-processor")}this._playbackPosition=this.offset;this._positionWorklet.port.onmessage=event=>{switch(event.data["type"]){case"position":this._playbackPosition=parseInt(event.data.data,10)/this.getSample().sampleRate+this.offset;break;default:}};const resetParameter=this._positionWorklet.parameters.get("reset");resetParameter.setValueAtTime(1,GodotAudio.ctx.currentTime);resetParameter.setValueAtTime(0,GodotAudio.ctx.currentTime+1);return this._positionWorklet}clear(){this.isCanceled=true;this.isPaused=false;this.pauseTime=0;if(this._source!=null){this._source.removeEventListener("ended",this._onended);this._onended=null;if(this.isStarted){this._source.stop()}this._source.disconnect();this._source=null}for(const sampleNodeBus of this._sampleNodeBuses.values()){sampleNodeBus.clear()}this._sampleNodeBuses.clear();if(this._positionWorklet){this._positionWorklet.disconnect();this._positionWorklet.port.onmessage=null;GodotAudio.audioPositionWorkletNodes.push(this._positionWorklet);this._positionWorklet=null}GodotAudio.SampleNode.delete(this.id)}_resetSourceStartTime(){this._sourceStartTime=GodotAudio.ctx.currentTime}_syncPlaybackRate(){this._source.playbackRate.value=this.getPlaybackRate()*this.getPitchScale()}_restart(){if(this._source!=null){this._source.disconnect()}this._source=GodotAudio.ctx.createBufferSource();this._source.buffer=this.getSample().getAudioBuffer();for(const sampleNodeBus of this._sampleNodeBuses.values()){this.connect(sampleNodeBus.getInputNode())}this._addEndedListener();const pauseTime=this.isPaused?this.pauseTime:0;if(this._positionWorklet!=null){this._positionWorklet.port.postMessage({type:"clear"});this._source.connect(this._positionWorklet)}this._source.start(this.startTime,this.offset+pauseTime);this.isStarted=true}_pause(){if(!this.isStarted){return}this.isPaused=true;this.pauseTime=(GodotAudio.ctx.currentTime-this._sourceStartTime)/this.getPlaybackRate();this._source.stop()}_unpause(){this._restart();this.isPaused=false;this.pauseTime=0}_addEndedListener(){if(this._onended!=null){this._source.removeEventListener("ended",this._onended)}const self=this;this._onended=_=>{if(self.isPaused){return}switch(self.getSample().loopMode){case"disabled":self.stop();break;case"forward":case"backward":self.restart();break;default:}};this._source.addEventListener("ended",this._onended)}},deleteSampleNode:pSampleNodeId=>{GodotAudio.sampleNodes.delete(pSampleNodeId);if(GodotAudio.sampleFinishedCallback==null){return}const sampleNodeIdPtr=GodotRuntime.allocString(pSampleNodeId);GodotAudio.sampleFinishedCallback(sampleNodeIdPtr);GodotRuntime.free(sampleNodeIdPtr)},buses:null,busSolo:null,Bus:class Bus{static getCount(){return GodotAudio.buses.length}static setCount(val){const buses=GodotAudio.buses;if(val===buses.length){return}if(val=GodotAudio.buses.length){throw new ReferenceError(`invalid bus index "${index}"`)}return GodotAudio.buses[index]}static getBusOrNull(index){if(index<0||index>=GodotAudio.buses.length){return null}return GodotAudio.buses[index]}static move(fromIndex,toIndex){const movedBus=GodotAudio.Bus.getBusOrNull(fromIndex);if(movedBus==null){return}const buses=GodotAudio.buses.filter((_,i)=>i!==fromIndex);buses.splice(toIndex-1,0,movedBus);GodotAudio.buses=buses}static addAt(index){const newBus=GodotAudio.Bus.create();if(index!==newBus.getId()){GodotAudio.Bus.move(newBus.getId(),index)}}static create(){const newBus=new GodotAudio.Bus;const isFirstBus=GodotAudio.buses.length===0;GodotAudio.buses.push(newBus);if(isFirstBus){newBus.setSend(null)}else{newBus.setSend(GodotAudio.Bus.getBus(0))}return newBus}constructor(){this._sampleNodes=new Set;this.isSolo=false;this._send=null;this._gainNode=GodotAudio.ctx.createGain();this._soloNode=GodotAudio.ctx.createGain();this._muteNode=GodotAudio.ctx.createGain();this._gainNode.connect(this._soloNode).connect(this._muteNode)}getId(){return GodotAudio.buses.indexOf(this)}getVolumeDb(){return GodotAudio.linear_to_db(this._gainNode.gain.value)}setVolumeDb(val){const linear=GodotAudio.db_to_linear(val);if(isFinite(linear)){this._gainNode.gain.value=linear}}getSend(){return this._send}setSend(val){this._send=val;if(val==null){if(this.getId()==0){this.getOutputNode().connect(GodotAudio.ctx.destination);return}throw new Error(`Cannot send to "${val}" without the bus being at index 0 (current index: ${this.getId()})`)}this.connect(val)}getInputNode(){return this._gainNode}getOutputNode(){return this._muteNode}mute(enable){this._muteNode.gain.value=enable?0:1}solo(enable){if(this.isSolo===enable){return}if(enable){if(GodotAudio.busSolo!=null&&GodotAudio.busSolo!==this){GodotAudio.busSolo._disableSolo()}this._enableSolo();return}this._disableSolo()}addSampleNode(sampleNode){this._sampleNodes.add(sampleNode);sampleNode.getOutputNode().connect(this.getInputNode())}removeSampleNode(sampleNode){this._sampleNodes.delete(sampleNode);sampleNode.getOutputNode().disconnect()}connect(bus){if(bus==null){throw new Error("cannot connect to null bus")}this.getOutputNode().disconnect();this.getOutputNode().connect(bus.getInputNode());return bus}clear(){GodotAudio.buses=GodotAudio.buses.filter(v=>v!==this)}_syncSampleNodes(){const sampleNodes=Array.from(this._sampleNodes);for(let i=0;iotherBus!==this);for(let i=0;iotherBus!==this);for(let i=0;iGodotAudio.Bus.getBus(busIndex));sampleNode.setVolumes(buses,volumes)},set_sample_bus_count:function(count){GodotAudio.Bus.setCount(count)},remove_sample_bus:function(index){const bus=GodotAudio.Bus.getBusOrNull(index);if(bus==null){return}bus.clear()},add_sample_bus:function(atPos){GodotAudio.Bus.addAt(atPos)},move_sample_bus:function(busIndex,toPos){GodotAudio.Bus.move(busIndex,toPos)},set_sample_bus_send:function(busIndex,sendIndex){const bus=GodotAudio.Bus.getBusOrNull(busIndex);if(bus==null){return}let targetBus=GodotAudio.Bus.getBusOrNull(sendIndex);if(targetBus==null){targetBus=GodotAudio.Bus.getBus(0)}bus.setSend(targetBus)},set_sample_bus_volume_db:function(busIndex,volumeDb){const bus=GodotAudio.Bus.getBusOrNull(busIndex);if(bus==null){return}bus.setVolumeDb(volumeDb)},set_sample_bus_solo:function(busIndex,enable){const bus=GodotAudio.Bus.getBusOrNull(busIndex);if(bus==null){return}bus.solo(enable)},set_sample_bus_mute:function(busIndex,enable){const bus=GodotAudio.Bus.getBusOrNull(busIndex);if(bus==null){return}bus.mute(enable)}};function _godot_audio_get_sample_playback_position(playbackObjectIdStrPtr){const playbackObjectId=GodotRuntime.parseString(playbackObjectIdStrPtr);const sampleNode=GodotAudio.SampleNode.getSampleNodeOrNull(playbackObjectId);if(sampleNode==null){return 0}return sampleNode.getPlaybackPosition()}function _godot_audio_has_script_processor(){return GodotAudio.ctx&&GodotAudio.ctx.createScriptProcessor?1:0}function _godot_audio_has_worklet(){return GodotAudio.ctx&&GodotAudio.ctx.audioWorklet?1:0}function _godot_audio_init(p_mix_rate,p_latency,p_state_change,p_latency_update){const statechange=GodotRuntime.get_func(p_state_change);const latencyupdate=GodotRuntime.get_func(p_latency_update);const mix_rate=GodotRuntime.getHeapValue(p_mix_rate,"i32");const channels=GodotAudio.init(mix_rate,p_latency,statechange,latencyupdate);GodotRuntime.setHeapValue(p_mix_rate,GodotAudio.ctx.sampleRate,"i32");return channels}function _godot_audio_input_start(){return GodotAudio.create_input(function(input){input.connect(GodotAudio.driver.get_node())})}function _godot_audio_input_stop(){if(GodotAudio.input){const tracks=GodotAudio.input["mediaStream"]["getTracks"]();for(let i=0;i=size){const high=size-wpos;wbuf.set(buffer.subarray(wpos,size));pending_samples-=high;wpos=0}if(pending_samples>0){wbuf.set(buffer.subarray(wpos,wpos+pending_samples),tot_sent-pending_samples)}port.postMessage({cmd:"chunk",data:wbuf.subarray(0,tot_sent)});wpos+=pending_samples;pending_samples=0}this.receive=function(recv_buf){const buffer=GodotRuntime.heapSub(HEAPF32,p_in_buf,p_in_size);const from=rpos;let to_write=recv_buf.length;let high=0;if(rpos+to_write>=p_in_size){high=p_in_size-rpos;buffer.set(recv_buf.subarray(0,high),rpos);to_write-=high;rpos=0}if(to_write){buffer.set(recv_buf.subarray(high,to_write),rpos)}in_callback(from,recv_buf.length);rpos+=to_write};this.consumed=function(size,port){pending_samples+=size;send(port)}}GodotAudioWorklet.ring_buffer=new RingBuffer;GodotAudioWorklet.promise.then(function(){const node=GodotAudioWorklet.worklet;const buffer=GodotRuntime.heapSlice(HEAPF32,p_out_buf,p_out_size);node.connect(GodotAudio.ctx.destination);node.port.postMessage({cmd:"start_nothreads",data:[buffer,p_in_size]});node.port.onmessage=function(event){if(!GodotAudioWorklet.worklet){return}if(event.data["cmd"]==="read"){const read=event.data["data"];GodotAudioWorklet.ring_buffer.consumed(read,GodotAudioWorklet.worklet.port)}else if(event.data["cmd"]==="input"){const buf=event.data["data"];if(buf.length>p_in_size){GodotRuntime.error("Input chunk is too big");return}GodotAudioWorklet.ring_buffer.receive(buf)}else{GodotRuntime.error(event.data)}}})},get_node:function(){return GodotAudioWorklet.worklet},close:function(){return new Promise(function(resolve,reject){if(GodotAudioWorklet.promise===null){return}const p=GodotAudioWorklet.promise;p.then(function(){GodotAudioWorklet.worklet.port.postMessage({cmd:"stop",data:null});GodotAudioWorklet.worklet.disconnect();GodotAudioWorklet.worklet.port.onmessage=null;GodotAudioWorklet.worklet=null;GodotAudioWorklet.promise=null;resolve()}).catch(function(err){GodotRuntime.error(err)})})}};function _godot_audio_worklet_create(channels){try{GodotAudioWorklet.create(channels)}catch(e){GodotRuntime.error("Error starting AudioDriverWorklet",e);return 1}return 0}function _godot_audio_worklet_start_no_threads(p_out_buf,p_out_size,p_out_callback,p_in_buf,p_in_size,p_in_callback){const out_callback=GodotRuntime.get_func(p_out_callback);const in_callback=GodotRuntime.get_func(p_in_callback);GodotAudioWorklet.start_no_threads(p_out_buf,p_out_size,out_callback,p_in_buf,p_in_size,in_callback)}function _godot_js_config_canvas_id_get(p_ptr,p_ptr_max){GodotRuntime.stringToHeap(`#${GodotConfig.canvas.id}`,p_ptr,p_ptr_max)}function _godot_js_config_locale_get(p_ptr,p_ptr_max){GodotRuntime.stringToHeap(GodotConfig.locale,p_ptr,p_ptr_max)}var GodotDisplayCursor={shape:"default",visible:true,cursors:{},set_style:function(style){GodotConfig.canvas.style.cursor=style},set_shape:function(shape){GodotDisplayCursor.shape=shape;let css=shape;if(shape in GodotDisplayCursor.cursors){const c=GodotDisplayCursor.cursors[shape];css=`url("${c.url}") ${c.x} ${c.y}, default`}if(GodotDisplayCursor.visible){GodotDisplayCursor.set_style(css)}},clear:function(){GodotDisplayCursor.set_style("");GodotDisplayCursor.shape="default";GodotDisplayCursor.visible=true;Object.keys(GodotDisplayCursor.cursors).forEach(function(key){URL.revokeObjectURL(GodotDisplayCursor.cursors[key]);delete GodotDisplayCursor.cursors[key]})},lockPointer:function(){const canvas=GodotConfig.canvas;if(canvas.requestPointerLock){canvas.requestPointerLock()}},releasePointer:function(){if(document.exitPointerLock){document.exitPointerLock()}},isPointerLocked:function(){return document.pointerLockElement===GodotConfig.canvas}};var GodotEventListeners={handlers:[],has:function(target,event,method,capture){return GodotEventListeners.handlers.findIndex(function(e){return e.target===target&&e.event===event&&e.method===method&&e.capture===capture})!==-1},add:function(target,event,method,capture){if(GodotEventListeners.has(target,event,method,capture)){return}function Handler(p_target,p_event,p_method,p_capture){this.target=p_target;this.event=p_event;this.method=p_method;this.capture=p_capture}GodotEventListeners.handlers.push(new Handler(target,event,method,capture));target.addEventListener(event,method,capture)},clear:function(){GodotEventListeners.handlers.forEach(function(h){h.target.removeEventListener(h.event,h.method,h.capture)});GodotEventListeners.handlers.length=0}};var _emscripten_webgl_do_get_current_context=()=>GL.currentContext?GL.currentContext.handle:0;var _emscripten_webgl_get_current_context=_emscripten_webgl_do_get_current_context;var GodotDisplayScreen={desired_size:[0,0],hidpi:true,getPixelRatio:function(){return GodotDisplayScreen.hidpi?window.devicePixelRatio||1:1},isFullscreen:function(){const elem=document.fullscreenElement||document.mozFullscreenElement||document.webkitFullscreenElement||document.msFullscreenElement;if(elem){return elem===GodotConfig.canvas}return document.fullscreen||document.mozFullScreen||document.webkitIsFullscreen},hasFullscreen:function(){return document.fullscreenEnabled||document.mozFullScreenEnabled||document.webkitFullscreenEnabled},requestFullscreen:function(){if(!GodotDisplayScreen.hasFullscreen()){return 1}const canvas=GodotConfig.canvas;try{const promise=(canvas.requestFullscreen||canvas.msRequestFullscreen||canvas.mozRequestFullScreen||canvas.mozRequestFullscreen||canvas.webkitRequestFullscreen).call(canvas);if(promise){promise.catch(function(){})}}catch(e){return 1}return 0},exitFullscreen:function(){if(!GodotDisplayScreen.isFullscreen()){return 0}try{const promise=document.exitFullscreen();if(promise){promise.catch(function(){})}}catch(e){return 1}return 0},_updateGL:function(){const gl_context_handle=_emscripten_webgl_get_current_context();const gl=GL.getContext(gl_context_handle);if(gl){GL.resizeOffscreenFramebuffer(gl)}},updateSize:function(){const isFullscreen=GodotDisplayScreen.isFullscreen();const wantsFullWindow=GodotConfig.canvas_resize_policy===2;const noResize=GodotConfig.canvas_resize_policy===0;const dWidth=GodotDisplayScreen.desired_size[0];const dHeight=GodotDisplayScreen.desired_size[1];const canvas=GodotConfig.canvas;let width=dWidth;let height=dHeight;if(noResize){if(canvas.width!==width||canvas.height!==height){GodotDisplayScreen.desired_size=[canvas.width,canvas.height];GodotDisplayScreen._updateGL();return 1}return 0}const scale=GodotDisplayScreen.getPixelRatio();if(isFullscreen||wantsFullWindow){width=Math.floor(window.innerWidth*scale);height=Math.floor(window.innerHeight*scale)}const csw=`${Math.floor(width/scale)}px`;const csh=`${Math.floor(height/scale)}px`;if(canvas.style.width!==csw||canvas.style.height!==csh||canvas.width!==width||canvas.height!==height){canvas.width=width;canvas.height=height;canvas.style.width=csw;canvas.style.height=csh;GodotDisplayScreen._updateGL();return 1}return 0}};var GodotDisplayVK={textinput:null,textarea:null,available:function(){return GodotConfig.virtual_keyboard&&"ontouchstart"in window},init:function(input_cb){function create(what){const elem=document.createElement(what);elem.style.display="none";elem.style.position="absolute";elem.style.zIndex="-1";elem.style.background="transparent";elem.style.padding="0px";elem.style.margin="0px";elem.style.overflow="hidden";elem.style.width="0px";elem.style.height="0px";elem.style.border="0px";elem.style.outline="none";elem.readonly=true;elem.disabled=true;GodotEventListeners.add(elem,"input",function(evt){const c_str=GodotRuntime.allocString(elem.value);input_cb(c_str,elem.selectionEnd);GodotRuntime.free(c_str)},false);GodotEventListeners.add(elem,"blur",function(evt){elem.style.display="none";elem.readonly=true;elem.disabled=true},false);GodotConfig.canvas.insertAdjacentElement("beforebegin",elem);return elem}GodotDisplayVK.textinput=create("input");GodotDisplayVK.textarea=create("textarea");GodotDisplayVK.updateSize()},show:function(text,type,start,end){if(!GodotDisplayVK.textinput||!GodotDisplayVK.textarea){return}if(GodotDisplayVK.textinput.style.display!==""||GodotDisplayVK.textarea.style.display!==""){GodotDisplayVK.hide()}GodotDisplayVK.updateSize();let elem=GodotDisplayVK.textinput;switch(type){case 0:elem.type="text";elem.inputmode="";break;case 1:elem=GodotDisplayVK.textarea;break;case 2:elem.type="text";elem.inputmode="numeric";break;case 3:elem.type="text";elem.inputmode="decimal";break;case 4:elem.type="tel";elem.inputmode="";break;case 5:elem.type="email";elem.inputmode="";break;case 6:elem.type="password";elem.inputmode="";break;case 7:elem.type="url";elem.inputmode="";break;default:elem.type="text";elem.inputmode="";break}elem.readonly=false;elem.disabled=false;elem.value=text;elem.style.display="block";elem.focus();elem.setSelectionRange(start,end)},hide:function(){if(!GodotDisplayVK.textinput||!GodotDisplayVK.textarea){return}[GodotDisplayVK.textinput,GodotDisplayVK.textarea].forEach(function(elem){elem.blur();elem.style.display="none";elem.value=""})},updateSize:function(){if(!GodotDisplayVK.textinput||!GodotDisplayVK.textarea){return}const rect=GodotConfig.canvas.getBoundingClientRect();function update(elem){elem.style.left=`${rect.left}px`;elem.style.top=`${rect.top}px`;elem.style.width=`${rect.width}px`;elem.style.height=`${rect.height}px`}update(GodotDisplayVK.textinput);update(GodotDisplayVK.textarea)},clear:function(){if(GodotDisplayVK.textinput){GodotDisplayVK.textinput.remove();GodotDisplayVK.textinput=null}if(GodotDisplayVK.textarea){GodotDisplayVK.textarea.remove();GodotDisplayVK.textarea=null}}};var GodotDisplay={window_icon:"",getDPI:function(){const dpi=Math.round(window.devicePixelRatio*96);return dpi>=96?dpi:96}};function _godot_js_display_alert(p_text){window.alert(GodotRuntime.parseString(p_text))}function _godot_js_display_canvas_focus(){GodotConfig.canvas.focus()}function _godot_js_display_canvas_is_focused(){return document.activeElement===GodotConfig.canvas}function _godot_js_display_clipboard_get(callback){const func=GodotRuntime.get_func(callback);try{navigator.clipboard.readText().then(function(result){const ptr=GodotRuntime.allocString(result);func(ptr);GodotRuntime.free(ptr)}).catch(function(e){})}catch(e){}}function _godot_js_display_clipboard_set(p_text){const text=GodotRuntime.parseString(p_text);if(!navigator.clipboard||!navigator.clipboard.writeText){return 1}navigator.clipboard.writeText(text).catch(function(e){GodotRuntime.error("Setting OS clipboard is only possible from an input callback for the Web platform. Exception:",e)});return 0}function _godot_js_display_cursor_is_hidden(){return!GodotDisplayCursor.visible}function _godot_js_display_cursor_is_locked(){return GodotDisplayCursor.isPointerLocked()?1:0}function _godot_js_display_cursor_lock_set(p_lock){if(p_lock){GodotDisplayCursor.lockPointer()}else{GodotDisplayCursor.releasePointer()}}function _godot_js_display_cursor_set_custom_shape(p_shape,p_ptr,p_len,p_hotspot_x,p_hotspot_y){const shape=GodotRuntime.parseString(p_shape);const old_shape=GodotDisplayCursor.cursors[shape];if(p_len>0){const png=new Blob([GodotRuntime.heapSlice(HEAPU8,p_ptr,p_len)],{type:"image/png"});const url=URL.createObjectURL(png);GodotDisplayCursor.cursors[shape]={url,x:p_hotspot_x,y:p_hotspot_y}}else{delete GodotDisplayCursor.cursors[shape]}if(shape===GodotDisplayCursor.shape){GodotDisplayCursor.set_shape(GodotDisplayCursor.shape)}if(old_shape){URL.revokeObjectURL(old_shape.url)}}function _godot_js_display_cursor_set_shape(p_string){GodotDisplayCursor.set_shape(GodotRuntime.parseString(p_string))}function _godot_js_display_cursor_set_visible(p_visible){const visible=p_visible!==0;if(visible===GodotDisplayCursor.visible){return}GodotDisplayCursor.visible=visible;if(visible){GodotDisplayCursor.set_shape(GodotDisplayCursor.shape)}else{GodotDisplayCursor.set_style("none")}}function _godot_js_display_desired_size_set(width,height){GodotDisplayScreen.desired_size=[width,height];GodotDisplayScreen.updateSize()}function _godot_js_display_fullscreen_cb(callback){const canvas=GodotConfig.canvas;const func=GodotRuntime.get_func(callback);function change_cb(evt){if(evt.target===canvas){func(GodotDisplayScreen.isFullscreen())}}GodotEventListeners.add(document,"fullscreenchange",change_cb,false);GodotEventListeners.add(document,"mozfullscreenchange",change_cb,false);GodotEventListeners.add(document,"webkitfullscreenchange",change_cb,false)}function _godot_js_display_fullscreen_exit(){return GodotDisplayScreen.exitFullscreen()}function _godot_js_display_fullscreen_request(){return GodotDisplayScreen.requestFullscreen()}function _godot_js_display_has_webgl(p_version){if(p_version!==1&&p_version!==2){return false}try{return!!document.createElement("canvas").getContext(p_version===2?"webgl2":"webgl")}catch(e){}return false}function _godot_js_display_is_swap_ok_cancel(){const win=["Windows","Win64","Win32","WinCE"];const plat=navigator.platform||"";if(win.indexOf(plat)!==-1){return 1}return 0}function _godot_js_display_notification_cb(callback,p_enter,p_exit,p_in,p_out){const canvas=GodotConfig.canvas;const func=GodotRuntime.get_func(callback);const notif=[p_enter,p_exit,p_in,p_out];["mouseover","mouseleave","focus","blur"].forEach(function(evt_name,idx){GodotEventListeners.add(canvas,evt_name,function(){func(notif[idx])},true)})}function _godot_js_display_pixel_ratio_get(){return GodotDisplayScreen.getPixelRatio()}function _godot_js_display_screen_dpi_get(){return GodotDisplay.getDPI()}function _godot_js_display_screen_size_get(width,height){const scale=GodotDisplayScreen.getPixelRatio();GodotRuntime.setHeapValue(width,window.screen.width*scale,"i32");GodotRuntime.setHeapValue(height,window.screen.height*scale,"i32")}function _godot_js_display_setup_canvas(p_width,p_height,p_fullscreen,p_hidpi){const canvas=GodotConfig.canvas;GodotEventListeners.add(canvas,"contextmenu",function(ev){ev.preventDefault()},false);GodotEventListeners.add(canvas,"webglcontextlost",function(ev){alert("WebGL context lost, please reload the page");ev.preventDefault()},false);GodotDisplayScreen.hidpi=!!p_hidpi;switch(GodotConfig.canvas_resize_policy){case 0:GodotDisplayScreen.desired_size=[canvas.width,canvas.height];break;case 1:GodotDisplayScreen.desired_size=[p_width,p_height];break;default:canvas.style.position="absolute";canvas.style.top=0;canvas.style.left=0;break}GodotDisplayScreen.updateSize();if(p_fullscreen){GodotDisplayScreen.requestFullscreen()}}function _godot_js_display_size_update(){const updated=GodotDisplayScreen.updateSize();if(updated){GodotDisplayVK.updateSize()}return updated}function _godot_js_display_touchscreen_is_available(){return"ontouchstart"in window}function _godot_js_display_tts_available(){return"speechSynthesis"in window}function _godot_js_display_vk_available(){return GodotDisplayVK.available()}function _godot_js_display_vk_cb(p_input_cb){const input_cb=GodotRuntime.get_func(p_input_cb);if(GodotDisplayVK.available()){GodotDisplayVK.init(input_cb)}}function _godot_js_display_vk_hide(){GodotDisplayVK.hide()}function _godot_js_display_vk_show(p_text,p_type,p_start,p_end){const text=GodotRuntime.parseString(p_text);const start=p_start>0?p_start:0;const end=p_end>0?p_end:start;GodotDisplayVK.show(text,p_type,start,end)}function _godot_js_display_window_blur_cb(callback){const func=GodotRuntime.get_func(callback);GodotEventListeners.add(window,"blur",function(){func()},false)}function _godot_js_display_window_icon_set(p_ptr,p_len){let link=document.getElementById("-gd-engine-icon");const old_icon=GodotDisplay.window_icon;if(p_ptr){if(link===null){link=document.createElement("link");link.rel="icon";link.id="-gd-engine-icon";document.head.appendChild(link)}const png=new Blob([GodotRuntime.heapSlice(HEAPU8,p_ptr,p_len)],{type:"image/png"});GodotDisplay.window_icon=URL.createObjectURL(png);link.href=GodotDisplay.window_icon}else{if(link){link.remove()}GodotDisplay.window_icon=null}if(old_icon){URL.revokeObjectURL(old_icon)}}function _godot_js_display_window_size_get(p_width,p_height){GodotRuntime.setHeapValue(p_width,GodotConfig.canvas.width,"i32");GodotRuntime.setHeapValue(p_height,GodotConfig.canvas.height,"i32")}function _godot_js_display_window_title_set(p_data){document.title=GodotRuntime.parseString(p_data)}function _godot_js_emscripten_get_version(){const emscriptenVersionPtr=GodotRuntime.allocString("4.0.10");return emscriptenVersionPtr}function _godot_js_eval(p_js,p_use_global_ctx,p_union_ptr,p_byte_arr,p_byte_arr_write,p_callback){const js_code=GodotRuntime.parseString(p_js);let eval_ret=null;try{if(p_use_global_ctx){const global_eval=eval;eval_ret=global_eval(js_code)}else{eval_ret=eval(js_code)}}catch(e){GodotRuntime.error(e)}switch(typeof eval_ret){case"boolean":GodotRuntime.setHeapValue(p_union_ptr,eval_ret,"i32");return 1;case"number":GodotRuntime.setHeapValue(p_union_ptr,eval_ret,"double");return 3;case"string":GodotRuntime.setHeapValue(p_union_ptr,GodotRuntime.allocString(eval_ret),"*");return 4;case"object":if(eval_ret===null){break}if(ArrayBuffer.isView(eval_ret)&&!(eval_ret instanceof Uint8Array)){eval_ret=new Uint8Array(eval_ret.buffer)}else if(eval_ret instanceof ArrayBuffer){eval_ret=new Uint8Array(eval_ret)}if(eval_ret instanceof Uint8Array){const func=GodotRuntime.get_func(p_callback);const bytes_ptr=func(p_byte_arr,p_byte_arr_write,eval_ret.length);HEAPU8.set(eval_ret,bytes_ptr);return 29}break}return 0}var IDHandler={_last_id:0,_references:{},get:function(p_id){return IDHandler._references[p_id]},add:function(p_data){const id=++IDHandler._last_id;IDHandler._references[id]=p_data;return id},remove:function(p_id){delete IDHandler._references[p_id]}};var GodotFetch={onread:function(id,result){const obj=IDHandler.get(id);if(!obj){return}if(result.value){obj.chunks.push(result.value)}obj.reading=false;obj.done=result.done},onresponse:function(id,response){const obj=IDHandler.get(id);if(!obj){return}let chunked=false;response.headers.forEach(function(value,header){const v=value.toLowerCase().trim();const h=header.toLowerCase().trim();if(h==="transfer-encoding"&&v==="chunked"){chunked=true}});obj.status=response.status;obj.response=response;obj.reader=response.body?.getReader();obj.chunked=chunked},onerror:function(id,err){GodotRuntime.error(err);const obj=IDHandler.get(id);if(!obj){return}obj.error=err},create:function(method,url,headers,body){const obj={request:null,response:null,reader:null,error:null,done:false,reading:false,status:0,chunks:[]};const id=IDHandler.add(obj);const init={method,headers,body};obj.request=fetch(url,init);obj.request.then(GodotFetch.onresponse.bind(null,id)).catch(GodotFetch.onerror.bind(null,id));return id},free:function(id){const obj=IDHandler.get(id);if(!obj){return}IDHandler.remove(id);if(!obj.request){return}obj.request.then(function(response){response.abort()}).catch(function(e){})},read:function(id){const obj=IDHandler.get(id);if(!obj){return}if(obj.reader&&!obj.reading){if(obj.done){obj.reader=null;return}obj.reading=true;obj.reader.read().then(GodotFetch.onread.bind(null,id)).catch(GodotFetch.onerror.bind(null,id))}else if(obj.reader==null&&obj.response.body==null){obj.reading=true;GodotFetch.onread(id,{value:undefined,done:true})}}};function _godot_js_fetch_create(p_method,p_url,p_headers,p_headers_size,p_body,p_body_size){const method=GodotRuntime.parseString(p_method);const url=GodotRuntime.parseString(p_url);const headers=GodotRuntime.parseStringArray(p_headers,p_headers_size);const body=p_body_size?GodotRuntime.heapSlice(HEAP8,p_body,p_body_size):null;return GodotFetch.create(method,url,headers.map(function(hv){const idx=hv.indexOf(":");if(idx<=0){return[]}return[hv.slice(0,idx).trim(),hv.slice(idx+1).trim()]}).filter(function(v){return v.length===2}),body)}function _godot_js_fetch_free(id){GodotFetch.free(id)}function _godot_js_fetch_http_status_get(p_id){const obj=IDHandler.get(p_id);if(!obj||!obj.response){return 0}return obj.status}function _godot_js_fetch_is_chunked(p_id){const obj=IDHandler.get(p_id);if(!obj||!obj.response){return-1}return obj.chunked?1:0}function _godot_js_fetch_read_chunk(p_id,p_buf,p_buf_size){const obj=IDHandler.get(p_id);if(!obj||!obj.response){return 0}let to_read=p_buf_size;const chunks=obj.chunks;while(to_read&&chunks.length){const chunk=obj.chunks[0];if(chunk.length>to_read){GodotRuntime.heapCopy(HEAP8,chunk.slice(0,to_read),p_buf);chunks[0]=chunk.slice(to_read);to_read=0}else{GodotRuntime.heapCopy(HEAP8,chunk,p_buf);to_read-=chunk.length;chunks.pop()}}if(!chunks.length){GodotFetch.read(p_id)}return p_buf_size-to_read}function _godot_js_fetch_read_headers(p_id,p_parse_cb,p_ref){const obj=IDHandler.get(p_id);if(!obj||!obj.response){return 1}const cb=GodotRuntime.get_func(p_parse_cb);const arr=[];obj.response.headers.forEach(function(v,h){arr.push(`${h}:${v}`)});const c_ptr=GodotRuntime.allocStringArray(arr);cb(arr.length,c_ptr,p_ref);GodotRuntime.freeStringArray(c_ptr,arr.length);return 0}function _godot_js_fetch_state_get(p_id){const obj=IDHandler.get(p_id);if(!obj){return-1}if(obj.error){return-1}if(!obj.response){return 0}if(obj.reader||obj.response.body==null&&!obj.done){return 1}if(obj.done){return 2}return-1}var GodotInputGamepads={samples:[],get_pads:function(){try{const pads=navigator.getGamepads();if(pads){return pads}return[]}catch(e){return[]}},get_samples:function(){return GodotInputGamepads.samples},get_sample:function(index){const samples=GodotInputGamepads.samples;return index=0){os="Android"}else if(ua.indexOf("Linux")>=0){os="Linux"}else if(ua.indexOf("iPhone")>=0){os="iOS"}else if(ua.indexOf("Macintosh")>=0){os="MacOSX"}else if(ua.indexOf("Windows")>=0){os="Windows"}const id=pad.id;const exp1=/vendor: ([0-9a-f]{4}) product: ([0-9a-f]{4})/i;const exp2=/^([0-9a-f]+)-([0-9a-f]+)-/i;let vendor="";let product="";if(exp1.test(id)){const match=exp1.exec(id);vendor=match[1].padStart(4,"0");product=match[2].padStart(4,"0")}else if(exp2.test(id)){const match=exp2.exec(id);vendor=match[1].padStart(4,"0");product=match[2].padStart(4,"0")}if(!vendor||!product){return`${os}Unknown`}return os+vendor+product}};var GodotInputDragDrop={promises:[],pending_files:[],add_entry:function(entry){if(entry.isDirectory){GodotInputDragDrop.add_dir(entry)}else if(entry.isFile){GodotInputDragDrop.add_file(entry)}else{GodotRuntime.error("Unrecognized entry...",entry)}},add_dir:function(entry){GodotInputDragDrop.promises.push(new Promise(function(resolve,reject){const reader=entry.createReader();reader.readEntries(function(entries){for(let i=0;i{const path=elem["path"];GodotFS.copy_to_fs(DROP+path,elem["data"]);let idx=path.indexOf("/");if(idx===-1){drops.push(DROP+path)}else{const sub=path.substr(0,idx);idx=sub.indexOf("/");if(idx<0&&drops.indexOf(DROP+sub)===-1){drops.push(DROP+sub)}}files.push(DROP+path)});GodotInputDragDrop.promises=[];GodotInputDragDrop.pending_files=[];callback(drops);if(GodotConfig.persistent_drops){GodotOS.atexit(function(resolve,reject){GodotInputDragDrop.remove_drop(files,DROP);resolve()})}else{GodotInputDragDrop.remove_drop(files,DROP)}})},remove_drop:function(files,drop_path){const dirs=[drop_path.substr(0,drop_path.length-1)];files.forEach(function(file){FS.unlink(file);let dir=file.replace(drop_path,"");let idx=dir.lastIndexOf("/");while(idx>0){dir=dir.substr(0,idx);if(dirs.indexOf(drop_path+dir)===-1){dirs.push(drop_path+dir)}idx=dir.lastIndexOf("/")}});dirs.sort(function(a,b){const al=(a.match(/\//g)||[]).length;const bl=(b.match(/\//g)||[]).length;if(al>bl){return-1}else if(al-1){clearFocusTimerInterval()}if(GodotIME.ime==null){return}GodotIME.active=active;if(active){GodotIME.ime.style.display="block";GodotIME.focusTimerIntervalId=setInterval(focusTimer,100)}else{GodotIME.ime.style.display="none";GodotConfig.canvas.focus()}},ime_position:function(x,y){if(GodotIME.ime==null){return}const canvas=GodotConfig.canvas;const rect=canvas.getBoundingClientRect();const rw=canvas.width/rect.width;const rh=canvas.height/rect.height;const clx=x/rw+rect.x;const cly=y/rh+rect.y;GodotIME.ime.style.left=`${clx}px`;GodotIME.ime.style.top=`${cly}px`},init:function(ime_cb,key_cb,code,key){function key_event_cb(pressed,evt){const modifiers=GodotIME.getModifiers(evt);GodotRuntime.stringToHeap(evt.code,code,32);GodotRuntime.stringToHeap(evt.key,key,32);key_cb(pressed,evt.repeat,modifiers);evt.preventDefault()}function ime_event_cb(event){if(GodotIME.ime==null){return}switch(event.type){case"compositionstart":ime_cb(0,null);GodotIME.ime.innerHTML="";break;case"compositionupdate":{const ptr=GodotRuntime.allocString(event.data);ime_cb(1,ptr);GodotRuntime.free(ptr)}break;case"compositionend":{const ptr=GodotRuntime.allocString(event.data);ime_cb(2,ptr);GodotRuntime.free(ptr);GodotIME.ime.innerHTML=""}break;default:}}const ime=document.createElement("div");ime.className="ime";ime.style.background="none";ime.style.opacity=0;ime.style.position="fixed";ime.style.textAlign="left";ime.style.fontSize="1px";ime.style.left="0px";ime.style.top="0px";ime.style.width="100%";ime.style.height="40px";ime.style.pointerEvents="none";ime.style.display="none";ime.contentEditable="true";GodotEventListeners.add(ime,"compositionstart",ime_event_cb,false);GodotEventListeners.add(ime,"compositionupdate",ime_event_cb,false);GodotEventListeners.add(ime,"compositionend",ime_event_cb,false);GodotEventListeners.add(ime,"keydown",key_event_cb.bind(null,1),false);GodotEventListeners.add(ime,"keyup",key_event_cb.bind(null,0),false);ime.onblur=function(){this.style.display="none";GodotConfig.canvas.focus();GodotIME.active=false};GodotConfig.canvas.parentElement.appendChild(ime);GodotIME.ime=ime},clear:function(){if(GodotIME.ime==null){return}if(GodotIME.focusTimerIntervalId>-1){clearInterval(GodotIME.focusTimerIntervalId);GodotIME.focusTimerIntervalId=-1}GodotIME.ime.remove();GodotIME.ime=null}};var GodotInput={getModifiers:function(evt){return evt.shiftKey+0+(evt.altKey+0<<1)+(evt.ctrlKey+0<<2)+(evt.metaKey+0<<3)},computePosition:function(evt,rect){const canvas=GodotConfig.canvas;const rw=canvas.width/rect.width;const rh=canvas.height/rect.height;const x=(evt.clientX-rect.x)*rw;const y=(evt.clientY-rect.y)*rh;return[x,y]}};function _godot_js_input_drop_files_cb(callback){const func=GodotRuntime.get_func(callback);const dropFiles=function(files){const args=files||[];if(!args.length){return}const argc=args.length;const argv=GodotRuntime.allocStringArray(args);func(argv,argc);GodotRuntime.freeStringArray(argv,argc)};const canvas=GodotConfig.canvas;GodotEventListeners.add(canvas,"dragover",function(ev){ev.preventDefault()},false);GodotEventListeners.add(canvas,"drop",GodotInputDragDrop.handler(dropFiles))}function _godot_js_input_gamepad_cb(change_cb){const onchange=GodotRuntime.get_func(change_cb);GodotInputGamepads.init(onchange)}function _godot_js_input_gamepad_sample(){GodotInputGamepads.sample();return 0}function _godot_js_input_gamepad_sample_count(){return GodotInputGamepads.get_samples().length}function _godot_js_input_gamepad_sample_get(p_index,r_btns,r_btns_num,r_axes,r_axes_num,r_standard){const sample=GodotInputGamepads.get_sample(p_index);if(!sample||!sample.connected){return 1}const btns=sample.buttons;const btns_len=btns.length<16?btns.length:16;for(let i=0;i{const inputs=[...midi.inputs.values()];const inputNames=inputs.map(input=>input.name);const c_ptr=GodotRuntime.allocStringArray(inputNames);setInputNamesCb(inputNames.length,c_ptr);GodotRuntime.freeStringArray(c_ptr,inputNames.length);inputs.forEach((input,i)=>{const abortController=new AbortController;GodotWebMidi.abortControllers.push(abortController);input.addEventListener("midimessage",event=>{const status=event.data[0];const data=event.data.slice(1);const size=data.length;if(size>dataBufferLen){throw new Error(`data too big ${size} > ${dataBufferLen}`)}HEAPU8.set(data,pDataBuffer);onMidiMessageCb(i,status,pDataBuffer,data.length)},{signal:abortController.signal})})});return 0}var GodotWebSocket={_onopen:function(p_id,callback,event){const ref=IDHandler.get(p_id);if(!ref){return}const c_str=GodotRuntime.allocString(ref.protocol);callback(c_str);GodotRuntime.free(c_str)},_onmessage:function(p_id,callback,event){const ref=IDHandler.get(p_id);if(!ref){return}let buffer;let is_string=0;if(event.data instanceof ArrayBuffer){buffer=new Uint8Array(event.data)}else if(event.data instanceof Blob){GodotRuntime.error("Blob type not supported");return}else if(typeof event.data==="string"){is_string=1;buffer=new TextEncoder("utf-8").encode(event.data)}else{GodotRuntime.error("Unknown message type");return}const len=buffer.length*buffer.BYTES_PER_ELEMENT;const out=GodotRuntime.malloc(len);HEAPU8.set(buffer,out);callback(out,len,is_string);GodotRuntime.free(out)},_onerror:function(p_id,callback,event){const ref=IDHandler.get(p_id);if(!ref){return}callback()},_onclose:function(p_id,callback,event){const ref=IDHandler.get(p_id);if(!ref){return}const c_str=GodotRuntime.allocString(event.reason);callback(event.code,c_str,event.wasClean?1:0);GodotRuntime.free(c_str)},send:function(p_id,p_data){const ref=IDHandler.get(p_id);if(!ref||ref.readyState!==ref.OPEN){return 1}ref.send(p_data);return 0},bufferedAmount:function(p_id){const ref=IDHandler.get(p_id);if(!ref){return 0}return ref.bufferedAmount},create:function(socket,p_on_open,p_on_message,p_on_error,p_on_close){const id=IDHandler.add(socket);socket.onopen=GodotWebSocket._onopen.bind(null,id,p_on_open);socket.onmessage=GodotWebSocket._onmessage.bind(null,id,p_on_message);socket.onerror=GodotWebSocket._onerror.bind(null,id,p_on_error);socket.onclose=GodotWebSocket._onclose.bind(null,id,p_on_close);return id},close:function(p_id,p_code,p_reason){const ref=IDHandler.get(p_id);if(ref&&ref.readyState=Number.MIN_SAFE_INTEGER&&heap_value<=Number.MAX_SAFE_INTEGER?Number(heap_value):heap_value}case 3:return Number(GodotRuntime.getHeapValue(val,"double"));case 4:return GodotRuntime.parseString(GodotRuntime.getHeapValue(val,"*"));case 24:return GodotJSWrapper.get_proxied_value(GodotRuntime.getHeapValue(val,"i64"));default:return undefined}},js2variant:function(p_val,p_exchange){if(p_val===undefined||p_val===null){return 0}const type=typeof p_val;if(type==="boolean"){GodotRuntime.setHeapValue(p_exchange,p_val,"i64");return 1}else if(type==="number"){if(Number.isInteger(p_val)){GodotRuntime.setHeapValue(p_exchange,p_val,"i64");return 2}GodotRuntime.setHeapValue(p_exchange,p_val,"double");return 3}else if(type==="bigint"){GodotRuntime.setHeapValue(p_exchange,p_val,"i64");return 2}else if(type==="string"){const c_str=GodotRuntime.allocString(p_val);GodotRuntime.setHeapValue(p_exchange,c_str,"*");return 4}const id=GodotJSWrapper.get_proxied(p_val);GodotRuntime.setHeapValue(p_exchange,id,"i64");return 24},isBuffer:function(obj){return obj instanceof ArrayBuffer||ArrayBuffer.isView(obj)}};function _godot_js_wrapper_create_cb(p_ref,p_func){const func=GodotRuntime.get_func(p_func);let id=0;const cb=function(){if(!GodotJSWrapper.get_proxied_value(id)){return undefined}GodotJSWrapper.cb_ret=null;const args=Array.from(arguments);const argsProxy=new GodotJSWrapper.MyProxy(args);func(p_ref,argsProxy.get_id(),args.length);argsProxy.unref();const ret=GodotJSWrapper.cb_ret;GodotJSWrapper.cb_ret=null;return ret};id=GodotJSWrapper.get_proxied(cb);return id}function _godot_js_wrapper_create_object(p_object,p_args,p_argc,p_convert_callback,p_exchange,p_lock,p_free_lock_callback){const name=GodotRuntime.parseString(p_object);if(typeof window[name]==="undefined"){return-1}const convert=GodotRuntime.get_func(p_convert_callback);const freeLock=GodotRuntime.get_func(p_free_lock_callback);const args=new Array(p_argc);for(let i=0;i{if(GodotWebXR.session&&GodotWebXR.space){const onFrame=function(time,frame){GodotWebXR.frame=frame;GodotWebXR.pose=frame.getViewerPose(GodotWebXR.space);callback(time);GodotWebXR.frame=null;GodotWebXR.pose=null};GodotWebXR.session.requestAnimationFrame(onFrame)}else{GodotWebXR.orig_requestAnimationFrame(callback)}},monkeyPatchRequestAnimationFrame:enable=>{if(GodotWebXR.orig_requestAnimationFrame===null){GodotWebXR.orig_requestAnimationFrame=MainLoop.requestAnimationFrame}MainLoop.requestAnimationFrame=enable?GodotWebXR.requestAnimationFrame:GodotWebXR.orig_requestAnimationFrame},pauseResumeMainLoop:()=>{MainLoop.pause();runtimeKeepalivePush();window.setTimeout(function(){runtimeKeepalivePop();MainLoop.resume()},0)},getLayer:()=>{const new_view_count=GodotWebXR.pose?GodotWebXR.pose.views.length:1;let layer=GodotWebXR.layer;if(layer&&GodotWebXR.view_count===new_view_count){return layer}if(!GodotWebXR.session||!GodotWebXR.gl_binding||!GodotWebXR.gl_binding.createProjectionLayer){return null}const gl=GodotWebXR.gl;layer=GodotWebXR.gl_binding.createProjectionLayer({textureType:new_view_count>1?"texture-array":"texture",colorFormat:gl.RGBA8,depthFormat:gl.DEPTH_COMPONENT24});GodotWebXR.session.updateRenderState({layers:[layer]});GodotWebXR.layer=layer;GodotWebXR.view_count=new_view_count;return layer},getSubImage:()=>{if(!GodotWebXR.pose){return null}const layer=GodotWebXR.getLayer();if(layer===null){return null}return GodotWebXR.gl_binding.getViewSubImage(layer,GodotWebXR.pose.views[0])},getTextureId:texture=>{if(texture.name!==undefined){return texture.name}const id=GL.getNewId(GL.textures);texture.name=id;GL.textures[id]=texture;return id},addInputSource:input_source=>{let name=-1;if(input_source.targetRayMode==="tracked-pointer"&&input_source.handedness==="left"){name=0}else if(input_source.targetRayMode==="tracked-pointer"&&input_source.handedness==="right"){name=1}else{for(let i=2;i<16;i++){if(!GodotWebXR.input_sources[i]){name=i;break}}}if(name>=0){GodotWebXR.input_sources[name]=input_source;input_source.name=name;if(input_source.targetRayMode==="screen"){let touch_index=-1;for(let i=0;i<5;i++){if(!GodotWebXR.touches[i]){touch_index=i;break}}if(touch_index>=0){GodotWebXR.touches[touch_index]=input_source;input_source.touch_index=touch_index}}}return name},removeInputSource:input_source=>{if(input_source.name!==undefined){const name=input_source.name;if(name>=0&&name<16){GodotWebXR.input_sources[name]=null}if(input_source.touch_index!==undefined){const touch_index=input_source.touch_index;if(touch_index>=0&&touch_index<5){GodotWebXR.touches[touch_index]=null}}return name}return-1},getInputSourceId:input_source=>{if(input_source!==undefined){return input_source.name}return-1},getTouchIndex:input_source=>{if(input_source.touch_index!==undefined){return input_source.touch_index}return-1}};function _godot_webxr_get_bounds_geometry(r_points){if(!GodotWebXR.space||!GodotWebXR.space.boundsGeometry){return 0}const point_count=GodotWebXR.space.boundsGeometry.length;if(point_count===0){return 0}const buf=GodotRuntime.malloc(point_count*3*4);for(let i=0;i=0){matrix=views[p_view].transform.matrix}else{matrix=GodotWebXR.pose.transform.matrix}for(let i=0;i<16;i++){GodotRuntime.setHeapValue(r_transform+i*4,matrix[i],"float")}return true}function _godot_webxr_get_velocity_texture(){const subimage=GodotWebXR.getSubImage();if(subimage===null){return 0}if(!subimage.motionVectorTexture){return 0}return GodotWebXR.getTextureId(subimage.motionVectorTexture)}function _godot_webxr_get_view_count(){if(!GodotWebXR.session||!GodotWebXR.pose){return 1}const view_count=GodotWebXR.pose.views.length;return view_count>0?view_count:1}function _godot_webxr_get_visibility_state(){if(!GodotWebXR.session||!GodotWebXR.session.visibilityState){return 0}return GodotRuntime.allocString(GodotWebXR.session.visibilityState)}var _godot_webxr_initialize=function(p_session_mode,p_required_features,p_optional_features,p_requested_reference_spaces,p_on_session_started,p_on_session_ended,p_on_session_failed,p_on_input_event,p_on_simple_event){GodotWebXR.monkeyPatchRequestAnimationFrame(true);const session_mode=GodotRuntime.parseString(p_session_mode);const required_features=GodotRuntime.parseString(p_required_features).split(",").map(s=>s.trim()).filter(s=>s!=="");const optional_features=GodotRuntime.parseString(p_optional_features).split(",").map(s=>s.trim()).filter(s=>s!=="");const requested_reference_space_types=GodotRuntime.parseString(p_requested_reference_spaces).split(",").map(s=>s.trim());const onstarted=GodotRuntime.get_func(p_on_session_started);const onended=GodotRuntime.get_func(p_on_session_ended);const onfailed=GodotRuntime.get_func(p_on_session_failed);const oninputevent=GodotRuntime.get_func(p_on_input_event);const onsimpleevent=GodotRuntime.get_func(p_on_simple_event);const session_init={};if(required_features.length>0){session_init["requiredFeatures"]=required_features}if(optional_features.length>0){session_init["optionalFeatures"]=optional_features}navigator.xr.requestSession(session_mode,session_init).then(function(session){GodotWebXR.session=session;session.addEventListener("end",function(evt){onended()});session.addEventListener("inputsourceschange",function(evt){evt.added.forEach(GodotWebXR.addInputSource);evt.removed.forEach(GodotWebXR.removeInputSource)});["selectstart","selectend","squeezestart","squeezeend"].forEach((input_event,index)=>{session.addEventListener(input_event,function(evt){GodotWebXR.frame=evt.frame;oninputevent(index,GodotWebXR.getInputSourceId(evt.inputSource));GodotWebXR.frame=null})});session.addEventListener("visibilitychange",function(evt){const c_str=GodotRuntime.allocString("visibility_state_changed");onsimpleevent(c_str);GodotRuntime.free(c_str)});GodotWebXR.onsimpleevent=onsimpleevent;const gl_context_handle=_emscripten_webgl_get_current_context();const gl=GL.getContext(gl_context_handle).GLctx;GodotWebXR.gl=gl;gl.makeXRCompatible().then(function(){const throwNoWebXRLayersError=()=>{throw new Error("This browser doesn't support WebXR Layers (which Godot requires) nor is the polyfill in use. If you are the developer of this application, please consider including the polyfill.")};try{GodotWebXR.gl_binding=new XRWebGLBinding(session,gl)}catch(error){throwNoWebXRLayersError()}if(!GodotWebXR.gl_binding.createProjectionLayer){throwNoWebXRLayersError()}const layer=GodotWebXR.getLayer();if(!layer){throw new Error("Unable to create WebXR Layer.")}function onReferenceSpaceSuccess(reference_space,reference_space_type){GodotWebXR.space=reference_space;reference_space.onreset=function(evt){const c_str=GodotRuntime.allocString("reference_space_reset");onsimpleevent(c_str);GodotRuntime.free(c_str)};GodotWebXR.pauseResumeMainLoop();window.setTimeout(function(){const reference_space_c_str=GodotRuntime.allocString(reference_space_type);const enabled_features="enabledFeatures"in session?Array.from(session.enabledFeatures):[];const enabled_features_c_str=GodotRuntime.allocString(enabled_features.join(","));const environment_blend_mode="environmentBlendMode"in session?session.environmentBlendMode:"";const environment_blend_mode_c_str=GodotRuntime.allocString(environment_blend_mode);onstarted(reference_space_c_str,enabled_features_c_str,environment_blend_mode_c_str);GodotRuntime.free(reference_space_c_str);GodotRuntime.free(enabled_features_c_str);GodotRuntime.free(environment_blend_mode_c_str)},0)}function requestReferenceSpace(){const reference_space_type=requested_reference_space_types.shift();session.requestReferenceSpace(reference_space_type).then(refSpace=>{onReferenceSpaceSuccess(refSpace,reference_space_type)}).catch(()=>{if(requested_reference_space_types.length===0){const c_str=GodotRuntime.allocString("Unable to get any of the requested reference space types");onfailed(c_str);GodotRuntime.free(c_str)}else{requestReferenceSpace()}})}requestReferenceSpace()}).catch(function(error){const c_str=GodotRuntime.allocString(`Unable to make WebGL context compatible with WebXR: ${error}`);onfailed(c_str);GodotRuntime.free(c_str)})}).catch(function(error){const c_str=GodotRuntime.allocString(`Unable to start session: ${error}`);onfailed(c_str);GodotRuntime.free(c_str)})};function _godot_webxr_is_session_supported(p_session_mode,p_callback){const session_mode=GodotRuntime.parseString(p_session_mode);const cb=GodotRuntime.get_func(p_callback);if(navigator.xr){navigator.xr.isSessionSupported(session_mode).then(function(supported){const c_str=GodotRuntime.allocString(session_mode);cb(c_str,supported?1:0);GodotRuntime.free(c_str)})}else{const c_str=GodotRuntime.allocString(session_mode);cb(c_str,0);GodotRuntime.free(c_str)}}function _godot_webxr_is_supported(){return!!navigator.xr}var _godot_webxr_uninitialize=function(){if(GodotWebXR.session){GodotWebXR.session.end().catch(e=>{})}GodotWebXR.session=null;GodotWebXR.gl_binding=null;GodotWebXR.layer=null;GodotWebXR.space=null;GodotWebXR.frame=null;GodotWebXR.pose=null;GodotWebXR.view_count=1;GodotWebXR.input_sources=new Array(16);GodotWebXR.touches=new Array(5);GodotWebXR.onsimpleevent=null;GodotWebXR.monkeyPatchRequestAnimationFrame(false);GodotWebXR.pauseResumeMainLoop()};function _godot_webxr_update_input_source(p_input_source_id,r_target_pose,r_target_ray_mode,r_touch_index,r_has_grip_pose,r_grip_pose,r_has_standard_mapping,r_button_count,r_buttons,r_axes_count,r_axes,r_has_hand_data,r_hand_joints,r_hand_radii){if(!GodotWebXR.session||!GodotWebXR.frame){return 0}if(p_input_source_id<0||p_input_source_id>=GodotWebXR.input_sources.length||!GodotWebXR.input_sources[p_input_source_id]){return false}const input_source=GodotWebXR.input_sources[p_input_source_id];const frame=GodotWebXR.frame;const space=GodotWebXR.space;const target_pose=frame.getPose(input_source.targetRaySpace,space);if(!target_pose){return false}const target_pose_matrix=target_pose.transform.matrix;for(let i=0;i<16;i++){GodotRuntime.setHeapValue(r_target_pose+i*4,target_pose_matrix[i],"float")}let target_ray_mode=0;switch(input_source.targetRayMode){case"gaze":target_ray_mode=1;break;case"tracked-pointer":target_ray_mode=2;break;case"screen":target_ray_mode=3;break;default:}GodotRuntime.setHeapValue(r_target_ray_mode,target_ray_mode,"i32");GodotRuntime.setHeapValue(r_touch_index,GodotWebXR.getTouchIndex(input_source),"i32");let has_grip_pose=false;if(input_source.gripSpace){const grip_pose=frame.getPose(input_source.gripSpace,space);if(grip_pose){const grip_pose_matrix=grip_pose.transform.matrix;for(let i=0;i<16;i++){GodotRuntime.setHeapValue(r_grip_pose+i*4,grip_pose_matrix[i],"float")}has_grip_pose=true}}GodotRuntime.setHeapValue(r_has_grip_pose,has_grip_pose?1:0,"i32");let has_standard_mapping=false;let button_count=0;let axes_count=0;if(input_source.gamepad){if(input_source.gamepad.mapping==="xr-standard"){has_standard_mapping=true}button_count=Math.min(input_source.gamepad.buttons.length,10);for(let i=0;i{const c_str=GodotRuntime.allocString("display_refresh_rate_changed");GodotWebXR.onsimpleevent(c_str);GodotRuntime.free(c_str)})}var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var getCFunc=ident=>{var func=Module["_"+ident];assert(func,"Cannot call unknown function "+ident+", make sure it is exported");return func};var writeArrayToMemory=(array,buffer)=>{assert(array.length>=0,"writeArrayToMemory array must have a length (should be an array or typed array)");HEAP8.set(array,buffer)};var stackSave=()=>_emscripten_stack_get_current();var stackRestore=val=>__emscripten_stack_restore(val);var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;assert(returnType!=="array",'Return type should not be "array".');if(args){for(var i=0;i(...args)=>ccall(ident,returnType,argTypes,args,opts);FS.createPreloadedFile=FS_createPreloadedFile;FS.staticInit();Module["requestAnimationFrame"]=MainLoop.requestAnimationFrame;Module["pauseMainLoop"]=MainLoop.pause;Module["resumeMainLoop"]=MainLoop.resume;MainLoop.init();for(let i=0;i<32;++i)tempFixedLengthArray.push(new Array(i));var miniTempWebGLIntBuffersStorage=new Int32Array(288);for(var i=0;i<=288;++i){miniTempWebGLIntBuffers[i]=miniTempWebGLIntBuffersStorage.subarray(0,i)}var miniTempWebGLFloatBuffersStorage=new Float32Array(288);for(var i=0;i<=288;++i){miniTempWebGLFloatBuffers[i]=miniTempWebGLFloatBuffersStorage.subarray(0,i)}Module["request_quit"]=function(){GodotOS.request_quit()};Module["onExit"]=GodotOS.cleanup;GodotOS._fs_sync_promise=Promise.resolve();Module["initConfig"]=GodotConfig.init_config;Module["initFS"]=GodotFS.init;Module["copyToFS"]=GodotFS.copy_to_fs;GodotOS.atexit(function(resolve,reject){GodotDisplayCursor.clear();resolve()});GodotOS.atexit(function(resolve,reject){GodotEventListeners.clear();resolve()});GodotOS.atexit(function(resolve,reject){GodotDisplayVK.clear();resolve()});GodotOS.atexit(function(resolve,reject){GodotIME.clear();resolve()});GodotJSWrapper.proxies=new Map;{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["preloadPlugins"])preloadPlugins=Module["preloadPlugins"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];checkIncomingModuleAPI();if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];assert(typeof Module["memoryInitializerPrefixURL"]=="undefined","Module.memoryInitializerPrefixURL option was removed, use Module.locateFile instead");assert(typeof Module["pthreadMainPrefixURL"]=="undefined","Module.pthreadMainPrefixURL option was removed, use Module.locateFile instead");assert(typeof Module["cdInitializerPrefixURL"]=="undefined","Module.cdInitializerPrefixURL option was removed, use Module.locateFile instead");assert(typeof Module["filePackagePrefixURL"]=="undefined","Module.filePackagePrefixURL option was removed, use Module.locateFile instead");assert(typeof Module["read"]=="undefined","Module.read option was removed");assert(typeof Module["readAsync"]=="undefined","Module.readAsync option was removed (modify readAsync in JS)");assert(typeof Module["readBinary"]=="undefined","Module.readBinary option was removed (modify readBinary in JS)");assert(typeof Module["setWindowTitle"]=="undefined","Module.setWindowTitle option was removed (modify emscripten_set_window_title in JS)");assert(typeof Module["TOTAL_MEMORY"]=="undefined","Module.TOTAL_MEMORY has been renamed Module.INITIAL_MEMORY");assert(typeof Module["ENVIRONMENT"]=="undefined","Module.ENVIRONMENT has been deprecated. To force the environment, use the ENVIRONMENT compile-time option (for example, -sENVIRONMENT=web or -sENVIRONMENT=node)");assert(typeof Module["STACK_SIZE"]=="undefined","STACK_SIZE can no longer be set at runtime. Use -sSTACK_SIZE at link time");assert(typeof Module["wasmMemory"]=="undefined","Use of `wasmMemory` detected. Use -sIMPORTED_MEMORY to define wasmMemory externally");assert(typeof Module["INITIAL_MEMORY"]=="undefined","Detected runtime INITIAL_MEMORY setting. Use -sIMPORTED_MEMORY to define wasmMemory dynamically")}Module["callMain"]=callMain;Module["cwrap"]=cwrap;var missingLibrarySymbols=["writeI53ToI64Clamped","writeI53ToI64Signaling","writeI53ToU64Clamped","writeI53ToU64Signaling","convertI32PairToI53","convertI32PairToI53Checked","convertU32PairToI53","getTempRet0","setTempRet0","zeroMemory","withStackSave","inetPton4","inetNtop4","inetPton6","inetNtop6","readSockaddr","writeSockaddr","emscriptenLog","readEmAsmArgs","autoResumeAudioContext","getDynCaller","dynCall","setWasmTableEntry","asmjsMangle","HandleAllocator","getNativeTypeSize","addOnInit","addOnPostCtor","addOnPreMain","addOnExit","STACK_SIZE","STACK_ALIGN","POINTER_SIZE","ASSERTIONS","uleb128Encode","sigToWasmTypes","generateFuncType","convertJsFunctionToWasm","getEmptyTableSlot","updateTableMap","getFunctionAddress","addFunction","removeFunction","reallyNegative","unSign","strLen","reSign","formatString","intArrayToString","AsciiToString","stringToAscii","UTF16ToString","stringToUTF16","lengthBytesUTF16","UTF32ToString","stringToUTF32","lengthBytesUTF32","registerKeyEventCallback","getBoundingClientRect","fillMouseEventData","registerMouseEventCallback","registerWheelEventCallback","registerUiEventCallback","registerFocusEventCallback","fillDeviceOrientationEventData","registerDeviceOrientationEventCallback","fillDeviceMotionEventData","registerDeviceMotionEventCallback","screenOrientation","fillOrientationChangeEventData","registerOrientationChangeEventCallback","fillFullscreenChangeEventData","registerFullscreenChangeEventCallback","JSEvents_requestFullscreen","JSEvents_resizeCanvasForFullscreen","registerRestoreOldStyle","hideEverythingExceptGivenElement","restoreHiddenElements","setLetterbox","softFullscreenResizeWebGLRenderTarget","doRequestFullscreen","fillPointerlockChangeEventData","registerPointerlockChangeEventCallback","registerPointerlockErrorEventCallback","requestPointerLock","fillVisibilityChangeEventData","registerVisibilityChangeEventCallback","registerTouchEventCallback","fillGamepadEventData","registerGamepadEventCallback","registerBeforeUnloadEventCallback","fillBatteryEventData","battery","registerBatteryEventCallback","setCanvasElementSize","getCanvasElementSize","jsStackTrace","getCallstack","convertPCtoSourceLocation","wasiRightsToMuslOFlags","wasiOFlagsToMuslOFlags","safeSetTimeout","setImmediateWrapped","safeRequestAnimationFrame","clearImmediateWrapped","registerPostMainLoop","registerPreMainLoop","getPromise","makePromise","idsToPromises","makePromiseCallback","Browser_asyncPrepareDataCounter","arraySum","addDays","getSocketFromFD","getSocketAddress","FS_mkdirTree","_setNetworkCallback","emscriptenWebGLGetUniform","emscriptenWebGLGetVertexAttrib","__glGetActiveAttribOrUniform","writeGLArray","registerWebGlEventCallback","runAndAbortIfError","emscriptenWebGLGetIndexed","ALLOC_NORMAL","ALLOC_STACK","allocate","writeStringToMemory","writeAsciiToMemory","demangle","stackTrace"];missingLibrarySymbols.forEach(missingLibrarySymbol);var unexportedSymbols=["run","addRunDependency","removeRunDependency","out","err","abort","wasmMemory","wasmExports","writeStackCookie","checkStackCookie","writeI53ToI64","readI53FromI64","readI53FromU64","INT53_MAX","INT53_MIN","bigintToI53Checked","stackSave","stackRestore","stackAlloc","ptrToString","exitJS","getHeapMax","growMemory","ENV","ERRNO_CODES","strError","DNS","Protocols","Sockets","timers","warnOnce","readEmAsmArgsArray","jstoi_q","getExecutableName","getWasmTableEntry","handleException","keepRuntimeAlive","runtimeKeepalivePush","runtimeKeepalivePop","callUserCallback","maybeExit","asyncLoad","alignMemory","mmapAlloc","wasmTable","getUniqueRunDependency","noExitRuntime","addOnPreRun","addOnPostRun","ccall","freeTableIndexes","functionsInTableMap","setValue","getValue","PATH","PATH_FS","UTF8Decoder","UTF8ArrayToString","UTF8ToString","stringToUTF8Array","stringToUTF8","lengthBytesUTF8","intArrayFromString","UTF16Decoder","stringToNewUTF8","stringToUTF8OnStack","writeArrayToMemory","JSEvents","specialHTMLTargets","maybeCStringToJsString","findEventTarget","findCanvasEventTarget","currentFullscreenStrategy","restoreOldWindowedStyle","UNWIND_CACHE","ExitStatus","getEnvStrings","checkWasiClock","doReadv","doWritev","initRandomFill","randomFill","emSetImmediate","emClearImmediate_deps","emClearImmediate","promiseMap","Browser","requestFullscreen","requestFullScreen","setCanvasSize","getUserMedia","createContext","getPreloadedImageData__data","wget","MONTH_DAYS_REGULAR","MONTH_DAYS_LEAP","MONTH_DAYS_REGULAR_CUMULATIVE","MONTH_DAYS_LEAP_CUMULATIVE","isLeapYear","ydayFromDate","SYSCALLS","preloadPlugins","FS_createPreloadedFile","FS_modeStringToFlags","FS_getMode","FS_stdin_getChar_buffer","FS_stdin_getChar","FS_unlink","FS_createPath","FS_createDevice","FS_readFile","FS","FS_root","FS_mounts","FS_devices","FS_streams","FS_nextInode","FS_nameTable","FS_currentPath","FS_initialized","FS_ignorePermissions","FS_filesystems","FS_syncFSRequests","FS_readFiles","FS_lookupPath","FS_getPath","FS_hashName","FS_hashAddNode","FS_hashRemoveNode","FS_lookupNode","FS_createNode","FS_destroyNode","FS_isRoot","FS_isMountpoint","FS_isFile","FS_isDir","FS_isLink","FS_isChrdev","FS_isBlkdev","FS_isFIFO","FS_isSocket","FS_flagsToPermissionString","FS_nodePermissions","FS_mayLookup","FS_mayCreate","FS_mayDelete","FS_mayOpen","FS_checkOpExists","FS_nextfd","FS_getStreamChecked","FS_getStream","FS_createStream","FS_closeStream","FS_dupStream","FS_doSetAttr","FS_chrdev_stream_ops","FS_major","FS_minor","FS_makedev","FS_registerDevice","FS_getDevice","FS_getMounts","FS_syncfs","FS_mount","FS_unmount","FS_lookup","FS_mknod","FS_statfs","FS_statfsStream","FS_statfsNode","FS_create","FS_mkdir","FS_mkdev","FS_symlink","FS_rename","FS_rmdir","FS_readdir","FS_readlink","FS_stat","FS_fstat","FS_lstat","FS_doChmod","FS_chmod","FS_lchmod","FS_fchmod","FS_doChown","FS_chown","FS_lchown","FS_fchown","FS_doTruncate","FS_truncate","FS_ftruncate","FS_utime","FS_open","FS_close","FS_isClosed","FS_llseek","FS_read","FS_write","FS_mmap","FS_msync","FS_ioctl","FS_writeFile","FS_cwd","FS_chdir","FS_createDefaultDirectories","FS_createDefaultDevices","FS_createSpecialDirectories","FS_createStandardStreams","FS_staticInit","FS_init","FS_quit","FS_findObject","FS_analyzePath","FS_createFile","FS_createDataFile","FS_forceLoadFile","FS_createLazyFile","FS_absolutePath","FS_createFolder","FS_createLink","FS_joinPath","FS_mmapAlloc","FS_standardizePath","MEMFS","TTY","PIPEFS","SOCKFS","tempFixedLengthArray","miniTempWebGLFloatBuffers","miniTempWebGLIntBuffers","heapObjectForWebGLType","toTypedArrayIndex","webgl_enable_ANGLE_instanced_arrays","webgl_enable_OES_vertex_array_object","webgl_enable_WEBGL_draw_buffers","webgl_enable_WEBGL_multi_draw","webgl_enable_EXT_polygon_offset_clamp","webgl_enable_EXT_clip_control","webgl_enable_WEBGL_polygon_mode","GL","emscriptenWebGLGet","computeUnpackAlignedImageSize","colorChannelsInGlTextureFormat","emscriptenWebGLGetTexPixelData","webglGetUniformLocation","webglPrepareUniformLocationsBeforeFirstUse","webglGetLeftBracePos","AL","GLUT","EGL","GLEW","IDBStore","SDL","SDL_gfx","webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance","webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance","allocateUTF8","allocateUTF8OnStack","print","printErr","jstoi_s","GodotWebXR","GodotWebSocket","GodotRTCDataChannel","GodotRTCPeerConnection","GodotAudio","GodotAudioWorklet","GodotAudioScript","GodotDisplayVK","GodotDisplayCursor","GodotDisplayScreen","GodotDisplay","GodotEmscripten","GodotFetch","GodotWebMidi","IDHandler","GodotConfig","GodotFS","GodotOS","GodotEventListeners","GodotPWA","GodotRuntime","GodotIME","GodotInputGamepads","GodotInputDragDrop","GodotInput","GodotWebGL2","GodotJSWrapper","IDBFS"];unexportedSymbols.forEach(unexportedRuntimeSymbol);function checkIncomingModuleAPI(){ignoredModuleProp("fetchSettings")}var _free=Module["_free"]=makeInvalidEarlyAccess("_free");var __Z14godot_web_mainiPPc=Module["__Z14godot_web_mainiPPc"]=makeInvalidEarlyAccess("__Z14godot_web_mainiPPc");var _main=Module["_main"]=makeInvalidEarlyAccess("_main");var _malloc=Module["_malloc"]=makeInvalidEarlyAccess("_malloc");var _fflush=makeInvalidEarlyAccess("_fflush");var __emwebxr_on_input_event=Module["__emwebxr_on_input_event"]=makeInvalidEarlyAccess("__emwebxr_on_input_event");var __emwebxr_on_simple_event=Module["__emwebxr_on_simple_event"]=makeInvalidEarlyAccess("__emwebxr_on_simple_event");var _strerror=makeInvalidEarlyAccess("_strerror");var ___funcs_on_exit=makeInvalidEarlyAccess("___funcs_on_exit");var _emscripten_stack_get_end=makeInvalidEarlyAccess("_emscripten_stack_get_end");var _emscripten_stack_get_base=makeInvalidEarlyAccess("_emscripten_stack_get_base");var __emscripten_timeout=makeInvalidEarlyAccess("__emscripten_timeout");var _emscripten_stack_init=makeInvalidEarlyAccess("_emscripten_stack_init");var _emscripten_stack_get_free=makeInvalidEarlyAccess("_emscripten_stack_get_free");var __emscripten_stack_restore=makeInvalidEarlyAccess("__emscripten_stack_restore");var __emscripten_stack_alloc=makeInvalidEarlyAccess("__emscripten_stack_alloc");var _emscripten_stack_get_current=makeInvalidEarlyAccess("_emscripten_stack_get_current");function assignWasmExports(wasmExports){Module["_free"]=_free=createExportWrapper("free",1);Module["__Z14godot_web_mainiPPc"]=__Z14godot_web_mainiPPc=createExportWrapper("_Z14godot_web_mainiPPc",2);Module["_main"]=_main=createExportWrapper("__main_argc_argv",2);Module["_malloc"]=_malloc=createExportWrapper("malloc",1);_fflush=createExportWrapper("fflush",1);Module["__emwebxr_on_input_event"]=__emwebxr_on_input_event=createExportWrapper("_emwebxr_on_input_event",2);Module["__emwebxr_on_simple_event"]=__emwebxr_on_simple_event=createExportWrapper("_emwebxr_on_simple_event",1);_strerror=createExportWrapper("strerror",1);___funcs_on_exit=createExportWrapper("__funcs_on_exit",0);_emscripten_stack_get_end=wasmExports["emscripten_stack_get_end"];_emscripten_stack_get_base=wasmExports["emscripten_stack_get_base"];__emscripten_timeout=createExportWrapper("_emscripten_timeout",2);_emscripten_stack_init=wasmExports["emscripten_stack_init"];_emscripten_stack_get_free=wasmExports["emscripten_stack_get_free"];__emscripten_stack_restore=wasmExports["_emscripten_stack_restore"];__emscripten_stack_alloc=wasmExports["_emscripten_stack_alloc"];_emscripten_stack_get_current=wasmExports["emscripten_stack_get_current"]}var wasmImports={__call_sighandler:___call_sighandler,__syscall_chdir:___syscall_chdir,__syscall_chmod:___syscall_chmod,__syscall_faccessat:___syscall_faccessat,__syscall_fchmod:___syscall_fchmod,__syscall_fcntl64:___syscall_fcntl64,__syscall_fstat64:___syscall_fstat64,__syscall_ftruncate64:___syscall_ftruncate64,__syscall_getcwd:___syscall_getcwd,__syscall_getdents64:___syscall_getdents64,__syscall_ioctl:___syscall_ioctl,__syscall_lstat64:___syscall_lstat64,__syscall_mkdirat:___syscall_mkdirat,__syscall_mknodat:___syscall_mknodat,__syscall_newfstatat:___syscall_newfstatat,__syscall_openat:___syscall_openat,__syscall_readlinkat:___syscall_readlinkat,__syscall_renameat:___syscall_renameat,__syscall_rmdir:___syscall_rmdir,__syscall_stat64:___syscall_stat64,__syscall_statfs64:___syscall_statfs64,__syscall_symlinkat:___syscall_symlinkat,__syscall_unlinkat:___syscall_unlinkat,_abort_js:__abort_js,_emscripten_runtime_keepalive_clear:__emscripten_runtime_keepalive_clear,_gmtime_js:__gmtime_js,_localtime_js:__localtime_js,_setitimer_js:__setitimer_js,_tzset_js:__tzset_js,clock_time_get:_clock_time_get,emscripten_cancel_main_loop:_emscripten_cancel_main_loop,emscripten_date_now:_emscripten_date_now,emscripten_err:_emscripten_err,emscripten_force_exit:_emscripten_force_exit,emscripten_get_heap_max:_emscripten_get_heap_max,emscripten_get_now:_emscripten_get_now,emscripten_resize_heap:_emscripten_resize_heap,emscripten_set_canvas_element_size:_emscripten_set_canvas_element_size,emscripten_set_main_loop:_emscripten_set_main_loop,emscripten_webgl_commit_frame:_emscripten_webgl_commit_frame,emscripten_webgl_create_context:_emscripten_webgl_create_context,emscripten_webgl_destroy_context:_emscripten_webgl_destroy_context,emscripten_webgl_enable_extension:_emscripten_webgl_enable_extension,emscripten_webgl_get_supported_extensions:_emscripten_webgl_get_supported_extensions,emscripten_webgl_make_context_current:_emscripten_webgl_make_context_current,environ_get:_environ_get,environ_sizes_get:_environ_sizes_get,exit:_exit,fd_close:_fd_close,fd_fdstat_get:_fd_fdstat_get,fd_read:_fd_read,fd_seek:_fd_seek,fd_write:_fd_write,glActiveTexture:_glActiveTexture,glAttachShader:_glAttachShader,glBeginTransformFeedback:_glBeginTransformFeedback,glBindBuffer:_glBindBuffer,glBindBufferBase:_glBindBufferBase,glBindBufferRange:_glBindBufferRange,glBindFramebuffer:_glBindFramebuffer,glBindRenderbuffer:_glBindRenderbuffer,glBindTexture:_glBindTexture,glBindVertexArray:_glBindVertexArray,glBlendColor:_glBlendColor,glBlendEquation:_glBlendEquation,glBlendFunc:_glBlendFunc,glBlendFuncSeparate:_glBlendFuncSeparate,glBlitFramebuffer:_glBlitFramebuffer,glBufferData:_glBufferData,glBufferSubData:_glBufferSubData,glCheckFramebufferStatus:_glCheckFramebufferStatus,glClear:_glClear,glClearBufferfv:_glClearBufferfv,glClearColor:_glClearColor,glClearDepthf:_glClearDepthf,glClearStencil:_glClearStencil,glColorMask:_glColorMask,glCompileShader:_glCompileShader,glCompressedTexImage2D:_glCompressedTexImage2D,glCompressedTexImage3D:_glCompressedTexImage3D,glCompressedTexSubImage3D:_glCompressedTexSubImage3D,glCopyBufferSubData:_glCopyBufferSubData,glCreateProgram:_glCreateProgram,glCreateShader:_glCreateShader,glCullFace:_glCullFace,glDeleteBuffers:_glDeleteBuffers,glDeleteFramebuffers:_glDeleteFramebuffers,glDeleteProgram:_glDeleteProgram,glDeleteQueries:_glDeleteQueries,glDeleteRenderbuffers:_glDeleteRenderbuffers,glDeleteShader:_glDeleteShader,glDeleteSync:_glDeleteSync,glDeleteTextures:_glDeleteTextures,glDeleteVertexArrays:_glDeleteVertexArrays,glDepthFunc:_glDepthFunc,glDepthMask:_glDepthMask,glDisable:_glDisable,glDisableVertexAttribArray:_glDisableVertexAttribArray,glDrawArrays:_glDrawArrays,glDrawArraysInstanced:_glDrawArraysInstanced,glDrawBuffers:_glDrawBuffers,glDrawElements:_glDrawElements,glDrawElementsInstanced:_glDrawElementsInstanced,glEnable:_glEnable,glEnableVertexAttribArray:_glEnableVertexAttribArray,glEndTransformFeedback:_glEndTransformFeedback,glFenceSync:_glFenceSync,glFinish:_glFinish,glFramebufferRenderbuffer:_glFramebufferRenderbuffer,glFramebufferTexture2D:_glFramebufferTexture2D,glFramebufferTextureLayer:_glFramebufferTextureLayer,glFrontFace:_glFrontFace,glGenBuffers:_glGenBuffers,glGenFramebuffers:_glGenFramebuffers,glGenQueries:_glGenQueries,glGenRenderbuffers:_glGenRenderbuffers,glGenTextures:_glGenTextures,glGenVertexArrays:_glGenVertexArrays,glGenerateMipmap:_glGenerateMipmap,glGetFloatv:_glGetFloatv,glGetInteger64v:_glGetInteger64v,glGetIntegerv:_glGetIntegerv,glGetProgramInfoLog:_glGetProgramInfoLog,glGetProgramiv:_glGetProgramiv,glGetShaderInfoLog:_glGetShaderInfoLog,glGetShaderiv:_glGetShaderiv,glGetString:_glGetString,glGetSynciv:_glGetSynciv,glGetUniformBlockIndex:_glGetUniformBlockIndex,glGetUniformLocation:_glGetUniformLocation,glLinkProgram:_glLinkProgram,glPixelStorei:_glPixelStorei,glReadBuffer:_glReadBuffer,glReadPixels:_glReadPixels,glRenderbufferStorage:_glRenderbufferStorage,glRenderbufferStorageMultisample:_glRenderbufferStorageMultisample,glScissor:_glScissor,glShaderSource:_glShaderSource,glStencilFunc:_glStencilFunc,glStencilMask:_glStencilMask,glStencilOp:_glStencilOp,glTexImage2D:_glTexImage2D,glTexImage3D:_glTexImage3D,glTexParameterf:_glTexParameterf,glTexParameteri:_glTexParameteri,glTexStorage2D:_glTexStorage2D,glTexSubImage3D:_glTexSubImage3D,glTransformFeedbackVaryings:_glTransformFeedbackVaryings,glUniform1f:_glUniform1f,glUniform1i:_glUniform1i,glUniform1iv:_glUniform1iv,glUniform1ui:_glUniform1ui,glUniform1uiv:_glUniform1uiv,glUniform2f:_glUniform2f,glUniform2fv:_glUniform2fv,glUniform2iv:_glUniform2iv,glUniform3fv:_glUniform3fv,glUniform4f:_glUniform4f,glUniform4fv:_glUniform4fv,glUniformBlockBinding:_glUniformBlockBinding,glUniformMatrix3fv:_glUniformMatrix3fv,glUniformMatrix4fv:_glUniformMatrix4fv,glUseProgram:_glUseProgram,glVertexAttrib4f:_glVertexAttrib4f,glVertexAttribDivisor:_glVertexAttribDivisor,glVertexAttribI4ui:_glVertexAttribI4ui,glVertexAttribIPointer:_glVertexAttribIPointer,glVertexAttribPointer:_glVertexAttribPointer,glViewport:_glViewport,godot_audio_get_sample_playback_position:_godot_audio_get_sample_playback_position,godot_audio_has_script_processor:_godot_audio_has_script_processor,godot_audio_has_worklet:_godot_audio_has_worklet,godot_audio_init:_godot_audio_init,godot_audio_input_start:_godot_audio_input_start,godot_audio_input_stop:_godot_audio_input_stop,godot_audio_is_available:_godot_audio_is_available,godot_audio_resume:_godot_audio_resume,godot_audio_sample_bus_add:_godot_audio_sample_bus_add,godot_audio_sample_bus_move:_godot_audio_sample_bus_move,godot_audio_sample_bus_remove:_godot_audio_sample_bus_remove,godot_audio_sample_bus_set_count:_godot_audio_sample_bus_set_count,godot_audio_sample_bus_set_mute:_godot_audio_sample_bus_set_mute,godot_audio_sample_bus_set_send:_godot_audio_sample_bus_set_send,godot_audio_sample_bus_set_solo:_godot_audio_sample_bus_set_solo,godot_audio_sample_bus_set_volume_db:_godot_audio_sample_bus_set_volume_db,godot_audio_sample_is_active:_godot_audio_sample_is_active,godot_audio_sample_register_stream:_godot_audio_sample_register_stream,godot_audio_sample_set_finished_callback:_godot_audio_sample_set_finished_callback,godot_audio_sample_set_pause:_godot_audio_sample_set_pause,godot_audio_sample_set_volumes_linear:_godot_audio_sample_set_volumes_linear,godot_audio_sample_start:_godot_audio_sample_start,godot_audio_sample_stop:_godot_audio_sample_stop,godot_audio_sample_stream_is_registered:_godot_audio_sample_stream_is_registered,godot_audio_sample_unregister_stream:_godot_audio_sample_unregister_stream,godot_audio_sample_update_pitch_scale:_godot_audio_sample_update_pitch_scale,godot_audio_script_create:_godot_audio_script_create,godot_audio_script_start:_godot_audio_script_start,godot_audio_worklet_create:_godot_audio_worklet_create,godot_audio_worklet_start_no_threads:_godot_audio_worklet_start_no_threads,godot_js_config_canvas_id_get:_godot_js_config_canvas_id_get,godot_js_config_locale_get:_godot_js_config_locale_get,godot_js_display_alert:_godot_js_display_alert,godot_js_display_canvas_focus:_godot_js_display_canvas_focus,godot_js_display_canvas_is_focused:_godot_js_display_canvas_is_focused,godot_js_display_clipboard_get:_godot_js_display_clipboard_get,godot_js_display_clipboard_set:_godot_js_display_clipboard_set,godot_js_display_cursor_is_hidden:_godot_js_display_cursor_is_hidden,godot_js_display_cursor_is_locked:_godot_js_display_cursor_is_locked,godot_js_display_cursor_lock_set:_godot_js_display_cursor_lock_set,godot_js_display_cursor_set_custom_shape:_godot_js_display_cursor_set_custom_shape,godot_js_display_cursor_set_shape:_godot_js_display_cursor_set_shape,godot_js_display_cursor_set_visible:_godot_js_display_cursor_set_visible,godot_js_display_desired_size_set:_godot_js_display_desired_size_set,godot_js_display_fullscreen_cb:_godot_js_display_fullscreen_cb,godot_js_display_fullscreen_exit:_godot_js_display_fullscreen_exit,godot_js_display_fullscreen_request:_godot_js_display_fullscreen_request,godot_js_display_has_webgl:_godot_js_display_has_webgl,godot_js_display_is_swap_ok_cancel:_godot_js_display_is_swap_ok_cancel,godot_js_display_notification_cb:_godot_js_display_notification_cb,godot_js_display_pixel_ratio_get:_godot_js_display_pixel_ratio_get,godot_js_display_screen_dpi_get:_godot_js_display_screen_dpi_get,godot_js_display_screen_size_get:_godot_js_display_screen_size_get,godot_js_display_setup_canvas:_godot_js_display_setup_canvas,godot_js_display_size_update:_godot_js_display_size_update,godot_js_display_touchscreen_is_available:_godot_js_display_touchscreen_is_available,godot_js_display_tts_available:_godot_js_display_tts_available,godot_js_display_vk_available:_godot_js_display_vk_available,godot_js_display_vk_cb:_godot_js_display_vk_cb,godot_js_display_vk_hide:_godot_js_display_vk_hide,godot_js_display_vk_show:_godot_js_display_vk_show,godot_js_display_window_blur_cb:_godot_js_display_window_blur_cb,godot_js_display_window_icon_set:_godot_js_display_window_icon_set,godot_js_display_window_size_get:_godot_js_display_window_size_get,godot_js_display_window_title_set:_godot_js_display_window_title_set,godot_js_emscripten_get_version:_godot_js_emscripten_get_version,godot_js_eval:_godot_js_eval,godot_js_fetch_create:_godot_js_fetch_create,godot_js_fetch_free:_godot_js_fetch_free,godot_js_fetch_http_status_get:_godot_js_fetch_http_status_get,godot_js_fetch_is_chunked:_godot_js_fetch_is_chunked,godot_js_fetch_read_chunk:_godot_js_fetch_read_chunk,godot_js_fetch_read_headers:_godot_js_fetch_read_headers,godot_js_fetch_state_get:_godot_js_fetch_state_get,godot_js_input_drop_files_cb:_godot_js_input_drop_files_cb,godot_js_input_gamepad_cb:_godot_js_input_gamepad_cb,godot_js_input_gamepad_sample:_godot_js_input_gamepad_sample,godot_js_input_gamepad_sample_count:_godot_js_input_gamepad_sample_count,godot_js_input_gamepad_sample_get:_godot_js_input_gamepad_sample_get,godot_js_input_key_cb:_godot_js_input_key_cb,godot_js_input_mouse_button_cb:_godot_js_input_mouse_button_cb,godot_js_input_mouse_move_cb:_godot_js_input_mouse_move_cb,godot_js_input_mouse_wheel_cb:_godot_js_input_mouse_wheel_cb,godot_js_input_paste_cb:_godot_js_input_paste_cb,godot_js_input_touch_cb:_godot_js_input_touch_cb,godot_js_input_vibrate_handheld:_godot_js_input_vibrate_handheld,godot_js_is_ime_focused:_godot_js_is_ime_focused,godot_js_os_download_buffer:_godot_js_os_download_buffer,godot_js_os_execute:_godot_js_os_execute,godot_js_os_finish_async:_godot_js_os_finish_async,godot_js_os_fs_is_persistent:_godot_js_os_fs_is_persistent,godot_js_os_fs_sync:_godot_js_os_fs_sync,godot_js_os_has_feature:_godot_js_os_has_feature,godot_js_os_hw_concurrency_get:_godot_js_os_hw_concurrency_get,godot_js_os_request_quit_cb:_godot_js_os_request_quit_cb,godot_js_os_shell_open:_godot_js_os_shell_open,godot_js_pwa_cb:_godot_js_pwa_cb,godot_js_pwa_update:_godot_js_pwa_update,godot_js_rtc_datachannel_close:_godot_js_rtc_datachannel_close,godot_js_rtc_datachannel_connect:_godot_js_rtc_datachannel_connect,godot_js_rtc_datachannel_destroy:_godot_js_rtc_datachannel_destroy,godot_js_rtc_datachannel_get_buffered_amount:_godot_js_rtc_datachannel_get_buffered_amount,godot_js_rtc_datachannel_id_get:_godot_js_rtc_datachannel_id_get,godot_js_rtc_datachannel_is_negotiated:_godot_js_rtc_datachannel_is_negotiated,godot_js_rtc_datachannel_is_ordered:_godot_js_rtc_datachannel_is_ordered,godot_js_rtc_datachannel_label_get:_godot_js_rtc_datachannel_label_get,godot_js_rtc_datachannel_max_packet_lifetime_get:_godot_js_rtc_datachannel_max_packet_lifetime_get,godot_js_rtc_datachannel_max_retransmits_get:_godot_js_rtc_datachannel_max_retransmits_get,godot_js_rtc_datachannel_protocol_get:_godot_js_rtc_datachannel_protocol_get,godot_js_rtc_datachannel_ready_state_get:_godot_js_rtc_datachannel_ready_state_get,godot_js_rtc_datachannel_send:_godot_js_rtc_datachannel_send,godot_js_rtc_pc_close:_godot_js_rtc_pc_close,godot_js_rtc_pc_create:_godot_js_rtc_pc_create,godot_js_rtc_pc_datachannel_create:_godot_js_rtc_pc_datachannel_create,godot_js_rtc_pc_destroy:_godot_js_rtc_pc_destroy,godot_js_rtc_pc_ice_candidate_add:_godot_js_rtc_pc_ice_candidate_add,godot_js_rtc_pc_local_description_set:_godot_js_rtc_pc_local_description_set,godot_js_rtc_pc_offer_create:_godot_js_rtc_pc_offer_create,godot_js_rtc_pc_remote_description_set:_godot_js_rtc_pc_remote_description_set,godot_js_set_ime_active:_godot_js_set_ime_active,godot_js_set_ime_cb:_godot_js_set_ime_cb,godot_js_set_ime_position:_godot_js_set_ime_position,godot_js_tts_get_voices:_godot_js_tts_get_voices,godot_js_tts_is_paused:_godot_js_tts_is_paused,godot_js_tts_is_speaking:_godot_js_tts_is_speaking,godot_js_tts_pause:_godot_js_tts_pause,godot_js_tts_resume:_godot_js_tts_resume,godot_js_tts_speak:_godot_js_tts_speak,godot_js_tts_stop:_godot_js_tts_stop,godot_js_webmidi_close_midi_inputs:_godot_js_webmidi_close_midi_inputs,godot_js_webmidi_open_midi_inputs:_godot_js_webmidi_open_midi_inputs,godot_js_websocket_buffered_amount:_godot_js_websocket_buffered_amount,godot_js_websocket_close:_godot_js_websocket_close,godot_js_websocket_create:_godot_js_websocket_create,godot_js_websocket_destroy:_godot_js_websocket_destroy,godot_js_websocket_send:_godot_js_websocket_send,godot_js_wrapper_create_cb:_godot_js_wrapper_create_cb,godot_js_wrapper_create_object:_godot_js_wrapper_create_object,godot_js_wrapper_interface_get:_godot_js_wrapper_interface_get,godot_js_wrapper_object_call:_godot_js_wrapper_object_call,godot_js_wrapper_object_get:_godot_js_wrapper_object_get,godot_js_wrapper_object_getvar:_godot_js_wrapper_object_getvar,godot_js_wrapper_object_is_buffer:_godot_js_wrapper_object_is_buffer,godot_js_wrapper_object_set:_godot_js_wrapper_object_set,godot_js_wrapper_object_set_cb_ret:_godot_js_wrapper_object_set_cb_ret,godot_js_wrapper_object_setvar:_godot_js_wrapper_object_setvar,godot_js_wrapper_object_transfer_buffer:_godot_js_wrapper_object_transfer_buffer,godot_js_wrapper_object_unref:_godot_js_wrapper_object_unref,godot_webgl2_glFramebufferTextureMultisampleMultiviewOVR:_godot_webgl2_glFramebufferTextureMultisampleMultiviewOVR,godot_webgl2_glFramebufferTextureMultiviewOVR:_godot_webgl2_glFramebufferTextureMultiviewOVR,godot_webgl2_glGetBufferSubData:_godot_webgl2_glGetBufferSubData,godot_webxr_get_bounds_geometry:_godot_webxr_get_bounds_geometry,godot_webxr_get_color_texture:_godot_webxr_get_color_texture,godot_webxr_get_depth_texture:_godot_webxr_get_depth_texture,godot_webxr_get_frame_rate:_godot_webxr_get_frame_rate,godot_webxr_get_projection_for_view:_godot_webxr_get_projection_for_view,godot_webxr_get_render_target_size:_godot_webxr_get_render_target_size,godot_webxr_get_supported_frame_rates:_godot_webxr_get_supported_frame_rates,godot_webxr_get_transform_for_view:_godot_webxr_get_transform_for_view,godot_webxr_get_velocity_texture:_godot_webxr_get_velocity_texture,godot_webxr_get_view_count:_godot_webxr_get_view_count,godot_webxr_get_visibility_state:_godot_webxr_get_visibility_state,godot_webxr_initialize:_godot_webxr_initialize,godot_webxr_is_session_supported:_godot_webxr_is_session_supported,godot_webxr_is_supported:_godot_webxr_is_supported,godot_webxr_uninitialize:_godot_webxr_uninitialize,godot_webxr_update_input_source:_godot_webxr_update_input_source,godot_webxr_update_target_frame_rate:_godot_webxr_update_target_frame_rate,proc_exit:_proc_exit};var wasmExports=await createWasm();var calledRun;function callMain(args=[]){assert(runDependencies==0,'cannot call main when async dependencies remain! (listen on Module["onRuntimeInitialized"])');assert(typeof onPreRuns==="undefined"||onPreRuns.length==0,"cannot call main when preRun functions remain to be called");var entryFunction=_main;args.unshift(thisProgram);var argc=args.length;var argv=stackAlloc((argc+1)*4);var argv_ptr=argv;args.forEach(arg=>{HEAPU32[argv_ptr>>2]=stringToUTF8OnStack(arg);argv_ptr+=4});HEAPU32[argv_ptr>>2]=0;try{var ret=entryFunction(argc,argv);exitJS(ret,true);return ret}catch(e){return handleException(e)}}function stackCheckInit(){_emscripten_stack_init();writeStackCookie()}function run(args=arguments_){if(runDependencies>0){dependenciesFulfilled=run;return}stackCheckInit();preRun();if(runDependencies>0){dependenciesFulfilled=run;return}function doRun(){assert(!calledRun);calledRun=true;Module["calledRun"]=true;if(ABORT)return;initRuntime();preMain();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();consumedModuleProp("onRuntimeInitialized");var noInitialRun=Module["noInitialRun"]||true;if(!noInitialRun)callMain(args);postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}checkStackCookie()}function preInit(){if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}consumedModuleProp("preInit")}preInit();run();addOnPostRun(function(){GL.getSource=(shader,count,string,length)=>{let source="";for(let i=0;i>2];const len=length?HEAPU32[length+i*4>>2]:undefined;if(len){const endPtr=ptr+len;const slice=HEAPU8.buffer instanceof ArrayBuffer?HEAPU8.subarray(ptr,endPtr):HEAPU8.slice(ptr,endPtr);source+=UTF8Decoder.decode(slice)}else{source+=UTF8ToString(ptr,len)}}return source}});if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})}for(const prop of Object.keys(Module)){if(!(prop in moduleArg)){Object.defineProperty(moduleArg,prop,{configurable:true,get(){abort(`Access to module property ('${prop}') is no longer possible via the module constructor argument; Instead, use the result of the module constructor.`)}})}} + + + return moduleRtn; +} +); +})(); +if (typeof exports === 'object' && typeof module === 'object') { + module.exports = Godot; + // This default export looks redundant, but it allows TS to import this + // commonjs style module. + module.exports.default = Godot; +} else if (typeof define === 'function' && define['amd']) + define([], () => Godot); + +const Features = { + /** + * Check whether WebGL is available. Optionally, specify a particular version of WebGL to check for. + * + * @param {number=} [majorVersion=1] The major WebGL version to check for. + * @returns {boolean} If the given major version of WebGL is available. + * @function Engine.isWebGLAvailable + */ + isWebGLAvailable: function (majorVersion = 1) { + try { + return !!document.createElement('canvas').getContext(['webgl', 'webgl2'][majorVersion - 1]); + } catch (e) { /* Not available */ } + return false; + }, + + /** + * Check whether the Fetch API available and supports streaming responses. + * + * @returns {boolean} If the Fetch API is available and supports streaming responses. + * @function Engine.isFetchAvailable + */ + isFetchAvailable: function () { + return 'fetch' in window && 'Response' in window && 'body' in window.Response.prototype; + }, + + /** + * Check whether the engine is running in a Secure Context. + * + * @returns {boolean} If the engine is running in a Secure Context. + * @function Engine.isSecureContext + */ + isSecureContext: function () { + return window['isSecureContext'] === true; + }, + + /** + * Check whether the engine is cross origin isolated. + * This value is dependent on Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers sent by the server. + * + * @returns {boolean} If the engine is running in a Secure Context. + * @function Engine.isSecureContext + */ + isCrossOriginIsolated: function () { + return window['crossOriginIsolated'] === true; + }, + + /** + * Check whether SharedBufferArray is available. + * + * Most browsers require the page to be running in a secure context, and the + * the server to provide specific CORS headers for SharedArrayBuffer to be available. + * + * @returns {boolean} If SharedArrayBuffer is available. + * @function Engine.isSharedArrayBufferAvailable + */ + isSharedArrayBufferAvailable: function () { + return 'SharedArrayBuffer' in window; + }, + + /** + * Check whether the AudioContext supports AudioWorkletNodes. + * + * @returns {boolean} If AudioWorkletNode is available. + * @function Engine.isAudioWorkletAvailable + */ + isAudioWorkletAvailable: function () { + return 'AudioContext' in window && 'audioWorklet' in AudioContext.prototype; + }, + + /** + * Return an array of missing required features (as string). + * + * @returns {Array} A list of human-readable missing features. + * @function Engine.getMissingFeatures + * @param {{threads: (boolean|undefined)}} supportedFeatures + */ + getMissingFeatures: function (supportedFeatures = {}) { + const { + // Quotes are needed for the Closure compiler. + 'threads': supportsThreads = true, + } = supportedFeatures; + + const missing = []; + if (!Features.isWebGLAvailable(2)) { + missing.push('WebGL2 - Check web browser configuration and hardware support'); + } + if (!Features.isFetchAvailable()) { + missing.push('Fetch - Check web browser version'); + } + if (!Features.isSecureContext()) { + missing.push('Secure Context - Check web server configuration (use HTTPS)'); + } + + if (supportsThreads) { + if (!Features.isCrossOriginIsolated()) { + missing.push('Cross-Origin Isolation - Check that the web server configuration sends the correct headers.'); + } + if (!Features.isSharedArrayBufferAvailable()) { + missing.push('SharedArrayBuffer - Check that the web server configuration sends the correct headers.'); + } + } + + // Audio is normally optional since we have a dummy fallback. + return missing; + }, +}; + +const Preloader = /** @constructor */ function () { // eslint-disable-line no-unused-vars + function getTrackedResponse(response, load_status) { + function onloadprogress(reader, controller) { + return reader.read().then(function (result) { + if (load_status.done) { + return Promise.resolve(); + } + if (result.value) { + controller.enqueue(result.value); + load_status.loaded += result.value.length; + } + if (!result.done) { + return onloadprogress(reader, controller); + } + load_status.done = true; + return Promise.resolve(); + }); + } + const reader = response.body.getReader(); + return new Response(new ReadableStream({ + start: function (controller) { + onloadprogress(reader, controller).then(function () { + controller.close(); + }); + }, + }), { headers: response.headers }); + } + + function loadFetch(file, tracker, fileSize, raw) { + tracker[file] = { + total: fileSize || 0, + loaded: 0, + done: false, + }; + return fetch(file).then(function (response) { + if (!response.ok) { + return Promise.reject(new Error(`Failed loading file '${file}'`)); + } + const tr = getTrackedResponse(response, tracker[file]); + if (raw) { + return Promise.resolve(tr); + } + return tr.arrayBuffer(); + }); + } + + function retry(func, attempts = 1) { + function onerror(err) { + if (attempts <= 1) { + return Promise.reject(err); + } + return new Promise(function (resolve, reject) { + setTimeout(function () { + retry(func, attempts - 1).then(resolve).catch(reject); + }, 1000); + }); + } + return func().catch(onerror); + } + + const DOWNLOAD_ATTEMPTS_MAX = 4; + const loadingFiles = {}; + const lastProgress = { loaded: 0, total: 0 }; + let progressFunc = null; + + const animateProgress = function () { + let loaded = 0; + let total = 0; + let totalIsValid = true; + let progressIsFinal = true; + + Object.keys(loadingFiles).forEach(function (file) { + const stat = loadingFiles[file]; + if (!stat.done) { + progressIsFinal = false; + } + if (!totalIsValid || stat.total === 0) { + totalIsValid = false; + total = 0; + } else { + total += stat.total; + } + loaded += stat.loaded; + }); + if (loaded !== lastProgress.loaded || total !== lastProgress.total) { + lastProgress.loaded = loaded; + lastProgress.total = total; + if (typeof progressFunc === 'function') { + progressFunc(loaded, total); + } + } + if (!progressIsFinal) { + requestAnimationFrame(animateProgress); + } + }; + + this.animateProgress = animateProgress; + + this.setProgressFunc = function (callback) { + progressFunc = callback; + }; + + this.loadPromise = function (file, fileSize, raw = false) { + return retry(loadFetch.bind(null, file, loadingFiles, fileSize, raw), DOWNLOAD_ATTEMPTS_MAX); + }; + + this.preloadedFiles = []; + this.preload = function (pathOrBuffer, destPath, fileSize) { + let buffer = null; + if (typeof pathOrBuffer === 'string') { + const me = this; + return this.loadPromise(pathOrBuffer, fileSize).then(function (buf) { + me.preloadedFiles.push({ + path: destPath || pathOrBuffer, + buffer: buf, + }); + return Promise.resolve(); + }); + } else if (pathOrBuffer instanceof ArrayBuffer) { + buffer = new Uint8Array(pathOrBuffer); + } else if (ArrayBuffer.isView(pathOrBuffer)) { + buffer = new Uint8Array(pathOrBuffer.buffer); + } + if (buffer) { + this.preloadedFiles.push({ + path: destPath, + buffer: pathOrBuffer, + }); + return Promise.resolve(); + } + return Promise.reject(new Error('Invalid object for preloading')); + }; +}; + +/** + * An object used to configure the Engine instance based on godot export options, and to override those in custom HTML + * templates if needed. + * + * @header Engine configuration + * @summary The Engine configuration object. This is just a typedef, create it like a regular object, e.g.: + * + * ``const MyConfig = { executable: 'godot', unloadAfterInit: false }`` + * + * @typedef {Object} EngineConfig + */ +const EngineConfig = {}; // eslint-disable-line no-unused-vars + +/** + * @struct + * @constructor + * @ignore + */ +const InternalConfig = function (initConfig) { // eslint-disable-line no-unused-vars + const cfg = /** @lends {InternalConfig.prototype} */ { + /** + * Whether to unload the engine automatically after the instance is initialized. + * + * @memberof EngineConfig + * @default + * @type {boolean} + */ + unloadAfterInit: true, + /** + * The HTML DOM Canvas object to use. + * + * By default, the first canvas element in the document will be used is none is specified. + * + * @memberof EngineConfig + * @default + * @type {?HTMLCanvasElement} + */ + canvas: null, + /** + * The name of the WASM file without the extension. (Set by Godot Editor export process). + * + * @memberof EngineConfig + * @default + * @type {string} + */ + executable: '', + /** + * An alternative name for the game pck to load. The executable name is used otherwise. + * + * @memberof EngineConfig + * @default + * @type {?string} + */ + mainPack: null, + /** + * Specify a language code to select the proper localization for the game. + * + * The browser locale will be used if none is specified. See complete list of + * :ref:`supported locales `. + * + * @memberof EngineConfig + * @type {?string} + * @default + */ + locale: null, + /** + * The canvas resize policy determines how the canvas should be resized by Godot. + * + * ``0`` means Godot won't do any resizing. This is useful if you want to control the canvas size from + * javascript code in your template. + * + * ``1`` means Godot will resize the canvas on start, and when changing window size via engine functions. + * + * ``2`` means Godot will adapt the canvas size to match the whole browser window. + * + * @memberof EngineConfig + * @type {number} + * @default + */ + canvasResizePolicy: 2, + /** + * The arguments to be passed as command line arguments on startup. + * + * See :ref:`command line tutorial `. + * + * **Note**: :js:meth:`startGame ` will always add the ``--main-pack`` argument. + * + * @memberof EngineConfig + * @type {Array} + * @default + */ + args: [], + /** + * When enabled, the game canvas will automatically grab the focus when the engine starts. + * + * @memberof EngineConfig + * @type {boolean} + * @default + */ + focusCanvas: true, + /** + * When enabled, this will turn on experimental virtual keyboard support on mobile. + * + * @memberof EngineConfig + * @type {boolean} + * @default + */ + experimentalVK: false, + /** + * The progressive web app service worker to install. + * @memberof EngineConfig + * @default + * @type {string} + */ + serviceWorker: '', + /** + * @ignore + * @type {Array.} + */ + persistentPaths: ['/userfs'], + /** + * @ignore + * @type {boolean} + */ + persistentDrops: false, + /** + * @ignore + * @type {Array.} + */ + gdextensionLibs: [], + /** + * @ignore + * @type {Array.} + */ + fileSizes: [], + /** + * @ignore + * @type {number} + */ + emscriptenPoolSize: 8, + /** + * @ignore + * @type {number} + */ + godotPoolSize: 4, + /** + * A callback function for handling Godot's ``OS.execute`` calls. + * + * This is for example used in the Web Editor template to switch between project manager and editor, and for running the game. + * + * @callback EngineConfig.onExecute + * @param {string} path The path that Godot's wants executed. + * @param {Array.} args The arguments of the "command" to execute. + */ + /** + * @ignore + * @type {?function(string, Array.)} + */ + onExecute: null, + /** + * A callback function for being notified when the Godot instance quits. + * + * **Note**: This function will not be called if the engine crashes or become unresponsive. + * + * @callback EngineConfig.onExit + * @param {number} status_code The status code returned by Godot on exit. + */ + /** + * @ignore + * @type {?function(number)} + */ + onExit: null, + /** + * A callback function for displaying download progress. + * + * The function is called once per frame while downloading files, so the usage of ``requestAnimationFrame()`` + * is not necessary. + * + * If the callback function receives a total amount of bytes as 0, this means that it is impossible to calculate. + * Possible reasons include: + * + * - Files are delivered with server-side chunked compression + * - Files are delivered with server-side compression on Chromium + * - Not all file downloads have started yet (usually on servers without multi-threading) + * + * @callback EngineConfig.onProgress + * @param {number} current The current amount of downloaded bytes so far. + * @param {number} total The total amount of bytes to be downloaded. + */ + /** + * @ignore + * @type {?function(number, number)} + */ + onProgress: null, + /** + * A callback function for handling the standard output stream. This method should usually only be used in debug pages. + * + * By default, ``console.log()`` is used. + * + * @callback EngineConfig.onPrint + * @param {...*} [var_args] A variadic number of arguments to be printed. + */ + /** + * @ignore + * @type {?function(...*)} + */ + onPrint: function () { + console.log.apply(console, Array.from(arguments)); // eslint-disable-line no-console + }, + /** + * A callback function for handling the standard error stream. This method should usually only be used in debug pages. + * + * By default, ``console.error()`` is used. + * + * @callback EngineConfig.onPrintError + * @param {...*} [var_args] A variadic number of arguments to be printed as errors. + */ + /** + * @ignore + * @type {?function(...*)} + */ + onPrintError: function (var_args) { + console.error.apply(console, Array.from(arguments)); // eslint-disable-line no-console + }, + }; + + /** + * @ignore + * @struct + * @constructor + * @param {EngineConfig} opts + */ + function Config(opts) { + this.update(opts); + } + + Config.prototype = cfg; + + /** + * @ignore + * @param {EngineConfig} opts + */ + Config.prototype.update = function (opts) { + const config = opts || {}; + // NOTE: We must explicitly pass the default, accessing it via + // the key will fail due to closure compiler renames. + function parse(key, def) { + if (typeof (config[key]) === 'undefined') { + return def; + } + return config[key]; + } + // Module config + this.unloadAfterInit = parse('unloadAfterInit', this.unloadAfterInit); + this.onPrintError = parse('onPrintError', this.onPrintError); + this.onPrint = parse('onPrint', this.onPrint); + this.onProgress = parse('onProgress', this.onProgress); + + // Godot config + this.canvas = parse('canvas', this.canvas); + this.executable = parse('executable', this.executable); + this.mainPack = parse('mainPack', this.mainPack); + this.locale = parse('locale', this.locale); + this.canvasResizePolicy = parse('canvasResizePolicy', this.canvasResizePolicy); + this.persistentPaths = parse('persistentPaths', this.persistentPaths); + this.persistentDrops = parse('persistentDrops', this.persistentDrops); + this.experimentalVK = parse('experimentalVK', this.experimentalVK); + this.focusCanvas = parse('focusCanvas', this.focusCanvas); + this.serviceWorker = parse('serviceWorker', this.serviceWorker); + this.gdextensionLibs = parse('gdextensionLibs', this.gdextensionLibs); + this.fileSizes = parse('fileSizes', this.fileSizes); + this.emscriptenPoolSize = parse('emscriptenPoolSize', this.emscriptenPoolSize); + this.godotPoolSize = parse('godotPoolSize', this.godotPoolSize); + this.args = parse('args', this.args); + this.onExecute = parse('onExecute', this.onExecute); + this.onExit = parse('onExit', this.onExit); + }; + + /** + * @ignore + * @param {string} loadPath + * @param {Response} response + */ + Config.prototype.getModuleConfig = function (loadPath, response) { + let r = response; + const gdext = this.gdextensionLibs; + return { + 'print': this.onPrint, + 'printErr': this.onPrintError, + 'thisProgram': this.executable, + 'noExitRuntime': false, + 'dynamicLibraries': [`${loadPath}.side.wasm`].concat(this.gdextensionLibs), + 'emscriptenPoolSize': this.emscriptenPoolSize, + 'instantiateWasm': function (imports, onSuccess) { + function done(result) { + onSuccess(result['instance'], result['module']); + } + if (typeof (WebAssembly.instantiateStreaming) !== 'undefined') { + WebAssembly.instantiateStreaming(Promise.resolve(r), imports).then(done); + } else { + r.arrayBuffer().then(function (buffer) { + WebAssembly.instantiate(buffer, imports).then(done); + }); + } + r = null; + return {}; + }, + 'locateFile': function (path) { + if (!path.startsWith('godot.')) { + return path; + } else if (path.endsWith('.audio.worklet.js')) { + return `${loadPath}.audio.worklet.js`; + } else if (path.endsWith('.audio.position.worklet.js')) { + return `${loadPath}.audio.position.worklet.js`; + } else if (path.endsWith('.js')) { + return `${loadPath}.js`; + } else if (path in gdext) { + return path; + } else if (path.endsWith('.side.wasm')) { + return `${loadPath}.side.wasm`; + } else if (path.endsWith('.wasm')) { + return `${loadPath}.wasm`; + } + return path; + }, + }; + }; + + /** + * @ignore + * @param {function()} cleanup + */ + Config.prototype.getGodotConfig = function (cleanup) { + // Try to find a canvas + if (!(this.canvas instanceof HTMLCanvasElement)) { + const nodes = document.getElementsByTagName('canvas'); + if (nodes.length && nodes[0] instanceof HTMLCanvasElement) { + const first = nodes[0]; + this.canvas = /** @type {!HTMLCanvasElement} */ (first); + } + if (!this.canvas) { + throw new Error('No canvas found in page'); + } + } + // Canvas can grab focus on click, or key events won't work. + if (this.canvas.tabIndex < 0) { + this.canvas.tabIndex = 0; + } + + // Browser locale, or custom one if defined. + let locale = this.locale; + if (!locale) { + locale = navigator.languages ? navigator.languages[0] : navigator.language; + locale = locale.split('.')[0]; + } + locale = locale.replace('-', '_'); + const onExit = this.onExit; + + // Godot configuration. + return { + 'canvas': this.canvas, + 'canvasResizePolicy': this.canvasResizePolicy, + 'locale': locale, + 'persistentDrops': this.persistentDrops, + 'virtualKeyboard': this.experimentalVK, + 'godotPoolSize': this.godotPoolSize, + 'focusCanvas': this.focusCanvas, + 'onExecute': this.onExecute, + 'onExit': function (p_code) { + cleanup(); // We always need to call the cleanup callback to free memory. + if (typeof (onExit) === 'function') { + onExit(p_code); + } + }, + }; + }; + return new Config(initConfig); +}; + +/** + * Projects exported for the Web expose the :js:class:`Engine` class to the JavaScript environment, that allows + * fine control over the engine's start-up process. + * + * This API is built in an asynchronous manner and requires basic understanding + * of `Promises `__. + * + * @module Engine + * @header Web export JavaScript reference + */ +const Engine = (function () { + const preloader = new Preloader(); + + let loadPromise = null; + let loadPath = ''; + let initPromise = null; + + /** + * @classdesc The ``Engine`` class provides methods for loading and starting exported projects on the Web. For default export + * settings, this is already part of the exported HTML page. To understand practical use of the ``Engine`` class, + * see :ref:`Custom HTML page for Web export `. + * + * @description Create a new Engine instance with the given configuration. + * + * @global + * @constructor + * @param {EngineConfig} initConfig The initial config for this instance. + */ + function Engine(initConfig) { // eslint-disable-line no-shadow + this.config = new InternalConfig(initConfig); + this.rtenv = null; + } + + /** + * Load the engine from the specified base path. + * + * @param {string} basePath Base path of the engine to load. + * @param {number=} [size=0] The file size if known. + * @returns {Promise} A Promise that resolves once the engine is loaded. + * + * @function Engine.load + */ + Engine.load = function (basePath, size) { + if (loadPromise == null) { + loadPath = basePath; + loadPromise = preloader.loadPromise(`${loadPath}.wasm`, size, true); + requestAnimationFrame(preloader.animateProgress); + } + return loadPromise; + }; + + /** + * Unload the engine to free memory. + * + * This method will be called automatically depending on the configuration. See :js:attr:`unloadAfterInit`. + * + * @function Engine.unload + */ + Engine.unload = function () { + loadPromise = null; + }; + + /** + * Safe Engine constructor, creates a new prototype for every new instance to avoid prototype pollution. + * @ignore + * @constructor + */ + function SafeEngine(initConfig) { + const proto = /** @lends Engine.prototype */ { + /** + * Initialize the engine instance. Optionally, pass the base path to the engine to load it, + * if it hasn't been loaded yet. See :js:meth:`Engine.load`. + * + * @param {string=} basePath Base path of the engine to load. + * @return {Promise} A ``Promise`` that resolves once the engine is loaded and initialized. + */ + init: function (basePath) { + if (initPromise) { + return initPromise; + } + if (loadPromise == null) { + if (!basePath) { + initPromise = Promise.reject(new Error('A base path must be provided when calling `init` and the engine is not loaded.')); + return initPromise; + } + Engine.load(basePath, this.config.fileSizes[`${basePath}.wasm`]); + } + const me = this; + function doInit(promise) { + // Care! Promise chaining is bogus with old emscripten versions. + // This caused a regression with the Mono build (which uses an older emscripten version). + // Make sure to test that when refactoring. + return new Promise(function (resolve, reject) { + promise.then(function (response) { + const cloned = new Response(response.clone().body, { 'headers': [['content-type', 'application/wasm']] }); + Godot(me.config.getModuleConfig(loadPath, cloned)).then(function (module) { + const paths = me.config.persistentPaths; + module['initFS'](paths).then(function (err) { + me.rtenv = module; + if (me.config.unloadAfterInit) { + Engine.unload(); + } + resolve(); + }); + }); + }); + }); + } + preloader.setProgressFunc(this.config.onProgress); + initPromise = doInit(loadPromise); + return initPromise; + }, + + /** + * Load a file so it is available in the instance's file system once it runs. Must be called **before** starting the + * instance. + * + * If not provided, the ``path`` is derived from the URL of the loaded file. + * + * @param {string|ArrayBuffer} file The file to preload. + * + * If a ``string`` the file will be loaded from that path. + * + * If an ``ArrayBuffer`` or a view on one, the buffer will used as the content of the file. + * + * @param {string=} path Path by which the file will be accessible. Required, if ``file`` is not a string. + * + * @returns {Promise} A Promise that resolves once the file is loaded. + */ + preloadFile: function (file, path) { + return preloader.preload(file, path, this.config.fileSizes[file]); + }, + + /** + * Start the engine instance using the given override configuration (if any). + * :js:meth:`startGame ` can be used in typical cases instead. + * + * This will initialize the instance if it is not initialized. For manual initialization, see :js:meth:`init `. + * The engine must be loaded beforehand. + * + * Fails if a canvas cannot be found on the page, or not specified in the configuration. + * + * @param {EngineConfig} override An optional configuration override. + * @return {Promise} Promise that resolves once the engine started. + */ + start: function (override) { + this.config.update(override); + const me = this; + return me.init().then(function () { + if (!me.rtenv) { + return Promise.reject(new Error('The engine must be initialized before it can be started')); + } + + let config = {}; + try { + config = me.config.getGodotConfig(function () { + me.rtenv = null; + }); + } catch (e) { + return Promise.reject(e); + } + // Godot configuration. + me.rtenv['initConfig'](config); + + // Preload GDExtension libraries. + if (me.config.gdextensionLibs.length > 0 && !me.rtenv['loadDynamicLibrary']) { + return Promise.reject(new Error('GDExtension libraries are not supported by this engine version. ' + + 'Enable "Extensions Support" for your export preset and/or build your custom template with "dlink_enabled=yes".')); + } + return new Promise(function (resolve, reject) { + for (const file of preloader.preloadedFiles) { + me.rtenv['copyToFS'](file.path, file.buffer); + } + preloader.preloadedFiles.length = 0; // Clear memory + me.rtenv['callMain'](me.config.args); + initPromise = null; + me.installServiceWorker(); + resolve(); + }); + }); + }, + + /** + * Start the game instance using the given configuration override (if any). + * + * This will initialize the instance if it is not initialized. For manual initialization, see :js:meth:`init `. + * + * This will load the engine if it is not loaded, and preload the main pck. + * + * This method expects the initial config (or the override) to have both the :js:attr:`executable` and :js:attr:`mainPack` + * properties set (normally done by the editor during export). + * + * @param {EngineConfig} override An optional configuration override. + * @return {Promise} Promise that resolves once the game started. + */ + startGame: function (override) { + this.config.update(override); + // Add main-pack argument. + const exe = this.config.executable; + const pack = this.config.mainPack || `${exe}.pck`; + this.config.args = ['--main-pack', pack].concat(this.config.args); + // Start and init with execName as loadPath if not inited. + const me = this; + return Promise.all([ + this.init(exe), + this.preloadFile(pack, pack), + ]).then(function () { + return me.start.apply(me); + }); + }, + + /** + * Create a file at the specified ``path`` with the passed as ``buffer`` in the instance's file system. + * + * @param {string} path The location where the file will be created. + * @param {ArrayBuffer} buffer The content of the file. + */ + copyToFS: function (path, buffer) { + if (this.rtenv == null) { + throw new Error('Engine must be inited before copying files'); + } + this.rtenv['copyToFS'](path, buffer); + }, + + /** + * Request that the current instance quit. + * + * This is akin the user pressing the close button in the window manager, and will + * have no effect if the engine has crashed, or is stuck in a loop. + * + */ + requestQuit: function () { + if (this.rtenv) { + this.rtenv['request_quit'](); + } + }, + + /** + * Install the progressive-web app service worker. + * @returns {Promise} The service worker registration promise. + */ + installServiceWorker: function () { + if (this.config.serviceWorker && 'serviceWorker' in navigator) { + try { + return navigator.serviceWorker.register(this.config.serviceWorker); + } catch (e) { + return Promise.reject(e); + } + } + return Promise.resolve(); + }, + }; + + Engine.prototype = proto; + // Closure compiler exported instance methods. + Engine.prototype['init'] = Engine.prototype.init; + Engine.prototype['preloadFile'] = Engine.prototype.preloadFile; + Engine.prototype['start'] = Engine.prototype.start; + Engine.prototype['startGame'] = Engine.prototype.startGame; + Engine.prototype['copyToFS'] = Engine.prototype.copyToFS; + Engine.prototype['requestQuit'] = Engine.prototype.requestQuit; + Engine.prototype['installServiceWorker'] = Engine.prototype.installServiceWorker; + // Also expose static methods as instance methods + Engine.prototype['load'] = Engine.load; + Engine.prototype['unload'] = Engine.unload; + return new Engine(initConfig); + } + + // Closure compiler exported static methods. + SafeEngine['load'] = Engine.load; + SafeEngine['unload'] = Engine.unload; + + // Feature-detection utilities. + SafeEngine['isWebGLAvailable'] = Features.isWebGLAvailable; + SafeEngine['isFetchAvailable'] = Features.isFetchAvailable; + SafeEngine['isSecureContext'] = Features.isSecureContext; + SafeEngine['isCrossOriginIsolated'] = Features.isCrossOriginIsolated; + SafeEngine['isSharedArrayBufferAvailable'] = Features.isSharedArrayBufferAvailable; + SafeEngine['isAudioWorkletAvailable'] = Features.isAudioWorkletAvailable; + SafeEngine['getMissingFeatures'] = Features.getMissingFeatures; + + return SafeEngine; +}()); +if (typeof window !== 'undefined') { + window['Engine'] = Engine; +} diff --git a/web_assets/index.manifest.json b/web_assets/index.manifest.json new file mode 100644 index 0000000..0a8ec5d --- /dev/null +++ b/web_assets/index.manifest.json @@ -0,0 +1 @@ +{"background_color":"#316cff","display":"standalone","icons":[{"sizes":"144x144","src":"index.144x144.png","type":"image/png"},{"sizes":"180x180","src":"index.180x180.png","type":"image/png"},{"sizes":"512x512","src":"index.512x512.png","type":"image/png"}],"name":"AI Town Game","orientation":"any","start_url":"./index.html"} \ No newline at end of file diff --git a/web_assets/index.offline.html b/web_assets/index.offline.html new file mode 100644 index 0000000..ee25d9a --- /dev/null +++ b/web_assets/index.offline.html @@ -0,0 +1,44 @@ + + + + + + + + 您处于离线状态 - You are offline + + + +

您处于离线状态 / You are offline

+

此应用程序首次运行需要互联网连接。

+

This application requires an Internet connection to run for the first time.

+

请点击下方按钮尝试重新加载:

+

Press the button below to try reloading:

+ + + + diff --git a/web_assets/index.png b/web_assets/index.png new file mode 100644 index 0000000..766b0b6 Binary files /dev/null and b/web_assets/index.png differ diff --git a/web_assets/index.service.worker.js b/web_assets/index.service.worker.js new file mode 100644 index 0000000..e0764f6 --- /dev/null +++ b/web_assets/index.service.worker.js @@ -0,0 +1,166 @@ +// This service worker is required to expose an exported Godot project as a +// Progressive Web App. It provides an offline fallback page telling the user +// that they need an Internet connection to run the project if desired. +// Incrementing CACHE_VERSION will kick off the install event and force +// previously cached resources to be updated from the network. +/** @type {string} */ +const CACHE_VERSION = '1765010488|2150866696'; +/** @type {string} */ +const CACHE_PREFIX = 'AI Town Game-sw-cache-'; +const CACHE_NAME = CACHE_PREFIX + CACHE_VERSION; +/** @type {string} */ +const OFFLINE_URL = 'index.offline.html'; +/** @type {boolean} */ +const ENSURE_CROSSORIGIN_ISOLATION_HEADERS = false; +// Files that will be cached on load. +/** @type {string[]} */ +const CACHED_FILES = ["index.html","index.js","index.offline.html","index.icon.png","index.apple-touch-icon.png","index.audio.worklet.js","index.audio.position.worklet.js"]; +// Files that we might not want the user to preload, and will only be cached on first load. +/** @type {string[]} */ +const CACHEABLE_FILES = ["index.wasm","index.pck"]; +const FULL_CACHE = CACHED_FILES.concat(CACHEABLE_FILES); + +self.addEventListener('install', (event) => { + event.waitUntil(caches.open(CACHE_NAME).then((cache) => cache.addAll(CACHED_FILES))); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil(caches.keys().then( + function (keys) { + // Remove old caches. + return Promise.all(keys.filter((key) => key.startsWith(CACHE_PREFIX) && key !== CACHE_NAME).map((key) => caches.delete(key))); + } + ).then(function () { + // Enable navigation preload if available. + return ('navigationPreload' in self.registration) ? self.registration.navigationPreload.enable() : Promise.resolve(); + })); +}); + +/** + * Ensures that the response has the correct COEP/COOP headers + * @param {Response} response + * @returns {Response} + */ +function ensureCrossOriginIsolationHeaders(response) { + if (response.headers.get('Cross-Origin-Embedder-Policy') === 'require-corp' + && response.headers.get('Cross-Origin-Opener-Policy') === 'same-origin') { + return response; + } + + const crossOriginIsolatedHeaders = new Headers(response.headers); + crossOriginIsolatedHeaders.set('Cross-Origin-Embedder-Policy', 'require-corp'); + crossOriginIsolatedHeaders.set('Cross-Origin-Opener-Policy', 'same-origin'); + const newResponse = new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers: crossOriginIsolatedHeaders, + }); + + return newResponse; +} + +/** + * Calls fetch and cache the result if it is cacheable + * @param {FetchEvent} event + * @param {Cache} cache + * @param {boolean} isCacheable + * @returns {Response} + */ +async function fetchAndCache(event, cache, isCacheable) { + // Use the preloaded response, if it's there + /** @type { Response } */ + let response = await event.preloadResponse; + if (response == null) { + // Or, go over network. + response = await self.fetch(event.request); + } + + if (ENSURE_CROSSORIGIN_ISOLATION_HEADERS) { + response = ensureCrossOriginIsolationHeaders(response); + } + + if (isCacheable) { + // And update the cache + cache.put(event.request, response.clone()); + } + + return response; +} + +self.addEventListener( + 'fetch', + /** + * Triggered on fetch + * @param {FetchEvent} event + */ + (event) => { + const isNavigate = event.request.mode === 'navigate'; + const url = event.request.url || ''; + const referrer = event.request.referrer || ''; + const base = referrer.slice(0, referrer.lastIndexOf('/') + 1); + const local = url.startsWith(base) ? url.replace(base, '') : ''; + const isCacheable = FULL_CACHE.some((v) => v === local) || (base === referrer && base.endsWith(CACHED_FILES[0])); + if (isNavigate || isCacheable) { + event.respondWith((async () => { + // Try to use cache first + const cache = await caches.open(CACHE_NAME); + if (isNavigate) { + // Check if we have full cache during HTML page request. + /** @type {Response[]} */ + const fullCache = await Promise.all(FULL_CACHE.map((name) => cache.match(name))); + const missing = fullCache.some((v) => v === undefined); + if (missing) { + try { + // Try network if some cached file is missing (so we can display offline page in case). + const response = await fetchAndCache(event, cache, isCacheable); + return response; + } catch (e) { + // And return the hopefully always cached offline page in case of network failure. + console.error('Network error: ', e); // eslint-disable-line no-console + return caches.match(OFFLINE_URL); + } + } + } + let cached = await cache.match(event.request); + if (cached != null) { + if (ENSURE_CROSSORIGIN_ISOLATION_HEADERS) { + cached = ensureCrossOriginIsolationHeaders(cached); + } + return cached; + } + // Try network if don't have it in cache. + const response = await fetchAndCache(event, cache, isCacheable); + return response; + })()); + } else if (ENSURE_CROSSORIGIN_ISOLATION_HEADERS) { + event.respondWith((async () => { + let response = await fetch(event.request); + response = ensureCrossOriginIsolationHeaders(response); + return response; + })()); + } + } +); + +self.addEventListener('message', (event) => { + // No cross origin + if (event.origin !== self.origin) { + return; + } + const id = event.source.id || ''; + const msg = event.data || ''; + // Ensure it's one of our clients. + self.clients.get(id).then(function (client) { + if (!client) { + return; // Not a valid client. + } + if (msg === 'claim') { + self.skipWaiting().then(() => self.clients.claim()); + } else if (msg === 'clear') { + caches.delete(CACHE_NAME); + } else if (msg === 'update') { + self.skipWaiting().then(() => self.clients.claim()).then(() => self.clients.matchAll()).then((all) => all.forEach((c) => c.navigate(c.url))); + } + }); +}); +